From 6af7307194dae25297931014d607e8c673d959bb Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Sun, 22 Oct 2023 18:11:15 -0700 Subject: [PATCH 1/8] add `deposit-confidential-tokens` command --- token/cli/src/main.rs | 195 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 3 deletions(-) diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index 58dbefdb546..a47bff25638 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -176,6 +176,7 @@ pub enum CommandName { DisableConfidentialCredits, EnableNonConfidentialCredits, DisableNonConfidentialCredits, + DepositConfidentialTokens, } impl fmt::Display for CommandName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -3071,6 +3072,108 @@ async fn command_enable_disable_confidential_transfers( }) } +async fn command_deposit_confidential_tokens( + config: &Config<'_>, + token_pubkey: Pubkey, + owner: Pubkey, + maybe_account: Option, + bulk_signers: BulkSigners, + ui_amount: Option, + mint_decimals: Option, +) -> CommandResult { + if config.sign_only { + panic!("Sign-only is not yet supported."); + } + + // check if mint decimals provided is consistent + let mint_info = config.get_mint_info(&token_pubkey, mint_decimals).await?; + + if !config.sign_only && mint_decimals.is_some() && mint_decimals != Some(mint_info.decimals) { + return Err(format!( + "Decimals {} was provided, but actual value is {}", + mint_decimals.unwrap(), + mint_info.decimals + ) + .into()); + } + + let decimals = if let Some(decimals) = mint_decimals { + decimals + } else { + mint_info.decimals + }; + + // derive ATA if account address not provided + let token_account_address = if let Some(account) = maybe_account { + account + } else { + let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; + token.get_associated_token_address(&owner) + }; + + let account = config.get_account_checked(&token_account_address).await?; + + let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; + let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; + + // the amount the user wants to deposit, as an f64 + let maybe_deposit_balance = + ui_amount.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals)); + + // the amount we will deposit, as a u64 + let deposit_balance = if !config.sign_only { + let current_balance = token + .get_account_info(&token_account_address) + .await? + .base + .amount; + let deposit_balance = maybe_deposit_balance.unwrap_or(current_balance); + + println_display( + config, + format!( + "Deposit {} tokens", + spl_token::amount_to_ui_amount(deposit_balance, mint_info.decimals), + ), + ); + + if deposit_balance > current_balance { + return Err(format!( + "Error: Insufficient funds, current balance is {}", + spl_token_2022::amount_to_ui_amount_string_trimmed( + current_balance, + mint_info.decimals + ) + ) + .into()); + } + + deposit_balance + } else { + maybe_deposit_balance.unwrap() + }; + + let res = token + .confidential_transfer_deposit( + &token_account_address, + &owner, + deposit_balance, + decimals, + &bulk_signers, + ) + .await?; + + let tx_return = finish_tx(config, &res, false).await?; + Ok(match tx_return { + TransactionReturnData::CliSignature(signature) => { + config.output_format.formatted_string(&signature) + } + TransactionReturnData::CliSignOnlyData(sign_only_data) => { + config.output_format.formatted_string(&sign_only_data) + } + }) +} + struct SignOnlyNeedsFullMintSpec {} impl offline::ArgsConfig for SignOnlyNeedsFullMintSpec { fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { @@ -4813,6 +4916,45 @@ fn app<'a, 'b>( .arg(multisig_signer_arg()) .nonce_args(true) ) + .subcommand( + SubCommand::with_name(CommandName::DepositConfidentialTokens.into()) + .about("Deposit amounts for confidential transfers") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("amount") + .validator(is_amount_or_all) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(2) + .required(true) + .help("Amount to deposit; accepts keyword ALL"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .conflicts_with("token") + .help("The address of the token account to configure confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + ) } #[tokio::main] @@ -5739,6 +5881,36 @@ async fn process_command<'a>( ) .await } + (CommandName::DepositConfidentialTokens, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let amount = match arg_matches.value_of("amount").unwrap() { + "ALL" => None, + amount => Some(amount.parse::().unwrap()), + }; + let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); + + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); + + command_deposit_confidential_tokens( + config, + token, + owner, + account, + bulk_signers, + amount, + mint_decimals, + ) + .await + } } } @@ -8279,7 +8451,7 @@ mod tests { let bulk_signers: Vec> = vec![Arc::new(clone_keypair(&payer)), Arc::new(token_keypair)]; let confidential_transfer_mint_authority = payer.pubkey(); - let auto_approve = true; + let auto_approve = false; command_create_token( &config, @@ -8325,7 +8497,7 @@ mod tests { // update confidential transfer mint settings let auditor_keypair = ElGamalKeypair::new_rand(); let auditor_pubkey: ElGamalPubkey = (*auditor_keypair.pubkey()).into(); - let new_auto_approve = false; + let new_auto_approve = true; command_update_confidential_transfer_settings( &config, @@ -8374,7 +8546,7 @@ mod tests { let extension = account_state .get_extension::() .unwrap(); - assert!(!bool::from(extension.approved)); + assert!(bool::from(extension.approved)); assert!(bool::from(extension.allow_confidential_credits)); assert!(bool::from(extension.allow_non_confidential_credits)); @@ -8456,6 +8628,23 @@ mod tests { .unwrap(); assert!(bool::from(extension.allow_non_confidential_credits)); + // deposit confidential tokens + let deposit_amount = 100.0; + mint_tokens(&config, &payer, token_pubkey, deposit_amount, token_account).await; + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::DepositConfidentialTokens.into(), + &token_pubkey.to_string(), + &deposit_amount.to_string(), + ], + ) + .await + .unwrap(); + // disable confidential transfers for mint process_test_command( &config, From 40d6f97ce2afc6b371b42d6ae778082f8fd11ec9 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Sun, 22 Oct 2023 18:11:49 -0700 Subject: [PATCH 2/8] add `withdraw-confidential-tokens` command --- token/cli/src/main.rs | 181 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 157 insertions(+), 24 deletions(-) diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index a47bff25638..150f3719e3c 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -177,6 +177,7 @@ pub enum CommandName { EnableNonConfidentialCredits, DisableNonConfidentialCredits, DepositConfidentialTokens, + WithdrawConfidentialTokens, } impl fmt::Display for CommandName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -3072,7 +3073,14 @@ async fn command_enable_disable_confidential_transfers( }) } -async fn command_deposit_confidential_tokens( +#[derive(PartialEq, Eq)] +enum ConfidentialInstructionType { + Deposit, + Withdraw, +} + +#[allow(clippy::too_many_arguments)] +async fn command_deposit_withdraw_confidential_tokens( config: &Config<'_>, token_pubkey: Pubkey, owner: Pubkey, @@ -3080,6 +3088,9 @@ async fn command_deposit_confidential_tokens( bulk_signers: BulkSigners, ui_amount: Option, mint_decimals: Option, + instruction_type: ConfidentialInstructionType, + elgamal_keypair: Option<&ElGamalKeypair>, + aes_key: Option<&AeKey>, ) -> CommandResult { if config.sign_only { panic!("Sign-only is not yet supported."); @@ -3116,28 +3127,28 @@ async fn command_deposit_confidential_tokens( let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - // the amount the user wants to deposit, as an f64 - let maybe_deposit_balance = + // the amount the user wants to deposit or withdraw, as an f64 + let maybe_amount = ui_amount.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals)); - // the amount we will deposit, as a u64 - let deposit_balance = if !config.sign_only { + // the amount we will deposit or withdraw, as a u64 + let amount = if !config.sign_only && instruction_type == ConfidentialInstructionType::Deposit { let current_balance = token .get_account_info(&token_account_address) .await? .base .amount; - let deposit_balance = maybe_deposit_balance.unwrap_or(current_balance); + let deposit_amount = maybe_amount.unwrap_or(current_balance); println_display( config, format!( - "Deposit {} tokens", - spl_token::amount_to_ui_amount(deposit_balance, mint_info.decimals), + "Depositing {} confidential tokens", + spl_token::amount_to_ui_amount(deposit_amount, mint_info.decimals), ), ); - if deposit_balance > current_balance { + if deposit_amount > current_balance { return Err(format!( "Error: Insufficient funds, current balance is {}", spl_token_2022::amount_to_ui_amount_string_trimmed( @@ -3148,20 +3159,59 @@ async fn command_deposit_confidential_tokens( .into()); } - deposit_balance + deposit_amount + } else if !config.sign_only && instruction_type == ConfidentialInstructionType::Withdraw { + // // TODO: expose account balance decryption in token + // let aes_key = aes_key.expect("AES key must be provided"); + // let current_balance = token + // .confidential_transfer_get_available_balance_with_key(&token_account_address, aes_key) + // .await?; + let withdraw_amount = + maybe_amount.expect("ALL keyword is not currently supported for withdraw"); + + println_display( + config, + format!( + "Withdrawing {} confidential tokens", + spl_token::amount_to_ui_amount(withdraw_amount, mint_info.decimals) + ), + ); + + withdraw_amount } else { - maybe_deposit_balance.unwrap() + maybe_amount.unwrap() }; - let res = token - .confidential_transfer_deposit( - &token_account_address, - &owner, - deposit_balance, - decimals, - &bulk_signers, - ) - .await?; + let res = if instruction_type == ConfidentialInstructionType::Deposit { + token + .confidential_transfer_deposit( + &token_account_address, + &owner, + amount, + decimals, + &bulk_signers, + ) + .await? + } else if instruction_type == ConfidentialInstructionType::Withdraw { + let elgamal_keypair = elgamal_keypair.expect("ElGamal keypair must be provided"); + let aes_key = aes_key.expect("AES key must be provided"); + + token + .confidential_transfer_withdraw( + &token_account_address, + &owner, + None, + amount, + decimals, + None, + elgamal_keypair, + aes_key, + &bulk_signers, + ) + .await? + } else { + panic!("Command not supported"); + }; let tx_return = finish_tx(config, &res, false).await?; Ok(match tx_return { @@ -4955,6 +5005,45 @@ fn app<'a, 'b>( .arg(mint_decimals_arg()) .nonce_args(true) ) + .subcommand( + SubCommand::with_name(CommandName::WithdrawConfidentialTokens.into()) + .about("Withdraw amounts for confidential transfers") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("amount") + .validator(is_amount_or_all) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(2) + .required(true) + .help("Amount to deposit; accepts keyword ALL"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .conflicts_with("token") + .help("The address of the token account to configure confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + ) } #[tokio::main] @@ -5881,7 +5970,8 @@ async fn process_command<'a>( ) .await } - (CommandName::DepositConfidentialTokens, arg_matches) => { + (c @ CommandName::DepositConfidentialTokens, arg_matches) + | (c @ CommandName::WithdrawConfidentialTokens, arg_matches) => { let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) .unwrap() .unwrap(); @@ -5894,13 +5984,36 @@ async fn process_command<'a>( let (owner_signer, owner) = config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); + + let (instruction_type, elgamal_keypair, aes_key) = match c { + CommandName::DepositConfidentialTokens => { + (ConfidentialInstructionType::Deposit, None, None) + } + CommandName::WithdrawConfidentialTokens => { + // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be + // supported in the future once upgrading to clap-v3. + // + // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be updated + // once custom ElGamal and AES keys are supported. + let elgamal_keypair = + ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); + let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); + + ( + ConfidentialInstructionType::Withdraw, + Some(elgamal_keypair), + Some(aes_key), + ) + } + _ => panic!("Instruction not supported"), + }; + if config.multisigner_pubkeys.is_empty() { push_signer_with_dedup(owner_signer, &mut bulk_signers); } - let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); - - command_deposit_confidential_tokens( + command_deposit_withdraw_confidential_tokens( config, token, owner, @@ -5908,6 +6021,9 @@ async fn process_command<'a>( bulk_signers, amount, mint_decimals, + instruction_type, + elgamal_keypair.as_ref(), + aes_key.as_ref(), ) .await } @@ -8645,6 +8761,23 @@ mod tests { .await .unwrap(); + // withdraw confidential tokens + let withdraw_amount = 100.0; + + // NOTE: this test will fail since we don't have `apply-pending-balance` + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::WithdrawConfidentialTokens.into(), + &token_pubkey.to_string(), + &withdraw_amount.to_string(), + ], + ) + .await + .unwrap(); + // disable confidential transfers for mint process_test_command( &config, From e8d72bf179536768769785b9d29bbe6fc1036879 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Sun, 22 Oct 2023 18:12:20 -0700 Subject: [PATCH 3/8] add `apply-pending-balance` command --- token/cli/src/main.rs | 139 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 6 deletions(-) diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index 150f3719e3c..3d7f99f95c0 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -178,6 +178,7 @@ pub enum CommandName { DisableNonConfidentialCredits, DepositConfidentialTokens, WithdrawConfidentialTokens, + ApplyPendingBalance, } impl fmt::Display for CommandName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -3224,6 +3225,57 @@ async fn command_deposit_withdraw_confidential_tokens( }) } +#[allow(clippy::too_many_arguments)] +async fn command_apply_pending_balance( + config: &Config<'_>, + maybe_token: Option, + owner: Pubkey, + maybe_account: Option, + bulk_signers: BulkSigners, + elgamal_keypair: &ElGamalKeypair, + aes_key: &AeKey, +) -> CommandResult { + if config.sign_only { + panic!("Sign-only is not yet supported."); + } + + // derive ATA if account address not provided + let token_account_address = if let Some(account) = maybe_account { + account + } else { + let token_pubkey = + maybe_token.expect("Either a valid token or account address must be provided"); + let token = token_client_from_config(config, &token_pubkey, None)?; + token.get_associated_token_address(&owner) + }; + + let account = config.get_account_checked(&token_account_address).await?; + + let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; + let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; + + let res = token + .confidential_transfer_apply_pending_balance( + &token_account_address, + &owner, + None, + elgamal_keypair.secret(), + aes_key, + &bulk_signers, + ) + .await?; + + let tx_return = finish_tx(config, &res, false).await?; + Ok(match tx_return { + TransactionReturnData::CliSignature(signature) => { + config.output_format.formatted_string(&signature) + } + TransactionReturnData::CliSignOnlyData(sign_only_data) => { + config.output_format.formatted_string(&sign_only_data) + } + }) +} + struct SignOnlyNeedsFullMintSpec {} impl offline::ArgsConfig for SignOnlyNeedsFullMintSpec { fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { @@ -5044,6 +5096,35 @@ fn app<'a, 'b>( .arg(mint_decimals_arg()) .nonce_args(true) ) + .subcommand( + SubCommand::with_name(CommandName::ApplyPendingBalance.into()) + .about("Collect confidential tokens from pending to available balance") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required_unless("address") + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .conflicts_with("token") + .help("The address of the token account to configure confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) } #[tokio::main] @@ -6027,6 +6108,37 @@ async fn process_command<'a>( ) .await } + (CommandName::ApplyPendingBalance, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); + + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + + let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); + + // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be + // supported in the future once upgrading to clap-v3. + // + // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be updated + // once custom ElGamal and AES keys are supported. + let elgamal_keypair = ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); + let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); + + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + command_apply_pending_balance( + config, + token, + owner, + account, + bulk_signers, + &elgamal_keypair, + &aes_key, + ) + .await + } } } @@ -8761,23 +8873,38 @@ mod tests { .await .unwrap(); - // withdraw confidential tokens - let withdraw_amount = 100.0; - - // NOTE: this test will fail since we don't have `apply-pending-balance` + // apply pending balance process_test_command( &config, &payer, &[ "spl-token", - CommandName::WithdrawConfidentialTokens.into(), + CommandName::ApplyPendingBalance.into(), &token_pubkey.to_string(), - &withdraw_amount.to_string(), ], ) .await .unwrap(); + // withdraw confidential tokens + // + // NOTE: the test fails due to transaction size limit :( + + // let withdraw_amount = 100.0; + // + // process_test_command( + // &config, + // &payer, + // &[ + // "spl-token", + // CommandName::WithdrawConfidentialTokens.into(), + // &token_pubkey.to_string(), + // &withdraw_amount.to_string(), + // ], + // ) + // .await + // .unwrap(); + // disable confidential transfers for mint process_test_command( &config, From 085f4955554589838d8924a6f1010d70e954f9b5 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Mon, 23 Oct 2023 13:17:31 -0700 Subject: [PATCH 4/8] add `--confidential` flag to `transfer` command --- token/cli/src/main.rs | 191 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 185 insertions(+), 6 deletions(-) diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index 3d7f99f95c0..e5188fc78e3 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -53,7 +53,10 @@ use spl_token_2022::{ }, instruction::*, solana_zk_token_sdk::{ - encryption::{auth_encryption::AeKey, elgamal::ElGamalKeypair}, + encryption::{ + auth_encryption::AeKey, + elgamal::{self, ElGamalKeypair}, + }, zk_token_elgamal::pod::ElGamalPubkey, }, state::{Account, AccountState, Mint}, @@ -1334,6 +1337,7 @@ async fn command_transfer( no_wait: bool, allow_non_system_account_recipient: bool, transfer_hook_accounts: Option>, + confidential_transfer_args: Option<&ConfidentialTransferArgs>, ) -> CommandResult { let mint_info = config.get_mint_info(&token_pubkey, mint_decimals).await?; @@ -1387,14 +1391,19 @@ async fn command_transfer( println_display( config, format!( - "Transfer {} tokens\n Sender: {}\n Recipient: {}", + "{}Transfer {} tokens\n Sender: {}\n Recipient: {}", + if confidential_transfer_args.is_some() { + "Confidential " + } else { + "" + }, spl_token::amount_to_ui_amount(transfer_balance, mint_info.decimals), sender, recipient ), ); - if transfer_balance > sender_balance { + if transfer_balance > sender_balance && confidential_transfer_args.is_none() { return Err(format!( "Error: Sender has insufficient funds, current balance is {}", spl_token_2022::amount_to_ui_amount_string_trimmed( @@ -1535,7 +1544,13 @@ async fn command_transfer( // and now we determine if we will actually fund it, based on its need and our willingness let fundable_owner = if needs_funding { - if fund_recipient { + if confidential_transfer_args.is_some() { + return Err( + "Error: Recipient's associated token account does not exist. \ + Accounts cannot be funded for confidential transfers." + .into(), + ); + } else if fund_recipient { println_display( config, format!(" Funding recipient: {}", recipient_token_account,), @@ -1561,6 +1576,70 @@ async fn command_transfer( token.with_memo(text, vec![config.default_signer()?.pubkey()]); } + // fetch confidential transfer info for recipient and auditor + let (recipient_elgamal_pubkey, auditor_elgamal_pubkey) = if let Some(args) = + confidential_transfer_args + { + if !config.sign_only { + let confidential_transfer_mint = config.get_account_checked(&token_pubkey).await?; + let mint_state = + StateWithExtensionsOwned::::unpack(confidential_transfer_mint.data) + .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; + + let auditor_elgamal_pubkey = if let Ok(confidential_transfer_mint) = + mint_state.get_extension::() + { + let expected_auditor_elgamal_pubkey = Option::::from( + confidential_transfer_mint.auditor_elgamal_pubkey, + ); + + // if auditor ElGamal pubkey is provided, check consistency with the one in the mint + // if auditor ElGamal pubkey is not provided, then use the expected one from the + // mint, which could also be `None` if auditing is disabled + if args.auditor_elgamal_pubkey.is_some() + && expected_auditor_elgamal_pubkey != args.auditor_elgamal_pubkey + { + return Err(format!( + "Mint {} has confidential transfer authority {}, but {} was provided", + token_pubkey, + expected_auditor_elgamal_pubkey + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "auditor disabled".to_string()), + args.auditor_elgamal_pubkey.unwrap(), + ) + .into()); + } + + expected_auditor_elgamal_pubkey + } else { + return Err(format!( + "Mint {} does not support confidential transfers", + token_pubkey + ) + .into()); + }; + + let recipient_account = config.get_account_checked(&recipient_token_account).await?; + let recipient_elgamal_pubkey = + StateWithExtensionsOwned::::unpack(recipient_account.data)? + .get_extension::()? + .elgamal_pubkey; + + (Some(recipient_elgamal_pubkey), auditor_elgamal_pubkey) + } else { + let recipient_elgamal_pubkey = args + .recipient_elgamal_pubkey + .expect("Recipient ElGamal pubkey must be provided"); + let auditor_elgamal_pubkey = args + .auditor_elgamal_pubkey + .expect("Auditor ElGamal pubkey must be provided"); + + (Some(recipient_elgamal_pubkey), Some(auditor_elgamal_pubkey)) + } + } else { + (None, None) + }; + // ...and, finally, the transfer let res = if let Some(recipient_owner) = fundable_owner { token @@ -1574,17 +1653,45 @@ async fn command_transfer( &bulk_signers, ) .await? - } else if let Some(fee) = maybe_fee { + } else if maybe_fee.is_some() && confidential_transfer_args.is_none() { token .transfer_with_fee( &sender, &recipient_token_account, &sender_owner, transfer_balance, - fee, + maybe_fee.unwrap(), &bulk_signers, ) .await? + } else if maybe_fee.is_none() && confidential_transfer_args.is_some() { + // deserialize `pod` ElGamal pubkeys + let recipient_elgamal_pubkey: elgamal::ElGamalPubkey = recipient_elgamal_pubkey + .unwrap() + .try_into() + .expect("Invalid recipient ElGamal pubkey"); + let auditor_elgamal_pubkey = auditor_elgamal_pubkey.map(|pubkey| { + let auditor_elgamal_pubkey: elgamal::ElGamalPubkey = + pubkey.try_into().expect("Invalid auditor ElGamal pubkey"); + auditor_elgamal_pubkey + }); + token + .confidential_transfer_transfer( + &sender, + &recipient_token_account, + &sender_owner, + None, + transfer_balance, + None, + &confidential_transfer_args.unwrap().sender_elgamal_keypair, + &confidential_transfer_args.unwrap().sender_aes_key, + &recipient_elgamal_pubkey, + auditor_elgamal_pubkey.as_ref(), + &bulk_signers, + ) + .await? + } else if maybe_fee.is_some() && confidential_transfer_args.is_some() { + panic!("Confidential transfer with fee is not yet supported."); } else { token .transfer( @@ -3326,6 +3433,13 @@ impl offline::ArgsConfig for SignOnlyNeedsTransferLamports { } } +struct ConfidentialTransferArgs { + sender_elgamal_keypair: ElGamalKeypair, + sender_aes_key: AeKey, + recipient_elgamal_pubkey: Option, + auditor_elgamal_pubkey: Option, +} + fn minimum_signers_help_string() -> String { format!( "The minimum number of signers required to allow the operation. [{} <= M <= N]", @@ -3924,6 +4038,7 @@ fn app<'a, 'b>( Arg::with_name("fund_recipient") .long("fund-recipient") .takes_value(false) + .conflicts_with("confidential") .help("Create the associated token account for the recipient if doesn't already exist") ) .arg( @@ -3975,6 +4090,14 @@ fn app<'a, 'b>( \"readonly\", \"writable\". \"readonly-signer\", or \"writable-signer\".\ Used for offline transaction creation and signing.") ) + .arg( + Arg::with_name("confidential") + .long("confidential") + .takes_value(false) + .conflicts_with("fund_recipient") + .help("Send tokens confidentially. Both sender and recipient accounts must \ + be pre-configured for confidential transfers.") + ) .arg(multisig_signer_arg()) .arg(mint_decimals_arg()) .nonce_args(true) @@ -5415,6 +5538,29 @@ async fn process_command<'a>( let (owner_signer, owner) = config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + + let confidential_transfer_args = if arg_matches.is_present("confidential") { + // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be + // supported in the future once upgrading to clap-v3. + // + // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be updated + // once custom ElGamal and AES keys are supported. + let sender_elgamal_keypair = + ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); + let sender_aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); + + // Sign-only mode is not yet supported for confidential transfers, so set + // recipient and auditor ElGamal public to `None` by default. + Some(ConfidentialTransferArgs { + sender_elgamal_keypair, + sender_aes_key, + recipient_elgamal_pubkey: None, + auditor_elgamal_pubkey: None, + }) + } else { + None + }; + if config.multisigner_pubkeys.is_empty() { push_signer_with_dedup(owner_signer, &mut bulk_signers); } @@ -5457,6 +5603,7 @@ async fn process_command<'a>( arg_matches.is_present("no_wait"), arg_matches.is_present("allow_non_system_account_recipient"), transfer_hook_accounts, + confidential_transfer_args.as_ref(), ) .await } @@ -8886,6 +9033,38 @@ mod tests { .await .unwrap(); + // confidential transfer + let destination_account = create_auxiliary_account(&config, &payer, token_pubkey).await; + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::ConfigureConfidentialTransferAccount.into(), + "--address", + &destination_account.to_string(), + ], + ) + .await + .unwrap(); // configure destination account for confidential transfers first + + // NOTE: the test fails due to transaction size limit + // let transfer_amount = 100.0; + // process_test_command( + // &config, + // &payer, + // &[ + // "spl-token", + // CommandName::Transfer.into(), + // &token_pubkey.to_string(), + // &transfer_amount.to_string(), + // &destination_account.to_string(), + // "--confidential", + // ], + // ) + // .await + // .unwrap(); + // withdraw confidential tokens // // NOTE: the test fails due to transaction size limit :( From c7a0afa64c345cf73b29f75c9638b9307139ebd4 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 24 Oct 2023 18:32:41 -0700 Subject: [PATCH 5/8] remove unnecessary fetch of account data --- token/cli/src/main.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index e5188fc78e3..3b15117fb17 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -38,7 +38,10 @@ use solana_sdk::{ use spl_associated_token_account::get_associated_token_address_with_program_id; use spl_token_2022::{ extension::{ - confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, + confidential_transfer::{ + account_info::WithdrawAccountInfo, ConfidentialTransferAccount, + ConfidentialTransferMint, + }, confidential_transfer_fee::ConfidentialTransferFeeConfig, cpi_guard::CpiGuard, default_account_state::DefaultAccountState, @@ -1581,6 +1584,9 @@ async fn command_transfer( confidential_transfer_args { if !config.sign_only { + // we can use the mint data from the start of the function, but will require + // non-trivial amount of refactoring the code due to ownership; for now, we fetch the mint + // a second time. This can potentially be optimized in the future. let confidential_transfer_mint = config.get_account_checked(&token_pubkey).await?; let mint_state = StateWithExtensionsOwned::::unpack(confidential_transfer_mint.data) @@ -3241,11 +3247,7 @@ async fn command_deposit_withdraw_confidential_tokens( // the amount we will deposit or withdraw, as a u64 let amount = if !config.sign_only && instruction_type == ConfidentialInstructionType::Deposit { - let current_balance = token - .get_account_info(&token_account_address) - .await? - .base - .amount; + let current_balance = state_with_extension.base.amount; let deposit_amount = maybe_amount.unwrap_or(current_balance); println_display( @@ -3304,6 +3306,10 @@ async fn command_deposit_withdraw_confidential_tokens( let elgamal_keypair = elgamal_keypair.expect("ElGamal keypair must be provided"); let aes_key = aes_key.expect("AES key must be provided"); + let extension_state = + state_with_extension.get_extension::()?; + let withdraw_account_info = WithdrawAccountInfo::new(extension_state); + token .confidential_transfer_withdraw( &token_account_address, @@ -3311,7 +3317,7 @@ async fn command_deposit_withdraw_confidential_tokens( None, amount, decimals, - None, + Some(withdraw_account_info), elgamal_keypair, aes_key, &bulk_signers, From 9c9b073eeb55c7b214eafb4ed2d8ac533548e15f Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 24 Oct 2023 18:44:08 -0700 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Jon Cinque --- token/cli/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index 3b15117fb17..843b9a656f5 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -1606,11 +1606,11 @@ async fn command_transfer( && expected_auditor_elgamal_pubkey != args.auditor_elgamal_pubkey { return Err(format!( - "Mint {} has confidential transfer authority {}, but {} was provided", + "Mint {} has confidential transfer auditor {}, but {} was provided", token_pubkey, expected_auditor_elgamal_pubkey .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "auditor disabled".to_string()), + .unwrap_or_else(|| "disabled".to_string()), args.auditor_elgamal_pubkey.unwrap(), ) .into()); From 2e979a5c9917588039e708b27ed4031b90e5170f Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 24 Oct 2023 18:49:01 -0700 Subject: [PATCH 7/8] remove unnecessary fetch of account data for apply pending balance --- token/cli/src/main.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index 843b9a656f5..ff1749b7530 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -39,8 +39,8 @@ use spl_associated_token_account::get_associated_token_address_with_program_id; use spl_token_2022::{ extension::{ confidential_transfer::{ - account_info::WithdrawAccountInfo, ConfidentialTransferAccount, - ConfidentialTransferMint, + account_info::{ApplyPendingBalanceAccountInfo, WithdrawAccountInfo}, + ConfidentialTransferAccount, ConfidentialTransferMint, }, confidential_transfer_fee::ConfidentialTransferFeeConfig, cpi_guard::CpiGuard, @@ -3367,11 +3367,14 @@ async fn command_apply_pending_balance( let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; + let extension_state = state_with_extension.get_extension::()?; + let account_info = ApplyPendingBalanceAccountInfo::new(extension_state); + let res = token .confidential_transfer_apply_pending_balance( &token_account_address, &owner, - None, + Some(account_info), elgamal_keypair.secret(), aes_key, &bulk_signers, From 9ac5489508e80da798c94ee6bcbf03b3c5ccf0ee Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 24 Oct 2023 19:08:38 -0700 Subject: [PATCH 8/8] use match statements for `command_transfer` --- token/cli/src/main.rs | 194 ++++++++++++++++++++++-------------------- 1 file changed, 102 insertions(+), 92 deletions(-) diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index ff1749b7530..96944aa2b92 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -1647,67 +1647,76 @@ async fn command_transfer( }; // ...and, finally, the transfer - let res = if let Some(recipient_owner) = fundable_owner { - token - .create_recipient_associated_account_and_transfer( - &sender, - &recipient_token_account, - &recipient_owner, - &sender_owner, - transfer_balance, - maybe_fee, - &bulk_signers, - ) - .await? - } else if maybe_fee.is_some() && confidential_transfer_args.is_none() { - token - .transfer_with_fee( - &sender, - &recipient_token_account, - &sender_owner, - transfer_balance, - maybe_fee.unwrap(), - &bulk_signers, - ) - .await? - } else if maybe_fee.is_none() && confidential_transfer_args.is_some() { - // deserialize `pod` ElGamal pubkeys - let recipient_elgamal_pubkey: elgamal::ElGamalPubkey = recipient_elgamal_pubkey - .unwrap() - .try_into() - .expect("Invalid recipient ElGamal pubkey"); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.map(|pubkey| { - let auditor_elgamal_pubkey: elgamal::ElGamalPubkey = - pubkey.try_into().expect("Invalid auditor ElGamal pubkey"); - auditor_elgamal_pubkey - }); - token - .confidential_transfer_transfer( - &sender, - &recipient_token_account, - &sender_owner, - None, - transfer_balance, - None, - &confidential_transfer_args.unwrap().sender_elgamal_keypair, - &confidential_transfer_args.unwrap().sender_aes_key, - &recipient_elgamal_pubkey, - auditor_elgamal_pubkey.as_ref(), - &bulk_signers, - ) - .await? - } else if maybe_fee.is_some() && confidential_transfer_args.is_some() { - panic!("Confidential transfer with fee is not yet supported."); - } else { - token - .transfer( - &sender, - &recipient_token_account, - &sender_owner, - transfer_balance, - &bulk_signers, - ) - .await? + let res = match (fundable_owner, maybe_fee, confidential_transfer_args) { + (Some(recipient_owner), None, None) => { + token + .create_recipient_associated_account_and_transfer( + &sender, + &recipient_token_account, + &recipient_owner, + &sender_owner, + transfer_balance, + maybe_fee, + &bulk_signers, + ) + .await? + } + (Some(_), _, _) => { + panic!("Recipient account cannot be created for transfer with fees or confidential transfers"); + } + (None, Some(fee), None) => { + token + .transfer_with_fee( + &sender, + &recipient_token_account, + &sender_owner, + transfer_balance, + fee, + &bulk_signers, + ) + .await? + } + (None, None, Some(args)) => { + // deserialize `pod` ElGamal pubkeys + let recipient_elgamal_pubkey: elgamal::ElGamalPubkey = recipient_elgamal_pubkey + .unwrap() + .try_into() + .expect("Invalid recipient ElGamal pubkey"); + let auditor_elgamal_pubkey = auditor_elgamal_pubkey.map(|pubkey| { + let auditor_elgamal_pubkey: elgamal::ElGamalPubkey = + pubkey.try_into().expect("Invalid auditor ElGamal pubkey"); + auditor_elgamal_pubkey + }); + token + .confidential_transfer_transfer( + &sender, + &recipient_token_account, + &sender_owner, + None, + transfer_balance, + None, + &args.sender_elgamal_keypair, + &args.sender_aes_key, + &recipient_elgamal_pubkey, + auditor_elgamal_pubkey.as_ref(), + &bulk_signers, + ) + .await? + } + (None, Some(_), Some(_)) => { + panic!("Confidential transfer with fee is not yet supported."); + } + (None, None, None) => { + token + .transfer( + &sender, + &recipient_token_account, + &sender_owner, + transfer_balance, + &bulk_signers, + ) + .await? + } }; let tx_return = finish_tx(config, &res, no_wait).await?; @@ -3292,39 +3301,40 @@ async fn command_deposit_withdraw_confidential_tokens( maybe_amount.unwrap() }; - let res = if instruction_type == ConfidentialInstructionType::Deposit { - token - .confidential_transfer_deposit( - &token_account_address, - &owner, - amount, - decimals, - &bulk_signers, - ) - .await? - } else if instruction_type == ConfidentialInstructionType::Withdraw { - let elgamal_keypair = elgamal_keypair.expect("ElGamal keypair must be provided"); - let aes_key = aes_key.expect("AES key must be provided"); + let res = match instruction_type { + ConfidentialInstructionType::Deposit => { + token + .confidential_transfer_deposit( + &token_account_address, + &owner, + amount, + decimals, + &bulk_signers, + ) + .await? + } + ConfidentialInstructionType::Withdraw => { + let elgamal_keypair = elgamal_keypair.expect("ElGamal keypair must be provided"); + let aes_key = aes_key.expect("AES key must be provided"); - let extension_state = - state_with_extension.get_extension::()?; - let withdraw_account_info = WithdrawAccountInfo::new(extension_state); + let extension_state = + state_with_extension.get_extension::()?; + let withdraw_account_info = WithdrawAccountInfo::new(extension_state); - token - .confidential_transfer_withdraw( - &token_account_address, - &owner, - None, - amount, - decimals, - Some(withdraw_account_info), - elgamal_keypair, - aes_key, - &bulk_signers, - ) - .await? - } else { - panic!("Command not supported"); + token + .confidential_transfer_withdraw( + &token_account_address, + &owner, + None, + amount, + decimals, + Some(withdraw_account_info), + elgamal_keypair, + aes_key, + &bulk_signers, + ) + .await? + } }; let tx_return = finish_tx(config, &res, false).await?;