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 #[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 #[arg(long)]
40 asset: String,
41 #[arg(long)]
43 amount: String,
44 #[arg(long)]
46 originator: String,
47 #[arg(long)]
49 beneficiary: String,
50 #[arg(long)]
52 agents: Option<String>,
53 #[arg(long)]
55 memo: Option<String>,
56 #[arg(long)]
58 expiry: Option<String>,
59 #[arg(long)]
61 transaction_value: Option<String>,
62 },
63 #[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 #[arg(long)]
79 amount: String,
80 #[arg(long)]
82 merchant: String,
83 #[arg(long, conflicts_with = "currency")]
85 asset: Option<String>,
86 #[arg(long, conflicts_with = "asset")]
88 currency: Option<String>,
89 #[arg(long)]
91 agents: Option<String>,
92 #[arg(long)]
94 memo: Option<String>,
95 #[arg(long)]
97 expiry: Option<String>,
98 #[arg(long)]
100 invoice_url: Option<String>,
101 #[arg(long, value_delimiter = ',')]
103 fallback_addresses: Option<Vec<String>>,
104 },
105 #[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 #[arg(long)]
117 recipient: String,
118 #[arg(long, name = "for")]
120 for_party: String,
121 #[arg(long)]
123 role: Option<String>,
124 #[arg(long)]
126 constraints: Option<String>,
127 #[arg(long)]
129 expiry: Option<String>,
130 #[arg(long)]
132 agreement: Option<String>,
133 },
134 #[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 #[arg(long)]
150 amount: String,
151 #[arg(long)]
153 originator: String,
154 #[arg(long)]
156 beneficiary: String,
157 #[arg(long)]
159 expiry: String,
160 #[arg(long)]
162 agents: String,
163 #[arg(long, conflicts_with = "currency")]
165 asset: Option<String>,
166 #[arg(long, conflicts_with = "asset")]
168 currency: Option<String>,
169 #[arg(long)]
171 agreement: Option<String>,
172 },
173 #[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 #[arg(long)]
187 escrow_id: String,
188 #[arg(long)]
190 amount: Option<String>,
191 #[arg(long)]
193 settlement_address: Option<String>,
194 },
195 Exchange {
197 #[arg(long, value_delimiter = ',')]
199 from_assets: Vec<String>,
200 #[arg(long, value_delimiter = ',')]
202 to_assets: Vec<String>,
203 #[arg(long, conflicts_with = "to_amount")]
205 from_amount: Option<String>,
206 #[arg(long, conflicts_with = "from_amount")]
208 to_amount: Option<String>,
209 #[arg(long)]
211 requester: String,
212 #[arg(long)]
214 provider: Option<String>,
215 #[arg(long)]
217 agents: Option<String>,
218 },
219 Quote {
221 #[arg(long)]
223 exchange_id: String,
224 #[arg(long)]
226 from_asset: String,
227 #[arg(long)]
229 to_asset: String,
230 #[arg(long)]
232 from_amount: String,
233 #[arg(long)]
235 to_amount: String,
236 #[arg(long)]
238 provider: String,
239 #[arg(long)]
241 agents: Option<String>,
242 #[arg(long)]
244 expires: String,
245 },
246 #[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 #[arg(long)]
260 agent_did: Option<String>,
261 #[arg(long, name = "type")]
263 msg_type: Option<String>,
264 #[arg(long)]
266 thread_id: Option<String>,
267 #[arg(long)]
269 from: Option<String>,
270 #[arg(long)]
272 to: Option<String>,
273 #[arg(long, default_value = "50")]
275 limit: u32,
276 #[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}