-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
token-group: create example program (#5548)
* token-group: create collections example * cut metadata * return to group nomenclature * add multi-group comment * address ze nits * add `MemberAccountIsGroupAccount` error and checks, tests
- Loading branch information
Joe C
authored
Oct 20, 2023
1 parent
035f425
commit 245f9a2
Showing
13 changed files
with
1,328 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
name: Token-Group Pull Request | ||
|
||
on: | ||
pull_request: | ||
paths: | ||
- 'token-group/**' | ||
- 'token/program-2022/**' | ||
- 'ci/*-version.sh' | ||
- '.github/workflows/pull-request-token-group.yml' | ||
push: | ||
branches: [master] | ||
paths: | ||
- 'token-group/**' | ||
- 'token/program-2022/**' | ||
- 'ci/*-version.sh' | ||
- '.github/workflows/pull-request-token-group.yml' | ||
|
||
jobs: | ||
cargo-test-sbf: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v2 | ||
|
||
- name: Set env vars | ||
run: | | ||
source ci/rust-version.sh | ||
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV | ||
source ci/solana-version.sh | ||
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV | ||
- uses: actions-rs/toolchain@v1 | ||
with: | ||
toolchain: ${{ env.RUST_STABLE }} | ||
override: true | ||
profile: minimal | ||
|
||
- uses: actions/cache@v2 | ||
with: | ||
path: | | ||
~/.cargo/registry | ||
~/.cargo/git | ||
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} | ||
|
||
- uses: actions/cache@v2 | ||
with: | ||
path: | | ||
~/.cargo/bin/rustfilt | ||
key: cargo-sbf-bins-${{ runner.os }} | ||
|
||
- uses: actions/cache@v2 | ||
with: | ||
path: ~/.cache/solana | ||
key: solana-${{ env.SOLANA_VERSION }} | ||
|
||
- name: Install dependencies | ||
run: | | ||
./ci/install-build-deps.sh | ||
./ci/install-program-deps.sh | ||
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH | ||
- name: Test token-group interface | ||
run: | | ||
cargo test \ | ||
--manifest-path=token-group/interface/Cargo.toml \ | ||
-- --nocapture | ||
- name: Build and test example | ||
run: ./ci/cargo-test-sbf.sh token-group/example |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
[package] | ||
name = "spl-token-group-example" | ||
version = "0.1.0" | ||
description = "Solana Program Library Token Group Example" | ||
authors = ["Solana Labs Maintainers <[email protected]>"] | ||
repository = "https://github.com/solana-labs/solana-program-library" | ||
license = "Apache-2.0" | ||
edition = "2021" | ||
|
||
[features] | ||
no-entrypoint = [] | ||
test-sbf = [] | ||
|
||
[dependencies] | ||
solana-program = "1.17.2" | ||
spl-pod = { version = "0.1.0", path = "../../libraries/pod" } | ||
spl-token-2022 = { version = "0.9.0", path = "../../token/program-2022", features = ["no-entrypoint"] } | ||
spl-token-group-interface = { version = "0.1.0", path = "../interface" } | ||
spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value" } | ||
|
||
[dev-dependencies] | ||
solana-program-test = "1.17.2" | ||
solana-sdk = "1.17.2" | ||
spl-discriminator = { version = "0.1.0", path = "../../libraries/discriminator" } | ||
spl-token-client = { version = "0.7", path = "../../token/client" } | ||
spl-token-metadata-interface = { version = "0.2", path = "../../token-metadata/interface" } | ||
|
||
[lib] | ||
crate-type = ["cdylib", "lib"] | ||
|
||
[package.metadata.docs.rs] | ||
targets = ["x86_64-unknown-linux-gnu"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
//! Program entrypoint | ||
use { | ||
crate::processor, | ||
solana_program::{ | ||
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, | ||
program_error::PrintProgramError, pubkey::Pubkey, | ||
}, | ||
spl_token_group_interface::error::TokenGroupError, | ||
}; | ||
|
||
entrypoint!(process_instruction); | ||
fn process_instruction( | ||
program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
instruction_data: &[u8], | ||
) -> ProgramResult { | ||
if let Err(error) = processor::process(program_id, accounts, instruction_data) { | ||
error.print::<TokenGroupError>(); | ||
return Err(error); | ||
} | ||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
//! Crate defining an example program for creating SPL token groups | ||
//! using the SPL Token Group interface. | ||
#![deny(missing_docs)] | ||
#![forbid(unsafe_code)] | ||
|
||
pub mod processor; | ||
|
||
#[cfg(not(feature = "no-entrypoint"))] | ||
mod entrypoint; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
//! Program state processor | ||
use { | ||
solana_program::{ | ||
account_info::{next_account_info, AccountInfo}, | ||
entrypoint::ProgramResult, | ||
msg, | ||
program_error::ProgramError, | ||
program_option::COption, | ||
pubkey::Pubkey, | ||
}, | ||
spl_pod::optional_keys::OptionalNonZeroPubkey, | ||
spl_token_2022::{extension::StateWithExtensions, state::Mint}, | ||
spl_token_group_interface::{ | ||
error::TokenGroupError, | ||
instruction::{ | ||
InitializeGroup, TokenGroupInstruction, UpdateGroupAuthority, UpdateGroupMaxSize, | ||
}, | ||
state::{TokenGroup, TokenGroupMember}, | ||
}, | ||
spl_type_length_value::state::TlvStateMut, | ||
}; | ||
|
||
fn check_update_authority( | ||
update_authority_info: &AccountInfo, | ||
expected_update_authority: &OptionalNonZeroPubkey, | ||
) -> Result<(), ProgramError> { | ||
if !update_authority_info.is_signer { | ||
return Err(ProgramError::MissingRequiredSignature); | ||
} | ||
let update_authority = Option::<Pubkey>::from(*expected_update_authority) | ||
.ok_or(TokenGroupError::ImmutableGroup)?; | ||
if update_authority != *update_authority_info.key { | ||
return Err(TokenGroupError::IncorrectUpdateAuthority.into()); | ||
} | ||
Ok(()) | ||
} | ||
|
||
/// Processes an [InitializeGroup](enum.GroupInterfaceInstruction.html) | ||
/// instruction | ||
pub fn process_initialize_group( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
data: InitializeGroup, | ||
) -> ProgramResult { | ||
// Assumes one has already created a mint for the group. | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
// Accounts expected by this instruction: | ||
// | ||
// 0. `[w]` Group | ||
// 1. `[]` Mint | ||
// 2. `[s]` Mint authority | ||
let group_info = next_account_info(account_info_iter)?; | ||
let mint_info = next_account_info(account_info_iter)?; | ||
let mint_authority_info = next_account_info(account_info_iter)?; | ||
|
||
{ | ||
// IMPORTANT: this example program is designed to work with any | ||
// program that implements the SPL token interface, so there is no | ||
// ownership check on the mint account. | ||
let mint_data = mint_info.try_borrow_data()?; | ||
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?; | ||
|
||
if !mint_authority_info.is_signer { | ||
return Err(ProgramError::MissingRequiredSignature); | ||
} | ||
if mint.base.mint_authority.as_ref() != COption::Some(mint_authority_info.key) { | ||
return Err(TokenGroupError::IncorrectMintAuthority.into()); | ||
} | ||
} | ||
|
||
// Allocate a TLV entry for the space and write it in | ||
let mut buffer = group_info.try_borrow_mut_data()?; | ||
let mut state = TlvStateMut::unpack(&mut buffer)?; | ||
let (group, _) = state.init_value::<TokenGroup>(false)?; | ||
*group = TokenGroup::new(mint_info.key, data.update_authority, data.max_size.into()); | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an | ||
/// [UpdateGroupMaxSize](enum.GroupInterfaceInstruction.html) | ||
/// instruction | ||
pub fn process_update_group_max_size( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
data: UpdateGroupMaxSize, | ||
) -> ProgramResult { | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
// Accounts expected by this instruction: | ||
// | ||
// 0. `[w]` Group | ||
// 1. `[s]` Update authority | ||
let group_info = next_account_info(account_info_iter)?; | ||
let update_authority_info = next_account_info(account_info_iter)?; | ||
|
||
let mut buffer = group_info.try_borrow_mut_data()?; | ||
let mut state = TlvStateMut::unpack(&mut buffer)?; | ||
let group = state.get_first_value_mut::<TokenGroup>()?; | ||
|
||
check_update_authority(update_authority_info, &group.update_authority)?; | ||
|
||
// Update the max size (zero-copy) | ||
group.update_max_size(data.max_size.into())?; | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an | ||
/// [UpdateGroupAuthority](enum.GroupInterfaceInstruction.html) | ||
/// instruction | ||
pub fn process_update_group_authority( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
data: UpdateGroupAuthority, | ||
) -> ProgramResult { | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
// Accounts expected by this instruction: | ||
// | ||
// 0. `[w]` Group | ||
// 1. `[s]` Current update authority | ||
let group_info = next_account_info(account_info_iter)?; | ||
let update_authority_info = next_account_info(account_info_iter)?; | ||
|
||
let mut buffer = group_info.try_borrow_mut_data()?; | ||
let mut state = TlvStateMut::unpack(&mut buffer)?; | ||
let group = state.get_first_value_mut::<TokenGroup>()?; | ||
|
||
check_update_authority(update_authority_info, &group.update_authority)?; | ||
|
||
// Update the authority (zero-copy) | ||
group.update_authority = data.new_authority; | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an [InitializeMember](enum.GroupInterfaceInstruction.html) | ||
/// instruction | ||
pub fn process_initialize_member(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { | ||
// For this group, we are going to assume the group has been | ||
// initialized, and we're also assuming a mint has been created for the | ||
// member. | ||
// Group members in this example can have their own separate | ||
// metadata that differs from the metadata of the group, since | ||
// metadata is not involved here. | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
// Accounts expected by this instruction: | ||
// | ||
// 0. `[w]` Member | ||
// 1. `[]` Member Mint | ||
// 2. `[s]` Member Mint authority | ||
// 3. `[w]` Group | ||
// 4. `[s]` Group update authority | ||
let member_info = next_account_info(account_info_iter)?; | ||
let member_mint_info = next_account_info(account_info_iter)?; | ||
let member_mint_authority_info = next_account_info(account_info_iter)?; | ||
let group_info = next_account_info(account_info_iter)?; | ||
let group_update_authority_info = next_account_info(account_info_iter)?; | ||
|
||
// Mint checks on the member | ||
{ | ||
// IMPORTANT: this example program is designed to work with any | ||
// program that implements the SPL token interface, so there is no | ||
// ownership check on the mint account. | ||
let member_mint_data = member_mint_info.try_borrow_data()?; | ||
let member_mint = StateWithExtensions::<Mint>::unpack(&member_mint_data)?; | ||
|
||
if !member_mint_authority_info.is_signer { | ||
return Err(ProgramError::MissingRequiredSignature); | ||
} | ||
if member_mint.base.mint_authority.as_ref() != COption::Some(member_mint_authority_info.key) | ||
{ | ||
return Err(TokenGroupError::IncorrectMintAuthority.into()); | ||
} | ||
} | ||
|
||
// Make sure the member account is not the same as the group accout | ||
if member_info.key == group_info.key { | ||
return Err(TokenGroupError::MemberAccountIsGroupAccount.into()); | ||
} | ||
|
||
// Increment the size of the group | ||
let mut buffer = group_info.try_borrow_mut_data()?; | ||
let mut state = TlvStateMut::unpack(&mut buffer)?; | ||
let group = state.get_first_value_mut::<TokenGroup>()?; | ||
|
||
check_update_authority(group_update_authority_info, &group.update_authority)?; | ||
let member_number = group.increment_size()?; | ||
|
||
// Allocate a TLV entry for the space and write it in | ||
let mut buffer = member_info.try_borrow_mut_data()?; | ||
let mut state = TlvStateMut::unpack(&mut buffer)?; | ||
// Note if `allow_repetition: true` is instead used here, one can initialize | ||
// the same token as a member of multiple groups! | ||
let (member, _) = state.init_value::<TokenGroupMember>(false)?; | ||
*member = TokenGroupMember::new(member_mint_info.key, group_info.key, member_number); | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an `SplTokenGroupInstruction` | ||
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { | ||
let instruction = TokenGroupInstruction::unpack(input)?; | ||
match instruction { | ||
TokenGroupInstruction::InitializeGroup(data) => { | ||
msg!("Instruction: InitializeGroup"); | ||
process_initialize_group(program_id, accounts, data) | ||
} | ||
TokenGroupInstruction::UpdateGroupMaxSize(data) => { | ||
msg!("Instruction: UpdateGroupMaxSize"); | ||
process_update_group_max_size(program_id, accounts, data) | ||
} | ||
TokenGroupInstruction::UpdateGroupAuthority(data) => { | ||
msg!("Instruction: UpdateGroupAuthority"); | ||
process_update_group_authority(program_id, accounts, data) | ||
} | ||
TokenGroupInstruction::InitializeMember(_) => { | ||
msg!("Instruction: InitializeMember"); | ||
process_initialize_member(program_id, accounts) | ||
} | ||
} | ||
} |
Oops, something went wrong.