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

Commit ad86abe

Browse files
author
Joe C
authored
[token cli]: add support for group member pointer
This PR adds support to the SPL Token CLI for Token Group Member Pointer!
1 parent a6c2c24 commit ad86abe

File tree

3 files changed

+249
-34
lines changed

3 files changed

+249
-34
lines changed

token/cli/src/clap_app.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ pub enum CommandName {
117117
WithdrawConfidentialTokens,
118118
ApplyPendingBalance,
119119
UpdateGroupAddress,
120+
UpdateMemberAddress,
120121
}
121122
impl fmt::Display for CommandName {
122123
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -186,6 +187,7 @@ pub enum CliAuthorityType {
186187
MetadataPointer,
187188
Metadata,
188189
GroupPointer,
190+
GroupMemberPointer,
189191
}
190192
impl TryFrom<CliAuthorityType> for AuthorityType {
191193
type Error = Error;
@@ -212,6 +214,7 @@ impl TryFrom<CliAuthorityType> for AuthorityType {
212214
Err("Metadata authority does not map to a token authority type".into())
213215
}
214216
CliAuthorityType::GroupPointer => Ok(AuthorityType::GroupPointer),
217+
CliAuthorityType::GroupMemberPointer => Ok(AuthorityType::GroupMemberPointer),
215218
}
216219
}
217220
}
@@ -684,6 +687,17 @@ pub fn app<'a, 'b>(
684687
"Specify address that stores token group configurations."
685688
),
686689
)
690+
.arg(
691+
Arg::with_name("member_address")
692+
.long("member-address")
693+
.value_name("ADDRESS")
694+
.validator(is_valid_pubkey)
695+
.takes_value(true)
696+
.conflicts_with("enable_member")
697+
.help(
698+
"Specify address that stores token member configurations."
699+
),
700+
)
687701
.arg(
688702
Arg::with_name("enable_non_transferable")
689703
.long("enable-non-transferable")
@@ -760,6 +774,15 @@ pub fn app<'a, 'b>(
760774
.takes_value(false)
761775
.help("Enables group configurations in the mint. The mint authority must initialize the group."),
762776
)
777+
.arg(
778+
Arg::with_name("enable_member")
779+
.long("enable-member")
780+
.conflicts_with("group_address")
781+
.conflicts_with("enable_group")
782+
.conflicts_with("member_address")
783+
.takes_value(false)
784+
.help("Enables group member configurations in the mint. The mint authority must initialize the member."),
785+
)
763786
.nonce_args(true)
764787
.arg(memo_arg())
765788
)
@@ -1961,6 +1984,49 @@ pub fn app<'a, 'b>(
19611984
.arg(multisig_signer_arg())
19621985
.nonce_args(true)
19631986
)
1987+
.subcommand(
1988+
SubCommand::with_name(CommandName::UpdateMemberAddress.into())
1989+
.about("Updates group member pointer address for the mint. Requires the group member pointer extension.")
1990+
.arg(
1991+
Arg::with_name("token")
1992+
.validator(is_valid_pubkey)
1993+
.value_name("TOKEN_MINT_ADDRESS")
1994+
.takes_value(true)
1995+
.index(1)
1996+
.required(true)
1997+
.help("The address of the token mint to update the group member pointer address"),
1998+
)
1999+
.arg(
2000+
Arg::with_name("member_address")
2001+
.index(2)
2002+
.validator(is_valid_pubkey)
2003+
.value_name("MEMBER_ADDRESS")
2004+
.takes_value(true)
2005+
.required_unless("disable")
2006+
.help("Specify address that stores token's group-member-pointer"),
2007+
)
2008+
.arg(
2009+
Arg::with_name("disable")
2010+
.long("disable")
2011+
.takes_value(false)
2012+
.conflicts_with("member_address")
2013+
.help("Unset group member pointer address.")
2014+
)
2015+
.arg(
2016+
Arg::with_name("authority")
2017+
.long("authority")
2018+
.value_name("KEYPAIR")
2019+
.validator(is_valid_signer)
2020+
.takes_value(true)
2021+
.help(
2022+
"Specify the token's group-member-pointer authority. \
2023+
This may be a keypair file or the ASK keyword. \
2024+
Defaults to the client keypair.",
2025+
),
2026+
)
2027+
.arg(multisig_signer_arg())
2028+
.nonce_args(true)
2029+
)
19642030
.subcommand(
19652031
SubCommand::with_name(CommandName::WithdrawWithheldTokens.into())
19662032
.about("Withdraw withheld transfer fee tokens from mint and / or account(s)")

token/cli/src/command.rs

Lines changed: 96 additions & 34 deletions
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_member_pointer::GroupMemberPointer,
4950
group_pointer::GroupPointer,
5051
interest_bearing_mint::InterestBearingConfig,
5152
memo_transfer::MemoTransfer,
@@ -176,6 +177,14 @@ fn native_token_client_from_config(
176177
}
177178
}
178179

