Skip to main content

flow_lib/
solana.rs

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    /* TODO: add this test back
393     * failed because it is a "legacy" tx, we are using "v0" tx
394    #[test]
395    fn test_compare_msg_logic() {
396        const OLD: &str = "AwEJE/I9QMIByO+GhMkfll9MXSsAYs1ITPmKAfxGS/USlNwuw0EUt8a41tLSp95YmtHPKWDGGcApBC0AEmN1Sd+5kfDOAq0G+/qWg2KKmXfDQF1HIuw9Op9LiSZK5iA7jcVQ9wceNyYLLzZIZ+cVomhs1zT04hQeIKdXkiMyUpH9KA95JukMx1A93RFsivUbXmW+wwO52yE0+21NxUpXL/eMTCpS1wQ6IUwmvO0o13hn6qE0Pi73WxtEGjlbBilP+HVyqFkAIKLtjJBJ25Jae9iO3Xe17TFanfbTgtEbgKAJ5nWVuJt84ctKVWEXbuPgqHbe6H8fchmNtE0iKLjuVOE0AJ3GIRyraKaGg0wqZXXkbS0qr6CQYxZVv7PeO7zsL/swgPucBbMHhqVF+Mv8NimuycfvB72jxeN3uhwn+c715MdKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAan1RcYe9FmNdrUBFX9wsDBJMaPIVZ1pdu6y18IAAAABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkLcGWx49F8RTidUn9rBMPNWLhscxqg/bVJttG8A/gpRlM2SFRbPsgTT3LuOBLPsJzpVN5CeDaecGGyxbawEE6Kcy72NeMo2v4ccHESWqcHq3GioOBRqLHY25fQEpaeCVSLCKI3/q1QflOctOQHXPk3VuQhThJQPfn/dD3sEZbonYyXJY9OJInxuz0QKRSODYMLWhOZ2v8QhASOe9jb6fhZdtEfrjiMo8c/EYJzRiXnOLehdv4i42eBpdbr4NYTAzkICwAJA+gDAAAAAAAACwAFAkANAwAOCQMFAQIAAgoMDdoBKgAYAAAAU3BhY2UgT3BlcmF0b3IgQ2hhbWVsZW9uBAAAAFNQT0NTAAAAaHR0cHM6Ly9hc3NldHMuc3BhY2VvcGVyYXRvci5jb20vbWV0YWRhdGEvMzU4NjY4MzItN2M4My00OWM2LWJmZjctY2FhMDBiNmE2NDE1Lmpzb276AAEBAAAAzgKtBvv6loNiipl3w0BdRyLsPTqfS4kmSuYgO43FUPcAZAABBAEAiwiiN/6tUH5TnLTkB1z5N1bkIU4SUD35/3Q97BGW6J0AAAABAAEBZAAAAAAAAAAOCAIOAxEJDwoMAjQBDggCDgMODg4KDAI0AA4OBxADBQQBCAIACgwNDg4DLAMADg8IAAMFBAECDgAKDA0SDg4LKwABAAAAAAAAAAAKAgAGDAIAAAAAu+6gAAAAAA==";
397        const NEW: &str = "AwEJE/I9QMIByO+GhMkfll9MXSsAYs1ITPmKAfxGS/USlNwuw0EUt8a41tLSp95YmtHPKWDGGcApBC0AEmN1Sd+5kfDOAq0G+/qWg2KKmXfDQF1HIuw9Op9LiSZK5iA7jcVQ9ybpDMdQPd0RbIr1G15lvsMDudshNPttTcVKVy/3jEwqUtcEOiFMJrztKNd4Z+qhND4u91sbRBo5WwYpT/h1cqhZACCi7YyQSduSWnvYjt13te0xWp3204LRG4CgCeZ1lbibfOHLSlVhF27j4Kh23uh/H3IZjbRNIii47lThNACdxiEcq2imhoNMKmV15G0tKq+gkGMWVb+z3ju87C/7MID7nAWzB4alRfjL/DYprsnH7we9o8Xjd7ocJ/nO9eTHSgceNyYLLzZIZ+cVomhs1zT04hQeIKdXkiMyUpH9KA95AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABTNkhUWz7IE09y7jgSz7Cc6VTeQng2nnBhssW2sBBOinMu9jXjKNr+HHBxElqnB6txoqDgUaix2NuX0BKWnglUiwiiN/6tUH5TnLTkB1z5N1bkIU4SUD35/3Q97BGW6J2MlyWPTiSJ8bs9ECkUjg2DC1oTmdr/EIQEjnvY2+n4WQMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAC3BlsePRfEU4nVJ/awTDzVi4bHMaoP21SbbRvAP4KUYGp9UXGHvRZjXa1ARV/cLAwSTGjyFWdaXbustfCAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpdtEfrjiMo8c/EYJzRiXnOLehdv4i42eBpdbr4NYTAzkIDwAJA+gDAAAAAAAADwAFAkANAwAQCQkEAQIAAgoREtoBKgAYAAAAU3BhY2UgT3BlcmF0b3IgQ2hhbWVsZW9uBAAAAFNQT0NTAAAAaHR0cHM6Ly9hc3NldHMuc3BhY2VvcGVyYXRvci5jb20vbWV0YWRhdGEvMzU4NjY4MzItN2M4My00OWM2LWJmZjctY2FhMDBiNmE2NDE1Lmpzb276AAEBAAAAzgKtBvv6loNiipl3w0BdRyLsPTqfS4kmSuYgO43FUPcAZAABBAEAiwiiN/6tUH5TnLTkB1z5N1bkIU4SUD35/3Q97BGW6J0AAAABAAEBZAAAAAAAAAAQCAIQCQ0ICwoRAjQBEAgCEAkQEBAKEQI0ABAOBgwJBAMBBwIAChESEBADLAMAEA8HAAkEAwECEAAKERIOEBALKwABAAAAAAAAAAAKAgAFDAIAAAAAu+6gAAAAAA==";
398        is_same_message_logic(
399            &BASE64_STANDARD.decode(OLD).unwrap(),
400            &BASE64_STANDARD.decode(NEW).unwrap(),
401        )
402        .unwrap();
403    }
404    */
405
406    #[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}