Skip to main content

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