Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit a54425b

Browse files
author
Joe C
authored
token group: init interface (#5512)
* token group: init interface * remove extra metas comment * reworked init member instruction * swap `u32` for `PodU32` * update initialize_member instruction * bump instruction discriminators * rename state to follow SPL patterns
1 parent fe6a570 commit a54425b

File tree

7 files changed

+552
-0
lines changed

7 files changed

+552
-0
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ members = [
4242
"stake-pool/cli",
4343
"stake-pool/program",
4444
"stateless-asks/program",
45+
"token-group/interface",
4546
"token-lending/cli",
4647
"token-lending/program",
4748
"token-metadata/example",

token-group/interface/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "spl-token-group-interface"
3+
version = "0.1.0"
4+
description = "Solana Program Library Token Group Interface"
5+
authors = ["Solana Labs Maintainers <[email protected]>"]
6+
repository = "https://github.com/solana-labs/solana-program-library"
7+
license = "Apache-2.0"
8+
edition = "2021"
9+
10+
[dependencies]
11+
bytemuck = "1.14.0"
12+
solana-program = "1.16.16"
13+
spl-discriminator = { version = "0.1.0" , path = "../../libraries/discriminator" }
14+
spl-pod = { version = "0.1.0" , path = "../../libraries/pod", features = ["borsh"] }
15+
spl-program-error = { version = "0.3.0" , path = "../../libraries/program-error" }
16+
17+
[dev-dependencies]
18+
spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value", features = ["derive"] }
19+
20+
[lib]
21+
crate-type = ["cdylib", "lib"]
22+
23+
[package.metadata.docs.rs]
24+
targets = ["x86_64-unknown-linux-gnu"]

token-group/interface/src/error.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//! Interface error types
2+
3+
use spl_program_error::*;
4+
5+
/// Errors that may be returned by the interface.
6+
#[spl_program_error]
7+
pub enum TokenGroupError {
8+
/// Size is greater than proposed max size
9+
#[error("Size is greater than proposed max size")]
10+
SizeExceedsNewMaxSize,
11+
/// Size is greater than max size
12+
#[error("Size is greater than max size")]
13+
SizeExceedsMaxSize,
14+
}
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
//! Instruction types
2+
3+
use {
4+
bytemuck::{Pod, Zeroable},
5+
solana_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")]
22+
pub 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")]
33+
pub 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")]
42+
pub 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")]
51+
pub struct InitializeMember;
52+
53+
/// All instructions that must be implemented in the SPL Token Group Interface
54+
#[derive(Clone, Debug, PartialEq)]
55+
pub 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+
}
98+
impl 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
152+
pub 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
179+
pub 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
200+
pub 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)]
222+
pub 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)]
245+
mod test {
246+
use {super::*, crate::NAMESPACE, solana_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+
}

token-group/interface/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//! Crate defining the SPL Token Group Interface
2+
3+
#![deny(missing_docs)]
4+
#![cfg_attr(not(test), forbid(unsafe_code))]
5+
6+
pub mod error;
7+
pub mod instruction;
8+
pub mod state;
9+
10+
/// Namespace for all programs implementing spl-token-group
11+
pub const NAMESPACE: &str = "spl_token_group_interface";

0 commit comments

Comments
 (0)