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
62pub mod utils;
64
65#[enum_dispatch(Command)]
67#[derive(Debug, clap::Subcommand)]
68pub enum Commands {
69 InitConfig(InitConfig),
71 Pubkey(GetPubkey),
73 Exchange(Box<Exchange>),
75 User(User),
77 Gt(Gt),
79 Alt(Alt),
81 Admin(Admin),
83 Timelock(Timelock),
85 Treasury(Treasury),
87 Market(Market),
89 Glv(Glv),
91 Configuration(Configuration),
93 Competition(Competition),
95 Inspect(Inspect),
97 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}