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

Commit ecba134

Browse files
committed
token 2022: add InitializeGroup instruction from SPL Token Group interface
1 parent 1b7a918 commit ecba134

File tree

10 files changed

+473
-1
lines changed

10 files changed

+473
-1
lines changed

Cargo.lock

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

token/client/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ spl-associated-token-account = { version = "2.0", path = "../../associated-token
2424
spl-memo = { version = "4.0.0", path = "../../memo/program", features = ["no-entrypoint"] }
2525
spl-token = { version = "4.0", path="../program", features = [ "no-entrypoint" ] }
2626
spl-token-2022 = { version = "0.9", path="../program-2022" }
27+
spl-token-group-interface = { version = "0.1", path="../../token-group/interface" }
2728
spl-token-metadata-interface = { version = "0.2", path="../../token-metadata/interface" }
2829
spl-transfer-hook-interface = { version = "0.3", path="../transfer-hook-interface" }
2930
thiserror = "1.0"

token/client/src/token.rs

+72
Original file line numberDiff line numberDiff line change
@@ -3846,4 +3846,76 @@ where
38463846
)
38473847
.await
38483848
}
3849+
3850+
/// Initialize token-group on a mint
3851+
pub async fn token_group_initialize<S: Signers>(
3852+
&self,
3853+
mint_authority: &Pubkey,
3854+
update_authority: &Pubkey,
3855+
max_size: u32,
3856+
signing_keypairs: &S,
3857+
) -> TokenResult<T::Output> {
3858+
self.process_ixs(
3859+
&[spl_token_group_interface::instruction::initialize_group(
3860+
&self.program_id,
3861+
&self.pubkey,
3862+
&self.pubkey,
3863+
mint_authority,
3864+
Some(*update_authority),
3865+
max_size,
3866+
)],
3867+
signing_keypairs,
3868+
)
3869+
.await
3870+
}
3871+
3872+
async fn get_additional_rent_for_new_group(&self) -> TokenResult<u64> {
3873+
let account = self.get_account(self.pubkey).await?;
3874+
let account_lamports = account.lamports;
3875+
let mint_state = self.unpack_mint_info(account)?;
3876+
let new_account_len = mint_state
3877+
.try_get_account_len()?
3878+
.checked_add(ExtensionType::try_calculate_account_len::<Mint>(&[
3879+
ExtensionType::TokenGroup,
3880+
])?)
3881+
.ok_or(TokenError::Program(
3882+
spl_token_2022::error::TokenError::Overflow.into(),
3883+
))?;
3884+
let new_rent_exempt_minimum = self
3885+
.client
3886+
.get_minimum_balance_for_rent_exemption(new_account_len)
3887+
.await
3888+
.map_err(TokenError::Client)?;
3889+
Ok(new_rent_exempt_minimum.saturating_sub(account_lamports))
3890+
}
3891+
3892+
/// Initialize token-group on a mint
3893+
#[allow(clippy::too_many_arguments)]
3894+
pub async fn token_group_initialize_with_rent_transfer<S: Signers>(
3895+
&self,
3896+
payer: &Pubkey,
3897+
mint_authority: &Pubkey,
3898+
update_authority: &Pubkey,
3899+
max_size: u32,
3900+
signing_keypairs: &S,
3901+
) -> TokenResult<T::Output> {
3902+
let additional_lamports = self.get_additional_rent_for_new_group().await?;
3903+
let mut instructions = vec![];
3904+
if additional_lamports > 0 {
3905+
instructions.push(system_instruction::transfer(
3906+
payer,
3907+
&self.pubkey,
3908+
additional_lamports,
3909+
));
3910+
}
3911+
instructions.push(spl_token_group_interface::instruction::initialize_group(
3912+
&self.program_id,
3913+
&self.pubkey,
3914+
&self.pubkey,
3915+
mint_authority,
3916+
Some(*update_authority),
3917+
max_size,
3918+
));
3919+
self.process_ixs(&instructions, signing_keypairs).await
3920+
}
38493921
}

