Skip to main content

soroban_cli/commands/contract/deploy/
asset.rs

1use crate::config::locator;
2use crate::print::Print;
3use crate::tx::sim_sign_and_send_tx;
4use crate::utils;
5use crate::xdr::{
6    Asset, ContractDataDurability, ContractExecutable, ContractIdPreimage, CreateContractArgs,
7    Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp, LedgerKey::ContractData,
8    LedgerKeyContractData, Limits, Memo, MuxedAccount, Operation, OperationBody, Preconditions,
9    ScAddress, ScVal, SequenceNumber, Transaction, TransactionExt, VecM, WriteXdr,
10};
11use clap::Parser;
12use std::convert::Infallible;
13use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError};
14
15use crate::commands::tx::fetch;
16use crate::{
17    commands::{
18        global,
19        txn_result::{TxnEnvelopeResult, TxnResult},
20        HEADING_TRANSACTION,
21    },
22    config::{self, data, network, token::UnresolvedToken},
23    rpc::Error as SorobanRpcError,
24    tx::builder,
25};
26
27use crate::config::address::AliasName;
28
29#[derive(thiserror::Error, Debug)]
30pub enum Error {
31    #[error("error parsing int: {0}")]
32    ParseIntError(#[from] ParseIntError),
33
34    #[error(transparent)]
35    Client(#[from] SorobanRpcError),
36
37    #[error("internal conversion error: {0}")]
38    TryFromSliceError(#[from] TryFromSliceError),
39
40    #[error("xdr processing error: {0}")]
41    Xdr(#[from] XdrError),
42
43    #[error(transparent)]
44    Config(#[from] config::Error),
45
46    #[error(transparent)]
47    Data(#[from] data::Error),
48
49    #[error(transparent)]
50    Network(#[from] network::Error),
51
52    #[error(transparent)]
53    Builder(#[from] builder::Error),
54
55    #[error(transparent)]
56    Locator(#[from] locator::Error),
57
58    #[error(transparent)]
59    Token(#[from] config::token::Error),
60
61    #[error(transparent)]
62    Fee(#[from] fetch::fee::Error),
63
64    #[error(transparent)]
65    Fetch(#[from] fetch::Error),
66}
67
68impl From<Infallible> for Error {
69    fn from(_: Infallible) -> Self {
70        unreachable!()
71    }
72}
73
74#[derive(Parser, Debug, Clone)]
75#[group(skip)]
76pub struct Cmd {
77    /// ID of the Stellar classic asset to wrap, e.g. "USDC:G...5"
78    #[arg(long)]
79    pub asset: builder::Asset,
80
81    #[command(flatten)]
82    pub config: config::Args,
83
84    #[command(flatten)]
85    pub resources: crate::resources::Args,
86
87    /// The alias that will be used to save the assets's id.
88    /// Whenever used, `--alias` will always overwrite the existing contract id
89    /// configuration without asking for confirmation.
90    #[arg(long)]
91    pub alias: Option<AliasName>,
92
93    /// Build the transaction and only write the base64 xdr to stdout
94    #[arg(long, help_heading = HEADING_TRANSACTION)]
95    pub build_only: bool,
96}
97
98impl Cmd {
99    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
100        // Validate the alias before simulating or deploying, so a reserved alias
101        // fails fast instead of after an on-chain deploy.
102        if let Some(alias) = &self.alias {
103            crate::config::alias::validate_reserved_aliases(alias)?;
104        }
105
106        let res = self
107            .execute(&self.config, global_args.quiet, global_args.no_cache)
108            .await?
109            .to_envelope();
110        match res {
111            TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
112            TxnEnvelopeResult::Res(contract) => {
113                let network = self.config.get_network()?;
114
115                if let Some(alias) = self.alias.clone() {
116                    if let Some(existing_contract) = self
117                        .config
118                        .locator
119                        .get_contract_id(&alias, &network.network_passphrase)?
120                    {
121                        let print = Print::new(global_args.quiet);
122                        print.warnln(format!(
123                            "Overwriting existing contract id: {existing_contract}"
124                        ));
125                    }
126
127                    self.config.locator.save_contract_id(
128                        &network.network_passphrase,
129                        &contract,
130                        &alias,
131                    )?;
132                }
133
134                println!("{contract}");
135            }
136        }
137        Ok(())
138    }
139
140    pub async fn execute(
141        &self,
142        config: &config::Args,
143        quiet: bool,
144        no_cache: bool,
145    ) -> Result<TxnResult<stellar_strkey::Contract>, Error> {
146        let print = Print::new(quiet);
147
148        let network = config.get_network()?;
149        // Resolve the asset up front, before any RPC, so an invalid asset or
150        // unresolvable issuer alias fails fast instead of after network calls.
151        let token = UnresolvedToken::Asset(self.asset.clone())
152            .resolve(&config.locator, &network.network_passphrase)?;
153        let contract_id = token.contract_id;
154        let asset = token
155            .asset()
156            .expect("an asset reference resolves to a SAC")
157            .clone();
158
159        let client = network.rpc_client()?;
160        client
161            .verify_network_passphrase(Some(&network.network_passphrase))
162            .await?;
163
164        let source_account = config.source_account()?;
165
166        // Get the account sequence number
167        // TODO: use symbols for the method names (both here and in serve)
168        let account_details = client
169            .get_account(&source_account.clone().to_string())
170            .await?;
171        let sequence: i64 = account_details.seq_num.into();
172        let network_passphrase = &network.network_passphrase;
173        let tx = build_wrap_token_tx(
174            asset,
175            &contract_id,
176            sequence + 1,
177            config.get_inclusion_fee()?,
178            network_passphrase,
179            source_account,
180        )?;
181
182        if self.build_only {
183            return Ok(TxnResult::Txn(Box::new(tx)));
184        }
185
186        sim_sign_and_send_tx::<Error>(
187            &client,
188            &tx,
189            config,
190            &self.resources,
191            &[],
192            // Asset wrapping cannot accept simulateTransaction authMode.
193            None,
194            quiet,
195            no_cache,
196        )
197        .await?;
198
199        if let Some(url) = utils::lab_url_for_contract(&network, &contract_id) {
200            print.linkln(url);
201        }
202        print.checkln("Deployed!");
203
204        Ok(TxnResult::Res(stellar_strkey::Contract(contract_id.0)))
205    }
206}
207
208fn build_wrap_token_tx(
209    asset: impl Into<Asset>,
210    contract_id: &stellar_strkey::Contract,
211    sequence: i64,
212    fee: u32,
213    _network_passphrase: &str,
214    source_account: MuxedAccount,
215) -> Result<Transaction, Error> {
216    let contract = ScAddress::Contract(stellar_xdr::ContractId(Hash(contract_id.0)));
217    let mut read_write = vec![
218        ContractData(LedgerKeyContractData {
219            contract: contract.clone(),
220            key: ScVal::LedgerKeyContractInstance,
221            durability: ContractDataDurability::Persistent,
222        }),
223        ContractData(LedgerKeyContractData {
224            contract: contract.clone(),
225            key: ScVal::Vec(Some(
226                vec![ScVal::Symbol("Metadata".try_into().unwrap())].try_into()?,
227            )),
228            durability: ContractDataDurability::Persistent,
229        }),
230    ];
231    let asset = asset.into();
232    if asset != Asset::Native {
233        read_write.push(ContractData(LedgerKeyContractData {
234            contract,
235            key: ScVal::Vec(Some(
236                vec![ScVal::Symbol("Admin".try_into().unwrap())].try_into()?,
237            )),
238            durability: ContractDataDurability::Persistent,
239        }));
240    }
241
242    let op = Operation {
243        source_account: None,
244        body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
245            host_function: HostFunction::CreateContract(CreateContractArgs {
246                contract_id_preimage: ContractIdPreimage::Asset(asset),
247                executable: ContractExecutable::StellarAsset,
248            }),
249            auth: VecM::default(),
250        }),
251    };
252
253    Ok(Transaction {
254        source_account,
255        fee,
256        seq_num: SequenceNumber(sequence),
257        cond: Preconditions::None,
258        memo: Memo::None,
259        operations: vec![op].try_into()?,
260        ext: TransactionExt::V0,
261    })
262}