spl_transfer_hook_interface/
instruction.rs

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