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 {
256        super::*, crate::NAMESPACE, solana_sha256_hasher::hashv, spl_pod::bytemuck::pod_from_bytes,
257    };
258
259    #[test]
260    fn system_program_id() {
261        assert_eq!(solana_system_interface::program::id(), SYSTEM_PROGRAM_ID);
262    }
263
264    #[test]
265    fn validate_packing() {
266        let amount = 111_111_111;
267        let check = TransferHookInstruction::Execute { amount };
268        let packed = check.pack();
269        // Please use ExecuteInstruction::SPL_DISCRIMINATOR in your program, the
270        // following is just for test purposes
271        let preimage = hashv(&[format!("{NAMESPACE}:execute").as_bytes()]);
272        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
273        let mut expect = vec![];
274        expect.extend_from_slice(discriminator.as_ref());
275        expect.extend_from_slice(&amount.to_le_bytes());
276        assert_eq!(packed, expect);
277        let unpacked = TransferHookInstruction::unpack(&expect).unwrap();
278        assert_eq!(unpacked, check);
279    }
280
281    #[test]
282    fn initialize_validation_pubkeys_packing() {
283        let extra_meta_len_bytes = &[
284            1, 0, 0, 0, // `1u32`
285        ];
286        let extra_meta_bytes = &[
287            0, // `AccountMeta`
288            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,
289            1, 1, 1, // pubkey
290            0, // is_signer
291            0, // is_writable
292        ];
293        let extra_account_metas =
294            vec![*pod_from_bytes::<ExtraAccountMeta>(extra_meta_bytes).unwrap()];
295        let check = TransferHookInstruction::InitializeExtraAccountMetaList {
296            extra_account_metas,
297        };
298        let packed = check.pack();
299        // Please use INITIALIZE_EXTRA_ACCOUNT_METAS_DISCRIMINATOR in your program,
300        // the following is just for test purposes
301        let preimage = hashv(&[format!("{NAMESPACE}:initialize-extra-account-metas").as_bytes()]);
302        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
303        let mut expect = vec![];
304        expect.extend_from_slice(discriminator.as_ref());
305        expect.extend_from_slice(extra_meta_len_bytes);
306        expect.extend_from_slice(extra_meta_bytes);
307        assert_eq!(packed, expect);
308        let unpacked = TransferHookInstruction::unpack(&expect).unwrap();
309        assert_eq!(unpacked, check);
310    }
311}