spl_token_group_interface/
instruction.rs

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