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