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 chrono::Utc;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use tap_caip::AssetId;
10
11use crate::didcomm::PlainMessage;
12use crate::error::{Error, Result};
13use crate::impl_tap_message;
14use crate::message::tap_message_trait::{Authorizable, Connectable, TapMessageBody};
15use crate::message::{Authorize, Participant, Policy, RemoveAgent, ReplaceAgent, UpdatePolicies};
16
17/// Transfer message body (TAIP-3).
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Transfer {
20    /// Network asset identifier (CAIP-19 format).
21    pub asset: AssetId,
22
23    /// Originator information.
24    #[serde(rename = "originator")]
25    pub originator: Participant,
26
27    /// Beneficiary information (optional).
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub beneficiary: Option<Participant>,
30
31    /// Transfer amount.
32    pub amount: String,
33
34    /// Agents involved in the transfer.
35    #[serde(default)]
36    pub agents: Vec<Participant>,
37
38    /// Memo for the transfer (optional).
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub memo: Option<String>,
41
42    /// Settlement identifier (optional).
43    #[serde(rename = "settlementId", skip_serializing_if = "Option::is_none")]
44    pub settlement_id: Option<String>,
45
46    /// Transaction identifier (not stored in the struct but accessible via the TapMessage trait).
47    #[serde(skip)]
48    pub transaction_id: String,
49
50    /// Additional metadata for the transfer.
51    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
52    pub metadata: HashMap<String, serde_json::Value>,
53}
54
55impl Transfer {
56    /// Create a new Transfer
57    ///
58    /// # Example
59    /// ```
60    /// use tap_msg::message::Transfer;
61    /// use tap_caip::{AssetId, ChainId};
62    /// use tap_msg::message::Participant;
63    /// use std::collections::HashMap;
64    ///
65    /// // Create chain ID and asset ID
66    /// let chain_id = ChainId::new("eip155", "1").unwrap();
67    /// let asset = AssetId::new(chain_id, "erc20", "0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
68    ///
69    /// // Create participant
70    /// let originator = Participant {
71    ///     id: "did:example:alice".to_string(),
72    ///     role: Some("originator".to_string()),
73    ///     policies: None,
74    ///     leiCode: None, name: None,
75    /// };
76    ///
77    /// // Create a transfer with required fields
78    /// let transfer = Transfer::builder()
79    ///     .asset(asset)
80    ///     .originator(originator)
81    ///     .amount("100".to_string())
82    ///     .build();
83    /// ```
84    pub fn builder() -> TransferBuilder {
85        TransferBuilder::default()
86    }
87
88    /// Generates a unique message ID for authorization, rejection, or settlement
89    pub fn message_id(&self) -> String {
90        uuid::Uuid::new_v4().to_string()
91    }
92
93    /// Validate the Transfer
94    pub fn validate(&self) -> Result<()> {
95        // CAIP-19 asset ID is validated by the AssetId type
96        // Validate asset
97        if self.asset.namespace().is_empty() || self.asset.reference().is_empty() {
98            return Err(Error::Validation("Asset ID is invalid".to_string()));
99        }
100
101        // Validate originator
102        if self.originator.id.is_empty() {
103            return Err(Error::Validation("Originator ID is required".to_string()));
104        }
105
106        // Validate amount
107        if self.amount.is_empty() {
108            return Err(Error::Validation("Amount is required".to_string()));
109        }
110
111        // Validate amount is a positive number
112        match self.amount.parse::<f64>() {
113            Ok(amount) if amount <= 0.0 => {
114                return Err(Error::Validation("Amount must be positive".to_string()));
115            }
116            Err(_) => {
117                return Err(Error::Validation(
118                    "Amount must be a valid number".to_string(),
119                ));
120            }
121            _ => {}
122        }
123
124        // Validate agents (if any are defined)
125        for agent in &self.agents {
126            if agent.id.is_empty() {
127                return Err(Error::Validation("Agent ID cannot be empty".to_string()));
128            }
129        }
130
131        Ok(())
132    }
133}
134
135/// Builder for creating Transfer objects in a more idiomatic way
136#[derive(Default)]
137pub struct TransferBuilder {
138    asset: Option<AssetId>,
139    originator: Option<Participant>,
140    amount: Option<String>,
141    beneficiary: Option<Participant>,
142    settlement_id: Option<String>,
143    memo: Option<String>,
144    transaction_id: Option<String>,
145    agents: Vec<Participant>,
146    metadata: HashMap<String, serde_json::Value>,
147}
148
149impl TransferBuilder {
150    /// Set the asset for this transfer
151    pub fn asset(mut self, asset: AssetId) -> Self {
152        self.asset = Some(asset);
153        self
154    }
155
156    /// Set the originator for this transfer
157    pub fn originator(mut self, originator: Participant) -> Self {
158        self.originator = Some(originator);
159        self
160    }
161
162    /// Set the amount for this transfer
163    pub fn amount(mut self, amount: String) -> Self {
164        self.amount = Some(amount);
165        self
166    }
167
168    /// Set the beneficiary for this transfer
169    pub fn beneficiary(mut self, beneficiary: Participant) -> Self {
170        self.beneficiary = Some(beneficiary);
171        self
172    }
173
174    /// Set the settlement ID for this transfer
175    pub fn settlement_id(mut self, settlement_id: String) -> Self {
176        self.settlement_id = Some(settlement_id);
177        self
178    }
179
180    /// Set the memo for this transfer
181    pub fn memo(mut self, memo: String) -> Self {
182        self.memo = Some(memo);
183        self
184    }
185
186    /// Set the transaction ID for this transfer
187    pub fn transaction_id(mut self, transaction_id: String) -> Self {
188        self.transaction_id = Some(transaction_id);
189        self
190    }
191
192    /// Add an agent to this transfer
193    pub fn add_agent(mut self, agent: Participant) -> Self {
194        self.agents.push(agent);
195        self
196    }
197
198    /// Set all agents for this transfer
199    pub fn agents(mut self, agents: Vec<Participant>) -> Self {
200        self.agents = agents;
201        self
202    }
203
204    /// Add a metadata field
205    pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
206        self.metadata.insert(key, value);
207        self
208    }
209
210    /// Set all metadata for this transfer
211    pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
212        self.metadata = metadata;
213        self
214    }
215
216    /// Build the Transfer object
217    ///
218    /// # Panics
219    ///
220    /// Panics if required fields (asset, originator, amount) are not set
221    pub fn build(self) -> Transfer {
222        Transfer {
223            asset: self.asset.expect("Asset is required"),
224            originator: self.originator.expect("Originator is required"),
225            amount: self.amount.expect("Amount is required"),
226            beneficiary: self.beneficiary,
227            settlement_id: self.settlement_id,
228            memo: self.memo,
229            transaction_id: self
230                .transaction_id
231                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
232            agents: self.agents,
233            metadata: self.metadata,
234        }
235    }
236
237    /// Try to build the Transfer object, returning an error if required fields are missing
238    pub fn try_build(self) -> Result<Transfer> {
239        let asset = self
240            .asset
241            .ok_or_else(|| Error::Validation("Asset is required".to_string()))?;
242        let originator = self
243            .originator
244            .ok_or_else(|| Error::Validation("Originator 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
251                .transaction_id
252                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
253            asset,
254            originator,
255            amount,
256            beneficiary: self.beneficiary,
257            settlement_id: self.settlement_id,
258            memo: self.memo,
259            agents: self.agents,
260            metadata: self.metadata,
261        };
262
263        // Validate the created transfer
264        transfer.validate()?;
265
266        Ok(transfer)
267    }
268}
269
270impl Connectable for Transfer {
271    fn with_connection(&mut self, connect_id: &str) -> &mut Self {
272        // Store the connect_id in metadata
273        self.metadata.insert(
274            "connect_id".to_string(),
275            serde_json::Value::String(connect_id.to_string()),
276        );
277        self
278    }
279
280    fn has_connection(&self) -> bool {
281        self.metadata.contains_key("connect_id")
282    }
283
284    fn connection_id(&self) -> Option<&str> {
285        self.metadata.get("connect_id").and_then(|v| v.as_str())
286    }
287}
288
289impl TapMessageBody for Transfer {
290    fn message_type() -> &'static str {
291        "https://tap.rsvp/schema/1.0#transfer"
292    }
293
294    fn validate(&self) -> Result<()> {
295        // Validate asset
296        if self.asset.namespace().is_empty() || self.asset.reference().is_empty() {
297            return Err(Error::Validation("Asset ID is invalid".to_string()));
298        }
299
300        // Validate originator
301        if self.originator.id.is_empty() {
302            return Err(Error::Validation("Originator ID is required".to_string()));
303        }
304
305        // Validate amount
306        if self.amount.is_empty() {
307            return Err(Error::Validation("Amount is required".to_string()));
308        }
309
310        // Validate amount is a positive number
311        match self.amount.parse::<f64>() {
312            Ok(amount) if amount <= 0.0 => {
313                return Err(Error::Validation("Amount must be positive".to_string()));
314            }
315            Err(_) => {
316                return Err(Error::Validation(
317                    "Amount must be a valid number".to_string(),
318                ));
319            }
320            _ => {}
321        }
322
323        Ok(())
324    }
325
326    fn to_didcomm(&self, from: &str) -> Result<PlainMessage> {
327        // Serialize the Transfer to a JSON value
328        let mut body_json =
329            serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
330
331        // Ensure the @type field is correctly set in the body
332        if let Some(body_obj) = body_json.as_object_mut() {
333            body_obj.insert(
334                "@type".to_string(),
335                serde_json::Value::String(Self::message_type().to_string()),
336            );
337        }
338
339        // Extract agent DIDs directly from the message
340        let mut agent_dids = Vec::new();
341
342        // Add originator DID
343        agent_dids.push(self.originator.id.clone());
344
345        // Add beneficiary DID if present
346        if let Some(beneficiary) = &self.beneficiary {
347            agent_dids.push(beneficiary.id.clone());
348        }
349
350        // Add DIDs from agents array
351        for agent in &self.agents {
352            agent_dids.push(agent.id.clone());
353        }
354
355        // Remove duplicates
356        agent_dids.sort();
357        agent_dids.dedup();
358
359        // Remove the sender from the recipients list to avoid sending to self
360        agent_dids.retain(|did| did != from);
361
362        let now = Utc::now().timestamp() as u64;
363
364        // Get the connection ID if this message is connected to a previous message
365        let pthid = self
366            .connection_id()
367            .map(|connect_id| connect_id.to_string());
368
369        // Create a new Message with required fields
370        let message = PlainMessage {
371            id: uuid::Uuid::new_v4().to_string(),
372            typ: "application/didcomm-plain+json".to_string(),
373            type_: Self::message_type().to_string(),
374            body: body_json,
375            from: from.to_string(),
376            to: agent_dids,
377            thid: None,
378            pthid,
379            created_time: Some(now),
380            expires_time: None,
381            extra_headers: std::collections::HashMap::new(),
382            from_prior: None,
383            attachments: None,
384        };
385
386        Ok(message)
387    }
388
389    fn to_didcomm_with_route<'a, I>(&self, from: &str, to: I) -> Result<PlainMessage>
390    where
391        I: IntoIterator<Item = &'a str>,
392    {
393        // First create a message with the sender, automatically extracting agent DIDs
394        let mut message = self.to_didcomm(from)?;
395
396        // Override with explicitly provided recipients if any
397        let to_vec: Vec<String> = to.into_iter().map(String::from).collect();
398        if !to_vec.is_empty() {
399            message.to = to_vec;
400        }
401
402        // Set the parent thread ID if this message is connected to a previous message
403        if let Some(connect_id) = self.connection_id() {
404            message.pthid = Some(connect_id.to_string());
405        }
406
407        Ok(message)
408    }
409}
410
411impl Authorizable for Transfer {
412    fn authorize(&self, note: Option<String>) -> Authorize {
413        Authorize {
414            transaction_id: self.transaction_id.clone(),
415            note,
416        }
417    }
418
419    fn update_policies(&self, transaction_id: String, policies: Vec<Policy>) -> UpdatePolicies {
420        UpdatePolicies {
421            transaction_id,
422            policies,
423        }
424    }
425
426    fn replace_agent(
427        &self,
428        transaction_id: String,
429        original_agent: String,
430        replacement: Participant,
431    ) -> ReplaceAgent {
432        ReplaceAgent {
433            transaction_id,
434            original: original_agent,
435            replacement,
436        }
437    }
438
439    fn remove_agent(&self, transaction_id: String, agent: String) -> RemoveAgent {
440        RemoveAgent {
441            transaction_id,
442            agent,
443        }
444    }
445}
446
447impl_tap_message!(Transfer);