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}
128
129impl Config {
130    /// Creates a wallet based on the config.
131    pub fn wallet(&self) -> eyre::Result<Payer> {
132        cfg_if::cfg_if! {
133            if #[cfg(feature = "remote-wallet")] {
134                self.create_wallet(None)
135            } else {
136                self.create_wallet()
137            }
138        }
139    }
140
141    /// Creates a wallet based on the config.
142    /// Supports remote wallets.
143    #[cfg(feature = "remote-wallet")]
144    pub fn wallet_with_remote_support(
145        &self,
146        wallet_manager: &mut Option<
147            std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>,
148        >,
149    ) -> eyre::Result<Payer> {
150        self.create_wallet(Some(wallet_manager))
151    }
152
153    pub(crate) fn create_wallet(
154        &self,
155        #[cfg(feature = "remote-wallet")] wallet_manager: Option<
156            &mut Option<std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>>,
157        >,
158    ) -> eyre::Result<Payer> {
159        if let Some(payer) = self.payer {
160            if self.serialize_only.is_some() {
161                let payer = NullSigner::new(&payer);
162                Ok(Payer::new(local_signer(payer)))
163            } else {
164                eyre::bail!("Setting payer is only allowed in `serialize-only` mode");
165            }
166        } else {
167            let wallet = signer_from_source(
168                self.wallet.as_deref().unwrap_or(DEFAULT_WALLET),
169                #[cfg(feature = "remote-wallet")]
170                false,
171                #[cfg(feature = "remote-wallet")]
172                "keypair",
173                #[cfg(feature = "remote-wallet")]
174                wallet_manager,
175            )?;
176
177            if let Some(role) = self.timelock.as_ref() {
178                let store = self.store_address();
179                let timelock_program_id = self.timelock_program_id();
180                let executor = pda::find_executor_address(
181                    &store,
182                    role,
183                    self.timelock_program
184                        .as_deref()
185                        .unwrap_or(timelock_program_id),
186                )?
187                .0;
188                let executor_wallet =
189                    pda::find_executor_wallet_address(&executor, timelock_program_id).0;
190
191                let payer = NullSigner::new(&executor_wallet);
192
193                return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
194            }
195
196            #[cfg(feature = "squads")]
197            if let Some(squads) = self.squads.as_ref() {
198                let (multisig, vault_index) = parse_squads(squads)?;
199                let vault_pda = gmsol_sdk::squads::get_vault_pda(&multisig, vault_index, None).0;
200
201                let payer = NullSigner::new(&vault_pda);
202
203                return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
204            }
205
206            Ok(Payer::new(wallet))
207        }
208    }
209
210    /// Returns the cluster.
211    pub fn cluster(&self) -> &Cluster {
212        self.cluster.as_ref().unwrap_or(&DEFAULT_CLUSTER)
213    }
214
215    /// Returns the client options.
216    pub fn options(&self) -> ClientOptions {
217        ClientOptions::builder()
218            .commitment(CommitmentConfig {
219                commitment: self.commitment.unwrap_or(DEFAULT_COMMITMENT),
220            })
221            .store_program_id(Some(*self.store_program_id()))
222            .treasury_program_id(Some(*self.treasury_program_id()))
223            .timelock_program_id(Some(*self.timelock_program_id()))
224            .liquidity_provider_program_id(Some(*self.liquidity_provider_program_id()))
225            .build()
226    }
227
228    /// Returns the program ID of store program.
229    pub fn store_program_id(&self) -> &Pubkey {
230        self.store_program
231            .as_deref()
232            .unwrap_or(&gmsol_sdk::programs::gmsol_store::ID)
233    }
234
235    /// Returns the program ID of treasury program.
236    pub fn treasury_program_id(&self) -> &Pubkey {
237        self.treasury_program
238            .as_deref()
239            .unwrap_or(&gmsol_sdk::programs::gmsol_treasury::ID)
240    }
241
242    /// Returns the program ID of timelock program.
243    pub fn timelock_program_id(&self) -> &Pubkey {
244        self.timelock_program
245            .as_deref()
246            .unwrap_or(&gmsol_sdk::programs::gmsol_timelock::ID)
247    }
248
249    /// Returns the program ID of liquidity provider program.
250    pub fn liquidity_provider_program_id(&self) -> &Pubkey {
251        self.liquidity_provider_program
252            .as_deref()
253            .unwrap_or(&gmsol_sdk::programs::gmsol_liquidity_provider::ID)
254    }
255
256    /// Returns the address of the store account.
257    pub fn store_address(&self) -> Pubkey {
258        self.store_address.address(self.store_program_id())
259    }
260
261    /// Returns serialize-only option.
262    pub fn serialize_only(&self) -> Option<InstructionSerialization> {
263        self.serialize_only
264    }
265
266    /// Returns instruction buffer.
267    pub fn ix_buffer(&self) -> eyre::Result<Option<InstructionBuffer>> {
268        if let Some(role) = self.timelock.as_ref() {
269            return Ok(Some(InstructionBuffer::Timelock { role: role.clone() }));
270        }
271
272        #[cfg(feature = "squads")]
273        if let Some(squads) = self.squads.as_ref() {
274            let (multisig, vault_index) = parse_squads(squads)?;
275            return Ok(Some(InstructionBuffer::Squads {
276                multisig,
277                vault_index,
278            }));
279        }
280
281        Ok(None)
282    }
283
284    /// Get oracle buffer address.
285    pub fn oracle(&self) -> eyre::Result<&Pubkey> {
286        self.oracle
287            .as_deref()
288            .ok_or_eyre("oracle buffer address is not provided")
289    }
290
291    /// Get address lookup tables.
292    pub fn alts(&self) -> impl Iterator<Item = &Pubkey> {
293        self.alts.iter().flat_map(|alts| alts.iter().map(|p| &p.0))
294    }
295
296    /// Get output format.
297    pub fn output(&self) -> OutputFormat {
298        self.output.unwrap_or_default()
299    }
300
301    /// Get bundle options.
302    pub fn bundle_options(&self) -> BundleOptions {
303        BundleOptions {
304            force_one_transaction: self.force_one_tx.unwrap_or(false),
305            max_packet_size: self.max_transaction_size,
306            max_instructions_for_one_tx: self
307                .max_transaction_instructions
308                .map(|m| m.get())
309                .unwrap_or(DEFAULT_MAX_INSTRUCTIONS_FOR_ONE_TX),
310        }
311    }
312
313    /// Get priority lamports.
314    pub fn priority_lamports(&self) -> eyre::Result<u64> {
315        Ok(self
316            .priority_lamports
317            .map(|a| a.to_u64())
318            .transpose()?
319            .unwrap_or(ComputeBudget::DEFAULT_MIN_PRIORITY_LAMPORTS))
320    }
321
322    /// Returns whether the transaction preflight test should be skipped.
323    pub fn skip_preflight(&self) -> bool {
324        self.skip_preflight
325    }
326}
327
328#[cfg(feature = "squads")]
329pub(crate) fn parse_squads(data: &str) -> eyre::Result<(Pubkey, u8)> {
330    let (multisig, vault_index) = match data.split_once(':') {
331        Some((multisig, vault_index)) => (multisig, vault_index.parse()?),
332        None => (data, 0),
333    };
334    Ok((multisig.parse()?, vault_index))
335}
336
337/// Represents the entities involved in signing a transaction,
338/// including the primary payer and an optional proposer.
339#[derive(Debug, Clone)]
340pub struct Payer {
341    /// Payer.
342    pub payer: LocalSignerRef,
343    /// Proposer.
344    pub proposer: Option<LocalSignerRef>,
345}
346
347impl Payer {
348    fn with_proposer(payer: LocalSignerRef, proposer: Option<LocalSignerRef>) -> Self {
349        Self { payer, proposer }
350    }
351
352    fn new(payer: LocalSignerRef) -> Self {
353        Self::with_proposer(payer, None)
354    }
355}
356
357/// Instruction Buffer.
358pub enum InstructionBuffer {
359    /// Timelock instruction buffer.
360    Timelock { role: String },
361    /// Squads instruction buffer.
362    #[cfg(feature = "squads")]
363    Squads { multisig: Pubkey, vault_index: u8 },
364}