diff --git a/Cargo.lock b/Cargo.lock index 50a78582aa..48900e2091 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4473,6 +4473,7 @@ dependencies = [ "mempool", "node-lib", "p2p", + "pos-accounting", "rpc", "serde_json", "serialization", @@ -8627,6 +8628,7 @@ dependencies = [ "mempool-types", "node-comm", "p2p-types", + "pos-accounting", "randomness", "rpc-description", "rstest", diff --git a/chainstate/src/rpc/mod.rs b/chainstate/src/rpc/mod.rs index 63324daafc..9b9964ad4b 100644 --- a/chainstate/src/rpc/mod.rs +++ b/chainstate/src/rpc/mod.rs @@ -24,7 +24,7 @@ use std::{ sync::Arc, }; -use self::types::{block::RpcBlock, event::RpcEvent}; +use self::types::{block::RpcBlock, event::RpcEvent, output::RpcPoolData}; use crate::{Block, BlockSource, ChainInfo, GenBlock}; use chainstate_types::BlockIndex; use common::{ @@ -142,6 +142,12 @@ trait ChainstateRpc { #[method(name = "staker_balance")] async fn staker_balance(&self, pool_address: String) -> RpcResult>; + /// Returns the pool data of the pool associated with the given pool address. + /// + /// Returns `None` (null) if the pool is not found. + #[method(name = "pool_data")] + async fn pool_data(&self, pool_address: String) -> RpcResult>; + /// Given a pool defined by a pool address, and a delegation address, /// returns the amount of coins owned by that delegation in that pool. #[method(name = "delegation_share")] @@ -332,6 +338,26 @@ impl ChainstateRpcServer for super::ChainstateHandle { ) } + async fn pool_data(&self, pool_address: String) -> RpcResult> { + rpc::handle_result( + self.call(move |this| { + let chain_config = this.get_chain_config(); + let result: Result, _> = + dynamize_err(Address::::from_string(chain_config, pool_address)) + .map(|address| address.into_object()) + .and_then(|pool_id| dynamize_err(this.get_stake_pool_data(pool_id))) + .and_then(|pool_data| { + dynamize_err( + pool_data.map(|d| RpcPoolData::new(chain_config, &d)).transpose(), + ) + }); + + result + }) + .await, + ) + } + async fn delegation_share( &self, pool_address: String, diff --git a/chainstate/src/rpc/types/output.rs b/chainstate/src/rpc/types/output.rs index 2084087b2f..188b52b37e 100644 --- a/chainstate/src/rpc/types/output.rs +++ b/chainstate/src/rpc/types/output.rs @@ -23,6 +23,7 @@ use common::{ primitives::amount::RpcAmountOut, }; use crypto::vrf::VRFPublicKey; +use pos_accounting::PoolData; use rpc::types::RpcHexString; use super::token::{RpcNftIssuance, RpcTokenIssuance}; @@ -55,6 +56,36 @@ impl RpcOutputValue { } } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rpc_description::HasValueHint)] +pub struct RpcPoolData { + pub pledge: RpcAmountOut, + pub rewards: RpcAmountOut, + pub vrf_public_key: RpcAddress, + pub decommission_key: RpcAddress, + pub margin_ratio_per_thousand: String, + pub cost_per_block: RpcAmountOut, +} + +impl RpcPoolData { + pub fn new(chain_config: &ChainConfig, data: &PoolData) -> Result { + let result = Self { + pledge: RpcAmountOut::from_amount(data.pledge_amount(), chain_config.coin_decimals()), + rewards: RpcAmountOut::from_amount(data.staker_rewards(), chain_config.coin_decimals()), + vrf_public_key: RpcAddress::new(chain_config, data.vrf_public_key().clone())?, + decommission_key: RpcAddress::new( + chain_config, + data.decommission_destination().clone(), + )?, + margin_ratio_per_thousand: data.margin_ratio_per_thousand().to_percentage_str(), + cost_per_block: RpcAmountOut::from_amount( + data.cost_per_block(), + chain_config.coin_decimals(), + ), + }; + Ok(result) + } +} + #[derive(Debug, Clone, serde::Serialize, rpc_description::HasValueHint)] pub struct RpcStakePoolData { pledge: RpcAmountOut, diff --git a/common/src/chain/tokens/rpc.rs b/common/src/chain/tokens/rpc.rs index b704404ee9..0585727656 100644 --- a/common/src/chain/tokens/rpc.rs +++ b/common/src/chain/tokens/rpc.rs @@ -51,6 +51,13 @@ impl RPCTokenInfo { Self::NonFungibleToken(_) => 0, } } + + pub fn token_ticker(&self) -> Vec { + match self { + Self::FungibleToken(info) => info.token_ticker.clone().into_bytes(), + Self::NonFungibleToken(info) => info.metadata.ticker.clone().into_bytes(), + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, HasValueHint)] diff --git a/common/src/chain/transaction/partially_signed_transaction.rs b/common/src/chain/transaction/partially_signed_transaction.rs index 39aa84caf5..e3b215cce4 100644 --- a/common/src/chain/transaction/partially_signed_transaction.rs +++ b/common/src/chain/transaction/partially_signed_transaction.rs @@ -17,16 +17,27 @@ use super::{ signature::{inputsig::InputWitness, Signable, Transactable}, Destination, Transaction, TxOutput, }; -use crate::chain::{SignedTransaction, TransactionCreationError, TxInput}; +use crate::{ + chain::{SignedTransaction, TransactionCreationError, TxInput}, + primitives::Amount, +}; use serialization::{Decode, Encode}; use utils::ensure; +/// Additional info for UTXOs +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] +pub enum UtxoAdditionalInfo { + TokenInfo { num_decimals: u8, ticker: Vec }, + PoolInfo { staker_balance: Amount }, + NoAdditionalInfo, +} + #[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] pub struct PartiallySignedTransaction { tx: Transaction, witnesses: Vec>, - input_utxos: Vec>, + input_utxos: Vec>, destinations: Vec>, } @@ -34,7 +45,7 @@ impl PartiallySignedTransaction { pub fn new( tx: Transaction, witnesses: Vec>, - input_utxos: Vec>, + input_utxos: Vec>, destinations: Vec>, ) -> Result { ensure!( @@ -73,7 +84,7 @@ impl PartiallySignedTransaction { self.tx } - pub fn input_utxos(&self) -> &[Option] { + pub fn input_utxos(&self) -> &[Option<(TxOutput, UtxoAdditionalInfo)>] { self.input_utxos.as_ref() } diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 85fbb5ed16..f8cc2a99f4 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -1086,62 +1086,38 @@ impl Account { self.output_cache.pool_data(pool_id).is_ok() } - pub fn tx_to_partially_signed_tx( + pub fn find_account_destination( &self, - tx: Transaction, - median_time: BlockTimestamp, - ) -> WalletResult { - let current_block_info = BlockInfo { - height: self.account_info.best_block_height(), - timestamp: median_time, - }; - - let (input_utxos, destinations) = tx - .inputs() - .iter() - .map(|tx_inp| match tx_inp { - TxInput::Utxo(outpoint) => { - // find utxo from cache - self.find_unspent_utxo_with_destination(outpoint, current_block_info) - .map(|(out, dest)| (Some(out), Some(dest))) - } - TxInput::Account(acc_outpoint) => { - // find delegation destination - match acc_outpoint.account() { - AccountSpending::DelegationBalance(delegation_id, _) => self - .output_cache - .delegation_data(delegation_id) - .map(|data| (None, Some(data.destination.clone()))) - .ok_or(WalletError::DelegationNotFound(*delegation_id)), - } - } - TxInput::AccountCommand(_, cmd) => { - // find authority of the token - match cmd { - AccountCommand::MintTokens(token_id, _) - | AccountCommand::UnmintTokens(token_id) - | AccountCommand::LockTokenSupply(token_id) - | AccountCommand::ChangeTokenAuthority(token_id, _) - | AccountCommand::FreezeToken(token_id, _) - | AccountCommand::UnfreezeToken(token_id) => self - .output_cache - .token_data(token_id) - .map(|data| (None, Some(data.authority.clone()))) - .ok_or(WalletError::UnknownTokenId(*token_id)), - // TODO(orders) - AccountCommand::ConcludeOrder(_) => unimplemented!(), - AccountCommand::FillOrder(_, _, _) => unimplemented!(), - } - } - }) - .collect::>>()? - .into_iter() - .unzip(); + acc_outpoint: &AccountOutPoint, + ) -> WalletResult { + match acc_outpoint.account() { + AccountSpending::DelegationBalance(delegation_id, _) => self + .output_cache + .delegation_data(delegation_id) + .map(|data| data.destination.clone()) + .ok_or(WalletError::DelegationNotFound(*delegation_id)), + } + } - let num_inputs = tx.inputs().len(); - let ptx = - PartiallySignedTransaction::new(tx, vec![None; num_inputs], input_utxos, destinations)?; - Ok(ptx) + pub fn find_account_command_destination( + &self, + cmd: &AccountCommand, + ) -> WalletResult { + match cmd { + AccountCommand::MintTokens(token_id, _) + | AccountCommand::UnmintTokens(token_id) + | AccountCommand::LockTokenSupply(token_id) + | AccountCommand::ChangeTokenAuthority(token_id, _) + | AccountCommand::FreezeToken(token_id, _) + | AccountCommand::UnfreezeToken(token_id) => self + .output_cache + .token_data(token_id) + .map(|data| data.authority.clone()) + .ok_or(WalletError::UnknownTokenId(*token_id)), + // TODO(orders) + AccountCommand::ConcludeOrder(_) => unimplemented!(), + AccountCommand::FillOrder(_, _, _) => unimplemented!(), + } } pub fn find_unspent_utxo_with_destination( diff --git a/wallet/src/send_request/mod.rs b/wallet/src/send_request/mod.rs index 7e8d2bdeb8..80d9f878f1 100644 --- a/wallet/src/send_request/mod.rs +++ b/wallet/src/send_request/mod.rs @@ -18,7 +18,7 @@ use std::mem::take; use common::address::Address; use common::chain::output_value::OutputValue; -use common::chain::partially_signed_transaction::PartiallySignedTransaction; +use common::chain::partially_signed_transaction::{PartiallySignedTransaction, UtxoAdditionalInfo}; use common::chain::stakelock::StakePoolData; use common::chain::timelock::OutputTimeLock::ForBlockCount; use common::chain::tokens::{Metadata, TokenId, TokenIssuance}; @@ -283,9 +283,13 @@ impl SendRequest { let num_inputs = self.inputs.len(); let tx = Transaction::new(self.flags, self.inputs, self.outputs)?; let destinations = self.destinations.into_iter().map(Some).collect(); + let utxos = self + .utxos + .into_iter() + .map(|utxo| utxo.map(|utxo| (utxo, UtxoAdditionalInfo::NoAdditionalInfo))) + .collect(); - let ptx = - PartiallySignedTransaction::new(tx, vec![None; num_inputs], self.utxos, destinations)?; + let ptx = PartiallySignedTransaction::new(tx, vec![None; num_inputs], utxos, destinations)?; Ok(ptx) } } diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index 1e66181efe..b8af4060e0 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -243,7 +243,8 @@ impl Signer for SoftwareSigner { Vec, Vec, )> { - let inputs_utxo_refs: Vec<_> = ptx.input_utxos().iter().map(|u| u.as_ref()).collect(); + let inputs_utxo_refs: Vec<_> = + ptx.input_utxos().iter().map(|u| u.as_ref().map(|(x, _)| x)).collect(); let (witnesses, prev_statuses, new_statuses) = ptx .witnesses() diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index 4571f7548e..ead8639b53 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -242,7 +242,8 @@ impl Signer for TrezorSigner { .mintlayer_sign_tx(inputs, outputs, utxos) .map_err(TrezorError::DeviceError)?; - let inputs_utxo_refs: Vec<_> = ptx.input_utxos().iter().map(|u| u.as_ref()).collect(); + let inputs_utxo_refs: Vec<_> = + ptx.input_utxos().iter().map(|u| u.as_ref().map(|(x, _)| x)).collect(); let (witnesses, prev_statuses, new_statuses) = ptx .witnesses() @@ -473,7 +474,7 @@ fn to_trezor_input_msgs( .map(|((inp, utxo), dest)| match (inp, utxo, dest) { (TxInput::Utxo(outpoint), Some(utxo), Some(dest)) => Ok(to_trezor_utxo_input( outpoint, - utxo, + &utxo.0, chain_config, dest, key_chain, @@ -717,7 +718,7 @@ fn to_trezor_utxo_msgs( OutPointSourceId::Transaction(id) => id.to_hash().0, OutPointSourceId::BlockReward(id) => id.to_hash().0, }; - let out = to_trezor_output_msg(chain_config, utxo); + let out = to_trezor_output_msg(chain_config, &utxo.0); map.entry(id).or_default().insert(outpoint.output_index(), out); } (TxInput::Utxo(_), None) => unimplemented!("missing utxo"), diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 1be56d92e0..ab3039da25 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -20,8 +20,8 @@ use std::sync::Arc; use crate::account::transaction_list::TransactionList; use crate::account::TxInfo; use crate::account::{ - currency_grouper::Currency, CurrentFeeRate, DelegationData, PoolData, TransactionToSign, - UnconfirmedTokenInfo, UtxoSelectorError, + currency_grouper::Currency, CurrentFeeRate, DelegationData, PoolData, UnconfirmedTokenInfo, + UtxoSelectorError, }; use crate::key_chain::{ make_account_path, make_path_to_vrf_key, AccountKeyChainImplSoftware, KeyChainError, @@ -47,8 +47,9 @@ use common::chain::tokens::{ make_token_id, IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance, }; use common::chain::{ - AccountNonce, Block, ChainConfig, DelegationId, Destination, GenBlock, PoolId, - SignedTransaction, Transaction, TransactionCreationError, TxInput, TxOutput, UtxoOutPoint, + AccountCommand, AccountNonce, AccountOutPoint, Block, ChainConfig, DelegationId, Destination, + GenBlock, PoolId, SignedTransaction, Transaction, TransactionCreationError, TxInput, TxOutput, + UtxoOutPoint, }; use common::primitives::id::{hash_encoded, WithId}; use common::primitives::{Amount, BlockHeight, Id, H256}; @@ -1025,7 +1026,7 @@ where let ptx = signer.sign_tx(ptx, account.key_chain(), db_tx).map(|(ptx, _, _)| ptx)?; let inputs_utxo_refs: Vec<_> = - ptx.input_utxos().iter().map(|u| u.as_ref()).collect(); + ptx.input_utxos().iter().map(|u| u.as_ref().map(|(x, _)| x)).collect(); let is_fully_signed = ptx.destinations().iter().enumerate().zip(ptx.witnesses()).all( |((i, destination), witness)| match (witness, destination) { @@ -1137,6 +1138,18 @@ where Ok(utxos) } + pub fn find_account_destination(&self, acc_outpoint: &AccountOutPoint) -> Option { + self.accounts + .values() + .find_map(|acc| acc.find_account_destination(acc_outpoint).ok()) + } + + pub fn find_account_command_destination(&self, cmd: &AccountCommand) -> Option { + self.accounts + .values() + .find_map(|acc| acc.find_account_command_destination(cmd).ok()) + } + pub fn find_unspent_utxo_with_destination( &self, outpoint: &UtxoOutPoint, @@ -1755,22 +1768,15 @@ where pub fn sign_raw_transaction( &mut self, account_index: U31, - tx: TransactionToSign, + ptx: PartiallySignedTransaction, ) -> WalletResult<( PartiallySignedTransaction, Vec, Vec, )> { - let latest_median_time = self.latest_median_time; self.for_account_rw_unlocked( account_index, |account, db_tx, chain_config, signer_provider| { - let ptx = match tx { - TransactionToSign::Partial(ptx) => ptx, - TransactionToSign::Tx(tx) => { - account.tx_to_partially_signed_tx(tx, latest_median_time)? - } - }; let mut signer = signer_provider.provide(Arc::new(chain_config.clone()), account_index); diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index f2a520f16e..89feff124c 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -35,6 +35,7 @@ use common::{ block::{consensus_data::PoSData, timestamp::BlockTimestamp, BlockReward, ConsensusData}, config::{create_mainnet, create_regtest, Builder, ChainType}, output_value::OutputValue, + partially_signed_transaction::UtxoAdditionalInfo, signature::inputsig::InputWitness, timelock::OutputTimeLock, tokens::{RPCIsTokenFrozen, TokenData, TokenIssuanceV0, TokenIssuanceV1}, @@ -4103,7 +4104,8 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let (addr, _) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let utxo = make_address_output(addr.clone(), block1_amount); let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -4134,8 +4136,16 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { // remove the signatures and try to sign it again let tx = stake_pool_transaction.transaction().clone(); + let inps = tx.inputs().len(); + let ptx = PartiallySignedTransaction::new( + tx, + vec![None; inps], + vec![Some((utxo, UtxoAdditionalInfo::NoAdditionalInfo))], + vec![Some(addr.into_object())], + ) + .unwrap(); let stake_pool_transaction = wallet - .sign_raw_transaction(acc_0_index, TransactionToSign::Tx(tx)) + .sign_raw_transaction(acc_0_index, ptx) .unwrap() .0 .into_signed_tx() @@ -4167,20 +4177,14 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { // Try to sign decommission request with wrong account let sign_from_acc0_res = wallet - .sign_raw_transaction( - acc_0_index, - TransactionToSign::Partial(decommission_partial_tx.clone()), - ) + .sign_raw_transaction(acc_0_index, decommission_partial_tx.clone()) .unwrap() .0; // the tx is still not fully signed assert!(!sign_from_acc0_res.all_signatures_available()); let signed_tx = wallet - .sign_raw_transaction( - acc_1_index, - TransactionToSign::Partial(decommission_partial_tx), - ) + .sign_raw_transaction(acc_1_index, decommission_partial_tx) .unwrap() .0 .into_signed_tx() @@ -4263,10 +4267,7 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { // sign the tx with cold wallet let partially_signed_transaction = cold_wallet - .sign_raw_transaction( - DEFAULT_ACCOUNT_INDEX, - TransactionToSign::Partial(decommission_partial_tx), - ) + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, decommission_partial_tx) .unwrap() .0; assert!(partially_signed_transaction.all_signatures_available()); @@ -4274,10 +4275,7 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { // sign it with the hot wallet should leave the signatures in place even if it can't find the // destinations for the inputs let partially_signed_transaction = hot_wallet - .sign_raw_transaction( - DEFAULT_ACCOUNT_INDEX, - TransactionToSign::Partial(partially_signed_transaction), - ) + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, partially_signed_transaction) .unwrap() .0; assert!(partially_signed_transaction.all_signatures_available()); @@ -4436,10 +4434,7 @@ fn sign_send_request_cold_wallet(#[case] seed: Seed) { // Try to sign request with the hot wallet let tx = hot_wallet - .sign_raw_transaction( - DEFAULT_ACCOUNT_INDEX, - TransactionToSign::Partial(send_req.clone()), - ) + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, send_req.clone()) .unwrap() .0; // the tx is not fully signed @@ -4447,7 +4442,7 @@ fn sign_send_request_cold_wallet(#[case] seed: Seed) { // sign the tx with cold wallet let signed_tx = cold_wallet - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, TransactionToSign::Partial(send_req)) + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, send_req) .unwrap() .0 .into_signed_tx() @@ -4669,41 +4664,40 @@ fn test_add_standalone_multisig(#[case] seed: Seed) { )], ) .unwrap(); + let spend_multisig_tx = PartiallySignedTransaction::new( + spend_multisig_tx, + vec![None; 1], + vec![Some(( + tx.outputs()[0].clone(), + UtxoAdditionalInfo::NoAdditionalInfo, + ))], + vec![Some(multisig_address.as_object().clone())], + ) + .unwrap(); // sign it with wallet1 - let (ptx, _, statuses) = wallet1 - .sign_raw_transaction( - DEFAULT_ACCOUNT_INDEX, - TransactionToSign::Tx(spend_multisig_tx), - ) - .unwrap(); + let (ptx, _, statuses) = + wallet1.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, spend_multisig_tx).unwrap(); // check it is still not fully signed assert!(ptx.all_signatures_available()); assert!(!statuses.iter().all(|s| *s == SignatureStatus::FullySigned)); // try to sign it with wallet1 again - let (ptx, _, statuses) = wallet1 - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, TransactionToSign::Partial(ptx)) - .unwrap(); + let (ptx, _, statuses) = wallet1.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx).unwrap(); // check it is still not fully signed assert!(ptx.all_signatures_available()); assert!(!statuses.iter().all(|s| *s == SignatureStatus::FullySigned)); // try to sign it with wallet2 but wallet2 does not have the multisig added as standalone - let ptx = wallet2 - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, TransactionToSign::Partial(ptx)) - .unwrap() - .0; + let ptx = wallet2.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx).unwrap().0; // add it to wallet2 as well wallet2.add_standalone_multisig(DEFAULT_ACCOUNT_INDEX, challenge, None).unwrap(); // now we can sign it - let (ptx, _, statuses) = wallet2 - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, TransactionToSign::Partial(ptx)) - .unwrap(); + let (ptx, _, statuses) = wallet2.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx).unwrap(); // now it is fully signed assert!(ptx.all_signatures_available()); diff --git a/wallet/wallet-controller/Cargo.toml b/wallet/wallet-controller/Cargo.toml index 2acc7e72e0..5e45de73d7 100644 --- a/wallet/wallet-controller/Cargo.toml +++ b/wallet/wallet-controller/Cargo.toml @@ -22,6 +22,7 @@ randomness = { path = "../../randomness" } serialization = { path = "../../serialization" } storage = { path = "../../storage" } storage-inmemory = { path = "../../storage/inmemory" } +pos-accounting = { path = "../../pos-accounting" } utils = { path = "../../utils" } utils-networking = { path = "../../utils/networking" } wallet = { path = ".." } diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 47bd75b4e0..f5c2b99b3c 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -15,6 +15,7 @@ //! Common code for wallet UI applications +mod helpers; pub mod mnemonic; pub mod read; mod sync; @@ -28,11 +29,8 @@ use blockprod::BlockProductionError; use chainstate::tx_verifier::{ self, error::ScriptError, input_check::signature_only_check::SignatureOnlyVerifiable, }; -use futures::{ - never::Never, - stream::{FuturesOrdered, FuturesUnordered}, - TryStreamExt, -}; +use futures::{never::Never, stream::FuturesOrdered, TryStreamExt}; +use helpers::{fetch_token_info, fetch_utxo, fetch_utxo_exra_info, into_balances}; use node_comm::rpc_client::ColdWalletClient; use std::{ collections::{BTreeMap, BTreeSet}, @@ -56,14 +54,13 @@ use common::{ address::AddressError, chain::{ block::timestamp::BlockTimestamp, - partially_signed_transaction::PartiallySignedTransaction, + partially_signed_transaction::{PartiallySignedTransaction, UtxoAdditionalInfo}, signature::{inputsig::InputWitness, DestinationSigError, Transactable}, tokens::{RPCTokenInfo, TokenId}, Block, ChainConfig, Destination, GenBlock, PoolId, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{ - amount::RpcAmountOut, time::{get_time, Time}, Amount, BlockHeight, Id, Idable, }, @@ -952,9 +949,11 @@ where &self, ptx: PartiallySignedTransaction, ) -> Result> { - let input_utxos: Vec<_> = ptx.input_utxos().iter().flatten().cloned().collect(); + let input_utxos: Vec<_> = + ptx.input_utxos().iter().flatten().map(|(utxo, _)| utxo).cloned().collect(); let fees = self.get_fees(&input_utxos, ptx.tx().outputs()).await?; - let inputs_utxos_refs: Vec<_> = ptx.input_utxos().iter().map(|out| out.as_ref()).collect(); + let inputs_utxos_refs: Vec<_> = + ptx.input_utxos().iter().map(|out| out.as_ref().map(|(utxo, _)| utxo)).collect(); let signature_statuses: Vec<_> = ptx .witnesses() .iter() @@ -1082,6 +1081,7 @@ where .collect::, WalletError>>() .map_err(ControllerError::WalletError)?; + let input_utxos = self.fetch_utxos_extra_info(input_utxos).await?; let tx = PartiallySignedTransaction::new( tx, vec![None; num_inputs], @@ -1161,29 +1161,24 @@ where &self, inputs: &[UtxoOutPoint], ) -> Result, ControllerError> { - let tasks: FuturesOrdered<_> = inputs.iter().map(|input| self.fetch_utxo(input)).collect(); + let tasks: FuturesOrdered<_> = inputs + .iter() + .map(|input| fetch_utxo(&self.rpc_client, input, &self.wallet)) + .collect(); let input_utxos: Vec = tasks.try_collect().await?; Ok(input_utxos) } - async fn fetch_utxo(&self, input: &UtxoOutPoint) -> Result> { - // search locally for the unspent utxo - if let Some(out) = match &self.wallet { - WalletType2::Software(w) => w.find_unspent_utxo_with_destination(input), - #[cfg(feature = "trezor")] - WalletType2::Trezor(w) => w.find_unspent_utxo_with_destination(input), - } { - return Ok(out.0); - } - - // check the chainstate - self.rpc_client - .get_utxo(input.clone()) - .await - .map_err(ControllerError::NodeCallError)? - .ok_or(ControllerError::WalletError(WalletError::CannotFindUtxo( - input.clone(), - ))) + async fn fetch_utxos_extra_info( + &self, + inputs: Vec, + ) -> Result, ControllerError> { + let tasks: FuturesOrdered<_> = inputs + .into_iter() + .map(|input| fetch_utxo_exra_info(&self.rpc_client, input)) + .collect(); + let input_utxos: Vec<(TxOutput, UtxoAdditionalInfo)> = tasks.try_collect().await?; + Ok(input_utxos) } async fn fetch_opt_utxo( @@ -1191,7 +1186,7 @@ where input: &TxInput, ) -> Result, ControllerError> { match input { - TxInput::Utxo(utxo) => self.fetch_utxo(utxo).await.map(Some), + TxInput::Utxo(utxo) => fetch_utxo(&self.rpc_client, utxo, &self.wallet).await.map(Some), TxInput::Account(_) => Ok(None), TxInput::AccountCommand(_, _) => Ok(None), } @@ -1274,43 +1269,3 @@ where } } } - -pub async fn fetch_token_info( - rpc_client: &T, - token_id: TokenId, -) -> Result> { - rpc_client - .get_token_info(token_id) - .await - .map_err(ControllerError::NodeCallError)? - .ok_or(ControllerError::WalletError(WalletError::UnknownTokenId( - token_id, - ))) -} - -pub async fn into_balances( - rpc_client: &T, - chain_config: &ChainConfig, - mut balances: BTreeMap, -) -> Result> { - let coins = balances.remove(&Currency::Coin).unwrap_or(Amount::ZERO); - let coins = RpcAmountOut::from_amount_no_padding(coins, chain_config.coin_decimals()); - - let tasks: FuturesUnordered<_> = balances - .into_iter() - .map(|(currency, amount)| async move { - let token_id = match currency { - Currency::Coin => panic!("Removed just above"), - Currency::Token(token_id) => token_id, - }; - - fetch_token_info(rpc_client, token_id).await.map(|info| { - let decimals = info.token_number_of_decimals(); - let amount = RpcAmountOut::from_amount_no_padding(amount, decimals); - (token_id, amount) - }) - }) - .collect(); - - Ok(Balances::new(coins, tasks.try_collect().await?)) -} diff --git a/wallet/wallet-controller/src/sync/tests/mod.rs b/wallet/wallet-controller/src/sync/tests/mod.rs index e3c35c83fe..754dc0f4f8 100644 --- a/wallet/wallet-controller/src/sync/tests/mod.rs +++ b/wallet/wallet-controller/src/sync/tests/mod.rs @@ -40,6 +40,7 @@ use node_comm::{ rpc_client::NodeRpcError, }; use p2p_types::{bannable_address::BannableAddress, socket_address::SocketAddress}; +use pos_accounting::PoolData; use randomness::{seq::IteratorRandom, CryptoRng, Rng}; use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; @@ -275,6 +276,10 @@ impl NodeInterface for MockNode { unreachable!() } + async fn get_pool_data(&self, _pool_id: PoolId) -> Result, Self::Error> { + unreachable!() + } + async fn get_delegation_share( &self, _pool_id: PoolId, diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index 5a96006774..d05d486640 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -58,7 +58,11 @@ use wallet_types::{ with_locked::WithLocked, }; -use crate::{into_balances, types::Balances, ControllerConfig, ControllerError, WalletType2}; +use crate::{ + helpers::{fetch_token_info, fetch_utxo, into_balances, tx_to_partially_signed_tx}, + types::Balances, + ControllerConfig, ControllerError, WalletType2, +}; pub struct SyncedController<'a, T, W, B: storage::Backend + 'static> { wallet: &'a mut WalletType2, @@ -96,25 +100,14 @@ where } } - pub async fn get_token_info( - &self, - token_id: TokenId, - ) -> Result> { - self.rpc_client - .get_token_info(token_id) - .await - .map_err(ControllerError::NodeCallError)? - .ok_or(ControllerError::WalletError(WalletError::UnknownTokenId( - token_id, - ))) - } - async fn fetch_token_infos( &self, tokens: BTreeSet, ) -> Result, ControllerError> { - let tasks: FuturesUnordered<_> = - tokens.into_iter().map(|token_id| self.get_token_info(token_id)).collect(); + let tasks: FuturesUnordered<_> = tokens + .into_iter() + .map(|token_id| fetch_token_info(&self.rpc_client, token_id)) + .collect(); tasks.try_collect().await } @@ -158,7 +151,7 @@ where let mut result = vec![]; for utxo in input_utxos { if let Some(token_id) = utxo.2 { - let token_info = self.get_token_info(token_id).await?; + let token_info = fetch_token_info(&self.rpc_client, token_id).await?; let ok_to_use = match token_info { RPCTokenInfo::FungibleToken(token_info) => match &self.wallet { @@ -788,7 +781,7 @@ where ) -> Result<(PartiallySignedTransaction, Balances), ControllerError> { let output = make_address_output(address, amount); - let utxo_output = self.fetch_utxo(&selected_utxo).await?; + let utxo_output = fetch_utxo(&self.rpc_client, &selected_utxo, self.wallet).await?; let change_address = if let Some(change_address) = change_address { change_address } else { @@ -1141,7 +1134,7 @@ where /// Tries to sign any unsigned inputs of a raw or partially signed transaction with the private /// keys in this wallet. - pub fn sign_raw_transaction( + pub async fn sign_raw_transaction( &mut self, tx: TransactionToSign, ) -> Result< @@ -1152,10 +1145,17 @@ where ), ControllerError, > { + let ptx = match tx { + TransactionToSign::Partial(ptx) => ptx, + TransactionToSign::Tx(tx) => { + tx_to_partially_signed_tx(&self.rpc_client, self.wallet, tx).await? + } + }; + match &mut self.wallet { - WalletType2::Software(w) => w.sign_raw_transaction(self.account_index, tx), + WalletType2::Software(w) => w.sign_raw_transaction(self.account_index, ptx), #[cfg(feature = "trezor")] - WalletType2::Trezor(w) => w.sign_raw_transaction(self.account_index, tx), + WalletType2::Trezor(w) => w.sign_raw_transaction(self.account_index, ptx), } .map_err(ControllerError::WalletError) } @@ -1326,16 +1326,4 @@ where let tx_id = self.broadcast_to_mempool_if_needed(tx).await?; Ok((tx_id, id)) } - - async fn fetch_utxo(&self, input: &UtxoOutPoint) -> Result> { - let utxo = self - .rpc_client - .get_utxo(input.clone()) - .await - .map_err(ControllerError::NodeCallError)?; - - utxo.ok_or(ControllerError::WalletError(WalletError::CannotFindUtxo( - input.clone(), - ))) - } } diff --git a/wallet/wallet-node-client/Cargo.toml b/wallet/wallet-node-client/Cargo.toml index 8970753a39..8d8741ee86 100644 --- a/wallet/wallet-node-client/Cargo.toml +++ b/wallet/wallet-node-client/Cargo.toml @@ -17,6 +17,7 @@ logging = { path = "../../logging" } mempool = { path = "../../mempool" } node-lib = { path = "../../node-lib" } p2p = { path = "../../p2p" } +pos-accounting = { path = "../../pos-accounting" } rpc = { path = "../../rpc" } serialization = { path = "../../serialization" } subsystem = { path = "../../subsystem" } diff --git a/wallet/wallet-node-client/src/handles_client/mod.rs b/wallet/wallet-node-client/src/handles_client/mod.rs index 515b3d6b4e..89c5b6d9fd 100644 --- a/wallet/wallet-node-client/src/handles_client/mod.rs +++ b/wallet/wallet-node-client/src/handles_client/mod.rs @@ -35,6 +35,7 @@ use p2p::{ types::{bannable_address::BannableAddress, peer_id::PeerId, socket_address::SocketAddress}, P2pHandle, }; +use pos_accounting::PoolData; use serialization::hex::HexError; use utils_networking::IpOrSocketAddress; use wallet_types::wallet_type::WalletType; @@ -195,6 +196,11 @@ impl NodeInterface for WalletHandlesClient { Ok(result) } + async fn get_pool_data(&self, pool_id: PoolId) -> Result, Self::Error> { + let result = self.chainstate.call(move |this| this.get_stake_pool_data(pool_id)).await??; + Ok(result) + } + async fn get_delegation_share( &self, pool_id: PoolId, diff --git a/wallet/wallet-node-client/src/node_traits.rs b/wallet/wallet-node-client/src/node_traits.rs index 681f373747..b7e3f56682 100644 --- a/wallet/wallet-node-client/src/node_traits.rs +++ b/wallet/wallet-node-client/src/node_traits.rs @@ -30,6 +30,7 @@ use crypto::ephemeral_e2e::EndToEndPublicKey; use mempool::{tx_accumulator::PackingStrategy, tx_options::TxOptionsOverrides, FeeRate}; use p2p::types::{bannable_address::BannableAddress, socket_address::SocketAddress}; pub use p2p::{interface::types::ConnectedPeer, types::peer_id::PeerId}; +use pos_accounting::PoolData; use utils_networking::IpOrSocketAddress; use wallet_types::wallet_type::WalletType; @@ -65,6 +66,7 @@ pub trait NodeInterface { ) -> Result, BlockHeight)>, Self::Error>; async fn get_stake_pool_balance(&self, pool_id: PoolId) -> Result, Self::Error>; async fn get_staker_balance(&self, pool_id: PoolId) -> Result, Self::Error>; + async fn get_pool_data(&self, pool_id: PoolId) -> Result, Self::Error>; async fn get_delegation_share( &self, pool_id: PoolId, diff --git a/wallet/wallet-node-client/src/rpc_client/client_impl.rs b/wallet/wallet-node-client/src/rpc_client/client_impl.rs index 2461810333..ca428faf92 100644 --- a/wallet/wallet-node-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-node-client/src/rpc_client/client_impl.rs @@ -24,7 +24,7 @@ use common::{ Block, DelegationId, GenBlock, PoolId, SignedTransaction, Transaction, TxOutput, UtxoOutPoint, }, - primitives::{time::Time, Amount, BlockHeight, Id}, + primitives::{per_thousand::PerThousand, time::Time, Amount, BlockHeight, Id}, }; use consensus::GenerateBlockInputData; use crypto::ephemeral_e2e::EndToEndPublicKey; @@ -36,6 +36,7 @@ use p2p::{ rpc::P2pRpcClient, types::{bannable_address::BannableAddress, peer_id::PeerId, socket_address::SocketAddress}, }; +use pos_accounting::PoolData; use serialization::hex_encoded::HexEncoded; use utils_networking::IpOrSocketAddress; use wallet_types::wallet_type::WalletType; @@ -141,6 +142,24 @@ impl NodeInterface for NodeRpcClient { .map_err(NodeRpcError::ResponseError) } + async fn get_pool_data(&self, pool_id: PoolId) -> Result, Self::Error> { + let pool_address = Address::new(&self.chain_config, pool_id)?; + ChainstateRpcClient::pool_data(&self.http_client, pool_address.into_string()) + .await + .map_err(NodeRpcError::ResponseError)? + .map(|d| { + Ok(PoolData::new( + d.decommission_key.decode_object(&self.chain_config)?, + d.pledge.amount(), + d.rewards.amount(), + d.vrf_public_key.decode_object(&self.chain_config)?, + PerThousand::from_decimal_str(&d.margin_ratio_per_thousand)?, + d.cost_per_block.amount(), + )) + }) + .transpose() + } + async fn get_delegation_share( &self, pool_id: PoolId, diff --git a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs index 46042178ac..6a74c59f58 100644 --- a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs +++ b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs @@ -31,6 +31,7 @@ use p2p::{ interface::types::ConnectedPeer, types::{bannable_address::BannableAddress, socket_address::SocketAddress, PeerId}, }; +use pos_accounting::PoolData; use utils_networking::IpOrSocketAddress; use wallet_types::wallet_type::WalletType; @@ -118,6 +119,10 @@ impl NodeInterface for ColdWalletClient { Err(ColdWalletRpcError::NotAvailable) } + async fn get_pool_data(&self, _pool_id: PoolId) -> Result, Self::Error> { + Err(ColdWalletRpcError::NotAvailable) + } + async fn get_delegation_share( &self, _pool_id: PoolId, diff --git a/wallet/wallet-node-client/src/rpc_client/mod.rs b/wallet/wallet-node-client/src/rpc_client/mod.rs index 3e646cc750..c24021e9b5 100644 --- a/wallet/wallet-node-client/src/rpc_client/mod.rs +++ b/wallet/wallet-node-client/src/rpc_client/mod.rs @@ -20,6 +20,7 @@ use std::sync::Arc; use common::address::AddressError; use common::chain::ChainConfig; +use common::primitives::per_thousand::PerThousandParseError; use rpc::new_http_client; use rpc::ClientError; use rpc::RpcAuthData; @@ -39,6 +40,8 @@ pub enum NodeRpcError { ResponseError(ClientError), #[error("Address error: {0}")] AddressError(#[from] AddressError), + #[error("PerThousand parse error: {0}")] + PerThousandParseError(#[from] PerThousandParseError), } #[derive(Clone, Debug)] diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index 274c0e889f..1a89ae905f 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -832,6 +832,7 @@ where .synced_controller(account_index, config) .await? .sign_raw_transaction(tx_to_sign) + .await .map_err(RpcError::Controller) }) })