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 #[arg(skip)]
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 chaos: Option<ChaosConfig>,
132}
133
134impl Config {
135 pub fn wallet(&self) -> eyre::Result<Payer> {
137 cfg_if::cfg_if! {
138 if #[cfg(feature = "remote-wallet")] {
139 self.create_wallet(None)
140 } else {
141 self.create_wallet()
142 }
143 }
144 }
145
146 #[cfg(feature = "remote-wallet")]
149 pub fn wallet_with_remote_support(
150 &self,
151 wallet_manager: &mut Option<
152 std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>,
153 >,
154 ) -> eyre::Result<Payer> {
155 self.create_wallet(Some(wallet_manager))
156 }
157
158 pub(crate) fn create_wallet(
159 &self,
160 #[cfg(feature = "remote-wallet")] wallet_manager: Option<
161 &mut Option<std::rc::Rc<solana_remote_wallet::remote_wallet::RemoteWalletManager>>,
162 >,
163 ) -> eyre::Result<Payer> {
164 if let Some(payer) = self.payer {
165 if self.serialize_only.is_some() {
166 let payer = NullSigner::new(&payer);
167 Ok(Payer::new(local_signer(payer)))
168 } else {
169 eyre::bail!("Setting payer is only allowed in `serialize-only` mode");
170 }
171 } else {
172 let wallet = signer_from_source(
173 self.wallet.as_deref().unwrap_or(DEFAULT_WALLET),
174 #[cfg(feature = "remote-wallet")]
175 false,
176 #[cfg(feature = "remote-wallet")]
177 "keypair",
178 #[cfg(feature = "remote-wallet")]
179 wallet_manager,
180 )?;
181
182 if let Some(role) = self.timelock.as_ref() {
183 let store = self.store_address();
184 let timelock_program_id = self.timelock_program_id();
185 let executor = pda::find_executor_address(
186 &store,
187 role,
188 self.timelock_program
189 .as_deref()
190 .unwrap_or(timelock_program_id),
191 )?
192 .0;
193 let executor_wallet =
194 pda::find_executor_wallet_address(&executor, timelock_program_id).0;
195
196 let payer = NullSigner::new(&executor_wallet);
197
198 return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
199 }
200
201 #[cfg(feature = "squads")]
202 if let Some(squads) = self.squads.as_ref() {
203 let (multisig, vault_index) = parse_squads(squads)?;
204 let vault_pda = gmsol_sdk::squads::get_vault_pda(&multisig, vault_index, None).0;
205
206 let payer = NullSigner::new(&vault_pda);
207
208 return Ok(Payer::with_proposer(local_signer(payer), Some(wallet)));
209 }
210
211 Ok(Payer::new(wallet))
212 }
213 }
214
215 pub fn cluster(&self) -> &Cluster {
217 self.cluster.as_ref().unwrap_or(&DEFAULT_CLUSTER)
218 }
219
220 pub fn options(&self) -> ClientOptions {
222 ClientOptions::builder()
223 .commitment(CommitmentConfig {
224 commitment: self.commitment.unwrap_or(DEFAULT_COMMITMENT),
225 })
226 .store_program_id(Some(*self.store_program_id()))
227 .treasury_program_id(Some(*self.treasury_program_id()))
228 .timelock_program_id(Some(*self.timelock_program_id()))
229 .liquidity_provider_program_id(Some(*self.liquidity_provider_program_id()))
230 .build()
231 }
232
233 pub fn store_program_id(&self) -> &Pubkey {
235 self.store_program
236 .as_deref()
237 .unwrap_or(&gmsol_sdk::programs::gmsol_store::ID)
238 }
239
240 pub fn treasury_program_id(&self) -> &Pubkey {
242 self.treasury_program
243 .as_deref()
244 .unwrap_or(&gmsol_sdk::programs::gmsol_treasury::ID)
245 }
246
247 pub fn timelock_program_id(&self) -> &Pubkey {
249 self.timelock_program
250 .as_deref()
251 .unwrap_or(&gmsol_sdk::programs::gmsol_timelock::ID)
252 }
253
254 pub fn liquidity_provider_program_id(&self) -> &Pubkey {
256 self.liquidity_provider_program
257 .as_deref()
258 .unwrap_or(&gmsol_sdk::programs::gmsol_liquidity_provider::ID)
259 }
260
261 pub fn store_address(&self) -> Pubkey {
263 self.store_address.address(self.store_program_id())
264 }
265
266 pub fn serialize_only(&self) -> Option<InstructionSerialization> {
268 self.serialize_only
269 }
270
271 pub fn ix_buffer(&self) -> eyre::Result<Option<InstructionBuffer>> {
273 if let Some(role) = self.timelock.as_ref() {
274 return Ok(Some(InstructionBuffer::Timelock { role: role.clone() }));
275 }
276
277 #[cfg(feature = "squads")]
278 if let Some(squads) = self.squads.as_ref() {
279 let (multisig, vault_index) = parse_squads(squads)?;
280 return Ok(Some(InstructionBuffer::Squads {
281 multisig,
282 vault_index,
283 }));
284 }
285
286 Ok(None)
287 }
288
289 pub fn oracle(&self) -> eyre::Result<&Pubkey> {
291 self.oracle
292 .as_deref()
293 .ok_or_eyre("oracle buffer address is not provided")
294 }
295
296 pub fn alts(&self) -> impl Iterator<Item = &Pubkey> {
298 self.alts.iter().flat_map(|alts| alts.iter().map(|p| &p.0))
299 }
300
301 pub fn output(&self) -> OutputFormat {
303 self.output.unwrap_or_default()
304 }
305
306 pub fn bundle_options(&self) -> BundleOptions {
308 BundleOptions {
309 force_one_transaction: self.force_one_tx.unwrap_or(false),
310 max_packet_size: self.max_transaction_size,
311 max_instructions_for_one_tx: self
312 .max_transaction_instructions
313 .map(|m| m.get())
314 .unwrap_or(DEFAULT_MAX_INSTRUCTIONS_FOR_ONE_TX),
315 }
316 }
317
318 pub fn priority_lamports(&self) -> eyre::Result<u64> {
320 Ok(self
321 .priority_lamports
322 .map(|a| a.to_u64())
323 .transpose()?
324 .unwrap_or(ComputeBudget::DEFAULT_MIN_PRIORITY_LAMPORTS))
325 }
326
327 pub fn skip_preflight(&self) -> bool {
329 self.skip_preflight
330 }
331
332 pub fn chaos_base_url(&self) -> String {
333 if let Ok(v) = std::env::var("CHAOS_BASE_URL") {
334 return v;
335 }
336 self.chaos
337 .as_ref()
338 .and_then(|c| c.base_url.clone())
339 .unwrap_or_else(|| "https://oracle.chaoslabs.co".to_string())
340 }
341
342 pub fn chaos_api_key(&self) -> Option<String> {
343 if let Ok(v) = std::env::var("CHAOS_API_KEY") {
344 return Some(v);
345 }
346 self.chaos.as_ref().and_then(|c| c.api_key.clone())
347 }
348
349 pub fn chaos_signer_strict(&self) -> eyre::Result<Option<Pubkey>> {
350 if let Ok(v) = std::env::var("RISK_ORACLE_SIGNER") {
351 let pk: Pubkey = v
352 .parse()
353 .map_err(|_| eyre::eyre!("invalid RISK_ORACLE_SIGNER: {v}"))?;
354 return Ok(Some(pk));
355 }
356 Ok(self
357 .chaos
358 .as_ref()
359 .and_then(|c| c.signer.as_ref().map(|s| s.0)))
360 }
361}
362
363#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
364pub struct ChaosConfig {
365 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub base_url: Option<String>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub api_key: Option<String>,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub signer: Option<StringPubkey>,
371}
372
373#[cfg(feature = "squads")]
374pub(crate) fn parse_squads(data: &str) -> eyre::Result<(Pubkey, u8)> {
375 let (multisig, vault_index) = match data.split_once(':') {
376 Some((multisig, vault_index)) => (multisig, vault_index.parse()?),
377 None => (data, 0),
378 };
379 Ok((multisig.parse()?, vault_index))
380}
381
382#[derive(Debug, Clone)]
385pub struct Payer {
386 pub payer: LocalSignerRef,
388 pub proposer: Option<LocalSignerRef>,
390}
391
392impl Payer {
393 fn with_proposer(payer: LocalSignerRef, proposer: Option<LocalSignerRef>) -> Self {
394 Self { payer, proposer }
395 }
396
397 fn new(payer: LocalSignerRef) -> Self {
398 Self::with_proposer(payer, None)
399 }
400}
401
402pub enum InstructionBuffer {
404 Timelock { role: String },
406 #[cfg(feature = "squads")]
408 Squads { multisig: Pubkey, vault_index: u8 },
409}