Skip to content

Commit

Permalink
token 2022: add InitializeGroup instruction from SPL Token Group in…
Browse files Browse the repository at this point in the history
…terface
  • Loading branch information
buffalojoec committed Oct 20, 2023
1 parent 1b7a918 commit d942fb4
Show file tree
Hide file tree
Showing 10 changed files with 472 additions and 1 deletion.
3 changes: 3 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 token/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ spl-associated-token-account = { version = "2.0", path = "../../associated-token
spl-memo = { version = "4.0.0", path = "../../memo/program", features = ["no-entrypoint"] }
spl-token = { version = "4.0", path="../program", features = [ "no-entrypoint" ] }
spl-token-2022 = { version = "0.9", path="../program-2022" }
spl-token-group-interface = { version = "0.1", path="../../token-group/interface" }
spl-token-metadata-interface = { version = "0.2", path="../../token-metadata/interface" }
spl-transfer-hook-interface = { version = "0.3", path="../transfer-hook-interface" }
thiserror = "1.0"
Expand Down
71 changes: 71 additions & 0 deletions token/client/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3846,4 +3846,75 @@ where
)
.await
}

/// Initialize token-group on a mint
pub async fn token_group_initialize<S: Signers>(
&self,
mint_authority: &Pubkey,
update_authority: &Pubkey,
max_size: u32,
signing_keypairs: &S,
) -> TokenResult<T::Output> {
self.process_ixs(
&[spl_token_group_interface::instruction::initialize_group(
&self.program_id,
&self.pubkey,
&self.pubkey,
mint_authority,
Some(*update_authority),
max_size,
)],
signing_keypairs,
)
.await
}

async fn get_additional_rent_for_new_group(
&self,
) -> TokenResult<u64> {
let account = self.get_account(self.pubkey).await?;
let account_lamports = account.lamports;
let mint_state = self.unpack_mint_info(account)?;
let new_account_len = mint_state.try_get_account_len()?
.checked_add(ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::TokenGroup])?)
.ok_or(TokenError::Program(spl_token_2022::error::TokenError::Overflow.into()))?;
let new_rent_exempt_minimum = self
.client
.get_minimum_balance_for_rent_exemption(new_account_len)
.await
.map_err(TokenError::Client)?;
Ok(new_rent_exempt_minimum.saturating_sub(account_lamports))
}

/// Initialize token-group on a mint
#[allow(clippy::too_many_arguments)]
pub async fn token_group_initialize_with_rent_transfer<S: Signers>(
&self,
payer: &Pubkey,
mint_authority: &Pubkey,
update_authority: &Pubkey,
max_size: u32,
signing_keypairs: &S,
) -> TokenResult<T::Output> {
let additional_lamports = self
.get_additional_rent_for_new_group()
.await?;
let mut instructions = vec![];
if additional_lamports > 0 {
instructions.push(system_instruction::transfer(
payer,
&self.pubkey,
additional_lamports,
));
}
instructions.push(spl_token_group_interface::instruction::initialize_group(
&self.program_id,
&self.pubkey,
&self.pubkey,
mint_authority,
Some(*update_authority),
max_size,
));
self.process_ixs(&instructions, signing_keypairs).await
}
}
1 change: 1 addition & 0 deletions token/program-2022-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ spl-pod = { version = "0.1.0", path = "../../libraries/pod" }
spl-token-2022 = { version = "0.9", path="../program-2022", features = ["no-entrypoint"] }
spl-instruction-padding = { version = "0.1.0", path="../../instruction-padding/program", features = ["no-entrypoint"] }
spl-token-client = { version = "0.7", path = "../client" }
spl-token-group-interface = { version = "0.1", path = "../../token-group/interface" }
spl-token-metadata-interface = { version = "0.2", path = "../../token-metadata/interface" }
spl-transfer-hook-example = { version = "0.3", path="../transfer-hook-example", features = ["no-entrypoint"] }
spl-transfer-hook-interface = { version = "0.3", path="../transfer-hook-interface" }
Expand Down
272 changes: 272 additions & 0 deletions token/program-2022-test/tests/token_group_initialize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
#![cfg(feature = "test-sbf")]

mod program_test;
use {
program_test::TestContext,
solana_program_test::{processor, tokio, ProgramTest},
solana_sdk::{
instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair,
transaction::TransactionError, transport::TransportError,
},
spl_pod::bytemuck::pod_from_bytes,
spl_token_2022::{error::TokenError, extension::BaseStateWithExtensions, processor::Processor},
spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError},
spl_token_group_interface::{error::TokenGroupError, state::TokenGroup},
std::{convert::TryInto, sync::Arc},
};

fn setup_program_test() -> ProgramTest {
let mut program_test = ProgramTest::default();
program_test.add_program(
"spl_token_2022",
spl_token_2022::id(),
processor!(Processor::process),
);
program_test
}

async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext {
let program_test = setup_program_test();

let context = program_test.start_with_context().await;
let context = Arc::new(tokio::sync::Mutex::new(context));
let mut context = TestContext {
context,
token_context: None,
};
let group_address = Some(mint.pubkey());
context
.init_token_with_mint_keypair_and_freeze_authority(
mint,
vec![ExtensionInitializationParams::GroupPointer {
authority: Some(*authority),
group_address,
}],
None,
)
.await
.unwrap();
context
}

