1use crate::SolanaNet;
2use serde::{Deserialize, Serialize};
3use serde_with::{DisplayFromStr, serde_as, serde_conv};
4use solana_commitment_config::CommitmentLevel;
5use solana_program::instruction::{AccountMeta, Instruction};
6use solana_signer::Signer;
7use std::{
8 borrow::Cow, collections::HashMap, convert::Infallible, fmt::Display, num::ParseIntError,
9 str::FromStr, time::Duration,
10};
11use value::{
12 Value,
13 with::{AsKeypair, AsPubkey},
14};
15
16pub use solana_keypair::Keypair;
17pub use solana_pubkey::Pubkey;
18pub use solana_signature::Signature;
19
20pub const SIGNATURE_TIMEOUT: Duration = Duration::from_secs(3 * 60);
21
22pub trait KeypairExt: Sized {
23 fn from_str(s: &str) -> Result<Self, anyhow::Error>;
24}
25
26impl KeypairExt for Keypair {
27 fn from_str(s: &str) -> Result<Self, anyhow::Error> {
28 let mut buf = [0u8; 64];
29 five8::decode_64(s, &mut buf)?;
30 Ok(Keypair::try_from(&buf[..])?)
31 }
32}
33
34#[serde_as]
35#[derive(Serialize, Deserialize, Debug, PartialEq)]
36#[serde(untagged)]
37pub enum Wallet {
38 Keypair(#[serde_as(as = "AsKeypair")] Keypair),
39 Adapter {
40 #[serde_as(as = "AsPubkey")]
41 public_key: Pubkey,
42 },
43}
44
45impl bincode::Encode for Wallet {
46 fn encode<E: bincode::enc::Encoder>(
47 &self,
48 encoder: &mut E,
49 ) -> Result<(), bincode::error::EncodeError> {
50 WalletBincode::from(self).encode(encoder)
51 }
52}
53
54impl<C> bincode::Decode<C> for Wallet {
55 fn decode<D: bincode::de::Decoder<Context = C>>(
56 decoder: &mut D,
57 ) -> Result<Self, bincode::error::DecodeError> {
58 Ok(WalletBincode::decode(decoder)?.into())
59 }
60}
61
62impl<'de, C> bincode::BorrowDecode<'de, C> for Wallet {
63 fn borrow_decode<D: bincode::de::BorrowDecoder<'de, Context = C>>(
64 decoder: &mut D,
65 ) -> Result<Self, bincode::error::DecodeError> {
66 Ok(WalletBincode::borrow_decode(decoder)?.into())
67 }
68}
69
70#[derive(bincode::Encode, bincode::Decode)]
71enum WalletBincode {
72 Keypair([u8; 32]),
73 Adapter([u8; 32]),
74}
75
76impl From<WalletBincode> for Wallet {
77 fn from(value: WalletBincode) -> Self {
78 match value {
79 WalletBincode::Keypair(value) => Wallet::Keypair(Keypair::new_from_array(value)),
80 WalletBincode::Adapter(value) => Wallet::Adapter {
81 public_key: Pubkey::new_from_array(value),
82 },
83 }
84 }
85}
86
87impl From<&Wallet> for WalletBincode {
88 fn from(value: &Wallet) -> Self {
89 match value {
90 Wallet::Keypair(keypair) => WalletBincode::Keypair(*keypair.secret_bytes()),
91 Wallet::Adapter { public_key } => WalletBincode::Adapter(public_key.to_bytes()),
92 }
93 }
94}
95
96impl From<Keypair> for Wallet {
97 fn from(value: Keypair) -> Self {
98 Self::Keypair(value)
99 }
100}
101
102impl Clone for Wallet {
103 fn clone(&self) -> Self {
104 match self {
105 Wallet::Keypair(keypair) => Wallet::Keypair(keypair.insecure_clone()),
106 Wallet::Adapter { public_key } => Wallet::Adapter {
107 public_key: *public_key,
108 },
109 }
110 }
111}
112
113impl Wallet {
114 pub fn is_adapter_wallet(&self) -> bool {
115 matches!(self, Wallet::Adapter { .. })
116 }
117
118 pub fn pubkey(&self) -> Pubkey {
119 match self {
120 Wallet::Keypair(keypair) => keypair.pubkey(),
121 Wallet::Adapter { public_key, .. } => *public_key,
122 }
123 }
124
125 pub fn keypair(&self) -> Option<&Keypair> {
126 match self {
127 Wallet::Keypair(keypair) => Some(keypair),
128 Wallet::Adapter { .. } => None,
129 }
130 }
131}
132
133#[serde_as]
134#[derive(Serialize, Deserialize, Debug, Default)]
135struct AsAccountMetaImpl {
136 #[serde_as(as = "AsPubkey")]
137 pubkey: Pubkey,
138 is_signer: bool,
139 is_writable: bool,
140}
141fn account_meta_ser(i: &AccountMeta) -> AsAccountMetaImpl {
142 AsAccountMetaImpl {
143 pubkey: i.pubkey,
144 is_signer: i.is_signer,
145 is_writable: i.is_writable,
146 }
147}
148fn account_meta_de(i: AsAccountMetaImpl) -> Result<AccountMeta, Infallible> {
149 Ok(AccountMeta {
150 pubkey: i.pubkey,
151 is_signer: i.is_signer,
152 is_writable: i.is_writable,
153 })
154}
155serde_conv!(
156 AsAccountMeta,
157 AccountMeta,
158 account_meta_ser,
159 account_meta_de
160);
161
162#[serde_as]
163#[derive(Serialize, Deserialize, Debug, Default)]
164struct AsInstructionImpl {
165 #[serde_as(as = "AsPubkey")]
166 program_id: Pubkey,
167 #[serde_as(as = "Vec<AsAccountMeta>")]
168 accounts: Vec<AccountMeta>,
169 #[serde_as(as = "serde_with::Bytes")]
170 data: Vec<u8>,
171}
172fn instruction_ser(i: &Instruction) -> AsInstructionImpl {
173 AsInstructionImpl {
174 program_id: i.program_id,
175 accounts: i.accounts.clone(),
176 data: i.data.clone(),
177 }
178}
179fn instruction_de(i: AsInstructionImpl) -> Result<Instruction, Infallible> {
180 Ok(Instruction {
181 program_id: i.program_id,
182 accounts: i.accounts,
183 data: i.data,
184 })
185}
186serde_conv!(AsInstruction, Instruction, instruction_ser, instruction_de);
187
188#[serde_as]
189#[derive(
190 Serialize, Deserialize, Debug, Clone, Default, bon::Builder, bincode::Encode, bincode::Decode,
191)]
192pub struct Instructions {
193 #[serde_as(as = "AsPubkey")]
194 #[bincode(with_serde)]
195 pub fee_payer: Pubkey,
196 pub signers: Vec<Wallet>,
197 #[serde_as(as = "Vec<AsInstruction>")]
198 #[bincode(with_serde)]
199 pub instructions: Vec<Instruction>,
200 #[serde_as(as = "Option<Vec<AsPubkey>>")]
201 #[bincode(with_serde)]
202 pub lookup_tables: Option<Vec<Pubkey>>,
203}
204
205#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
206pub enum InsertionBehavior {
207 #[default]
208 Auto,
209 No,
210 Value(u64),
211}
212
213impl FromStr for InsertionBehavior {
214 type Err = ParseIntError;
215
216 fn from_str(s: &str) -> Result<Self, Self::Err> {
217 Ok(match s {
218 "auto" => InsertionBehavior::Auto,
219 "no" => InsertionBehavior::No,
220 s => InsertionBehavior::Value(s.parse()?),
221 })
222 }
223}
224
225impl Display for InsertionBehavior {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 match self {
228 InsertionBehavior::Auto => f.write_str("auto"),
229 InsertionBehavior::No => f.write_str("no"),
230 InsertionBehavior::Value(v) => v.fmt(f),
231 }
232 }
233}
234
235impl Serialize for InsertionBehavior {
236 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
237 where
238 S: serde::Serializer,
239 {
240 self.to_string().serialize(serializer)
241 }
242}
243
244impl<'de> Deserialize<'de> for InsertionBehavior {
245 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
246 where
247 D: serde::Deserializer<'de>,
248 {
249 use serde::de::Error;
250 <Cow<'de, str> as Deserialize>::deserialize(deserializer)?
251 .parse()
252 .map_err(D::Error::custom)
253 }
254}
255
256const fn default_simulation_level() -> CommitmentLevel {
257 CommitmentLevel::Finalized
258}
259
260const fn default_tx_level() -> CommitmentLevel {
261 CommitmentLevel::Confirmed
262}
263
264const fn default_wait_level() -> CommitmentLevel {
265 CommitmentLevel::Confirmed
266}
267
268#[serde_as]
269#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
270#[serde(untagged)]
271pub enum WalletOrPubkey {
272 Wallet(Wallet),
273 Pubkey(#[serde_as(as = "AsPubkey")] Pubkey),
274}
275
276impl WalletOrPubkey {
277 pub fn to_keypair(self) -> Wallet {
278 match self {
279 WalletOrPubkey::Wallet(k) => k,
280 WalletOrPubkey::Pubkey(public_key) => Wallet::Adapter { public_key },
281 }
282 }
283}
284
285#[serde_with::serde_as]
286#[derive(Debug, Clone, Deserialize, Serialize)]
287#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
288pub struct ExecutionConfig {
289 pub overwrite_feepayer: Option<WalletOrPubkey>,
290
291 pub devnet_lookup_table: Option<Pubkey>,
292 pub mainnet_lookup_table: Option<Pubkey>,
293
294 #[serde(default)]
295 pub compute_budget: InsertionBehavior,
296 #[serde_as(as = "Option<DisplayFromStr>")]
297 pub fallback_compute_budget: Option<u64>,
298 #[serde(default)]
299 pub priority_fee: InsertionBehavior,
300
301 #[serde(default = "default_simulation_level")]
302 pub simulation_commitment_level: CommitmentLevel,
303 #[serde(default = "default_tx_level")]
304 pub tx_commitment_level: CommitmentLevel,
305 #[serde(default = "default_wait_level")]
306 pub wait_commitment_level: CommitmentLevel,
307
308 #[serde(skip)]
309 pub execute_on: ExecuteOn,
310}
311
312#[derive(Debug, Clone, Deserialize, Serialize)]
313pub struct SolanaActionConfig {
314 #[serde(with = "value::pubkey")]
315 pub action_signer: Pubkey,
316 #[serde(with = "value::pubkey")]
317 pub action_identity: Pubkey,
318}
319
320#[derive(Default, Debug, Clone, Deserialize, Serialize)]
321pub enum ExecuteOn {
322 SolanaAction(SolanaActionConfig),
323 #[default]
324 CurrentMachine,
325}
326
327impl ExecutionConfig {
328 pub fn from_env(map: &HashMap<String, String>) -> Result<Self, value::Error> {
329 let map = map
330 .iter()
331 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
332 .collect::<value::Map>();
333 value::from_map(map)
334 }
335
336 pub fn lookup_table(&self, network: SolanaNet) -> Option<Pubkey> {
337 match network {
338 SolanaNet::Devnet => self.devnet_lookup_table,
339 SolanaNet::Testnet => None,
340 SolanaNet::Mainnet => self.mainnet_lookup_table,
341 }
342 }
343}
344
345impl Default for ExecutionConfig {
346 fn default() -> Self {
347 Self {
348 overwrite_feepayer: None,
349 devnet_lookup_table: None,
350 mainnet_lookup_table: None,
351 compute_budget: InsertionBehavior::default(),
352 fallback_compute_budget: None,
353 priority_fee: InsertionBehavior::default(),
354 simulation_commitment_level: default_simulation_level(),
355 tx_commitment_level: default_tx_level(),
356 wait_commitment_level: default_wait_level(),
357 execute_on: ExecuteOn::default(),
358 }
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use crate::context::env::{
366 COMPUTE_BUDGET, FALLBACK_COMPUTE_BUDGET, OVERWRITE_FEEPAYER, PRIORITY_FEE,
367 SIMULATION_COMMITMENT_LEVEL, TX_COMMITMENT_LEVEL, WAIT_COMMITMENT_LEVEL,
368 };
369 use bincode::config::standard;
370 use solana_program::{pubkey, system_instruction::transfer};
371
372 #[test]
373 fn test_wallet_serde() {
374 let keypair = Keypair::new();
375 let input = Value::String(keypair.to_base58_string());
376 let Wallet::Keypair(result) = value::from_value(input).unwrap() else {
377 panic!()
378 };
379 assert_eq!(result.to_base58_string(), keypair.to_base58_string());
380 }
381
382 #[test]
397 fn test_parse_config() {
398 fn t<const N: usize>(kv: [(&str, &str); N], result: ExecutionConfig) {
399 let map = kv
400 .into_iter()
401 .map(|(k, v)| (k.to_owned(), v.to_owned()))
402 .collect::<HashMap<_, _>>();
403 let c = ExecutionConfig::from_env(&map).unwrap();
404 let l = serde_json::to_string_pretty(&c).unwrap();
405 let r = serde_json::to_string_pretty(&result).unwrap();
406 assert_eq!(l, r);
407 }
408 t(
409 [(
410 OVERWRITE_FEEPAYER,
411 "HJbqSuV94woJfyxFNnJyfQdACvvJYaNWsW1x6wmJ8kiq",
412 )],
413 ExecutionConfig {
414 overwrite_feepayer: Some(WalletOrPubkey::Pubkey(pubkey!(
415 "HJbqSuV94woJfyxFNnJyfQdACvvJYaNWsW1x6wmJ8kiq"
416 ))),
417 ..<_>::default()
418 },
419 );
420 t(
421 [
422 (COMPUTE_BUDGET, "auto"),
423 (FALLBACK_COMPUTE_BUDGET, "500000"),
424 (PRIORITY_FEE, "1000"),
425 (SIMULATION_COMMITMENT_LEVEL, "confirmed"),
426 (TX_COMMITMENT_LEVEL, "finalized"),
427 (WAIT_COMMITMENT_LEVEL, "processed"),
428 ],
429 ExecutionConfig {
430 compute_budget: InsertionBehavior::Auto,
431 fallback_compute_budget: Some(500000),
432 priority_fee: InsertionBehavior::Value(1000),
433 simulation_commitment_level: CommitmentLevel::Confirmed,
434 tx_commitment_level: CommitmentLevel::Finalized,
435 wait_commitment_level: CommitmentLevel::Processed,
436 ..<_>::default()
437 },
438 );
439 }
440
441 #[test]
442 fn test_keypair_or_pubkey_keypair() {
443 let keypair = Keypair::new();
444 let x = WalletOrPubkey::Wallet(Wallet::Keypair(keypair.insecure_clone()));
445 let value = value::to_value(&x).unwrap();
446 assert_eq!(value, Value::B64(keypair.to_bytes()));
447 assert_eq!(value::from_value::<WalletOrPubkey>(value).unwrap(), x);
448 }
449
450 #[test]
451 fn test_keypair_or_pubkey_adapter() {
452 let pubkey = Pubkey::new_unique();
453 let x = WalletOrPubkey::Wallet(Wallet::Adapter { public_key: pubkey });
454 let value = value::to_value(&x).unwrap();
455 assert_eq!(
456 value,
457 Value::Map(value::map! {
458 "public_key" => pubkey,
459 })
460 );
461 assert_eq!(value::from_value::<WalletOrPubkey>(value).unwrap(), x);
462 }
463
464 #[test]
465 fn test_keypair_or_pubkey_pubkey() {
466 let pubkey = Pubkey::new_unique();
467 let x = WalletOrPubkey::Pubkey(pubkey);
468 let value = value::to_value(&x).unwrap();
469 assert_eq!(value, Value::B32(pubkey.to_bytes()));
470 assert_eq!(value::from_value::<WalletOrPubkey>(value).unwrap(), x);
471 }
472
473 #[test]
474 fn test_wallet_keypair() {
475 let keypair = Keypair::new();
476 let x = Wallet::Keypair(keypair.insecure_clone());
477 let value = value::to_value(&x).unwrap();
478 assert_eq!(value, Value::B64(keypair.to_bytes()));
479 assert_eq!(value::from_value::<Wallet>(value).unwrap(), x);
480 }
481
482 #[test]
483 fn test_wallet_adapter() {
484 let pubkey = Pubkey::new_unique();
485 let x = Wallet::Adapter { public_key: pubkey };
486 let value = value::to_value(&x).unwrap();
487 assert_eq!(
488 value,
489 Value::Map(value::map! {
490 "public_key" => pubkey,
491 })
492 );
493 assert_eq!(value::from_value::<Wallet>(value).unwrap(), x);
494 }
495
496 #[test]
497 fn test_instructions_bincode() {
498 let instructions = Instructions {
499 fee_payer: Pubkey::new_unique(),
500 signers: [
501 Wallet::Keypair(Keypair::new()),
502 Wallet::Adapter {
503 public_key: Pubkey::new_unique(),
504 },
505 ]
506 .into(),
507 instructions: [transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1000)].into(),
508 lookup_tables: Some([Pubkey::new_unique()].into()),
509 };
510 let data = bincode::encode_to_vec(&instructions, standard()).unwrap();
511 let decoded: Instructions = bincode::decode_from_slice(&data, standard()).unwrap().0;
512 dbg!(decoded);
513 }
514}