Skip to content

Commit

Permalink
token-group: create example program (#5548)
Browse files Browse the repository at this point in the history
* 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
Show file tree
Hide file tree
Showing 13 changed files with 1,328 additions and 0 deletions.
68 changes: 68 additions & 0 deletions .github/workflows/pull-request-token-group.yml
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
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ members = [
"stake-pool/cli",
"stake-pool/program",
"stateless-asks/program",
"token-group/example",
"token-group/interface",
"token-lending/cli",
"token-lending/program",
Expand Down
32 changes: 32 additions & 0 deletions token-group/example/Cargo.toml
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"]
23 changes: 23 additions & 0 deletions token-group/example/src/entrypoint.rs
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(())
}
10 changes: 10 additions & 0 deletions token-group/example/src/lib.rs
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;
226 changes: 226 additions & 0 deletions token-group/example/src/processor.rs
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)
}
}
}
Loading

0 comments on commit 245f9a2

Please sign in to comment.