Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

token-group: create example program #5548

Merged
merged 6 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
joncinque marked this conversation as resolved.
Show resolved Hide resolved
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);
buffalojoec marked this conversation as resolved.
Show resolved Hide resolved

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
Loading