tap_msg/message/
transfer.rs

1//! Transfer message implementation for the Transaction Authorization Protocol.
2//!
3//! This module defines the Transfer message type and its builder, which is
4//! the foundational message type for initiating a transfer in the TAP protocol.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use tap_caip::AssetId;
9
10use crate::error::{Error, Result};
11use crate::message::agent::TapParticipant;
12use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
13use crate::message::{Agent, Party};
14use crate::TapMessage;
15
16/// Transfer message body (TAIP-3).
17#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
18#[tap(
19    message_type = "https://tap.rsvp/schema/1.0#Transfer",
20    initiator,
21    authorizable,
22    transactable
23)]
24pub struct Transfer {
25    /// Network asset identifier (CAIP-19 format).
26    pub asset: AssetId,
27
28    /// Originator information.
29    #[serde(rename = "originator")]
30    #[tap(participant)]
31    pub originator: Party,
32
33    /// Beneficiary information (optional).
34    #[serde(skip_serializing_if = "Option::is_none")]
35    #[tap(participant)]
36    pub beneficiary: Option<Party>,
37
38    /// Transfer amount.
39    pub amount: String,
40
41    /// Agents involved in the transfer.
42    #[serde(default)]
43    #[tap(participant_list)]
44    pub agents: Vec<Agent>,
45
46    /// Memo for the transfer (optional).
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub memo: Option<String>,
49
50    /// Settlement identifier (optional).
51    #[serde(rename = "settlementId", skip_serializing_if = "Option::is_none")]
52    pub settlement_id: Option<String>,
53
54    /// Transaction identifier.
55    #[tap(transaction_id)]
56    pub transaction_id: String,
57
58    /// Connection ID for linking to Connect messages
59    #[serde(skip_serializing_if = "Option::is_none")]
60    #[tap(connection_id)]
61    pub connection_id: Option<String>,
62
63    /// Additional metadata for the transfer.
64    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
65    pub metadata: HashMap<String, serde_json::Value>,
66}
67
68impl Transfer {
69    /// Create a new Transfer
70    ///
71    /// # Example
72    /// ```
73    /// use tap_msg::message::{Transfer, Party};
74    /// use tap_caip::{AssetId, ChainId};
75    /// use std::collections::HashMap;
76    ///
77    /// // Create chain ID and asset ID
78    /// let chain_id = ChainId::new("eip155", "1").unwrap();
79    /// let asset = AssetId::new(chain_id, "erc20", "0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
80    ///
81    /// // Create originator party
82    /// let originator = Party::new("did:example:alice");
83    ///
84    /// // Create a transfer with required fields
85    /// let transfer = Transfer::builder()
86    ///     .asset(asset)
87    ///     .originator(originator)
88    ///     .amount("100".to_string())
89    ///     .build();
90    /// ```
91    pub fn builder() -> TransferBuilder {
92        TransferBuilder::default()
93    }
94
95    /// Generates a unique message ID for authorization, rejection, or settlement
96    pub fn message_id(&self) -> String {
97        uuid::Uuid::new_v4().to_string()
98    }
99}
100
101/// Builder for creating Transfer objects in a more idiomatic way
102#[derive(Default)]
103pub struct TransferBuilder {
104    asset: Option<AssetId>,
105    originator: Option<Party>,
106    amount: Option<String>,
107    beneficiary: Option<Party>,
108    settlement_id: Option<String>,
109    memo: Option<String>,
110    transaction_id: Option<String>,
111    agents: Vec<Agent>,
112    metadata: HashMap<String, serde_json::Value>,
113}
114
115impl TransferBuilder {
116    /// Set the asset for this transfer
117    pub fn asset(mut self, asset: AssetId) -> Self {
118        self.asset = Some(asset);
119        self
120    }
121
122    /// Set the originator for this transfer
123    pub fn originator(mut self, originator: Party) -> Self {
124        self.originator = Some(originator);
125        self
126    }
127
128    /// Set the amount for this transfer
129    pub fn amount(mut self, amount: String) -> Self {
130        self.amount = Some(amount);
131        self
132    }
133
134    /// Set the beneficiary for this transfer
135    pub fn beneficiary(mut self, beneficiary: Party) -> Self {
136        self.beneficiary = Some(beneficiary);
137        self
138    }
139
140    /// Set the settlement ID for this transfer
141    pub fn settlement_id(mut self, settlement_id: String) -> Self {
142        self.settlement_id = Some(settlement_id);
143        self
144    }
145
146    /// Set the memo for this transfer
147    pub fn memo(mut self, memo: String) -> Self {
148        self.memo = Some(memo);
149        self
150    }
151
152    /// Set the transaction ID for this transfer
153    pub fn transaction_id(mut self, transaction_id: String) -> Self {
154        self.transaction_id = Some(transaction_id);
155        self
156    }
157
158    /// Add an agent to this transfer
159    pub fn add_agent(mut self, agent: Agent) -> Self {
160        self.agents.push(agent);
161        self
162    }
163
164    /// Set all agents for this transfer
165    pub fn agents(mut self, agents: Vec<Agent>) -> Self {
166        self.agents = agents;
167        self
168    }
169
170    /// Add a metadata field
171    pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
172        self.metadata.insert(key, value);
173        self
174    }
175
176    /// Set all metadata for this transfer
177    pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
178        self.metadata = metadata;
179        self
180    }
181
182    /// Build the Transfer object
183    ///
184    /// # Panics
185    ///
186    /// Panics if required fields (asset, originator, amount) are not set
187    pub fn build(self) -> Transfer {
188        Transfer {
189            asset: self.asset.expect("Asset is required"),
190            originator: self.originator.expect("Originator is required"),
191            amount: self.amount.expect("Amount is required"),
192            beneficiary: self.beneficiary,
193            settlement_id: self.settlement_id,
194            memo: self.memo,
195            transaction_id: self
196                .transaction_id
197                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
198            agents: self.agents,
199            connection_id: None,
200            metadata: self.metadata,
201        }
202    }
203
204    /// Try to build the Transfer object, returning an error if required fields are missing
205    pub fn try_build(self) -> Result<Transfer> {
206        let asset = self
207            .asset
208            .ok_or_else(|| Error::Validation("Asset is required".to_string()))?;
209        let originator = self
210            .originator
211            .ok_or_else(|| Error::Validation("Originator is required".to_string()))?;
212        let amount = self
213            .amount
214            .ok_or_else(|| Error::Validation("Amount is required".to_string()))?;
215
216        let transfer = Transfer {
217            transaction_id: self
218                .transaction_id
219                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
220            asset,
221            originator,
222            amount,
223            beneficiary: self.beneficiary,
224            settlement_id: self.settlement_id,
225            memo: self.memo,
226            agents: self.agents,
227            connection_id: None,
228            metadata: self.metadata,
229        };
230
231        // Validate the created transfer
232        transfer.validate()?;
233
234        Ok(transfer)
235    }
236}
237
238impl Transfer {
239    /// Custom validation for Transfer messages
240    pub fn validate(&self) -> Result<()> {
241        // Validate asset
242        if self.asset.namespace().is_empty() || self.asset.reference().is_empty() {
243            return Err(Error::Validation("Asset ID is invalid".to_string()));
244        }
245
246        // Validate originator
247        if self.originator.id().is_empty() {
248            return Err(Error::Validation("Originator ID is required".to_string()));
249        }
250
251        // Validate amount
252        if self.amount.is_empty() {
253            return Err(Error::Validation("Amount is required".to_string()));
254        }
255
256        // Validate amount is a positive number
257        match self.amount.parse::<f64>() {
258            Ok(amount) if amount <= 0.0 => {
259                return Err(Error::Validation("Amount must be positive".to_string()));
260            }
261            Err(_) => {
262                return Err(Error::Validation(
263                    "Amount must be a valid number".to_string(),
264                ));
265            }
266            _ => {}
267        }
268
269        Ok(())
270    }
271}