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    },
21    config::{self, data, network},
22    rpc::Error as SorobanRpcError,
23    tx::builder,
24    utils::contract_id_hash_from_asset,
25};
26
27use crate::config::address::AliasName;
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)]
91    pub alias: Option<AliasName>,
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            .execute(&self.config, global_args.quiet, global_args.no_cache)
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    pub async fn execute(
135        &self,
136        config: &config::Args,
137        quiet: bool,
138        no_cache: bool,
139    ) -> Result<TxnResult<stellar_strkey::Contract>, Error> {
140        // Parse asset
141        let print = Print::new(quiet);
142        let asset = self.asset.resolve(&config.locator)?;
143
144        let network = config.get_network()?;
145        let client = network.rpc_client()?;
146        client
147            .verify_network_passphrase(Some(&network.network_passphrase))
148            .await?;
149
150        let source_account = config.source_account().await?;
151
152        // Get the account sequence number
153        // TODO: use symbols for the method names (both here and in serve)
154        let account_details = client
155            .get_account(&source_account.clone().to_string())
156            .await?;
157        let sequence: i64 = account_details.seq_num.into();
158        let network_passphrase = &network.network_passphrase;
159        let contract_id = contract_id_hash_from_asset(&asset, network_passphrase);
160        let tx = build_wrap_token_tx(
161            asset,
162            &contract_id,
163            sequence + 1,
164            config.get_inclusion_fee()?,
165            network_passphrase,
166            source_account,
167        )?;
168
169        if self.build_only {
170            return Ok(TxnResult::Txn(Box::new(tx)));
171        }
172
173        sim_sign_and_send_tx::<Error>(&client, &tx, config, &self.resources, &[], quiet, no_cache)
174            .await?;
175
176        if let Some(url) = utils::lab_url_for_contract(&network, &contract_id) {
177            print.linkln(url);
178        }
179        print.checkln("Deployed!");
180
181        Ok(TxnResult::Res(stellar_strkey::Contract(contract_id.0)))
182    }
183}
184
185fn build_wrap_token_tx(
186    asset: impl Into<Asset>,
187    contract_id: &stellar_strkey::Contract,
188    sequence: i64,
189    fee: u32,
190    _network_passphrase: &str,
191    source_account: MuxedAccount,
192) -> Result<Transaction, Error> {
193    let contract = ScAddress::Contract(stellar_xdr::curr::ContractId(Hash(contract_id.0)));
194    let mut read_write = vec![
195        ContractData(LedgerKeyContractData {
196            contract: contract.clone(),
197            key: ScVal::LedgerKeyContractInstance,
198            durability: ContractDataDurability::Persistent,
199        }),
200        ContractData(LedgerKeyContractData {
201            contract: contract.clone(),
202            key: ScVal::Vec(Some(
203                vec![ScVal::Symbol("Metadata".try_into().unwrap())].try_into()?,
204            )),
205            durability: ContractDataDurability::Persistent,
206        }),
207    ];
208    let asset = asset.into();
209    if asset != Asset::Native {
210        read_write.push(ContractData(LedgerKeyContractData {
211            contract,
212            key: ScVal::Vec(Some(
213                vec![ScVal::Symbol("Admin".try_into().unwrap())].try_into()?,
214            )),
215            durability: ContractDataDurability::Persistent,
216        }));
217    }
218
219    let op = Operation {
220        source_account: None,
221        body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
222            host_function: HostFunction::CreateContract(CreateContractArgs {
223                contract_id_preimage: ContractIdPreimage::Asset(asset),
224                executable: ContractExecutable::StellarAsset,
225            }),
226            auth: VecM::default(),
227        }),
228    };
229
230    Ok(Transaction {
231        source_account,
232        fee,
233        seq_num: SequenceNumber(sequence),
234        cond: Preconditions::None,
235        memo: Memo::None,
236        operations: vec![op].try_into()?,
237        ext: TransactionExt::V0,
238    })
239}