Skip to main content

tap_cli/commands/
transaction.rs

1use crate::error::{Error, Result};
2use crate::output::{print_success, OutputFormat};
3use crate::tap_integration::TapIntegration;
4use clap::Subcommand;
5use serde::Serialize;
6use std::collections::HashMap;
7use tap_caip::AssetId;
8use tap_msg::message::payment::InvoiceReference;
9use tap_msg::message::tap_message_trait::TapMessageBody;
10use tap_msg::message::transfer::TransactionValue;
11use tap_msg::message::{
12    Agent, Capture, Connect, ConnectionConstraints, Escrow, Exchange, Party, Payment, Quote,
13    TransactionLimits, Transfer,
14};
15use tap_msg::settlement_address::SettlementAddress;
16use tracing::debug;
17
18#[derive(Subcommand, Debug)]
19pub enum TransactionCommands {
20    /// Create a new transfer transaction (TAIP-3)
21    #[command(long_about = "\
22Create a new VASP-to-VASP transfer transaction (TAIP-3).
23
24Initiates a transfer of a crypto asset between an originator and beneficiary. \
25The asset must be specified in CAIP-19 format. Optionally include agents \
26(VASPs, compliance providers) as a JSON array.
27
28Examples:
29  tap-cli transaction transfer \\
30    --asset eip155:1/erc20:0xdac17f958d2ee523a2206206994597c13d831ec7 \\
31    --amount 100.0 --originator did:key:z6Mk... --beneficiary did:key:z6Mk...
32
33  # With agents
34  tap-cli transaction transfer --asset eip155:1/slip44:60 --amount 500.0 \\
35    --originator did:key:z6Mk... --beneficiary did:key:z6Mk... \\
36    --agents '[{\"@id\":\"did:key:z6MkAgent...\",\"role\":\"SourceAgent\",\"for\":\"did:key:z6Mk...\"}]'")]
37    Transfer {
38        /// CAIP-19 asset identifier (e.g., eip155:1/erc20:0x... or eip155:1/slip44:60)
39        #[arg(long)]
40        asset: String,
41        /// Transfer amount
42        #[arg(long)]
43        amount: String,
44        /// Originator DID (the sender)
45        #[arg(long)]
46        originator: String,
47        /// Beneficiary DID (the receiver)
48        #[arg(long)]
49        beneficiary: String,
50        /// Agents as JSON array of objects with @id, role, and for fields
51        #[arg(long)]
52        agents: Option<String>,
53        /// Optional memo text
54        #[arg(long)]
55        memo: Option<String>,
56        /// Optional ISO 8601 expiry timestamp (e.g., 2026-12-31T23:59:59Z)
57        #[arg(long)]
58        expiry: Option<String>,
59        /// Optional fiat equivalent value as "amount:currency" (e.g., "1000.00:USD") for Travel Rule
60        #[arg(long)]
61        transaction_value: Option<String>,
62    },
63    /// Create a new payment request (TAIP-14)
64    #[command(long_about = "\
65Create a new payment request (TAIP-14).
66
67Initiates a payment from a customer to a merchant. Specify either --asset \
68(CAIP-19) for crypto payments or --currency (ISO 4217) for fiat-denominated payments.
69
70Examples:
71  tap-cli transaction payment --amount 99.99 --merchant did:key:z6Mk... \\
72    --asset eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
73
74  tap-cli transaction payment --amount 99.99 --merchant did:key:z6Mk... \\
75    --currency USD --memo \"Order #5678\"")]
76    Payment {
77        /// Payment amount
78        #[arg(long)]
79        amount: String,
80        /// Merchant DID (payment recipient)
81        #[arg(long)]
82        merchant: String,
83        /// CAIP-19 asset identifier (mutually exclusive with --currency)
84        #[arg(long, conflicts_with = "currency")]
85        asset: Option<String>,
86        /// ISO 4217 currency code, e.g., USD, EUR (mutually exclusive with --asset)
87        #[arg(long, conflicts_with = "asset")]
88        currency: Option<String>,
89        /// Agents as JSON array
90        #[arg(long)]
91        agents: Option<String>,
92        /// Optional memo text
93        #[arg(long)]
94        memo: Option<String>,
95        /// Optional ISO 8601 expiry timestamp (e.g., 2026-12-31T23:59:59Z)
96        #[arg(long)]
97        expiry: Option<String>,
98        /// Optional invoice URL
99        #[arg(long)]
100        invoice_url: Option<String>,
101        /// Optional fallback settlement addresses (comma-separated CAIP-10)
102        #[arg(long, value_delimiter = ',')]
103        fallback_addresses: Option<Vec<String>>,
104    },
105    /// Create a new connection request (TAIP-15)
106    #[command(long_about = "\
107Create a new connection request (TAIP-15).
108
109Establishes a relationship between agents for a party. Used to set up agent \
110relationships before initiating transfers.
111
112Examples:
113  tap-cli transaction connect --recipient did:key:z6Mk... --for did:key:z6Mk... --role SourceAgent")]
114    Connect {
115        /// Recipient DID (the agent to connect with)
116        #[arg(long)]
117        recipient: String,
118        /// Party DID this connection is for
119        #[arg(long, name = "for")]
120        for_party: String,
121        /// Role in the connection (e.g., SourceAgent, DestinationAgent)
122        #[arg(long)]
123        role: Option<String>,
124        /// Connection constraints as JSON (e.g., max_amount, daily_limit, allowed_assets)
125        #[arg(long)]
126        constraints: Option<String>,
127        /// Optional ISO 8601 expiry timestamp
128        #[arg(long)]
129        expiry: Option<String>,
130        /// Optional URL to terms of service or agreement
131        #[arg(long)]
132        agreement: Option<String>,
133    },
134    /// Create a new escrow request (TAIP-17)
135    #[command(long_about = "\
136Create a new escrow request (TAIP-17).
137
138Places funds in escrow with an escrow agent. The agents JSON array must \
139include at least one agent with the 'EscrowAgent' role.
140
141Examples:
142  tap-cli transaction escrow --amount 1000.0 \\
143    --originator did:key:z6Mk... --beneficiary did:key:z6Mk... \\
144    --expiry 2026-12-31T23:59:59Z \\
145    --asset eip155:1/erc20:0xdac17f958d2ee523a2206206994597c13d831ec7 \\
146    --agents '[{\"@id\":\"did:key:z6MkEscrow...\",\"role\":\"EscrowAgent\",\"for\":\"did:key:z6Mk...\"}]'")]
147    Escrow {
148        /// Escrow amount
149        #[arg(long)]
150        amount: String,
151        /// Originator DID
152        #[arg(long)]
153        originator: String,
154        /// Beneficiary DID
155        #[arg(long)]
156        beneficiary: String,
157        /// Expiry timestamp (ISO 8601, e.g., 2026-12-31T23:59:59Z)
158        #[arg(long)]
159        expiry: String,
160        /// Agents as JSON array (must include one EscrowAgent)
161        #[arg(long)]
162        agents: String,
163        /// CAIP-19 asset identifier (mutually exclusive with --currency)
164        #[arg(long, conflicts_with = "currency")]
165        asset: Option<String>,
166        /// ISO 4217 currency code (mutually exclusive with --asset)
167        #[arg(long, conflicts_with = "asset")]
168        currency: Option<String>,
169        /// Agreement URL
170        #[arg(long)]
171        agreement: Option<String>,
172    },
173    /// Capture escrowed funds (TAIP-17)
174    #[command(long_about = "\
175Release escrowed funds (TAIP-17).
176
177Captures funds held in escrow. Supports partial capture by specifying an amount \
178less than the escrowed total.
179
180Examples:
181  tap-cli transaction capture --escrow-id <ESCROW_TX_ID>
182  tap-cli transaction capture --escrow-id <ESCROW_TX_ID> --amount 500.0 \\
183    --settlement-address eip155:1:0x742d35Cc...")]
184    Capture {
185        /// Escrow transaction ID to capture from
186        #[arg(long)]
187        escrow_id: String,
188        /// Amount to capture (for partial capture; omit for full capture)
189        #[arg(long)]
190        amount: Option<String>,
191        /// Settlement address (CAIP-10 format)
192        #[arg(long)]
193        settlement_address: Option<String>,
194    },
195    /// Create a new exchange request (TAIP-18)
196    Exchange {
197        /// Source asset identifiers (comma-separated CAIP-19, DTI, or ISO 4217)
198        #[arg(long, value_delimiter = ',')]
199        from_assets: Vec<String>,
200        /// Target asset identifiers (comma-separated CAIP-19, DTI, or ISO 4217)
201        #[arg(long, value_delimiter = ',')]
202        to_assets: Vec<String>,
203        /// Amount of source asset to exchange
204        #[arg(long, conflicts_with = "to_amount")]
205        from_amount: Option<String>,
206        /// Amount of target asset desired
207        #[arg(long, conflicts_with = "from_amount")]
208        to_amount: Option<String>,
209        /// Requester DID
210        #[arg(long)]
211        requester: String,
212        /// Provider DID (optional, omit to broadcast)
213        #[arg(long)]
214        provider: Option<String>,
215        /// Agents as JSON array
216        #[arg(long)]
217        agents: Option<String>,
218    },
219    /// Respond with a quote to an exchange request (TAIP-18)
220    Quote {
221        /// Exchange transaction ID to quote against
222        #[arg(long)]
223        exchange_id: String,
224        /// Source asset identifier
225        #[arg(long)]
226        from_asset: String,
227        /// Target asset identifier
228        #[arg(long)]
229        to_asset: String,
230        /// Amount of source asset
231        #[arg(long)]
232        from_amount: String,
233        /// Amount of target asset
234        #[arg(long)]
235        to_amount: String,
236        /// Provider DID
237        #[arg(long)]
238        provider: String,
239        /// Agents as JSON array
240        #[arg(long)]
241        agents: Option<String>,
242        /// ISO 8601 expiry timestamp
243        #[arg(long)]
244        expires: String,
245    },
246    /// List transactions
247    #[command(long_about = "\
248List transactions stored in the agent's database.
249
250Returns transactions with their type, direction, and status. Supports filtering \
251by message type, thread ID, sender, or recipient.
252
253Examples:
254  tap-cli transaction list
255  tap-cli transaction list --type Transfer --limit 20
256  tap-cli transaction list --thread-id <THREAD_ID>")]
257    List {
258        /// Agent DID to list transactions for (defaults to --agent-did global flag)
259        #[arg(long)]
260        agent_did: Option<String>,
261        /// Filter by message type (e.g., Transfer, Payment, Authorize, Reject)
262        #[arg(long, name = "type")]
263        msg_type: Option<String>,
264        /// Filter by thread ID
265        #[arg(long)]
266        thread_id: Option<String>,
267        /// Filter by sender DID
268        #[arg(long)]
269        from: Option<String>,
270        /// Filter by recipient DID
271        #[arg(long)]
272        to: Option<String>,
273        /// Maximum results
274        #[arg(long, default_value = "50")]
275        limit: u32,
276        /// Offset for pagination
277        #[arg(long, default_value = "0")]
278        offset: u32,
279    },
280}
281
282#[derive(Debug, Serialize)]
283struct TransactionResponse {
284    transaction_id: String,
285    message_id: String,
286    status: String,
287    created_at: String,
288}
289
290#[derive(Debug, serde::Deserialize)]
291struct AgentInput {
292    #[serde(rename = "@id")]
293    id: String,
294    role: String,
295    #[serde(rename = "for")]
296    for_party: String,
297}
298
299#[derive(Debug, serde::Deserialize)]
300struct ConstraintsInput {
301    #[serde(default)]
302    max_amount: Option<String>,
303    #[serde(default)]
304    daily_limit: Option<String>,
305    #[serde(default)]
306    allowed_beneficiaries: Option<Vec<String>>,
307    #[serde(default)]
308    allowed_settlement_addresses: Option<Vec<String>>,
309    #[serde(default)]
310    allowed_assets: Option<Vec<String>>,
311}
312
313pub async fn handle(
314    cmd: &TransactionCommands,
315    format: OutputFormat,
316    agent_did: &str,
317    tap_integration: &TapIntegration,
318) -> Result<()> {
319    match cmd {
320        TransactionCommands::Transfer {
321            asset,
322            amount,
323            originator,
324            beneficiary,
325            agents,
326            memo,
327            expiry,
328            transaction_value,
329        } => {
330            handle_transfer(
331                agent_did,
332                asset,
333                amount,
334                originator,
335                beneficiary,
336                agents.as_deref(),
337                memo.clone(),
338                expiry.clone(),
339                transaction_value.clone(),
340                format,
341                tap_integration,
342            )
343            .await
344        }
345        TransactionCommands::Payment {
346            amount,
347            merchant,
348            asset,
349            currency,
350            agents,
351            memo,
352            expiry,
353            invoice_url,
354            fallback_addresses,
355        } => {
356            handle_payment(
357                agent_did,
358                amount,
359                merchant,
360                asset.as_deref(),
361                currency.as_deref(),
362                agents.as_deref(),
363                memo.clone(),
364                expiry.clone(),
365                invoice_url.clone(),
366                fallback_addresses.clone(),
367                format,
368                tap_integration,
369            )
370            .await
371        }
372        TransactionCommands::Connect {
373            recipient,
374            for_party,
375            role,
376            constraints,
377            expiry,
378            agreement,
379        } => {
380            handle_connect(
381                agent_did,
382                recipient,
383                for_party,
384                role.as_deref(),
385                constraints.as_deref(),
386                expiry.clone(),
387                agreement.clone(),
388                format,
389                tap_integration,
390            )
391            .await
392        }
393        TransactionCommands::Escrow {
394            amount,
395            originator,
396            beneficiary,
397            expiry,
398            agents,
399            asset,
400            currency,
401            agreement,
402        } => {
403            handle_escrow(
404                agent_did,
405                amount,
406                originator,
407                beneficiary,
408                expiry,
409                agents,
410                asset.as_deref(),
411                currency.as_deref(),
412                agreement.as_deref(),
413                format,
414                tap_integration,
415            )
416            .await
417        }
418        TransactionCommands::Capture {
419            escrow_id,
420            amount,
421            settlement_address,
422        } => {
423            handle_capture(
424                agent_did,
425                escrow_id,
426                amount.as_deref(),
427                settlement_address.as_deref(),
428                format,
429                tap_integration,
430            )
431            .await
432        }
433        TransactionCommands::Exchange {
434            from_assets,
435            to_assets,
436            from_amount,
437            to_amount,
438            requester,
439            provider,
440            agents,
441        } => {
442            handle_exchange(
443                agent_did,
444                from_assets,
445                to_assets,
446                from_amount.as_deref(),
447                to_amount.as_deref(),
448                requester,
449                provider.as_deref(),
450                agents.as_deref(),
451                format,
452                tap_integration,
453            )
454            .await
455        }
456        TransactionCommands::Quote {
457            exchange_id,
458            from_asset,
459            to_asset,
460            from_amount,
461            to_amount,
462            provider,
463            agents,
464            expires,
465        } => {
466            handle_quote(
467                agent_did,
468                exchange_id,
469                from_asset,
470                to_asset,
471                from_amount,
472                to_amount,
473                provider,
474                agents.as_deref(),
475                expires,
476                format,
477                tap_integration,
478            )
479            .await
480        }
481        TransactionCommands::List {
482            agent_did: list_agent_did,
483            msg_type,
484            thread_id,
485            from,
486            to,
487            limit,
488            offset,
489        } => {
490            let effective_did = list_agent_did.as_deref().unwrap_or(agent_did);
491            handle_list(
492                effective_did,
493                msg_type.as_deref(),
494                thread_id.as_deref(),
495                from.as_deref(),
496                to.as_deref(),
497                *limit,
498                *offset,
499                format,
500                tap_integration,
501            )
502            .await
503        }
504    }
505}
506
507#[allow(clippy::too_many_arguments)]
508async fn handle_transfer(
509    agent_did: &str,
510    asset: &str,
511    amount: &str,
512    originator_did: &str,
513    beneficiary_did: &str,
514    agents_json: Option<&str>,
515    memo: Option<String>,
516    expiry: Option<String>,
517    transaction_value_str: Option<String>,
518    format: OutputFormat,
519    tap_integration: &TapIntegration,
520) -> Result<()> {
521    let asset_id = asset
522        .parse::<AssetId>()
523        .map_err(|e| Error::invalid_parameter(format!("Invalid asset ID: {}", e)))?;
524
525    let originator = Party::new(originator_did);
526    let beneficiary = Party::new(beneficiary_did);
527    let agents = parse_agents(agents_json)?;
528
529    let transaction_value = if let Some(tv) = transaction_value_str {
530        let parts: Vec<&str> = tv.splitn(2, ':').collect();
531        if parts.len() != 2 {
532            return Err(Error::invalid_parameter(
533                "transaction-value must be in 'amount:currency' format (e.g., '1000.00:USD')",
534            ));
535        }
536        Some(TransactionValue {
537            amount: parts[0].to_string(),
538            currency: parts[1].to_string(),
539        })
540    } else {
541        None
542    };
543
544    let transfer = Transfer {
545        transaction_id: None,
546        asset: asset_id,
547        originator: Some(originator),
548        beneficiary: Some(beneficiary),
549        amount: amount.to_string(),
550        agents,
551        memo,
552        settlement_id: None,
553        expiry,
554        transaction_value,
555        connection_id: None,
556        metadata: HashMap::new(),
557    };
558
559    transfer
560        .validate()
561        .map_err(|e| Error::invalid_parameter(format!("Transfer validation failed: {}", e)))?;
562
563    let didcomm_message = transfer
564        .to_didcomm(agent_did)
565        .map_err(|e| Error::command_failed(format!("Failed to create DIDComm message: {}", e)))?;
566
567    debug!("Sending transfer from {}", agent_did);
568    tap_integration
569        .node()
570        .send_message(agent_did.to_string(), didcomm_message.clone())
571        .await
572        .map_err(|e| Error::command_failed(format!("Failed to send transfer: {}", e)))?;
573
574    let response = TransactionResponse {
575        transaction_id: didcomm_message
576            .thid
577            .clone()
578            .unwrap_or(didcomm_message.id.clone()),
579        message_id: didcomm_message.id,
580        status: "sent".to_string(),
581        created_at: chrono::Utc::now().to_rfc3339(),
582    };
583    print_success(format, &response);
584    Ok(())
585}
586
587#[allow(clippy::too_many_arguments)]
588async fn handle_payment(
589    agent_did: &str,
590    amount: &str,
591    merchant_did: &str,
592    asset: Option<&str>,
593    currency: Option<&str>,
594    agents_json: Option<&str>,
595    memo: Option<String>,
596    expiry: Option<String>,
597    invoice_url: Option<String>,
598    fallback_addresses: Option<Vec<String>>,
599    format: OutputFormat,
600    tap_integration: &TapIntegration,
601) -> Result<()> {
602    let merchant = Party::new(merchant_did);
603    let agents = parse_agents(agents_json)?;
604
605    let mut payment = if let Some(asset) = asset {
606        let asset_id = asset
607            .parse::<AssetId>()
608            .map_err(|e| Error::invalid_parameter(format!("Invalid asset ID: {}", e)))?;
609        Payment::with_asset(asset_id, amount.to_string(), merchant, agents)
610    } else if let Some(currency) = currency {
611        Payment::with_currency(currency.to_string(), amount.to_string(), merchant, agents)
612    } else {
613        return Err(Error::invalid_parameter(
614            "Either --asset or --currency must be specified",
615        ));
616    };
617
618    if let Some(memo) = memo {
619        payment.memo = Some(memo);
620    }
621    if let Some(expiry) = expiry {
622        payment.expiry = Some(expiry);
623    }
624    if let Some(url) = invoice_url {
625        payment.invoice = Some(InvoiceReference::Url(url));
626    }
627    if let Some(addresses) = fallback_addresses {
628        let parsed: Vec<SettlementAddress> = addresses
629            .into_iter()
630            .filter_map(|a| SettlementAddress::from_string(a).ok())
631            .collect();
632        if !parsed.is_empty() {
633            payment.fallback_settlement_addresses = Some(parsed);
634        }
635    }
636
637    payment
638        .validate()
639        .map_err(|e| Error::invalid_parameter(format!("Payment validation failed: {}", e)))?;
640
641    let didcomm_message = payment
642        .to_didcomm(agent_did)
643        .map_err(|e| Error::command_failed(format!("Failed to create DIDComm message: {}", e)))?;
644
645    tap_integration
646        .node()
647        .send_message(agent_did.to_string(), didcomm_message.clone())
648        .await
649        .map_err(|e| Error::command_failed(format!("Failed to send payment: {}", e)))?;
650
651    let response = TransactionResponse {
652        transaction_id: didcomm_message.id.clone(),
653        message_id: didcomm_message.id,
654        status: "sent".to_string(),
655        created_at: chrono::Utc::now().to_rfc3339(),
656    };
657    print_success(format, &response);
658    Ok(())
659}
660
661#[allow(clippy::too_many_arguments)]
662async fn handle_connect(
663    agent_did: &str,
664    recipient: &str,
665    for_party: &str,
666    role: Option<&str>,
667    constraints_json: Option<&str>,
668    expiry: Option<String>,
669    agreement: Option<String>,
670    format: OutputFormat,
671    tap_integration: &TapIntegration,
672) -> Result<()> {
673    let transaction_id = format!("connect-{}", uuid::Uuid::new_v4());
674    let mut connect = Connect::new(&transaction_id, agent_did, for_party, role);
675
676    if let Some(json) = constraints_json {
677        let input: ConstraintsInput = serde_json::from_str(json)
678            .map_err(|e| Error::invalid_parameter(format!("Invalid constraints JSON: {}", e)))?;
679
680        let mut constraints = ConnectionConstraints {
681            purposes: None,
682            category_purposes: None,
683            limits: None,
684            allowed_beneficiaries: None,
685            allowed_settlement_addresses: None,
686            allowed_assets: None,
687        };
688
689        let mut limits = TransactionLimits {
690            per_transaction: None,
691            per_day: None,
692            per_week: None,
693            per_month: None,
694            per_year: None,
695            currency: None,
696        };
697        limits.per_transaction = input.max_amount;
698        limits.per_day = input.daily_limit;
699        constraints.limits = Some(limits);
700
701        if let Some(beneficiaries) = input.allowed_beneficiaries {
702            constraints.allowed_beneficiaries =
703                Some(beneficiaries.into_iter().map(|b| Party::new(&b)).collect());
704        }
705        constraints.allowed_settlement_addresses = input.allowed_settlement_addresses;
706        constraints.allowed_assets = input.allowed_assets;
707
708        connect.constraints = Some(constraints);
709    }
710
711    if let Some(expiry) = expiry {
712        connect.expiry = Some(expiry);
713    }
714    if let Some(agreement) = agreement {
715        connect.agreement = Some(agreement);
716    }
717
718    connect
719        .validate()
720        .map_err(|e| Error::invalid_parameter(format!("Connect validation failed: {}", e)))?;
721
722    let mut didcomm_message = connect
723        .to_didcomm(agent_did)
724        .map_err(|e| Error::command_failed(format!("Failed to create DIDComm message: {}", e)))?;
725
726    didcomm_message.to = vec![recipient.to_string()];
727
728    tap_integration
729        .node()
730        .send_message(agent_did.to_string(), didcomm_message.clone())
731        .await
732        .map_err(|e| Error::command_failed(format!("Failed to send connect: {}", e)))?;
733
734    let response = TransactionResponse {
735        transaction_id: didcomm_message.id.clone(),
736        message_id: didcomm_message.id,
737        status: "sent".to_string(),
738        created_at: chrono::Utc::now().to_rfc3339(),
739    };
740    print_success(format, &response);
741    Ok(())
742}
743
744#[allow(clippy::too_many_arguments)]
745async fn handle_escrow(
746    agent_did: &str,
747    amount: &str,
748    originator_did: &str,
749    beneficiary_did: &str,
750    expiry: &str,
751    agents_json: &str,
752    asset: Option<&str>,
753    currency: Option<&str>,
754    agreement: Option<&str>,
755    format: OutputFormat,
756    tap_integration: &TapIntegration,
757) -> Result<()> {
758    let originator = Party::new(originator_did);
759    let beneficiary = Party::new(beneficiary_did);
760    let agents = parse_agents(Some(agents_json))?;
761
762    let escrow_agent_count = agents
763        .iter()
764        .filter(|a| a.role == Some("EscrowAgent".to_string()))
765        .count();
766    if escrow_agent_count != 1 {
767        return Err(Error::invalid_parameter(format!(
768            "Escrow must have exactly one EscrowAgent, found {}",
769            escrow_agent_count
770        )));
771    }
772
773    let mut escrow = if let Some(asset) = asset {
774        Escrow::new_with_asset(
775            asset.to_string(),
776            amount.to_string(),
777            originator,
778            beneficiary,
779            expiry.to_string(),
780            agents,
781        )
782    } else if let Some(currency) = currency {
783        Escrow::new_with_currency(
784            currency.to_string(),
785            amount.to_string(),
786            originator,
787            beneficiary,
788            expiry.to_string(),
789            agents,
790        )
791    } else {
792        return Err(Error::invalid_parameter(
793            "Either --asset or --currency must be specified",
794        ));
795    };
796
797    if let Some(agreement) = agreement {
798        escrow = escrow.with_agreement(agreement.to_string());
799    }
800
801    escrow
802        .validate()
803        .map_err(|e| Error::invalid_parameter(format!("Escrow validation failed: {}", e)))?;
804
805    let didcomm_message = escrow
806        .to_didcomm(agent_did)
807        .map_err(|e| Error::command_failed(format!("Failed to create DIDComm message: {}", e)))?;
808
809    tap_integration
810        .node()
811        .send_message(agent_did.to_string(), didcomm_message.clone())
812        .await
813        .map_err(|e| Error::command_failed(format!("Failed to send escrow: {}", e)))?;
814
815    let response = TransactionResponse {
816        transaction_id: didcomm_message.id.clone(),
817        message_id: didcomm_message.id,
818        status: "sent".to_string(),
819        created_at: chrono::Utc::now().to_rfc3339(),
820    };
821    print_success(format, &response);
822    Ok(())
823}
824
825async fn handle_capture(
826    agent_did: &str,
827    escrow_id: &str,
828    amount: Option<&str>,
829    settlement_address: Option<&str>,
830    format: OutputFormat,
831    tap_integration: &TapIntegration,
832) -> Result<()> {
833    let mut capture = if let Some(amount) = amount {
834        Capture::with_amount(amount.to_string())
835    } else {
836        Capture::new()
837    };
838
839    if let Some(address) = settlement_address {
840        capture = capture.with_settlement_address(address.to_string());
841    }
842
843    capture
844        .validate()
845        .map_err(|e| Error::invalid_parameter(format!("Capture validation failed: {}", e)))?;
846
847    let mut didcomm_message = capture
848        .to_didcomm(agent_did)
849        .map_err(|e| Error::command_failed(format!("Failed to create DIDComm message: {}", e)))?;
850
851    didcomm_message.thid = Some(escrow_id.to_string());
852
853    tap_integration
854        .node()
855        .send_message(agent_did.to_string(), didcomm_message.clone())
856        .await
857        .map_err(|e| Error::command_failed(format!("Failed to send capture: {}", e)))?;
858
859    let response = TransactionResponse {
860        transaction_id: escrow_id.to_string(),
861        message_id: didcomm_message.id,
862        status: "sent".to_string(),
863        created_at: chrono::Utc::now().to_rfc3339(),
864    };
865    print_success(format, &response);
866    Ok(())
867}
868
869#[allow(clippy::too_many_arguments)]
870async fn handle_exchange(
871    agent_did: &str,
872    from_assets: &[String],
873    to_assets: &[String],
874    from_amount: Option<&str>,
875    to_amount: Option<&str>,
876    requester_did: &str,
877    provider_did: Option<&str>,
878    agents_json: Option<&str>,
879    format: OutputFormat,
880    tap_integration: &TapIntegration,
881) -> Result<()> {
882    if from_amount.is_none() && to_amount.is_none() {
883        return Err(Error::invalid_parameter(
884            "Either --from-amount or --to-amount must be specified",
885        ));
886    }
887
888    let requester = Party::new(requester_did);
889    let agents = parse_agents(agents_json)?;
890
891    let mut exchange = if let Some(amount) = from_amount {
892        Exchange::new_from(
893            from_assets.to_vec(),
894            to_assets.to_vec(),
895            amount.to_string(),
896            requester,
897            agents,
898        )
899    } else {
900        Exchange::new_to(
901            from_assets.to_vec(),
902            to_assets.to_vec(),
903            to_amount.unwrap().to_string(),
904            requester,
905            agents,
906        )
907    };
908
909    if let Some(provider) = provider_did {
910        exchange = exchange.with_provider(Party::new(provider));
911    }
912
913    exchange
914        .validate()
915        .map_err(|e| Error::invalid_parameter(format!("Exchange validation failed: {}", e)))?;
916
917    let didcomm_message = exchange
918        .to_didcomm(agent_did)
919        .map_err(|e| Error::command_failed(format!("Failed to create DIDComm message: {}", e)))?;
920
921    tap_integration
922        .node()
923        .send_message(agent_did.to_string(), didcomm_message.clone())
924        .await
925        .map_err(|e| Error::command_failed(format!("Failed to send exchange: {}", e)))?;
926
927    let response = TransactionResponse {
928        transaction_id: didcomm_message.id.clone(),
929        message_id: didcomm_message.id,
930        status: "sent".to_string(),
931        created_at: chrono::Utc::now().to_rfc3339(),
932    };
933    print_success(format, &response);
934    Ok(())
935}
936
937#[allow(clippy::too_many_arguments)]
938async fn handle_quote(
939    agent_did: &str,
940    exchange_id: &str,
941    from_asset: &str,
942    to_asset: &str,
943    from_amount: &str,
944    to_amount: &str,
945    provider_did: &str,
946    agents_json: Option<&str>,
947    expires: &str,
948    format: OutputFormat,
949    tap_integration: &TapIntegration,
950) -> Result<()> {
951    let provider = Party::new(provider_did);
952    let agents = parse_agents(agents_json)?;
953
954    let quote = Quote::new(
955        from_asset.to_string(),
956        to_asset.to_string(),
957        from_amount.to_string(),
958        to_amount.to_string(),
959        provider,
960        agents,
961        expires.to_string(),
962    );
963
964    quote
965        .validate()
966        .map_err(|e| Error::invalid_parameter(format!("Quote validation failed: {}", e)))?;
967
968    let mut didcomm_message = quote
969        .to_didcomm(agent_did)
970        .map_err(|e| Error::command_failed(format!("Failed to create DIDComm message: {}", e)))?;
971
972    didcomm_message.thid = Some(exchange_id.to_string());
973
974    tap_integration
975        .node()
976        .send_message(agent_did.to_string(), didcomm_message.clone())
977        .await
978        .map_err(|e| Error::command_failed(format!("Failed to send quote: {}", e)))?;
979
980    let response = TransactionResponse {
981        transaction_id: exchange_id.to_string(),
982        message_id: didcomm_message.id,
983        status: "sent".to_string(),
984        created_at: chrono::Utc::now().to_rfc3339(),
985    };
986    print_success(format, &response);
987    Ok(())
988}
989
990#[derive(Debug, Serialize)]
991struct TransactionListResponse {
992    transactions: Vec<TransactionInfo>,
993    total: usize,
994}
995
996#[derive(Debug, Serialize)]
997struct TransactionInfo {
998    id: String,
999    #[serde(rename = "type")]
1000    message_type: String,
1001    thread_id: Option<String>,
1002    from: Option<String>,
1003    to: Option<String>,
1004    direction: String,
1005    created_at: String,
1006    body: serde_json::Value,
1007}
1008
1009#[allow(clippy::too_many_arguments)]
1010async fn handle_list(
1011    agent_did: &str,
1012    msg_type: Option<&str>,
1013    thread_id: Option<&str>,
1014    from: Option<&str>,
1015    to: Option<&str>,
1016    limit: u32,
1017    offset: u32,
1018    format: OutputFormat,
1019    tap_integration: &TapIntegration,
1020) -> Result<()> {
1021    let storage = tap_integration.storage_for_agent(agent_did).await?;
1022    let direction_filter = None;
1023    let messages = storage
1024        .list_messages(limit, offset, direction_filter)
1025        .await?;
1026
1027    let filtered: Vec<_> = messages
1028        .into_iter()
1029        .filter(|msg| {
1030            if let Some(mt) = msg_type {
1031                if !msg.message_type.contains(mt) {
1032                    return false;
1033                }
1034            }
1035            if let Some(tid) = thread_id {
1036                if msg.thread_id.as_ref() != Some(&tid.to_string()) {
1037                    return false;
1038                }
1039            }
1040            if let Some(f) = from {
1041                if msg.from_did.as_ref() != Some(&f.to_string()) {
1042                    return false;
1043                }
1044            }
1045            if let Some(t) = to {
1046                if msg.to_did.as_ref() != Some(&t.to_string()) {
1047                    return false;
1048                }
1049            }
1050            true
1051        })
1052        .collect();
1053
1054    let transactions: Vec<TransactionInfo> = filtered
1055        .iter()
1056        .map(|msg| TransactionInfo {
1057            id: msg.message_id.clone(),
1058            message_type: msg.message_type.clone(),
1059            thread_id: msg.thread_id.clone(),
1060            from: msg.from_did.clone(),
1061            to: msg.to_did.clone(),
1062            direction: msg.direction.to_string(),
1063            created_at: msg.created_at.clone(),
1064            body: msg.message_json.clone(),
1065        })
1066        .collect();
1067
1068    let response = TransactionListResponse {
1069        total: transactions.len(),
1070        transactions,
1071    };
1072    print_success(format, &response);
1073    Ok(())
1074}
1075
1076fn parse_agents(json: Option<&str>) -> Result<Vec<Agent>> {
1077    match json {
1078        Some(j) => {
1079            let inputs: Vec<AgentInput> = serde_json::from_str(j)
1080                .map_err(|e| Error::invalid_parameter(format!("Invalid agents JSON: {}", e)))?;
1081            Ok(inputs
1082                .iter()
1083                .map(|a| Agent::new(&a.id, &a.role, &a.for_party))
1084                .collect())
1085        }
1086        None => Ok(vec![]),
1087    }
1088}