gmsol_solana_utils/
program_trait.rs

1use solana_sdk::{
2    instruction::{AccountMeta, Instruction},
3    pubkey::Pubkey,
4};
5
6#[cfg(anchor_lang)]
7use anchor_lang::{Id, InstructionData, ToAccountMetas};
8
9/// A solana program.
10pub trait Program {
11    /// Returns the current program ID.
12    fn id(&self) -> &Pubkey;
13}
14
15impl<P: Program> Program for &P {
16    fn id(&self) -> &Pubkey {
17        (**self).id()
18    }
19}
20
21/// Extension trait for [`Program`].
22pub trait ProgramExt: Program {
23    /// Create an [`InstructionBuilder`]
24    fn instruction(&self, data: Vec<u8>) -> InstructionBuilder<Self>
25    where
26        Self: Sized,
27    {
28        InstructionBuilder {
29            program: self,
30            data,
31            accounts: vec![],
32        }
33    }
34
35    /// Create [`InstructionBuilder`] with [`InstructionData`].
36    #[cfg(anchor_lang)]
37    fn anchor_instruction(&self, args: impl InstructionData) -> InstructionBuilder<Self>
38    where
39        Self: Sized,
40    {
41        self.instruction(args.data())
42    }
43
44    /// Convert to account metas.
45    ///
46    /// If `convert_optional` is `true`, read-only non-signer accounts with
47    /// the default program ID as pubkey will be replaced with the current
48    /// program ID.
49    #[cfg(anchor_lang)]
50    fn anchor_accounts(
51        &self,
52        accounts: impl ToAccountMetas,
53        convert_optional: bool,
54    ) -> Vec<AccountMeta>
55    where
56        Self: Id,
57    {
58        if convert_optional {
59            fix_optional_account_metas(accounts, &<Self as Id>::id(), self.id())
60        } else {
61            accounts.to_account_metas(None)
62        }
63    }
64}
65
66impl<P: ?Sized + Program> ProgramExt for P {}
67
68/// Generic Instruction Builder.
69#[derive(Debug, Clone)]
70pub struct InstructionBuilder<'a, P> {
71    program: &'a P,
72    data: Vec<u8>,
73    accounts: Vec<AccountMeta>,
74}
75
76impl<P> InstructionBuilder<'_, P> {
77    /// Append accounts to account list.
78    pub fn accounts(mut self, mut accounts: Vec<AccountMeta>) -> Self {
79        self.accounts.append(&mut accounts);
80        self
81    }
82}
83
84impl<P: Program> InstructionBuilder<'_, P> {
85    /// Build an [`Instruction`].
86    pub fn build(self) -> Instruction {
87        Instruction {
88            program_id: *self.program.id(),
89            accounts: self.accounts,
90            data: self.data,
91        }
92    }
93}
94
95#[cfg(anchor_lang)]
96impl<P: Program + Id> InstructionBuilder<'_, P> {
97    /// Append a [`ToAccountMetas`] to account list.
98    pub fn anchor_accounts(self, accounts: impl ToAccountMetas, convert_optional: bool) -> Self {
99        let accounts = self.program.anchor_accounts(accounts, convert_optional);
100        self.accounts(accounts)
101    }
102}
103
104/// Change the `pubkey` of any readonly, non-signer [`AccountMeta`]
105/// with the `pubkey` equal to the original program id to the new one.
106///
107/// This is a workaround since Anchor will automatically set optional accounts
108/// to the Program ID of the program that defines them when they are `None`s,
109/// if we use the same program but with different Program IDs, the optional
110/// accounts will be set to the wrong addresses.
111///
112/// ## Warning
113/// Use this function only if you fully understand the implications.
114#[cfg(anchor_lang)]
115pub fn fix_optional_account_metas(
116    accounts: impl ToAccountMetas,
117    original: &Pubkey,
118    current: &Pubkey,
119) -> Vec<AccountMeta> {
120    let mut metas = accounts.to_account_metas(None);
121    if *original == *current {
122        // No-op in this case.
123        return metas;
124    }
125    metas.iter_mut().for_each(|meta| {
126        if !meta.is_signer && !meta.is_writable && meta.pubkey == *original {
127            // We consider it a `None` account. If it is not, please do not use this function.
128            meta.pubkey = *current;
129        }
130    });
131    metas
132}