use std::ffi::OsString;
use clap::Parser;
use crate::{
commands::{
contract::invoke,
global,
token::args::{self, OutputFormat},
},
config::{
self, locator, network, sign_with, token::UnresolvedToken, UnresolvedContract,
UnresolvedMuxedAccount, UnresolvedScAddress,
},
output::Output,
};
#[derive(Debug, Parser, Clone)]
#[group(skip)]
pub struct Cmd {
#[arg(long = "id")]
pub id: UnresolvedToken,
#[arg(long)]
pub from: UnresolvedMuxedAccount,
#[arg(long)]
pub to: UnresolvedScAddress,
#[arg(long, value_parser = parse_nonneg_i128)]
pub amount: i128,
#[arg(long, default_value = "text")]
pub output: OutputFormat,
#[command(flatten)]
pub network: network::Args,
#[command(flatten)]
pub locator: locator::Args,
#[command(flatten)]
pub sign_with: sign_with::Args,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Config(#[from] config::Error),
#[error(transparent)]
Network(#[from] network::Error),
#[error(transparent)]
Args(#[from] args::Error),
#[error(transparent)]
Token(#[from] config::token::Error),
#[error(transparent)]
ScAddress(#[from] config::sc_address::Error),
#[error(transparent)]
Invoke(#[from] invoke::Error),
#[error(transparent)]
Serde(#[from] serde_json::Error),
#[error(
"muxed (M…) source accounts are not yet supported for `token transfer`; \
use the underlying G… account as `--from` instead"
)]
MuxedSourceNotSupported,
}
fn parse_nonneg_i128(value: &str) -> Result<i128, String> {
let amount: i128 = value
.parse()
.map_err(|_| format!("invalid amount: {value}"))?;
if amount < 0 {
return Err(format!("amount must not be negative: {value}"));
}
Ok(amount)
}
impl Error {
#[must_use]
pub fn error_type(&self) -> &'static str {
match self {
Error::Config(_) => "config",
Error::Network(_) => "network",
Error::Args(e) => e.error_type(),
Error::Token(e) => e.error_type(),
Error::ScAddress(_) => "invalid_address",
Error::Invoke(_) => "invoke",
Error::Serde(_) => "internal",
Error::MuxedSourceNotSupported => "unsupported",
}
}
}
#[derive(Debug, serde::Serialize)]
struct Receipt {
tx_hash: Option<String>,
result: serde_json::Value,
}
impl Cmd {
fn config(&self) -> config::Args {
config::Args {
network: self.network.clone(),
source_account: self.from.clone(),
locator: self.locator.clone(),
sign_with: self.sign_with.clone(),
fee: None,
inclusion_fee: None,
}
}
pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
let output = Output::new(self.output.into(), global_args.quiet);
let quiet = global_args.quiet || output.is_json();
let config = self.config();
let network = config.get_network()?;
let token = self
.id
.resolve(&config.locator, &network.network_passphrase)?;
let source_account = config.source_account()?;
if matches!(source_account, crate::xdr::MuxedAccount::MuxedEd25519(_)) {
return Err(Error::MuxedSourceNotSupported);
}
let from = source_account.to_string();
let to = self
.to
.clone()
.resolve(&config.locator, &network.network_passphrase, None)?
.to_string();
let amount = self.amount.to_string();
let slop: Vec<OsString> = [
"transfer", "--from", &from, "--to", &to, "--amount", &amount,
]
.into_iter()
.map(OsString::from)
.collect();
let invoke_cmd = invoke::Cmd {
contract_id: UnresolvedContract::Resolved(token.contract_id),
slop,
config: config.clone(),
send: invoke::Send::Yes,
..Default::default()
};
let receipt = invoke_cmd
.execute_with_receipt(&config, quiet, global_args.no_cache)
.await
.map_err(|e| {
args::not_deployed_error(&token, &e).map_or(Error::Invoke(e), Error::Args)
})?
.into_result();
let Some(receipt) = receipt else {
return Ok(());
};
let result = if receipt.output.is_empty() {
serde_json::Value::Null
} else {
serde_json::from_str(&receipt.output)
.unwrap_or(serde_json::Value::String(receipt.output.clone()))
};
if !output.is_json() {
if let Some(tx_hash) = &receipt.tx_hash {
println!("{tx_hash}");
}
}
output.json_value(&Receipt {
tx_hash: receipt.tx_hash,
result,
})?;
Ok(())
}
}