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