-
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 collections example
- Loading branch information
1 parent
5ce6eac
commit 5d2a991
Showing
13 changed files
with
1,294 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,69 @@ | ||
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 with "serde" activated | ||
run: | | ||
cargo test \ | ||
--manifest-path=token-group/interface/Cargo.toml \ | ||
--features serde-traits \ | ||
-- --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.16.16" | ||
spl-pod = { version = "0.1.0", path = "../../libraries/pod" } | ||
spl-token-2022 = { version = "0.9.0", path = "../../token/program-2022" } | ||
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.16.16" | ||
solana-sdk = "1.16.16" | ||
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 collections | ||
//! using the SPL Token Group interface. | ||
#![deny(missing_docs)] | ||
#![cfg_attr(not(test), 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,225 @@ | ||
//! 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::IncorrectAuthority.into()); | ||
} | ||
Ok(()) | ||
} | ||
|
||
/// Processes an [InitializeGroup](enum.GroupInterfaceInstruction.html) | ||
/// instruction for a `Collection` | ||
pub fn process_initialize_collection( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
data: InitializeGroup, | ||
) -> ProgramResult { | ||
// Assumes one has already created a mint for the collection. | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
// Accounts expected by this instruction: | ||
// | ||
// 0. `[w]` Collection (Group) | ||
// 1. `[]` Mint | ||
// 2. `[s]` Mint authority | ||
let collection_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::IncorrectAuthority.into()); | ||
} | ||
} | ||
|
||
// Allocate a TLV entry for the space and write it in | ||
let mut buffer = collection_info.try_borrow_mut_data()?; | ||
let mut state = TlvStateMut::unpack(&mut buffer)?; | ||
let (collection, _) = state.init_value::<TokenGroup>(false)?; | ||
*collection = TokenGroup::new(data.update_authority, data.max_size.into()); | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an | ||
/// [UpdateGroupMaxSize](enum.GroupInterfaceInstruction.html) | ||
/// instruction for a `Collection` | ||
pub fn process_update_collection_max_size( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
data: UpdateGroupMaxSize, | ||
) -> ProgramResult { | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
// Accounts expected by this instruction: | ||
// | ||
// 0. `[w]` Collection (Group) | ||
// 1. `[s]` Update authority | ||
let collection_info = next_account_info(account_info_iter)?; | ||
let update_authority_info = next_account_info(account_info_iter)?; | ||
|
||
let mut buffer = collection_info.try_borrow_mut_data()?; | ||
let mut state = TlvStateMut::unpack(&mut buffer)?; | ||
let collection = state.get_first_value_mut::<TokenGroup>()?; | ||
|
||
check_update_authority(update_authority_info, &collection.update_authority)?; | ||
|
||
// Update the max size (zero-copy) | ||
collection.update_max_size(data.max_size.into())?; | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an | ||
/// [UpdateGroupAuthority](enum.GroupInterfaceInstruction.html) | ||
/// instruction for a `Collection` | ||
pub fn process_update_collection_authority( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
data: UpdateGroupAuthority, | ||
) -> ProgramResult { | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
// Accounts expected by this instruction: | ||
// | ||
// 0. `[w]` Collection (Group) | ||
// 1. `[s]` Current update authority | ||
let collection_info = next_account_info(account_info_iter)?; | ||
let update_authority_info = next_account_info(account_info_iter)?; | ||
|
||
let mut buffer = collection_info.try_borrow_mut_data()?; | ||
let mut state = TlvStateMut::unpack(&mut buffer)?; | ||
let mut collection = state.get_first_value_mut::<TokenGroup>()?; | ||
|
||
check_update_authority(update_authority_info, &collection.update_authority)?; | ||
|
||
// Update the authority (zero-copy) | ||
collection.update_authority = data.new_authority; | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an [InitializeMember](enum.GroupInterfaceInstruction.html) | ||
/// instruction for a `Collection` | ||
pub fn process_initialize_collection_member( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
) -> ProgramResult { | ||
// For this group, we are going to assume the collection has been | ||
// initialized, and we're also assuming a mint has been created for the | ||
// member. | ||
// Collection members in this example can have their own separate | ||
// metadata that differs from the metadata of the collection, since | ||
// metadata is not involved here. | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
// Accounts expected by this instruction: | ||
// | ||
// 0. `[w]` Collection Member (Member) | ||
// 1. `[]` Collection Member (Member) Mint | ||
// 2. `[s]` Collection Member (Member) Mint authority | ||
// 3. `[w]` Collection (Group) | ||
// 4. `[s]` Collection (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 collection_info = next_account_info(account_info_iter)?; | ||
let collection_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::IncorrectAuthority.into()); | ||
} | ||
} | ||
|
||
// Increment the size of the collection | ||
let mut buffer = collection_info.try_borrow_mut_data()?; | ||
let mut state = TlvStateMut::unpack(&mut buffer)?; | ||
let collection = state.get_first_value_mut::<TokenGroup>()?; | ||
|
||
check_update_authority( | ||
collection_update_authority_info, | ||
&collection.update_authority, | ||
)?; | ||
let member_number = collection.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)?; | ||
let (member, _) = state.init_value::<TokenGroupMember>(false)?; | ||
*member = TokenGroupMember::new(*collection_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: InitializeCollection"); | ||
process_initialize_collection(program_id, accounts, data) | ||
} | ||
TokenGroupInstruction::UpdateGroupMaxSize(data) => { | ||
msg!("Instruction: UpdateCollectionMaxSize"); | ||
process_update_collection_max_size(program_id, accounts, data) | ||
} | ||
TokenGroupInstruction::UpdateGroupAuthority(data) => { | ||
msg!("Instruction: UpdateCollectionAuthority"); | ||
process_update_collection_authority(program_id, accounts, data) | ||
} | ||
TokenGroupInstruction::InitializeMember(_) => { | ||
msg!("Instruction: InitializeCollectionMember"); | ||
process_initialize_collection_member(program_id, accounts) | ||
} | ||
} | ||
} |
Oops, something went wrong.