soroban_cli/commands/contract/deploy/
asset.rs

1use crate::config::locator;
2use crate::print::Print;
3use crate::xdr::{
4    Asset, ContractDataDurability, ContractExecutable, ContractIdPreimage, CreateContractArgs,
5    Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp, LedgerKey::ContractData,
6    LedgerKeyContractData, Limits, Memo, MuxedAccount, Operation, OperationBody, Preconditions,
7    ScAddress, ScVal, SequenceNumber, Transaction, TransactionExt, VecM, WriteXdr,
8};
9use clap::{arg, command, Parser};
10use std::convert::Infallible;
11use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError};
12
13use crate::{
14    assembled::simulate_and_assemble_transaction,
15    commands::{
16        global,
17        txn_result::{TxnEnvelopeResult, TxnResult},
18        NetworkRunnable,
19    },
20    config::{self, data, network},
21    rpc::Error as SorobanRpcError,
22    tx::builder,
23    utils::contract_id_hash_from_asset,
24};
25
26use crate::commands::contract::deploy::utils::alias_validator;
27
28#[derive(thiserror::Error, Debug)]
29pub enum Error {
30    #[error("error parsing int: {0}")]
31    ParseIntError(#[from] ParseIntError),
32    #[error(transparent)]
33    Client(#[from] SorobanRpcError),
34    #[error("internal conversion error: {0}")]
35    TryFromSliceError(#[from] TryFromSliceError),
36    #[error("xdr processing error: {0}")]
37    Xdr(#[from] XdrError),
38    #[error(transparent)]
39    Config(#[from] config::Error),
40    #[error(transparent)]
41    Data(#[from] data::Error),
42    #[error(transparent)]
43    Network(#[from] network::Error),
44    #[error(transparent)]
45    Builder(#[from] builder::Error),
46    #[error(transparent)]
47    Asset(#[from] builder::asset::Error),
48    #[error(transparent)]
49    Locator(#[from] locator::Error),
50}
51
52impl From<Infallible> for Error {
53    fn from(_: Infallible) -> Self {
54        unreachable!()
55    }
56}
57
58#[derive(Parser, Debug, Clone)]
59#[group(skip)]
60pub struct Cmd {
61    /// ID of the Stellar classic asset to wrap, e.g. "USDC:G...5"
62    #[arg(long)]
63    pub asset: builder::Asset,
64
65    #[command(flatten)]
66    pub config: config::Args,
67
68    #[command(flatten)]
69    pub fee: crate::fee::Args,
70
71    /// The alias that will be used to save the assets's id.
72    /// Whenever used, `--alias` will always overwrite the existing contract id
73    /// configuration without asking for confirmation.
74    #[arg(long, value_parser = clap::builder::ValueParser::new(alias_validator))]
75    pub alias: Option<String>,
76}
77
78impl Cmd {
79    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
80        let res = self.run_against_rpc_server(None, None).await?.to_envelope();
81        match res {
82            TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
83            TxnEnvelopeResult::Res(contract) => {
84                let network = self.config.get_network()?;
85
86                if let Some(alias) = self.alias.clone() {
87                    if let Some(existing_contract) = self
88                        .config
89                        .locator
90                        .get_contract_id(&alias, &network.network_passphrase)?
91                    {
92                        let print = Print::new(global_args.quiet);
93                        print.warnln(format!(
94                            "Overwriting existing contract id: {existing_contract}"
95                        ));
96                    }
97
98                    self.config.locator.save_contract_id(
99                        &network.network_passphrase,
100                        &contract,
101                        &alias,
102                    )?;
103                }
104
105                println!("{contract}");
106            }
107        }
108        Ok(())
109    }
110}
111
112#[async_trait::async_trait]
113impl NetworkRunnable for Cmd {
114    type Error = Error;
115    type Result = TxnResult<stellar_strkey::Contract>;
116
117    async fn run_against_rpc_server(
118        &self,
119        args: Option<&global::Args>,
120        config: Option<&config::Args>,
121    ) -> Result<Self::Result, Error> {
122        let config = config.unwrap_or(&self.config);
123        // Parse asset
124        let asset = self.asset.resolve(&config.locator)?;
125
126        let network = config.get_network()?;
127        let client = network.rpc_client()?;
128        client
129            .verify_network_passphrase(Some(&network.network_passphrase))
130            .await?;
131
132        let source_account = config.source_account().await?;
133
134        // Get the account sequence number
135        // TODO: use symbols for the method names (both here and in serve)
136        let account_details = client
137            .get_account(&source_account.clone().to_string())
138            .await?;
139        let sequence: i64 = account_details.seq_num.into();
140        let network_passphrase = &network.network_passphrase;
141        let contract_id = contract_id_hash_from_asset(&asset, network_passphrase);
142        let tx = build_wrap_token_tx(
143            asset,
144            &contract_id,
145            sequence + 1,
146            self.fee.fee,
147            network_passphrase,
148            source_account,
149        )?;
150        if self.fee.build_only {
151            return Ok(TxnResult::Txn(Box::new(tx)));
152        }
153        let txn = simulate_and_assemble_transaction(&client, &tx).await?;
154        let txn = self.fee.apply_to_assembled_txn(txn).transaction().clone();
155        #[cfg(feature = "version_lt_23")]
156        if self.fee.sim_only {
157            return Ok(TxnResult::Txn(Box::new(txn)));
158        }
159        let get_txn_resp = client
160            .send_transaction_polling(&self.config.sign(txn).await?)
161            .await?
162            .try_into()?;
163        if args.is_none_or(|a| !a.no_cache) {
164            data::write(get_txn_resp, &network.rpc_uri()?)?;
165        }
166
167        Ok(TxnResult::Res(stellar_strkey::Contract(contract_id.0)))
168    }
169}
170
171fn build_wrap_token_tx(
172    asset: impl Into<Asset>,
173    contract_id: &stellar_strkey::Contract,
174    sequence: i64,
175    fee: u32,
176    _network_passphrase: &str,
177    source_account: MuxedAccount,
178) -> Result<Transaction, Error> {
179    let contract = ScAddress::Contract(stellar_xdr::curr::ContractId(Hash(contract_id.0)));
180    let mut read_write = vec![
181        ContractData(LedgerKeyContractData {
182            contract: contract.clone(),
183            key: ScVal::LedgerKeyContractInstance,
184            durability: ContractDataDurability::Persistent,
185        }),
186        ContractData(LedgerKeyContractData {
187            contract: contract.clone(),
188            key: ScVal::Vec(Some(
189                vec![ScVal::Symbol("Metadata".try_into().unwrap())].try_into()?,
190            )),
191            durability: ContractDataDurability::Persistent,
192        }),
193    ];
194    let asset = asset.into();
195    if asset != Asset::Native {
196        read_write.push(ContractData(LedgerKeyContractData {
197            contract,
198            key: ScVal::Vec(Some(
199                vec![ScVal::Symbol("Admin".try_into().unwrap())].try_into()?,
200            )),
201            durability: ContractDataDurability::Persistent,
202        }));
203    }
204
205    let op = Operation {
206        source_account: None,
207        body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
208            host_function: HostFunction::CreateContract(CreateContractArgs {
209                contract_id_preimage: ContractIdPreimage::Asset(asset),
210                executable: ContractExecutable::StellarAsset,
211            }),
212            auth: VecM::default(),
213        }),
214    };
215
216    Ok(Transaction {
217        source_account,
218        fee,
219        seq_num: SequenceNumber(sequence),
220        cond: Preconditions::None,
221        memo: Memo::None,
222        operations: vec![op].try_into()?,
223        ext: TransactionExt::V0,
224    })
225}