From aa4867dbbc658822f1cde6ff0a9a805bff70f678 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 13 Oct 2023 15:45:25 +0200 Subject: [PATCH 1/7] token group: init interface --- Cargo.lock | 12 + Cargo.toml | 1 + token-group/interface/Cargo.toml | 24 ++ token-group/interface/src/error.rs | 14 + token-group/interface/src/instruction.rs | 330 +++++++++++++++++++++++ token-group/interface/src/lib.rs | 11 + token-group/interface/src/state.rs | 180 +++++++++++++ 7 files changed, 572 insertions(+) create mode 100644 token-group/interface/Cargo.toml create mode 100644 token-group/interface/src/error.rs create mode 100644 token-group/interface/src/instruction.rs create mode 100644 token-group/interface/src/lib.rs create mode 100644 token-group/interface/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index d9203781e30..7cc37cfe0ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7176,6 +7176,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "spl-token-group-interface" +version = "0.1.0" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator 0.1.0", + "spl-pod 0.1.0", + "spl-program-error 0.3.0", + "spl-type-length-value 0.3.0", +] + [[package]] name = "spl-token-lending" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 45fd58854fd..209c1a091e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ members = [ "stake-pool/cli", "stake-pool/program", "stateless-asks/program", + "token-group/interface", "token-lending/cli", "token-lending/program", "token-metadata/example", diff --git a/token-group/interface/Cargo.toml b/token-group/interface/Cargo.toml new file mode 100644 index 00000000000..40271ad1092 --- /dev/null +++ b/token-group/interface/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "spl-token-group-interface" +version = "0.1.0" +description = "Solana Program Library Token Group Interface" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2021" + +[dependencies] +bytemuck = "1.14.0" +solana-program = "1.16.3" +spl-discriminator = { version = "0.1.0" , path = "../../libraries/discriminator" } +spl-pod = { version = "0.1.0" , path = "../../libraries/pod", features = ["borsh"] } +spl-program-error = { version = "0.3.0" , path = "../../libraries/program-error" } + +[dev-dependencies] +spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value", features = ["derive"] } + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/token-group/interface/src/error.rs b/token-group/interface/src/error.rs new file mode 100644 index 00000000000..f2eff4d60f2 --- /dev/null +++ b/token-group/interface/src/error.rs @@ -0,0 +1,14 @@ +//! Interface error types + +use spl_program_error::*; + +/// Errors that may be returned by the interface. +#[spl_program_error] +pub enum TokenGroupError { + /// Size is greater than proposed max size + #[error("Size is greater than proposed max size")] + SizeExceedsNewMaxSize, + /// Size is greater than max size + #[error("Size is greater than max size")] + SizeExceedsMaxSize, +} diff --git a/token-group/interface/src/instruction.rs b/token-group/interface/src/instruction.rs new file mode 100644 index 00000000000..20b246a1981 --- /dev/null +++ b/token-group/interface/src/instruction.rs @@ -0,0 +1,330 @@ +//! Instruction types + +use { + bytemuck::{Pod, Zeroable}, + solana_program::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + }, + spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, + spl_pod::{ + bytemuck::{pod_bytes_of, pod_from_bytes}, + optional_keys::OptionalNonZeroPubkey, + }, +}; + +/// Instruction data for initializing a new `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:initialize_group")] +pub struct InitializeGroup { + /// Update authority for the group + pub update_authority: OptionalNonZeroPubkey, + /// The maximum number of group members + pub max_size: u32, +} + +/// Instruction data for updating the max size of a `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:update_group_max_size")] +pub struct UpdateGroupMaxSize { + /// New max size for the group + pub max_size: u32, +} + +/// Instruction data for updating the authority of a `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:update_group_authority")] +pub struct UpdateGroupAuthority { + /// New authority for the group, or unset if `None` + pub new_authority: OptionalNonZeroPubkey, +} + +/// Instruction data for initializing a new `Member` of a `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:initialize_member")] +pub struct InitializeMember { + /// The pubkey of the `Group` + pub group: Pubkey, + /// The member number + pub member_number: u32, +} + +/// All instructions that must be implemented in the SPL Token Group Interface +/// +/// Note: Any instruction can be extended using additional required accounts by +/// using the `InitializeExtraAccountMetaList` instruction to write +/// configurations for extra required accounts into validation data +/// corresponding to an instruction's unique discriminator. +#[derive(Clone, Debug, PartialEq)] +pub enum TokenGroupInstruction { + /// Initialize a new `Group` + /// + /// Assumes one has already initialized a mint for the + /// group. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[w]` Group + /// 1. `[]` Mint + /// 2. `[s]` Mint authority + InitializeGroup(InitializeGroup), + + /// Update the max size of a `Group` + /// + /// Accounts expected by this instruction: + /// + /// 0. `[w]` Group + /// 1. `[s]` Update authority + UpdateGroupMaxSize(UpdateGroupMaxSize), + + /// Update the authority of a `Group` + /// + /// Accounts expected by this instruction: + /// + /// 0. `[w]` Group + /// 1. `[s]` Current update authority + UpdateGroupAuthority(UpdateGroupAuthority), + + /// Initialize a new `Member` of a `Group` + /// + /// Assumes the `Group` has already been initialized, + /// as well as the mint for the member. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[w]` Member + /// 1. `[]` Member Mint + /// 2. `[w]` Group + /// 3. `[]` Group Mint + /// 4. `[s]` Group Mint authority + InitializeMember(InitializeMember), +} +impl TokenGroupInstruction { + /// Unpacks a byte buffer into a `TokenGroupInstruction` + pub fn unpack(input: &[u8]) -> Result { + if input.len() < ArrayDiscriminator::LENGTH { + return Err(ProgramError::InvalidInstructionData); + } + let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH); + Ok(match discriminator { + InitializeGroup::SPL_DISCRIMINATOR_SLICE => { + let data = pod_from_bytes::(rest)?; + Self::InitializeGroup(*data) + } + UpdateGroupMaxSize::SPL_DISCRIMINATOR_SLICE => { + let data = pod_from_bytes::(rest)?; + Self::UpdateGroupMaxSize(*data) + } + UpdateGroupAuthority::SPL_DISCRIMINATOR_SLICE => { + let data = pod_from_bytes::(rest)?; + Self::UpdateGroupAuthority(*data) + } + InitializeMember::SPL_DISCRIMINATOR_SLICE => { + let data = pod_from_bytes::(rest)?; + Self::InitializeMember(*data) + } + _ => return Err(ProgramError::InvalidInstructionData), + }) + } + + /// Packs a `TokenGroupInstruction` into a byte buffer. + pub fn pack(&self) -> Vec { + let mut buf = vec![]; + match self { + Self::InitializeGroup(data) => { + buf.extend_from_slice(InitializeGroup::SPL_DISCRIMINATOR_SLICE); + buf.extend_from_slice(pod_bytes_of(data)); + } + Self::UpdateGroupMaxSize(data) => { + buf.extend_from_slice(UpdateGroupMaxSize::SPL_DISCRIMINATOR_SLICE); + buf.extend_from_slice(pod_bytes_of(data)); + } + Self::UpdateGroupAuthority(data) => { + buf.extend_from_slice(UpdateGroupAuthority::SPL_DISCRIMINATOR_SLICE); + buf.extend_from_slice(pod_bytes_of(data)); + } + Self::InitializeMember(data) => { + buf.extend_from_slice(InitializeMember::SPL_DISCRIMINATOR_SLICE); + buf.extend_from_slice(pod_bytes_of(data)); + } + }; + buf + } +} + +/// Creates a `InitializeGroup` instruction +pub fn initialize_group( + program_id: &Pubkey, + group: &Pubkey, + mint: &Pubkey, + mint_authority: &Pubkey, + update_authority: Option, + max_size: u32, + extra_account_metas: &[AccountMeta], +) -> Instruction { + let update_authority = OptionalNonZeroPubkey::try_from(update_authority) + .expect("Failed to deserialize `Option`"); + let data = TokenGroupInstruction::InitializeGroup(InitializeGroup { + update_authority, + max_size, + }) + .pack(); + let mut accounts = vec![ + AccountMeta::new(*group, false), + AccountMeta::new_readonly(*mint, false), + AccountMeta::new_readonly(*mint_authority, true), + ]; + accounts.extend_from_slice(extra_account_metas); + + Instruction { + program_id: *program_id, + accounts, + data, + } +} + +/// Creates a `UpdateGroupMaxSize` instruction +pub fn update_group_max_size( + program_id: &Pubkey, + group: &Pubkey, + update_authority: &Pubkey, + max_size: u32, +) -> Instruction { + let data = TokenGroupInstruction::UpdateGroupMaxSize(UpdateGroupMaxSize { max_size }).pack(); + Instruction { + program_id: *program_id, + accounts: vec![ + AccountMeta::new(*group, false), + AccountMeta::new_readonly(*update_authority, true), + ], + data, + } +} + +/// Creates a `UpdateGroupAuthority` instruction +pub fn update_group_authority( + program_id: &Pubkey, + group: &Pubkey, + current_authority: &Pubkey, + new_authority: Option, +) -> Instruction { + let new_authority = OptionalNonZeroPubkey::try_from(new_authority) + .expect("Failed to deserialize `Option`"); + let data = + TokenGroupInstruction::UpdateGroupAuthority(UpdateGroupAuthority { new_authority }).pack(); + Instruction { + program_id: *program_id, + accounts: vec![ + AccountMeta::new(*group, false), + AccountMeta::new_readonly(*current_authority, true), + ], + data, + } +} + +/// Creates a `InitializeMember` instruction +#[allow(clippy::too_many_arguments)] +pub fn initialize_member( + program_id: &Pubkey, + group: &Pubkey, + group_mint: &Pubkey, + group_mint_authority: &Pubkey, + member: &Pubkey, + member_mint: &Pubkey, + member_mint_authority: &Pubkey, + member_number: u32, + extra_account_metas: &[AccountMeta], +) -> Instruction { + let data = TokenGroupInstruction::InitializeMember(InitializeMember { + group: *group, + member_number, + }) + .pack(); + let mut accounts = vec![ + AccountMeta::new(*member, false), + AccountMeta::new_readonly(*member_mint, false), + AccountMeta::new_readonly(*member_mint_authority, true), + AccountMeta::new(*group, false), + AccountMeta::new_readonly(*group_mint, false), + AccountMeta::new_readonly(*group_mint_authority, true), + ]; + accounts.extend_from_slice(extra_account_metas); + + Instruction { + program_id: *program_id, + accounts, + data, + } +} + +#[cfg(test)] +mod test { + use {super::*, crate::NAMESPACE, solana_program::hash}; + + #[repr(C)] + #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)] + #[discriminator_hash_input("mock_group")] + struct MockGroup; + + fn instruction_pack_unpack(instruction: TokenGroupInstruction, discriminator: &[u8], data: I) + where + I: core::fmt::Debug + PartialEq + Pod + Zeroable + SplDiscriminate, + { + let mut expect = vec![]; + expect.extend_from_slice(discriminator.as_ref()); + expect.extend_from_slice(pod_bytes_of(&data)); + let packed = instruction.pack(); + assert_eq!(packed, expect); + let unpacked = TokenGroupInstruction::unpack(&expect).unwrap(); + assert_eq!(unpacked, instruction); + } + + #[test] + fn initialize_group_pack() { + let data = InitializeGroup { + update_authority: OptionalNonZeroPubkey::default(), + max_size: 100, + }; + let instruction = TokenGroupInstruction::InitializeGroup(data); + let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_group").as_bytes()]); + let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; + instruction_pack_unpack::(instruction, discriminator, data); + } + + #[test] + fn update_group_max_size_pack() { + let data = UpdateGroupMaxSize { max_size: 200 }; + let instruction = TokenGroupInstruction::UpdateGroupMaxSize(data); + let preimage = hash::hashv(&[format!("{NAMESPACE}:update_group_max_size").as_bytes()]); + let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; + instruction_pack_unpack::(instruction, discriminator, data); + } + + #[test] + fn update_authority_pack() { + let data = UpdateGroupAuthority { + new_authority: OptionalNonZeroPubkey::default(), + }; + let instruction = TokenGroupInstruction::UpdateGroupAuthority(data); + let preimage = hash::hashv(&[format!("{NAMESPACE}:update_group_authority").as_bytes()]); + let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; + instruction_pack_unpack::(instruction, discriminator, data); + } + + #[test] + fn initialize_member_pack() { + let data = InitializeMember { + group: Pubkey::new_unique(), + member_number: 100, + }; + let instruction = TokenGroupInstruction::InitializeMember(data); + let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_member").as_bytes()]); + let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; + instruction_pack_unpack::(instruction, discriminator, data); + } +} diff --git a/token-group/interface/src/lib.rs b/token-group/interface/src/lib.rs new file mode 100644 index 00000000000..6867b86acf8 --- /dev/null +++ b/token-group/interface/src/lib.rs @@ -0,0 +1,11 @@ +//! Crate defining the SPL Token Group Interface + +#![deny(missing_docs)] +#![cfg_attr(not(test), forbid(unsafe_code))] + +pub mod error; +pub mod instruction; +pub mod state; + +/// Namespace for all programs implementing spl-token-group +pub const NAMESPACE: &str = "spl_token_group_interface"; diff --git a/token-group/interface/src/state.rs b/token-group/interface/src/state.rs new file mode 100644 index 00000000000..b4a40cb0534 --- /dev/null +++ b/token-group/interface/src/state.rs @@ -0,0 +1,180 @@ +//! Interface state types + +use { + crate::error::TokenGroupError, + bytemuck::{Pod, Zeroable}, + solana_program::{program_error::ProgramError, pubkey::Pubkey}, + spl_discriminator::SplDiscriminate, + spl_pod::{error::PodSliceError, optional_keys::OptionalNonZeroPubkey}, +}; + +/// Data struct for a `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:group")] +pub struct Group { + /// The authority that can sign to update the group + pub update_authority: OptionalNonZeroPubkey, + /// The current number of group members + pub size: u32, + /// The maximum number of group members + pub max_size: u32, +} + +impl Group { + /// Creates a new `Group` state + pub fn new(update_authority: OptionalNonZeroPubkey, max_size: u32) -> Self { + Self { + update_authority, + size: 0, + max_size, + } + } + + /// Updates the max size for a group + pub fn update_max_size(&mut self, new_max_size: u32) -> Result<(), ProgramError> { + // The new max size cannot be less than the current size + if new_max_size < self.size { + return Err(TokenGroupError::SizeExceedsNewMaxSize.into()); + } + self.max_size = new_max_size; + Ok(()) + } + + /// Increment the size for a group, returning the new size + pub fn increment_size(&mut self) -> Result { + // The new size cannot be greater than the max size + let new_size = self + .size + .checked_add(1) + .ok_or::(PodSliceError::CalculationFailure.into())?; + if new_size > self.max_size { + return Err(TokenGroupError::SizeExceedsMaxSize.into()); + } + self.size = new_size; + Ok(self.size) + } +} + +/// Data struct for a `Member` of a `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:member")] +pub struct Member { + /// The pubkey of the `Group` + pub group: Pubkey, + /// The member number + pub member_number: u32, +} +impl Member { + /// Creates a new `Member` state + pub fn new(group: Pubkey, member_number: u32) -> Self { + Self { + group, + member_number, + } + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::NAMESPACE, + solana_program::hash, + spl_discriminator::ArrayDiscriminator, + spl_type_length_value::state::{TlvState, TlvStateBorrowed, TlvStateMut}, + std::mem::size_of, + }; + + #[test] + fn discriminators() { + let preimage = hash::hashv(&[format!("{NAMESPACE}:group").as_bytes()]); + let discriminator = + ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap(); + assert_eq!(Group::SPL_DISCRIMINATOR, discriminator); + + let preimage = hash::hashv(&[format!("{NAMESPACE}:member").as_bytes()]); + let discriminator = + ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap(); + assert_eq!(Member::SPL_DISCRIMINATOR, discriminator); + } + + #[test] + fn tlv_state_pack() { + // Make sure we can pack more than one instance of each type + let group = Group { + update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), + size: 10, + max_size: 20, + }; + + let member = Member { + group: Pubkey::new_unique(), + member_number: 0, + }; + + let account_size = TlvStateBorrowed::get_base_len() + + size_of::() + + TlvStateBorrowed::get_base_len() + + size_of::(); + let mut buffer = vec![0; account_size]; + let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); + + let group_data = state.init_value::(false).unwrap().0; + *group_data = group; + + let member_data = state.init_value::(false).unwrap().0; + *member_data = member; + + assert_eq!(state.get_first_value::().unwrap(), &group); + assert_eq!(state.get_first_value::().unwrap(), &member); + } + + #[test] + fn update_max_size() { + // Test with a `Some` max size + let max_size = 10; + let mut group = Group { + update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), + size: 0, + max_size, + }; + + let new_max_size = 30; + group.update_max_size(new_max_size).unwrap(); + assert_eq!(group.max_size, new_max_size); + + // Change the current size to 30 + group.size = 30; + + // Try to set the max size to 20, which is less than the current size + let new_max_size = 20; + assert_eq!( + group.update_max_size(new_max_size), + Err(ProgramError::from(TokenGroupError::SizeExceedsNewMaxSize)) + ); + + let new_max_size = 30; + group.update_max_size(new_max_size).unwrap(); + assert_eq!(group.max_size, new_max_size); + } + + #[test] + fn increment_current_size() { + let mut group = Group { + update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), + size: 0, + max_size: 1, + }; + + group.increment_size().unwrap(); + assert_eq!(group.size, 1); + + // Try to increase the current size to 2, which is greater than the max size + assert_eq!( + group.increment_size(), + Err(ProgramError::from(TokenGroupError::SizeExceedsMaxSize)) + ); + } +} From 8381d96b9db989207154bcb1a103dc86c9faefd2 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 13 Oct 2023 16:37:10 +0200 Subject: [PATCH 2/7] remove extra metas comment --- token-group/interface/src/instruction.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/token-group/interface/src/instruction.rs b/token-group/interface/src/instruction.rs index 20b246a1981..f57a8fbc5e8 100644 --- a/token-group/interface/src/instruction.rs +++ b/token-group/interface/src/instruction.rs @@ -55,11 +55,6 @@ pub struct InitializeMember { } /// All instructions that must be implemented in the SPL Token Group Interface -/// -/// Note: Any instruction can be extended using additional required accounts by -/// using the `InitializeExtraAccountMetaList` instruction to write -/// configurations for extra required accounts into validation data -/// corresponding to an instruction's unique discriminator. #[derive(Clone, Debug, PartialEq)] pub enum TokenGroupInstruction { /// Initialize a new `Group` From 9cda74a1b22f72165e0a6404afae06fc104c2d9c Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 13 Oct 2023 16:57:56 +0200 Subject: [PATCH 3/7] reworked init member instruction --- token-group/interface/src/instruction.rs | 39 +++++------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/token-group/interface/src/instruction.rs b/token-group/interface/src/instruction.rs index f57a8fbc5e8..c2dc96fc217 100644 --- a/token-group/interface/src/instruction.rs +++ b/token-group/interface/src/instruction.rs @@ -47,12 +47,7 @@ pub struct UpdateGroupAuthority { #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] #[discriminator_hash_input("spl_token_group_interface:initialize_member")] -pub struct InitializeMember { - /// The pubkey of the `Group` - pub group: Pubkey, - /// The member number - pub member_number: u32, -} +pub struct InitializeMember; /// All instructions that must be implemented in the SPL Token Group Interface #[derive(Clone, Debug, PartialEq)] @@ -93,10 +88,8 @@ pub enum TokenGroupInstruction { /// Accounts expected by this instruction: /// /// 0. `[w]` Member - /// 1. `[]` Member Mint - /// 2. `[w]` Group - /// 3. `[]` Group Mint - /// 4. `[s]` Group Mint authority + /// 1. `[w]` Group + /// 2. `[s]` Group update authority InitializeMember(InitializeMember), } impl TokenGroupInstruction { @@ -227,28 +220,15 @@ pub fn update_group_authority( pub fn initialize_member( program_id: &Pubkey, group: &Pubkey, - group_mint: &Pubkey, - group_mint_authority: &Pubkey, + group_update_authority: &Pubkey, member: &Pubkey, - member_mint: &Pubkey, - member_mint_authority: &Pubkey, - member_number: u32, - extra_account_metas: &[AccountMeta], ) -> Instruction { - let data = TokenGroupInstruction::InitializeMember(InitializeMember { - group: *group, - member_number, - }) - .pack(); - let mut accounts = vec![ + let data = TokenGroupInstruction::InitializeMember(InitializeMember {}).pack(); + let accounts = vec![ AccountMeta::new(*member, false), - AccountMeta::new_readonly(*member_mint, false), - AccountMeta::new_readonly(*member_mint_authority, true), AccountMeta::new(*group, false), - AccountMeta::new_readonly(*group_mint, false), - AccountMeta::new_readonly(*group_mint_authority, true), + AccountMeta::new_readonly(*group_update_authority, true), ]; - accounts.extend_from_slice(extra_account_metas); Instruction { program_id: *program_id, @@ -313,10 +293,7 @@ mod test { #[test] fn initialize_member_pack() { - let data = InitializeMember { - group: Pubkey::new_unique(), - member_number: 100, - }; + let data = InitializeMember {}; let instruction = TokenGroupInstruction::InitializeMember(data); let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_member").as_bytes()]); let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; From 9d01840ab1e41e79a576658138c887fb97bc43b6 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 16 Oct 2023 13:31:09 +0200 Subject: [PATCH 4/7] swap `u32` for `PodU32` --- token-group/interface/src/instruction.rs | 18 ++++++---- token-group/interface/src/state.rs | 43 ++++++++++++------------ 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/token-group/interface/src/instruction.rs b/token-group/interface/src/instruction.rs index c2dc96fc217..176b6647155 100644 --- a/token-group/interface/src/instruction.rs +++ b/token-group/interface/src/instruction.rs @@ -11,6 +11,7 @@ use { spl_pod::{ bytemuck::{pod_bytes_of, pod_from_bytes}, optional_keys::OptionalNonZeroPubkey, + primitives::PodU32, }, }; @@ -22,7 +23,7 @@ pub struct InitializeGroup { /// Update authority for the group pub update_authority: OptionalNonZeroPubkey, /// The maximum number of group members - pub max_size: u32, + pub max_size: PodU32, } /// Instruction data for updating the max size of a `Group` @@ -31,7 +32,7 @@ pub struct InitializeGroup { #[discriminator_hash_input("spl_token_group_interface:update_group_max_size")] pub struct UpdateGroupMaxSize { /// New max size for the group - pub max_size: u32, + pub max_size: PodU32, } /// Instruction data for updating the authority of a `Group` @@ -159,7 +160,7 @@ pub fn initialize_group( .expect("Failed to deserialize `Option`"); let data = TokenGroupInstruction::InitializeGroup(InitializeGroup { update_authority, - max_size, + max_size: max_size.into(), }) .pack(); let mut accounts = vec![ @@ -183,7 +184,10 @@ pub fn update_group_max_size( update_authority: &Pubkey, max_size: u32, ) -> Instruction { - let data = TokenGroupInstruction::UpdateGroupMaxSize(UpdateGroupMaxSize { max_size }).pack(); + let data = TokenGroupInstruction::UpdateGroupMaxSize(UpdateGroupMaxSize { + max_size: max_size.into(), + }) + .pack(); Instruction { program_id: *program_id, accounts: vec![ @@ -263,7 +267,7 @@ mod test { fn initialize_group_pack() { let data = InitializeGroup { update_authority: OptionalNonZeroPubkey::default(), - max_size: 100, + max_size: 100.into(), }; let instruction = TokenGroupInstruction::InitializeGroup(data); let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_group").as_bytes()]); @@ -273,7 +277,9 @@ mod test { #[test] fn update_group_max_size_pack() { - let data = UpdateGroupMaxSize { max_size: 200 }; + let data = UpdateGroupMaxSize { + max_size: 200.into(), + }; let instruction = TokenGroupInstruction::UpdateGroupMaxSize(data); let preimage = hash::hashv(&[format!("{NAMESPACE}:update_group_max_size").as_bytes()]); let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; diff --git a/token-group/interface/src/state.rs b/token-group/interface/src/state.rs index b4a40cb0534..d26fd2fd2e8 100644 --- a/token-group/interface/src/state.rs +++ b/token-group/interface/src/state.rs @@ -5,7 +5,7 @@ use { bytemuck::{Pod, Zeroable}, solana_program::{program_error::ProgramError, pubkey::Pubkey}, spl_discriminator::SplDiscriminate, - spl_pod::{error::PodSliceError, optional_keys::OptionalNonZeroPubkey}, + spl_pod::{error::PodSliceError, optional_keys::OptionalNonZeroPubkey, primitives::PodU32}, }; /// Data struct for a `Group` @@ -16,9 +16,9 @@ pub struct Group { /// The authority that can sign to update the group pub update_authority: OptionalNonZeroPubkey, /// The current number of group members - pub size: u32, + pub size: PodU32, /// The maximum number of group members - pub max_size: u32, + pub max_size: PodU32, } impl Group { @@ -26,33 +26,32 @@ impl Group { pub fn new(update_authority: OptionalNonZeroPubkey, max_size: u32) -> Self { Self { update_authority, - size: 0, - max_size, + size: PodU32::default(), // [0, 0, 0, 0] + max_size: max_size.into(), } } /// Updates the max size for a group pub fn update_max_size(&mut self, new_max_size: u32) -> Result<(), ProgramError> { // The new max size cannot be less than the current size - if new_max_size < self.size { + if new_max_size < u32::from(self.size) { return Err(TokenGroupError::SizeExceedsNewMaxSize.into()); } - self.max_size = new_max_size; + self.max_size = new_max_size.into(); Ok(()) } /// Increment the size for a group, returning the new size pub fn increment_size(&mut self) -> Result { // The new size cannot be greater than the max size - let new_size = self - .size + let new_size = u32::from(self.size) .checked_add(1) .ok_or::(PodSliceError::CalculationFailure.into())?; - if new_size > self.max_size { + if new_size > u32::from(self.max_size) { return Err(TokenGroupError::SizeExceedsMaxSize.into()); } - self.size = new_size; - Ok(self.size) + self.size = new_size.into(); + Ok(new_size) } } @@ -105,8 +104,8 @@ mod tests { // Make sure we can pack more than one instance of each type let group = Group { update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), - size: 10, - max_size: 20, + size: 10.into(), + max_size: 20.into(), }; let member = Member { @@ -137,16 +136,16 @@ mod tests { let max_size = 10; let mut group = Group { update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), - size: 0, - max_size, + size: 0.into(), + max_size: max_size.into(), }; let new_max_size = 30; group.update_max_size(new_max_size).unwrap(); - assert_eq!(group.max_size, new_max_size); + assert_eq!(u32::from(group.max_size), new_max_size); // Change the current size to 30 - group.size = 30; + group.size = 30.into(); // Try to set the max size to 20, which is less than the current size let new_max_size = 20; @@ -157,19 +156,19 @@ mod tests { let new_max_size = 30; group.update_max_size(new_max_size).unwrap(); - assert_eq!(group.max_size, new_max_size); + assert_eq!(u32::from(group.max_size), new_max_size); } #[test] fn increment_current_size() { let mut group = Group { update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), - size: 0, - max_size: 1, + size: 0.into(), + max_size: 1.into(), }; group.increment_size().unwrap(); - assert_eq!(group.size, 1); + assert_eq!(u32::from(group.size), 1); // Try to increase the current size to 2, which is greater than the max size assert_eq!( From 99e5c314a297843983778b511cccb98e2c7bf690 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 16 Oct 2023 13:34:54 +0200 Subject: [PATCH 5/7] update initialize_member instruction --- token-group/interface/src/instruction.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/token-group/interface/src/instruction.rs b/token-group/interface/src/instruction.rs index 176b6647155..ab960a67b58 100644 --- a/token-group/interface/src/instruction.rs +++ b/token-group/interface/src/instruction.rs @@ -89,8 +89,9 @@ pub enum TokenGroupInstruction { /// Accounts expected by this instruction: /// /// 0. `[w]` Member - /// 1. `[w]` Group - /// 2. `[s]` Group update authority + /// 1. `[s]` Member update authority + /// 2. `[w]` Group + /// 3. `[s]` Group update authority InitializeMember(InitializeMember), } impl TokenGroupInstruction { @@ -223,13 +224,15 @@ pub fn update_group_authority( #[allow(clippy::too_many_arguments)] pub fn initialize_member( program_id: &Pubkey, + member: &Pubkey, + member_update_authority: &Pubkey, group: &Pubkey, group_update_authority: &Pubkey, - member: &Pubkey, ) -> Instruction { let data = TokenGroupInstruction::InitializeMember(InitializeMember {}).pack(); let accounts = vec![ AccountMeta::new(*member, false), + AccountMeta::new_readonly(*member_update_authority, true), AccountMeta::new(*group, false), AccountMeta::new_readonly(*group_update_authority, true), ]; From adefc5cbf57fef2c56ef28e4fe09fc4a07dfd799 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 16 Oct 2023 13:48:22 +0200 Subject: [PATCH 6/7] bump instruction discriminators --- token-group/interface/Cargo.toml | 2 +- token-group/interface/src/instruction.rs | 43 +++++++++++------------- token-group/interface/src/state.rs | 6 ++-- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/token-group/interface/Cargo.toml b/token-group/interface/Cargo.toml index 40271ad1092..b748beab31b 100644 --- a/token-group/interface/Cargo.toml +++ b/token-group/interface/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" [dependencies] bytemuck = "1.14.0" -solana-program = "1.16.3" +solana-program = "1.16.16" spl-discriminator = { version = "0.1.0" , path = "../../libraries/discriminator" } spl-pod = { version = "0.1.0" , path = "../../libraries/pod", features = ["borsh"] } spl-program-error = { version = "0.3.0" , path = "../../libraries/program-error" } diff --git a/token-group/interface/src/instruction.rs b/token-group/interface/src/instruction.rs index ab960a67b58..67536d896bd 100644 --- a/token-group/interface/src/instruction.rs +++ b/token-group/interface/src/instruction.rs @@ -18,7 +18,7 @@ use { /// Instruction data for initializing a new `Group` #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] -#[discriminator_hash_input("spl_token_group_interface:initialize_group")] +#[discriminator_hash_input("spl_token_group_interface:initialize_token_group")] pub struct InitializeGroup { /// Update authority for the group pub update_authority: OptionalNonZeroPubkey, @@ -38,7 +38,7 @@ pub struct UpdateGroupMaxSize { /// Instruction data for updating the authority of a `Group` #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] -#[discriminator_hash_input("spl_token_group_interface:update_group_authority")] +#[discriminator_hash_input("spl_token_group_interface:update_authority")] pub struct UpdateGroupAuthority { /// New authority for the group, or unset if `None` pub new_authority: OptionalNonZeroPubkey, @@ -89,7 +89,8 @@ pub enum TokenGroupInstruction { /// Accounts expected by this instruction: /// /// 0. `[w]` Member - /// 1. `[s]` Member update authority + /// 1. `[]` Member mint + /// 1. `[s]` Member mint authority /// 2. `[w]` Group /// 3. `[s]` Group update authority InitializeMember(InitializeMember), @@ -155,7 +156,6 @@ pub fn initialize_group( mint_authority: &Pubkey, update_authority: Option, max_size: u32, - extra_account_metas: &[AccountMeta], ) -> Instruction { let update_authority = OptionalNonZeroPubkey::try_from(update_authority) .expect("Failed to deserialize `Option`"); @@ -164,16 +164,13 @@ pub fn initialize_group( max_size: max_size.into(), }) .pack(); - let mut accounts = vec![ - AccountMeta::new(*group, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new_readonly(*mint_authority, true), - ]; - accounts.extend_from_slice(extra_account_metas); - Instruction { program_id: *program_id, - accounts, + accounts: vec![ + AccountMeta::new(*group, false), + AccountMeta::new_readonly(*mint, false), + AccountMeta::new_readonly(*mint_authority, true), + ], data, } } @@ -225,21 +222,21 @@ pub fn update_group_authority( pub fn initialize_member( program_id: &Pubkey, member: &Pubkey, - member_update_authority: &Pubkey, + member_mint: &Pubkey, + member_mint_authority: &Pubkey, group: &Pubkey, group_update_authority: &Pubkey, ) -> Instruction { let data = TokenGroupInstruction::InitializeMember(InitializeMember {}).pack(); - let accounts = vec![ - AccountMeta::new(*member, false), - AccountMeta::new_readonly(*member_update_authority, true), - AccountMeta::new(*group, false), - AccountMeta::new_readonly(*group_update_authority, true), - ]; - Instruction { program_id: *program_id, - accounts, + accounts: vec![ + AccountMeta::new(*member, false), + AccountMeta::new_readonly(*member_mint, false), + AccountMeta::new_readonly(*member_mint_authority, true), + AccountMeta::new(*group, false), + AccountMeta::new_readonly(*group_update_authority, true), + ], data, } } @@ -273,7 +270,7 @@ mod test { max_size: 100.into(), }; let instruction = TokenGroupInstruction::InitializeGroup(data); - let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_group").as_bytes()]); + let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_token_group").as_bytes()]); let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; instruction_pack_unpack::(instruction, discriminator, data); } @@ -295,7 +292,7 @@ mod test { new_authority: OptionalNonZeroPubkey::default(), }; let instruction = TokenGroupInstruction::UpdateGroupAuthority(data); - let preimage = hash::hashv(&[format!("{NAMESPACE}:update_group_authority").as_bytes()]); + let preimage = hash::hashv(&[format!("{NAMESPACE}:update_authority").as_bytes()]); let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; instruction_pack_unpack::(instruction, discriminator, data); } diff --git a/token-group/interface/src/state.rs b/token-group/interface/src/state.rs index d26fd2fd2e8..8c5993e5cb6 100644 --- a/token-group/interface/src/state.rs +++ b/token-group/interface/src/state.rs @@ -63,14 +63,14 @@ pub struct Member { /// The pubkey of the `Group` pub group: Pubkey, /// The member number - pub member_number: u32, + pub member_number: PodU32, } impl Member { /// Creates a new `Member` state pub fn new(group: Pubkey, member_number: u32) -> Self { Self { group, - member_number, + member_number: member_number.into(), } } } @@ -110,7 +110,7 @@ mod tests { let member = Member { group: Pubkey::new_unique(), - member_number: 0, + member_number: 0.into(), }; let account_size = TlvStateBorrowed::get_base_len() From eecc418e4f607ae777c54e3d7dc93ebbaf910e3e Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 18 Oct 2023 14:16:20 +0200 Subject: [PATCH 7/7] rename state to follow SPL patterns --- token-group/interface/src/state.rs | 45 ++++++++++++++++-------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/token-group/interface/src/state.rs b/token-group/interface/src/state.rs index 8c5993e5cb6..592fc0bb71e 100644 --- a/token-group/interface/src/state.rs +++ b/token-group/interface/src/state.rs @@ -8,11 +8,11 @@ use { spl_pod::{error::PodSliceError, optional_keys::OptionalNonZeroPubkey, primitives::PodU32}, }; -/// Data struct for a `Group` +/// Data struct for a `TokenGroup` #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)] #[discriminator_hash_input("spl_token_group_interface:group")] -pub struct Group { +pub struct TokenGroup { /// The authority that can sign to update the group pub update_authority: OptionalNonZeroPubkey, /// The current number of group members @@ -21,8 +21,8 @@ pub struct Group { pub max_size: PodU32, } -impl Group { - /// Creates a new `Group` state +impl TokenGroup { + /// Creates a new `TokenGroup` state pub fn new(update_authority: OptionalNonZeroPubkey, max_size: u32) -> Self { Self { update_authority, @@ -55,18 +55,18 @@ impl Group { } } -/// Data struct for a `Member` of a `Group` +/// Data struct for a `TokenGroupMember` #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)] #[discriminator_hash_input("spl_token_group_interface:member")] -pub struct Member { - /// The pubkey of the `Group` +pub struct TokenGroupMember { + /// The pubkey of the `TokenGroup` pub group: Pubkey, /// The member number pub member_number: PodU32, } -impl Member { - /// Creates a new `Member` state +impl TokenGroupMember { + /// Creates a new `TokenGroupMember` state pub fn new(group: Pubkey, member_number: u32) -> Self { Self { group, @@ -91,50 +91,53 @@ mod tests { let preimage = hash::hashv(&[format!("{NAMESPACE}:group").as_bytes()]); let discriminator = ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap(); - assert_eq!(Group::SPL_DISCRIMINATOR, discriminator); + assert_eq!(TokenGroup::SPL_DISCRIMINATOR, discriminator); let preimage = hash::hashv(&[format!("{NAMESPACE}:member").as_bytes()]); let discriminator = ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap(); - assert_eq!(Member::SPL_DISCRIMINATOR, discriminator); + assert_eq!(TokenGroupMember::SPL_DISCRIMINATOR, discriminator); } #[test] fn tlv_state_pack() { // Make sure we can pack more than one instance of each type - let group = Group { + let group = TokenGroup { update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), size: 10.into(), max_size: 20.into(), }; - let member = Member { + let member = TokenGroupMember { group: Pubkey::new_unique(), member_number: 0.into(), }; let account_size = TlvStateBorrowed::get_base_len() - + size_of::() + + size_of::() + TlvStateBorrowed::get_base_len() - + size_of::(); + + size_of::(); let mut buffer = vec![0; account_size]; let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - let group_data = state.init_value::(false).unwrap().0; + let group_data = state.init_value::(false).unwrap().0; *group_data = group; - let member_data = state.init_value::(false).unwrap().0; + let member_data = state.init_value::(false).unwrap().0; *member_data = member; - assert_eq!(state.get_first_value::().unwrap(), &group); - assert_eq!(state.get_first_value::().unwrap(), &member); + assert_eq!(state.get_first_value::().unwrap(), &group); + assert_eq!( + state.get_first_value::().unwrap(), + &member + ); } #[test] fn update_max_size() { // Test with a `Some` max size let max_size = 10; - let mut group = Group { + let mut group = TokenGroup { update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), size: 0.into(), max_size: max_size.into(), @@ -161,7 +164,7 @@ mod tests { #[test] fn increment_current_size() { - let mut group = Group { + let mut group = TokenGroup { update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), size: 0.into(), max_size: 1.into(),