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

Commit 25817cb

Browse files
authored
token-cli: Support DefaultAccountState extension (#3785)
* token-cli: Support DefaultAccountState extension * Switch to `default-account-state` flag * Change error to assert * Rebase fix
1 parent 8e98905 commit 25817cb

File tree

1 file changed

+230
-4
lines changed

1 file changed

+230
-4
lines changed

token/cli/src/main.rs

Lines changed: 230 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ use solana_sdk::{
3737
use spl_associated_token_account::get_associated_token_address_with_program_id;
3838
use spl_token_2022::{
3939
extension::{
40-
cpi_guard::CpiGuard, interest_bearing_mint::InterestBearingConfig,
41-
memo_transfer::MemoTransfer, mint_close_authority::MintCloseAuthority, ExtensionType,
42-
StateWithExtensionsOwned,
40+
cpi_guard::CpiGuard, default_account_state::DefaultAccountState,
41+
interest_bearing_mint::InterestBearingConfig, memo_transfer::MemoTransfer,
42+
mint_close_authority::MintCloseAuthority, ExtensionType, StateWithExtensionsOwned,
4343
},
4444
instruction::*,
45-
state::{Account, Mint},
45+
state::{Account, AccountState, Mint},
4646
};
4747
use spl_token_client::{
4848
client::{ProgramRpcClientSendTransaction, RpcClientResponse},
@@ -134,6 +134,7 @@ pub enum CommandName {
134134
DisableRequiredTransferMemos,
135135
EnableCpiGuard,
136136
DisableCpiGuard,
137+
UpdateDefaultAccountState,
137138
}
138139
impl fmt::Display for CommandName {
139140
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -389,6 +390,7 @@ async fn command_create_token(
389390
enable_non_transferable: bool,
390391
memo: Option<String>,
391392
rate_bps: Option<i16>,
393+
default_account_state: Option<AccountState>,
392394
bulk_signers: Vec<Arc<dyn Signer>>,
393395
) -> CommandResult {
394396
println_display(
@@ -422,6 +424,14 @@ async fn command_create_token(
422424
extensions.push(ExtensionInitializationParams::NonTransferable);
423425
}
424426

427+
if let Some(state) = default_account_state {
428+
assert!(
429+
enable_freeze,
430+
"Token requires a freeze authority to default to frozen accounts"
431+
);
432+
extensions.push(ExtensionInitializationParams::DefaultAccountState { state })
433+
}
434+
425435
if let Some(text) = memo {
426436
token.with_memo(text, vec![config.default_signer()?.pubkey()]);
427437
}
@@ -1994,6 +2004,71 @@ async fn command_cpi_guard(
19942004
})
19952005
}
19962006

2007+
async fn command_update_default_account_state(
2008+
config: &Config<'_>,
2009+
token_pubkey: Pubkey,
2010+
freeze_authority: Pubkey,
2011+
new_default_state: AccountState,
2012+
bulk_signers: BulkSigners,
2013+
) -> CommandResult {
2014+
if !config.sign_only {
2015+
let mint_account = config.get_account_checked(&token_pubkey).await?;
2016+
2017+
let mint_state = StateWithExtensionsOwned::<Mint>::unpack(mint_account.data)
2018+
.map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?;
2019+
match mint_state.base.freeze_authority {
2020+
COption::None => {
2021+
return Err(format!("Mint {} has no freeze authority.", token_pubkey).into())
2022+
}
2023+
COption::Some(mint_freeze_authority) => {
2024+
if mint_freeze_authority != freeze_authority {
2025+
return Err(format!(
2026+
"Mint {} has a freeze authority {}, {} provided",
2027+
token_pubkey, mint_freeze_authority, freeze_authority
2028+
)
2029+
.into());
2030+
}
2031+
}
2032+
}
2033+
2034+
if let Ok(default_account_state) = mint_state.get_extension::<DefaultAccountState>() {
2035+
if default_account_state.state == u8::from(new_default_state) {
2036+
let state_string = match new_default_state {
2037+
AccountState::Frozen => "frozen",
2038+
AccountState::Initialized => "initialized",
2039+
_ => unreachable!(),
2040+
};
2041+
return Err(format!(
2042+
"Mint {} already has default account state {}",
2043+
token_pubkey, state_string
2044+
)
2045+
.into());
2046+
}
2047+
} else {
2048+
return Err(format!(
2049+
"Mint {} does not support default account states",
2050+
token_pubkey
2051+
)
2052+
.into());
2053+
}
2054+
}
2055+
2056+
let token = token_client_from_config(config, &token_pubkey, None)?;
2057+
let res = token
2058+
.set_default_account_state(&freeze_authority, &new_default_state, &bulk_signers)
2059+
.await?;
2060+
2061+
let tx_return = finish_tx(config, &res, false).await?;
2062+
Ok(match tx_return {
2063+
TransactionReturnData::CliSignature(signature) => {
2064+
config.output_format.formatted_string(&signature)
2065+
}
2066+
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
2067+
config.output_format.formatted_string(&sign_only_data)
2068+
}
2069+
})
2070+
}
2071+
19972072
struct SignOnlyNeedsFullMintSpec {}
19982073
impl offline::ArgsConfig for SignOnlyNeedsFullMintSpec {
19992074
fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> {
@@ -2174,6 +2249,17 @@ fn app<'a, 'b>(
21742249
"Permanently force tokens to be non-transferable. Thay may still be burned."
21752250
),
21762251
)
2252+
.arg(
2253+
Arg::with_name("default_account_state")
2254+
.long("default-account-state")
2255+
.requires("enable_freeze")
2256+
.possible_values(&["initialized", "frozen"])
2257+
.help("Specify that accounts have a default state. \
2258+
Note: specifying \"initialized\" adds an extension, which gives \
2259+
the option of specifying default frozen accounts in the future. \
2260+
This behavior is not the same as the default, which makes it \
2261+
impossible to specify a default account state in the future."),
2262+
)
21772263
.nonce_args(true)
21782264
.arg(memo_arg())
21792265
)
@@ -3053,6 +3139,44 @@ fn app<'a, 'b>(
30533139
.arg(multisig_signer_arg())
30543140
.nonce_args(true)
30553141
)
3142+
.subcommand(
3143+
SubCommand::with_name(CommandName::UpdateDefaultAccountState.into())
3144+
.about("Updates default account state for the mint. Requires the default account state extension.")
3145+
.arg(
3146+
Arg::with_name("token")
3147+
.validator(is_valid_pubkey)
3148+
.value_name("TOKEN_MINT_ADDRESS")
3149+
.takes_value(true)
3150+
.index(1)
3151+
.required(true)
3152+
.help("The address of the token mint to update default account state"),
3153+
)
3154+
.arg(
3155+
Arg::with_name("state")
3156+
.value_name("STATE")
3157+
.takes_value(true)
3158+
.possible_values(&["initialized", "frozen"])
3159+
.index(2)
3160+
.required(true)
3161+
.help("The new default account state."),
3162+
)
3163+
.arg(
3164+
Arg::with_name("freeze_authority")
3165+
.long("freeze-authority")
3166+
.value_name("KEYPAIR")
3167+
.validator(is_valid_signer)
3168+
.takes_value(true)
3169+
.help(
3170+
"Specify the token's freeze authority. \
3171+
This may be a keypair file or the ASK keyword. \
3172+
Defaults to the client keypair.",
3173+
),
3174+
)
3175+
.arg(owner_address_arg())
3176+
.arg(multisig_signer_arg())
3177+
.nonce_args(true)
3178+
.offline_args(),
3179+
)
30563180
}
30573181

30583182
#[tokio::main]
@@ -3121,6 +3245,15 @@ async fn process_command<'a>(
31213245
bulk_signers.push(token_signer);
31223246
}
31233247

3248+
let default_account_state =
3249+
arg_matches
3250+
.value_of("default_account_state")
3251+
.map(|s| match s {
3252+
"initialized" => AccountState::Initialized,
3253+
"frozen" => AccountState::Frozen,
3254+
_ => unreachable!(),
3255+
});
3256+
31243257
command_create_token(
31253258
config,
31263259
decimals,
@@ -3131,6 +3264,7 @@ async fn process_command<'a>(
31313264
arg_matches.is_present("enable_non_transferable"),
31323265
memo,
31333266
rate_bps,
3267+
default_account_state,
31343268
bulk_signers,
31353269
)
31363270
.await
@@ -3657,6 +3791,31 @@ async fn process_command<'a>(
36573791
config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?;
36583792
command_cpi_guard(config, token_account, owner, bulk_signers, false).await
36593793
}
3794+
(CommandName::UpdateDefaultAccountState, arg_matches) => {
3795+
// Since account is required argument it will always be present
3796+
let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager)
3797+
.unwrap()
3798+
.unwrap();
3799+
let (freeze_authority_signer, freeze_authority) =
3800+
config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager);
3801+
if !bulk_signers.contains(&freeze_authority_signer) {
3802+
bulk_signers.push(freeze_authority_signer);
3803+
}
3804+
let new_default_state = arg_matches.value_of("state").unwrap();
3805+
let new_default_state = match new_default_state {
3806+
"initialized" => AccountState::Initialized,
3807+
"frozen" => AccountState::Frozen,
3808+
_ => unreachable!(),
3809+
};
3810+
command_update_default_account_state(
3811+
config,
3812+
token,
3813+
freeze_authority,
3814+
new_default_state,
3815+
bulk_signers,
3816+
)
3817+
.await
3818+
}
36603819
}
36613820
}
36623821

