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

Commit feeda6a

Browse files
authored
token-cli: Memo transfer extension (#3525)
token-cli: Add support for required transfer memos
1 parent 6099f41 commit feeda6a

File tree

1 file changed

+217
-1
lines changed

1 file changed

+217
-1
lines changed

token/cli/src/main.rs

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ use spl_associated_token_account::{
4444
use spl_token_2022::{
4545
extension::{
4646
interest_bearing_mint, interest_bearing_mint::InterestBearingConfig,
47-
mint_close_authority::MintCloseAuthority, StateWithExtensionsOwned,
47+
memo_transfer::MemoTransfer, mint_close_authority::MintCloseAuthority, ExtensionType,
48+
StateWithExtensionsOwned,
4849
},
4950
instruction::*,
5051
state::{Account, Mint, Multisig},
@@ -153,6 +154,8 @@ pub enum CommandName {
153154
Display,
154155
Gc,
155156
SyncNative,
157+
EnableRequiredTransferMemos,
158+
DisableRequiredTransferMemos,
156159
}
157160
impl fmt::Display for CommandName {
158161
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -1887,6 +1890,103 @@ async fn command_sync_native(
18871890
})
18881891
}
18891892

1893+
// Both enable_required_transfer_mesos and disable_required_transfer_mesos
1894+
// Switches with enable_memos bool
1895+
async fn command_required_transfer_memos(
1896+
config: &Config<'_>,
1897+
token_account_address: Pubkey,
1898+
owner: Pubkey,
1899+
bulk_signers: BulkSigners,
1900+
enable_memos: bool,
1901+
) -> CommandResult {
1902+
if config.sign_only {
1903+
panic!("Config can not be sign only for enabling/disabling required transfer memos.");
1904+
}
1905+
let account_fetch = config
1906+
.rpc_client
1907+
.get_account(&token_account_address)
1908+
.await
1909+
.map_err(|err| {
1910+
format!(
1911+
"Token account {} does not exist: {}",
1912+
token_account_address, err
1913+
)
1914+
})?;
1915+
let program_id = config.program_id;
1916+
config.get_account_checked(&token_account_address).await?;
1917+
let mut instructions: Vec<Instruction> = Vec::new();
1918+
// Reallocation (if needed)
1919+
let current_account_len = account_fetch.data.len();
1920+
let state_with_extension = StateWithExtensionsOwned::<Account>::unpack(account_fetch.data)?;
1921+
let mut existing_extensions: Vec<ExtensionType> = state_with_extension.get_extension_types()?;
1922+
if existing_extensions.contains(&ExtensionType::MemoTransfer) {
1923+
let extension_data: bool = state_with_extension
1924+
.get_extension::<MemoTransfer>()?
1925+
.require_incoming_transfer_memos
1926+
.into();
1927+
if extension_data == enable_memos {
1928+
return Ok(format!(
1929+
"Required memo transfer was already {}",
1930+
if extension_data {
1931+
"enabled"
1932+
} else {
1933+
"disabled"
1934+
}
1935+
));
1936+
}
1937+
} else {
1938+
existing_extensions.push(ExtensionType::MemoTransfer);
1939+
let needed_account_len = ExtensionType::get_account_len::<Account>(&existing_extensions);
1940+
if needed_account_len > current_account_len {
1941+
instructions.push(reallocate(
1942+
&program_id,
1943+
&token_account_address,
1944+
&config.fee_payer.pubkey(),
1945+
&owner,
1946+
&config.multisigner_pubkeys,
1947+
&existing_extensions,
1948+
)?);
1949+
}
1950+
}
1951+
if enable_memos {
1952+
instructions.push(
1953+
spl_token_2022::extension::memo_transfer::instruction::enable_required_transfer_memos(
1954+
&program_id,
1955+
&token_account_address,
1956+
&owner,
1957+
&config.multisigner_pubkeys,
1958+
)?,
1959+
);
1960+
} else {
1961+
instructions.push(
1962+
spl_token_2022::extension::memo_transfer::instruction::disable_required_transfer_memos(
1963+
&program_id,
1964+
&token_account_address,
1965+
&owner,
1966+
&config.multisigner_pubkeys,
1967+
)?,
1968+
);
1969+
}
1970+
let tx_return = handle_tx(
1971+
&CliSignerInfo {
1972+
signers: bulk_signers,
1973+
},
1974+
config,
1975+
false,
1976+
0,
1977+
instructions,
1978+
)
1979+
.await?;
1980+
Ok(match tx_return {
1981+
TransactionReturnData::CliSignature(signature) => {
1982+
config.output_format.formatted_string(&signature)
1983+
}
1984+
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
1985+
config.output_format.formatted_string(&sign_only_data)
1986+
}
1987+
})
1988+
}
1989+
18901990
struct SignOnlyNeedsFullMintSpec {}
18911991
impl offline::ArgsConfig for SignOnlyNeedsFullMintSpec {
18921992
fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> {
@@ -2821,6 +2921,44 @@ fn app<'a, 'b>(
28212921
.help("Specify the specific token account address to sync"),
28222922
),
28232923
)
2924+
.subcommand(
2925+
SubCommand::with_name(CommandName::EnableRequiredTransferMemos.into())
2926+
.about("Enable required transfer memos for token account")
2927+
.arg(
2928+
Arg::with_name("account")
2929+
.validator(is_valid_pubkey)
2930+
.value_name("TOKEN_ACCOUNT_ADDRESS")
2931+
.takes_value(true)
2932+
.index(1)
2933+
.required(true)
2934+
.help("The address of the token account to enable required transfer memos")
2935+
)
2936+
.arg(
2937+
owner_address_arg()
2938+
)
2939+
.arg(multisig_signer_arg())
2940+
.nonce_args(true)
2941+
.offline_args()
2942+
)
2943+
.subcommand(
2944+
SubCommand::with_name(CommandName::DisableRequiredTransferMemos.into())
2945+
.about("Disable required transfer memos for token account")
2946+
.arg(
2947+
Arg::with_name("account")
2948+
.validator(is_valid_pubkey)
2949+
.value_name("TOKEN_ACCOUNT_ADDRESS")
2950+
.takes_value(true)
2951+
.index(1)
2952+
.required(true)
2953+
.help("The address of the token account to disable required transfer memos"),
2954+
)
2955+
.arg(
2956+
owner_address_arg()
2957+
)
2958+
.arg(multisig_signer_arg())
2959+
.nonce_args(true)
2960+
.offline_args()
2961+
)
28242962
}
28252963

28262964
#[tokio::main]
@@ -3356,6 +3494,28 @@ async fn process_command<'a>(
33563494
.await;
33573495
command_sync_native(address, bulk_signers, config).await
33583496
}
3497+
(CommandName::EnableRequiredTransferMemos, arg_matches) => {
3498+
let (owner_signer, owner) =
3499+
config.signer_or_default(arg_matches, "owner", &mut wallet_manager);
3500+
if !bulk_signers.contains(&owner_signer) {
3501+
bulk_signers.push(owner_signer);
3502+
}
3503+
// Since account is required argument it will always be present
3504+
let token_account =
3505+
config.pubkey_or_default(arg_matches, "account", &mut wallet_manager);
3506+
command_required_transfer_memos(config, token_account, owner, bulk_signers, true).await
3507+
}
3508+
(CommandName::DisableRequiredTransferMemos, arg_matches) => {
3509+
let (owner_signer, owner) =
3510+
config.signer_or_default(arg_matches, "owner", &mut wallet_manager);
3511+
if !bulk_signers.contains(&owner_signer) {
3512+
bulk_signers.push(owner_signer);
3513+
}
3514+
// Since account is required argument it will always be present
3515+
let token_account =
3516+
config.pubkey_or_default(arg_matches, "account", &mut wallet_manager);
3517+
command_required_transfer_memos(config, token_account, owner, bulk_signers, false).await
3518+
}
33593519
}
33603520
}
33613521

