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

Commit 245f9a2

Browse files
author
Joe C
authored
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
1 parent 035f425 commit 245f9a2

File tree

13 files changed

+1328
-0
lines changed

13 files changed

+1328
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Token-Group Pull Request
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'token-group/**'
7+
- 'token/program-2022/**'
8+
- 'ci/*-version.sh'
9+
- '.github/workflows/pull-request-token-group.yml'
10+
push:
11+
branches: [master]
12+
paths:
13+
- 'token-group/**'
14+
- 'token/program-2022/**'
15+
- 'ci/*-version.sh'
16+
- '.github/workflows/pull-request-token-group.yml'
17+
18+
jobs:
19+
cargo-test-sbf:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v2
23+
24+
- name: Set env vars
25+
run: |
26+
source ci/rust-version.sh
27+
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
28+
source ci/solana-version.sh
29+
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
30+
31+
- uses: actions-rs/toolchain@v1
32+
with:
33+
toolchain: ${{ env.RUST_STABLE }}
34+
override: true
35+
profile: minimal
36+
37+
- uses: actions/cache@v2
38+
with:
39+
path: |
40+
~/.cargo/registry
41+
~/.cargo/git
42+
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
43+
44+
- uses: actions/cache@v2
45+
with:
46+
path: |
47+
~/.cargo/bin/rustfilt
48+
key: cargo-sbf-bins-${{ runner.os }}
49+
50+
- uses: actions/cache@v2
51+
with:
52+
path: ~/.cache/solana
53+
key: solana-${{ env.SOLANA_VERSION }}
54+
55+
- name: Install dependencies
56+
run: |
57+
./ci/install-build-deps.sh
58+
./ci/install-program-deps.sh
59+
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
60+
61+
- name: Test token-group interface
62+
run: |
63+
cargo test \
64+
--manifest-path=token-group/interface/Cargo.toml \
65+
-- --nocapture
66+
67+
- name: Build and test example
68+
run: ./ci/cargo-test-sbf.sh token-group/example

Cargo.lock

Lines changed: 16 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/example",
4546
"token-group/interface",
4647
"token-lending/cli",
4748
"token-lending/program",

token-group/example/Cargo.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "spl-token-group-example"
3+
version = "0.1.0"
4+
description = "Solana Program Library Token Group Example"
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+
[features]
11+
no-entrypoint = []
12+
test-sbf = []
13+
14+
[dependencies]
15+
solana-program = "1.17.2"
16+
spl-pod = { version = "0.1.0", path = "../../libraries/pod" }
17+
spl-token-2022 = { version = "0.9.0", path = "../../token/program-2022", features = ["no-entrypoint"] }
18+
spl-token-group-interface = { version = "0.1.0", path = "../interface" }
19+
spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value" }
20+
21+
[dev-dependencies]
22+
solana-program-test = "1.17.2"
23+
solana-sdk = "1.17.2"
24+
spl-discriminator = { version = "0.1.0", path = "../../libraries/discriminator" }
25+
spl-token-client = { version = "0.7", path = "../../token/client" }
26+
spl-token-metadata-interface = { version = "0.2", path = "../../token-metadata/interface" }
27+
28+
[lib]
29+
crate-type = ["cdylib", "lib"]
30+
31+
[package.metadata.docs.rs]
32+
targets = ["x86_64-unknown-linux-gnu"]

token-group/example/src/entrypoint.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//! Program entrypoint
2+
3+
use {
4+
crate::processor,
5+
solana_program::{
6+
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
7+
program_error::PrintProgramError, pubkey::Pubkey,
8+
},
9+
spl_token_group_interface::error::TokenGroupError,
10+
};
11+
12+
entrypoint!(process_instruction);
13+
fn process_instruction(
14+
program_id: &Pubkey,
15+
accounts: &[AccountInfo],
16+
instruction_data: &[u8],
17+
) -> ProgramResult {
18+
if let Err(error) = processor::process(program_id, accounts, instruction_data) {
19+
error.print::<TokenGroupError>();
20+
return Err(error);
21+
}
22+
Ok(())
23+
}

token-group/example/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//! Crate defining an example program for creating SPL token groups
2+
//! using the SPL Token Group interface.
3+
4+
#![deny(missing_docs)]
5+
#![forbid(unsafe_code)]
6+
7+
pub mod processor;
8+
9+
#[cfg(not(feature = "no-entrypoint"))]
10+
mod entrypoint;

