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