spl_token_group_interface/
instruction.rs

1//! Instruction types
2
3use {
4    bytemuck::{Pod, Zeroable},
5    miraland_program::{
6        instruction::{AccountMeta, Instruction},
7        program_error::ProgramError,
8        pubkey::Pubkey,
9    },
10    spl_discriminator::{ArrayDiscriminator, SplDiscriminate},
11    spl_pod::{
12        bytemuck::{pod_bytes_of, pod_from_bytes},
13        optional_keys::OptionalNonZeroPubkey,
14        primitives::PodU32,
15    },
16};
17
18/// Instruction data for initializing a new `Group`
19#[repr(C)]
20#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)]
21#[discriminator_hash_input("spl_token_group_interface:initialize_token_group")]
22pub struct InitializeGroup {
23    /// Update authority for the group
24    pub update_authority: OptionalNonZeroPubkey,
25    /// The maximum number of group members
26    pub max_size: PodU32,
27}
28
29/// Instruction data for updating the max size of a `Group`
30#[repr(C)]
31#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)]
32#[discriminator_hash_input("spl_token_group_interface:update_group_max_size")]
33pub struct UpdateGroupMaxSize {
34    /// New max size for the group
35    pub max_size: PodU32,
36}
37
38/// Instruction data for updating the authority of a `Group`
39#[repr(C)]
40#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)]
41#[discriminator_hash_input("spl_token_group_interface:update_authority")]
42pub struct UpdateGroupAuthority {
43    /// New authority for the group, or unset if `None`
44    pub new_authority: OptionalNonZeroPubkey,
45}
46
47/// Instruction data for initializing a new `Member` of a `Group`
48#[repr(C)]
49#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)]
50#[discriminator_hash_input("spl_token_group_interface:initialize_member")]
51pub struct InitializeMember;
52
53/// All instructions that must be implemented in the Solarti Token Group Interface
54#[derive(Clone, Debug, PartialEq)]
55pub enum TokenGroupInstruction {
56    /// Initialize a new `Group`
57    ///
58    /// Assumes one has already initialized a mint for the
59    /// group.
60    ///
61    /// Accounts expected by this instruction:
62    ///
63    ///   0. `[w]`  Group
64    ///   1. `[]`   Mint
65    ///   2. `[s]`  Mint authority
66    InitializeGroup(InitializeGroup),
67
68    /// Update the max size of a `Group`
69    ///
70    /// Accounts expected by this instruction:
71    ///
72    ///   0. `[w]`  Group
73    ///   1. `[s]`  Update authority
74    UpdateGroupMaxSize(UpdateGroupMaxSize),
75
76    /// Update the authority of a `Group`
77    ///
78    /// Accounts expected by this instruction:
79    ///
80    ///   0. `[w]`  Group
81    ///   1. `[s]`  Current update authority
82    UpdateGroupAuthority(UpdateGroupAuthority),
83
84    /// Initialize a new `Member` of a `Group`
85    ///
86    /// Assumes the `Group` has already been initialized,
87    /// as well as the mint for the member.
88    ///
89    /// Accounts expected by this instruction:
90    ///
91    ///   0. `[w]`  Member
92    ///   1. `[]`   Member mint
93    ///   1. `[s]`  Member mint authority
94    ///   2. `[w]`  Group
95    ///   3. `[s]`  Group update authority
96    InitializeMember(InitializeMember),
97}
98impl TokenGroupInstruction {
99    /// Unpacks a byte buffer into a `TokenGroupInstruction`
100    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
101        if input.len() < ArrayDiscriminator::LENGTH {
102            return Err(ProgramError::InvalidInstructionData);
103        }
104        let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH);
105        Ok(match discriminator {
106            InitializeGroup::SPL_DISCRIMINATOR_SLICE => {
107                let data = pod_from_bytes::<InitializeGroup>(rest)?;
108                Self::InitializeGroup(*data)
109            }
110            UpdateGroupMaxSize::SPL_DISCRIMINATOR_SLICE => {
111                let data = pod_from_bytes::<UpdateGroupMaxSize>(rest)?;
112                Self::UpdateGroupMaxSize(*data)
113            }
114            UpdateGroupAuthority::SPL_DISCRIMINATOR_SLICE => {
115                let data = pod_from_bytes::<UpdateGroupAuthority>(rest)?;
116                Self::UpdateGroupAuthority(*data)
117            }
118            InitializeMember::SPL_DISCRIMINATOR_SLICE => {
119                let data = pod_from_bytes::<InitializeMember>(rest)?;
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(pod_bytes_of(data));
133            }
134            Self::UpdateGroupMaxSize(data) => {
135                buf.extend_from_slice(UpdateGroupMaxSize::SPL_DISCRIMINATOR_SLICE);
136                buf.extend_from_slice(pod_bytes_of(data));
137            }
138            Self::UpdateGroupAuthority(data) => {
139                buf.extend_from_slice(UpdateGroupAuthority::SPL_DISCRIMINATOR_SLICE);
140                buf.extend_from_slice(pod_bytes_of(data));
141            }
142            Self::InitializeMember(data) => {
143                buf.extend_from_slice(InitializeMember::SPL_DISCRIMINATOR_SLICE);
144                buf.extend_from_slice(pod_bytes_of(data));
145            }
146        };
147        buf
148    }
149}
150
151/// Creates a `InitializeGroup` instruction
152pub fn initialize_group(
153    program_id: &Pubkey,
154    group: &Pubkey,
155    mint: &Pubkey,
156    mint_authority: &Pubkey,
157    update_authority: Option<Pubkey>,
158    max_size: u32,
159) -> Instruction {
160    let update_authority = OptionalNonZeroPubkey::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: &Pubkey,
181    group: &Pubkey,
182    update_authority: &Pubkey,
183    max_size: u32,
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: &Pubkey,
202    group: &Pubkey,
203    current_authority: &Pubkey,
204    new_authority: Option<Pubkey>,
205) -> Instruction {
206    let new_authority = OptionalNonZeroPubkey::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: &Pubkey,
224    member: &Pubkey,
225    member_mint: &Pubkey,
226    member_mint_authority: &Pubkey,
227    group: &Pubkey,
228    group_update_authority: &Pubkey,
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, miraland_program::hash};
247
248    #[repr(C)]
249    #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)]
250    #[discriminator_hash_input("mock_group")]
251    struct MockGroup;
252
253    fn instruction_pack_unpack<I>(instruction: TokenGroupInstruction, discriminator: &[u8], data: I)
254    where
255        I: core::fmt::Debug + PartialEq + Pod + Zeroable + SplDiscriminate,
256    {
257        let mut expect = vec![];
258        expect.extend_from_slice(discriminator.as_ref());
259        expect.extend_from_slice(pod_bytes_of(&data));
260        let packed = instruction.pack();
261        assert_eq!(packed, expect);
262        let unpacked = TokenGroupInstruction::unpack(&expect).unwrap();
263        assert_eq!(unpacked, instruction);
264    }
265
266    #[test]
267    fn initialize_group_pack() {
268        let data = InitializeGroup {
269            update_authority: OptionalNonZeroPubkey::default(),
270            max_size: 100.into(),
271        };
272        let instruction = TokenGroupInstruction::InitializeGroup(data);
273        let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_token_group").as_bytes()]);
274        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
275        instruction_pack_unpack::<InitializeGroup>(instruction, discriminator, data);
276    }
277
278    #[test]
279    fn update_group_max_size_pack() {
280        let data = UpdateGroupMaxSize {
281            max_size: 200.into(),
282        };
283        let instruction = TokenGroupInstruction::UpdateGroupMaxSize(data);
284        let preimage = hash::hashv(&[format!("{NAMESPACE}:update_group_max_size").as_bytes()]);
285        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
286        instruction_pack_unpack::<UpdateGroupMaxSize>(instruction, discriminator, data);
287    }
288
289    #[test]
290    fn update_authority_pack() {
291        let data = UpdateGroupAuthority {
292            new_authority: OptionalNonZeroPubkey::default(),
293        };
294        let instruction = TokenGroupInstruction::UpdateGroupAuthority(data);
295        let preimage = hash::hashv(&[format!("{NAMESPACE}:update_authority").as_bytes()]);
296        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
297        instruction_pack_unpack::<UpdateGroupAuthority>(instruction, discriminator, data);
298    }
299
300    #[test]
301    fn initialize_member_pack() {
302        let data = InitializeMember {};
303        let instruction = TokenGroupInstruction::InitializeMember(data);
304        let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_member").as_bytes()]);
305        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
306        instruction_pack_unpack::<InitializeMember>(instruction, discriminator, data);
307    }
308}