diff --git a/test/functional/test_framework/wallet_rpc_controller.py b/test/functional/test_framework/wallet_rpc_controller.py index 136c4f410a..45bde52bc2 100644 --- a/test/functional/test_framework/wallet_rpc_controller.py +++ b/test/functional/test_framework/wallet_rpc_controller.py @@ -245,7 +245,7 @@ async def create_stake_pool(self, return "The transaction was submitted successfully" async def decommission_stake_pool(self, pool_id: str) -> str: - self._write_command("staking_decommission_pool", [self.account, pool_id, {'in_top_x_mb': 5}])['result'] + self._write_command("staking_decommission_pool", [self.account, pool_id, None, {'in_top_x_mb': 5}])['result'] return "The transaction was submitted successfully" async def list_pool_ids(self) -> List[PoolData]: diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 925b3f7f12..4d84d6f7c5 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -536,8 +536,18 @@ impl Account { db_tx: &mut impl WalletStorageWriteUnlocked, pool_id: PoolId, pool_balance: Amount, + output_address: Option, current_fee_rate: FeeRate, ) -> WalletResult { + let output_destination = if let Some(dest) = output_address { + dest + } else { + self.get_new_address(db_tx, KeyPurpose::ReceiveFunds)? + .1 + .decode_object(&self.chain_config) + .expect("already checked") + }; + let pool_data = self.output_cache.pool_data(pool_id)?; let best_block_height = self.best_block().1; let tx_input = TxInput::Utxo(pool_data.utxo_outpoint.clone()); @@ -545,7 +555,7 @@ impl Account { let network_fee: Amount = { let output = make_decommission_stake_pool_output( self.chain_config.as_ref(), - pool_data.decommission_key.clone(), + output_destination.clone(), pool_balance, best_block_height, )?; @@ -563,7 +573,7 @@ impl Account { let output = make_decommission_stake_pool_output( self.chain_config.as_ref(), - pool_data.decommission_key.clone(), + output_destination, (pool_balance - network_fee) .ok_or(WalletError::NotEnoughUtxo(network_fee, pool_balance))?, best_block_height, @@ -580,10 +590,16 @@ impl Account { db_tx: &mut impl WalletStorageWriteUnlocked, pool_id: PoolId, pool_balance: Amount, + output_address: Option, current_fee_rate: FeeRate, ) -> WalletResult { - let result = - self.decommission_stake_pool_impl(db_tx, pool_id, pool_balance, current_fee_rate)?; + let result = self.decommission_stake_pool_impl( + db_tx, + pool_id, + pool_balance, + output_address, + current_fee_rate, + )?; result .into_signed_tx() .map_err(|_| WalletError::PartiallySignedTransactionInDecommissionCommand) @@ -594,10 +610,16 @@ impl Account { db_tx: &mut impl WalletStorageWriteUnlocked, pool_id: PoolId, pool_balance: Amount, + output_address: Option, current_fee_rate: FeeRate, ) -> WalletResult { - let result = - self.decommission_stake_pool_impl(db_tx, pool_id, pool_balance, current_fee_rate)?; + let result = self.decommission_stake_pool_impl( + db_tx, + pool_id, + pool_balance, + output_address, + current_fee_rate, + )?; if result.is_fully_signed() { return Err(WalletError::FullySignedTransactionInDecommissionReq); } diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 67194c2e1f..31c0259a3d 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -1329,10 +1329,17 @@ impl Wallet { account_index: U31, pool_id: PoolId, pool_balance: Amount, + output_address: Option, current_fee_rate: FeeRate, ) -> WalletResult { self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.decommission_stake_pool(db_tx, pool_id, pool_balance, current_fee_rate) + account.decommission_stake_pool( + db_tx, + pool_id, + pool_balance, + output_address, + current_fee_rate, + ) }) } @@ -1341,10 +1348,17 @@ impl Wallet { account_index: U31, pool_id: PoolId, pool_balance: Amount, + output_address: Option, current_fee_rate: FeeRate, ) -> WalletResult { self.for_account_rw_unlocked(account_index, |account, db_tx, _| { - account.decommission_stake_pool_request(db_tx, pool_id, pool_balance, current_fee_rate) + account.decommission_stake_pool_request( + db_tx, + pool_id, + pool_balance, + output_address, + current_fee_rate, + ) }) } diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index ba22fedcbb..c718442d68 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -1403,6 +1403,7 @@ fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { DEFAULT_ACCOUNT_INDEX, *pool_id, pool_amount, + None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) .unwrap(); @@ -3698,6 +3699,7 @@ fn decommission_pool_wrong_account(#[case] seed: Seed) { acc_0_index, pool_id, pool_amount, + None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ); assert_eq!( @@ -3711,6 +3713,7 @@ fn decommission_pool_wrong_account(#[case] seed: Seed) { acc_1_index, pool_id, pool_amount, + None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) .unwrap(); @@ -3799,6 +3802,7 @@ fn decommission_pool_request_wrong_account(#[case] seed: Seed) { acc_1_index, pool_id, pool_amount, + None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ); assert_eq!( @@ -3811,6 +3815,7 @@ fn decommission_pool_request_wrong_account(#[case] seed: Seed) { acc_0_index, pool_id, pool_amount, + None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) .unwrap(); @@ -3883,6 +3888,8 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { 1, ); + assert_eq!(get_coin_balance(&wallet), Amount::ZERO); + let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); @@ -3892,6 +3899,7 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { acc_0_index, pool_id, pool_amount, + None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) .unwrap(); @@ -3915,20 +3923,11 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { .into_signed_tx() .unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![signed_tx], Amount::ZERO, 1); + let _ = create_block(&chain_config, &mut wallet, vec![signed_tx], Amount::ZERO, 2); - let currency_balances = wallet - .get_balance( - acc_1_index, - UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::CreateStakePool, - UtxoState::Confirmed.into(), - WithLocked::Unlocked, - ) - .unwrap(); - assert_eq!( - currency_balances.get(&Currency::Coin).copied().unwrap_or(Amount::ZERO), - pool_amount, - ); + // the pool amount is back after decommission + assert_eq!(get_coin_balance(&wallet), pool_amount); + assert_eq!(get_coin_balance_for_acc(&wallet, acc_1_index), Amount::ZERO); } #[rstest] @@ -3994,6 +3993,7 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { DEFAULT_ACCOUNT_INDEX, pool_id, pool_amount, + None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) .unwrap(); @@ -4013,7 +4013,7 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { &mut hot_wallet, vec![signed_tx], Amount::ZERO, - 1, + 2, ); let currency_balances = hot_wallet diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index 96934df04a..6dc7cb25d7 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -478,6 +478,9 @@ pub enum WalletCommand { /// The pool id of the pool to be decommissioned. /// Notice that this only works if the selected account in this wallet owns the decommission key. pool_id: String, + /// The address that will be receiving the staker's balance (both pledge and proceeds from staking). + /// If not specified, a new receiving address will be created by this wallet's selected account + output_address: Option, }, /// Create a request to decommission a pool. This assumes that the decommission key is owned @@ -488,6 +491,9 @@ pub enum WalletCommand { DecommissionStakePoolRequest { /// The pool id of the pool to be decommissioned. pool_id: String, + /// The address that will be receiving the staker's balance (both pledge and proceeds from staking). + /// If not specified, a new receiving address will be created by this wallet's selected account + output_address: Option, }, /// Rescan the blockchain and re-detect all operations related to the selected account in this wallet @@ -1529,20 +1535,31 @@ where Ok(Self::new_tx_submitted_command(new_tx)) } - WalletCommand::DecommissionStakePool { pool_id } => { + WalletCommand::DecommissionStakePool { + pool_id, + output_address, + } => { let selected_account = self.get_selected_acc()?; let new_tx = self .wallet_rpc - .decommission_stake_pool(selected_account, pool_id, self.config) + .decommission_stake_pool(selected_account, pool_id, output_address, self.config) .await?; Ok(Self::new_tx_submitted_command(new_tx)) } - WalletCommand::DecommissionStakePoolRequest { pool_id } => { + WalletCommand::DecommissionStakePoolRequest { + pool_id, + output_address, + } => { let selected_account = self.get_selected_acc()?; let result = self .wallet_rpc - .decommission_stake_pool_request(selected_account, pool_id, self.config) + .decommission_stake_pool_request( + selected_account, + pool_id, + output_address, + self.config, + ) .await?; let result_hex: HexEncoded = result.into(); diff --git a/wallet/wallet-cli-lib/tests/basic.rs b/wallet/wallet-cli-lib/tests/basic.rs index 0c08163daf..46ab9c374b 100644 --- a/wallet/wallet-cli-lib/tests/basic.rs +++ b/wallet/wallet-cli-lib/tests/basic.rs @@ -15,6 +15,9 @@ mod cli_test_framework; +use crypto::random::Rng; + +use common::{address::Address, chain::PoolId, primitives::H256}; use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; @@ -82,3 +85,94 @@ async fn produce_blocks(#[case] seed: Seed) { test.shutdown().await; } + +#[rstest] +#[case(test_utils::random::Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn produce_blocks_decommission_genesis_pool(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let test = CliTestFramework::setup(&mut rng).await; + + test.create_genesis_wallet(); + + assert_eq!(test.exec("account-balance"), "Coins amount: 99960000"); + + // create a new pool + let address = test.exec("address-new"); + assert!(test + .exec(&format!( + "staking-create-pool 40000 {} 0.{} {}", + rng.gen_range(0..100), + rng.gen_range(1..100), + address, + ),) + .starts_with("The transaction was submitted successfully with ID")); + // create some blocks + assert_eq!(test.exec("node-generate-blocks 20"), "Success"); + + // create a new account and a new pool + assert_eq!( + test.exec("account-create"), + "Success, the new account index is: 1" + ); + assert_eq!(test.exec("account-select 1"), "Success"); + let acc2_address = test.exec("address-new"); + assert_eq!(test.exec("account-select 0"), "Success"); + assert!(test + .exec(&format!("address-send {} 50000", acc2_address)) + .starts_with("The transaction was submitted successfully with ID")); + assert_eq!(test.exec("account-select 1"), "Success"); + assert!(test + .exec(&format!( + "staking-create-pool 40000 {} 0.{} {}", + rng.gen_range(0..100), + rng.gen_range(1..100), + address, + ),) + .starts_with("The transaction was submitted successfully with ID")); + assert_eq!(test.exec("account-select 0"), "Success"); + + // create some blocks + assert_eq!(test.exec("node-generate-blocks 1"), "Success"); + + // create some blocks with the other pool + assert_eq!(test.exec("account-select 1"), "Success"); + assert_eq!(test.exec("node-generate-blocks 1"), "Success"); + + // create the decommission request + assert_eq!(test.exec("account-select 0"), "Success"); + let pool_id: PoolId = H256::zero().into(); + let output = test.exec(&format!( + "staking-decommission-pool-request {}", + Address::new(&test.chain_config, &pool_id).unwrap() + )); + let req = output.lines().nth(2).unwrap(); + + assert_eq!(test.exec("wallet-close"), "Successfully closed the wallet."); + + test.create_genesis_cold_wallet(); + let output = test.exec(&format!("account-sign-raw-transaction {req}")); + let signed_tx = output.lines().nth(2).unwrap(); + assert_eq!(test.exec("wallet-close"), "Successfully closed the wallet."); + + // submit the tx + test.create_genesis_wallet(); + assert_eq!(test.exec("wallet-sync"), "Success"); + assert_eq!( + test.exec(&format!("node-submit-transaction {signed_tx}")), + "The transaction was submitted successfully" + ); + + // stake with the other acc + assert_eq!(test.exec("account-select 1"), "Success"); + assert_eq!(test.exec("node-generate-blocks 10"), "Success"); + + // stake with the first acc + assert_eq!(test.exec("account-select 0"), "Success"); + assert!(test.exec("account-balance").starts_with("Coins amount: 99869999")); + assert!(test.exec("account-balance locked").starts_with("Coins amount: 44242")); + assert_eq!(test.exec("node-generate-blocks 2"), "Success"); + + test.shutdown().await; +} diff --git a/wallet/wallet-cli-lib/tests/cli_test_framework.rs b/wallet/wallet-cli-lib/tests/cli_test_framework.rs index b3433dc139..a9b3cd40e5 100644 --- a/wallet/wallet-cli-lib/tests/cli_test_framework.rs +++ b/wallet/wallet-cli-lib/tests/cli_test_framework.rs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use common::chain::ChainConfig; use crypto::random::Rng; use tokio::task::JoinHandle; use wallet_controller::NodeInterface; @@ -30,7 +31,8 @@ use wallet_cli_lib::{ errors::WalletCliError, }; use wallet_test_node::{ - create_chain_config, default_chain_config_options, start_node, RPC_PASSWORD, RPC_USERNAME, + create_chain_config, default_chain_config_options, start_node, COLD_WALLET_MENEMONIC, + RPC_PASSWORD, RPC_USERNAME, }; pub use wallet_test_node::MNEMONIC; @@ -70,6 +72,7 @@ pub struct CliTestFramework { pub shutdown_trigger: ShutdownTrigger, pub manager_task: ManagerJoinHandle, pub test_root: TestRoot, + pub chain_config: Arc, } impl CliTestFramework { @@ -142,10 +145,11 @@ impl CliTestFramework { let output = MockConsoleOutput { output_tx }; + let wallet_chain_config = chain_config.clone(); let wallet_task = tokio::spawn(async move { tokio::time::timeout( Duration::from_secs(120), - wallet_cli_lib::run(input, output, wallet_options, Some(chain_config)), + wallet_cli_lib::run(input, output, wallet_options, Some(wallet_chain_config)), ) .await .unwrap() @@ -159,6 +163,7 @@ impl CliTestFramework { test_root, input_tx, output_rx, + chain_config, } } @@ -184,6 +189,24 @@ impl CliTestFramework { assert_eq!(self.exec(&cmd), "New wallet created successfully"); } + #[allow(dead_code)] + pub fn create_genesis_cold_wallet(&self) { + // Use dir name with spaces to make sure quoting works as expected + let file_name = self + .test_root + .fresh_test_dir("wallet dir") + .as_ref() + .join("genesis_cold_wallet") + .to_str() + .unwrap() + .to_owned(); + let cmd = format!( + "wallet-create \"{}\" store-seed-phrase \"{}\"", + file_name, COLD_WALLET_MENEMONIC, + ); + assert_eq!(self.exec(&cmd), "New wallet created successfully"); + } + pub async fn shutdown(self) { drop(self.input_tx); self.wallet_task.await.unwrap(); diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index ee91bc1872..5ee4014bb0 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -563,6 +563,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { pub async fn decommission_stake_pool( &mut self, pool_id: PoolId, + output_address: Option, ) -> Result, ControllerError> { let staker_balance = self .rpc_client @@ -582,6 +583,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { account_index, pool_id, staker_balance, + output_address, current_fee_rate, ) }, @@ -592,6 +594,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { pub async fn decommission_stake_pool_request( &mut self, pool_id: PoolId, + output_address: Option, ) -> Result> { let staker_balance = self .rpc_client @@ -609,6 +612,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.account_index, pool_id, staker_balance, + output_address, current_fee_rate, ) .map_err(ControllerError::WalletError) diff --git a/wallet/wallet-rpc-lib/src/rpc/interface.rs b/wallet/wallet-rpc-lib/src/rpc/interface.rs index a36c00a90c..b66a1211ec 100644 --- a/wallet/wallet-rpc-lib/src/rpc/interface.rs +++ b/wallet/wallet-rpc-lib/src/rpc/interface.rs @@ -147,6 +147,7 @@ trait WalletRpc { &self, account_index: AccountIndexArg, pool_id: String, + output_address: Option, options: TransactionOptions, ) -> rpc::RpcResult; diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index 09b5be40da..2fc50a56cc 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -36,8 +36,8 @@ use common::{ address::Address, chain::{ tokens::{IsTokenFreezable, IsTokenUnfreezable, Metadata, TokenTotalSupply}, - Block, ChainConfig, DelegationId, GenBlock, PoolId, SignedTransaction, Transaction, - TxOutput, UtxoOutPoint, + Block, ChainConfig, DelegationId, Destination, GenBlock, PoolId, SignedTransaction, + Transaction, TxOutput, UtxoOutPoint, }, primitives::{id::WithId, per_thousand::PerThousand, Amount, BlockHeight, DecimalAmount, Id}, }; @@ -502,19 +502,28 @@ impl WalletRpc { &self, account_index: U31, pool_id: String, + output_address: Option, config: ControllerConfig, ) -> WRpcResult { let pool_id = Address::from_str(&self.chain_config, &pool_id) .and_then(|addr| addr.decode_object(&self.chain_config)) .map_err(|_| RpcError::InvalidPoolId)?; + let output_address = output_address + .map(|addr| { + Address::::from_str(&self.chain_config, &addr) + .and_then(|addr| addr.decode_object(&self.chain_config)) + .map_err(|_| RpcError::InvalidPoolId) + }) + .transpose()?; + self.wallet .call_async(move |controller| { Box::pin(async move { controller .synced_controller(account_index, config) .await? - .decommission_stake_pool(pool_id) + .decommission_stake_pool(pool_id, output_address) .await .map_err(RpcError::Controller) .map(NewTransaction::new) @@ -527,19 +536,28 @@ impl WalletRpc { &self, account_index: U31, pool_id: String, + output_address: Option, config: ControllerConfig, ) -> WRpcResult { let pool_id = Address::from_str(&self.chain_config, &pool_id) .and_then(|addr| addr.decode_object(&self.chain_config)) .map_err(|_| RpcError::InvalidPoolId)?; + let output_address = output_address + .map(|addr| { + Address::::from_str(&self.chain_config, &addr) + .and_then(|addr| addr.decode_object(&self.chain_config)) + .map_err(|_| RpcError::InvalidPoolId) + }) + .transpose()?; + self.wallet .call_async(move |controller| { Box::pin(async move { controller .synced_controller(account_index, config) .await? - .decommission_stake_pool_request(pool_id) + .decommission_stake_pool_request(pool_id, output_address) .await .map_err(RpcError::Controller) }) diff --git a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs index 30f3e76805..35103344c4 100644 --- a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs +++ b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs @@ -240,13 +240,20 @@ impl WalletRpcServer f &self, account_index: AccountIndexArg, pool_id: String, + output_address: Option, options: TransactionOptions, ) -> rpc::RpcResult { let config = ControllerConfig { in_top_x_mb: options.in_top_x_mb, }; rpc::handle_result( - self.decommission_stake_pool(account_index.index::()?, pool_id, config).await, + self.decommission_stake_pool( + account_index.index::()?, + pool_id, + output_address, + config, + ) + .await, ) } diff --git a/wallet/wallet-test-node/src/lib.rs b/wallet/wallet-test-node/src/lib.rs index ceb49f4733..e6e4e0d851 100644 --- a/wallet/wallet-test-node/src/lib.rs +++ b/wallet/wallet-test-node/src/lib.rs @@ -53,6 +53,11 @@ pub const MNEMONIC: &str = concat!( "prison submit rescue pool panic unable enact oven trap lava floor toward", ); +pub const COLD_WALLET_MENEMONIC: &str = concat!( + "milk idle bicycle taxi pear gold teach left broom pill close alcohol ", + "mule medal morning shine famous like spare buzz fatigue same drift wall", +); + pub fn decode_hex(hex: &str) -> T { let bytes = Vec::from_hex(hex).expect("Hex decoding shouldn't fail"); ::decode_all(&mut bytes.as_slice()) @@ -71,7 +76,8 @@ fn create_custom_regtest_genesis(rng: &mut impl Rng) -> Genesis { "00027a9771bbb58170a0df36ed43e56490530f0f2f45b100c42f6f405af3ef21f54e", ); let decommission_pub_key = decode_hex::( - "0002ea30f3bb179c58022dcf2f4fd2c88685695f9532d6a9dd071da8d7ac1fe91a7d", + // "0002ea30f3bb179c58022dcf2f4fd2c88685695f9532d6a9dd071da8d7ac1fe91a7d", + "00035ca9a5797bb62e9adaec0911b6c431aefa1fb4628a206cfed1da04c0a55ba364", ); let staker_pub_key = decode_hex::( "0002884adf48b0b32ab3d66e1a8b46576dfacca5dd25b66603650de792de4dd2e483",