spl_transfer_hook_interface/
instruction.rs

1//! Instruction types
2
3use {
4    miraland_program::{
5        instruction::{AccountMeta, Instruction},
6        program_error::ProgramError,
7        pubkey::Pubkey,
8        system_program,
9    },
10    spl_discriminator::{ArrayDiscriminator, SplDiscriminate},
11    spl_pod::{bytemuck::pod_slice_to_bytes, slice::PodSlice},
12    spl_tlv_account_resolution::account::ExtraAccountMeta,
13    std::convert::TryInto,
14};
15
16/// Instructions supported by the transfer hook interface.
17#[repr(C)]
18#[derive(Clone, Debug, PartialEq)]
19pub enum TransferHookInstruction {
20    /// Runs additional transfer logic.
21    ///
22    /// Accounts expected by this instruction:
23    ///
24    ///   0. `[]` Source account
25    ///   1. `[]` Token mint
26    ///   2. `[]` Destination account
27    ///   3. `[]` Source account's owner/delegate
28    ///   4. `[]` Validation account
29    ///   5..5+M `[]` `M` additional accounts, written in validation account
30    ///     data
31    Execute {
32        /// Amount of tokens to transfer
33        amount: u64,
34    },
35
36    /// Initializes the extra account metas on an account, writing into the
37    /// first open TLV space.
38    ///
39    /// Accounts expected by this instruction:
40    ///
41    ///   0. `[w]` Account with extra account metas
42    ///   1. `[]` Mint
43    ///   2. `[s]` Mint authority
44    ///   3. `[]` System program
45    InitializeExtraAccountMetaList {
46        /// List of `ExtraAccountMeta`s to write into the account
47        extra_account_metas: Vec<ExtraAccountMeta>,
48    },
49    /// Updates the extra account metas on an account by overwriting the
50    /// existing list.
51    ///
52    /// Accounts expected by this instruction:
53    ///
54    ///   0. `[w]` Account with extra account metas
55    ///   1. `[]` Mint
56    ///   2. `[s]` Mint authority
57    UpdateExtraAccountMetaList {
58        /// The new list of `ExtraAccountMetas` to overwrite the existing entry
59        /// in the account.
60        extra_account_metas: Vec<ExtraAccountMeta>,
61    },
62}
63/// TLV instruction type only used to define the discriminator. The actual data
64/// is entirely managed by `ExtraAccountMetaList`, and it is the only data
65/// contained by this type.
66#[derive(SplDiscriminate)]
67#[discriminator_hash_input("solarti-transfer-hook-interface:execute")]
68pub struct ExecuteInstruction;
69
70/// TLV instruction type used to initialize extra account metas
71/// for the transfer hook
72#[derive(SplDiscriminate)]
73#[discriminator_hash_input("solarti-transfer-hook-interface:initialize-extra-account-metas")]
74pub struct InitializeExtraAccountMetaListInstruction;
75
76/// TLV instruction type used to update extra account metas
77/// for the transfer hook
78#[derive(SplDiscriminate)]
79#[discriminator_hash_input("solarti-transfer-hook-interface:update-extra-account-metas")]
80pub struct UpdateExtraAccountMetaListInstruction;
81
82impl TransferHookInstruction {
83    /// Unpacks a byte buffer into a
84    /// [TransferHookInstruction](enum.TransferHookInstruction.html).
85    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
86        if input.len() < ArrayDiscriminator::LENGTH {
87            return Err(ProgramError::InvalidInstructionData);
88        }
89        let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH);
90        Ok(match discriminator {
91            ExecuteInstruction::SPL_DISCRIMINATOR_SLICE => {
92                let amount = rest
93                    .get(..8)
94                    .and_then(|slice| slice.try_into().ok())
95                    .map(u64::from_le_bytes)
96                    .ok_or(ProgramError::InvalidInstructionData)?;
97                Self::Execute { amount }
98            }
99            InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE => {
100                let pod_slice = PodSlice::<ExtraAccountMeta>::unpack(rest)?;
101                let extra_account_metas = pod_slice.data().to_vec();
102                Self::InitializeExtraAccountMetaList {
103                    extra_account_metas,
104                }
105            }
106            UpdateExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE => {
107                let pod_slice = PodSlice::<ExtraAccountMeta>::unpack(rest)?;
108                let extra_account_metas = pod_slice.data().to_vec();
109                Self::UpdateExtraAccountMetaList {
110                    extra_account_metas,
111                }
112            }
113            _ => return Err(ProgramError::InvalidInstructionData),
114        })
115    }
116
117    /// Packs a [TokenInstruction](enum.TokenInstruction.html) into a byte
118    /// buffer.
119    pub fn pack(&self) -> Vec<u8> {
120        let mut buf = vec![];
121        match self {
122            Self::Execute { amount } => {
123                buf.extend_from_slice(ExecuteInstruction::SPL_DISCRIMINATOR_SLICE);
124                buf.extend_from_slice(&amount.to_le_bytes());
125            }
126            Self::InitializeExtraAccountMetaList {
127                extra_account_metas,
128            } => {
129                buf.extend_from_slice(
130                    InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE,
131                );
132                buf.extend_from_slice(&(extra_account_metas.len() as u32).to_le_bytes());
133                buf.extend_from_slice(pod_slice_to_bytes(extra_account_metas));
134            }
135            Self::UpdateExtraAccountMetaList {
136                extra_account_metas,
137            } => {
138                buf.extend_from_slice(
139                    UpdateExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE,
140                );
141                buf.extend_from_slice(&(extra_account_metas.len() as u32).to_le_bytes());
142                buf.extend_from_slice(pod_slice_to_bytes(extra_account_metas));
143            }
144        };
145        buf
146    }
147}
148
149/// Creates an `Execute` instruction, provided all of the additional required
150/// account metas
151#[allow(clippy::too_many_arguments)]
152pub fn execute_with_extra_account_metas(
153    program_id: &Pubkey,
154    source_pubkey: &Pubkey,
155    mint_pubkey: &Pubkey,
156    destination_pubkey: &Pubkey,
157    authority_pubkey: &Pubkey,
158    validate_state_pubkey: &Pubkey,
159    additional_accounts: &[AccountMeta],
160    amount: u64,
161) -> Instruction {
162    let mut instruction = execute(
163        program_id,
164        source_pubkey,
165        mint_pubkey,
166        destination_pubkey,
167        authority_pubkey,
168        validate_state_pubkey,
169        amount,
170    );
171    instruction.accounts.extend_from_slice(additional_accounts);
172    instruction
173}
174
175/// Creates an `Execute` instruction, without the additional accounts
176#[allow(clippy::too_many_arguments)]
177pub fn execute(
178    program_id: &Pubkey,
179    source_pubkey: &Pubkey,
180    mint_pubkey: &Pubkey,
181    destination_pubkey: &Pubkey,
182    authority_pubkey: &Pubkey,
183    validate_state_pubkey: &Pubkey,
184    amount: u64,
185) -> Instruction {
186    let data = TransferHookInstruction::Execute { amount }.pack();
187    let accounts = vec![
188        AccountMeta::new_readonly(*source_pubkey, false),
189        AccountMeta::new_readonly(*mint_pubkey, false),
190        AccountMeta::new_readonly(*destination_pubkey, false),
191        AccountMeta::new_readonly(*authority_pubkey, false),
192        AccountMeta::new_readonly(*validate_state_pubkey, false),
193    ];
194    Instruction {
195        program_id: *program_id,
196        accounts,
197        data,
198    }
199}
200
201/// Creates a `InitializeExtraAccountMetaList` instruction.
202pub fn initialize_extra_account_meta_list(
203    program_id: &Pubkey,
204    extra_account_metas_pubkey: &Pubkey,
205    mint_pubkey: &Pubkey,
206    authority_pubkey: &Pubkey,
207    extra_account_metas: &[ExtraAccountMeta],
208) -> Instruction {
209    let data = TransferHookInstruction::InitializeExtraAccountMetaList {
210        extra_account_metas: extra_account_metas.to_vec(),
211    }
212    .pack();
213
214    let accounts = vec![
215        AccountMeta::new(*extra_account_metas_pubkey, false),
216        AccountMeta::new_readonly(*mint_pubkey, false),
217        AccountMeta::new_readonly(*authority_pubkey, true),
218        AccountMeta::new_readonly(system_program::id(), false),
219    ];
220
221    Instruction {
222        program_id: *program_id,
223        accounts,
224        data,
225    }
226}
227
228/// Creates a `UpdateExtraAccountMetaList` instruction.
229pub fn update_extra_account_meta_list(
230    program_id: &Pubkey,
231    extra_account_metas_pubkey: &Pubkey,
232    mint_pubkey: &Pubkey,
233    authority_pubkey: &Pubkey,
234    extra_account_metas: &[ExtraAccountMeta],
235) -> Instruction {
236    let data = TransferHookInstruction::UpdateExtraAccountMetaList {
237        extra_account_metas: extra_account_metas.to_vec(),
238    }
239    .pack();
240
241    let accounts = vec![
242        AccountMeta::new(*extra_account_metas_pubkey, false),
243        AccountMeta::new_readonly(*mint_pubkey, false),
244        AccountMeta::new_readonly(*authority_pubkey, true),
245    ];
246
247    Instruction {
248        program_id: *program_id,
249        accounts,
250        data,
251    }
252}
253
254#[cfg(test)]
255mod test {
256    use {super::*, crate::NAMESPACE, miraland_program::hash, spl_pod::bytemuck::pod_from_bytes};
257
258    #[test]
259    fn validate_packing() {
260        let amount = 111_111_111;
261        let check = TransferHookInstruction::Execute { amount };
262        let packed = check.pack();
263        // Please use ExecuteInstruction::SPL_DISCRIMINATOR in your program, the
264        // following is just for test purposes
265        let preimage = hash::hashv(&[format!("{NAMESPACE}:execute").as_bytes()]);
266        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
267        let mut expect = vec![];
268        expect.extend_from_slice(discriminator.as_ref());
269        expect.extend_from_slice(&amount.to_le_bytes());
270        assert_eq!(packed, expect);
271        let unpacked = TransferHookInstruction::unpack(&expect).unwrap();
272        assert_eq!(unpacked, check);
273    }
274
275    #[test]
276    fn initialize_validation_pubkeys_packing() {
277        let extra_meta_len_bytes = &[
278            1, 0, 0, 0, // `1u32`
279        ];
280        let extra_meta_bytes = &[
281            0, // `AccountMeta`
282            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
283            1, 1, 1, // pubkey
284            0, // is_signer
285            0, // is_writable
286        ];
287        let extra_account_metas =
288            vec![*pod_from_bytes::<ExtraAccountMeta>(extra_meta_bytes).unwrap()];
289        let check = TransferHookInstruction::InitializeExtraAccountMetaList {
290            extra_account_metas,
291        };
292        let packed = check.pack();
293        // Please use INITIALIZE_EXTRA_ACCOUNT_METAS_DISCRIMINATOR in your program,
294        // the following is just for test purposes
295        let preimage =
296            hash::hashv(&[format!("{NAMESPACE}:initialize-extra-account-metas").as_bytes()]);
297        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
298        let mut expect = vec![];
299        expect.extend_from_slice(discriminator.as_ref());
300        expect.extend_from_slice(extra_meta_len_bytes);
301        expect.extend_from_slice(extra_meta_bytes);
302        assert_eq!(packed, expect);
303        let unpacked = TransferHookInstruction::unpack(&expect).unwrap();
304        assert_eq!(unpacked, check);
305    }
306}