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    },
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    /* TODO: add this test back
378     * failed because it is a "legacy" tx, we are using "v0" tx
379    #[test]
380    fn test_compare_msg_logic() {
381        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==";
382        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==";
383        is_same_message_logic(
384            &BASE64_STANDARD.decode(OLD).unwrap(),
385            &BASE64_STANDARD.decode(NEW).unwrap(),
386        )
387        .unwrap();
388    }
389    */
390
391    #[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}