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

Commit 4af4cca

Browse files
author
Joe C
authored
token collection: create spec program (#5614)
* token collections: create spec program * import processor code from token-group example program
1 parent 2502297 commit 4af4cca

File tree

10 files changed

+850
-0
lines changed

10 files changed

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

Cargo.lock

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

token-collection/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# SPL Token Collection
2+
3+
This program serves as a reference implementation for using the SPL Token Group
4+
interface to create an on-chain program for managing token collections - such
5+
as NFT Collections.
6+
7+
This program bears a lot of similarity to the example program found at
8+
`token-group/example`, but with some additional implementations centered around
9+
specifically token collections.
10+
11+
## How Collections Work in this Program
12+
13+
Strictly for demonstration purposes, this program is going to require the
14+
following:
15+
16+
- Group tokens must be NFTs (0 decimals, 1 supply)
17+
- Group tokens must have metadata
18+
- Member tokens can be any SPL token, but must have metadata
19+
- Member tokens can be part of multiple collections
20+
21+
## Demonstration
22+
23+
For a particularly fleshed-out example of this program in action, check out the
24+
`token-collections.rs` test under `tests`!

token-collection/program/Cargo.toml

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//! Crate defining the Token Collection program implementing the
2+
//! 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;
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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::{
14+
extension::{
15+
metadata_pointer::MetadataPointer, BaseStateWithExtensions, StateWithExtensions,
16+
},
17+
state::Mint,
18+
},
19+
spl_token_group_interface::{
20+
error::TokenGroupError,
21+
instruction::{InitializeGroup, TokenGroupInstruction},
22+
state::{TokenGroup, TokenGroupMember},
23+
},
24+
spl_token_metadata_interface::state::TokenMetadata,
25+
spl_type_length_value::state::TlvStateMut,
26+
};
27+
28+
fn check_update_authority(
29+
update_authority_info: &AccountInfo,
30+
expected_update_authority: &OptionalNonZeroPubkey,
31+
) -> ProgramResult {
32+
if !update_authority_info.is_signer {
33+
return Err(ProgramError::MissingRequiredSignature);
34+
}
35+
let update_authority = Option::<Pubkey>::from(*expected_update_authority)
36+
.ok_or(TokenGroupError::ImmutableGroup)?;
37+
if update_authority != *update_authority_info.key {
38+
return Err(TokenGroupError::IncorrectUpdateAuthority.into());
39+
}
40+
Ok(())
41+
}
42+
43+
/// Checks that a mint is valid and contains metadata.
44+
fn check_mint_and_metadata(
45+
mint_info: &AccountInfo,
46+
mint_authority_info: &AccountInfo,
47+
) -> ProgramResult {
48+
let mint_data = mint_info.try_borrow_data()?;
49+
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
50+
51+
if !mint_authority_info.is_signer {
52+
return Err(ProgramError::MissingRequiredSignature);
53+
}
54+
if mint.base.mint_authority.as_ref() != COption::Some(mint_authority_info.key) {
55+
return Err(TokenGroupError::IncorrectMintAuthority.into());
56+
}
57+
58+
let metadata_pointer = mint.get_extension::<MetadataPointer>()?;
59+
let metadata_pointer_address = Option::<Pubkey>::from(metadata_pointer.metadata_address);
60+
61+
// If the metadata is inside the mint (Token2022), make sure it contains
62+
// valid TokenMetadata
63+
if metadata_pointer_address == Some(*mint_info.key) {
64+
mint.get_variable_len_extension::<TokenMetadata>()?;
65+
}
66+
67+
Ok(())
68+
}
69+
70+
/// Processes an [InitializeGroup](enum.GroupInterfaceInstruction.html)
71+
/// instruction to initialize a collection.
72+
pub fn process_initialize_collection(
73+
_program_id: &Pubkey,
74+
accounts: &[AccountInfo],
75+
data: InitializeGroup,
76+
) -> ProgramResult {
77+
let account_info_iter = &mut accounts.iter();
78+
79+
let collection_info = next_account_info(account_info_iter)?;
80+
let mint_info = next_account_info(account_info_iter)?;
81+
let mint_authority_info = next_account_info(account_info_iter)?;
82+
83+
check_mint_and_metadata(mint_info, mint_authority_info)?;
84+
85+
// Initialize the collection
86+
let mut buffer = collection_info.try_borrow_mut_data()?;
87+
let mut state = TlvStateMut::unpack(&mut buffer)?;
88+
let (collection, _) = state.init_value::<TokenGroup>(false)?;
89+
*collection = TokenGroup::new(mint_info.key, data.update_authority, data.max_size.into());
90+
91+
Ok(())
92+
}
93+
94+
/// Processes an [InitializeMember](enum.GroupInterfaceInstruction.html)
95+
/// instruction
96+
pub fn process_initialize_collection_member(
97+
_program_id: &Pubkey,
98+
accounts: &[AccountInfo],
99+
) -> ProgramResult {
100+
let account_info_iter = &mut accounts.iter();
101+
102+
let member_info = next_account_info(account_info_iter)?;
103+
let mint_info = next_account_info(account_info_iter)?;
104+
let mint_authority_info = next_account_info(account_info_iter)?;
105+
let collection_info = next_account_info(account_info_iter)?;
106+
let collection_update_authority_info = next_account_info(account_info_iter)?;
107+
108+
check_mint_and_metadata(mint_info, mint_authority_info)?;
109+
110+
if member_info.key == collection_info.key {
111+
return Err(TokenGroupError::MemberAccountIsGroupAccount.into());
112+
}
113+
114+
let mut buffer = collection_info.try_borrow_mut_data()?;
115+
let mut state = TlvStateMut::unpack(&mut buffer)?;
116+
let collection = state.get_first_value_mut::<TokenGroup>()?;
117+
118+
check_update_authority(
119+
collection_update_authority_info,
120+
&collection.update_authority,
121+
)?;
122+
let member_number = collection.increment_size()?;
123+
124+
let mut buffer = member_info.try_borrow_mut_data()?;
125+
let mut state = TlvStateMut::unpack(&mut buffer)?;
126+
127+
// This program uses `allow_repetition: true` because the same mint can be
128+
// a member of multiple collections.
129+
let (member, _) = state.init_value::<TokenGroupMember>(/* allow_repetition */ true)?;
130+
*member = TokenGroupMember::new(mint_info.key, collection_info.key, member_number);
131+
132+
Ok(())
133+
}
134+
135+
/// Processes an `SplTokenGroupInstruction`
136+
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
137+
let instruction = TokenGroupInstruction::unpack(input)?;
138+
match instruction {
139+
TokenGroupInstruction::InitializeGroup(data) => {
140+
msg!("Instruction: InitializeCollection");
141+
process_initialize_collection(program_id, accounts, data)
142+
}
143+
TokenGroupInstruction::UpdateGroupMaxSize(data) => {
144+
msg!("Instruction: UpdateCollectionMaxSize");
145+
// Same functionality as the example program
146+
spl_token_group_example::processor::process_update_group_max_size(
147+
program_id, accounts, data,
148+
)
149+
}
150+
TokenGroupInstruction::UpdateGroupAuthority(data) => {
151+
msg!("Instruction: UpdateCollectionAuthority");
152+
// Same functionality as the example program
153+
spl_token_group_example::processor::process_update_group_authority(
154+
program_id, accounts, data,
155+
)
156+
}
157+
TokenGroupInstruction::InitializeMember(_) => {
158+
msg!("Instruction: InitializeCollectionMember");
159+
process_initialize_collection_member(program_id, accounts)
160+
}
161+
}
162+
}

0 commit comments

Comments
 (0)