From a2116d1734f27052062b00f69b173e5b505771d9 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Mon, 18 Sep 2023 10:36:34 +0200 Subject: [PATCH 1/7] Refactor wallet controller - separate synced controller and read only controller - synced controller guarantees that before each write operation the wallet is synced --- node-gui/src/backend/backend_impl.rs | 106 ++- wallet/wallet-cli-lib/src/commands/mod.rs | 169 ++--- wallet/wallet-controller/src/lib.rs | 615 ++---------------- wallet/wallet-controller/src/read.rs | 217 ++++++ .../src/synced_controller.rs | 397 +++++++++++ 5 files changed, 806 insertions(+), 698 deletions(-) create mode 100644 wallet/wallet-controller/src/read.rs create mode 100644 wallet/wallet-controller/src/synced_controller.rs diff --git a/node-gui/src/backend/backend_impl.rs b/node-gui/src/backend/backend_impl.rs index 2c2b6cb9e6..a775b3bc9a 100644 --- a/node-gui/src/backend/backend_impl.rs +++ b/node-gui/src/backend/backend_impl.rs @@ -30,7 +30,10 @@ use wallet::{ account::{transaction_list::TransactionList, Currency}, DefaultWallet, }; -use wallet_controller::{HandlesController, UtxoState, WalletHandlesClient}; +use wallet_controller::{ + read::ReadOnlyController, synced_controller::SyncedController, HandlesController, UtxoState, + WalletHandlesClient, +}; use wallet_types::{seed_phrase::StoreSeedPhrase, with_locked::WithLocked}; use super::{ @@ -147,16 +150,17 @@ impl Backend { .nth(account_index.into_u32() as usize) .cloned() .flatten(); + let controller = controller.readonly_controller(account_index); let transaction_list = controller - .get_transaction_list(account_index, 0, TRANSACTION_LIST_PAGE_COUNT) + .get_transaction_list(0, TRANSACTION_LIST_PAGE_COUNT) .expect("load_transaction_list failed"); AccountInfo { name, addresses: controller - .get_all_issued_addresses(account_index) + .get_all_issued_addresses() .expect("get_all_issued_addresses should not fail normally"), staking_enabled: false, - balance: Self::get_account_balance(controller, account_index), + balance: Self::get_account_balance(&controller), staking_balance: BTreeMap::new(), transaction_list, } @@ -192,7 +196,10 @@ impl Backend { handles_client, wallet, wallet_events, - ); + ) + .await + .map_err(|e| BackendError::WalletError(e.to_string()))?; + let best_block = controller.best_block(); let accounts_info = account_indexes @@ -294,18 +301,15 @@ impl Backend { Ok((wallet_id, account_id, account_info)) } - fn new_address( + async fn new_address( &mut self, wallet_id: WalletId, account_id: AccountId, ) -> Result { - let wallet = self - .wallets - .get_mut(&wallet_id) - .ok_or(BackendError::UnknownWalletIndex(wallet_id))?; - let (index, address) = wallet - .controller - .new_address(account_id.account_index()) + let (index, address) = self + .synced_wallet_controller(wallet_id, account_id.account_index()) + .await? + .new_address() .map_err(|e| BackendError::WalletError(e.to_string()))?; Ok(AddressInfo { wallet_id, @@ -321,18 +325,15 @@ impl Backend { account_id: AccountId, enabled: bool, ) -> Result<(WalletId, AccountId, bool), BackendError> { - let wallet = self - .wallets - .get_mut(&wallet_id) - .ok_or(BackendError::UnknownWalletIndex(wallet_id))?; if enabled { - wallet - .controller - .start_staking(account_id.account_index()) - .await + self.synced_wallet_controller(wallet_id, account_id.account_index()) + .await? + .start_staking() .map_err(|e| BackendError::WalletError(e.to_string()))?; } else { - wallet + self.wallets + .get_mut(&wallet_id) + .ok_or(BackendError::UnknownWalletIndex(wallet_id))? .controller .stop_staking(account_id.account_index()) .map_err(|e| BackendError::WalletError(e.to_string()))?; @@ -351,26 +352,35 @@ impl Backend { amount, } = send_request; - let wallet = self - .wallets - .get_mut(&send_request.wallet_id) - .ok_or(BackendError::UnknownWalletIndex(wallet_id))?; - let address = parse_address(&self.chain_config, &address) .map_err(|err| BackendError::AddressError(err.to_string()))?; let amount = parse_coin_amount(&self.chain_config, &amount) .ok_or(BackendError::InvalidAmount(amount))?; // TODO: add support for utxo selection in the GUI - wallet - .controller - .send_to_address(account_id.account_index(), address, amount, vec![]) + self.synced_wallet_controller(wallet_id, account_id.account_index()) + .await? + .send_to_address(address, amount, vec![]) .await .map_err(|e| BackendError::WalletError(e.to_string()))?; Ok(TransactionInfo { wallet_id }) } + async fn synced_wallet_controller( + &mut self, + wallet_id: WalletId, + account_index: U31, + ) -> Result, BackendError> { + self.wallets + .get_mut(&wallet_id) + .ok_or(BackendError::UnknownWalletIndex(wallet_id))? + .controller + .synced_controller(account_index) + .await + .map_err(|e| BackendError::WalletError(e.to_string())) + } + async fn stake_amount( &mut self, stake_request: StakeRequest, @@ -381,18 +391,12 @@ impl Backend { amount, } = stake_request; - let wallet = self - .wallets - .get_mut(&stake_request.wallet_id) - .ok_or(BackendError::UnknownWalletIndex(wallet_id))?; - let amount = parse_coin_amount(&self.chain_config, &amount) .ok_or(BackendError::InvalidAmount(amount))?; - wallet - .controller + self.synced_wallet_controller(wallet_id, account_id.account_index()) + .await? .create_stake_pool_tx( - account_id.account_index(), amount, None, // TODO: get value from gui @@ -406,15 +410,10 @@ impl Backend { } fn get_account_balance( - controller: &GuiController, - account_index: U31, + controller: &ReadOnlyController, ) -> BTreeMap { controller - .get_balance( - account_index, - UtxoState::Confirmed.into(), - WithLocked::Unlocked, - ) + .get_balance(UtxoState::Confirmed.into(), WithLocked::Unlocked) .expect("get_balance should not fail normally") } @@ -435,11 +434,8 @@ impl Backend { account.transaction_list_skip = skip; wallet .controller - .get_transaction_list( - account_id.account_index(), - account.transaction_list_skip, - TRANSACTION_LIST_PAGE_COUNT, - ) + .readonly_controller(account_id.account_index()) + .get_transaction_list(account.transaction_list_skip, TRANSACTION_LIST_PAGE_COUNT) .map_err(|e| BackendError::WalletError(e.to_string())) } @@ -474,7 +470,7 @@ impl Backend { } BackendRequest::NewAddress(wallet_id, account_id) => { - let address_res = self.new_address(wallet_id, account_id); + let address_res = self.new_address(wallet_id, account_id).await; Self::send_event(&self.event_tx, BackendEvent::NewAddress(address_res)); } BackendRequest::ToggleStaking(wallet_id, account_id, enabled) => { @@ -533,10 +529,11 @@ impl Backend { } for (account_id, account_data) in wallet_data.accounts.iter_mut() { + let controller = + wallet_data.controller.readonly_controller(account_id.account_index()); // GuiWalletEvents will notify about wallet balance update // (when a wallet transaction is added/updated/removed) - let balance = - Self::get_account_balance(&wallet_data.controller, account_id.account_index()); + let balance = Self::get_account_balance(&controller); Self::send_event( &self.event_tx, BackendEvent::Balance(*wallet_id, *account_id, balance), @@ -548,8 +545,7 @@ impl Backend { // GuiWalletEvents will notify about transaction list // (when a wallet transaction is added/updated/removed) - let transaction_list_res = wallet_data.controller.get_transaction_list( - account_id.account_index(), + let transaction_list_res = controller.get_transaction_list( account_data.transaction_list_skip, TRANSACTION_LIST_PAGE_COUNT, ); diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index 9d1bd7e09c..e88c49b273 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -30,7 +30,10 @@ use crypto::key::{hdkd::u31::U31, PublicKey}; use p2p_types::{bannable_address::BannableAddress, ip_or_socket_address::IpOrSocketAddress}; use serialization::{hex::HexEncode, hex_encoded::HexEncoded}; use wallet::{account::Currency, version::get_version, wallet_events::WalletEventsNoOp}; -use wallet_controller::{NodeInterface, NodeRpcClient, PeerId, DEFAULT_ACCOUNT_INDEX}; +use wallet_controller::{ + read::ReadOnlyController, synced_controller::SyncedController, NodeInterface, NodeRpcClient, + PeerId, DEFAULT_ACCOUNT_INDEX, +}; use crate::{errors::WalletCliError, CliController}; @@ -462,6 +465,22 @@ impl CommandHandler { .ok_or(WalletCliError::NoWallet) } + async fn get_synced_controller( + &mut self, + ) -> Result, WalletCliError> { + let (controller, state) = self.state.as_mut().ok_or(WalletCliError::NoWallet)?; + controller + .synced_controller(state.selected_account) + .await + .map_err(WalletCliError::Controller) + } + fn get_readonly_controller( + &mut self, + ) -> Result, WalletCliError> { + let (controller, state) = self.state.as_mut().ok_or(WalletCliError::NoWallet)?; + Ok(controller.readonly_controller(state.selected_account)) + } + pub fn tx_submitted_command() -> ConsoleCommand { let status_text = "The transaction was submitted successfully"; ConsoleCommand::Print(status_text.to_owned()) @@ -529,7 +548,9 @@ impl CommandHandler { rpc_client.clone(), wallet, WalletEventsNoOp, - ), + ) + .await + .map_err(WalletCliError::Controller)?, CliWalletState { selected_account: DEFAULT_ACCOUNT_INDEX, }, @@ -564,7 +585,9 @@ impl CommandHandler { rpc_client.clone(), wallet, WalletEventsNoOp, - ), + ) + .await + .map_err(WalletCliError::Controller)?, CliWalletState { selected_account: DEFAULT_ACCOUNT_INDEX, }, @@ -703,10 +726,9 @@ impl CommandHandler { } WalletCommand::StartStaking => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - controller - .start_staking(selected_account) - .await + self.get_synced_controller() + .await? + .start_staking() .map_err(WalletCliError::Controller)?; Ok(ConsoleCommand::Print( "Staking started successfully".to_owned(), @@ -746,9 +768,9 @@ impl CommandHandler { } WalletCommand::AbandonTransaction { transaction_id } => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - controller - .abandon_transaction(selected_account, transaction_id.take()) + self.get_synced_controller() + .await? + .abandon_transaction(transaction_id.take()) .map_err(WalletCliError::Controller)?; Ok(ConsoleCommand::Print( "The transaction was marked as abandoned successfully".to_owned(), @@ -765,10 +787,10 @@ impl CommandHandler { let amount_to_issue = parse_token_amount(number_of_decimals, &amount_to_issue)?; let destination_address = parse_address(chain_config, &destination_address)?; - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let token_id = controller + let token_id = self + .get_synced_controller() + .await? .issue_new_token( - selected_account, destination_address, token_ticker.into_bytes(), amount_to_issue, @@ -810,9 +832,10 @@ impl CommandHandler { media_hash: media_hash.into_bytes(), }; - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let token_id = controller - .issue_new_nft(selected_account, destination_address, metadata) + let token_id = self + .get_synced_controller() + .await? + .issue_new_nft(destination_address, metadata) .await .map_err(WalletCliError::Controller)?; Ok(ConsoleCommand::Print(format!( @@ -840,10 +863,9 @@ impl CommandHandler { utxo_states, with_locked, } => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let mut balances = controller + let mut balances = self + .get_readonly_controller()? .get_balance( - selected_account, CliUtxoState::to_wallet_states(utxo_states), with_locked.to_wallet_type(), ) @@ -855,7 +877,8 @@ impl CommandHandler { { let out = match currency { Currency::Token(token_id) => { - let token_number_of_decimals = controller + let token_number_of_decimals = self + .controller()? .get_token_number_of_decimals(token_id) .await .map_err(WalletCliError::Controller)?; @@ -883,10 +906,9 @@ impl CommandHandler { utxo_states, with_locked, } => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let utxos = controller + let utxos = self + .get_readonly_controller()? .get_utxos( - selected_account, utxo_type.to_wallet_types(), CliUtxoState::to_wallet_states(utxo_states), with_locked.to_wallet_type(), @@ -896,32 +918,36 @@ impl CommandHandler { } WalletCommand::ListPendingTransactions => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let utxos = controller - .pending_transactions(selected_account) + let utxos = self + .get_readonly_controller()? + .pending_transactions() .map_err(WalletCliError::Controller)?; Ok(ConsoleCommand::Print(format!("{utxos:#?}"))) } WalletCommand::NewAddress => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let address = - controller.new_address(selected_account).map_err(WalletCliError::Controller)?; + let address = self + .get_synced_controller() + .await? + .new_address() + .map_err(WalletCliError::Controller)?; Ok(ConsoleCommand::Print(address.1.get().to_owned())) } WalletCommand::NewPublicKey => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let public_key = controller - .new_public_key(selected_account) + let public_key = self + .get_synced_controller() + .await? + .new_public_key() .map_err(WalletCliError::Controller)?; Ok(ConsoleCommand::Print(public_key.hex_encode())) } WalletCommand::GetVrfPublicKey => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let vrf_public_key = controller - .get_vrf_public_key(selected_account) + let vrf_public_key = self + .get_synced_controller() + .await? + .get_vrf_public_key() .map_err(WalletCliError::Controller)?; Ok(ConsoleCommand::Print(vrf_public_key.hex_encode())) } @@ -937,9 +963,9 @@ impl CommandHandler { .collect::, WalletCliError>>()?; let amount = parse_coin_amount(chain_config, &amount)?; let address = parse_address(chain_config, &address)?; - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - controller - .send_to_address(selected_account, address, amount, utxos) + self.get_synced_controller() + .await? + .send_to_address(address, amount, utxos) .await .map_err(WalletCliError::Controller)?; Ok(Self::tx_submitted_command()) @@ -961,9 +987,9 @@ impl CommandHandler { parse_token_amount(token_number_of_decimals, &amount)? }; - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - controller - .send_tokens_to_address(selected_account, token_id, address, amount) + self.get_synced_controller() + .await? + .send_tokens_to_address(token_id, address, amount) .await .map_err(WalletCliError::Controller)?; Ok(Self::tx_submitted_command()) @@ -973,13 +999,10 @@ impl CommandHandler { let address = parse_address(chain_config, &address)?; let pool_id_address = Address::from_str(chain_config, &pool_id)?; - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let delegation_id = controller - .create_delegation( - selected_account, - address, - pool_id_address.decode_object(chain_config)?, - ) + let delegation_id = self + .get_synced_controller() + .await? + .create_delegation(address, pool_id_address.decode_object(chain_config)?) .await .map_err(WalletCliError::Controller)?; Ok(ConsoleCommand::Print(format!( @@ -995,13 +1018,9 @@ impl CommandHandler { let amount = parse_coin_amount(chain_config, &amount)?; let delegation_id_address = Address::from_str(chain_config, &delegation_id)?; - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - controller - .delegate_staking( - selected_account, - amount, - delegation_id_address.decode_object(chain_config)?, - ) + self.get_synced_controller() + .await? + .delegate_staking(amount, delegation_id_address.decode_object(chain_config)?) .await .map_err(WalletCliError::Controller)?; Ok(ConsoleCommand::Print( @@ -1018,10 +1037,9 @@ impl CommandHandler { let amount = parse_coin_amount(chain_config, &amount)?; let delegation_id_address = Address::from_str(chain_config, &delegation_id)?; let address = parse_address(chain_config, &address)?; - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - controller + self.get_synced_controller() + .await? .send_to_address_from_delegation( - selected_account, address, amount, delegation_id_address.decode_object(chain_config)?, @@ -1044,10 +1062,9 @@ impl CommandHandler { let cost_per_block = parse_coin_amount(chain_config, &cost_per_block)?; let margin_ratio_per_thousand = to_per_thousand(&margin_ratio_per_thousand, "margin ratio")?; - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - controller + self.get_synced_controller() + .await? .create_stake_pool_tx( - selected_account, amount, decommission_key, margin_ratio_per_thousand, @@ -1061,9 +1078,9 @@ impl CommandHandler { WalletCommand::DecommissionStakePool { pool_id } => { let pool_id = parse_pool_id(chain_config, pool_id.as_str())?; - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - controller - .decommission_stake_pool(selected_account, pool_id) + self.get_synced_controller() + .await? + .decommission_stake_pool(pool_id) .await .map_err(WalletCliError::Controller)?; Ok(Self::tx_submitted_command()) @@ -1101,9 +1118,9 @@ impl CommandHandler { } WalletCommand::ListPoolIds => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let pool_ids: Vec<_> = controller - .get_pool_ids(chain_config, selected_account) + let pool_ids: Vec<_> = self + .get_readonly_controller()? + .get_pool_ids() .await .map_err(WalletCliError::Controller)? .into_iter() @@ -1121,9 +1138,9 @@ impl CommandHandler { } WalletCommand::ListDelegationIds => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; - let delegations: Vec<_> = controller - .get_delegations(selected_account) + let delegations: Vec<_> = self + .get_readonly_controller()? + .get_delegations() .await .map_err(WalletCliError::Controller)? .into_iter() @@ -1186,12 +1203,14 @@ impl CommandHandler { Ok(ConsoleCommand::Print("Success".to_owned())) } WalletCommand::ShowReceiveAddresses => { - let (controller, selected_account) = self.get_controller_and_selected_acc()?; + let controller = self.get_readonly_controller()?; - let addresses_with_usage = - controller.get_addresses_with_usage(selected_account).map_err(|e| { - WalletCliError::AddressesRetrievalFailed(selected_account, e.to_string()) - })?; + let addresses_with_usage = controller.get_addresses_with_usage().map_err(|e| { + WalletCliError::AddressesRetrievalFailed( + controller.account_index(), + e.to_string(), + ) + })?; let addresses_table = { let mut addresses_table = prettytable::Table::new(); diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 48b036e5c3..abc773d241 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -16,14 +16,12 @@ //! Common code for wallet UI applications pub mod mnemonic; +pub mod read; mod sync; +pub mod synced_controller; const NORMAL_DELAY: Duration = Duration::from_secs(1); const ERROR_DELAY: Duration = Duration::from_secs(10); -/// In which top N MB should we aim for our transactions to be in the mempool -/// e.g. for 5, we aim to be in the top 5 MB of transactions based on paid fees -/// This is to avoid getting trimmed off the lower end if the mempool runs out of memory -const IN_TOP_N_MB: usize = 5; use std::{ collections::{BTreeMap, BTreeSet}, @@ -33,56 +31,37 @@ use std::{ time::Duration, }; +use read::ReadOnlyController; +use synced_controller::SyncedController; use utils::tap_error_log::LogError; use common::{ - address::{Address, AddressError}, + address::AddressError, chain::{ tokens::{ - Metadata, RPCTokenInfo::{FungibleToken, NonFungibleToken}, - TokenId, TokenIssuance, + TokenId, }, - Block, ChainConfig, DelegationId, Destination, GenBlock, PoolId, SignedTransaction, - Transaction, TxOutput, UtxoOutPoint, - }, - primitives::{ - id::WithId, per_thousand::PerThousand, time::get_time, Amount, BlockHeight, Id, Idable, + Block, ChainConfig, GenBlock, PoolId, SignedTransaction, TxOutput, }, + primitives::{time::get_time, Amount, BlockHeight, Id, Idable}, }; use consensus::GenerateBlockInputData; use crypto::{ - key::{ - hdkd::{child_number::ChildNumber, u31::U31}, - PublicKey, - }, + key::hdkd::u31::U31, random::{make_pseudo_rng, Rng}, - vrf::VRFPublicKey, }; -use futures::stream::FuturesUnordered; -use futures::TryStreamExt; use logging::log; pub use node_comm::node_traits::{ConnectedPeer, NodeInterface, PeerId}; pub use node_comm::{ handles_client::WalletHandlesClient, make_rpc_client, rpc_client::NodeRpcClient, }; -use wallet::{ - account::Currency, - account::{transaction_list::TransactionList, DelegationData}, - send_request::{ - make_address_output, make_address_output_token, make_create_delegation_output, - StakePoolDataArguments, - }, - wallet_events::WalletEvents, - DefaultWallet, WalletError, -}; +use wallet::{wallet_events::WalletEvents, DefaultWallet, WalletError}; pub use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, utxo_types::{UtxoState, UtxoStates, UtxoType, UtxoTypes}, }; -use wallet_types::{ - seed_phrase::StoreSeedPhrase, with_locked::WithLocked, BlockInfo, KeychainUsageState, -}; +use wallet_types::{seed_phrase::StoreSeedPhrase, with_locked::WithLocked}; #[derive(thiserror::Error, Debug)] pub enum ControllerError { @@ -128,19 +107,24 @@ pub type RpcController = Controller; pub type HandlesController = Controller; impl Controller { - pub fn new( + pub async fn new( chain_config: Arc, rpc_client: T, wallet: DefaultWallet, wallet_events: W, - ) -> Self { - Self { + ) -> Result> { + let mut controller = Self { chain_config, rpc_client, wallet, staking_started: BTreeSet::new(), wallet_events, - } + }; + + log::info!("Syncing the wallet..."); + controller.sync_once().await?; + + Ok(controller) } pub fn create_wallet( @@ -269,11 +253,10 @@ impl Controll } } - /// Retrieve the seed phrase if stored in the database pub fn seed_phrase(&self) -> Result>, ControllerError> { self.wallet .seed_phrase() - .map(|opt| opt.map(|phrase| Self::serializable_seed_phrase_to_vec(phrase))) + .map(|opt| opt.map(Self::serializable_seed_phrase_to_vec)) .map_err(ControllerError::WalletError) } @@ -281,7 +264,7 @@ impl Controll pub fn delete_seed_phrase(&self) -> Result>, ControllerError> { self.wallet .delete_seed_phrase() - .map(|opt| opt.map(|phrase| Self::serializable_seed_phrase_to_vec(phrase))) + .map(|opt| opt.map(Self::serializable_seed_phrase_to_vec)) .map_err(ControllerError::WalletError) } @@ -334,409 +317,8 @@ impl Controll self.wallet.account_names() } - pub fn get_balance( - &self, - account_index: U31, - utxo_states: UtxoStates, - with_locked: WithLocked, - ) -> Result, ControllerError> { - self.wallet - .get_balance( - account_index, - UtxoType::Transfer | UtxoType::LockThenTransfer, - utxo_states, - with_locked, - ) - .map_err(ControllerError::WalletError) - } - - pub fn get_utxos( - &self, - account_index: U31, - utxo_types: UtxoTypes, - utxo_states: UtxoStates, - with_locked: WithLocked, - ) -> Result, ControllerError> { - self.wallet - .get_utxos(account_index, utxo_types, utxo_states, with_locked) - .map_err(ControllerError::WalletError) - } - - pub fn pending_transactions( - &self, - account_index: U31, - ) -> Result>, ControllerError> { - self.wallet - .pending_transactions(account_index) - .map_err(ControllerError::WalletError) - } - - pub fn abandon_transaction( - &mut self, - account_index: U31, - tx_id: Id, - ) -> Result<(), ControllerError> { - self.wallet - .abandon_transaction(account_index, tx_id) - .map_err(ControllerError::WalletError) - } - - #[allow(clippy::too_many_arguments)] - pub async fn issue_new_token( - &mut self, - account_index: U31, - address: Address, - token_ticker: Vec, - amount_to_issue: Amount, - number_of_decimals: u8, - metadata_uri: Vec, - ) -> Result> { - let current_fee_rate = self - .rpc_client - .mempool_get_fee_rate(IN_TOP_N_MB) - .await - .map_err(ControllerError::NodeCallError)?; - - let consolidate_fee_rate = current_fee_rate; - let (token_id, tx) = self - .wallet - .issue_new_token( - account_index, - address, - TokenIssuance { - token_ticker, - amount_to_issue, - number_of_decimals, - metadata_uri, - }, - current_fee_rate, - consolidate_fee_rate, - ) - .map_err(ControllerError::WalletError)?; - - self.broadcast_to_mempool(tx).await?; - - Ok(token_id) - } - - pub async fn issue_new_nft( - &mut self, - account_index: U31, - address: Address, - metadata: Metadata, - ) -> Result> { - let current_fee_rate = self - .rpc_client - .mempool_get_fee_rate(IN_TOP_N_MB) - .await - .map_err(ControllerError::NodeCallError)?; - - let consolidate_fee_rate = current_fee_rate; - let (token_id, tx) = self - .wallet - .issue_new_nft( - account_index, - address, - metadata, - current_fee_rate, - consolidate_fee_rate, - ) - .map_err(ControllerError::WalletError)?; - - self.broadcast_to_mempool(tx).await?; - - Ok(token_id) - } - - pub fn new_address( - &mut self, - account_index: U31, - ) -> Result<(ChildNumber, Address), ControllerError> { - self.wallet.get_new_address(account_index).map_err(ControllerError::WalletError) - } - - pub fn new_public_key(&mut self, account_index: U31) -> Result> { - self.wallet - .get_new_public_key(account_index) - .map_err(ControllerError::WalletError) - } - - async fn get_pool_info( - &self, - chain_config: &ChainConfig, - pool_id: PoolId, - block_info: BlockInfo, - ) -> Result<(PoolId, BlockInfo, Amount), ControllerError> { - self.rpc_client - .get_stake_pool_balance(pool_id) - .await - .map_err(ControllerError::NodeCallError) - .and_then(|balance| { - balance.ok_or(ControllerError::SyncError(format!( - "Pool id {} from wallet not found in node", - Address::new(chain_config, &pool_id)? - ))) - }) - .map(|balance| (pool_id, block_info, balance)) - .log_err() - } - - async fn get_delegation_share( - &self, - chain_config: &ChainConfig, - delegation_data: &DelegationData, - delegation_id: DelegationId, - ) -> Result<(DelegationId, Amount), ControllerError> { - if delegation_data.not_staked_yet { - return Ok((delegation_id, Amount::ZERO)); - } - - self.rpc_client - .get_delegation_share(delegation_data.pool_id, delegation_id) - .await - .map_err(ControllerError::NodeCallError) - .and_then(|balance| { - balance.ok_or(ControllerError::SyncError(format!( - "Delegation id {} from wallet not found in node", - Address::new(chain_config, &delegation_id)? - ))) - }) - .map(|balance| (delegation_id, balance)) - .log_err() - } - - pub async fn get_pool_ids( - &self, - chain_config: &ChainConfig, - account_index: U31, - ) -> Result, ControllerError> { - let pools = - self.wallet.get_pool_ids(account_index).map_err(ControllerError::WalletError)?; - - let tasks: FuturesUnordered<_> = pools - .into_iter() - .map(|(pool_id, block_info)| self.get_pool_info(chain_config, pool_id, block_info)) - .collect(); - - tasks.try_collect().await - } - - pub async fn get_delegations( - &mut self, - account_index: U31, - ) -> Result, ControllerError> { - let delegations = self - .wallet - .get_delegations(account_index) - .map_err(ControllerError::WalletError)?; - - let tasks: FuturesUnordered<_> = delegations - .into_iter() - .map(|(delegation_id, delegation_data)| { - self.get_delegation_share( - self.chain_config.as_ref(), - delegation_data, - *delegation_id, - ) - }) - .collect(); - - tasks.try_collect().await - } - - pub fn get_vrf_public_key( - &mut self, - account_index: U31, - ) -> Result> { - self.wallet - .get_vrf_public_key(account_index) - .map_err(ControllerError::WalletError) - } - - /// Broadcast a singed transaction to the mempool and update the wallets state if the - /// transaction has been added to the mempool - async fn broadcast_to_mempool( - &mut self, - tx: SignedTransaction, - ) -> Result<(), ControllerError> { - self.rpc_client - .submit_transaction(tx.clone()) - .await - .map_err(ControllerError::NodeCallError)?; - - self.wallet - .add_unconfirmed_tx(tx, &self.wallet_events) - .map_err(ControllerError::WalletError)?; - - Ok(()) - } - - pub async fn send_to_address( - &mut self, - account_index: U31, - address: Address, - amount: Amount, - selected_utxos: Vec, - ) -> Result<(), ControllerError> { - let output = make_address_output(self.chain_config.as_ref(), address, amount) - .map_err(ControllerError::WalletError)?; - let current_fee_rate = self - .rpc_client - .mempool_get_fee_rate(IN_TOP_N_MB) - .await - .map_err(ControllerError::NodeCallError)?; - - let consolidate_fee_rate = current_fee_rate; - - let tx = self - .wallet - .create_transaction_to_addresses( - account_index, - [output], - selected_utxos, - current_fee_rate, - consolidate_fee_rate, - ) - .map_err(ControllerError::WalletError)?; - - self.broadcast_to_mempool(tx).await - } - - pub async fn create_delegation( - &mut self, - account_index: U31, - address: Address, - pool_id: PoolId, - ) -> Result> { - let current_fee_rate = self - .rpc_client - .mempool_get_fee_rate(IN_TOP_N_MB) - .await - .map_err(ControllerError::NodeCallError)?; - - let consolidate_fee_rate = current_fee_rate; - let output = make_create_delegation_output(self.chain_config.as_ref(), address, pool_id) - .map_err(ControllerError::WalletError)?; - let (delegation_id, tx) = self - .wallet - .create_delegation( - account_index, - vec![output], - current_fee_rate, - consolidate_fee_rate, - ) - .map_err(ControllerError::WalletError)?; - - self.broadcast_to_mempool(tx).await?; - - Ok(delegation_id) - } - - pub async fn delegate_staking( - &mut self, - account_index: U31, - amount: Amount, - delegation_id: DelegationId, - ) -> Result<(), ControllerError> { - let output = TxOutput::DelegateStaking(amount, delegation_id); - - let current_fee_rate = self - .rpc_client - .mempool_get_fee_rate(IN_TOP_N_MB) - .await - .map_err(ControllerError::NodeCallError)?; - let consolidate_fee_rate = current_fee_rate; - - let tx = self - .wallet - .create_transaction_to_addresses( - account_index, - [output], - [], - current_fee_rate, - consolidate_fee_rate, - ) - .map_err(ControllerError::WalletError)?; - - self.broadcast_to_mempool(tx).await - } - - pub async fn send_to_address_from_delegation( - &mut self, - account_index: U31, - address: Address, - amount: Amount, - delegation_id: DelegationId, - ) -> Result<(), ControllerError> { - let current_fee_rate = self - .rpc_client - .mempool_get_fee_rate(IN_TOP_N_MB) - .await - .map_err(ControllerError::NodeCallError)?; - - let pool_id = self - .wallet - .get_delegation(account_index, delegation_id) - .map_err(ControllerError::WalletError)? - .pool_id; - - let delegation_share = self - .rpc_client - .get_delegation_share(pool_id, delegation_id) - .await - .map_err(ControllerError::NodeCallError)? - .ok_or(ControllerError::WalletError( - WalletError::DelegationNotFound(delegation_id), - ))?; - - let tx = self - .wallet - .create_transaction_to_addresses_from_delegation( - account_index, - address, - amount, - delegation_id, - delegation_share, - current_fee_rate, - ) - .map_err(ControllerError::WalletError)?; - - self.broadcast_to_mempool(tx).await - } - - pub async fn send_tokens_to_address( - &mut self, - account_index: U31, - token_id: TokenId, - address: Address, - amount: Amount, - ) -> Result<(), ControllerError> { - let current_fee_rate = self - .rpc_client - .mempool_get_fee_rate(IN_TOP_N_MB) - .await - .map_err(ControllerError::NodeCallError)?; - - let consolidate_fee_rate = current_fee_rate; - let output = - make_address_output_token(self.chain_config.as_ref(), address, amount, token_id) - .map_err(ControllerError::WalletError)?; - let tx = self - .wallet - .create_transaction_to_addresses( - account_index, - [output], - [], - current_fee_rate, - consolidate_fee_rate, - ) - .map_err(ControllerError::WalletError)?; - - self.broadcast_to_mempool(tx).await - } - pub async fn get_token_number_of_decimals( - &mut self, + &self, token_id: TokenId, ) -> Result> { let token_info = self @@ -756,68 +338,6 @@ impl Controll Ok(decimals) } - pub async fn create_stake_pool_tx( - &mut self, - account_index: U31, - amount: Amount, - decommission_key: Option, - margin_ratio_per_thousand: PerThousand, - cost_per_block: Amount, - ) -> Result<(), ControllerError> { - let current_fee_rate = self - .rpc_client - .mempool_get_fee_rate(IN_TOP_N_MB) - .await - .map_err(ControllerError::NodeCallError)?; - - let consolidate_fee_rate = current_fee_rate; - - let tx = self - .wallet - .create_stake_pool_tx( - account_index, - decommission_key, - current_fee_rate, - consolidate_fee_rate, - StakePoolDataArguments { - amount, - margin_ratio_per_thousand, - cost_per_block, - }, - ) - .map_err(ControllerError::WalletError)?; - - self.broadcast_to_mempool(tx).await - } - - pub async fn decommission_stake_pool( - &mut self, - account_index: U31, - pool_id: PoolId, - ) -> Result<(), ControllerError> { - let current_fee_rate = self - .rpc_client - .mempool_get_fee_rate(IN_TOP_N_MB) - .await - .map_err(ControllerError::NodeCallError)?; - - let staker_balance = self - .rpc_client - .get_stake_pool_pledge(pool_id) - .await - .map_err(ControllerError::NodeCallError)? - .ok_or(ControllerError::WalletError(WalletError::UnknownPoolId( - pool_id, - )))?; - - let tx = self - .wallet - .decommission_stake_pool(account_index, pool_id, staker_balance, current_fee_rate) - .map_err(ControllerError::WalletError)?; - - self.broadcast_to_mempool(tx).await - } - pub async fn generate_block_by_pool( &self, account_index: U31, @@ -888,37 +408,12 @@ impl Controll self.wallet.create_next_account(name).map_err(ControllerError::WalletError) } - pub fn get_transaction_list( - &self, - account_index: U31, - skip: usize, - count: usize, - ) -> Result> { - self.wallet - .get_transaction_list(account_index, skip, count) - .map_err(ControllerError::WalletError) - } - - pub async fn start_staking(&mut self, account_index: U31) -> Result<(), ControllerError> { - utils::ensure!(!self.wallet.is_locked(), ControllerError::WalletIsLocked); - // Sync once to get the updated pool list - self.sync_once().await?; - // Make sure that account_index is valid and that pools exist - let pool_ids = - self.wallet.get_pool_ids(account_index).map_err(ControllerError::WalletError)?; - utils::ensure!(!pool_ids.is_empty(), ControllerError::NoStakingPool); - log::info!("Start staking, account_index: {}", account_index); - self.staking_started.insert(account_index); - Ok(()) - } - pub fn stop_staking(&mut self, account_index: U31) -> Result<(), ControllerError> { log::info!("Stop staking, account_index: {}", account_index); self.staking_started.remove(&account_index); Ok(()) } - /// Wallet sync progress pub fn best_block(&self) -> (Id, BlockHeight) { *self .wallet @@ -965,46 +460,6 @@ impl Controll Ok(balances) } - pub fn get_all_issued_addresses( - &self, - account_index: U31, - ) -> Result>, ControllerError> { - self.wallet - .get_all_issued_addresses(account_index) - .map_err(ControllerError::WalletError) - } - - pub fn get_addresses_usage( - &self, - account_index: U31, - ) -> Result<&KeychainUsageState, ControllerError> { - self.wallet - .get_addresses_usage(account_index) - .map_err(ControllerError::WalletError) - } - - /// Get all addresses with usage information - /// The boolean in the BTreeMap's value is true if the address is used, false is otherwise - /// Note that the usage statistics follow strictly the rules of the wallet. For example, - /// the initial wallet only stored information about the last used address, so the usage - /// of all addresses after the first unused address will have the result `false`. - #[allow(clippy::type_complexity)] - pub fn get_addresses_with_usage( - &self, - account_index: U31, - ) -> Result, bool)>, ControllerError> { - let addresses = self.get_all_issued_addresses(account_index)?; - let usage = self.get_addresses_usage(account_index)?; - - Ok(addresses - .into_iter() - .map(|(child_number, address)| { - let used = usage.last_used().is_some_and(|used| used >= child_number.get_index()); - (child_number, (address, used)) - }) - .collect()) - } - /// Synchronize the wallet to the current node tip height and return pub async fn sync_once(&mut self) -> Result<(), ControllerError> { sync::sync_once( @@ -1017,6 +472,30 @@ impl Controll Ok(()) } + pub async fn synced_controller( + &mut self, + account_index: U31, + ) -> Result, ControllerError> { + self.sync_once().await?; + Ok(SyncedController::new( + &mut self.wallet, + self.rpc_client.clone(), + self.chain_config.as_ref(), + &self.wallet_events, + &mut self.staking_started, + account_index, + )) + } + + pub fn readonly_controller(&self, account_index: U31) -> ReadOnlyController { + ReadOnlyController::new( + &self.wallet, + self.rpc_client.clone(), + self.chain_config.as_ref(), + account_index, + ) + } + /// Synchronize the wallet in the background from the node's blockchain. /// Try staking new blocks if staking was started. pub async fn run(&mut self) -> Result<(), ControllerError> { diff --git a/wallet/wallet-controller/src/read.rs b/wallet/wallet-controller/src/read.rs new file mode 100644 index 0000000000..07b3d19a77 --- /dev/null +++ b/wallet/wallet-controller/src/read.rs @@ -0,0 +1,217 @@ +// Copyright (c) 2023 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Read operations for the wallet + +use std::collections::BTreeMap; + +use common::{ + address::Address, + chain::{ChainConfig, DelegationId, Destination, PoolId, Transaction, TxOutput, UtxoOutPoint}, + primitives::{id::WithId, Amount}, +}; +use crypto::key::hdkd::{child_number::ChildNumber, u31::U31}; +use futures::{stream::FuturesUnordered, TryStreamExt}; +use node_comm::node_traits::NodeInterface; +use utils::tap_error_log::LogError; +use wallet::{ + account::{transaction_list::TransactionList, Currency, DelegationData}, + DefaultWallet, +}; +use wallet_types::{ + utxo_types::{UtxoStates, UtxoType, UtxoTypes}, + with_locked::WithLocked, + BlockInfo, KeychainUsageState, +}; + +use crate::ControllerError; + +pub struct ReadOnlyController<'a, T> { + wallet: &'a DefaultWallet, + rpc_client: T, + chain_config: &'a ChainConfig, + account_index: U31, +} + +impl<'a, T: NodeInterface> ReadOnlyController<'a, T> { + pub fn new( + wallet: &'a DefaultWallet, + rpc_client: T, + chain_config: &'a ChainConfig, + account_index: U31, + ) -> Self { + Self { + wallet, + rpc_client, + chain_config, + account_index, + } + } + + pub fn account_index(&self) -> U31 { + self.account_index + } + + pub fn get_balance( + &self, + utxo_states: UtxoStates, + with_locked: WithLocked, + ) -> Result, ControllerError> { + self.wallet + .get_balance( + self.account_index, + UtxoType::Transfer | UtxoType::LockThenTransfer, + utxo_states, + with_locked, + ) + .map_err(ControllerError::WalletError) + } + + pub fn get_utxos( + &self, + utxo_types: UtxoTypes, + utxo_states: UtxoStates, + with_locked: WithLocked, + ) -> Result, ControllerError> { + self.wallet + .get_utxos(self.account_index, utxo_types, utxo_states, with_locked) + .map_err(ControllerError::WalletError) + } + + pub fn pending_transactions(&self) -> Result>, ControllerError> { + self.wallet + .pending_transactions(self.account_index) + .map_err(ControllerError::WalletError) + } + + pub fn get_transaction_list( + &self, + skip: usize, + count: usize, + ) -> Result> { + self.wallet + .get_transaction_list(self.account_index, skip, count) + .map_err(ControllerError::WalletError) + } + + pub fn get_all_issued_addresses( + &self, + ) -> Result>, ControllerError> { + self.wallet + .get_all_issued_addresses(self.account_index) + .map_err(ControllerError::WalletError) + } + + pub fn get_addresses_usage(&self) -> Result<&'a KeychainUsageState, ControllerError> { + self.wallet + .get_addresses_usage(self.account_index) + .map_err(ControllerError::WalletError) + } + + /// Get all addresses with usage information + /// The boolean in the BTreeMap's value is true if the address is used, false is otherwise + /// Note that the usage statistics follow strictly the rules of the wallet. For example, + /// the initial wallet only stored information about the last used address, so the usage + /// of all addresses after the first unused address will have the result `false`. + #[allow(clippy::type_complexity)] + pub fn get_addresses_with_usage( + &self, + ) -> Result, bool)>, ControllerError> { + let addresses = self.get_all_issued_addresses()?; + let usage = self.get_addresses_usage()?; + + Ok(addresses + .into_iter() + .map(|(child_number, address)| { + let used = usage.last_used().is_some_and(|used| used >= child_number.get_index()); + (child_number, (address, used)) + }) + .collect()) + } + + pub async fn get_pool_ids( + &self, + ) -> Result, ControllerError> { + let pools = self + .wallet + .get_pool_ids(self.account_index) + .map_err(ControllerError::WalletError)?; + + let tasks: FuturesUnordered<_> = pools + .into_iter() + .map(|(pool_id, block_info)| self.get_pool_info(pool_id, block_info)) + .collect(); + + tasks.try_collect().await + } + + async fn get_pool_info( + &self, + pool_id: PoolId, + block_info: BlockInfo, + ) -> Result<(PoolId, BlockInfo, Amount), ControllerError> { + self.rpc_client + .get_stake_pool_balance(pool_id) + .await + .map_err(ControllerError::NodeCallError) + .and_then(|balance| { + balance.ok_or(ControllerError::SyncError(format!( + "Pool id {} from wallet not found in node", + Address::new(self.chain_config, &pool_id)? + ))) + }) + .map(|balance| (pool_id, block_info, balance)) + .log_err() + } + + pub async fn get_delegations(&self) -> Result, ControllerError> { + let delegations = self + .wallet + .get_delegations(self.account_index) + .map_err(ControllerError::WalletError)?; + + let tasks: FuturesUnordered<_> = delegations + .into_iter() + .map(|(delegation_id, delegation_data)| { + self.get_delegation_share(delegation_data, *delegation_id) + }) + .collect(); + + tasks.try_collect().await + } + + async fn get_delegation_share( + &self, + delegation_data: &DelegationData, + delegation_id: DelegationId, + ) -> Result<(DelegationId, Amount), ControllerError> { + if delegation_data.not_staked_yet { + return Ok((delegation_id, Amount::ZERO)); + } + + self.rpc_client + .get_delegation_share(delegation_data.pool_id, delegation_id) + .await + .map_err(ControllerError::NodeCallError) + .and_then(|balance| { + balance.ok_or(ControllerError::SyncError(format!( + "Delegation id {} from wallet not found in node", + Address::new(self.chain_config, &delegation_id)? + ))) + }) + .map(|balance| (delegation_id, balance)) + .log_err() + } +} diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs new file mode 100644 index 0000000000..8665904de3 --- /dev/null +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -0,0 +1,397 @@ +// Copyright (c) 2023 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeSet; + +use common::{ + address::Address, + chain::{ + tokens::{Metadata, TokenId, TokenIssuance}, + ChainConfig, DelegationId, Destination, PoolId, SignedTransaction, Transaction, TxOutput, + UtxoOutPoint, + }, + primitives::{per_thousand::PerThousand, Amount, Id}, +}; +use crypto::{ + key::{ + hdkd::{child_number::ChildNumber, u31::U31}, + PublicKey, + }, + vrf::VRFPublicKey, +}; +use logging::log; +use node_comm::node_traits::NodeInterface; +use wallet::{ + send_request::{ + make_address_output, make_address_output_token, make_create_delegation_output, + StakePoolDataArguments, + }, + wallet_events::WalletEvents, + DefaultWallet, WalletError, +}; + +use crate::ControllerError; + +pub struct SyncedController<'a, T, W> { + wallet: &'a mut DefaultWallet, + rpc_client: T, + chain_config: &'a ChainConfig, + wallet_events: &'a W, + staking_started: &'a mut BTreeSet, + account_index: U31, +} + +/// In which top N MB should we aim for our transactions to be in the mempool +/// e.g. for 5, we aim to be in the top 5 MB of transactions based on paid fees +/// This is to avoid getting trimmed off the lower end if the mempool runs out of memory +const IN_TOP_N_MB: usize = 5; + +impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { + pub fn new( + wallet: &'a mut DefaultWallet, + rpc_client: T, + chain_config: &'a ChainConfig, + wallet_events: &'a W, + staking_started: &'a mut BTreeSet, + account_index: U31, + ) -> Self { + Self { + wallet, + rpc_client, + chain_config, + wallet_events, + staking_started, + account_index, + } + } + + pub fn abandon_transaction( + &mut self, + tx_id: Id, + ) -> Result<(), ControllerError> { + self.wallet + .abandon_transaction(self.account_index, tx_id) + .map_err(ControllerError::WalletError) + } + + pub fn new_address( + &mut self, + ) -> Result<(ChildNumber, Address), ControllerError> { + self.wallet + .get_new_address(self.account_index) + .map_err(ControllerError::WalletError) + } + + pub fn new_public_key(&mut self) -> Result> { + self.wallet + .get_new_public_key(self.account_index) + .map_err(ControllerError::WalletError) + } + + pub fn get_vrf_public_key(&mut self) -> Result> { + self.wallet + .get_vrf_public_key(self.account_index) + .map_err(ControllerError::WalletError) + } + + pub async fn issue_new_token( + &mut self, + address: Address, + token_ticker: Vec, + amount_to_issue: Amount, + number_of_decimals: u8, + metadata_uri: Vec, + ) -> Result> { + let (current_fee_rate, consolidate_fee_rate) = + self.get_current_and_consolidation_fee_rate().await?; + let (token_id, tx) = self + .wallet + .issue_new_token( + self.account_index, + address, + TokenIssuance { + token_ticker, + amount_to_issue, + number_of_decimals, + metadata_uri, + }, + current_fee_rate, + consolidate_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await?; + + Ok(token_id) + } + + pub async fn issue_new_nft( + &mut self, + address: Address, + metadata: Metadata, + ) -> Result> { + let (current_fee_rate, consolidate_fee_rate) = + self.get_current_and_consolidation_fee_rate().await?; + let (token_id, tx) = self + .wallet + .issue_new_nft( + self.account_index, + address, + metadata, + current_fee_rate, + consolidate_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await?; + + Ok(token_id) + } + + pub async fn send_to_address( + &mut self, + address: Address, + amount: Amount, + selected_utxos: Vec, + ) -> Result<(), ControllerError> { + let output = make_address_output(self.chain_config, address, amount) + .map_err(ControllerError::WalletError)?; + let (current_fee_rate, consolidate_fee_rate) = + self.get_current_and_consolidation_fee_rate().await?; + + let tx = self + .wallet + .create_transaction_to_addresses( + self.account_index, + [output], + selected_utxos, + current_fee_rate, + consolidate_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await + } + + pub async fn create_delegation( + &mut self, + address: Address, + pool_id: PoolId, + ) -> Result> { + let (current_fee_rate, consolidate_fee_rate) = + self.get_current_and_consolidation_fee_rate().await?; + let output = make_create_delegation_output(self.chain_config, address, pool_id) + .map_err(ControllerError::WalletError)?; + let (delegation_id, tx) = self + .wallet + .create_delegation( + self.account_index, + vec![output], + current_fee_rate, + consolidate_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await?; + + Ok(delegation_id) + } + + pub async fn delegate_staking( + &mut self, + amount: Amount, + delegation_id: DelegationId, + ) -> Result<(), ControllerError> { + let output = TxOutput::DelegateStaking(amount, delegation_id); + + let (current_fee_rate, consolidate_fee_rate) = + self.get_current_and_consolidation_fee_rate().await?; + + let tx = self + .wallet + .create_transaction_to_addresses( + self.account_index, + [output], + [], + current_fee_rate, + consolidate_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await + } + + pub async fn send_to_address_from_delegation( + &mut self, + address: Address, + amount: Amount, + delegation_id: DelegationId, + ) -> Result<(), ControllerError> { + let (current_fee_rate, _) = self.get_current_and_consolidation_fee_rate().await?; + + let pool_id = self + .wallet + .get_delegation(self.account_index, delegation_id) + .map_err(ControllerError::WalletError)? + .pool_id; + + let delegation_share = self + .rpc_client + .get_delegation_share(pool_id, delegation_id) + .await + .map_err(ControllerError::NodeCallError)? + .ok_or(ControllerError::WalletError( + WalletError::DelegationNotFound(delegation_id), + ))?; + + let tx = self + .wallet + .create_transaction_to_addresses_from_delegation( + self.account_index, + address, + amount, + delegation_id, + delegation_share, + current_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await + } + + pub async fn send_tokens_to_address( + &mut self, + token_id: TokenId, + address: Address, + amount: Amount, + ) -> Result<(), ControllerError> { + let (current_fee_rate, consolidate_fee_rate) = + self.get_current_and_consolidation_fee_rate().await?; + + let output = make_address_output_token(self.chain_config, address, amount, token_id) + .map_err(ControllerError::WalletError)?; + let tx = self + .wallet + .create_transaction_to_addresses( + self.account_index, + [output], + [], + current_fee_rate, + consolidate_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await + } + + pub async fn create_stake_pool_tx( + &mut self, + amount: Amount, + decommission_key: Option, + margin_ratio_per_thousand: PerThousand, + cost_per_block: Amount, + ) -> Result<(), ControllerError> { + let (current_fee_rate, consolidate_fee_rate) = + self.get_current_and_consolidation_fee_rate().await?; + + let tx = self + .wallet + .create_stake_pool_tx( + self.account_index, + decommission_key, + current_fee_rate, + consolidate_fee_rate, + StakePoolDataArguments { + amount, + margin_ratio_per_thousand, + cost_per_block, + }, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await + } + + pub async fn decommission_stake_pool( + &mut self, + pool_id: PoolId, + ) -> Result<(), ControllerError> { + let (current_fee_rate, _) = self.get_current_and_consolidation_fee_rate().await?; + + let staker_balance = self + .rpc_client + .get_stake_pool_pledge(pool_id) + .await + .map_err(ControllerError::NodeCallError)? + .ok_or(ControllerError::WalletError(WalletError::UnknownPoolId( + pool_id, + )))?; + + let tx = self + .wallet + .decommission_stake_pool( + self.account_index, + pool_id, + staker_balance, + current_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await + } + + pub fn start_staking(&mut self) -> Result<(), ControllerError> { + utils::ensure!(!self.wallet.is_locked(), ControllerError::WalletIsLocked); + // Make sure that account_index is valid and that pools exist + let pool_ids = self + .wallet + .get_pool_ids(self.account_index) + .map_err(ControllerError::WalletError)?; + utils::ensure!(!pool_ids.is_empty(), ControllerError::NoStakingPool); + log::info!("Start staking, account_index: {}", self.account_index); + self.staking_started.insert(self.account_index); + Ok(()) + } + + async fn get_current_and_consolidation_fee_rate( + &mut self, + ) -> Result<(mempool::FeeRate, mempool::FeeRate), ControllerError> { + let current_fee_rate = self + .rpc_client + .mempool_get_fee_rate(IN_TOP_N_MB) + .await + .map_err(ControllerError::NodeCallError)?; + let consolidate_fee_rate = current_fee_rate; + Ok((current_fee_rate, consolidate_fee_rate)) + } + + /// Broadcast a singed transaction to the mempool and update the wallets state if the + /// transaction has been added to the mempool + async fn broadcast_to_mempool( + &mut self, + tx: SignedTransaction, + ) -> Result<(), ControllerError> { + self.rpc_client + .submit_transaction(tx.clone()) + .await + .map_err(ControllerError::NodeCallError)?; + + self.wallet + .add_unconfirmed_tx(tx, self.wallet_events) + .map_err(ControllerError::WalletError)?; + + Ok(()) + } +} From 2f4ae01586f01a2b5d790ff5469fe756355b6774 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Tue, 19 Sep 2023 15:33:26 +0200 Subject: [PATCH 2/7] Add info log when syncing --- wallet/wallet-controller/src/sync/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/wallet/wallet-controller/src/sync/mod.rs b/wallet/wallet-controller/src/sync/mod.rs index 604b2c989e..887025b19c 100644 --- a/wallet/wallet-controller/src/sync/mod.rs +++ b/wallet/wallet-controller/src/sync/mod.rs @@ -145,6 +145,13 @@ pub async fn sync_once( ) .await?; + let lowest_acc_height = + accounts_grouped.first().expect("empty accounts").0.common_block_height; + log::info!( + "Syncing the wallet from height: {} to {}", + lowest_acc_height, + chain_info.best_block_height + ); // Sync all account groups together from last to first, // where the last has the lowest block height. // Once a group is synced with the next one, merge them, From c237d8f97115722b0d85fa3c43b2d3ca9d3fc7e6 Mon Sep 17 00:00:00 2001 From: Samer Afach Date: Tue, 19 Sep 2023 17:52:39 +0400 Subject: [PATCH 3/7] Some minor refactoring/renames --- wallet/wallet-cli-lib/src/commands/mod.rs | 20 ++++++++++---------- wallet/wallet-controller/src/lib.rs | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index e88c49b273..906c79dc34 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -55,7 +55,7 @@ pub enum WalletCommand { /// Not storing the seed-phrase can be seen as a security measure /// to ensure sufficient secrecy in case that seed-phrase is reused /// elsewhere if this wallet is compromised. - save_seed_phrase: CliStoreSeedPhrase, + whether_to_store_seed_phrase: CliStoreSeedPhrase, /// Mnemonic phrase (12, 15, or 24 words as a single quoted argument). If not specified, a new mnemonic phrase is generated and printed. mnemonic: Option, @@ -277,10 +277,10 @@ pub enum WalletCommand { pool_id: String, }, - /// Show the seed phrase for the loaded wallet if it has been saved + /// Show the seed phrase for the loaded wallet if it has been stored ShowSeedPhrase, - /// Delete the seed phrase from the loaded wallet if it has been saved + /// Delete the seed phrase from the loaded wallet if it has been stored PurgeSeedPhrase, /// Node version @@ -504,7 +504,7 @@ impl CommandHandler { WalletCommand::CreateWallet { wallet_path, mnemonic, - save_seed_phrase, + whether_to_store_seed_phrase, } => { utils::ensure!(self.state.is_none(), WalletCliError::WalletFileAlreadyOpen); @@ -527,7 +527,7 @@ impl CommandHandler { wallet_path, mnemonic.clone(), None, - save_seed_phrase.to_walet_type(), + whether_to_store_seed_phrase.to_walet_type(), info.best_block_height, info.best_block_id, ) @@ -537,7 +537,7 @@ impl CommandHandler { wallet_path, mnemonic.clone(), None, - save_seed_phrase.to_walet_type(), + whether_to_store_seed_phrase.to_walet_type(), ) } .map_err(WalletCliError::Controller)?; @@ -1091,9 +1091,9 @@ impl CommandHandler { self.controller()?.seed_phrase().map_err(WalletCliError::Controller)?; let msg = if let Some(phrase) = phrase { - format!("The saved seed phrase is \"{}\"", phrase.join(" ")) + format!("The stored seed phrase is \"{}\"", phrase.join(" ")) } else { - "No saved seed phrase for this wallet. This was your choice when you created the wallet as a security option. Make sure not to lose this wallet file if you don't have the seed-phrase saved elsewhere when you created the wallet.".into() + "No stored seed phrase for this wallet. This was your choice when you created the wallet as a security option. Make sure not to lose this wallet file if you don't have the seed-phrase stored elsewhere when you created the wallet.".into() }; Ok(ConsoleCommand::Print(msg)) @@ -1104,9 +1104,9 @@ impl CommandHandler { self.controller()?.delete_seed_phrase().map_err(WalletCliError::Controller)?; let msg = if let Some(phrase) = phrase { - format!("The seed phrase has been deleted, you can save it if you haven't do so yet: \"{}\"", phrase.join(" ")) + format!("The seed phrase has been deleted, you can store it if you haven't do so yet: \"{}\"", phrase.join(" ")) } else { - "No saved seed phrase for this wallet.".into() + "No stored seed phrase for this wallet.".into() }; Ok(ConsoleCommand::Print(msg)) diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index abc773d241..6b9f3e731e 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -132,7 +132,7 @@ impl Controll file_path: impl AsRef, mnemonic: mnemonic::Mnemonic, passphrase: Option<&str>, - save_seed_phrase: StoreSeedPhrase, + whether_to_store_seed_phrase: StoreSeedPhrase, best_block_height: BlockHeight, best_block_id: Id, ) -> Result> { @@ -151,7 +151,7 @@ impl Controll db, &mnemonic.to_string(), passphrase, - save_seed_phrase, + whether_to_store_seed_phrase, best_block_height, best_block_id, ) @@ -165,7 +165,7 @@ impl Controll file_path: impl AsRef, mnemonic: mnemonic::Mnemonic, passphrase: Option<&str>, - save_seed_phrase: StoreSeedPhrase, + whether_to_store_seed_phrase: StoreSeedPhrase, ) -> Result> { utils::ensure!( !file_path.as_ref().exists(), @@ -182,7 +182,7 @@ impl Controll db, &mnemonic.to_string(), passphrase, - save_seed_phrase, + whether_to_store_seed_phrase, ) .map_err(ControllerError::WalletError)?; From c201207adc555c61dea860fec3e204a0455bdf60 Mon Sep 17 00:00:00 2001 From: Samer Afach Date: Tue, 19 Sep 2023 18:02:59 +0400 Subject: [PATCH 4/7] Add message at the end of syncing --- wallet/wallet-controller/src/sync/mod.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/wallet/wallet-controller/src/sync/mod.rs b/wallet/wallet-controller/src/sync/mod.rs index 887025b19c..9f8ddbb86e 100644 --- a/wallet/wallet-controller/src/sync/mod.rs +++ b/wallet/wallet-controller/src/sync/mod.rs @@ -145,13 +145,16 @@ pub async fn sync_once( ) .await?; - let lowest_acc_height = - accounts_grouped.first().expect("empty accounts").0.common_block_height; - log::info!( - "Syncing the wallet from height: {} to {}", - lowest_acc_height, - chain_info.best_block_height - ); + { + let lowest_acc_height = + accounts_grouped.first().expect("empty accounts").0.common_block_height; + log::info!( + "Syncing the wallet from height: {} to {}", + lowest_acc_height, + chain_info.best_block_height + ); + } + // Sync all account groups together from last to first, // where the last has the lowest block height. // Once a group is synced with the next one, merge them, @@ -180,6 +183,8 @@ pub async fn sync_once( wallet_events, ) .await?; + + log::info!("Wallet syncing done to {}", chain_info.best_block_height); } } From 2247ecc4cc09e70599dd2f32bab0f780196bfb32 Mon Sep 17 00:00:00 2001 From: Samer Afach Date: Tue, 19 Sep 2023 20:09:17 +0400 Subject: [PATCH 5/7] Print sync log message once --- wallet/wallet-controller/src/sync/mod.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/wallet/wallet-controller/src/sync/mod.rs b/wallet/wallet-controller/src/sync/mod.rs index 9f8ddbb86e..6db1af53b2 100644 --- a/wallet/wallet-controller/src/sync/mod.rs +++ b/wallet/wallet-controller/src/sync/mod.rs @@ -22,6 +22,7 @@ use common::{ use crypto::key::hdkd::u31::U31; use logging::log; use node_comm::node_traits::NodeInterface; +use utils::{once_destructor::OnceDestructor, set_flag::SetFlag}; use wallet::{ wallet::WalletSyncingState, wallet_events::WalletEvents, DefaultWallet, WalletResult, }; @@ -113,6 +114,9 @@ pub async fn sync_once( wallet: &mut impl SyncingWallet, wallet_events: &impl WalletEvents, ) -> Result<(), ControllerError> { + let mut print_flag = SetFlag::new(); + let mut _log_on_exit = None; + loop { let chain_info = rpc_client.chainstate_info().await.map_err(ControllerError::NodeCallError)?; @@ -145,11 +149,19 @@ pub async fn sync_once( ) .await?; - { + // Print the log message informing about the syncing process only once + if !print_flag.test_and_set() { + _log_on_exit = Some(OnceDestructor::new(move || { + log::info!( + "Wallet syncing done to height {}", + chain_info.best_block_height + ) + })); + let lowest_acc_height = accounts_grouped.first().expect("empty accounts").0.common_block_height; log::info!( - "Syncing the wallet from height: {} to {}", + "Syncing wallet from height {} to {}", lowest_acc_height, chain_info.best_block_height ); @@ -183,8 +195,6 @@ pub async fn sync_once( wallet_events, ) .await?; - - log::info!("Wallet syncing done to {}", chain_info.best_block_height); } } From b36e21459e6ff73e4c6fe707b12a5ec70e74b941 Mon Sep 17 00:00:00 2001 From: Samer Afach Date: Tue, 19 Sep 2023 20:09:49 +0400 Subject: [PATCH 6/7] Improve OnceDestructor --- utils/src/once_destructor.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/utils/src/once_destructor.rs b/utils/src/once_destructor.rs index feca902c64..76b392757c 100644 --- a/utils/src/once_destructor.rs +++ b/utils/src/once_destructor.rs @@ -29,8 +29,6 @@ impl OnceDestructor { impl Drop for OnceDestructor { fn drop(&mut self) { - let mut finalizer: Option = None; - std::mem::swap(&mut finalizer, &mut self.call_on_drop); - finalizer.expect("Must exist")(); + self.call_on_drop.take().expect("Must exist")(); } } From 3eeb7bcb35ff829f3d00133f265f2bdfd25b545c Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Tue, 19 Sep 2023 21:50:08 +0200 Subject: [PATCH 7/7] fix renaming of save -> store for seed phrase --- test/functional/test_framework/wallet_cli_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index 40799581be..78a800fc38 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -117,7 +117,7 @@ async def close_wallet(self) -> str: async def show_seed_phrase(self) -> Optional[str]: output = await self._write_command("showseedphrase\n") - if output.startswith("The saved seed phrase is"): + if output.startswith("The stored seed phrase is"): mnemonic = output[output.find("\"") + 1:-1] return mnemonic # wallet doesn't have the seed phrase stored