-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
token collections: create spec program
- Loading branch information
1 parent
052500a
commit 7119ded
Showing
9 changed files
with
830 additions
and
0 deletions.
There are no files selected for viewing
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,24 @@ | ||
# SPL Token Collections | ||
|
||
This program serves as a reference implementation for using the SPL Token Group | ||
interface to create an on-chain program for managing token collections - such | ||
as NFT Collections. | ||
|
||
This program bears a lot of similarity to the example program found at | ||
`token-group/example`, but with some additional implementations centered around | ||
specifically token collections. | ||
|
||
## How Collections Work in this Program | ||
|
||
Strictly for demonstration purposes, this program is going to require the | ||
following: | ||
|
||
- Group tokens must be NFTs (0 decimals, 1 supply) | ||
- Group tokens must have metadata | ||
- Member tokens can be any SPL token, but must have metadata | ||
- Member tokens can be part of multiple collections | ||
|
||
## Demonstration | ||
|
||
For a particularly fleshed-out example of this program in action, check out the | ||
`token-collections.rs` test under `tests`! |
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,33 @@ | ||
[package] | ||
name = "spl-token-collections" | ||
version = "0.1.0" | ||
description = "Solana Program Library Token Collections" | ||
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-program-error = { version = "0.3.0" , path = "../../libraries/program-error" } | ||
spl-token-2022 = { version = "0.9.0", path = "../../token/program-2022" } | ||
spl-token-group-interface = { version = "0.1.0", path = "../../token-group/interface" } | ||
spl-token-metadata-interface = { version = "0.2", path = "../../token-metadata/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" } | ||
|
||
[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 the Token Collections program implementing 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,201 @@ | ||
//! 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::{ | ||
metadata_pointer::MetadataPointer, BaseStateWithExtensions, StateWithExtensions, | ||
}, | ||
state::Mint, | ||
}, | ||
spl_token_group_interface::{ | ||
error::TokenGroupError, | ||
instruction::{ | ||
InitializeGroup, TokenGroupInstruction, UpdateGroupAuthority, UpdateGroupMaxSize, | ||
}, | ||
state::{TokenGroup, TokenGroupMember}, | ||
}, | ||
spl_token_metadata_interface::state::TokenMetadata, | ||
spl_type_length_value::state::TlvStateMut, | ||
}; | ||
|
||
fn check_update_authority( | ||
update_authority_info: &AccountInfo, | ||
expected_update_authority: &OptionalNonZeroPubkey, | ||
) -> ProgramResult { | ||
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(()) | ||
} | ||
|
||
/// Checks that a mint is valid and contains metadata. | ||
fn check_mint_and_metadata( | ||
mint_info: &AccountInfo, | ||
mint_authority_info: &AccountInfo, | ||
) -> ProgramResult { | ||
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()); | ||
} | ||
|
||
let metadata_pointer = mint.get_extension::<MetadataPointer>()?; | ||
let metadata_pointer_address = Option::<Pubkey>::from(metadata_pointer.metadata_address); | ||
if metadata_pointer_address != Some(*mint_info.key) { | ||
return Err(ProgramError::InvalidAccountData); | ||
} | ||
|
||
mint.get_variable_len_extension::<TokenMetadata>()?; // Ensure it contains valid TokenMetadata | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an [InitializeGroup](enum.GroupInterfaceInstruction.html) | ||
/// instruction to initialize a collection. | ||
pub fn process_initialize_collection( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
data: InitializeGroup, | ||
) -> ProgramResult { | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
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)?; | ||
|
||
check_mint_and_metadata(mint_info, mint_authority_info)?; | ||
|
||
// Initialize the collection | ||
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(mint_info.key, data.update_authority, data.max_size.into()); | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an | ||
/// [UpdateGroupMaxSize](enum.GroupInterfaceInstruction.html) | ||
/// instruction to update the max size of a collection. | ||
pub fn process_update_collection_max_size( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
data: UpdateGroupMaxSize, | ||
) -> ProgramResult { | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
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)?; | ||
|
||
collection.update_max_size(data.max_size.into())?; | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an | ||
/// [UpdateGroupAuthority](enum.GroupInterfaceInstruction.html) | ||
/// instruction to update the authority of a collection. | ||
pub fn process_update_collection_authority( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
data: UpdateGroupAuthority, | ||
) -> ProgramResult { | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
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)?; | ||
|
||
collection.update_authority = data.new_authority; | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Processes an [InitializeMember](enum.GroupInterfaceInstruction.html) | ||
/// instruction | ||
pub fn process_initialize_collection_member( | ||
_program_id: &Pubkey, | ||
accounts: &[AccountInfo], | ||
) -> ProgramResult { | ||
let account_info_iter = &mut accounts.iter(); | ||
|
||
let member_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)?; | ||
let collection_info = next_account_info(account_info_iter)?; | ||
let collection_update_authority_info = next_account_info(account_info_iter)?; | ||
|
||
check_mint_and_metadata(mint_info, mint_authority_info)?; | ||
|
||
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()?; | ||
|
||
let mut buffer = member_info.try_borrow_mut_data()?; | ||
let mut state = TlvStateMut::unpack(&mut buffer)?; | ||
|
||
// This program uses `allow_repetition: true` because the same mint can be | ||
// a member of multiple collections. | ||
let (member, _) = state.init_value::<TokenGroupMember>(/* allow_repetition */ true)?; | ||
*member = TokenGroupMember::new(mint_info.key, 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.