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