token/program-2022-test/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ spl-pod = { version = "0.1.0", path = "../../libraries/pod" }
2929
spl-token-2022 = { version = "0.9", path="../program-2022", features = ["no-entrypoint"] }
3030
spl-instruction-padding = { version = "0.1.0", path="../../instruction-padding/program", features = ["no-entrypoint"] }
3131
spl-token-client = { version = "0.7", path = "../client" }
32+
spl-token-group-interface = { version = "0.1", path = "../../token-group/interface" }
3233
spl-token-metadata-interface = { version = "0.2", path = "../../token-metadata/interface" }
3334
spl-transfer-hook-example = { version = "0.3", path="../transfer-hook-example", features = ["no-entrypoint"] }
3435
spl-transfer-hook-interface = { version = "0.3", path="../transfer-hook-interface" }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
#![cfg(feature = "test-sbf")]
2+
3+
mod program_test;
4+
use {
5+
program_test::TestContext,
6+
solana_program_test::{processor, tokio, ProgramTest},
7+
solana_sdk::{
8+
instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair,
9+
transaction::TransactionError, transport::TransportError,
10+
},
11+
spl_pod::bytemuck::pod_from_bytes,
12+
spl_token_2022::{error::TokenError, extension::BaseStateWithExtensions, processor::Processor},
13+
spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError},
14+
spl_token_group_interface::{error::TokenGroupError, state::TokenGroup},
15+
std::{convert::TryInto, sync::Arc},
16+
};
17+
18+
fn setup_program_test() -> ProgramTest {
19+
let mut program_test = ProgramTest::default();
20+
program_test.add_program(
21+
"spl_token_2022",
22+
spl_token_2022::id(),
23+
processor!(Processor::process),
24+
);
25+
program_test
26+
}
27+
28+
async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext {
29+
let program_test = setup_program_test();
30+
31+
let context = program_test.start_with_context().await;
32+
let context = Arc::new(tokio::sync::Mutex::new(context));
33+
let mut context = TestContext {
34+
context,
35+
token_context: None,
36+
};
37+
let group_address = Some(mint.pubkey());
38+
context
39+
.init_token_with_mint_keypair_and_freeze_authority(
40+
mint,
41+
vec![ExtensionInitializationParams::GroupPointer {
42+
authority: Some(*authority),
43+
group_address,
44+
}],
45+
None,
46+
)
47+
.await
48+
.unwrap();
49+
context
50+
}
51+
52+
#[tokio::test]
53+
async fn success_initialize() {
54+
let authority = Pubkey::new_unique();
55+
let mint_keypair = Keypair::new();
56+
let mut test_context = setup(mint_keypair, &authority).await;
57+
let payer_pubkey = test_context.context.lock().await.payer.pubkey();
58+
let token_context = test_context.token_context.take().unwrap();
59+
60+
let update_authority = Pubkey::new_unique();
61+
let max_size = 10;
62+
let token_group = TokenGroup {
63+
update_authority: Some(update_authority).try_into().unwrap(),
64+
mint: *token_context.token.get_address(),
65+
size: 0.into(),
66+
max_size: max_size.into(),
67+
};
68+
69+
// fails without more lamports for new rent-exemption
70+
let error = token_context
71+
.token
72+
.token_group_initialize(
73+
&token_context.mint_authority.pubkey(),
74+
&update_authority,
75+
max_size,
76+
&[&token_context.mint_authority],
77+
)
78+
.await
79+
.unwrap_err();
80+
assert_eq!(
81+
error,
82+
TokenClientError::Client(Box::new(TransportError::TransactionError(
83+
TransactionError::InsufficientFundsForRent { account_index: 2 }
84+
)))
85+
);
86+
87+
// fail wrong signer
88+
let not_mint_authority = Keypair::new();
89+
let error = token_context
90+
.token
91+
.token_group_initialize_with_rent_transfer(
92+
&payer_pubkey,
93+
&not_mint_authority.pubkey(),
94+
&update_authority,
95+
max_size,
96+
&[&not_mint_authority],
97+
)
98+
.await
99+
.unwrap_err();
100+
assert_eq!(
101+
error,
102+
TokenClientError::Client(Box::new(TransportError::TransactionError(
103+
TransactionError::InstructionError(
104+
1,
105+
InstructionError::Custom(TokenGroupError::IncorrectMintAuthority as u32)
106+
)
107+
)))
108+
);
109+
110+
token_context
111+
.token
112+
.token_group_initialize_with_rent_transfer(
113+
&payer_pubkey,
114+
&token_context.mint_authority.pubkey(),
115+
&update_authority,
116+
max_size,
117+
&[&token_context.mint_authority],
118+
)
119+
.await
120+
.unwrap();
121+
122+
// check that the data is correct
123+
let mint_info = token_context.token.get_mint_info().await.unwrap();
124+
let group_bytes = mint_info.get_extension_bytes::<TokenGroup>().unwrap();
125+
let fetched_group = pod_from_bytes::<TokenGroup>(group_bytes).unwrap();
126+
assert_eq!(fetched_group, &token_group);
127+
128+
// fail double-init
129+
let error = token_context
130+
.token
131+
.token_group_initialize_with_rent_transfer(
132+
&payer_pubkey,
133+
&token_context.mint_authority.pubkey(),
134+
&update_authority,
135+
max_size,
136+
&[&token_context.mint_authority],
137+
)
138+
.await
139+
.unwrap_err();
140+
assert_eq!(
141+
error,
142+
TokenClientError::Client(Box::new(TransportError::TransactionError(
143+
TransactionError::InstructionError(
144+
1,
145+
InstructionError::Custom(TokenError::ExtensionAlreadyInitialized as u32)
146+
)
147+
)))
148+
);
149+
}
150+
151+
#[tokio::test]
152+
async fn fail_without_group_pointer() {
153+
let mut test_context = {
154+
let mint_keypair = Keypair::new();
155+
let program_test = setup_program_test();
156+
let context = program_test.start_with_context().await;
157+
let context = Arc::new(tokio::sync::Mutex::new(context));
158+
let mut context = TestContext {
159+
context,
160+
token_context: None,
161+
};
162+
context
163+
.init_token_with_mint_keypair_and_freeze_authority(mint_keypair, vec![], None)
164+
.await
165+
.unwrap();
166+
context
167+
};
168+
169+
let payer_pubkey = test_context.context.lock().await.payer.pubkey();
170+
let token_context = test_context.token_context.take().unwrap();
171+
172+
let error = token_context
173+
.token
174+
.token_group_initialize_with_rent_transfer(
175+
&payer_pubkey,
176+
&token_context.mint_authority.pubkey(),
177+
&Pubkey::new_unique(),
178+
5,
179+
&[&token_context.mint_authority],
180+
)
181+
.await
182+
.unwrap_err();
183+
assert_eq!(
184+
error,
185+
TokenClientError::Client(Box::new(TransportError::TransactionError(
186+
TransactionError::InstructionError(
187+
1,
188+
InstructionError::Custom(TokenError::InvalidExtensionCombination as u32)
189+
)
190+
)))
191+
);
192+
}
193+
194+
#[tokio::test]
195+
async fn fail_init_in_another_mint() {
196+
let authority = Pubkey::new_unique();
197+
let first_mint_keypair = Keypair::new();
198+
let first_mint = first_mint_keypair.pubkey();
199+
let mut test_context = setup(first_mint_keypair, &authority).await;
200+
let second_mint_keypair = Keypair::new();
201+
let second_mint = second_mint_keypair.pubkey();
202+
test_context
203+
.init_token_with_mint_keypair_and_freeze_authority(
204+
second_mint_keypair,
205+
vec![ExtensionInitializationParams::GroupPointer {
206+
authority: Some(authority),
207+
group_address: Some(second_mint),
208+
}],
209+
None,
210+
)
211+
.await
212+
.unwrap();
213+
214+
let token_context = test_context.token_context.take().unwrap();
215+
216+
let error = token_context
217+
.token
218+
.process_ixs(
219+
&[spl_token_group_interface::instruction::initialize_group(
220+
&spl_token_2022::id(),
221+
&first_mint,
222+
token_context.token.get_address(),
223+
&token_context.mint_authority.pubkey(),
224+
Some(Pubkey::new_unique()),
225+
5,
226+
)],
227+
&[&token_context.mint_authority],
228+
)
229+
.await
230+
.unwrap_err();
231+
232+
assert_eq!(
233+
error,
234+
TokenClientError::Client(Box::new(TransportError::TransactionError(
235+
TransactionError::InstructionError(
236+
0,
237+
InstructionError::Custom(TokenError::MintMismatch as u32)
238+
)
239+
)))
240+
);
241+
}
242+
243+
#[tokio::test]
244+
async fn fail_without_signature() {
245+
let authority = Pubkey::new_unique();
246+
let mint_keypair = Keypair::new();
247+
let mut test_context = setup(mint_keypair, &authority).await;
248+
249+
let token_context = test_context.token_context.take().unwrap();
250+
251+
let mut instruction = spl_token_group_interface::instruction::initialize_group(
252+
&spl_token_2022::id(),
253+
token_context.token.get_address(),
254+
token_context.token.get_address(),
255+
&token_context.mint_authority.pubkey(),
256+
Some(Pubkey::new_unique()),
257+
5,
258+
);
259+
instruction.accounts[2].is_signer = false;
260+
let error = token_context
261+
.token
262+
.process_ixs(&[instruction], &[] as &[&dyn Signer; 0]) // yuck, but the compiler needs it
263+
.await
264+
.unwrap_err();
265+
266+
assert_eq!(
267+
error,
268+
TokenClientError::Client(Box::new(TransportError::TransactionError(
269+
TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature)
270+
)))
271+
);
272+
}

token/program-2022/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ solana-program = "1.17.2"
2727
solana-zk-token-sdk = "1.17.2"
2828
spl-memo = { version = "4.0.0", path = "../../memo/program", features = [ "no-entrypoint" ] }
2929
spl-token = { version = "4.0", path = "../program", features = ["no-entrypoint"] }
30+
spl-token-group-interface = { version = "0.1.0", path = "../../token-group/interface" }
3031
spl-token-metadata-interface = { version = "0.2.0", path = "../../token-metadata/interface" }
3132
spl-transfer-hook-interface = { version = "0.3.0", path = "../transfer-hook-interface" }
3233
spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value" }

0 commit comments

Comments
 (0)