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::commands::tx::fetch;
14use crate::{
15    assembled::simulate_and_assemble_transaction,
16    commands::{
17        global,
18        txn_result::{TxnEnvelopeResult, TxnResult},
19        NetworkRunnable,
20    },
21    config::{self, data, network},
22    rpc::Error as SorobanRpcError,
23    tx::builder,
24    utils::contract_id_hash_from_asset,
25};
26
27use crate::commands::contract::deploy::utils::alias_validator;
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    Asset(#[from] builder::asset::Error),
57
58    #[error(transparent)]
59    Locator(#[from] locator::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 fee: crate::fee::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, value_parser = clap::builder::ValueParser::new(alias_validator))]
91    pub alias: Option<String>,
92}
93
94impl Cmd {
95    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
96        let res = self.run_against_rpc_server(None, None).await?.to_envelope();
97        match res {
98            TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
99            TxnEnvelopeResult::Res(contract) => {
100                let network = self.config.get_network()?;
101
102                if let Some(alias) = self.alias.clone() {
103                    if let Some(existing_contract) = self
104                        .config
105                        .locator
106                        .get_contract_id(&alias, &network.network_passphrase)?
107                    {
108                        let print = Print::new(global_args.quiet);
109                        print.warnln(format!(
110                            "Overwriting existing contract id: {existing_contract}"
111                        ));
112                    }
113
114                    self.config.locator.save_contract_id(
115                        &network.network_passphrase,
116                        &contract,
117                        &alias,
118                    )?;
119                }
120
121                println!("{contract}");
122            }
123        }
124        Ok(())
125    }
126}
127
128#[async_trait::async_trait]
129impl NetworkRunnable for Cmd {
130    type Error = Error;
131    type Result = TxnResult<stellar_strkey::Contract>;
132
133    async fn run_against_rpc_server(
134        &self,
135        args: Option<&global::Args>,
136        config: Option<&config::Args>,
137    ) -> Result<Self::Result, Error> {
138        let config = config.unwrap_or(&self.config);
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            self.fee.fee,
163            network_passphrase,
164            source_account,
165        )?;
166
167        if self.fee.build_only {
168            return Ok(TxnResult::Txn(Box::new(tx)));
169        }
170
171        let assembled =
172            simulate_and_assemble_transaction(&client, &tx, self.fee.resource_config()).await?;
173        let assembled = self.fee.apply_to_assembled_txn(assembled);
174
175        let txn = assembled.transaction().clone();
176        let get_txn_resp = client
177            .send_transaction_polling(&self.config.sign(txn).await?)
178            .await?;
179
180        self.fee.print_cost_info(&get_txn_resp)?;
181
182        if args.is_none_or(|a| !a.no_cache) {
183            data::write(get_txn_resp.clone().try_into()?, &network.rpc_uri()?)?;
184        }
185
186        Ok(TxnResult::Res(stellar_strkey::Contract(contract_id.0)))
187    }
188}
189
190fn build_wrap_token_tx(
191    asset: impl Into<Asset>,
192    contract_id: &stellar_strkey::Contract,
193    sequence: i64,
194    fee: u32,
195    _network_passphrase: &str,
196    source_account: MuxedAccount,
197) -> Result<Transaction, Error> {
198    let contract = ScAddress::Contract(stellar_xdr::curr::ContractId(Hash(contract_id.0)));
199    let mut read_write = vec![
200        ContractData(LedgerKeyContractData {
201            contract: contract.clone(),
202            key: ScVal::LedgerKeyContractInstance,
203            durability: ContractDataDurability::Persistent,
204        }),
205        ContractData(LedgerKeyContractData {
206            contract: contract.clone(),
207            key: ScVal::Vec(Some(
208                vec![ScVal::Symbol("Metadata".try_into().unwrap())].try_into()?,
209            )),
210            durability: ContractDataDurability::Persistent,
211        }),
212    ];
213    let asset = asset.into();
214    if asset != Asset::Native {
215        read_write.push(ContractData(LedgerKeyContractData {
216            contract,
217            key: ScVal::Vec(Some(
218                vec![ScVal::Symbol("Admin".try_into().unwrap())].try_into()?,
219            )),
220            durability: ContractDataDurability::Persistent,
221        }));
222    }
223
224    let op = Operation {
225        source_account: None,
226        body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
227            host_function: HostFunction::CreateContract(CreateContractArgs {
228                contract_id_preimage: ContractIdPreimage::Asset(asset),
229                executable: ContractExecutable::StellarAsset,
230            }),
231            auth: VecM::default(),
232        }),
233    };
234
235    Ok(Transaction {
236        source_account,
237        fee,
238        seq_num: SequenceNumber(sequence),
239        cond: Preconditions::None,
240        memo: Memo::None,
241        operations: vec![op].try_into()?,
242        ext: TransactionExt::V0,
243    })
244}