Skip to main content

gmsol_cli/config/
mod.rs

1mod output;
2mod store_address;
3
4use std::num::NonZeroUsize;
5
6use eyre::OptionExt;
7use gmsol_sdk::{
8    client::ClientOptions,
9    pda,
10    programs::anchor_lang::prelude::Pubkey,
11    serde::StringPubkey,
12    solana_utils::{
13        bundle_builder::{BundleOptions, DEFAULT_MAX_INSTRUCTIONS_FOR_ONE_TX},
14        cluster::Cluster,
15        compute_budget::ComputeBudget,
16        signer::{local_signer, LocalSignerRef},
17        solana_sdk::{
18            commitment_config::{CommitmentConfig, CommitmentLevel},
19            signature::NullSigner,
20        },
21    },
22    utils::{instruction_serialization::InstructionSerialization, Lamport},
23};
24use store_address::StoreAddress;
25
26use crate::wallet::signer_from_source;
27
28pub use output::{DisplayOptions, OutputFormat};
29
30cfg_if::cfg_if! {
31    if #[cfg(feature = "devnet")] {
32        const DEFAULT_CLUSTER: Cluster = Cluster::Devnet;
33    } else {
34        const DEFAULT_CLUSTER: Cluster = Cluster::Mainnet;
35    }
36}
37
38const DEFAULT_WALLET: &str = "~/.config/solana/id.json";
39
40const DEFAULT_COMMITMENT: CommitmentLevel = CommitmentLevel::Confirmed;
41
42/// Configuration.
43#[derive(Debug, clap::Args, serde::Serialize, serde::Deserialize, Clone, Default)]
44pub struct Config {
45    /// Output format.
46    #[arg(long, global = true)]
47    output: Option<OutputFormat>,
48    /// Path to the wallet.
49    #[arg(long, short = 'k', global = true)]
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    wallet: Option<String>,
52    /// Cluster to connect to.
53    #[arg(long = "url", short = 'u', global = true)]
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    cluster: Option<Cluster>,
56    /// Commitment level.
57    #[arg(long, global = true)]
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    commitment: Option<CommitmentLevel>,
60    /// Store address.
61    #[command(flatten)]
62    #[serde(flatten)]
63    store_address: StoreAddress,
64    /// Store Program ID.
65    #[arg(long)]
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    store_program: Option<StringPubkey>,
68    /// Treasury Program ID.
69    #[arg(long)]
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    treasury_program: Option<StringPubkey>,
72    /// Timelock Program ID.
73    #[arg(long)]
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    timelock_program: Option<StringPubkey>,
76    /// Liquidity Provider Program ID.
77    #[arg(long)]
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    liquidity_provider_program: Option<StringPubkey>,
80    /// Print the serialized instructions,
81    /// instead of sending the transaction.
82    #[arg(long, global = true, default_missing_value = "base64", num_args=0..=1, group = "tx-opts")]
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    serialize_only: Option<InstructionSerialization>,
85    /// Whether to skip preflight.
86    #[arg(long, global = true, group = "tx-opts")]
87    skip_preflight: bool,
88    /// Use this address as payer.
89    ///
90    /// Only available in `serialize-only` mode.
91    #[arg(long, requires = "serialize_only", global = true)]
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    payer: Option<StringPubkey>,
94    /// Provides to create as timelocked instruction buffers.
95    #[arg(long, group = "ix-buffer", global = true)]
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    timelock: Option<String>,
98    /// Provides to create as a Squads vault transaction.
99    #[cfg(feature = "squads")]
100    #[arg(long, group = "ix-buffer", global = true)]
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    squads: Option<String>,
103    /// ALTs.
104    #[arg(long, short = 't', global = true)]
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    alts: Option<Vec<StringPubkey>>,
107    /// Oracle buffer to use.
108    #[arg(long, global = true)]
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    oracle: Option<StringPubkey>,
111    /// Max transaction size.
112    #[arg(long, global = true)]
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    max_transaction_size: Option<usize>,
115    /// Force one transaction.
116    #[arg(long, global = true)]
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    force_one_tx: Option<bool>,
119    /// Max instructions per transaction.
120    #[arg(long, global = true)]
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    max_transaction_instructions: Option<NonZeroUsize>,
123    /// Priority fee lamports.
124    #[arg(long, global = true)]
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    priority_lamports: Option<Lamport>,
127    /// Chaos Labs Risk Oracle configuration.
128    /// Not a CLI flag; only loaded from config file and env.
129    #[arg(skip)]
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    chaos: Option<ChaosConfig>,
132}
133
134impl Config {
135    /// Creates a wallet based on the config.
136    pub fn wallet(&self) -> eyre::Result<Payer> {
137        cfg_if::cfg_if! {
138            if #[cfg(feature = "remote-wallet")] {
139                self.create_wallet(None)
140            } else {
141                self.create_wallet()
142            }
143        }
144    }
145
146    /// Creates a wallet based on the config.
147    /// Supports remote wallets.
148    #[cfg(feature = "remote-wallet")]
149    pub fn wallet_with_remote_support(
150        &self,
151        wallet_manager: &mut Option<
152            std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>,
153        >,
154    ) -> eyre::Result<Payer> {
155        self.create_wallet(Some(wallet_manager))
156    }
157
158    pub(crate) fn create_wallet(
159        &self,
160        #[cfg(feature = "remote-wallet")] wallet_manager: Option<
161            &mut Option<std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>>,
162        >,
163    ) -> eyre::Result<Payer> {
164        if let Some(payer) = self.payer {
165            if self.serialize_only.is_some() {
166                let payer = NullSigner::new(&payer);
167                Ok(Payer::new(local_signer(payer)))
168            } else {
169                eyre::bail!("Setting payer is only allowed in `serialize-only` mode");
170            }
171        } else {
172            let wallet = signer_from_source(
173                self.wallet.as_deref().unwrap_or(DEFAULT_WALLET),
174                #[cfg(feature = "remote-wallet")]
175                false,
176                #[cfg(feature = "remote-wallet")]
177                "keypair",
178                #[cfg(feature = "remote-wallet")]
179                wallet_manager,
180            )?;
181
182            if let Some(role) = self.timelock.as_ref() {
183                let store = self.store_address();
184                let timelock_program_id = self.timelock_program_id();
185                let executor = pda::find_executor_address(
186                    &store,
187                    role,
188                    self.timelock_program
189                        .as_deref()
190                        .unwrap_or(timelock_program_id),
191                )?
192                .0;
193                let executor_wallet =
194                    pda::find_executor_wallet_address(&executor, timelock_program_id).0;
195
196                let payer = NullSigner::new(&executor_wallet);
197
198                return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
199            }
200
201            #[cfg(feature = "squads")]
202            if let Some(squads) = self.squads.as_ref() {
203                let (multisig, vault_index) = parse_squads(squads)?;
204                let vault_pda = gmsol_sdk::squads::get_vault_pda(&multisig, vault_index, None).0;
205
206                let payer = NullSigner::new(&vault_pda);
207
208                return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
209            }
210
211            Ok(Payer::new(wallet))
212        }
213    }
214
215    /// Returns the cluster.
216    pub fn cluster(&self) -> &Cluster {
217        self.cluster.as_ref().unwrap_or(&DEFAULT_CLUSTER)
218    }
219
220    /// Returns the client options.
221    pub fn options(&self) -> ClientOptions {
222        ClientOptions::builder()
223            .commitment(CommitmentConfig {
224                commitment: self.commitment.unwrap_or(DEFAULT_COMMITMENT),
225            })
226            .store_program_id(Some(*self.store_program_id()))
227            .treasury_program_id(Some(*self.treasury_program_id()))
228            .timelock_program_id(Some(*self.timelock_program_id()))
229            .liquidity_provider_program_id(Some(*self.liquidity_provider_program_id()))
230            .build()
231    }
232
233    /// Returns the program ID of store program.
234    pub fn store_program_id(&self) -> &Pubkey {
235        self.store_program
236            .as_deref()
237            .unwrap_or(&gmsol_sdk::programs::gmsol_store::ID)
238    }
239
240    /// Returns the program ID of treasury program.
241    pub fn treasury_program_id(&self) -> &Pubkey {
242        self.treasury_program
243            .as_deref()
244            .unwrap_or(&gmsol_sdk::programs::gmsol_treasury::ID)
245    }
246
247    /// Returns the program ID of timelock program.
248    pub fn timelock_program_id(&self) -> &Pubkey {
249        self.timelock_program
250            .as_deref()
251            .unwrap_or(&gmsol_sdk::programs::gmsol_timelock::ID)
252    }
253
254    /// Returns the program ID of liquidity provider program.
255    pub fn liquidity_provider_program_id(&self) -> &Pubkey {
256        self.liquidity_provider_program
257            .as_deref()
258            .unwrap_or(&gmsol_sdk::programs::gmsol_liquidity_provider::ID)
259    }
260
261    /// Returns the address of the store account.
262    pub fn store_address(&self) -> Pubkey {
263        self.store_address.address(self.store_program_id())
264    }
265
266    /// Returns serialize-only option.
267    pub fn serialize_only(&self) -> Option<InstructionSerialization> {
268        self.serialize_only
269    }
270
271    /// Returns instruction buffer.
272    pub fn ix_buffer(&self) -> eyre::Result<Option<InstructionBuffer>> {
273        if let Some(role) = self.timelock.as_ref() {
274            return Ok(Some(InstructionBuffer::Timelock { role: role.clone() }));
275        }
276
277        #[cfg(feature = "squads")]
278        if let Some(squads) = self.squads.as_ref() {
279            let (multisig, vault_index) = parse_squads(squads)?;
280            return Ok(Some(InstructionBuffer::Squads {
281                multisig,
282                vault_index,
283            }));
284        }
285
286        Ok(None)
287    }
288
289    /// Get oracle buffer address.
290    pub fn oracle(&self) -> eyre::Result<&Pubkey> {
291        self.oracle
292            .as_deref()
293            .ok_or_eyre("oracle buffer address is not provided")
294    }
295
296    /// Get address lookup tables.
297    pub fn alts(&self) -> impl Iterator<Item = &Pubkey> {
298        self.alts.iter().flat_map(|alts| alts.iter().map(|p| &p.0))
299    }
300
301    /// Get output format.
302    pub fn output(&self) -> OutputFormat {
303        self.output.unwrap_or_default()
304    }
305
306    /// Get bundle options.
307    pub fn bundle_options(&self) -> BundleOptions {
308        BundleOptions {
309            force_one_transaction: self.force_one_tx.unwrap_or(false),
310            max_packet_size: self.max_transaction_size,
311            max_instructions_for_one_tx: self
312                .max_transaction_instructions
313                .map(|m| m.get())
314                .unwrap_or(DEFAULT_MAX_INSTRUCTIONS_FOR_ONE_TX),
315        }
316    }
317
318    /// Get priority lamports.
319    pub fn priority_lamports(&self) -> eyre::Result<u64> {
320        Ok(self
321            .priority_lamports
322            .map(|a| a.to_u64())
323            .transpose()?
324            .unwrap_or(ComputeBudget::DEFAULT_MIN_PRIORITY_LAMPORTS))
325    }
326
327    /// Returns whether the transaction preflight test should be skipped.
328    pub fn skip_preflight(&self) -> bool {
329        self.skip_preflight
330    }
331
332    pub fn chaos_base_url(&self) -> String {
333        if let Ok(v) = std::env::var("CHAOS_BASE_URL") {
334            return v;
335        }
336        self.chaos
337            .as_ref()
338            .and_then(|c| c.base_url.clone())
339            .unwrap_or_else(|| "https://oracle.chaoslabs.co".to_string())
340    }
341
342    pub fn chaos_api_key(&self) -> Option<String> {
343        if let Ok(v) = std::env::var("CHAOS_API_KEY") {
344            return Some(v);
345        }
346        self.chaos.as_ref().and_then(|c| c.api_key.clone())
347    }
348
349    pub fn chaos_signer_strict(&self) -> eyre::Result<Option<Pubkey>> {
350        if let Ok(v) = std::env::var("RISK_ORACLE_SIGNER") {
351            let pk: Pubkey = v
352                .parse()
353                .map_err(|_| eyre::eyre!("invalid RISK_ORACLE_SIGNER: {v}"))?;
354            return Ok(Some(pk));
355        }
356        Ok(self
357            .chaos
358            .as_ref()
359            .and_then(|c| c.signer.as_ref().map(|s| s.0)))
360    }
361}
362
363#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
364pub struct ChaosConfig {
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub base_url: Option<String>,
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub api_key: Option<String>,
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub signer: Option<StringPubkey>,
371}
372
373#[cfg(feature = "squads")]
374pub(crate) fn parse_squads(data: &str) -> eyre::Result<(Pubkey, u8)> {
375    let (multisig, vault_index) = match data.split_once(':') {
376        Some((multisig, vault_index)) => (multisig, vault_index.parse()?),
377        None => (data, 0),
378    };
379    Ok((multisig.parse()?, vault_index))
380}
381
382/// Represents the entities involved in signing a transaction,
383/// including the primary payer and an optional proposer.
384#[derive(Debug, Clone)]
385pub struct Payer {
386    /// Payer.
387    pub payer: LocalSignerRef,
388    /// Proposer.
389    pub proposer: Option<LocalSignerRef>,
390}
391
392impl Payer {
393    fn with_proposer(payer: LocalSignerRef, proposer: Option<LocalSignerRef>) -> Self {
394        Self { payer, proposer }
395    }
396
397    fn new(payer: LocalSignerRef) -> Self {
398        Self::with_proposer(payer, None)
399    }
400}
401
402/// Instruction Buffer.
403pub enum InstructionBuffer {
404    /// Timelock instruction buffer.
405    Timelock { role: String },
406    /// Squads instruction buffer.
407    #[cfg(feature = "squads")]
408    Squads { multisig: Pubkey, vault_index: u8 },
409}