#[tokio::test]
async fn success_initialize() {
let authority = Pubkey::new_unique();
let mint_keypair = Keypair::new();
let mut test_context = setup(mint_keypair, &authority).await;
let payer_pubkey = test_context.context.lock().await.payer.pubkey();
let token_context = test_context.token_context.take().unwrap();

let update_authority = Pubkey::new_unique();
let max_size = 10;
let token_group = TokenGroup {
update_authority: Some(update_authority).try_into().unwrap(),
mint: *token_context.token.get_address(),
size: 0.into(),
max_size: max_size.into(),
};

// fails without more lamports for new rent-exemption
let error = token_context
.token
.token_group_initialize(
&token_context.mint_authority.pubkey(),
&update_authority,
max_size,
&[&token_context.mint_authority],
)
.await
.unwrap_err();
assert_eq!(
error,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InsufficientFundsForRent { account_index: 2 }
)))
);

// fail wrong signer
let not_mint_authority = Keypair::new();
let error = token_context
.token
.token_group_initialize_with_rent_transfer(
&payer_pubkey,
&not_mint_authority.pubkey(),
&update_authority,
max_size,
&[&not_mint_authority],
)
.await
.unwrap_err();
assert_eq!(
error,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
1,
InstructionError::Custom(TokenGroupError::IncorrectMintAuthority as u32)
)
)))
);

token_context
.token
.token_group_initialize_with_rent_transfer(
&payer_pubkey,
&token_context.mint_authority.pubkey(),
&update_authority,
max_size,
&[&token_context.mint_authority],
)
.await
.unwrap();

// check that the data is correct
let mint_info = token_context.token.get_mint_info().await.unwrap();
let group_bytes = mint_info.get_extension_bytes::<TokenGroup>().unwrap();
let fetched_group = pod_from_bytes::<TokenGroup>(group_bytes).unwrap();
assert_eq!(fetched_group, &token_group);

// fail double-init
let error = token_context
.token
.token_group_initialize_with_rent_transfer(
&payer_pubkey,
&token_context.mint_authority.pubkey(),
&update_authority,
max_size,
&[&token_context.mint_authority],
)
.await
.unwrap_err();
assert_eq!(
error,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
1,
InstructionError::Custom(TokenError::ExtensionAlreadyInitialized as u32)
)
)))
);
}

#[tokio::test]
async fn fail_without_group_pointer() {
let mut test_context = {
let mint_keypair = Keypair::new();
let program_test = setup_program_test();
let context = program_test.start_with_context().await;
let context = Arc::new(tokio::sync::Mutex::new(context));
let mut context = TestContext {
context,
token_context: None,
};
context
.init_token_with_mint_keypair_and_freeze_authority(mint_keypair, vec![], None)
.await
.unwrap();
context
};

let payer_pubkey = test_context.context.lock().await.payer.pubkey();
let token_context = test_context.token_context.take().unwrap();

let error = token_context
.token
.token_group_initialize_with_rent_transfer(
&payer_pubkey,
&token_context.mint_authority.pubkey(),
&Pubkey::new_unique(),
5,
&[&token_context.mint_authority],
)
.await
.unwrap_err();
assert_eq!(
error,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
1,
InstructionError::Custom(TokenError::InvalidExtensionCombination as u32)
)
)))
);
}

#[tokio::test]
async fn fail_init_in_another_mint() {
let authority = Pubkey::new_unique();
let first_mint_keypair = Keypair::new();
let first_mint = first_mint_keypair.pubkey();
let mut test_context = setup(first_mint_keypair, &authority).await;
let second_mint_keypair = Keypair::new();
let second_mint = second_mint_keypair.pubkey();
test_context
.init_token_with_mint_keypair_and_freeze_authority(
second_mint_keypair,
vec![ExtensionInitializationParams::GroupPointer {
authority: Some(authority),
group_address: Some(second_mint),
}],
None,
)
.await
.unwrap();

let token_context = test_context.token_context.take().unwrap();

let error = token_context
.token
.process_ixs(
&[spl_token_group_interface::instruction::initialize_group(
&spl_token_2022::id(),
&first_mint,
token_context.token.get_address(),
&token_context.mint_authority.pubkey(),
Some(Pubkey::new_unique()),
5,
)],
&[&token_context.mint_authority],
)
.await
.unwrap_err();

assert_eq!(
error,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
0,
InstructionError::Custom(TokenError::MintMismatch as u32)
)
)))
);
}

#[tokio::test]
async fn fail_without_signature() {
let authority = Pubkey::new_unique();
let mint_keypair = Keypair::new();
let mut test_context = setup(mint_keypair, &authority).await;

let token_context = test_context.token_context.take().unwrap();

let mut instruction = spl_token_group_interface::instruction::initialize_group(
&spl_token_2022::id(),
token_context.token.get_address(),
token_context.token.get_address(),
&token_context.mint_authority.pubkey(),
Some(Pubkey::new_unique()),
5,
);
instruction.accounts[2].is_signer = false;
let error = token_context
.token
.process_ixs(&[instruction], &[] as &[&dyn Signer; 0]) // yuck, but the compiler needs it
.await
.unwrap_err();

assert_eq!(
error,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature)
)))
);
}
1 change: 1 addition & 0 deletions token/program-2022/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ solana-program = "1.17.2"
solana-zk-token-sdk = "1.17.2"
spl-memo = { version = "4.0.0", path = "../../memo/program", features = [ "no-entrypoint" ] }
spl-token = { version = "4.0", path = "../program", features = ["no-entrypoint"] }
spl-token-group-interface = { version = "0.1.0", path = "../../token-group/interface" }
spl-token-metadata-interface = { version = "0.2.0", path = "../../token-metadata/interface" }
spl-transfer-hook-interface = { version = "0.3.0", path = "../transfer-hook-interface" }
spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value" }
Expand Down
Loading

0 comments on commit d942fb4

Please sign in to comment.