gmsol_cli/commands/
mod.rs

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