@@ -4546,4 +4706,60 @@ mod tests {
45464706
let account = config.rpc_client.get_account(&token_pubkey).await;
45474707
assert!(account.is_err());
45484708
}
4709+
4710+
#[tokio::test]
4711+
#[serial]
4712+
async fn required_transfer_memos() {
4713+
let (test_validator, payer) = new_validator_for_test().await;
4714+
let program_id = spl_token_2022::id();
4715+
let config = test_config(&test_validator, &payer, &program_id);
4716+
let token = create_token(&config, &payer).await;
4717+
let token_account = create_associated_account(&config, &payer, token).await;
4718+
let result = process_test_command(
4719+
&config,
4720+
&payer,
4721+
&[
4722+
"spl-token",
4723+
CommandName::EnableRequiredTransferMemos.into(),
4724+
&token_account.to_string(),
4725+
],
4726+
)
4727+
.await;
4728+
result.unwrap();
4729+
let extensions = StateWithExtensionsOwned::<Account>::unpack(
4730+
config
4731+
.rpc_client
4732+
.get_account(&token_account)
4733+
.await
4734+
.unwrap()
4735+
.data,
4736+
)
4737+
.unwrap();
4738+
let memo_transfer = extensions.get_extension::<MemoTransfer>().unwrap();
4739+
let enabled: bool = memo_transfer.require_incoming_transfer_memos.into();
4740+
assert!(enabled);
4741+
let result = process_test_command(
4742+
&config,
4743+
&payer,
4744+
&[
4745+
"spl-token",
4746+
CommandName::DisableRequiredTransferMemos.into(),
4747+
&token_account.to_string(),
4748+
],
4749+
)
4750+
.await;
4751+
result.unwrap();
4752+
let extensions = StateWithExtensionsOwned::<Account>::unpack(
4753+
config
4754+
.rpc_client
4755+
.get_account(&token_account)
4756+
.await
4757+
.unwrap()
4758+
.data,
4759+
)
4760+
.unwrap();
4761+
let memo_transfer = extensions.get_extension::<MemoTransfer>().unwrap();
4762+
let enabled: bool = memo_transfer.require_incoming_transfer_memos.into();
4763+
assert!(!enabled);
4764+
}
45494765
}

0 commit comments

Comments
 (0)