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