180+
#[derive(strum_macros::Display, Debug)]
181+
#[strum(serialize_all = "kebab-case")]
182+
enum Pointer {
183+
Metadata,
184+
Group,
185+
GroupMember,
186+
}
187+
179188
#[allow(clippy::too_many_arguments)]
180189
async fn command_create_token(
181190
config: &Config<'_>,
@@ -189,13 +198,15 @@ async fn command_create_token(
189198
memo: Option<String>,
190199
metadata_address: Option<Pubkey>,
191200
group_address: Option<Pubkey>,
201+
member_address: Option<Pubkey>,
192202
rate_bps: Option<i16>,
193203
default_account_state: Option<AccountState>,
194204
transfer_fee: Option<(u16, u64)>,
195205
confidential_transfer_auto_approve: Option<bool>,
196206
transfer_hook_program_id: Option<Pubkey>,
197207
enable_metadata: bool,
198208
enable_group: bool,
209+
enable_member: bool,
199210
bulk_signers: Vec<Arc<dyn Signer>>,
200211
) -> CommandResult {
201212
println_display(
@@ -296,6 +307,18 @@ async fn command_create_token(
296307
});
297308
}
298309

310+
if member_address.is_some() || enable_member {
311+
let member_address = if enable_member {
312+
Some(token_pubkey)
313+
} else {
314+
member_address
315+
};
316+
extensions.push(ExtensionInitializationParams::GroupMemberPointer {
317+
authority: Some(authority),
318+
member_address,
319+
});
320+
}
321+
299322
let res = token
300323
.create_mint(
301324
&authority,
@@ -327,6 +350,15 @@ async fn command_create_token(
327350
);
328351
}
329352

353+
if enable_member {
354+
println_display(
355+
config,
356+
format!(
357+
"To initialize group member configurations inside the mint, please run `spl-token initialize-member {token_pubkey}`, and sign with the mint authority and the group's update authority.",
358+
),
359+
);
360+
}
361+
330362
Ok(match tx_return {
331363
TransactionReturnData::CliSignature(cli_signature) => format_output(
332364
CliCreateToken {
@@ -871,6 +903,16 @@ async fn command_authorize(
871903
))
872904
}
873905
}
906+
CliAuthorityType::GroupMemberPointer => {
907+
if let Ok(extension) = mint.get_extension::<GroupMemberPointer>() {
908+
Ok(Option::<Pubkey>::from(extension.authority))
909+
} else {
910+
Err(format!(
911+
"Mint `{}` does not support a group member pointer",
912+
account
913+
))
914+
}
915+
}
874916
}?;
875917

876918
Ok((account, previous_authority))
@@ -910,7 +952,8 @@ async fn command_authorize(
910952
| CliAuthorityType::ConfidentialTransferFee
911953
| CliAuthorityType::MetadataPointer
912954
| CliAuthorityType::Metadata
913-
| CliAuthorityType::GroupPointer => Err(format!(
955+
| CliAuthorityType::GroupPointer
956+
| CliAuthorityType::GroupMemberPointer => Err(format!(
914957
"Authority type `{auth_str}` not supported for SPL Token accounts",
915958
)),
916959
CliAuthorityType::Owner => {
@@ -2491,48 +2534,39 @@ async fn command_cpi_guard(
24912534
})
24922535
}
24932536

2494-
async fn command_update_metadata_pointer_address(
2537+
async fn command_update_pointer_address(
24952538
config: &Config<'_>,
24962539
token_pubkey: Pubkey,
24972540
authority: Pubkey,
2498-
new_metadata_address: Option<Pubkey>,
2541+
new_address: Option<Pubkey>,
24992542
bulk_signers: BulkSigners,
2543+
pointer: Pointer,
25002544
) -> CommandResult {
25012545
if config.sign_only {
2502-
panic!("Config can not be sign-only for updating metadata pointer address.");
2546+
panic!(
2547+
"Config can not be sign-only for updating {} pointer address.",
2548+
pointer
2549+
);
25032550
}
25042551

25052552
let token = token_client_from_config(config, &token_pubkey, None)?;
2506-
let res = token
2507-
.update_metadata_address(&authority, new_metadata_address, &bulk_signers)
2508-
.await?;
2509-
2510-
let tx_return = finish_tx(config, &res, false).await?;
2511-
Ok(match tx_return {
2512-
TransactionReturnData::CliSignature(signature) => {
2513-
config.output_format.formatted_string(&signature)
2553+
let res = match pointer {
2554+
Pointer::Metadata => {
2555+
token
2556+
.update_metadata_address(&authority, new_address, &bulk_signers)
2557+
.await
25142558
}
2515-
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
2516-
config.output_format.formatted_string(&sign_only_data)
2559+
Pointer::Group => {
2560+
token
2561+
.update_group_address(&authority, new_address, &bulk_signers)
2562+
.await
25172563
}
2518-
})
2519-
}
2520-
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?;
2564+
Pointer::GroupMember => {
2565+
token
2566+
.update_group_member_address(&authority, new_address, &bulk_signers)
2567+
.await
2568+
}
2569+
}?;
25362570

25372571
let tx_return = finish_tx(config, &res, false).await?;
25382572
Ok(match tx_return {
@@ -3253,6 +3287,7 @@ pub async fn process_command<'a>(
32533287
let rate_bps = value_t!(arg_matches, "interest_rate", i16).ok();
32543288
let metadata_address = value_t!(arg_matches, "metadata_address", Pubkey).ok();
32553289
let group_address = value_t!(arg_matches, "group_address", Pubkey).ok();
3290+
let member_address = value_t!(arg_matches, "member_address", Pubkey).ok();
32563291

32573292
let transfer_fee = arg_matches.values_of("transfer_fee").map(|mut v| {
32583293
(
@@ -3298,13 +3333,15 @@ pub async fn process_command<'a>(
32983333
memo,
32993334
metadata_address,
33003335
group_address,
3336+
member_address,
33013337
rate_bps,
33023338
default_account_state,
33033339
transfer_fee,
33043340
confidential_transfer_auto_approve,
33053341
transfer_hook_program_id,
33063342
arg_matches.is_present("enable_metadata"),
33073343
arg_matches.is_present("enable_group"),
3344+
arg_matches.is_present("enable_member"),
33083345
bulk_signers,
33093346
)
33103347
.await
@@ -3961,12 +3998,13 @@ pub async fn process_command<'a>(
39613998
}
39623999
let metadata_address = value_t!(arg_matches, "metadata_address", Pubkey).ok();
39634000

3964-
command_update_metadata_pointer_address(
4001+
command_update_pointer_address(
39654002
config,
39664003
token,
39674004
authority,
39684005
metadata_address,
39694006
bulk_signers,
4007+
Pointer::Metadata,
39704008
)
39714009
.await
39724010
}
@@ -3983,12 +4021,36 @@ pub async fn process_command<'a>(
39834021
}
39844022
let group_address = value_t!(arg_matches, "group_address", Pubkey).ok();
39854023

3986-
command_update_group_pointer_address(
4024+
command_update_pointer_address(
39874025
config,
39884026
token,
39894027
authority,
39904028
group_address,
39914029
bulk_signers,
4030+
Pointer::Group,
4031+
)
4032+
.await
4033+
}
4034+
(CommandName::UpdateMemberAddress, arg_matches) => {
4035+
// Since account is required argument it will always be present
4036+
let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager)
4037+
.unwrap()
4038+
.unwrap();
4039+
4040+
let (authority_signer, authority) =
4041+
config.signer_or_default(arg_matches, "authority", &mut wallet_manager);
4042+
if config.multisigner_pubkeys.is_empty() {
4043+
push_signer_with_dedup(authority_signer, &mut bulk_signers);
4044+
}
4045+
let member_address = value_t!(arg_matches, "member_address", Pubkey).ok();
4046+
4047+
command_update_pointer_address(
4048+
config,
4049+
token,
4050+
authority,
4051+
member_address,
4052+
bulk_signers,
4053+
Pointer::GroupMember,
39924054
)
39934055
.await
39944056
}

0 commit comments

Comments
 (0)