Skip to main content

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