Skip to main content

soroban_cli/config/
mod.rs

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