soroban_cli/config/
mod.rs

1use clap::{arg, command};
2use serde::{Deserialize, Serialize};
3use std::{
4    fs::{self, File},
5    io::Write,
6};
7
8use crate::{
9    print::Print,
10    signer::{self, LocalKey, Signer, SignerKind},
11    xdr::{self, SequenceNumber, Transaction, TransactionEnvelope},
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
27pub use address::UnresolvedMuxedAccount;
28pub use alias::UnresolvedContract;
29pub use sc_address::UnresolvedScAddress;
30
31#[derive(thiserror::Error, Debug)]
32pub enum Error {
33    #[error(transparent)]
34    Network(#[from] network::Error),
35    #[error(transparent)]
36    Secret(#[from] secret::Error),
37    #[error(transparent)]
38    Config(#[from] locator::Error),
39    #[error(transparent)]
40    Rpc(#[from] soroban_rpc::Error),
41    #[error(transparent)]
42    Signer(#[from] signer::Error),
43    #[error(transparent)]
44    StellarStrkey(#[from] stellar_strkey::DecodeError),
45    #[error(transparent)]
46    Address(#[from] address::Error),
47}
48
49#[derive(Debug, clap::Args, Clone, Default)]
50#[group(skip)]
51pub struct Args {
52    #[command(flatten)]
53    pub network: network::Args,
54
55    #[arg(long, short = 's', visible_alias = "source", env = "STELLAR_ACCOUNT")]
56    /// Account that where transaction originates from. Alias `source`.
57    /// Can be an identity (--source alice), a public key (--source GDKW...),
58    /// a muxed account (--source MDA…), a secret key (--source SC36…),
59    /// or a seed phrase (--source "kite urban…").
60    /// If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to
61    /// sign the final transaction. In that case, trying to sign with public key will fail.
62    pub source_account: UnresolvedMuxedAccount,
63
64    #[arg(long)]
65    /// If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0`
66    pub hd_path: Option<usize>,
67
68    #[command(flatten)]
69    pub locator: locator::Args,
70}
71
72impl Args {
73    // TODO: Replace PublicKey with MuxedAccount once https://github.com/stellar/rs-stellar-xdr/pull/396 is merged.
74    pub async fn source_account(&self) -> Result<xdr::MuxedAccount, Error> {
75        Ok(self
76            .source_account
77            .resolve_muxed_account(&self.locator, self.hd_path)
78            .await?)
79    }
80
81    pub fn key_pair(&self) -> Result<ed25519_dalek::SigningKey, Error> {
82        let key = &self.source_account.resolve_secret(&self.locator)?;
83        Ok(key.key_pair(self.hd_path)?)
84    }
85
86    pub async fn sign_with_local_key(&self, tx: Transaction) -> Result<TransactionEnvelope, Error> {
87        self.sign(tx).await
88    }
89
90    #[allow(clippy::unused_async)]
91    pub async fn sign(&self, tx: Transaction) -> Result<TransactionEnvelope, Error> {
92        let key = self.key_pair()?;
93        let network = &self.get_network()?;
94        let signer = Signer {
95            kind: SignerKind::Local(LocalKey { key }),
96            print: Print::new(false),
97        };
98        Ok(signer.sign_tx(tx, network).await?)
99    }
100
101    pub async fn sign_soroban_authorizations(
102        &self,
103        tx: &Transaction,
104        signers: &[ed25519_dalek::SigningKey],
105    ) -> Result<Option<Transaction>, Error> {
106        let network = self.get_network()?;
107        let source_key = self.key_pair()?;
108        let client = network.rpc_client()?;
109        let latest_ledger = client.get_latest_ledger().await?.sequence;
110        let seq_num = latest_ledger + 60; // ~ 5 min
111        Ok(signer::sign_soroban_authorizations(
112            tx,
113            &source_key,
114            signers,
115            seq_num,
116            &network.network_passphrase,
117        )?)
118    }
119
120    pub fn get_network(&self) -> Result<Network, Error> {
121        Ok(self.network.get(&self.locator)?)
122    }
123
124    pub async fn next_sequence_number(
125        &self,
126        account: impl Into<xdr::AccountId>,
127    ) -> Result<SequenceNumber, Error> {
128        let network = self.get_network()?;
129        let client = network.rpc_client()?;
130        Ok((client
131            .get_account(&account.into().to_string())
132            .await?
133            .seq_num
134            .0
135            + 1)
136        .into())
137    }
138}
139
140impl Pwd for Args {
141    fn set_pwd(&mut self, pwd: &std::path::Path) {
142        self.locator.set_pwd(pwd);
143    }
144}
145
146#[derive(Debug, clap::Args, Clone, Default)]
147#[group(skip)]
148pub struct ArgsLocatorAndNetwork {
149    #[command(flatten)]
150    pub network: network::Args,
151
152    #[command(flatten)]
153    pub locator: locator::Args,
154}
155
156impl ArgsLocatorAndNetwork {
157    pub fn get_network(&self) -> Result<Network, Error> {
158        Ok(self.network.get(&self.locator)?)
159    }
160}
161
162#[derive(Serialize, Deserialize, Debug, Default)]
163pub struct Config {
164    pub defaults: Defaults,
165}
166
167#[derive(Serialize, Deserialize, Debug, Default)]
168pub struct Defaults {
169    pub network: Option<String>,
170    pub identity: Option<String>,
171}
172
173impl Config {
174    pub fn new() -> Result<Config, locator::Error> {
175        let path = locator::config_file()?;
176
177        if path.exists() {
178            let data = fs::read_to_string(&path).map_err(|_| locator::Error::FileRead { path })?;
179            Ok(toml::from_str(&data)?)
180        } else {
181            Ok(Config::default())
182        }
183    }
184
185    #[must_use]
186    pub fn set_network(mut self, s: &str) -> Self {
187        self.defaults.network = Some(s.to_string());
188        self
189    }
190
191    #[must_use]
192    pub fn set_identity(mut self, s: &str) -> Self {
193        self.defaults.identity = Some(s.to_string());
194        self
195    }
196
197    pub fn save(&self) -> Result<(), locator::Error> {
198        let toml_string = toml::to_string(&self)?;
199        let path = locator::config_file()?;
200        // Depending on the platform, this function may fail if the full directory path does not exist
201        let mut file = File::create(locator::ensure_directory(path)?)?;
202        file.write_all(toml_string.as_bytes())?;
203
204        Ok(())
205    }
206}