tap-msg 0.7.0

Core message processing library for the Transaction Authorization Protocol
Documentation
//! Transfer message implementation for the Transaction Authorization Protocol.
//!
//! This module defines the Transfer message type and its builder, which is
//! the foundational message type for initiating a transfer in the TAP protocol.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tap_caip::AssetId;

use crate::error::{Error, Result};
use crate::message::agent::TapParticipant;
use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
use crate::message::{Agent, Party};
use crate::TapMessage;

/// Fiat equivalent value for compliance purposes (TAIP-3).
///
/// Used for Travel Rule threshold determination when the virtual asset
/// is not widely traded and its fiat value cannot be easily resolved.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TransactionValue {
    /// Decimal string representation of the fiat amount.
    pub amount: String,

    /// ISO 4217 3-letter currency code (e.g., "USD", "EUR").
    pub currency: String,
}

/// Transfer message body (TAIP-3).
#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
#[tap(
    message_type = "https://tap.rsvp/schema/1.0#Transfer",
    initiator,
    authorizable,
    transactable
)]
pub struct Transfer {
    /// Network asset identifier (CAIP-19 format).
    pub asset: AssetId,

    /// Originator information (optional).
    #[serde(rename = "originator", skip_serializing_if = "Option::is_none")]
    #[tap(participant)]
    pub originator: Option<Party>,

    /// Beneficiary information (optional).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[tap(participant)]
    pub beneficiary: Option<Party>,

    /// Transfer amount.
    pub amount: String,

    /// Agents involved in the transfer.
    #[serde(default)]
    #[tap(participant_list)]
    pub agents: Vec<Agent>,

    /// Memo for the transfer (optional).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub memo: Option<String>,

    /// Settlement identifier (optional).
    #[serde(rename = "settlementId", skip_serializing_if = "Option::is_none")]
    pub settlement_id: Option<String>,

    /// Expiration time in ISO 8601 format (optional).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expiry: Option<String>,

    /// Fiat equivalent value for compliance (optional, TAIP-3).
    #[serde(rename = "transactionValue", skip_serializing_if = "Option::is_none")]
    pub transaction_value: Option<TransactionValue>,

    /// Transaction identifier (only available after creation).
    #[serde(skip)]
    #[tap(transaction_id)]
    pub transaction_id: Option<String>,

    /// Connection ID for linking to Connect messages
    #[serde(skip_serializing_if = "Option::is_none")]
    #[tap(connection_id)]
    pub connection_id: Option<String>,

    /// Additional metadata for the transfer.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub metadata: HashMap<String, serde_json::Value>,
}

impl Transfer {
    /// Create a new Transfer
    ///
    /// # Example
    /// ```
    /// use tap_msg::message::{Transfer, Party};
    /// use tap_caip::{AssetId, ChainId};
    /// use std::collections::HashMap;
    ///
    /// // Create chain ID and asset ID
    /// let chain_id = ChainId::new("eip155", "1").unwrap();
    /// let asset = AssetId::new(chain_id, "erc20", "0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
    ///
    /// // Create originator party
    /// let originator = Party::new("did:example:alice");
    ///
    /// // Create a transfer with required fields
    /// let transfer = Transfer::builder()
    ///     .asset(asset)
    ///     .originator(originator)
    ///     .amount("100".to_string())
    ///     .build();
    /// ```
    pub fn builder() -> TransferBuilder {
        TransferBuilder::default()
    }

    /// Generates a unique message ID for authorization, rejection, or settlement
    pub fn message_id(&self) -> String {
        uuid::Uuid::new_v4().to_string()
    }
}

/// Builder for creating Transfer objects in a more idiomatic way
#[derive(Default)]
pub struct TransferBuilder {
    asset: Option<AssetId>,
    originator: Option<Party>,
    amount: Option<String>,
    beneficiary: Option<Party>,
    settlement_id: Option<String>,
    expiry: Option<String>,
    transaction_value: Option<TransactionValue>,
    memo: Option<String>,
    transaction_id: Option<String>,
    agents: Vec<Agent>,
    metadata: HashMap<String, serde_json::Value>,
}

impl TransferBuilder {
    /// Set the asset for this transfer
    pub fn asset(mut self, asset: AssetId) -> Self {
        self.asset = Some(asset);
        self
    }

