flow_lib/
solana.rs

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