gmsol_cli/commands/
mod.rs

1use std::{collections::BTreeSet, ops::Deref, path::Path, sync::Arc};
2
3use admin::Admin;
4use alt::Alt;
5use competition::Competition;
6use configuration::Configuration;
7use either::Either;
8use enum_dispatch::enum_dispatch;
9use exchange::Exchange;
10use eyre::OptionExt;
11use get_pubkey::GetPubkey;
12use glv::Glv;
13use gmsol_sdk::{
14    ops::{AddressLookupTableOps, TimelockOps},
15    programs::anchor_lang::prelude::Pubkey,
16    solana_utils::{
17        bundle_builder::{Bundle, BundleBuilder, BundleOptions, SendBundleOptions},
18        instruction_group::{ComputeBudgetOptions, GetInstructionsOptions},
19        signer::LocalSignerRef,
20        solana_client::rpc_config::RpcSendTransactionConfig,
21        solana_sdk::{
22            message::VersionedMessage,
23            signature::{Keypair, NullSigner, Signature},
24            transaction::VersionedTransaction,
25        },
26        transaction_builder::default_before_sign,
27        utils::{inspect_transaction, WithSlot},
28    },
29    utils::instruction_serialization::{serialize_message, InstructionSerialization},
30    Client,
31};
32use gt::Gt;
33use init_config::InitConfig;
34
35use inspect::Inspect;
36use market::Market;
37use other::Other;
38#[cfg(feature = "remote-wallet")]
39use solana_remote_wallet::remote_wallet::RemoteWalletManager;
40use timelock::Timelock;
41use treasury::Treasury;
42use user::User;
43
44use crate::config::{Config, InstructionBuffer, Payer};
45
46mod admin;
47mod alt;
48mod competition;
49mod configuration;
50mod exchange;
51mod get_pubkey;
52mod glv;
53mod gt;
54mod init_config;
55mod inspect;
56mod market;
57mod other;
58mod timelock;
59mod treasury;
60mod user;
61
62/// Utils for command implementations.
63pub mod utils;
64
65/// Commands.
66#[enum_dispatch(Command)]
67#[derive(Debug, clap::Subcommand)]
68pub enum Commands {
69    /// Initialize config file.
70    InitConfig(InitConfig),
71    /// Get pubkey of the payer.
72    Pubkey(GetPubkey),
73    /// Exchange-related commands.
74    Exchange(Box<Exchange>),
75    /// User account commands.
76    User(User),
77    /// GT-related commands.
78    Gt(Gt),
79    /// Address Lookup Table commands.
80    Alt(Alt),
81    /// Administrative commands.
82    Admin(Admin),
83    /// Timelock commands.
84    Timelock(Timelock),
85    /// Treasury management commands.
86    Treasury(Treasury),
87    /// Market management commands.
88    Market(Market),
89    /// GLV management commands.
90    Glv(Glv),
91    /// On-chain configuration and features management.
92    Configuration(Configuration),
93    /// Competition management commands.
94    Competition(Competition),
95    /// Inspect protocol data.
96    Inspect(Inspect),
97    /// Miscellaneous useful commands.
98    Other(Other),
99}
100
101#[enum_dispatch]
102pub(crate) trait Command {
103    fn is_client_required(&self) -> bool {
104        false
105    }
106
107    async fn execute(&self, ctx: Context<'_>) -> eyre::Result<()>;
108}
109
110impl<T: Command> Command for Box<T> {
111    fn is_client_required(&self) -> bool {
112        (**self).is_client_required()
113    }
114
115    async fn execute(&self, ctx: Context<'_>) -> eyre::Result<()> {
116        (**self).execute(ctx).await
117    }
118}
119
120pub(crate) struct Context<'a> {
121    store: Pubkey,
122    config_path: &'a Path,
123    config: &'a Config,
124    client: Option<&'a CommandClient>,
125    _verbose: bool,
126}
127
128impl<'a> Context<'a> {
129    pub(super) fn new(
130        store: Pubkey,
131        config_path: &'a Path,
132        config: &'a Config,
133        client: Option<&'a CommandClient>,
134        verbose: bool,
135    ) -> Self {
136        Self {
137            store,
138            config_path,
139            config,
140            client,
141            _verbose: verbose,
142        }
143    }
144
145    pub(crate) fn config(&self) -> &Config {
146        self.config
147    }
148
149    pub(crate) fn client(&self) -> eyre::Result<&CommandClient> {
150        self.client.ok_or_eyre("client is not provided")
151    }
152
153    pub(crate) fn store(&self) -> &Pubkey {
154        &self.store
155    }
156
157    pub(crate) fn bundle_options(&self) -> BundleOptions {
158        self.config.bundle_options()
159    }
160
161    pub(crate) fn require_not_serialize_only_mode(&self) -> eyre::Result<()> {
162        let client = self.client()?;
163        if client.serialize_only.is_some() {
164            eyre::bail!("serialize-only mode is not supported");
165        } else {
166            Ok(())
167        }
168    }
169
170    pub(crate) fn require_not_ix_buffer_mode(&self) -> eyre::Result<()> {
171        let client = self.client()?;
172        if client.ix_buffer_ctx.is_some() {
173            eyre::bail!("instruction buffer is not supported");
174        } else {
175            Ok(())
176        }
177    }
178
179    pub(crate) fn _verbose(&self) -> bool {
180        self._verbose
181    }
182}
183
184struct IxBufferCtx<C> {
185    buffer: InstructionBuffer,
186    client: Client<C>,
187    is_draft: bool,
188}
189
190pub(crate) struct CommandClient {
191    store: Pubkey,
192    client: Client<LocalSignerRef>,
193    ix_buffer_ctx: Option<IxBufferCtx<LocalSignerRef>>,
194    serialize_only: Option<InstructionSerialization>,
195    verbose: bool,
196    priority_lamports: u64,
197    skip_preflight: bool,
198    luts: BTreeSet<Pubkey>,
199}
200
201impl CommandClient {
202    pub(crate) fn new(
203        config: &Config,
204        #[cfg(feature = "remote-wallet")] wallet_manager: &mut Option<
205            std::rc::Rc<RemoteWalletManager>,
206        >,
207        verbose: bool,
208    ) -> eyre::Result<Self> {
209        let Payer { payer, proposer } = config.create_wallet(
210            #[cfg(feature = "remote-wallet")]
211            Some(wallet_manager),
212        )?;
213
214        let cluster = config.cluster();
215        let options = config.options();
216        let client = Client::new_with_options(cluster.clone(), payer, options.clone())?;
217        let ix_buffer_client = proposer
218            .map(|payer| Client::new_with_options(cluster.clone(), payer, options))
219            .transpose()?;
220        let ix_buffer = config.ix_buffer()?;
221
222        Ok(Self {
223            store: config.store_address(),
224            client,
225            ix_buffer_ctx: ix_buffer_client.map(|client| {
226                let buffer = ix_buffer.expect("must be present");
227                IxBufferCtx {
228                    buffer,
229                    client,
230                    is_draft: false,
231                }
232            }),
233            serialize_only: config.serialize_only(),
234            verbose,
235            priority_lamports: config.priority_lamports()?,
236            skip_preflight: config.skip_preflight(),
237            luts: config.alts().copied().collect(),
238        })
239    }
240
241    pub(self) fn send_bundle_options(&self) -> SendBundleOptions {
242        SendBundleOptions {
243            compute_unit_min_priority_lamports: Some(self.priority_lamports),
244            config: RpcSendTransactionConfig {
245                skip_preflight: self.skip_preflight,
246                ..Default::default()
247            },
248            ..Default::default()
249        }
250    }
251
252    pub(crate) async fn send_or_serialize_with_callback(
253        &self,
254        mut bundle: BundleBuilder<'_, LocalSignerRef>,
255        callback: impl FnOnce(
256            Vec<WithSlot<Signature>>,
257            Option<gmsol_sdk::Error>,
258            usize,
259        ) -> gmsol_sdk::Result<()>,
260    ) -> gmsol_sdk::Result<()> {
261        let serialize_only = self.serialize_only;
262        let luts = bundle.luts_mut();
263        for lut in self.luts.iter() {
264            if !luts.contains_key(lut) {
265                if let Some(lut) = self.alt(lut).await? {
266                    luts.add(&lut);
267                }
268            }
269        }
270        let cache = luts.clone();
271        if let Some(format) = serialize_only {
272            println!("\n[Transactions]");
273            let txns = to_transactions(bundle.build()?)?;
274            for (idx, rpc) in txns.into_iter().enumerate() {
275                println!("TXN[{idx}]: {}", serialize_message(&rpc.message, format)?);
276            }
277        } else if let Some(IxBufferCtx {
278            buffer,
279            client,
280            is_draft,
281        }) = self.ix_buffer_ctx.as_ref()
282        {
283            let tg = bundle.build()?.into_group();
284            let ags = tg.groups().iter().flat_map(|pg| pg.iter());
285
286            let mut bundle = client.bundle();
287            bundle.luts_mut().extend(cache);
288            let len = tg.len();
289            let steps = len + 1;
290            for (txn_idx, txn) in ags.enumerate() {
291                match buffer {
292                    InstructionBuffer::Timelock { role } => {
293                        if *is_draft {
294                            tracing::warn!(
295                                "draft timelocked instruction buffer is not supported currently"
296                            );
297                        }
298
299                        tracing::info!("Creating instruction buffers for transaction {txn_idx}");
300
301                        for (idx, ix) in txn
302                            .instructions_with_options(GetInstructionsOptions {
303                                compute_budget: ComputeBudgetOptions {
304                                    without_compute_budget: true,
305                                    ..Default::default()
306                                },
307                                ..Default::default()
308                            })
309                            .enumerate()
310                        {
311                            let buffer = Keypair::new();
312                            let (rpc, buffer) = client
313                                .create_timelocked_instruction(
314                                    &self.store,
315                                    role,
316                                    buffer,
317                                    (*ix).clone(),
318                                )?
319                                .swap_output(());
320                            bundle.push(rpc)?;
321                            println!("ix[{idx}]: {buffer}");
322                        }
323                    }
324                    #[cfg(feature = "squads")]
325                    InstructionBuffer::Squads {
326                        multisig,
327                        vault_index,
328                    } => {
329                        use gmsol_sdk::client::squads::{SquadsOps, VaultTransactionOptions};
330                        use gmsol_sdk::solana_utils::utils::inspect_transaction;
331
332                        let luts = tg.luts();
333                        let message = txn.message_with_blockhash_and_options(
334                            Default::default(),
335                            GetInstructionsOptions {
336                                compute_budget: ComputeBudgetOptions {
337                                    without_compute_budget: true,
338                                    ..Default::default()
339                                },
340                                ..Default::default()
341                            },
342                            Some(luts),
343                        )?;
344
345                        let (rpc, transaction) = client
346                            .squads_create_vault_transaction_with_message(
347                                multisig,
348                                *vault_index,
349                                &message,
350                                VaultTransactionOptions {
351                                    draft: *is_draft,
352                                    ..Default::default()
353                                },
354                                Some(txn_idx as u64),
355                            )
356                            .await?
357                            .swap_output(());
358
359                        let txn_count = txn_idx + 1;
360                        println!("Adding a vault transaction {txn_idx}: id = {transaction}");
361                        println!(
362                            "Inspector URL for transaction {txn_idx}: {}",
363                            inspect_transaction(&message, Some(client.cluster()), false),
364                        );
365
366                        let confirmation = dialoguer::Confirm::new()
367                            .with_prompt(format!(
368                            "[{txn_count}/{steps}] Confirm to add vault transaction {txn_idx} ?"
369                        ))
370                            .default(false)
371                            .interact()
372                            .map_err(gmsol_sdk::Error::custom)?;
373
374                        if !confirmation {
375                            tracing::info!("Cancelled");
376                            return Ok(());
377                        }
378
379                        bundle.push(rpc)?;
380                    }
381                }
382            }
383
384            let confirmation = dialoguer::Confirm::new()
385                .with_prompt(format!(
386                    "[{steps}/{steps}] Confirm creation of {len} vault transactions?"
387                ))
388                .default(false)
389                .interact()
390                .map_err(gmsol_sdk::Error::custom)?;
391
392            if !confirmation {
393                tracing::info!("Cancelled");
394                return Ok(());
395            }
396            self.send_bundle_with_callback(bundle, callback).await?;
397        } else {
398            self.send_bundle_with_callback(bundle, callback).await?;
399        }
400        Ok(())
401    }
402
403    pub(crate) async fn send_or_serialize(
404        &self,
405        bundle: BundleBuilder<'_, LocalSignerRef>,
406    ) -> gmsol_sdk::Result<()> {
407        self.send_or_serialize_with_callback(bundle, display_signatures)
408            .await
409    }
410
411    #[cfg(feature = "squads")]
412    pub(crate) fn squads_ctx(&self) -> Option<(Pubkey, u8)> {
413        let ix_buffer_ctx = self.ix_buffer_ctx.as_ref()?;
414        if let InstructionBuffer::Squads {
415            multisig,
416            vault_index,
417        } = ix_buffer_ctx.buffer
418        {
419            Some((multisig, vault_index))
420        } else {
421            None
422        }
423    }
424
425    #[allow(dead_code)]
426    pub(crate) fn host_client(&self) -> &Client<LocalSignerRef> {
427        if let Some(ix_buffer_ctx) = self.ix_buffer_ctx.as_ref() {
428            &ix_buffer_ctx.client
429        } else {
430            &self.client
431        }
432    }
433
434    async fn send_bundle_with_callback(
435        &self,
436        bundle: BundleBuilder<'_, LocalSignerRef>,
437        callback: impl FnOnce(
438            Vec<WithSlot<Signature>>,
439            Option<gmsol_sdk::Error>,
440            usize,
441        ) -> gmsol_sdk::Result<()>,
442    ) -> gmsol_sdk::Result<()> {
443        let mut idx = 0;
444        let bundle = bundle.build()?;
445        let steps = bundle.len();
446        match bundle
447            .send_all_with_opts(self.send_bundle_options(), |m| {
448                before_sign(&mut idx, steps, self.verbose, m)
449            })
450            .await
451        {
452            Ok(signatures) => (callback)(signatures, None, steps)?,
453            Err((signatures, error)) => (callback)(signatures, Some(error.into()), steps)?,
454        }
455        Ok(())
456    }
457
458    #[allow(dead_code)]
459    pub(crate) async fn send_bundle(
460        &self,
461        bundle: BundleBuilder<'_, LocalSignerRef>,
462    ) -> gmsol_sdk::Result<()> {
463        self.send_bundle_with_callback(bundle, display_signatures)
464            .await
465    }
466}
467
468impl Deref for CommandClient {
469    type Target = Client<LocalSignerRef>;
470
471    fn deref(&self) -> &Self::Target {
472        &self.client
473    }
474}
475
476fn before_sign(
477    idx: &mut usize,
478    steps: usize,
479    verbose: bool,
480    message: &VersionedMessage,
481) -> Result<(), gmsol_sdk::SolanaUtilsError> {
482    use gmsol_sdk::solana_utils::solana_sdk::hash::hash;
483    println!(
484        "[{}/{steps}] Signing transaction {idx}: hash = {}{}",
485        *idx + 1,
486        hash(&message.serialize()),
487        if verbose {
488            format!(", message = {}", inspect_transaction(message, None, true))
489        } else {
490            String::new()
491        }
492    );
493    *idx += 1;
494
495    Ok(())
496}
497
498fn display_signatures(
499    signatures: Vec<WithSlot<Signature>>,
500    err: Option<gmsol_sdk::Error>,
501    steps: usize,
502) -> gmsol_sdk::Result<()> {
503    let failed_start = signatures.len();
504    let failed = steps.saturating_sub(signatures.len());
505    for (idx, signature) in signatures.into_iter().enumerate() {
506        println!("Transaction {idx}: signature = {}", signature.value());
507    }
508    for idx in 0..failed {
509        println!("Transaction {}: failed", idx + failed_start);
510    }
511    match err {
512        None => Ok(()),
513        Some(err) => Err(err),
514    }
515}
516
517fn to_transactions(
518    bundle: Bundle<'_, LocalSignerRef>,
519) -> gmsol_sdk::Result<Vec<VersionedTransaction>> {
520    let bundle = bundle.into_group();
521    bundle
522        .to_transactions_with_options::<Arc<NullSigner>, _>(
523            &Default::default(),
524            Default::default(),
525            true,
526            ComputeBudgetOptions {
527                without_compute_budget: true,
528                ..Default::default()
529            },
530            default_before_sign,
531        )
532        .flat_map(|txns| match txns {
533            Ok(txns) => Either::Left(txns.into_iter().map(Ok)),
534            Err(err) => Either::Right(std::iter::once(Err(err.into()))),
535        })
536        .collect()
537}