Skip to main content

soroban_cli/config/
mod.rs

1use serde::{Deserialize, Serialize};
2use std::{
3    fs::{self, File},
4    io::Write,
5};
6
7use crate::{
8    print::Print,
9    signer::{self, Signer},
10    utils::deprecate_message,
11    xdr::{
12        self, FeeBumpTransaction, FeeBumpTransactionEnvelope, SequenceNumber, Transaction,
13        TransactionEnvelope, TransactionV1Envelope, VecM,
14    },
15    Pwd,
16};
17use network::Network;
18
19pub mod address;
20pub mod alias;
21pub mod data;
22pub mod key;
23pub mod locator;
24pub mod network;
25pub mod sc_address;
26pub mod secret;
27pub mod sign_with;
28pub mod upgrade_check;
29pub mod utils;
30
31use crate::config::locator::cli_config_file;
32pub use address::UnresolvedMuxedAccount;
33pub use alias::UnresolvedContract;
34pub use sc_address::UnresolvedScAddress;
35
36#[derive(thiserror::Error, Debug)]
37pub enum Error {
38    #[error(transparent)]
39    Network(#[from] network::Error),
40    #[error(transparent)]
41    Secret(#[from] secret::Error),
42    #[error(transparent)]
43    Locator(#[from] locator::Error),
44    #[error(transparent)]
45    Rpc(#[from] soroban_rpc::Error),
46    #[error(transparent)]
47    Signer(#[from] signer::Error),
48    #[error(transparent)]
49    SignWith(#[from] sign_with::Error),
50    #[error(transparent)]
51    StellarStrkey(#[from] stellar_strkey::DecodeError),
52    #[error(transparent)]
53    Address(#[from] address::Error),
54}
55
56#[derive(Debug, clap::Args, Clone, Default)]
57#[group(skip)]
58pub struct Args {
59    #[command(flatten)]
60    pub network: network::Args,
61
62    #[arg(long, short = 's', visible_alias = "source", env = "STELLAR_ACCOUNT")]
63    /// Account that where transaction originates from. Alias `source`.
64    /// Can be an identity (--source alice), a public key (--source GDKW...),
65    /// a muxed account (--source MDA…), a secret key (--source SC36…),
66    /// or a seed phrase (--source "kite urban…").
67    /// If `--build-only` was NOT provided, this key will also be used to
68    /// sign the final transaction. In that case, trying to sign with public key will fail.
69    pub source_account: UnresolvedMuxedAccount,
70
71    #[command(flatten)]
72    pub locator: locator::Args,
73
74    #[command(flatten)]
75    pub sign_with: sign_with::Args,
76
77    /// ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm
78    #[arg(long, env = "STELLAR_FEE")]
79    pub fee: Option<u32>,
80
81    /// Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided
82    #[arg(long, env = "STELLAR_INCLUSION_FEE")]
83    pub inclusion_fee: Option<u32>,
84}
85
86impl Args {
87    // TODO: Replace PublicKey with MuxedAccount once https://github.com/stellar/rs-stellar-xdr/pull/396 is merged.
88    pub async fn source_account(&self) -> Result<xdr::MuxedAccount, Error> {
89        Ok(self
90            .source_account
91            .resolve_muxed_account(&self.locator, self.hd_path())
92            .await?)
93    }
94
95    pub async fn source_signer(&self) -> Result<Signer, Error> {
96        let print = Print::new(true);
97        let secret = &self.source_account.resolve_secret(&self.locator)?;
98        Ok(secret.signer(self.hd_path(), print).await?)
99    }
100
101    pub fn key_pair(&self) -> Result<ed25519_dalek::SigningKey, Error> {
102        let key = &self.source_account.resolve_secret(&self.locator)?;
103        Ok(key.key_pair(self.hd_path())?)
104    }
105
106    pub async fn sign(&self, tx: Transaction, quiet: bool) -> Result<TransactionEnvelope, Error> {
107        let tx_env = TransactionEnvelope::Tx(TransactionV1Envelope {
108            tx,
109            signatures: VecM::default(),
110        });
111        Ok(self
112            .sign_with
113            .sign_tx_env(
114                &tx_env,
115                &self.locator,
116                &self.network.get(&self.locator)?,
117                quiet,
118                Some(&self.source_account),
119            )
120            .await?)
121    }
122
123    pub async fn sign_fee_bump(
124        &self,
125        tx: FeeBumpTransaction,
126        quiet: bool,
127    ) -> Result<TransactionEnvelope, Error> {
128        let tx_env = TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope {
129            tx,
130            signatures: VecM::default(),
131        });
132        Ok(self
133            .sign_with
134            .sign_tx_env(
135                &tx_env,
136                &self.locator,
137                &self.network.get(&self.locator)?,
138                quiet,
139                Some(&self.source_account),
140            )
141            .await?)
142    }
143
144    pub async fn sign_soroban_authorizations(
145        &self,
146        tx: &Transaction,
147        signers: &[Signer],
148    ) -> Result<Option<Transaction>, Error> {
149        let network = self.get_network()?;
150        let source_signer = self.source_signer().await?;
151        let client = network.rpc_client()?;
152        let latest_ledger = client.get_latest_ledger().await?.sequence;
153        let seq_num = latest_ledger + 60; // ~ 5 min
154        Ok(signer::sign_soroban_authorizations(
155            tx,
156            &source_signer,
157            signers,
158            seq_num,
159            &network.network_passphrase,
160        )?)
161    }
162
163    pub fn get_network(&self) -> Result<Network, Error> {
164        Ok(self.network.get(&self.locator)?)
165    }
166
167    /// Get the inclusion fee if available from args, otherwise fall back to fee,
168    /// and finally return 100 if nothing is set.
169    ///
170    /// Precedence is:
171    /// 1. inclusion_fee (via clap, arg then env var)
172    /// 2. fee (via clap, arg then env var)
173    /// 3. default of 100 stroops
174    pub fn get_inclusion_fee(&self) -> Result<u32, Error> {
175        if self.fee.is_some() {
176            deprecate_message(
177                Print::new(false),
178                "--fee [env: STELLAR_FEE]",
179                "Use `--inclusion-fee [env: STELLAR_INCLUSION_FEE]` instead.",
180            );
181        }
182        Ok(self.inclusion_fee.or(self.fee).unwrap_or(100))
183    }
184
185    pub async fn next_sequence_number(
186        &self,
187        account: impl Into<xdr::AccountId>,
188    ) -> Result<SequenceNumber, Error> {
189        let network = self.get_network()?;
190        let client = network.rpc_client()?;
191        Ok((client
192            .get_account(&account.into().to_string())
193            .await?
194            .seq_num
195            .0
196            + 1)
197        .into())
198    }
199
200    pub fn hd_path(&self) -> Option<usize> {
201        self.sign_with.hd_path
202    }
203}
204
205impl Pwd for Args {
206    fn set_pwd(&mut self, pwd: &std::path::Path) {
207        self.locator.set_pwd(pwd);
208    }
209}
210
211#[derive(Debug, clap::Args, Clone, Default)]
212#[group(skip)]
213pub struct ArgsLocatorAndNetwork {
214    #[command(flatten)]
215    pub network: network::Args,
216
217    #[command(flatten)]
218    pub locator: locator::Args,
219}
220
221impl ArgsLocatorAndNetwork {
222    pub fn get_network(&self) -> Result<Network, Error> {
223        Ok(self.network.get(&self.locator)?)
224    }
225}
226
227#[derive(Serialize, Deserialize, Debug, Default)]
228pub struct Config {
229    pub defaults: Defaults,
230}
231
232#[derive(Serialize, Deserialize, Debug, Default)]
233pub struct Defaults {
234    pub network: Option<String>,
235    pub identity: Option<String>,
236    pub inclusion_fee: Option<u32>,
237}
238
239impl Config {
240    pub fn new() -> Result<Config, locator::Error> {
241        let path = cli_config_file()?;
242
243        if path.exists() {
244            let data = fs::read_to_string(&path).map_err(|_| locator::Error::FileRead { path })?;
245            Ok(toml::from_str(&data)?)
246        } else {
247            Ok(Config::default())
248        }
249    }
250
251    #[must_use]
252    pub fn set_network(mut self, s: &str) -> Self {
253        self.defaults.network = Some(s.to_string());
254        self
255    }
256
257    #[must_use]
258    pub fn set_identity(mut self, s: &str) -> Self {
259        self.defaults.identity = Some(s.to_string());
260        self
261    }
262
263    #[must_use]
264    pub fn set_inclusion_fee(mut self, uint: u32) -> Self {
265        self.defaults.inclusion_fee = Some(uint);
266        self
267    }
268
269    #[must_use]
270    pub fn unset_identity(mut self) -> Self {
271        self.defaults.identity = None;
272        self
273    }
274
275    #[must_use]
276    pub fn unset_network(mut self) -> Self {
277        self.defaults.network = None;
278        self
279    }
280
281    #[must_use]
282    pub fn unset_inclusion_fee(mut self) -> Self {
283        self.defaults.inclusion_fee = None;
284        self
285    }
286
287    pub fn save(&self) -> Result<(), locator::Error> {
288        let toml_string = toml::to_string(&self)?;
289        let path = cli_config_file()?;
290        // Depending on the platform, this function may fail if the full directory path does not exist
291        let mut file = File::create(locator::ensure_directory(path)?)?;
292        file.write_all(toml_string.as_bytes())?;
293
294        Ok(())
295    }
296}