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)]
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 liquidity_provider_program: Option<StringPubkey>,
80 #[arg(long, global = true, default_missing_value = "base64", num_args=0..=1, group = "tx-opts")]
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 serialize_only: Option<InstructionSerialization>,
85 #[arg(long, global = true, group = "tx-opts")]
87 skip_preflight: bool,
88 #[arg(long, requires = "serialize_only", global = true)]
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 payer: Option<StringPubkey>,
94 #[arg(long, group = "ix-buffer", global = true)]
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 timelock: Option<String>,
98 #[cfg(feature = "squads")]
100 #[arg(long, group = "ix-buffer", global = true)]
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 squads: Option<String>,
103 #[arg(long, short = 't', global = true)]
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 alts: Option<Vec<StringPubkey>>,
107 #[arg(long, global = true)]
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 oracle: Option<StringPubkey>,
111 #[arg(long, global = true)]
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 max_transaction_size: Option<usize>,
115 #[arg(long, global = true)]
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 force_one_tx: Option<bool>,
119 #[arg(long, global = true)]
121 #[serde(default, skip_serializing_if = "Option::is_none")]
122 max_transaction_instructions: Option<NonZeroUsize>,
123 #[arg(long, global = true)]
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 priority_lamports: Option<Lamport>,
127}
128
129impl Config {
130 pub fn wallet(&self) -> eyre::Result<Payer> {
132 cfg_if::cfg_if! {
133 if #[cfg(feature = "remote-wallet")] {
134 self.create_wallet(None)
135 } else {
136 self.create_wallet()
137 }
138 }
139 }
140
141 #[cfg(feature = "remote-wallet")]
144 pub fn wallet_with_remote_support(
145 &self,
146 wallet_manager: &mut Option<
147 std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>,
148 >,
149 ) -> eyre::Result<Payer> {
150 self.create_wallet(Some(wallet_manager))
151 }
152
153 pub(crate) fn create_wallet(
154 &self,
155 #[cfg(feature = "remote-wallet")] wallet_manager: Option<
156 &mut Option<std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>>,
157 >,
158 ) -> eyre::Result<Payer> {
159 if let Some(payer) = self.payer {
160 if self.serialize_only.is_some() {
161 let payer = NullSigner::new(&payer);
162 Ok(Payer::new(local_signer(payer)))
163 } else {
164 eyre::bail!("Setting payer is only allowed in `serialize-only` mode");
165 }
166 } else {
167 let wallet = signer_from_source(
168 self.wallet.as_deref().unwrap_or(DEFAULT_WALLET),
169 #[cfg(feature = "remote-wallet")]
170 false,
171 #[cfg(feature = "remote-wallet")]
172 "keypair",
173 #[cfg(feature = "remote-wallet")]
174 wallet_manager,
175 )?;
176
177 if let Some(role) = self.timelock.as_ref() {
178 let store = self.store_address();
179 let timelock_program_id = self.timelock_program_id();
180 let executor = pda::find_executor_address(
181 &store,
182 role,
183 self.timelock_program
184 .as_deref()
185 .unwrap_or(timelock_program_id),
186 )?
187 .0;
188 let executor_wallet =
189 pda::find_executor_wallet_address(&executor, timelock_program_id).0;
190
191 let payer = NullSigner::new(&executor_wallet);
192
193 return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
194 }
195
196 #[cfg(feature = "squads")]
197 if let Some(squads) = self.squads.as_ref() {
198 let (multisig, vault_index) = parse_squads(squads)?;
199 let vault_pda = gmsol_sdk::squads::get_vault_pda(&multisig, vault_index, None).0;
200
201 let payer = NullSigner::new(&vault_pda);
202
203 return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
204 }
205
206 Ok(Payer::new(wallet))
207 }
208 }
209
210 pub fn cluster(&self) -> &Cluster {
212 self.cluster.as_ref().unwrap_or(&DEFAULT_CLUSTER)
213 }
214
215 pub fn options(&self) -> ClientOptions {
217 ClientOptions::builder()
218 .commitment(CommitmentConfig {
219 commitment: self.commitment.unwrap_or(DEFAULT_COMMITMENT),
220 })
221 .store_program_id(Some(*self.store_program_id()))
222 .treasury_program_id(Some(*self.treasury_program_id()))
223 .timelock_program_id(Some(*self.timelock_program_id()))
224 .liquidity_provider_program_id(Some(*self.liquidity_provider_program_id()))
225 .build()
226 }
227
228 pub fn store_program_id(&self) -> &Pubkey {
230 self.store_program
231 .as_deref()
232 .unwrap_or(&gmsol_sdk::programs::gmsol_store::ID)
233 }
234
235 pub fn treasury_program_id(&self) -> &Pubkey {
237 self.treasury_program
238 .as_deref()
239 .unwrap_or(&gmsol_sdk::programs::gmsol_treasury::ID)
240 }
241
242 pub fn timelock_program_id(&self) -> &Pubkey {
244 self.timelock_program
245 .as_deref()
246 .unwrap_or(&gmsol_sdk::programs::gmsol_timelock::ID)
247 }
248
249 pub fn liquidity_provider_program_id(&self) -> &Pubkey {
251 self.liquidity_provider_program
252 .as_deref()
253 .unwrap_or(&gmsol_sdk::programs::gmsol_liquidity_provider::ID)
254 }
255
256 pub fn store_address(&self) -> Pubkey {
258 self.store_address.address(self.store_program_id())
259 }
260
261 pub fn serialize_only(&self) -> Option<InstructionSerialization> {
263 self.serialize_only
264 }
265
266 pub fn ix_buffer(&self) -> eyre::Result<Option<InstructionBuffer>> {
268 if let Some(role) = self.timelock.as_ref() {
269 return Ok(Some(InstructionBuffer::Timelock { role: role.clone() }));
270 }
271
272 #[cfg(feature = "squads")]
273 if let Some(squads) = self.squads.as_ref() {
274 let (multisig, vault_index) = parse_squads(squads)?;
275 return Ok(Some(InstructionBuffer::Squads {
276 multisig,
277 vault_index,
278 }));
279 }
280
281 Ok(None)
282 }
283
284 pub fn oracle(&self) -> eyre::Result<&Pubkey> {
286 self.oracle
287 .as_deref()
288 .ok_or_eyre("oracle buffer address is not provided")
289 }
290
291 pub fn alts(&self) -> impl Iterator<Item = &Pubkey> {
293 self.alts.iter().flat_map(|alts| alts.iter().map(|p| &p.0))
294 }
295
296 pub fn output(&self) -> OutputFormat {
298 self.output.unwrap_or_default()
299 }
300
301 pub fn bundle_options(&self) -> BundleOptions {
303 BundleOptions {
304 force_one_transaction: self.force_one_tx.unwrap_or(false),
305 max_packet_size: self.max_transaction_size,
306 max_instructions_for_one_tx: self
307 .max_transaction_instructions
308 .map(|m| m.get())
309 .unwrap_or(DEFAULT_MAX_INSTRUCTIONS_FOR_ONE_TX),
310 }
311 }
312
313 pub fn priority_lamports(&self) -> eyre::Result<u64> {
315 Ok(self
316 .priority_lamports
317 .map(|a| a.to_u64())
318 .transpose()?
319 .unwrap_or(ComputeBudget::DEFAULT_MIN_PRIORITY_LAMPORTS))
320 }
321
322 pub fn skip_preflight(&self) -> bool {
324 self.skip_preflight
325 }
326}
327
328#[cfg(feature = "squads")]
329pub(crate) fn parse_squads(data: &str) -> eyre::Result<(Pubkey, u8)> {
330 let (multisig, vault_index) = match data.split_once(':') {
331 Some((multisig, vault_index)) => (multisig, vault_index.parse()?),
332 None => (data, 0),
333 };
334 Ok((multisig.parse()?, vault_index))
335}
336
337#[derive(Debug, Clone)]
340pub struct Payer {
341 pub payer: LocalSignerRef,
343 pub proposer: Option<LocalSignerRef>,
345}
346
347impl Payer {
348 fn with_proposer(payer: LocalSignerRef, proposer: Option<LocalSignerRef>) -> Self {
349 Self { payer, proposer }
350 }
351
352 fn new(payer: LocalSignerRef) -> Self {
353 Self::with_proposer(payer, None)
354 }
355}
356
357pub enum InstructionBuffer {
359 Timelock { role: String },
361 #[cfg(feature = "squads")]
363 Squads { multisig: Pubkey, vault_index: u8 },
364}