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

Commit 3860a11

Browse files
committed
token 2022: add InitializeGroup instruction from SPL Token Group interface
1 parent 260e7b2 commit 3860a11

File tree

10 files changed

+473
-2
lines changed

10 files changed

+473
-2
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,271 @@
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::new(
63+
token_context.token.get_address(),
64+
Some(update_authority).try_into().unwrap(),
65+
max_size,
66+
);
67+
68+
// fails without more lamports for new rent-exemption
69+
let error = token_context
70+
.token
71+
.token_group_initialize(
72+
&token_context.mint_authority.pubkey(),
73+
&update_authority,
74+
max_size,
75+
&[&token_context.mint_authority],
76+
)
77+
.await
78+
.unwrap_err();
79+
assert_eq!(
80+
error,
81+
TokenClientError::Client(Box::new(TransportError::TransactionError(
82+
TransactionError::InsufficientFundsForRent { account_index: 2 }
83+
)))
84+
);
85+
86+
// fail wrong signer
87+
let not_mint_authority = Keypair::new();
88+
let error = token_context
89+
.token
90+
.token_group_initialize_with_rent_transfer(
91+
&payer_pubkey,
92+
&not_mint_authority.pubkey(),
93+
&update_authority,
94+
max_size,
95+
&[&not_mint_authority],
96+
)
97+
.await
98+
.unwrap_err();
99+
assert_eq!(
100+
error,
101+
TokenClientError::Client(Box::new(TransportError::TransactionError(
102+
TransactionError::InstructionError(
103+
1,
104+
InstructionError::Custom(TokenGroupError::IncorrectMintAuthority as u32)
105+
)
106+
)))
107+
);
108+
109+
token_context
110+
.token
111+
.token_group_initialize_with_rent_transfer(
112+
&payer_pubkey,
113+
&token_context.mint_authority.pubkey(),
114+
&update_authority,
115+
max_size,
116+
&[&token_context.mint_authority],
117+
)
118+
.await
119+
.unwrap();
120+
121+
// check that the data is correct
122+
let mint_info = token_context.token.get_mint_info().await.unwrap();
123+
let group_bytes = mint_info.get_extension_bytes::<TokenGroup>().unwrap();
124+
let fetched_group = pod_from_bytes::<TokenGroup>(group_bytes).unwrap();
125+
assert_eq!(fetched_group, &token_group);
126+
127+
// fail double-init
128+
let error = token_context
129+
.token
130+
.token_group_initialize_with_rent_transfer(
131+
&payer_pubkey,
132+
&token_context.mint_authority.pubkey(),
133+
&update_authority,
134+
max_size,
135+
&[&token_context.mint_authority],
136+
)
137+
.await
138+
.unwrap_err();
139+
assert_eq!(
140+
error,
141+
TokenClientError::Client(Box::new(TransportError::TransactionError(
142+
TransactionError::InstructionError(
143+
1,
144+
InstructionError::Custom(TokenError::ExtensionAlreadyInitialized as u32)
145+
)
146+
)))
147+
);
148+
}
149+
150+
#[tokio::test]
151+
async fn fail_without_group_pointer() {
152+
let mut test_context = {
153+
let mint_keypair = Keypair::new();
154+
let program_test = setup_program_test();
155+
let context = program_test.start_with_context().await;
156+
let context = Arc::new(tokio::sync::Mutex::new(context));
157+
let mut context = TestContext {
158+
context,
159+
token_context: None,
160+
};
161+
context
162+
.init_token_with_mint_keypair_and_freeze_authority(mint_keypair, vec![], None)
163+
.await
164+
.unwrap();
165+
context
166+
};
167+
168+
let payer_pubkey = test_context.context.lock().await.payer.pubkey();
169+
let token_context = test_context.token_context.take().unwrap();
170+
171+
let error = token_context
172+
.token
173+
.token_group_initialize_with_rent_transfer(
174+
&payer_pubkey,
175+
&token_context.mint_authority.pubkey(),
176+
&Pubkey::new_unique(),
177+
5,
178+
&[&token_context.mint_authority],
179+
)
180+
.await
181+
.unwrap_err();
182+
assert_eq!(
183+
error,
184+
TokenClientError::Client(Box::new(TransportError::TransactionError(
185+
TransactionError::InstructionError(
186+
1,
187+
InstructionError::Custom(TokenError::InvalidExtensionCombination as u32)
188+
)
189+
)))
190+
);
191+
}
192+
193+
#[tokio::test]
194+
async fn fail_init_in_another_mint() {
195+
let authority = Pubkey::new_unique();
196+
let first_mint_keypair = Keypair::new();
197+
let first_mint = first_mint_keypair.pubkey();
198+
let mut test_context = setup(first_mint_keypair, &authority).await;
199+
let second_mint_keypair = Keypair::new();
200+
let second_mint = second_mint_keypair.pubkey();
201+
test_context
202+
.init_token_with_mint_keypair_and_freeze_authority(
203+
second_mint_keypair,
204+
vec![ExtensionInitializationParams::GroupPointer {
205+
authority: Some(authority),
206+
group_address: Some(second_mint),
207+
}],
208+
None,
209+
)
210+
.await
211+
.unwrap();
212+
213+
let token_context = test_context.token_context.take().unwrap();
214+
215+
let error = token_context
216+
.token
217+
.process_ixs(
218+
&[spl_token_group_interface::instruction::initialize_group(
219+
&spl_token_2022::id(),
220+
&first_mint,
221+
token_context.token.get_address(),
222+
&token_context.mint_authority.pubkey(),
223+
Some(Pubkey::new_unique()),
224+
5,
225+
)],
226+
&[&token_context.mint_authority],
227+
)
228+
.await
229+
.unwrap_err();
230+
231+
assert_eq!(
232+
error,
233+
TokenClientError::Client(Box::new(TransportError::TransactionError(
234+
TransactionError::InstructionError(
235+
0,
236+
InstructionError::Custom(TokenError::MintMismatch as u32)
237+
)
238+
)))
239+
);
240+
}
241+
242+
#[tokio::test]
243+
async fn fail_without_signature() {
244+
let authority = Pubkey::new_unique();
245+
let mint_keypair = Keypair::new();
246+
let mut test_context = setup(mint_keypair, &authority).await;
247+
248+
let token_context = test_context.token_context.take().unwrap();
249+
250+
let mut instruction = spl_token_group_interface::instruction::initialize_group(
251+
&spl_token_2022::id(),
252+
token_context.token.get_address(),
253+
token_context.token.get_address(),
254+
&token_context.mint_authority.pubkey(),
255+
Some(Pubkey::new_unique()),
256+
5,
257+
);
258+
instruction.accounts[2].is_signer = false;
259+
let error = token_context
260+
.token
261+
.process_ixs(&[instruction], &[] as &[&dyn Signer; 0]) // yuck, but the compiler needs it
262+
.await
263+
.unwrap_err();
264+
265+
assert_eq!(
266+
error,
267+
TokenClientError::Client(Box::new(TransportError::TransactionError(
268+
TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature)
269+
)))
270+
);
271+
}

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)