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    /// Print the serialized instructions,
77    /// instead of sending the transaction.
78    #[arg(long, global = true, default_missing_value = "base64", num_args=0..=1, group = "tx-opts")]
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    serialize_only: Option<InstructionSerialization>,
81    /// Whether to skip preflight.
82    #[arg(long, global = true, group = "tx-opts")]
83    skip_preflight: bool,
84    /// Use this address as payer.
85    ///
86    /// Only available in `serialize-only` mode.
87    #[arg(long, requires = "serialize_only", global = true)]
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    payer: Option<StringPubkey>,
90    /// Provides to create as timelocked instruction buffers.
91    #[arg(long, group = "ix-buffer", global = true)]
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    timelock: Option<String>,
94    /// Provides to create as a Squads vault transaction.
95    #[cfg(feature = "squads")]
96    #[arg(long, group = "ix-buffer", global = true)]
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    squads: Option<String>,
99    /// ALTs.
100    #[arg(long, short = 't', global = true)]
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    alts: Option<Vec<StringPubkey>>,
103    /// Oracle buffer to use.
104    #[arg(long, global = true)]
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    oracle: Option<StringPubkey>,
107    /// Max transaction size.
108    #[arg(long, global = true)]
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    max_transaction_size: Option<usize>,
111    /// Force one transaction.
112    #[arg(long, global = true)]
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    force_one_tx: Option<bool>,
115    /// Max instructions per transaction.
116    #[arg(long, global = true)]
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    max_transaction_instructions: Option<NonZeroUsize>,
119    /// Priority fee lamports.
120    #[arg(long, global = true)]
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    priority_lamports: Option<Lamport>,
123}
124
125impl Config {
126    /// Creates a wallet based on the config.
127    pub fn wallet(&self) -> eyre::Result<Payer> {
128        cfg_if::cfg_if! {
129            if #[cfg(feature = "remote-wallet")] {
130                self.create_wallet(None)
131            } else {
132                self.create_wallet()
133            }
134        }
135    }
136
137    /// Creates a wallet based on the config.
138    /// Supports remote wallets.
139    #[cfg(feature = "remote-wallet")]
140    pub fn wallet_with_remote_support(
141        &self,
142        wallet_manager: &mut Option<
143            std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>,
144        >,
145    ) -> eyre::Result<Payer> {
146        self.create_wallet(Some(wallet_manager))
147    }
148
149    pub(crate) fn create_wallet(
150        &self,
151        #[cfg(feature = "remote-wallet")] wallet_manager: Option<
152            &mut Option<std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>>,
153        >,
154    ) -> eyre::Result<Payer> {
155        if let Some(payer) = self.payer {
156            if self.serialize_only.is_some() {
157                let payer = NullSigner::new(&payer);
158                Ok(Payer::new(local_signer(payer)))
159            } else {
160                eyre::bail!("Setting payer is only allowed in `serialize-only` mode");
161            }
162        } else {
163            let wallet = signer_from_source(
164                self.wallet.as_deref().unwrap_or(DEFAULT_WALLET),
165                #[cfg(feature = "remote-wallet")]
166                false,
167                #[cfg(feature = "remote-wallet")]
168                "keypair",
169                #[cfg(feature = "remote-wallet")]
170                wallet_manager,
171            )?;
172
173            if let Some(role) = self.timelock.as_ref() {
174                let store = self.store_address();
175                let timelock_program_id = self.timelock_program_id();
176                let executor = pda::find_executor_address(
177                    &store,
178                    role,
179                    self.timelock_program
180                        .as_deref()
181                        .unwrap_or(timelock_program_id),
182                )?
183                .0;
184                let executor_wallet =
185                    pda::find_executor_wallet_address(&executor, timelock_program_id).0;
186
187                let payer = NullSigner::new(&executor_wallet);
188
189                return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
190            }
191
192            #[cfg(feature = "squads")]
193            if let Some(squads) = self.squads.as_ref() {
194                let (multisig, vault_index) = parse_squads(squads)?;
195                let vault_pda = gmsol_sdk::squads::get_vault_pda(&multisig, vault_index, None).0;
196
197                let payer = NullSigner::new(&vault_pda);
198
199                return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
200            }
201
202            Ok(Payer::new(wallet))
203        }
204    }
205
206    /// Returns the cluster.
207    pub fn cluster(&self) -> &Cluster {
208        self.cluster.as_ref().unwrap_or(&DEFAULT_CLUSTER)
209    }
210
211    /// Returns the client options.
212    pub fn options(&self) -> ClientOptions {
213        ClientOptions::builder()
214            .commitment(CommitmentConfig {
215                commitment: self.commitment.unwrap_or(DEFAULT_COMMITMENT),
216            })
217            .store_program_id(Some(*self.store_program_id()))
218            .treasury_program_id(Some(*self.treasury_program_id()))
219            .timelock_program_id(Some(*self.timelock_program_id()))
220            .build()
221    }
222
223    /// Returns the program ID of store program.
224    pub fn store_program_id(&self) -> &Pubkey {
225        self.store_program
226            .as_deref()
227            .unwrap_or(&gmsol_sdk::programs::gmsol_store::ID)
228    }
229
230    /// Returns the program ID of treasury program.
231    pub fn treasury_program_id(&self) -> &Pubkey {
232        self.treasury_program
233            .as_deref()
234            .unwrap_or(&gmsol_sdk::programs::gmsol_treasury::ID)
235    }
236
237    /// Returns the program ID of timelock program.
238    pub fn timelock_program_id(&self) -> &Pubkey {
239        self.timelock_program
240            .as_deref()
241            .unwrap_or(&gmsol_sdk::programs::gmsol_timelock::ID)
242    }
243
244    /// Returns the address of the store account.
245    pub fn store_address(&self) -> Pubkey {
246        self.store_address.address(self.store_program_id())
247    }
248
249    /// Returns serialize-only option.
250    pub fn serialize_only(&self) -> Option<InstructionSerialization> {
251        self.serialize_only
252    }
253
254    /// Returns instruction buffer.
255    pub fn ix_buffer(&self) -> eyre::Result<Option<InstructionBuffer>> {
256        if let Some(role) = self.timelock.as_ref() {
257            return Ok(Some(InstructionBuffer::Timelock { role: role.clone() }));
258        }
259
260        #[cfg(feature = "squads")]
261        if let Some(squads) = self.squads.as_ref() {
262            let (multisig, vault_index) = parse_squads(squads)?;
263            return Ok(Some(InstructionBuffer::Squads {
264                multisig,
265                vault_index,
266            }));
267        }
268
269        Ok(None)
270    }
271
272    /// Get oracle buffer address.
273    pub fn oracle(&self) -> eyre::Result<&Pubkey> {
274        self.oracle
275            .as_deref()
276            .ok_or_eyre("oracle buffer address is not provided")
277    }
278
279    /// Get address lookup tables.
280    pub fn alts(&self) -> impl Iterator<Item = &Pubkey> {
281        self.alts.iter().flat_map(|alts| alts.iter().map(|p| &p.0))
282    }
283
284    /// Get output format.
285    pub fn output(&self) -> OutputFormat {
286        self.output.unwrap_or_default()
287    }
288
289    /// Get bundle options.
290    pub fn bundle_options(&self) -> BundleOptions {
291        BundleOptions {
292            force_one_transaction: self.force_one_tx.unwrap_or(false),
293            max_packet_size: self.max_transaction_size,
294            max_instructions_for_one_tx: self
295                .max_transaction_instructions
296                .map(|m| m.get())
297                .unwrap_or(DEFAULT_MAX_INSTRUCTIONS_FOR_ONE_TX),
298        }
299    }
300
301    /// Get priority lamports.
302    pub fn priority_lamports(&self) -> eyre::Result<u64> {
303        Ok(self
304            .priority_lamports
305            .map(|a| a.to_u64())
306            .transpose()?
307            .unwrap_or(ComputeBudget::DEFAULT_MIN_PRIORITY_LAMPORTS))
308    }
309
310    /// Returns whether the transaction preflight test should be skipped.
311    pub fn skip_preflight(&self) -> bool {
312        self.skip_preflight
313    }
314}
315
316#[cfg(feature = "squads")]
317pub(crate) fn parse_squads(data: &str) -> eyre::Result<(Pubkey, u8)> {
318    let (multisig, vault_index) = match data.split_once(':') {
319        Some((multisig, vault_index)) => (multisig, vault_index.parse()?),
320        None => (data, 0),
321    };
322    Ok((multisig.parse()?, vault_index))
323}
324
325/// Represents the entities involved in signing a transaction,
326/// including the primary payer and an optional proposer.
327#[derive(Debug, Clone)]
328pub struct Payer {
329    /// Payer.
330    pub payer: LocalSignerRef,
331    /// Proposer.
332    pub proposer: Option<LocalSignerRef>,
333}
334
335impl Payer {
336    fn with_proposer(payer: LocalSignerRef, proposer: Option<LocalSignerRef>) -> Self {
337        Self { payer, proposer }
338    }
339
340    fn new(payer: LocalSignerRef) -> Self {
341        Self::with_proposer(payer, None)
342    }
343}
344
345/// Instruction Buffer.
346pub enum InstructionBuffer {
347    /// Timelock instruction buffer.
348    Timelock { role: String },
349    /// Squads instruction buffer.
350    #[cfg(feature = "squads")]
351    Squads { multisig: Pubkey, vault_index: u8 },
352}