@@ -3847,6 +4006,7 @@ mod tests {
38474006
false,
38484007
None,
38494008
None,
4009+
None,
38504010
bulk_signers,
38514011
)
38524012
.await
@@ -3874,6 +4034,7 @@ mod tests {
38744034
false,
38754035
None,
38764036
Some(rate_bps),
4037+
None,
38774038
bulk_signers,
38784039
)
38794040
.await
@@ -5121,6 +5282,7 @@ mod tests {
51215282
false,
51225283
None,
51235284
None,
5285+
None,
51245286
bulk_signers,
51255287
)
51265288
.await
@@ -5425,6 +5587,7 @@ mod tests {
54255587
true,
54265588
None,
54275589
None,
5590+
None,
54285591
bulk_signers,
54295592
)
54305593
.await
@@ -5455,4 +5618,67 @@ mod tests {
54555618
.await
54565619
.unwrap_err();
54575620
}
5621+
5622+
#[tokio::test]
5623+
#[serial]
5624+
async fn default_account_state() {
5625+
let (test_validator, payer) = new_validator_for_test().await;
5626+
let program_id = spl_token_2022::id();
5627+
let config = test_config_with_default_signer(&test_validator, &payer, &program_id);
5628+
let token_keypair = Keypair::new();
5629+
let token_pubkey = token_keypair.pubkey();
5630+
let bulk_signers: Vec<Arc<dyn Signer>> =
5631+
vec![Arc::new(clone_keypair(&payer)), Arc::new(token_keypair)];
5632+
5633+
command_create_token(
5634+
&config,
5635+
TEST_DECIMALS,
5636+
token_pubkey,
5637+
payer.pubkey(),
5638+
true,
5639+
false,
5640+
false,
5641+
None,
5642+
None,
5643+
Some(AccountState::Frozen),
5644+
bulk_signers,
5645+
)
5646+
.await
5647+
.unwrap();
5648+
5649+
let mint_account = config.rpc_client.get_account(&token_pubkey).await.unwrap();
5650+
let mint = StateWithExtensionsOwned::<Mint>::unpack(mint_account.data).unwrap();
5651+
let extension = mint.get_extension::<DefaultAccountState>().unwrap();
5652+
assert_eq!(extension.state, u8::from(AccountState::Frozen));
5653+
5654+
let frozen_account = create_associated_account(&config, &payer, token_pubkey).await;
5655+
let token_account = config
5656+
.rpc_client
5657+
.get_account(&frozen_account)
5658+
.await
5659+
.unwrap();
5660+
let account = StateWithExtensionsOwned::<Account>::unpack(token_account.data).unwrap();
5661+
assert_eq!(account.base.state, AccountState::Frozen);
5662+
5663+
process_test_command(
5664+
&config,
5665+
&payer,
5666+
&[
5667+
"spl-token",
5668+
CommandName::UpdateDefaultAccountState.into(),
5669+
&token_pubkey.to_string(),
5670+
"initialized",
5671+
],
5672+
)
5673+
.await
5674+
.unwrap();
5675+
let unfrozen_account = create_auxiliary_account(&config, &payer, token_pubkey).await;
5676+
let token_account = config
5677+
.rpc_client
5678+
.get_account(&unfrozen_account)
5679+
.await
5680+
.unwrap();
5681+
let account = StateWithExtensionsOwned::<Account>::unpack(token_account.data).unwrap();
5682+
assert_eq!(account.base.state, AccountState::Initialized);
5683+
}
54585684
}

0 commit comments

Comments
 (0)