Skip to main content

spl_token_group_interface/
instruction.rs

1//! Instruction types
2
3use {
4    alloc::{vec, vec::Vec},
5    bytemuck::{Pod, Zeroable},
6    solana_address::Address,
7    solana_instruction::{account_meta::AccountMeta, Instruction},
8    solana_nullable::MaybeNull,
9    solana_program_error::ProgramError,
10    solana_zero_copy::unaligned::U64,
11    spl_discriminator::{ArrayDiscriminator, SplDiscriminate},
12};
13
14/// Instruction data for initializing a new `Group`
15#[repr(C)]
16#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)]
17#[discriminator_hash_input("spl_token_group_interface:initialize_token_group")]
18pub struct InitializeGroup {
19    /// Update authority for the group
20    pub update_authority: MaybeNull<Address>,
21    /// The maximum number of group members
22    pub max_size: U64,
23}
24
25/// Instruction data for updating the max size of a `Group`
26#[repr(C)]
27#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)]
28#[discriminator_hash_input("spl_token_group_interface:update_group_max_size")]
29pub struct UpdateGroupMaxSize {
30    /// New max size for the group
31    pub max_size: U64,
32}
33
34/// Instruction data for updating the authority of a `Group`
35#[repr(C)]
36#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)]
37#[discriminator_hash_input("spl_token_group_interface:update_authority")]
38pub struct UpdateGroupAuthority {
39    /// New authority for the group, or unset if `None`
40    pub new_authority: MaybeNull<Address>,
41}
42
43/// Instruction data for initializing a new `Member` of a `Group`
44#[repr(C)]
45#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)]
46#[discriminator_hash_input("spl_token_group_interface:initialize_member")]
47pub struct InitializeMember;
48
49/// All instructions that must be implemented in the SPL Token Group Interface
50#[derive(Clone, Debug, PartialEq)]
51pub enum TokenGroupInstruction {
52    /// Initialize a new `Group`
53    ///
54    /// Assumes one has already initialized a mint for the
55    /// group.
56    ///
57    /// Accounts expected by this instruction:
58    ///
59    ///   0. `[w]`  Group
60    ///   1. `[]`   Mint
61    ///   2. `[s]`  Mint authority
62    InitializeGroup(InitializeGroup),
63
64    /// Update the max size of a `Group`
65    ///
66    /// Accounts expected by this instruction:
67    ///
68    ///   0. `[w]`  Group
69    ///   1. `[s]`  Update authority
70    UpdateGroupMaxSize(UpdateGroupMaxSize),
71
72    /// Update the authority of a `Group`
73    ///
74    /// Accounts expected by this instruction:
75    ///
76    ///   0. `[w]`  Group
77    ///   1. `[s]`  Current update authority
78    UpdateGroupAuthority(UpdateGroupAuthority),
79
80    /// Initialize a new `Member` of a `Group`
81    ///
82    /// Assumes the `Group` has already been initialized,
83    /// as well as the mint for the member.
84    ///
85    /// Accounts expected by this instruction:
86    ///
87    ///   0. `[w]`  Member
88    ///   1. `[]`   Member mint
89    ///   1. `[s]`  Member mint authority
90    ///   2. `[w]`  Group
91    ///   3. `[s]`  Group update authority
92    InitializeMember(InitializeMember),
93}
94impl TokenGroupInstruction {
95    /// Unpacks a byte buffer into a `TokenGroupInstruction`
96    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
97        if input.len() < ArrayDiscriminator::LENGTH {
98            return Err(ProgramError::InvalidInstructionData);
99        }
100        let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH);
101        Ok(match discriminator {
102            InitializeGroup::SPL_DISCRIMINATOR_SLICE => {
103                let data = bytemuck::try_from_bytes::<InitializeGroup>(rest)
104                    .map_err(|_| ProgramError::InvalidArgument)?;
105                Self::InitializeGroup(*data)
106            }
107            UpdateGroupMaxSize::SPL_DISCRIMINATOR_SLICE => {
108                let data = bytemuck::try_from_bytes::<UpdateGroupMaxSize>(rest)
109                    .map_err(|_| ProgramError::InvalidArgument)?;
110                Self::UpdateGroupMaxSize(*data)
111            }
112            UpdateGroupAuthority::SPL_DISCRIMINATOR_SLICE => {
113                let data = bytemuck::try_from_bytes::<UpdateGroupAuthority>(rest)
114                    .map_err(|_| ProgramError::InvalidArgument)?;
115                Self::UpdateGroupAuthority(*data)
116            }
117            InitializeMember::SPL_DISCRIMINATOR_SLICE => {
118                let data = bytemuck::try_from_bytes::<InitializeMember>(rest)
119                    .map_err(|_| ProgramError::InvalidArgument)?;
120                Self::InitializeMember(*data)
121            }
122            _ => return Err(ProgramError::InvalidInstructionData),
123        })
124    }
125
126    /// Packs a `TokenGroupInstruction` into a byte buffer.
127    pub fn pack(&self) -> Vec<u8> {
128        let mut buf = vec![];
129        match self {
130            Self::InitializeGroup(data) => {
131                buf.extend_from_slice(InitializeGroup::SPL_DISCRIMINATOR_SLICE);
132                buf.extend_from_slice(bytemuck::bytes_of(data));
133            }
134            Self::UpdateGroupMaxSize(data) => {
135                buf.extend_from_slice(UpdateGroupMaxSize::SPL_DISCRIMINATOR_SLICE);
136                buf.extend_from_slice(bytemuck::bytes_of(data));
137            }
138            Self::UpdateGroupAuthority(data) => {
139                buf.extend_from_slice(UpdateGroupAuthority::SPL_DISCRIMINATOR_SLICE);
140                buf.extend_from_slice(bytemuck::bytes_of(data));
141            }
142            Self::InitializeMember(data) => {
143                buf.extend_from_slice(InitializeMember::SPL_DISCRIMINATOR_SLICE);
144                buf.extend_from_slice(bytemuck::bytes_of(data));
145            }
146        };
147        buf
148    }
149}
150
151/// Creates a `InitializeGroup` instruction
152pub fn initialize_group(
153    program_id: &Address,
154    group: &Address,
155    mint: &Address,
156    mint_authority: &Address,
157    update_authority: Option<Address>,
158    max_size: u64,
159) -> Instruction {
160    let update_authority = MaybeNull::<Address>::try_from(update_authority)
161        .expect("Failed to deserialize `Option<Pubkey>`");
162    let data = TokenGroupInstruction::InitializeGroup(InitializeGroup {
163        update_authority,
164        max_size: max_size.into(),
165    })
166    .pack();
167    Instruction {
168        program_id: *program_id,
169        accounts: vec![
170            AccountMeta::new(*group, false),
171            AccountMeta::new_readonly(*mint, false),
172            AccountMeta::new_readonly(*mint_authority, true),
173        ],
174        data,
175    }
176}
177
178/// Creates a `UpdateGroupMaxSize` instruction
179pub fn update_group_max_size(
180    program_id: &Address,
181    group: &Address,
182    update_authority: &Address,
183    max_size: u64,
184) -> Instruction {
185    let data = TokenGroupInstruction::UpdateGroupMaxSize(UpdateGroupMaxSize {
186        max_size: max_size.into(),
187    })
188    .pack();
189    Instruction {
190        program_id: *program_id,
191        accounts: vec![
192            AccountMeta::new(*group, false),
193            AccountMeta::new_readonly(*update_authority, true),
194        ],
195        data,
196    }
197}
198
199/// Creates a `UpdateGroupAuthority` instruction
200pub fn update_group_authority(
201    program_id: &Address,
202    group: &Address,
203    current_authority: &Address,
204    new_authority: Option<Address>,
205) -> Instruction {
206    let new_authority = MaybeNull::<Address>::try_from(new_authority)
207        .expect("Failed to deserialize `Option<Pubkey>`");
208    let data =
209        TokenGroupInstruction::UpdateGroupAuthority(UpdateGroupAuthority { new_authority }).pack();
210    Instruction {
211        program_id: *program_id,
212        accounts: vec![
213            AccountMeta::new(*group, false),
214            AccountMeta::new_readonly(*current_authority, true),
215        ],
216        data,
217    }
218}
219
220/// Creates a `InitializeMember` instruction
221#[allow(clippy::too_many_arguments)]
222pub fn initialize_member(
223    program_id: &Address,
224    member: &Address,
225    member_mint: &Address,
226    member_mint_authority: &Address,
227    group: &Address,
228    group_update_authority: &Address,
229) -> Instruction {
230    let data = TokenGroupInstruction::InitializeMember(InitializeMember {}).pack();
231    Instruction {
232        program_id: *program_id,
233        accounts: vec![
234            AccountMeta::new(*member, false),
235            AccountMeta::new_readonly(*member_mint, false),
236            AccountMeta::new_readonly(*member_mint_authority, true),
237            AccountMeta::new(*group, false),
238            AccountMeta::new_readonly(*group_update_authority, true),
239        ],
240        data,
241    }
242}
243
244#[cfg(test)]
245mod test {
246    use {super::*, crate::NAMESPACE, alloc::format, solana_sha256_hasher::hashv};
247
248    fn instruction_pack_unpack<I>(instruction: TokenGroupInstruction, discriminator: &[u8], data: I)
249    where
250        I: core::fmt::Debug + PartialEq + Pod + Zeroable + SplDiscriminate,
251    {
252        let mut expect = vec![];
253        expect.extend_from_slice(discriminator.as_ref());
254        expect.extend_from_slice(bytemuck::bytes_of(&data));
255        let packed = instruction.pack();
256        assert_eq!(packed, expect);
257        let unpacked = TokenGroupInstruction::unpack(&expect).unwrap();
258        assert_eq!(unpacked, instruction);
259    }
260
261    #[test]
262    fn initialize_group_pack() {
263        let data = InitializeGroup {
264            update_authority: MaybeNull::<Address>::default(),
265            max_size: 100.into(),
266        };
267        let instruction = TokenGroupInstruction::InitializeGroup(data);
268        let preimage = hashv(&[format!("{NAMESPACE}:initialize_token_group").as_bytes()]);
269        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
270        instruction_pack_unpack::<InitializeGroup>(instruction, discriminator, data);
271    }
272
273    #[test]
274    fn update_group_max_size_pack() {
275        let data = UpdateGroupMaxSize {
276            max_size: 200.into(),
277        };
278        let instruction = TokenGroupInstruction::UpdateGroupMaxSize(data);
279        let preimage = hashv(&[format!("{NAMESPACE}:update_group_max_size").as_bytes()]);
280        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
281        instruction_pack_unpack::<UpdateGroupMaxSize>(instruction, discriminator, data);
282    }
283
284    #[test]
285    fn update_authority_pack() {
286        let data = UpdateGroupAuthority {
287            new_authority: MaybeNull::<Address>::default(),
288        };
289        let instruction = TokenGroupInstruction::UpdateGroupAuthority(data);
290        let preimage = hashv(&[format!("{NAMESPACE}:update_authority").as_bytes()]);
291        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
292        instruction_pack_unpack::<UpdateGroupAuthority>(instruction, discriminator, data);
293    }
294
295    #[test]
296    fn initialize_member_pack() {
297        let data = InitializeMember {};
298        let instruction = TokenGroupInstruction::InitializeMember(data);
299        let preimage = hashv(&[format!("{NAMESPACE}:initialize_member").as_bytes()]);
300        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
301        instruction_pack_unpack::<InitializeMember>(instruction, discriminator, data);
302    }
303}