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