soroban_cli/commands/token/
transfer.rs1use std::ffi::OsString;
2
3use clap::Parser;
4
5use crate::{
6 commands::{
7 contract::invoke,
8 global,
9 token::args::{self, OutputFormat},
10 },
11 config::{
12 self, locator, network, sign_with, token::UnresolvedToken, UnresolvedContract,
13 UnresolvedMuxedAccount, UnresolvedScAddress,
14 },
15 output::Output,
16};
17
18#[derive(Debug, Parser, Clone)]
19#[group(skip)]
20pub struct Cmd {
21 #[arg(long = "id")]
24 pub id: UnresolvedToken,
25
26 #[arg(long)]
29 pub from: UnresolvedMuxedAccount,
30
31 #[arg(long)]
34 pub to: UnresolvedScAddress,
35
36 #[arg(long, value_parser = parse_nonneg_i128)]
39 pub amount: i128,
40
41 #[arg(long, default_value = "text")]
43 pub output: OutputFormat,
44
45 #[command(flatten)]
46 pub network: network::Args,
47
48 #[command(flatten)]
49 pub locator: locator::Args,
50
51 #[command(flatten)]
52 pub sign_with: sign_with::Args,
53}
54
55#[derive(thiserror::Error, Debug)]
56pub enum Error {
57 #[error(transparent)]
58 Config(#[from] config::Error),
59 #[error(transparent)]
60 Network(#[from] network::Error),
61 #[error(transparent)]
62 Args(#[from] args::Error),
63 #[error(transparent)]
64 Token(#[from] config::token::Error),
65 #[error(transparent)]
66 ScAddress(#[from] config::sc_address::Error),
67 #[error(transparent)]
68 Invoke(#[from] invoke::Error),
69 #[error(transparent)]
70 Serde(#[from] serde_json::Error),
71
72 #[error(
73 "muxed (M…) source accounts are not yet supported for `token transfer`; \
74 use the underlying G… account as `--from` instead"
75 )]
76 MuxedSourceNotSupported,
77}
78
79fn parse_nonneg_i128(value: &str) -> Result<i128, String> {
83 let amount: i128 = value
84 .parse()
85 .map_err(|_| format!("invalid amount: {value}"))?;
86 if amount < 0 {
87 return Err(format!("amount must not be negative: {value}"));
88 }
89 Ok(amount)
90}
91
92impl Error {
93 #[must_use]
95 pub fn error_type(&self) -> &'static str {
96 match self {
97 Error::Config(_) => "config",
98 Error::Network(_) => "network",
99 Error::Args(e) => e.error_type(),
100 Error::Token(e) => e.error_type(),
101 Error::ScAddress(_) => "invalid_address",
102 Error::Invoke(_) => "invoke",
103 Error::Serde(_) => "internal",
104 Error::MuxedSourceNotSupported => "unsupported",
105 }
106 }
107}
108
109#[derive(Debug, serde::Serialize)]
111struct Receipt {
112 tx_hash: Option<String>,
114 result: serde_json::Value,
117}
118
119impl Cmd {
120 fn config(&self) -> config::Args {
125 config::Args {
126 network: self.network.clone(),
127 source_account: self.from.clone(),
128 locator: self.locator.clone(),
129 sign_with: self.sign_with.clone(),
130 fee: None,
131 inclusion_fee: None,
132 }
133 }
134
135 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
136 let output = Output::new(self.output.into(), global_args.quiet);
137 let quiet = global_args.quiet || output.is_json();
141 let config = self.config();
142 let network = config.get_network()?;
143
144 let token = self
145 .id
146 .resolve(&config.locator, &network.network_passphrase)?;
147
148 let source_account = config.source_account()?;
156 if matches!(source_account, crate::xdr::MuxedAccount::MuxedEd25519(_)) {
157 return Err(Error::MuxedSourceNotSupported);
158 }
159 let from = source_account.to_string();
160 let to = self
164 .to
165 .clone()
166 .resolve(&config.locator, &network.network_passphrase, None)?
167 .to_string();
168 let amount = self.amount.to_string();
169
170 let slop: Vec<OsString> = [
171 "transfer", "--from", &from, "--to", &to, "--amount", &amount,
172 ]
173 .into_iter()
174 .map(OsString::from)
175 .collect();
176
177 let invoke_cmd = invoke::Cmd {
178 contract_id: UnresolvedContract::Resolved(token.contract_id),
179 slop,
180 config: config.clone(),
181 send: invoke::Send::Yes,
185 ..Default::default()
186 };
187
188 let receipt = invoke_cmd
189 .execute_with_receipt(&config, quiet, global_args.no_cache)
190 .await
191 .map_err(|e| {
192 args::not_deployed_error(&token, &e).map_or(Error::Invoke(e), Error::Args)
193 })?
194 .into_result();
195
196 let Some(receipt) = receipt else {
200 return Ok(());
201 };
202
203 let result = if receipt.output.is_empty() {
204 serde_json::Value::Null
205 } else {
206 serde_json::from_str(&receipt.output)
207 .unwrap_or(serde_json::Value::String(receipt.output.clone()))
208 };
209
210 if !output.is_json() {
213 if let Some(tx_hash) = &receipt.tx_hash {
214 println!("{tx_hash}");
215 }
216 }
217
218 output.json_value(&Receipt {
219 tx_hash: receipt.tx_hash,
220 result,
221 })?;
222
223 Ok(())
224 }
225}