Skip to main content

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/// Fiat equivalent value for compliance purposes (TAIP-3).
17///
18/// Used for Travel Rule threshold determination when the virtual asset
19/// is not widely traded and its fiat value cannot be easily resolved.
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct TransactionValue {
22    /// Decimal string representation of the fiat amount.
23    pub amount: String,
24
25    /// ISO 4217 3-letter currency code (e.g., "USD", "EUR").
26    pub currency: String,
27}
28
29/// Transfer message body (TAIP-3).
30#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
31#[tap(
32    message_type = "https://tap.rsvp/schema/1.0#Transfer",
33    initiator,
34    authorizable,
35    transactable
36)]
37pub struct Transfer {
38    /// Network asset identifier (CAIP-19 format).
39    pub asset: AssetId,
40
41    /// Originator information (optional).
42    #[serde(rename = "originator", skip_serializing_if = "Option::is_none")]
43    #[tap(participant)]
44    pub originator: Option<Party>,
45
46    /// Beneficiary information (optional).
47    #[serde(skip_serializing_if = "Option::is_none")]
48    #[tap(participant)]
49    pub beneficiary: Option<Party>,
50
51    /// Transfer amount.
52    pub amount: String,
53
54    /// Agents involved in the transfer.
55    #[serde(default)]
56    #[tap(participant_list)]
57    pub agents: Vec<Agent>,
58
59    /// Memo for the transfer (optional).
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub memo: Option<String>,
62
63    /// Settlement identifier (optional).
64    #[serde(rename = "settlementId", skip_serializing_if = "Option::is_none")]
65    pub settlement_id: Option<String>,
66
67    /// Expiration time in ISO 8601 format (optional).
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub expiry: Option<String>,
70
71    /// Fiat equivalent value for compliance (optional, TAIP-3).
72    #[serde(rename = "transactionValue", skip_serializing_if = "Option::is_none")]
73    pub transaction_value: Option<TransactionValue>,
74
75    /// Transaction identifier (only available after creation).
76    #[serde(skip)]
77    #[tap(transaction_id)]
78    pub transaction_id: Option<String>,
79
80    /// Connection ID for linking to Connect messages
81    #[serde(skip_serializing_if = "Option::is_none")]
82    #[tap(connection_id)]
83    pub connection_id: Option<String>,
84
85    /// Additional metadata for the transfer.
86    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
87    pub metadata: HashMap<String, serde_json::Value>,
88}
89
90impl Transfer {
91    /// Create a new Transfer
92    ///
93    /// # Example
94    /// ```
95    /// use tap_msg::message::{Transfer, Party};
96    /// use tap_caip::{AssetId, ChainId};
97    /// use std::collections::HashMap;
98    ///
99    /// // Create chain ID and asset ID
100    /// let chain_id = ChainId::new("eip155", "1").unwrap();
101    /// let asset = AssetId::new(chain_id, "erc20", "0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
102    ///
103    /// // Create originator party
104    /// let originator = Party::new("did:example:alice");
105    ///
106    /// // Create a transfer with required fields
107    /// let transfer = Transfer::builder()
108    ///     .asset(asset)
109    ///     .originator(originator)
110    ///     .amount("100".to_string())
111    ///     .build();
112    /// ```
113    pub fn builder() -> TransferBuilder {
114        TransferBuilder::default()
115    }
116
117    /// Generates a unique message ID for authorization, rejection, or settlement
118    pub fn message_id(&self) -> String {
119        uuid::Uuid::new_v4().to_string()
120    }
121}
122
123/// Builder for creating Transfer objects in a more idiomatic way
124#[derive(Default)]
125pub struct TransferBuilder {
126    asset: Option<AssetId>,
127    originator: Option<Party>,
128    amount: Option<String>,
129    beneficiary: Option<Party>,
130    settlement_id: Option<String>,
131    expiry: Option<String>,
132    transaction_value: Option<TransactionValue>,
133    memo: Option<String>,
134    transaction_id: Option<String>,
135    agents: Vec<Agent>,
136    metadata: HashMap<String, serde_json::Value>,
137}
138
139impl TransferBuilder {
140    /// Set the asset for this transfer
141    pub fn asset(mut self, asset: AssetId) -> Self {
142        self.asset = Some(asset);
143        self
144    }
145
146    /// Set the originator for this transfer
147    pub fn originator(mut self, originator: Party) -> Self {
148        self.originator = Some(originator);
149        self
150    }
151
152    /// Set the amount for this transfer
153    pub fn amount(mut self, amount: String) -> Self {
154        self.amount = Some(amount);
155        self
156    }
157
158    /// Set the beneficiary for this transfer
159    pub fn beneficiary(mut self, beneficiary: Party) -> Self {
160        self.beneficiary = Some(beneficiary);
161        self
162    }
163
164    /// Set the settlement ID for this transfer
165    pub fn settlement_id(mut self, settlement_id: String) -> Self {
166        self.settlement_id = Some(settlement_id);
167        self
168    }
169
170    /// Set the expiry for this transfer
171    pub fn expiry(mut self, expiry: String) -> Self {
172        self.expiry = Some(expiry);
173        self
174    }
175
176    /// Set the transaction value (fiat equivalent) for this transfer
177    pub fn transaction_value(mut self, transaction_value: TransactionValue) -> Self {
178        self.transaction_value = Some(transaction_value);
179        self
180    }
181
182    /// Set the memo for this transfer
183    pub fn memo(mut self, memo: String) -> Self {
184        self.memo = Some(memo);
185        self
186    }
187
188    /// Set the transaction ID for this transfer
189    pub fn transaction_id(mut self, transaction_id: String) -> Self {
190        self.transaction_id = Some(transaction_id);
191        self
192    }
193
194    /// Add an agent to this transfer
195    pub fn add_agent(mut self, agent: Agent) -> Self {
196        self.agents.push(agent);
197        self
198    }
199
200    /// Set all agents for this transfer
201    pub fn agents(mut self, agents: Vec<Agent>) -> Self {
202        self.agents = agents;
203        self
204    }
205
206    /// Add a metadata field
207    pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
208        self.metadata.insert(key, value);
209        self
210    }
211
212    /// Set all metadata for this transfer
213    pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
214        self.metadata = metadata;
215        self
216    }
217
218    /// Build the Transfer object
219    ///
220    /// # Panics
221    ///
222    /// Panics if required fields (asset, amount) are not set
223    pub fn build(self) -> Transfer {
224        Transfer {
225            asset: self.asset.expect("Asset is required"),
226            originator: self.originator,
227            amount: self.amount.expect("Amount is required"),
228            beneficiary: self.beneficiary,
229            settlement_id: self.settlement_id,
230            expiry: self.expiry,
231            transaction_value: self.transaction_value,
232            memo: self.memo,
233            transaction_id: self.transaction_id,
234            agents: self.agents,
235            connection_id: None,
236            metadata: self.metadata,
237        }
238    }
239
240    /// Try to build the Transfer object, returning an error if required fields are missing
241    pub fn try_build(self) -> Result<Transfer> {
242        let asset = self
243            .asset
244            .ok_or_else(|| Error::Validation("Asset is required".to_string()))?;
245        let amount = self
246            .amount
247            .ok_or_else(|| Error::Validation("Amount is required".to_string()))?;
248
249        let transfer = Transfer {
250            transaction_id: self.transaction_id,
251            asset,
252            originator: self.originator,
253            amount,
254            beneficiary: self.beneficiary,
255            settlement_id: self.settlement_id,
256            expiry: self.expiry,
257            transaction_value: self.transaction_value,
258            memo: self.memo,
259            agents: self.agents,
260            connection_id: None,
261            metadata: self.metadata,
262        };
263
264        // Validate the created transfer
265        transfer.validate()?;
266
267        Ok(transfer)
268    }
269}
270
271impl Transfer {
272    /// Custom validation for Transfer messages
273    pub fn validate(&self) -> Result<()> {
274        // Validate asset
275        if self.asset.namespace().is_empty() || self.asset.reference().is_empty() {
276            return Err(Error::Validation("Asset ID is invalid".to_string()));
277        }
278
279        // Validate originator if present
280        if let Some(originator) = &self.originator {
281            if originator.id().is_empty() {
282                return Err(Error::Validation(
283                    "Originator ID cannot be empty".to_string(),
284                ));
285            }
286        }
287
288        // Validate amount
289        if self.amount.is_empty() {
290            return Err(Error::Validation("Amount is required".to_string()));
291        }
292
293        // Validate amount is a finite positive number
294        match self.amount.parse::<f64>() {
295            Ok(amount) if !amount.is_finite() => {
296                return Err(Error::Validation(
297                    "Amount must be a finite number".to_string(),
298                ));
299            }
300            Ok(amount) if amount <= 0.0 => {
301                return Err(Error::Validation("Amount must be positive".to_string()));
302            }
303            Err(_) => {
304                return Err(Error::Validation(
305                    "Amount must be a valid number".to_string(),
306                ));
307            }
308            _ => {}
309        }
310
311        Ok(())
312    }
313}