token-group/example/src/processor.rs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
//! Program state processor
2+
3+
use {
4+
solana_program::{
5+
account_info::{next_account_info, AccountInfo},
6+
entrypoint::ProgramResult,
7+
msg,
8+
program_error::ProgramError,
9+
program_option::COption,
10+
pubkey::Pubkey,
11+
},
12+
spl_pod::optional_keys::OptionalNonZeroPubkey,
13+
spl_token_2022::{extension::StateWithExtensions, state::Mint},
14+
spl_token_group_interface::{
15+
error::TokenGroupError,
16+
instruction::{
17+
InitializeGroup, TokenGroupInstruction, UpdateGroupAuthority, UpdateGroupMaxSize,
18+
},
19+
state::{TokenGroup, TokenGroupMember},
20+
},
21+
spl_type_length_value::state::TlvStateMut,
22+
};
23+
24+
fn check_update_authority(
25+
update_authority_info: &AccountInfo,
26+
expected_update_authority: &OptionalNonZeroPubkey,
27+
) -> Result<(), ProgramError> {
28+
if !update_authority_info.is_signer {
29+
return Err(ProgramError::MissingRequiredSignature);
30+
}
31+
let update_authority = Option::<Pubkey>::from(*expected_update_authority)
32+
.ok_or(TokenGroupError::ImmutableGroup)?;
33+
if update_authority != *update_authority_info.key {
34+
return Err(TokenGroupError::IncorrectUpdateAuthority.into());
35+
}
36+
Ok(())
37+
}
38+
39+
/// Processes an [InitializeGroup](enum.GroupInterfaceInstruction.html)
40+
/// instruction
41+
pub fn process_initialize_group(
42+
_program_id: &Pubkey,
43+
accounts: &[AccountInfo],
44+
data: InitializeGroup,
45+
) -> ProgramResult {
46+
// Assumes one has already created a mint for the group.
47+
let account_info_iter = &mut accounts.iter();
48+
49+
// Accounts expected by this instruction:
50+
//
51+
// 0. `[w]` Group
52+
// 1. `[]` Mint
53+
// 2. `[s]` Mint authority
54+
let group_info = next_account_info(account_info_iter)?;
55+
let mint_info = next_account_info(account_info_iter)?;
56+
let mint_authority_info = next_account_info(account_info_iter)?;
57+
58+
{
59+
// IMPORTANT: this example program is designed to work with any
60+
// program that implements the SPL token interface, so there is no
61+
// ownership check on the mint account.
62+
let mint_data = mint_info.try_borrow_data()?;
63+
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
64+
65+
if !mint_authority_info.is_signer {
66+
return Err(ProgramError::MissingRequiredSignature);
67+
}
68+
if mint.base.mint_authority.as_ref() != COption::Some(mint_authority_info.key) {
69+
return Err(TokenGroupError::IncorrectMintAuthority.into());
70+
}
71+
}
72+
73+
// Allocate a TLV entry for the space and write it in
74+
let mut buffer = group_info.try_borrow_mut_data()?;
75+
let mut state = TlvStateMut::unpack(&mut buffer)?;
76+
let (group, _) = state.init_value::<TokenGroup>(false)?;
77+
*group = TokenGroup::new(mint_info.key, data.update_authority, data.max_size.into());
78+
79+
Ok(())
80+
}
81+
82+
/// Processes an
83+
/// [UpdateGroupMaxSize](enum.GroupInterfaceInstruction.html)
84+
/// instruction
85+
pub fn process_update_group_max_size(
86+
_program_id: &Pubkey,
87+
accounts: &[AccountInfo],
88+
data: UpdateGroupMaxSize,
89+
) -> ProgramResult {
90+
let account_info_iter = &mut accounts.iter();
91+
92+
// Accounts expected by this instruction:
93+
//
94+
// 0. `[w]` Group
95+
// 1. `[s]` Update authority
96+
let group_info = next_account_info(account_info_iter)?;
97+
let update_authority_info = next_account_info(account_info_iter)?;
98+
99+
let mut buffer = group_info.try_borrow_mut_data()?;
100+
let mut state = TlvStateMut::unpack(&mut buffer)?;
101+
let group = state.get_first_value_mut::<TokenGroup>()?;
102+
103+
check_update_authority(update_authority_info, &group.update_authority)?;
104+
105+
// Update the max size (zero-copy)
106+
group.update_max_size(data.max_size.into())?;
107+
108+
Ok(())
109+
}
110+
111+
/// Processes an
112+
/// [UpdateGroupAuthority](enum.GroupInterfaceInstruction.html)
113+
/// instruction
114+
pub fn process_update_group_authority(
115+
_program_id: &Pubkey,
116+
accounts: &[AccountInfo],
117+
data: UpdateGroupAuthority,
118+
) -> ProgramResult {
119+
let account_info_iter = &mut accounts.iter();
120+
121+
// Accounts expected by this instruction:
122+
//
123+
// 0. `[w]` Group
124+
// 1. `[s]` Current update authority
125+
let group_info = next_account_info(account_info_iter)?;
126+
let update_authority_info = next_account_info(account_info_iter)?;
127+
128+
let mut buffer = group_info.try_borrow_mut_data()?;
129+
let mut state = TlvStateMut::unpack(&mut buffer)?;
130+
let group = state.get_first_value_mut::<TokenGroup>()?;
131+
132+
check_update_authority(update_authority_info, &group.update_authority)?;
133+
134+
// Update the authority (zero-copy)
135+
group.update_authority = data.new_authority;
136+
137+
Ok(())
138+
}
139+
140+
/// Processes an [InitializeMember](enum.GroupInterfaceInstruction.html)
141+
/// instruction
142+
pub fn process_initialize_member(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
143+
// For this group, we are going to assume the group has been
144+
// initialized, and we're also assuming a mint has been created for the
145+
// member.
146+
// Group members in this example can have their own separate
147+
// metadata that differs from the metadata of the group, since
148+
// metadata is not involved here.
149+
let account_info_iter = &mut accounts.iter();
150+
151+
// Accounts expected by this instruction:
152+
//
153+
// 0. `[w]` Member
154+
// 1. `[]` Member Mint
155+
// 2. `[s]` Member Mint authority
156+
// 3. `[w]` Group
157+
// 4. `[s]` Group update authority
158+
let member_info = next_account_info(account_info_iter)?;
159+
let member_mint_info = next_account_info(account_info_iter)?;
160+
let member_mint_authority_info = next_account_info(account_info_iter)?;
161+
let group_info = next_account_info(account_info_iter)?;
162+
let group_update_authority_info = next_account_info(account_info_iter)?;
163+
164+
// Mint checks on the member
165+
{
166+
// IMPORTANT: this example program is designed to work with any
167+
// program that implements the SPL token interface, so there is no
168+
// ownership check on the mint account.
169+
let member_mint_data = member_mint_info.try_borrow_data()?;
170+
let member_mint = StateWithExtensions::<Mint>::unpack(&member_mint_data)?;
171+
172+
if !member_mint_authority_info.is_signer {
173+
return Err(ProgramError::MissingRequiredSignature);
174+
}
175+
if member_mint.base.mint_authority.as_ref() != COption::Some(member_mint_authority_info.key)
176+
{
177+
return Err(TokenGroupError::IncorrectMintAuthority.into());
178+
}
179+
}
180+
181+
// Make sure the member account is not the same as the group accout
182+
if member_info.key == group_info.key {
183+
return Err(TokenGroupError::MemberAccountIsGroupAccount.into());
184+
}
185+
186+
// Increment the size of the group
187+
let mut buffer = group_info.try_borrow_mut_data()?;
188+
let mut state = TlvStateMut::unpack(&mut buffer)?;
189+
let group = state.get_first_value_mut::<TokenGroup>()?;
190+
191+
check_update_authority(group_update_authority_info, &group.update_authority)?;
192+
let member_number = group.increment_size()?;
193+
194+
// Allocate a TLV entry for the space and write it in
195+
let mut buffer = member_info.try_borrow_mut_data()?;
196+
let mut state = TlvStateMut::unpack(&mut buffer)?;
197+
// Note if `allow_repetition: true` is instead used here, one can initialize
198+
// the same token as a member of multiple groups!
199+
let (member, _) = state.init_value::<TokenGroupMember>(false)?;
200+
*member = TokenGroupMember::new(member_mint_info.key, group_info.key, member_number);
201+
202+
Ok(())
203+
}
204+
205+
/// Processes an `SplTokenGroupInstruction`
206+
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
207+
let instruction = TokenGroupInstruction::unpack(input)?;
208+
match instruction {
209+
TokenGroupInstruction::InitializeGroup(data) => {
210+
msg!("Instruction: InitializeGroup");
211+
process_initialize_group(program_id, accounts, data)
212+
}
213+
TokenGroupInstruction::UpdateGroupMaxSize(data) => {
214+
msg!("Instruction: UpdateGroupMaxSize");
215+
process_update_group_max_size(program_id, accounts, data)
216+
}
217+
TokenGroupInstruction::UpdateGroupAuthority(data) => {
218+
msg!("Instruction: UpdateGroupAuthority");
219+
process_update_group_authority(program_id, accounts, data)
220+
}
221+
TokenGroupInstruction::InitializeMember(_) => {
222+
msg!("Instruction: InitializeMember");
223+
process_initialize_member(program_id, accounts)
224+
}
225+
}
226+
}

0 commit comments

Comments
 (0)