    /// Set the originator for this transfer
    pub fn originator(mut self, originator: Party) -> Self {
        self.originator = Some(originator);
        self
    }

    /// Set the amount for this transfer
    pub fn amount(mut self, amount: String) -> Self {
        self.amount = Some(amount);
        self
    }

    /// Set the beneficiary for this transfer
    pub fn beneficiary(mut self, beneficiary: Party) -> Self {
        self.beneficiary = Some(beneficiary);
        self
    }

    /// Set the settlement ID for this transfer
    pub fn settlement_id(mut self, settlement_id: String) -> Self {
        self.settlement_id = Some(settlement_id);
        self
    }

    /// Set the expiry for this transfer
    pub fn expiry(mut self, expiry: String) -> Self {
        self.expiry = Some(expiry);
        self
    }

    /// Set the transaction value (fiat equivalent) for this transfer
    pub fn transaction_value(mut self, transaction_value: TransactionValue) -> Self {
        self.transaction_value = Some(transaction_value);
        self
    }

    /// Set the memo for this transfer
    pub fn memo(mut self, memo: String) -> Self {
        self.memo = Some(memo);
        self
    }

    /// Set the transaction ID for this transfer
    pub fn transaction_id(mut self, transaction_id: String) -> Self {
        self.transaction_id = Some(transaction_id);
        self
    }

    /// Add an agent to this transfer
    pub fn add_agent(mut self, agent: Agent) -> Self {
        self.agents.push(agent);
        self
    }

    /// Set all agents for this transfer
    pub fn agents(mut self, agents: Vec<Agent>) -> Self {
        self.agents = agents;
        self
    }

    /// Add a metadata field
    pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
        self.metadata.insert(key, value);
        self
    }

    /// Set all metadata for this transfer
    pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
        self.metadata = metadata;
        self
    }

    /// Build the Transfer object
    ///
    /// # Panics
    ///
    /// Panics if required fields (asset, amount) are not set
    pub fn build(self) -> Transfer {
        Transfer {
            asset: self.asset.expect("Asset is required"),
            originator: self.originator,
            amount: self.amount.expect("Amount is required"),
            beneficiary: self.beneficiary,
            settlement_id: self.settlement_id,
            expiry: self.expiry,
            transaction_value: self.transaction_value,
            memo: self.memo,
            transaction_id: self.transaction_id,
            agents: self.agents,
            connection_id: None,
            metadata: self.metadata,
        }
    }

    /// Try to build the Transfer object, returning an error if required fields are missing
    pub fn try_build(self) -> Result<Transfer> {
        let asset = self
            .asset
            .ok_or_else(|| Error::Validation("Asset is required".to_string()))?;
        let amount = self
            .amount
            .ok_or_else(|| Error::Validation("Amount is required".to_string()))?;

        let transfer = Transfer {
            transaction_id: self.transaction_id,
            asset,
            originator: self.originator,
            amount,
            beneficiary: self.beneficiary,
            settlement_id: self.settlement_id,
            expiry: self.expiry,
            transaction_value: self.transaction_value,
            memo: self.memo,
            agents: self.agents,
            connection_id: None,
            metadata: self.metadata,
        };

        // Validate the created transfer
        transfer.validate()?;

        Ok(transfer)
    }
}

impl Transfer {
    /// Custom validation for Transfer messages
    pub fn validate(&self) -> Result<()> {
        // Validate asset
        if self.asset.namespace().is_empty() || self.asset.reference().is_empty() {
            return Err(Error::Validation("Asset ID is invalid".to_string()));
        }

        // Validate originator if present
        if let Some(originator) = &self.originator {
            if originator.id().is_empty() {
                return Err(Error::Validation(
                    "Originator ID cannot be empty".to_string(),
                ));
            }
        }

        // Validate amount
        if self.amount.is_empty() {
            return Err(Error::Validation("Amount is required".to_string()));
        }

        // Validate amount is a finite positive number
        match self.amount.parse::<f64>() {
            Ok(amount) if !amount.is_finite() => {
                return Err(Error::Validation(
                    "Amount must be a finite number".to_string(),
                ));
            }
            Ok(amount) if amount <= 0.0 => {
                return Err(Error::Validation("Amount must be positive".to_string()));
            }
            Err(_) => {
                return Err(Error::Validation(
                    "Amount must be a valid number".to_string(),
                ));
            }
            _ => {}
        }

        Ok(())
    }
}