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

Commit a6c2c24

Browse files
author
Joe C
authored
[token cli]: add support for group pointer
This PR adds support for the group pointer in the Token CLI!
1 parent b49a7ce commit a6c2c24

File tree

3 files changed

+237
-1
lines changed

3 files changed

+237
-1
lines changed

token/cli/src/clap_app.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ pub enum CommandName {
116116
DepositConfidentialTokens,
117117
WithdrawConfidentialTokens,
118118
ApplyPendingBalance,
119+
UpdateGroupAddress,
119120
}
120121
impl fmt::Display for CommandName {
121122
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -184,6 +185,7 @@ pub enum CliAuthorityType {
184185
ConfidentialTransferFee,
185186
MetadataPointer,
186187
Metadata,
188+
GroupPointer,
187189
}
188190
impl TryFrom<CliAuthorityType> for AuthorityType {
189191
type Error = Error;
@@ -209,6 +211,7 @@ impl TryFrom<CliAuthorityType> for AuthorityType {
209211
CliAuthorityType::Metadata => {
210212
Err("Metadata authority does not map to a token authority type".into())
211213
}
214+
CliAuthorityType::GroupPointer => Ok(AuthorityType::GroupPointer),
212215
}
213216
}
214217
}
@@ -670,6 +673,17 @@ pub fn app<'a, 'b>(
670673
"Specify address that stores token metadata."
671674
),
672675
)
676+
.arg(
677+
Arg::with_name("group_address")
678+
.long("group-address")
679+
.value_name("ADDRESS")
680+
.validator(is_valid_pubkey)
681+
.takes_value(true)
682+
.conflicts_with("enable_group")
683+
.help(
684+
"Specify address that stores token group configurations."
685+
),
686+
)
673687
.arg(
674688
Arg::with_name("enable_non_transferable")
675689
.long("enable-non-transferable")
@@ -739,6 +753,13 @@ pub fn app<'a, 'b>(
739753
.takes_value(false)
740754
.help("Enables metadata in the mint. The mint authority must initialize the metadata."),
741755
)
756+
.arg(
757+
Arg::with_name("enable_group")
758+
.long("enable-group")
759+
.conflicts_with("group_address")
760+
.takes_value(false)
761+
.help("Enables group configurations in the mint. The mint authority must initialize the group."),
762+
)
742763
.nonce_args(true)
743764
.arg(memo_arg())
744765
)
@@ -1897,6 +1918,49 @@ pub fn app<'a, 'b>(
18971918
.arg(multisig_signer_arg())
18981919
.nonce_args(true)
18991920
)
1921+
.subcommand(
1922+
SubCommand::with_name(CommandName::UpdateGroupAddress.into())
1923+
.about("Updates group pointer address for the mint. Requires the group pointer extension.")
1924+
.arg(
1925+
Arg::with_name("token")
1926+
.validator(is_valid_pubkey)
1927+
.value_name("TOKEN_MINT_ADDRESS")
1928+
.takes_value(true)
1929+
.index(1)
1930+
.required(true)
1931+
.help("The address of the token mint to update the group pointer address"),
1932+
)
1933+
.arg(
1934+
Arg::with_name("group_address")
1935+
.index(2)
1936+
.validator(is_valid_pubkey)
1937+
.value_name("GROUP_ADDRESS")
1938+
.takes_value(true)
1939+
.required_unless("disable")
1940+
.help("Specify address that stores token's group-pointer"),
1941+
)
1942+
.arg(
1943+
Arg::with_name("disable")
1944+
.long("disable")
1945+
.takes_value(false)
1946+
.conflicts_with("group_address")
1947+
.help("Unset group pointer address.")
1948+
)
1949+
.arg(
1950+
Arg::with_name("authority")
1951+
.long("authority")
1952+
.value_name("KEYPAIR")
1953+
.validator(is_valid_signer)
1954+
.takes_value(true)
1955+
.help(
1956+
"Specify the token's group-pointer authority. \
1957+
This may be a keypair file or the ASK keyword. \
1958+
Defaults to the client keypair.",
1959+
),
1960+
)
1961+
.arg(multisig_signer_arg())
1962+
.nonce_args(true)
1963+
)
19001964
.subcommand(
19011965
SubCommand::with_name(CommandName::WithdrawWithheldTokens.into())
19021966
.about("Withdraw withheld transfer fee tokens from mint and / or account(s)")

token/cli/src/command.rs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ use {
4646
confidential_transfer_fee::ConfidentialTransferFeeConfig,
4747
cpi_guard::CpiGuard,
4848
default_account_state::DefaultAccountState,
49+
group_pointer::GroupPointer,
4950
interest_bearing_mint::InterestBearingConfig,
5051
memo_transfer::MemoTransfer,
5152
metadata_pointer::MetadataPointer,
@@ -187,12 +188,14 @@ async fn command_create_token(
187188
enable_permanent_delegate: bool,
188189
memo: Option<String>,
189190
metadata_address: Option<Pubkey>,
191+
group_address: Option<Pubkey>,
190192
rate_bps: Option<i16>,
191193
default_account_state: Option<AccountState>,
192194
transfer_fee: Option<(u16, u64)>,
193195
confidential_transfer_auto_approve: Option<bool>,
194196
transfer_hook_program_id: Option<Pubkey>,
195197
enable_metadata: bool,
198+
enable_group: bool,
196199
bulk_signers: Vec<Arc<dyn Signer>>,
197200
) -> CommandResult {
198201
println_display(
@@ -281,6 +284,18 @@ async fn command_create_token(
281284
});
282285
}
283286

287+
if group_address.is_some() || enable_group {
288+
let group_address = if enable_group {
289+
Some(token_pubkey)
290+
} else {
291+
group_address
292+
};
293+
extensions.push(ExtensionInitializationParams::GroupPointer {
294+
authority: Some(authority),
295+
group_address,
296+
});
297+
}
298+
284299
let res = token
285300
.create_mint(
286301
&authority,
@@ -303,6 +318,15 @@ async fn command_create_token(
303318
);
304319
}
305320

321+
if enable_group {
322+
println_display(
323+
config,
324+
format!(
325+
"To initialize group configurations inside the mint, please run `spl-token initialize-group {token_pubkey} <MAX_SIZE>`, and sign with the mint authority.",
326+
),
327+
);
328+
}
329+
306330
Ok(match tx_return {
307331
TransactionReturnData::CliSignature(cli_signature) => format_output(
308332
CliCreateToken {
@@ -837,6 +861,16 @@ async fn command_authorize(
837861
Err(format!("Mint `{account}` does not support metadata"))
838862
}
839863
}
864+
CliAuthorityType::GroupPointer => {
865+
if let Ok(extension) = mint.get_extension::<GroupPointer>() {
866+
Ok(Option::<Pubkey>::from(extension.authority))
867+
} else {
868+
Err(format!(
869+
"Mint `{}` does not support a group pointer",
870+
account
871+
))
872+
}
873+
}
840874
}?;
841875

842876
Ok((account, previous_authority))
@@ -875,7 +909,8 @@ async fn command_authorize(
875909
| CliAuthorityType::TransferHookProgramId
876910
| CliAuthorityType::ConfidentialTransferFee
877911
| CliAuthorityType::MetadataPointer
878-
| CliAuthorityType::Metadata => Err(format!(
912+
| CliAuthorityType::Metadata
913+
| CliAuthorityType::GroupPointer => Err(format!(
879914
"Authority type `{auth_str}` not supported for SPL Token accounts",
880915
)),
881916
CliAuthorityType::Owner => {
@@ -2483,6 +2518,33 @@ async fn command_update_metadata_pointer_address(
24832518
})
24842519
}
24852520

2521+
async fn command_update_group_pointer_address(
2522+
config: &Config<'_>,
2523+
token_pubkey: Pubkey,
2524+
authority: Pubkey,
2525+
new_group_address: Option<Pubkey>,
2526+
bulk_signers: BulkSigners,
2527+
) -> CommandResult {
2528+
if config.sign_only {
2529+
panic!("Config can not be sign-only for updating group pointer address.");
2530+
}
2531+
2532+
let token = token_client_from_config(config, &token_pubkey, None)?;
2533+
let res = token
2534+
.update_group_address(&authority, new_group_address, &bulk_signers)
2535+
.await?;
2536+
2537+
let tx_return = finish_tx(config, &res, false).await?;
2538+
Ok(match tx_return {
2539+
TransactionReturnData::CliSignature(signature) => {
2540+
config.output_format.formatted_string(&signature)
2541+
}
2542+
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
2543+
config.output_format.formatted_string(&sign_only_data)
2544+
}
2545+
})
2546+
}
2547+
24862548
async fn command_update_default_account_state(
24872549
config: &Config<'_>,
24882550
token_pubkey: Pubkey,
@@ -3190,6 +3252,7 @@ pub async fn process_command<'a>(
31903252
let memo = value_t!(arg_matches, "memo", String).ok();
31913253
let rate_bps = value_t!(arg_matches, "interest_rate", i16).ok();
31923254
let metadata_address = value_t!(arg_matches, "metadata_address", Pubkey).ok();
3255+
let group_address = value_t!(arg_matches, "group_address", Pubkey).ok();
31933256

31943257
let transfer_fee = arg_matches.values_of("transfer_fee").map(|mut v| {
31953258
(
@@ -3234,12 +3297,14 @@ pub async fn process_command<'a>(
32343297
arg_matches.is_present("enable_permanent_delegate"),
32353298
memo,
32363299
metadata_address,
3300+
group_address,
32373301
rate_bps,
32383302
default_account_state,
32393303
transfer_fee,
32403304
confidential_transfer_auto_approve,
32413305
transfer_hook_program_id,
32423306
arg_matches.is_present("enable_metadata"),
3307+
arg_matches.is_present("enable_group"),
32433308
bulk_signers,
32443309
)
32453310
.await
@@ -3905,6 +3970,28 @@ pub async fn process_command<'a>(
39053970
)
39063971
.await
39073972
}
3973+
(CommandName::UpdateGroupAddress, arg_matches) => {
3974+
// Since account is required argument it will always be present
3975+
let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager)
3976+
.unwrap()
3977+
.unwrap();
3978+
3979+
let (authority_signer, authority) =
3980+
config.signer_or_default(arg_matches, "authority", &mut wallet_manager);
3981+
if config.multisigner_pubkeys.is_empty() {
3982+
push_signer_with_dedup(authority_signer, &mut bulk_signers);
3983+
}
3984+
let group_address = value_t!(arg_matches, "group_address", Pubkey).ok();
3985+
3986+
command_update_group_pointer_address(
3987+
config,
3988+
token,
3989+
authority,
3990+
group_address,
3991+
bulk_signers,
3992+
)
3993+
.await
3994+
}
39083995
(CommandName::WithdrawWithheldTokens, arg_matches) => {
39093996
let (authority_signer, authority) = config.signer_or_default(
39103997
arg_matches,

token/cli/tests/command.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use {
2020
confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint},
2121
cpi_guard::CpiGuard,
2222
default_account_state::DefaultAccountState,
23+
group_pointer::GroupPointer,
2324
interest_bearing_mint::InterestBearingConfig,
2425
memo_transfer::MemoTransfer,
2526
metadata_pointer::MetadataPointer,
@@ -124,6 +125,7 @@ async fn main() {
124125
async_trial!(withdraw_excess_lamports_from_mint, test_validator, payer),
125126
async_trial!(withdraw_excess_lamports_from_account, test_validator, payer),
126127
async_trial!(metadata_pointer, test_validator, payer),
128+
async_trial!(group_pointer, test_validator, payer),
127129
async_trial!(transfer_hook, test_validator, payer),
128130
async_trial!(metadata, test_validator, payer),
129131
// GC messes with every other test, so have it on its own test validator
@@ -3275,6 +3277,89 @@ async fn metadata_pointer(test_validator: &TestValidator, payer: &Keypair) {
32753277
);
32763278
}
32773279

3280+
async fn group_pointer(test_validator: &TestValidator, payer: &Keypair) {
3281+
let program_id = spl_token_2022::id();
3282+
let config = test_config_with_default_signer(test_validator, payer, &program_id);
3283+
let group_address = Pubkey::new_unique();
3284+
3285+
let result = process_test_command(
3286+
&config,
3287+
payer,
3288+
&[
3289+
"spl-token",
3290+
CommandName::CreateToken.into(),
3291+
"--program-id",
3292+
&program_id.to_string(),
3293+
"--group-address",
3294+
&group_address.to_string(),
3295+
],
3296+
)
3297+
.await
3298+
.unwrap();
3299+
3300+
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
3301+
let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap();
3302+
let account = config.rpc_client.get_account(&mint).await.unwrap();
3303+
let mint_state = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
3304+
3305+
let extension = mint_state.get_extension::<GroupPointer>().unwrap();
3306+
3307+
assert_eq!(
3308+
extension.group_address,
3309+
Some(group_address).try_into().unwrap()
3310+
);
3311+
3312+
let new_group_address = Pubkey::new_unique();
3313+
3314+
let _new_result = process_test_command(
3315+
&config,
3316+
payer,
3317+
&[
3318+
"spl-token",
3319+
CommandName::UpdateGroupAddress.into(),
3320+
&mint.to_string(),
3321+
&new_group_address.to_string(),
3322+
],
3323+
)
3324+
.await;
3325+
3326+
let new_account = config.rpc_client.get_account(&mint).await.unwrap();
3327+
let new_mint_state = StateWithExtensionsOwned::<Mint>::unpack(new_account.data).unwrap();
3328+
3329+
let new_extension = new_mint_state.get_extension::<GroupPointer>().unwrap();
3330+
3331+
assert_eq!(
3332+
new_extension.group_address,
3333+
Some(new_group_address).try_into().unwrap()
3334+
);
3335+
3336+
let _result_with_disable = process_test_command(
3337+
&config,
3338+
payer,
3339+
&[
3340+
"spl-token",
3341+
CommandName::UpdateGroupAddress.into(),
3342+
&mint.to_string(),
3343+
"--disable",
3344+
],
3345+
)
3346+
.await
3347+
.unwrap();
3348+
3349+
let new_account_disbale = config.rpc_client.get_account(&mint).await.unwrap();
3350+
let new_mint_state_disable =
3351+
StateWithExtensionsOwned::<Mint>::unpack(new_account_disbale.data).unwrap();
3352+
3353+
let new_extension_disable = new_mint_state_disable
3354+
.get_extension::<GroupPointer>()
3355+
.unwrap();
3356+
3357+
assert_eq!(
3358+
new_extension_disable.group_address,
3359+
None.try_into().unwrap()
3360+
);
3361+
}
3362+
32783363
async fn transfer_hook(test_validator: &TestValidator, payer: &Keypair) {
32793364
let program_id = spl_token_2022::id();
32803365
let mut config = test_config_with_default_signer(test_validator, payer, &program_id);

0 commit comments

Comments
 (0)