From 3dff5c9dee44f0e824484d6c2ff24f7911a6d11c Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Mon, 22 Mar 2021 11:10:44 -0700 Subject: [PATCH] transfer now requires --allow-unfunded-recipient if the recipient doesn't exist --- cli/src/cli.rs | 37 ++++++++++++++++++- cli/tests/nonce.rs | 2 ++ cli/tests/transfer.rs | 59 +++++++++++++++++++++++++++++++ cli/tests/vote.rs | 1 + docs/src/cli/transfer-tokens.md | 4 +-- docs/src/integrations/exchange.md | 4 +-- multinode-demo/delegate-stake.sh | 4 ++- multinode-demo/validator.sh | 4 ++- 8 files changed, 108 insertions(+), 7 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 18ab314fc..8ec777c7f 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -360,6 +360,7 @@ pub enum CliCommand { from: SignerIndex, sign_only: bool, dump_transaction_message: bool, + allow_unfunded_recipient: bool, no_wait: bool, blockhash_query: BlockhashQuery, nonce_account: Option, @@ -865,6 +866,7 @@ pub fn parse_command( let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; let (from, from_pubkey) = signer_of(matches, "from", wallet_manager)?; + let allow_unfunded_recipient = matches.is_present("allow_unfunded_recipient"); let mut bulk_signers = vec![fee_payer, from]; if nonce_account.is_some() { @@ -886,6 +888,7 @@ pub fn parse_command( to, sign_only, dump_transaction_message, + allow_unfunded_recipient, no_wait, blockhash_query, nonce_account, @@ -1139,6 +1142,7 @@ fn process_transfer( from: SignerIndex, sign_only: bool, dump_transaction_message: bool, + allow_unfunded_recipient: bool, no_wait: bool, blockhash_query: &BlockhashQuery, nonce_account: Option<&Pubkey>, @@ -1153,6 +1157,21 @@ fn process_transfer( let (recent_blockhash, fee_calculator) = blockhash_query.get_blockhash_and_fee_calculator(rpc_client, config.commitment)?; + if !allow_unfunded_recipient { + let recipient_balance = rpc_client + .get_balance_with_commitment(to, config.commitment)? + .value; + if recipient_balance == 0 { + return Err(format!( + "The recipient address ({}) is not funded. \ + Add `--allow-unfunded-recipient` to complete the transfer \ + ", + to + ) + .into()); + } + } + let nonce_authority = config.signers[nonce_authority]; let fee_payer = config.signers[fee_payer]; @@ -1822,6 +1841,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { from, sign_only, dump_transaction_message, + allow_unfunded_recipient, no_wait, ref blockhash_query, ref nonce_account, @@ -1837,6 +1857,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { *from, *sign_only, *dump_transaction_message, + *allow_unfunded_recipient, *no_wait, blockhash_query, nonce_account.as_ref(), @@ -2205,6 +2226,12 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .requires("derived_address_seed") .hidden(true) ) + .arg( + Arg::with_name("allow_unfunded_recipient") + .long("allow-unfunded-recipient") + .takes_value(false) + .help("Complete the transfer even if the recipient address is not funded") + ) .offline_args() .nonce_args(false) .arg(fee_payer_arg()), @@ -2908,6 +2935,7 @@ mod tests { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: false, no_wait: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -2933,6 +2961,7 @@ mod tests { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: false, no_wait: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -2945,11 +2974,12 @@ mod tests { } ); - // Test Transfer no-wait + // Test Transfer no-wait and --allow-unfunded-recipient let test_transfer = test_commands.clone().get_matches_from(vec![ "test", "transfer", "--no-wait", + "--allow-unfunded-recipient", &to_string, "42", ]); @@ -2962,6 +2992,7 @@ mod tests { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: true, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -2995,6 +3026,7 @@ mod tests { from: 0, sign_only: true, dump_transaction_message: false, + allow_unfunded_recipient: false, no_wait: false, blockhash_query: BlockhashQuery::None(blockhash), nonce_account: None, @@ -3033,6 +3065,7 @@ mod tests { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: false, no_wait: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::Cluster, @@ -3075,6 +3108,7 @@ mod tests { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: false, no_wait: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_address), @@ -3115,6 +3149,7 @@ mod tests { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: false, no_wait: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, diff --git a/cli/tests/nonce.rs b/cli/tests/nonce.rs index 6e51de405..3b537bcdf 100644 --- a/cli/tests/nonce.rs +++ b/cli/tests/nonce.rs @@ -294,6 +294,7 @@ fn test_create_account_with_seed() { from: 0, sign_only: true, dump_transaction_message: true, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::None(nonce_hash), nonce_account: Some(nonce_address), @@ -318,6 +319,7 @@ fn test_create_account_with_seed() { from: 0, sign_only: false, dump_transaction_message: true, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_address), diff --git a/cli/tests/transfer.rs b/cli/tests/transfer.rs index 6184bf83a..da5311b80 100644 --- a/cli/tests/transfer.rs +++ b/cli/tests/transfer.rs @@ -52,6 +52,7 @@ fn test_transfer() { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -71,6 +72,7 @@ fn test_transfer() { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -102,6 +104,7 @@ fn test_transfer() { from: 0, sign_only: true, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::None(blockhash), nonce_account: None, @@ -122,6 +125,7 @@ fn test_transfer() { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash), nonce_account: None, @@ -167,6 +171,7 @@ fn test_transfer() { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account.pubkey()), @@ -219,6 +224,7 @@ fn test_transfer() { from: 0, sign_only: true, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::None(nonce_hash), nonce_account: Some(nonce_account.pubkey()), @@ -238,6 +244,7 @@ fn test_transfer() { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account.pubkey()), @@ -307,6 +314,7 @@ fn test_transfer_multisession_signing() { from: 1, sign_only: true, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::None(blockhash), nonce_account: None, @@ -336,6 +344,7 @@ fn test_transfer_multisession_signing() { from: 1, sign_only: true, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::None(blockhash), nonce_account: None, @@ -362,6 +371,7 @@ fn test_transfer_multisession_signing() { from: 1, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash), nonce_account: None, @@ -410,6 +420,7 @@ fn test_transfer_all() { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -423,6 +434,53 @@ fn test_transfer_all() { check_recent_balance(49_999, &rpc_client, &recipient_pubkey); } +#[test] +fn test_transfer_unfunded_recipient() { + solana_logger::setup(); + let mint_keypair = Keypair::new(); + let test_validator = TestValidator::with_custom_fees(mint_keypair.pubkey(), 1); + let faucet_addr = run_local_faucet(mint_keypair, None); + + let rpc_client = + RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed()); + + let default_signer = Keypair::new(); + + let mut config = CliConfig::recent_for_tests(); + config.json_rpc_url = test_validator.rpc_url(); + config.signers = vec![&default_signer]; + + let sender_pubkey = config.signers[0].pubkey(); + let recipient_pubkey = Pubkey::new(&[1u8; 32]); + + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000, &config) + .unwrap(); + check_recent_balance(50_000, &rpc_client, &sender_pubkey); + check_recent_balance(0, &rpc_client, &recipient_pubkey); + + check_ready(&rpc_client); + + // Plain ole transfer + config.command = CliCommand::Transfer { + amount: SpendAmount::All, + to: recipient_pubkey, + from: 0, + sign_only: false, + dump_transaction_message: false, + allow_unfunded_recipient: false, + no_wait: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + derived_address_seed: None, + derived_address_program_id: None, + }; + + // Expect failure due to unfunded recipient and the lack of the `allow_unfunded_recipient` flag + process_command(&config).unwrap_err(); +} + #[test] fn test_transfer_with_seed() { solana_logger::setup(); @@ -466,6 +524,7 @@ fn test_transfer_with_seed() { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, diff --git a/cli/tests/vote.rs b/cli/tests/vote.rs index 8c1d82e18..fe95ecba4 100644 --- a/cli/tests/vote.rs +++ b/cli/tests/vote.rs @@ -72,6 +72,7 @@ fn test_vote_authorize_and_withdraw() { from: 0, sign_only: false, dump_transaction_message: false, + allow_unfunded_recipient: true, no_wait: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, diff --git a/docs/src/cli/transfer-tokens.md b/docs/src/cli/transfer-tokens.md index f2f5e3a4e..b5e580be6 100644 --- a/docs/src/cli/transfer-tokens.md +++ b/docs/src/cli/transfer-tokens.md @@ -71,7 +71,7 @@ with the private keypair corresponding to the sender's public key in the transaction. ```bash -solana transfer --from 5 --url https://devnet.solana.com --fee-payer +solana transfer --from 5 --allow-unfunded-recipient --url https://devnet.solana.com --fee-payer ``` where you replace `` with the path to a keypair in your first wallet, @@ -118,7 +118,7 @@ Save this seed phrase to recover your new keypair: clump panic cousin hurt coast charge engage fall eager urge win love # If this was a real wallet, never share these words on the internet like this! ==================================================================== -$ solana transfer --from my_solana_wallet.json 7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv 5 --url https://devnet.solana.com --fee-payer my_solana_wallet.json # Transferring tokens to the public address of the paper wallet +$ solana transfer --from my_solana_wallet.json 7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv 5 --allow-unfunded-recipient --url https://devnet.solana.com --fee-payer my_solana_wallet.json # Transferring tokens to the public address of the paper wallet 3gmXvykAd1nCQQ7MjosaHLf69Xyaqyq1qw2eu1mgPyYXd5G4v1rihhg1CiRw35b9fHzcftGKKEu4mbUeXY2pEX2z # This is the transaction signature $ solana balance DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK --url https://devnet.solana.com diff --git a/docs/src/integrations/exchange.md b/docs/src/integrations/exchange.md index 92ae18c76..9689e08d5 100644 --- a/docs/src/integrations/exchange.md +++ b/docs/src/integrations/exchange.md @@ -388,7 +388,7 @@ will wait and track progress on stderr until the transaction has been finalized by the cluster. If the transaction fails, it will report any transaction errors. ```bash -solana transfer --keypair --url http://localhost:8899 +solana transfer --allow-unfunded-recipient --keypair --url http://localhost:8899 ``` The [Solana Javascript SDK](https://github.com/solana-labs/solana-web3.js) @@ -420,7 +420,7 @@ In the command-line tool, pass the `--no-wait` argument to send a transfer asynchronously, and include your recent blockhash with the `--blockhash` argument: ```bash -solana transfer --no-wait --blockhash --keypair --url http://localhost:8899 +solana transfer --no-wait --allow-unfunded-recipient --blockhash --keypair --url http://localhost:8899 ``` You can also build, sign, and serialize the transaction manually, and fire it off to diff --git a/multinode-demo/delegate-stake.sh b/multinode-demo/delegate-stake.sh index 336a48b6f..fb24c0ca3 100755 --- a/multinode-demo/delegate-stake.sh +++ b/multinode-demo/delegate-stake.sh @@ -102,7 +102,9 @@ if ((airdrops_enabled)); then echo "--keypair argument must be provided" exit 1 fi - $solana_cli "${common_args[@]}" --keypair "$SOLANA_CONFIG_DIR/faucet.json" transfer "$keypair" "$stake_sol" + $solana_cli \ + "${common_args[@]}" --keypair "$SOLANA_CONFIG_DIR/faucet.json" \ + transfer --allow-unfunded-recipient "$keypair" "$stake_sol" fi if [[ -n $keypair ]]; then diff --git a/multinode-demo/validator.sh b/multinode-demo/validator.sh index b58485be5..bfecdee4e 100755 --- a/multinode-demo/validator.sh +++ b/multinode-demo/validator.sh @@ -274,7 +274,9 @@ setup_validator_accounts() { echo "Adding $node_sol to validator identity account:" ( set -x - $solana_cli --keypair "$SOLANA_CONFIG_DIR/faucet.json" --url "$rpc_url" transfer "$identity" "$node_sol" + $solana_cli \ + --keypair "$SOLANA_CONFIG_DIR/faucet.json" --url "$rpc_url" \ + transfer --allow-unfunded-recipient "$identity" "$node_sol" ) || return $? fi