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 (optional).
29    #[serde(rename = "originator", skip_serializing_if = "Option::is_none")]
30    #[tap(participant)]
31    pub originator: Option<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 (only available after creation).
55    #[serde(skip)]
56    #[tap(transaction_id)]
57    pub transaction_id: Option<String>,
58
59    /// Connection ID for linking to Connect messages
60    #[serde(skip_serializing_if = "Option::is_none")]
61    #[tap(connection_id)]
62    pub connection_id: Option<String>,
63
64    /// Additional metadata for the transfer.
65    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
66    pub metadata: HashMap<String, serde_json::Value>,
67}
68
69impl Transfer {
70    /// Create a new Transfer
71    ///
72    /// # Example
73    /// ```
74    /// use tap_msg::message::{Transfer, Party};
75    /// use tap_caip::{AssetId, ChainId};
76    /// use std::collections::HashMap;
77    ///
78    /// // Create chain ID and asset ID
79    /// let chain_id = ChainId::new("eip155", "1").unwrap();
80    /// let asset = AssetId::new(chain_id, "erc20", "0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
81    ///
82    /// // Create originator party
83    /// let originator = Party::new("did:example:alice");
84    ///
85    /// // Create a transfer with required fields
86    /// let transfer = Transfer::builder()
87    ///     .asset(asset)
88    ///     .originator(originator)
89    ///     .amount("100".to_string())
90    ///     .build();
91    /// ```
92    pub fn builder() -> TransferBuilder {
93        TransferBuilder::default()
94    }
95
96    /// Generates a unique message ID for authorization, rejection, or settlement
97    pub fn message_id(&self) -> String {
98        uuid::Uuid::new_v4().to_string()
99    }
100}
101
102/// Builder for creating Transfer objects in a more idiomatic way
103#[derive(Default)]
104pub struct TransferBuilder {
105    asset: Option<AssetId>,
106    originator: Option<Party>,
107    amount: Option<String>,
108    beneficiary: Option<Party>,
109    settlement_id: Option<String>,
110    memo: Option<String>,
111    transaction_id: Option<String>,
112    agents: Vec<Agent>,
113    metadata: HashMap<String, serde_json::Value>,
114}
115
116impl TransferBuilder {
117    /// Set the asset for this transfer
118    pub fn asset(mut self, asset: AssetId) -> Self {
119        self.asset = Some(asset);
120        self
121    }
122
123    /// Set the originator for this transfer
124    pub fn originator(mut self, originator: Party) -> Self {
125        self.originator = Some(originator);
126        self
127    }
128
129    /// Set the amount for this transfer
130    pub fn amount(mut self, amount: String) -> Self {
131        self.amount = Some(amount);
132        self
133    }
134
135    /// Set the beneficiary for this transfer
136    pub fn beneficiary(mut self, beneficiary: Party) -> Self {
137        self.beneficiary = Some(beneficiary);
138        self
139    }
140
141    /// Set the settlement ID for this transfer
142    pub fn settlement_id(mut self, settlement_id: String) -> Self {
143        self.settlement_id = Some(settlement_id);
144        self
145    }
146
147    /// Set the memo for this transfer
148    pub fn memo(mut self, memo: String) -> Self {
149        self.memo = Some(memo);
150        self
151    }
152
153    /// Set the transaction ID for this transfer
154    pub fn transaction_id(mut self, transaction_id: String) -> Self {
155        self.transaction_id = Some(transaction_id);
156        self
157    }
158
159    /// Add an agent to this transfer
160    pub fn add_agent(mut self, agent: Agent) -> Self {
161        self.agents.push(agent);
162        self
163    }
164
165    /// Set all agents for this transfer
166    pub fn agents(mut self, agents: Vec<Agent>) -> Self {
167        self.agents = agents;
168        self
169    }
170
171    /// Add a metadata field
172    pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
173        self.metadata.insert(key, value);
174        self
175    }
176
177    /// Set all metadata for this transfer
178    pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
179        self.metadata = metadata;
180        self
181    }
182
183    /// Build the Transfer object
184    ///
185    /// # Panics
186    ///
187    /// Panics if required fields (asset, amount) are not set
188    pub fn build(self) -> Transfer {
189        Transfer {
190            asset: self.asset.expect("Asset is required"),
191            originator: self.originator,
192            amount: self.amount.expect("Amount is required"),
193            beneficiary: self.beneficiary,
194            settlement_id: self.settlement_id,
195            memo: self.memo,
196            transaction_id: self.transaction_id,
197            agents: self.agents,
198            connection_id: None,
199            metadata: self.metadata,
200        }
201    }
202
203    /// Try to build the Transfer object, returning an error if required fields are missing
204    pub fn try_build(self) -> Result<Transfer> {
205        let asset = self
206            .asset
207            .ok_or_else(|| Error::Validation("Asset is required".to_string()))?;
208        let amount = self
209            .amount
210            .ok_or_else(|| Error::Validation("Amount is required".to_string()))?;
211
212        let transfer = Transfer {
213            transaction_id: self.transaction_id,
214            asset,
215            originator: self.originator,
216            amount,
217            beneficiary: self.beneficiary,
218            settlement_id: self.settlement_id,
219            memo: self.memo,
220            agents: self.agents,
221            connection_id: None,
222            metadata: self.metadata,
223        };
224
225        // Validate the created transfer
226        transfer.validate()?;
227
228        Ok(transfer)
229    }
230}
231
232impl Transfer {
233    /// Custom validation for Transfer messages
234    pub fn validate(&self) -> Result<()> {
235        // Validate asset
236        if self.asset.namespace().is_empty() || self.asset.reference().is_empty() {
237            return Err(Error::Validation("Asset ID is invalid".to_string()));
238        }
239
240        // Validate originator if present
241        if let Some(originator) = &self.originator {
242            if originator.id().is_empty() {
243                return Err(Error::Validation(
244                    "Originator ID cannot be empty".to_string(),
245                ));
246            }
247        }
248
249        // Validate amount
250        if self.amount.is_empty() {
251            return Err(Error::Validation("Amount is required".to_string()));
252        }
253
254        // Validate amount is a positive number
255        match self.amount.parse::<f64>() {
256            Ok(amount) if amount <= 0.0 => {
257                return Err(Error::Validation("Amount must be positive".to_string()));
258            }
259            Err(_) => {
260                return Err(Error::Validation(
261                    "Amount must be a valid number".to_string(),
262                ));
263            }
264            _ => {}
265        }
266
267        Ok(())
268    }
269}