1mod output;
2mod store_address;
3
4use std::num::NonZeroUsize;
5
6use eyre::OptionExt;
7use gmsol_sdk::{
8 client::ClientOptions,
9 pda,
10 programs::anchor_lang::prelude::Pubkey,
11 serde::StringPubkey,
12 solana_utils::{
13 bundle_builder::{BundleOptions, DEFAULT_MAX_INSTRUCTIONS_FOR_ONE_TX},
14 cluster::Cluster,
15 compute_budget::ComputeBudget,
16 signer::{local_signer, LocalSignerRef},
17 solana_sdk::{
18 commitment_config::{CommitmentConfig, CommitmentLevel},
19 signature::NullSigner,
20 },
21 },
22 utils::{instruction_serialization::InstructionSerialization, Lamport},
23};
24use store_address::StoreAddress;
25
26use crate::wallet::signer_from_source;
27
28pub use output::{DisplayOptions, OutputFormat};
29
30cfg_if::cfg_if! {
31 if #[cfg(feature = "devnet")] {
32 const DEFAULT_CLUSTER: Cluster = Cluster::Devnet;
33 } else {
34 const DEFAULT_CLUSTER: Cluster = Cluster::Mainnet;
35 }
36}
37
38const DEFAULT_WALLET: &str = "~/.config/solana/id.json";
39
40const DEFAULT_COMMITMENT: CommitmentLevel = CommitmentLevel::Confirmed;
41
42#[derive(Debug, clap::Args, serde::Serialize, serde::Deserialize, Clone, Default)]
44pub struct Config {
45 #[arg(long, global = true)]
47 output: Option<OutputFormat>,
48 #[arg(long, short = 'k', global = true)]
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 wallet: Option<String>,
52 #[arg(long = "url", short = 'u', global = true)]
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 cluster: Option<Cluster>,
56 #[arg(long, global = true)]
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 commitment: Option<CommitmentLevel>,
60 #[command(flatten)]
62 #[serde(flatten)]
63 store_address: StoreAddress,
64 #[arg(long)]
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 store_program: Option<StringPubkey>,
68 #[arg(long)]
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 treasury_program: Option<StringPubkey>,
72 #[arg(long)]
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 timelock_program: Option<StringPubkey>,
76 #[arg(long, global = true, default_missing_value = "base64", num_args=0..=1, group = "tx-opts")]
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 serialize_only: Option<InstructionSerialization>,
81 #[arg(long, global = true, group = "tx-opts")]
83 skip_preflight: bool,
84 #[arg(long, requires = "serialize_only", global = true)]
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 payer: Option<StringPubkey>,
90 #[arg(long, group = "ix-buffer", global = true)]
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 timelock: Option<String>,
94 #[cfg(feature = "squads")]
96 #[arg(long, group = "ix-buffer", global = true)]
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 squads: Option<String>,
99 #[arg(long, short = 't', global = true)]
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 alts: Option<Vec<StringPubkey>>,
103 #[arg(long, global = true)]
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 oracle: Option<StringPubkey>,
107 #[arg(long, global = true)]
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 max_transaction_size: Option<usize>,
111 #[arg(long, global = true)]
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 force_one_tx: Option<bool>,
115 #[arg(long, global = true)]
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 max_transaction_instructions: Option<NonZeroUsize>,
119 #[arg(long, global = true)]
121 #[serde(default, skip_serializing_if = "Option::is_none")]
122 priority_lamports: Option<Lamport>,
123}
124
125impl Config {
126 pub fn wallet(&self) -> eyre::Result<Payer> {
128 cfg_if::cfg_if! {
129 if #[cfg(feature = "remote-wallet")] {
130 self.create_wallet(None)
131 } else {
132 self.create_wallet()
133 }
134 }
135 }
136
137 #[cfg(feature = "remote-wallet")]
140 pub fn wallet_with_remote_support(
141 &self,
142 wallet_manager: &mut Option<
143 std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>,
144 >,
145 ) -> eyre::Result<Payer> {
146 self.create_wallet(Some(wallet_manager))
147 }
148
149 pub(crate) fn create_wallet(
150 &self,
151 #[cfg(feature = "remote-wallet")] wallet_manager: Option<
152 &mut Option<std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>>,
153 >,
154 ) -> eyre::Result<Payer> {
155 if let Some(payer) = self.payer {
156 if self.serialize_only.is_some() {
157 let payer = NullSigner::new(&payer);
158 Ok(Payer::new(local_signer(payer)))
159 } else {
160 eyre::bail!("Setting payer is only allowed in `serialize-only` mode");
161 }
162 } else {
163 let wallet = signer_from_source(
164 self.wallet.as_deref().unwrap_or(DEFAULT_WALLET),
165 #[cfg(feature = "remote-wallet")]
166 false,
167 #[cfg(feature = "remote-wallet")]
168 "keypair",
169 #[cfg(feature = "remote-wallet")]
170 wallet_manager,
171 )?;
172
173 if let Some(role) = self.timelock.as_ref() {
174 let store = self.store_address();
175 let timelock_program_id = self.timelock_program_id();
176 let executor = pda::find_executor_address(
177 &store,
178 role,
179 self.timelock_program
180 .as_deref()
181 .unwrap_or(timelock_program_id),
182 )?
183 .0;
184 let executor_wallet =
185 pda::find_executor_wallet_address(&executor, timelock_program_id).0;
186
187 let payer = NullSigner::new(&executor_wallet);
188
189 return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
190 }
191
192 #[cfg(feature = "squads")]
193 if let Some(squads) = self.squads.as_ref() {
194 let (multisig, vault_index) = parse_squads(squads)?;
195 let vault_pda = gmsol_sdk::squads::get_vault_pda(&multisig, vault_index, None).0;
196
197 let payer = NullSigner::new(&vault_pda);
198
199 return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
200 }
201
202 Ok(Payer::new(wallet))
203 }
204 }
205
206 pub fn cluster(&self) -> &Cluster {
208 self.cluster.as_ref().unwrap_or(&DEFAULT_CLUSTER)
209 }
210
211 pub fn options(&self) -> ClientOptions {
213 ClientOptions::builder()
214 .commitment(CommitmentConfig {
215 commitment: self.commitment.unwrap_or(DEFAULT_COMMITMENT),
216 })
217 .store_program_id(Some(*self.store_program_id()))
218 .treasury_program_id(Some(*self.treasury_program_id()))
219 .timelock_program_id(Some(*self.timelock_program_id()))
220 .build()
221 }
222
223 pub fn store_program_id(&self) -> &Pubkey {
225 self.store_program
226 .as_deref()
227 .unwrap_or(&gmsol_sdk::programs::gmsol_store::ID)
228 }
229
230 pub fn treasury_program_id(&self) -> &Pubkey {
232 self.treasury_program
233 .as_deref()
234 .unwrap_or(&gmsol_sdk::programs::gmsol_treasury::ID)
235 }
236
237 pub fn timelock_program_id(&self) -> &Pubkey {
239 self.timelock_program
240 .as_deref()
241 .unwrap_or(&gmsol_sdk::programs::gmsol_timelock::ID)
242 }
243
244 pub fn store_address(&self) -> Pubkey {
246 self.store_address.address(self.store_program_id())
247 }
248
249 pub fn serialize_only(&self) -> Option<InstructionSerialization> {
251 self.serialize_only
252 }
253
254 pub fn ix_buffer(&self) -> eyre::Result<Option<InstructionBuffer>> {
256 if let Some(role) = self.timelock.as_ref() {
257 return Ok(Some(InstructionBuffer::Timelock { role: role.clone() }));
258 }
259
260 #[cfg(feature = "squads")]
261 if let Some(squads) = self.squads.as_ref() {
262 let (multisig, vault_index) = parse_squads(squads)?;
263 return Ok(Some(InstructionBuffer::Squads {
264 multisig,
265 vault_index,
266 }));
267 }
268
269 Ok(None)
270 }
271
272 pub fn oracle(&self) -> eyre::Result<&Pubkey> {
274 self.oracle
275 .as_deref()
276 .ok_or_eyre("oracle buffer address is not provided")
277 }
278
279 pub fn alts(&self) -> impl Iterator<Item = &Pubkey> {
281 self.alts.iter().flat_map(|alts| alts.iter().map(|p| &p.0))
282 }
283
284 pub fn output(&self) -> OutputFormat {
286 self.output.unwrap_or_default()
287 }
288
289 pub fn bundle_options(&self) -> BundleOptions {
291 BundleOptions {
292 force_one_transaction: self.force_one_tx.unwrap_or(false),
293 max_packet_size: self.max_transaction_size,
294 max_instructions_for_one_tx: self
295 .max_transaction_instructions
296 .map(|m| m.get())
297 .unwrap_or(DEFAULT_MAX_INSTRUCTIONS_FOR_ONE_TX),
298 }
299 }
300
301 pub fn priority_lamports(&self) -> eyre::Result<u64> {
303 Ok(self
304 .priority_lamports
305 .map(|a| a.to_u64())
306 .transpose()?
307 .unwrap_or(ComputeBudget::DEFAULT_MIN_PRIORITY_LAMPORTS))
308 }
309
310 pub fn skip_preflight(&self) -> bool {
312 self.skip_preflight
313 }
314}
315
316#[cfg(feature = "squads")]
317fn parse_squads(data: &str) -> eyre::Result<(Pubkey, u8)> {
318 let (multisig, vault_index) = match data.split_once(':') {
319 Some((multisig, vault_index)) => (multisig, vault_index.parse()?),
320 None => (data, 0),
321 };
322 Ok((multisig.parse()?, vault_index))
323}
324
325#[derive(Debug, Clone)]
328pub struct Payer {
329 pub payer: LocalSignerRef,
331 pub proposer: Option<LocalSignerRef>,
333}
334
335impl Payer {
336 fn with_proposer(payer: LocalSignerRef, proposer: Option<LocalSignerRef>) -> Self {
337 Self { payer, proposer }
338 }
339
340 fn new(payer: LocalSignerRef) -> Self {
341 Self::with_proposer(payer, None)
342 }
343}
344
345pub enum InstructionBuffer {
347 Timelock { role: String },
349 #[cfg(feature = "squads")]
351 Squads { multisig: Pubkey, vault_index: u8 },
352}