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        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 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, value_parser = clap::builder::ValueParser::new(alias_validator))]
91    pub alias: Option<String>,
92
93    /// Build the transaction and only write the base64 xdr to stdout
94    #[arg(long)]
95    pub build_only: bool,
96}
97
98impl Cmd {
99    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
100        let res = self
101            .run_against_rpc_server(Some(global_args), None)
102            .await?
103            .to_envelope();
104        match res {
105            TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
106            TxnEnvelopeResult::Res(contract) => {
107                let network = self.config.get_network()?;
108
109                if let Some(alias) = self.alias.clone() {
110                    if let Some(existing_contract) = self
111                        .config
112                        .locator
113                        .get_contract_id(&alias, &network.network_passphrase)?
114                    {
115                        let print = Print::new(global_args.quiet);
116                        print.warnln(format!(
117                            "Overwriting existing contract id: {existing_contract}"
118                        ));
119                    }
120
121                    self.config.locator.save_contract_id(
122                        &network.network_passphrase,
123                        &contract,
124                        &alias,
125                    )?;
126                }
127
128                println!("{contract}");
129            }
130        }
131        Ok(())
132    }
133}
134
135#[async_trait::async_trait]
136impl NetworkRunnable for Cmd {
137    type Error = Error;
138    type Result = TxnResult<stellar_strkey::Contract>;
139
140    async fn run_against_rpc_server(
141        &self,
142        args: Option<&global::Args>,
143        config: Option<&config::Args>,
144    ) -> Result<Self::Result, Error> {
145        let config = config.unwrap_or(&self.config);
146        // Parse asset
147        let asset = self.asset.resolve(&config.locator)?;
148
149        let network = config.get_network()?;
150        let client = network.rpc_client()?;
151        client
152            .verify_network_passphrase(Some(&network.network_passphrase))
153            .await?;
154
155        let source_account = config.source_account().await?;
156
157        // Get the account sequence number
158        // TODO: use symbols for the method names (both here and in serve)
159        let account_details = client
160            .get_account(&source_account.clone().to_string())
161            .await?;
162        let sequence: i64 = account_details.seq_num.into();
163        let network_passphrase = &network.network_passphrase;
164        let contract_id = contract_id_hash_from_asset(&asset, network_passphrase);
165        let tx = build_wrap_token_tx(
166            asset,
167            &contract_id,
168            sequence + 1,
169            config.get_inclusion_fee()?,
170            network_passphrase,
171            source_account,
172        )?;
173
174        if self.build_only {
175            return Ok(TxnResult::Txn(Box::new(tx)));
176        }
177
178        let assembled = simulate_and_assemble_transaction(
179            &client,
180            &tx,
181            self.resources.resource_config(),
182            self.resources.resource_fee,
183        )
184        .await?;
185        let assembled = self.resources.apply_to_assembled_txn(assembled);
186        let txn = assembled.transaction().clone();
187        let get_txn_resp = client
188            .send_transaction_polling(&self.config.sign(txn, args.is_some_and(|g| g.quiet)).await?)
189            .await?;
190
191        self.resources.print_cost_info(&get_txn_resp)?;
192
193        if args.is_none_or(|a| !a.no_cache) {
194            data::write(get_txn_resp.clone().try_into()?, &network.rpc_uri()?)?;
195        }
196
197        Ok(TxnResult::Res(stellar_strkey::Contract(contract_id.0)))
198    }
199}
200
201fn build_wrap_token_tx(
202    asset: impl Into<Asset>,
203    contract_id: &stellar_strkey::Contract,
204    sequence: i64,
205    fee: u32,
206    _network_passphrase: &str,
207    source_account: MuxedAccount,
208) -> Result<Transaction, Error> {
209    let contract = ScAddress::Contract(stellar_xdr::curr::ContractId(Hash(contract_id.0)));
210    let mut read_write = vec![
211        ContractData(LedgerKeyContractData {
212            contract: contract.clone(),
213            key: ScVal::LedgerKeyContractInstance,
214            durability: ContractDataDurability::Persistent,
215        }),
216        ContractData(LedgerKeyContractData {
217            contract: contract.clone(),
218            key: ScVal::Vec(Some(
219                vec![ScVal::Symbol("Metadata".try_into().unwrap())].try_into()?,
220            )),
221            durability: ContractDataDurability::Persistent,
222        }),
223    ];
224    let asset = asset.into();
225    if asset != Asset::Native {
226        read_write.push(ContractData(LedgerKeyContractData {
227            contract,
228            key: ScVal::Vec(Some(
229                vec![ScVal::Symbol("Admin".try_into().unwrap())].try_into()?,
230            )),
231            durability: ContractDataDurability::Persistent,
232        }));
233    }
234
235    let op = Operation {
236        source_account: None,
237        body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
238            host_function: HostFunction::CreateContract(CreateContractArgs {
239                contract_id_preimage: ContractIdPreimage::Asset(asset),
240                executable: ContractExecutable::StellarAsset,
241            }),
242            auth: VecM::default(),
243        }),
244    };
245
246    Ok(Transaction {
247        source_account,
248        fee,
249        seq_num: SequenceNumber(sequence),
250        cond: Preconditions::None,
251        memo: Memo::None,
252        operations: vec![op].try_into()?,
253        ext: TransactionExt::V0,
254    })
255}