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