diff --git a/CHANGELOG.md b/CHANGELOG.md index e76efa9d3..7301d2316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ is normally done for misbehaving peers) and the node won't try connecting to it again.\ Also, the peer will be sent an appropriate `WillDisconnect` message prior to disconnection. + - Wallet CLI and RPC: the commands `account-utxos` and `standalone-multisig-utxos` and their RPC + counterparts now return correct decimal amounts for tokens with non-default number of decimals. + ## [1.2.0] - 2025-10-27 ### Changed diff --git a/chainstate/src/rpc/mod.rs b/chainstate/src/rpc/mod.rs index 85f8be833..c693c1316 100644 --- a/chainstate/src/rpc/mod.rs +++ b/chainstate/src/rpc/mod.rs @@ -18,18 +18,18 @@ mod types; use std::{ + collections::BTreeMap, convert::Infallible, io::{Read, Write}, num::NonZeroUsize, sync::Arc, }; -use self::types::{block::RpcBlock, event::RpcEvent}; -use crate::{Block, BlockSource, ChainInfo, GenBlock}; use chainstate_types::BlockIndex; use common::{ address::{dehexify::to_dehexified_json, Address}, chain::{ + output_values_holder::collect_token_v1_ids_from_output_values_holder, tokens::{RPCTokenInfo, TokenId}, ChainConfig, DelegationId, Destination, OrderId, PoolId, RpcOrderInfo, TxOutput, }, @@ -37,10 +37,20 @@ use common::{ }; use rpc::{subscription, RpcResult}; use serialization::hex_encoded::HexEncoded; + +use crate::{ + chainstate_interface::ChainstateInterface, Block, BlockSource, ChainInfo, ChainstateError, + GenBlock, +}; + +use self::types::{block::RpcBlock, event::RpcEvent}; + pub use types::{ input::RpcUtxoOutpoint, output::{RpcOutputValueIn, RpcOutputValueOut, RpcTxOutput}, signed_transaction::RpcSignedTransaction, + token_decimals_provider::{TokenDecimals, TokenDecimalsProvider}, + RpcTypeError, }; #[rpc::describe] @@ -231,15 +241,33 @@ impl ChainstateRpcServer for super::ChainstateHandle { .await, )?; - let rpc_blk: Option = both - .map(|(block, block_index)| { - rpc::handle_result(RpcBlock::new(&chain_config, block, block_index)) - }) - .transpose()?; - - let result = rpc_blk.map(|rpc_blk| to_dehexified_json(&chain_config, rpc_blk)).transpose(); - - rpc::handle_result(result) + if let Some((block, block_index)) = both { + let token_ids = collect_token_v1_ids_from_output_values_holder(&block); + let mut token_decimals = BTreeMap::new(); + + // TODO replace this loop with a single ChainstateInterface function call obtaining + // all infos at once (when the function is implemented). + for token_id in token_ids { + let token_info: RPCTokenInfo = rpc::handle_result( + self.call(move |this| get_existing_token_info_for_rpc(this, token_id)).await, + )?; + token_decimals.insert( + token_id, + TokenDecimals(token_info.token_number_of_decimals()), + ); + } + + let rpc_block: RpcBlock = rpc::handle_result(RpcBlock::new( + &chain_config, + &token_decimals, + block, + block_index, + ))?; + let json = rpc::handle_result(to_dehexified_json(&chain_config, rpc_block))?; + Ok(Some(json)) + } else { + Ok(None) + } } async fn get_mainchain_blocks( @@ -469,6 +497,24 @@ where o.map_err(Into::into) } +fn get_existing_token_info_for_rpc( + chainstate: &(impl ChainstateInterface + ?Sized), + token_id: TokenId, +) -> Result { + chainstate + .get_token_info_for_rpc(token_id)? + .ok_or(LocalRpcError::MissingTokenInfo(token_id)) +} + +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +enum LocalRpcError { + #[error("Token info missing for token {0:x}")] + MissingTokenInfo(TokenId), + + #[error(transparent)] + ChainstateError(#[from] ChainstateError), +} + #[cfg(test)] mod test { use super::*; diff --git a/chainstate/src/rpc/types/account.rs b/chainstate/src/rpc/types/account.rs index 6ee0681ef..a9ff73ba0 100644 --- a/chainstate/src/rpc/types/account.rs +++ b/chainstate/src/rpc/types/account.rs @@ -14,7 +14,7 @@ // limitations under the License. use common::{ - address::{AddressError, RpcAddress}, + address::RpcAddress, chain::{ tokens::{IsTokenUnfreezable, TokenId}, AccountCommand, AccountSpending, ChainConfig, DelegationId, Destination, @@ -24,6 +24,8 @@ use common::{ }; use rpc::types::RpcHexString; +use super::RpcTypeError; + #[derive(Debug, Clone, serde::Serialize, rpc_description::HasValueHint)] #[serde(tag = "type", content = "content")] pub enum RpcAccountSpending { @@ -37,7 +39,7 @@ impl RpcAccountSpending { pub fn new( chain_config: &ChainConfig, spending: AccountSpending, - ) -> Result { + ) -> Result { let result = match spending { AccountSpending::DelegationBalance(id, amount) => { RpcAccountSpending::DelegationBalance { @@ -89,7 +91,7 @@ pub enum RpcAccountCommand { } impl RpcAccountCommand { - pub fn new(chain_config: &ChainConfig, command: &AccountCommand) -> Result { + pub fn new(chain_config: &ChainConfig, command: &AccountCommand) -> Result { let result = match command { AccountCommand::MintTokens(id, amount) => RpcAccountCommand::MintTokens { token_id: RpcAddress::new(chain_config, *id)?, @@ -155,7 +157,7 @@ impl RpcOrderAccountCommand { pub fn new( chain_config: &ChainConfig, command: &OrderAccountCommand, - ) -> Result { + ) -> Result { let result = match command { OrderAccountCommand::ConcludeOrder(order_id) => RpcOrderAccountCommand::Conclude { order_id: RpcAddress::new(chain_config, *order_id)?, diff --git a/chainstate/src/rpc/types/block.rs b/chainstate/src/rpc/types/block.rs index 4dfdd8772..8723eccb6 100644 --- a/chainstate/src/rpc/types/block.rs +++ b/chainstate/src/rpc/types/block.rs @@ -15,7 +15,6 @@ use chainstate_types::BlockIndex; use common::{ - address::AddressError, chain::{block::timestamp::BlockTimestamp, Block, ChainConfig, GenBlock}, primitives::{BlockHeight, Id, Idable}, }; @@ -23,17 +22,10 @@ use serialization::hex_encoded::HexEncoded; use super::{ block_reward::RpcBlockReward, consensus_data::RpcConsensusData, - signed_transaction::RpcSignedTransaction, + signed_transaction::RpcSignedTransaction, token_decimals_provider::TokenDecimalsProvider, + RpcTypeError, }; -#[derive(thiserror::Error, Debug)] -pub enum RpcTypeSerializationError { - #[error("Address error: {0}")] - Address(#[from] AddressError), - #[error("FromHex error: {0}")] - FromHex(#[from] hex::FromHexError), -} - #[derive(Debug, Clone, serde::Serialize)] pub struct RpcBlock { id: Id, @@ -53,15 +45,17 @@ pub struct RpcBlock { impl RpcBlock { pub fn new( chain_config: &ChainConfig, + token_decimals_provider: &impl TokenDecimalsProvider, block: Block, block_index: BlockIndex, - ) -> Result { + ) -> Result { let rpc_consensus_data = RpcConsensusData::new(chain_config, block.consensus_data())?; - let rpc_block_reward = RpcBlockReward::new(chain_config, block.block_reward())?; + let rpc_block_reward = + RpcBlockReward::new(chain_config, token_decimals_provider, block.block_reward())?; let rpc_transactions = block .transactions() .iter() - .map(|tx| RpcSignedTransaction::new(chain_config, tx.clone())) + .map(|tx| RpcSignedTransaction::new(chain_config, token_decimals_provider, tx.clone())) .collect::, _>>()?; let rpc_block = Self { diff --git a/chainstate/src/rpc/types/block_reward.rs b/chainstate/src/rpc/types/block_reward.rs index 3f5d4e72e..487a10b40 100644 --- a/chainstate/src/rpc/types/block_reward.rs +++ b/chainstate/src/rpc/types/block_reward.rs @@ -13,12 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::{ - address::AddressError, - chain::{block::BlockReward, ChainConfig}, -}; +use common::chain::{block::BlockReward, ChainConfig}; -use super::output::RpcTxOutput; +use super::{output::RpcTxOutput, token_decimals_provider::TokenDecimalsProvider, RpcTypeError}; #[derive(Debug, Clone, serde::Serialize)] pub struct RpcBlockReward { @@ -27,11 +24,17 @@ pub struct RpcBlockReward { } impl RpcBlockReward { - pub fn new(chain_config: &ChainConfig, reward: &BlockReward) -> Result { + // Note: in a real blockchain BlockReward will never reference tokens. But it's still + // possible to manually construct it this way. + pub fn new( + chain_config: &ChainConfig, + token_decimals_provider: &impl TokenDecimalsProvider, + reward: &BlockReward, + ) -> Result { let rpc_outputs = reward .outputs() .iter() - .map(|output| RpcTxOutput::new(chain_config, output.clone())) + .map(|output| RpcTxOutput::new(chain_config, token_decimals_provider, output.clone())) .collect::, _>>()?; let rpc_tx = Self { diff --git a/chainstate/src/rpc/types/consensus_data.rs b/chainstate/src/rpc/types/consensus_data.rs index cae4856bb..13585853a 100644 --- a/chainstate/src/rpc/types/consensus_data.rs +++ b/chainstate/src/rpc/types/consensus_data.rs @@ -13,15 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::str::FromStr; - use common::{ address::RpcAddress, chain::{block::ConsensusData, ChainConfig, PoolId}, }; use rpc::types::RpcHexString; -use super::{block::RpcTypeSerializationError, input::RpcTxInput}; +use super::{input::RpcTxInput, RpcTypeError}; #[derive(Debug, Clone, serde::Serialize)] #[serde(tag = "type", content = "content")] @@ -35,7 +33,7 @@ impl RpcConsensusData { pub fn new( chain_config: &ChainConfig, consensus_data: &ConsensusData, - ) -> Result { + ) -> Result { let rpc_consensus_data = match consensus_data { ConsensusData::None => RpcConsensusData::None, ConsensusData::PoW(_) => RpcConsensusData::PoW, @@ -47,16 +45,14 @@ impl RpcConsensusData { .collect::, _>>()?; let compact_target = - RpcHexString::from_str(format!("{:x}", pos_data.compact_target().0).as_str())?; + RpcHexString::from_bytes(pos_data.compact_target().0.to_be_bytes().to_vec()); - let target = RpcHexString::from_str( - format!( - "{:x}", - TryInto::::try_into(pos_data.compact_target()) - .expect("valid target") - ) - .as_str(), - )?; + let target = RpcHexString::from_bytes( + TryInto::::try_into(pos_data.compact_target()) + .expect("valid target") + .to_be_bytes() + .to_vec(), + ); RpcConsensusData::PoS { pos_data: RpcPoSData { diff --git a/chainstate/src/rpc/types/input.rs b/chainstate/src/rpc/types/input.rs index 7c278ae94..3019f4b4b 100644 --- a/chainstate/src/rpc/types/input.rs +++ b/chainstate/src/rpc/types/input.rs @@ -14,12 +14,14 @@ // limitations under the License. use common::{ - address::AddressError, chain::{ChainConfig, GenBlock, OutPointSourceId, Transaction, TxInput, UtxoOutPoint}, primitives::Id, }; -use super::account::{RpcAccountCommand, RpcAccountSpending, RpcOrderAccountCommand}; +use super::{ + account::{RpcAccountCommand, RpcAccountSpending, RpcOrderAccountCommand}, + RpcTypeError, +}; #[derive(Debug, Clone, serde::Serialize, rpc_description::HasValueHint)] #[serde(tag = "type", content = "content")] @@ -42,7 +44,7 @@ pub enum RpcTxInput { } impl RpcTxInput { - pub fn new(chain_config: &ChainConfig, input: &TxInput) -> Result { + pub fn new(chain_config: &ChainConfig, input: &TxInput) -> Result { let result = match input { TxInput::Utxo(outpoint) => match outpoint.source_id() { OutPointSourceId::Transaction(id) => RpcTxInput::Utxo { diff --git a/chainstate/src/rpc/types/mod.rs b/chainstate/src/rpc/types/mod.rs index f9adec8b9..ba4ed937f 100644 --- a/chainstate/src/rpc/types/mod.rs +++ b/chainstate/src/rpc/types/mod.rs @@ -22,3 +22,15 @@ pub mod input; pub mod output; pub mod signed_transaction; pub mod token; +pub mod token_decimals_provider; + +use common::{address::AddressError, chain::tokens::TokenId}; + +#[derive(thiserror::Error, Debug)] +pub enum RpcTypeError { + #[error("Address error: {0}")] + Address(#[from] AddressError), + + #[error("Token decimals unavailable for token {0:x}")] + TokenDecimalsUnavailable(TokenId), +} diff --git a/chainstate/src/rpc/types/output.rs b/chainstate/src/rpc/types/output.rs index 814c4487d..dbbadeac4 100644 --- a/chainstate/src/rpc/types/output.rs +++ b/chainstate/src/rpc/types/output.rs @@ -14,7 +14,7 @@ // limitations under the License. use common::{ - address::{AddressError, RpcAddress}, + address::RpcAddress, chain::{ htlc::HashedTimelockContract, output_value::OutputValue, stakelock::StakePoolData, timelock::OutputTimeLock, tokens::TokenId, ChainConfig, DelegationId, Destination, PoolId, @@ -25,7 +25,11 @@ use common::{ use crypto::vrf::VRFPublicKey; use rpc::types::RpcHexString; -use super::token::{RpcNftIssuance, RpcTokenIssuance}; +use super::{ + token::{RpcNftIssuance, RpcTokenIssuance}, + token_decimals_provider::TokenDecimalsProvider, + RpcTypeError, +}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rpc_description::HasValueHint)] #[serde(tag = "type", content = "content")] @@ -52,7 +56,11 @@ pub enum RpcOutputValueOut { } impl RpcOutputValueOut { - pub fn new(chain_config: &ChainConfig, value: OutputValue) -> Result { + pub fn new( + chain_config: &ChainConfig, + token_decimals_provider: &impl TokenDecimalsProvider, + value: OutputValue, + ) -> Result { let result = match value { OutputValue::Coin(amount) => RpcOutputValueOut::Coin { amount: RpcAmountOut::from_amount(amount, chain_config.coin_decimals()), @@ -60,7 +68,13 @@ impl RpcOutputValueOut { OutputValue::TokenV0(_) => unimplemented!(), OutputValue::TokenV1(token_id, amount) => RpcOutputValueOut::Token { id: RpcAddress::new(chain_config, token_id)?, - amount: RpcAmountOut::from_amount(amount, chain_config.coin_decimals()), + amount: RpcAmountOut::from_amount( + amount, + token_decimals_provider + .get_token_decimals(&token_id) + .ok_or(RpcTypeError::TokenDecimalsUnavailable(token_id))? + .0, + ), }, }; Ok(result) @@ -78,7 +92,7 @@ pub struct RpcStakePoolData { } impl RpcStakePoolData { - fn new(chain_config: &ChainConfig, data: &StakePoolData) -> Result { + fn new(chain_config: &ChainConfig, data: &StakePoolData) -> Result { let result = Self { pledge: RpcAmountOut::from_amount(data.pledge(), chain_config.coin_decimals()), staker: RpcAddress::new(chain_config, data.staker().clone())?, @@ -106,7 +120,7 @@ impl RpcHashedTimelockContract { fn new( chain_config: &ChainConfig, htlc: &HashedTimelockContract, - ) -> Result { + ) -> Result { let result = Self { secret_hash: RpcHexString::from_bytes(htlc.secret_hash.as_bytes().to_owned()), spend_key: RpcAddress::new(chain_config, htlc.spend_key.clone())?, @@ -171,25 +185,29 @@ pub enum RpcTxOutput { } impl RpcTxOutput { - pub fn new(chain_config: &ChainConfig, output: TxOutput) -> Result { + pub fn new( + chain_config: &ChainConfig, + token_decimals_provider: &impl TokenDecimalsProvider, + output: TxOutput, + ) -> Result { let result = match output { TxOutput::Transfer(value, destination) => RpcTxOutput::Transfer { - value: RpcOutputValueOut::new(chain_config, value)?, + value: RpcOutputValueOut::new(chain_config, token_decimals_provider, value)?, destination: RpcAddress::new(chain_config, destination)?, }, TxOutput::LockThenTransfer(value, destination, timelock) => { RpcTxOutput::LockThenTransfer { - value: RpcOutputValueOut::new(chain_config, value)?, + value: RpcOutputValueOut::new(chain_config, token_decimals_provider, value)?, destination: RpcAddress::new(chain_config, destination)?, timelock, } } TxOutput::Htlc(value, htlc) => RpcTxOutput::Htlc { - value: RpcOutputValueOut::new(chain_config, value)?, + value: RpcOutputValueOut::new(chain_config, token_decimals_provider, value)?, htlc: RpcHashedTimelockContract::new(chain_config, &htlc)?, }, TxOutput::Burn(value) => RpcTxOutput::Burn { - value: RpcOutputValueOut::new(chain_config, value)?, + value: RpcOutputValueOut::new(chain_config, token_decimals_provider, value)?, }, TxOutput::CreateStakePool(id, data) => RpcTxOutput::CreateStakePool { pool_id: RpcAddress::new(chain_config, id)?, @@ -222,8 +240,16 @@ impl RpcTxOutput { }, TxOutput::CreateOrder(data) => RpcTxOutput::CreateOrder { authority: RpcAddress::new(chain_config, data.conclude_key().clone())?, - ask_value: RpcOutputValueOut::new(chain_config, data.ask().clone())?, - give_value: RpcOutputValueOut::new(chain_config, data.give().clone())?, + ask_value: RpcOutputValueOut::new( + chain_config, + token_decimals_provider, + data.ask().clone(), + )?, + give_value: RpcOutputValueOut::new( + chain_config, + token_decimals_provider, + data.give().clone(), + )?, }, }; Ok(result) diff --git a/chainstate/src/rpc/types/signed_transaction.rs b/chainstate/src/rpc/types/signed_transaction.rs index 9beb8ba8f..a33015164 100644 --- a/chainstate/src/rpc/types/signed_transaction.rs +++ b/chainstate/src/rpc/types/signed_transaction.rs @@ -14,13 +14,15 @@ // limitations under the License. use common::{ - address::AddressError, chain::{ChainConfig, SignedTransaction, Transaction}, primitives::{Id, Idable}, }; use serialization::hex_encoded::HexEncoded; -use super::{input::RpcTxInput, output::RpcTxOutput}; +use super::{ + input::RpcTxInput, output::RpcTxOutput, token_decimals_provider::TokenDecimalsProvider, + RpcTypeError, +}; #[derive(Debug, Clone, serde::Serialize, rpc_description::HasValueHint)] pub struct RpcSignedTransaction { @@ -34,7 +36,11 @@ pub struct RpcSignedTransaction { } impl RpcSignedTransaction { - pub fn new(chain_config: &ChainConfig, tx: SignedTransaction) -> Result { + pub fn new( + chain_config: &ChainConfig, + token_decimals_provider: &impl TokenDecimalsProvider, + tx: SignedTransaction, + ) -> Result { let rpc_tx_inputs = tx .transaction() .inputs() @@ -46,7 +52,7 @@ impl RpcSignedTransaction { .transaction() .outputs() .iter() - .map(|output| RpcTxOutput::new(chain_config, output.clone())) + .map(|output| RpcTxOutput::new(chain_config, token_decimals_provider, output.clone())) .collect::, _>>()?; let rpc_tx = Self { diff --git a/chainstate/src/rpc/types/token.rs b/chainstate/src/rpc/types/token.rs index 6b58bd1e1..3d4bbba48 100644 --- a/chainstate/src/rpc/types/token.rs +++ b/chainstate/src/rpc/types/token.rs @@ -14,7 +14,7 @@ // limitations under the License. use common::{ - address::{AddressError, RpcAddress}, + address::RpcAddress, chain::{ tokens::{IsTokenFreezable, NftIssuance, TokenIssuance, TokenTotalSupply}, ChainConfig, Destination, @@ -23,6 +23,8 @@ use common::{ }; use rpc::types::{RpcHexString, RpcString}; +use super::RpcTypeError; + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rpc_description::HasValueHint)] #[serde(tag = "type", content = "content")] pub enum RpcTokenTotalSupply { @@ -32,7 +34,7 @@ pub enum RpcTokenTotalSupply { } impl RpcTokenTotalSupply { - pub fn new(chain_config: &ChainConfig, supply: TokenTotalSupply) -> Result { + pub fn new(chain_config: &ChainConfig, supply: TokenTotalSupply) -> Result { let result = match supply { TokenTotalSupply::Fixed(amount) => RpcTokenTotalSupply::Fixed { amount: RpcAmountOut::from_amount(amount, chain_config.coin_decimals()), @@ -55,7 +57,7 @@ pub struct RpcTokenIssuance { } impl RpcTokenIssuance { - pub fn new(chain_config: &ChainConfig, issuance: &TokenIssuance) -> Result { + pub fn new(chain_config: &ChainConfig, issuance: &TokenIssuance) -> Result { let result = match issuance { TokenIssuance::V1(issuance) => Self { token_ticker: RpcString::from_bytes(issuance.token_ticker.clone()), @@ -80,7 +82,7 @@ pub struct RpcNftIssuance { } impl RpcNftIssuance { - pub fn new(chain_config: &ChainConfig, issuance: &NftIssuance) -> Result { + pub fn new(chain_config: &ChainConfig, issuance: &NftIssuance) -> Result { let result = match issuance { NftIssuance::V0(issuance) => Self { metadata: RpcNftMetadata::new(chain_config, &issuance.metadata)?, @@ -106,7 +108,7 @@ impl RpcNftMetadata { fn new( chain_config: &ChainConfig, metadata: &common::chain::tokens::Metadata, - ) -> Result { + ) -> Result { let result = Self { creator: metadata .creator diff --git a/chainstate/src/rpc/types/token_decimals_provider.rs b/chainstate/src/rpc/types/token_decimals_provider.rs new file mode 100644 index 000000000..1998b7326 --- /dev/null +++ b/chainstate/src/rpc/types/token_decimals_provider.rs @@ -0,0 +1,31 @@ +// Copyright (c) 2021-2025 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::BTreeMap; + +use common::chain::tokens::TokenId; + +#[derive(Clone, Copy, Debug)] +pub struct TokenDecimals(pub u8); + +pub trait TokenDecimalsProvider { + fn get_token_decimals(&self, token_id: &TokenId) -> Option; +} + +impl TokenDecimalsProvider for BTreeMap { + fn get_token_decimals(&self, token_id: &TokenId) -> Option { + self.get(token_id).copied() + } +} diff --git a/common/src/chain/block/block_body/mod.rs b/common/src/chain/block/block_body/mod.rs index 7c835f266..568d84b48 100644 --- a/common/src/chain/block/block_body/mod.rs +++ b/common/src/chain/block/block_body/mod.rs @@ -21,7 +21,9 @@ pub mod merkle_proxy; use merkletree_mintlayer::{MerkleTreeFormError, MerkleTreeProofExtractionError}; use serialization::{Decode, Encode}; -use crate::chain::SignedTransaction; +use crate::chain::{ + output_value::OutputValue, output_values_holder::OutputValuesHolder, SignedTransaction, +}; use self::merkle_proxy::BlockBodyMerkleProxy; @@ -63,6 +65,16 @@ impl BlockBody { } } +impl OutputValuesHolder for BlockBody { + fn output_values_iter(&self) -> impl Iterator { + let values_from_reward = + self.reward.outputs().iter().flat_map(|output| output.output_values_iter()); + let values_from_txs = self.transactions.iter().flat_map(|tx| tx.output_values_iter()); + + values_from_reward.chain(values_from_txs) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/src/chain/block/mod.rs b/common/src/chain/block/mod.rs index 81652da01..92b9e54fb 100644 --- a/common/src/chain/block/mod.rs +++ b/common/src/chain/block/mod.rs @@ -37,7 +37,11 @@ use typename::TypeName; use utils::ensure; use crate::{ - chain::block::{block_size::BlockSize, block_v1::BlockV1, timestamp::BlockTimestamp}, + chain::{ + block::{block_size::BlockSize, block_v1::BlockV1, timestamp::BlockTimestamp}, + output_value::OutputValue, + output_values_holder::OutputValuesHolder, + }, primitives::{ id::{HasSubObjWithSameId, WithId}, Id, Idable, VersionTag, H256, @@ -241,6 +245,13 @@ impl<'de> serde::Deserialize<'de> for Id { impl Eq for WithId {} +impl OutputValuesHolder for Block { + fn output_values_iter(&self) -> impl Iterator { + // Note: there are no OutputValue's in the header + self.body().output_values_iter() + } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/common/src/chain/partially_signed_transaction/additional_info.rs b/common/src/chain/partially_signed_transaction/additional_info.rs index 172cdcfd2..85d11efd6 100644 --- a/common/src/chain/partially_signed_transaction/additional_info.rs +++ b/common/src/chain/partially_signed_transaction/additional_info.rs @@ -20,6 +20,7 @@ use serialization::{Decode, Encode}; use crate::{ chain::{ output_value::OutputValue, + output_values_holder::OutputValuesHolder, signature::sighash::{self}, OrderId, PoolId, }, @@ -57,6 +58,12 @@ pub struct OrderAdditionalInfo { pub give_balance: Amount, } +impl OutputValuesHolder for OrderAdditionalInfo { + fn output_values_iter(&self) -> impl Iterator { + [&self.initially_asked, &self.initially_given].into_iter() + } +} + #[derive(Debug, Eq, PartialEq, Clone, Encode, Decode, serde::Serialize)] pub struct TxAdditionalInfo { pool_info: BTreeMap, @@ -147,3 +154,10 @@ impl sighash::input_commitments::OrderInfoProvider for TxAdditionalInfo { ) } } + +impl OutputValuesHolder for TxAdditionalInfo { + fn output_values_iter(&self) -> impl Iterator { + self.order_info_iter() + .flat_map(|(_, order_info)| order_info.output_values_iter()) + } +} diff --git a/common/src/chain/partially_signed_transaction/mod.rs b/common/src/chain/partially_signed_transaction/mod.rs index 896eb14cf..adc1846e3 100644 --- a/common/src/chain/partially_signed_transaction/mod.rs +++ b/common/src/chain/partially_signed_transaction/mod.rs @@ -20,6 +20,8 @@ use serialization::{Decode, Encode}; use crate::{ chain::{ htlc::HtlcSecret, + output_value::OutputValue, + output_values_holder::OutputValuesHolder, partially_signed_transaction::v1::PartiallySignedTransactionV1, signature::{ inputsig::InputWitness, @@ -224,6 +226,18 @@ impl PartiallySignedTransaction { } } +impl OutputValuesHolder for PartiallySignedTransaction { + fn output_values_iter(&self) -> impl Iterator { + let tx_values_iter = self.tx().output_values_iter(); + let input_utxos_values_iter = self.input_utxos().iter().flat_map(|opt_output| { + opt_output.iter().flat_map(|output| output.output_values_iter()) + }); + let additional_info_values_iter = self.additional_info().output_values_iter(); + + tx_values_iter.chain(input_utxos_values_iter).chain(additional_info_values_iter) + } +} + pub fn make_sighash_input_commitments_at_height<'a>( tx_inputs: &[TxInput], input_utxos: &'a [Option], diff --git a/common/src/chain/transaction/mod.rs b/common/src/chain/transaction/mod.rs index 8ca38c998..7172d16f3 100644 --- a/common/src/chain/transaction/mod.rs +++ b/common/src/chain/transaction/mod.rs @@ -18,8 +18,11 @@ use thiserror::Error; use serialization::{DirectDecode, DirectEncode}; use typename::TypeName; -use crate::primitives::{id::WithId, Id, Idable, H256}; -use crate::text_summary::TextSummary; +use crate::{ + chain::{output_value::OutputValue, output_values_holder::OutputValuesHolder}, + primitives::{id::WithId, Id, Idable, H256}, + text_summary::TextSummary, +}; pub mod input; pub use input::*; @@ -185,6 +188,13 @@ impl<'de> serde::Deserialize<'de> for Id { } } +impl OutputValuesHolder for Transaction { + fn output_values_iter(&self) -> impl Iterator { + // Note: TxInput's don't contain OutputValue's. + self.outputs().iter().flat_map(|output| output.output_values_iter()) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/common/src/chain/transaction/output/mod.rs b/common/src/chain/transaction/output/mod.rs index 28ecff103..db5fcd6d3 100644 --- a/common/src/chain/transaction/output/mod.rs +++ b/common/src/chain/transaction/output/mod.rs @@ -24,6 +24,7 @@ use crate::{ chain::{ order::OrderData, output_value::OutputValue, + output_values_holder::OutputValuesHolder, tokens::{IsTokenFreezable, NftIssuance, TokenId, TokenIssuance, TokenTotalSupply}, ChainConfig, DelegationId, PoolId, }, @@ -33,6 +34,7 @@ use crate::{ use crypto::vrf::VRFPublicKey; use script::Script; use serialization::{Decode, DecodeAll, Encode}; +use smallvec::SmallVec; use strum::{EnumCount, EnumDiscriminants, EnumIter}; use self::{htlc::HashedTimelockContract, stakelock::StakePoolData, timelock::OutputTimeLock}; @@ -40,6 +42,7 @@ use self::{htlc::HashedTimelockContract, stakelock::StakePoolData, timelock::Out pub mod classic_multisig; pub mod htlc; pub mod output_value; +pub mod output_values_holder; pub mod stakelock; pub mod timelock; @@ -380,3 +383,30 @@ impl TextSummary for TxOutput { impl rpc_description::HasValueHint for TxOutput { const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::GENERIC_OBJECT; } + +impl OutputValuesHolder for TxOutput { + fn output_values_iter(&self) -> impl Iterator { + // Use SmallVec to avoid allocations (we'll be producing at most 2 values here). + let mut values = SmallVec::<[_; 2]>::new(); + + match self { + TxOutput::Transfer(value, _) + | TxOutput::LockThenTransfer(value, _, _) + | TxOutput::Burn(value) + | TxOutput::Htlc(value, _) => values.push(value), + TxOutput::CreateOrder(order_data) => { + values.push(order_data.ask()); + values.push(order_data.give()) + } + TxOutput::CreateStakePool(_, _) + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::DataDeposit(_) => {} + } + + values.into_iter() + } +} diff --git a/common/src/chain/transaction/output/output_values_holder.rs b/common/src/chain/transaction/output/output_values_holder.rs new file mode 100644 index 000000000..021585005 --- /dev/null +++ b/common/src/chain/transaction/output/output_values_holder.rs @@ -0,0 +1,52 @@ +// Copyright (c) 2021-2025 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 crate::chain::{output_value::OutputValue, tokens::TokenId}; + +/// A trait that will be implemented by types that contain one or more `OutputValue`'s, e.g. +/// a transaction output, a transaction itself, a block. +pub trait OutputValuesHolder { + fn output_values_iter(&self) -> impl Iterator; +} + +pub fn collect_token_v1_ids_from_output_values_holder_into( + holder: &impl OutputValuesHolder, + dest: &mut BTreeSet, +) { + for token_id in holder + .output_values_iter() + .flat_map(|output_value| output_value.token_v1_id().into_iter()) + { + dest.insert(*token_id); + } +} + +pub fn collect_token_v1_ids_from_output_values_holder( + holder: &impl OutputValuesHolder, +) -> BTreeSet { + collect_token_v1_ids_from_output_values_holders(std::iter::once(holder)) +} + +pub fn collect_token_v1_ids_from_output_values_holders<'a, H: OutputValuesHolder + 'a>( + holders: impl IntoIterator, +) -> BTreeSet { + let mut result = BTreeSet::new(); + for holder in holders { + collect_token_v1_ids_from_output_values_holder_into(holder, &mut result); + } + result +} diff --git a/common/src/chain/transaction/signed_transaction.rs b/common/src/chain/transaction/signed_transaction.rs index d8920edea..8958c1ef0 100644 --- a/common/src/chain/transaction/signed_transaction.rs +++ b/common/src/chain/transaction/signed_transaction.rs @@ -18,7 +18,10 @@ use super::{ Transaction, TransactionSize, TxOutput, }; use crate::{ - chain::{TransactionCreationError, TxInput}, + chain::{ + output_value::OutputValue, output_values_holder::OutputValuesHolder, + TransactionCreationError, TxInput, + }, primitives::id::{self, H256}, }; use serialization::{Decode, Encode}; @@ -137,6 +140,12 @@ impl Decode for SignedTransaction { } } +impl OutputValuesHolder for SignedTransaction { + fn output_values_iter(&self) -> impl Iterator { + self.transaction.output_values_iter() + } +} + #[cfg(test)] mod tests { use crate::primitives::Amount; diff --git a/test/functional/blockprod_generate_blocks_all_sources.py b/test/functional/blockprod_generate_blocks_all_sources.py index 346355a66..8186e6b7d 100755 --- a/test/functional/blockprod_generate_blocks_all_sources.py +++ b/test/functional/blockprod_generate_blocks_all_sources.py @@ -14,12 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from scalecodec.base import ScaleBytes, RuntimeConfiguration, ScaleDecoder +from scalecodec.base import ScaleBytes, ScaleDecoder from test_framework.test_framework import BitcoinTestFramework from test_framework.mintlayer import * -import time - class GenerateBlocksFromAllSourcesTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True diff --git a/test/functional/blockprod_generate_pos_genesis_blocks.py b/test/functional/blockprod_generate_pos_genesis_blocks.py index 27d18ac06..e99fb582b 100755 --- a/test/functional/blockprod_generate_pos_genesis_blocks.py +++ b/test/functional/blockprod_generate_pos_genesis_blocks.py @@ -14,13 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from hashlib import blake2b from scalecodec.base import ScaleBytes, ScaleDecoder from test_framework.authproxy import JSONRPCException from test_framework.mintlayer import ( - base_tx_obj, block_input_data_obj, - mintlayer_hash, ATOMS_PER_COIN, ) from test_framework.test_framework import BitcoinTestFramework diff --git a/test/functional/mempool_eviction.py b/test/functional/mempool_eviction.py index 8559a09bc..1b9c53ac8 100644 --- a/test/functional/mempool_eviction.py +++ b/test/functional/mempool_eviction.py @@ -22,7 +22,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_raises_rpc_error from test_framework.mintlayer import * -import time class MempoolTxEvictionTest(BitcoinTestFramework): diff --git a/test/functional/mempool_feerate_points.py b/test/functional/mempool_feerate_points.py index 293253a32..1e217df1b 100644 --- a/test/functional/mempool_feerate_points.py +++ b/test/functional/mempool_feerate_points.py @@ -23,7 +23,7 @@ from typing import List from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import (assert_equal, assert_raises_rpc_error) +from test_framework.util import assert_equal from test_framework.mintlayer import (block_input_data_obj, make_tx , reward_input, tx_input, tx_output) import random diff --git a/test/functional/mempool_local_orphan_rejected.py b/test/functional/mempool_local_orphan_rejected.py index a28b582a3..ee9eff7b5 100644 --- a/test/functional/mempool_local_orphan_rejected.py +++ b/test/functional/mempool_local_orphan_rejected.py @@ -24,7 +24,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.mintlayer import (make_tx, reward_input, tx_input) from test_framework.util import assert_raises_rpc_error -import scalecodec class MempoolLocalOrphanSubmissionTest(BitcoinTestFramework): diff --git a/test/functional/mempool_orphan_peer_disconnected.py b/test/functional/mempool_orphan_peer_disconnected.py index e1af4b6d5..f9e44c830 100644 --- a/test/functional/mempool_orphan_peer_disconnected.py +++ b/test/functional/mempool_orphan_peer_disconnected.py @@ -23,7 +23,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.mintlayer import (make_tx, reward_input, tx_input) -import scalecodec class MempoolOrphanFromDisconnectedPeerTest(BitcoinTestFramework): diff --git a/test/functional/p2p_submit_orphan.py b/test/functional/p2p_submit_orphan.py index 5e83d98d1..2e57b3872 100644 --- a/test/functional/p2p_submit_orphan.py +++ b/test/functional/p2p_submit_orphan.py @@ -21,10 +21,9 @@ * After submitting a transaction that defines the UTXO, both are in non-orphan mempool """ -from test_framework.p2p import (P2PInterface, P2PDataStore) +from test_framework.p2p import P2PDataStore from test_framework.test_framework import BitcoinTestFramework from test_framework.mintlayer import (make_tx_dict, reward_input, tx_input, calc_tx_id) -import scalecodec import time diff --git a/test/functional/test_framework/mintlayer.py b/test/functional/test_framework/mintlayer.py index 19341b98c..c57f5051e 100644 --- a/test/functional/test_framework/mintlayer.py +++ b/test/functional/test_framework/mintlayer.py @@ -16,10 +16,14 @@ """ Module for mintlayer-specific utilities for testing """ import hashlib +import random import scalecodec import time +from decimal import Decimal + ATOMS_PER_COIN = 100_000_000_000 +COINS_NUM_DECIMALS = 11 DEFAULT_INITIAL_MINT = 400_000_000 * ATOMS_PER_COIN MIN_POOL_PLEDGE = 40_000 * ATOMS_PER_COIN @@ -96,7 +100,7 @@ def calc_block_id(block): # If block is already a header without signature, we keep the object as is (no 'header' field) while 'header' in block: block = block['header'] - return hash_object(block_header_obj, tx) + return hash_object(block_header_obj, block) def make_tx(inputs, outputs, flags = 0): @@ -163,3 +167,7 @@ def make_delegation_id(outpoint): # Truncate output to match Rust's split() return hex_to_dec_array(blake2b_hasher.hexdigest()[:64]) + +def random_decimal_amount(min: int, max: int, num_decimals: int) -> Decimal: + atoms_per_unit = 10 ** num_decimals + return Decimal(random.randint(min * atoms_per_unit, max * atoms_per_unit)) / atoms_per_unit diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py index 9ae987ef3..31f4124e2 100755 --- a/test/functional/test_framework/p2p.py +++ b/test/functional/test_framework/p2p.py @@ -35,7 +35,6 @@ msg_addr, msg_addrv2, msg_block, - MSG_BLOCK, msg_blocktxn, msg_cfcheckpt, msg_cfheaders, @@ -62,12 +61,10 @@ msg_sendheaders, msg_tx, MSG_TX, - MSG_TYPE_MASK, msg_verack, msg_version, MSG_WTX, msg_wtxidrelay, - sha256, ) from test_framework.util import ( MAX_NODES, @@ -76,6 +73,7 @@ ) from test_framework.mintlayer import ( calc_tx_id, + calc_block_id, ) logger = logging.getLogger("TestFramework.p2p") @@ -702,6 +700,12 @@ def on_header_list_request(self, message): self.send_message({'header_list': []}) return + # Note: below goes some old code which at the time of writing this was compeltely broken, + # because `calc_block_id` wasn't imported. Chances are that it has other problems too. + # If you've hit this assertion when writing a new test, remove the assertion and check + # the code for correctness. + assert False, "Currently unreachable" + locator = message['header_list_request'] headers_list = [self.block_store[self.last_block_hash]] diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index a8f37e353..f8874e4f8 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -16,13 +16,14 @@ # limitations under the License. """A wrapper around a CLI wallet instance""" +import asyncio import json import os -import asyncio import re from dataclasses import dataclass +from decimal import Decimal from tempfile import NamedTemporaryFile -from typing import Optional, List, Tuple, Union +from typing import Optional, List, Tuple from test_framework.util import assert_in from test_framework.wallet_controller_common import PartialSigInfo, TokenTxOutput, UtxoOutpoint, WalletCliControllerBase @@ -270,24 +271,30 @@ async def account_extended_public_key(self) -> str: async def new_address(self) -> str: return await self._write_command(f"address-new\n") - async def list_utxos(self, utxo_types: str = '', with_locked: str = '', utxo_states: List[str] = []) -> List[UtxoOutpoint]: - output = await self._write_command(f"account-utxos {utxo_types} {with_locked} {''.join(utxo_states)}\n") - - j = json.loads(output) - - return [UtxoOutpoint(id=match["outpoint"]["source_id"]["content"]["tx_id"], index=int(match["outpoint"]["index"])) for match in j] + # List UTXOs of the specified kind, returning a list of UtxoOutpoint's. + # By default, all unlocked UTXOs are returned (i.e. any type, any state). + async def list_utxos(self, utxo_type: str = '', with_locked: str = '', utxo_states: List[str] = []) -> List[UtxoOutpoint]: + output = await self.list_utxos_raw(utxo_type, with_locked, utxo_states) + return [UtxoOutpoint(id=match["outpoint"]["source_id"]["content"]["tx_id"], index=int(match["outpoint"]["index"])) for match in output] - # Note: probably this function should have been called `list_utxos` and the current `list_utxos` have a more specific name. - async def list_utxos_raw(self, utxo_types: str = '', with_locked: str = '', utxo_states: List[str] = []) -> List[UtxoOutpoint]: - output = await self._write_command(f"account-utxos {utxo_types} {with_locked} {''.join(utxo_states)}\n") + # Same as list_utxos, but return a raw dict. + async def list_utxos_raw(self, utxo_type: str = '', with_locked: str = '', utxo_states: List[str] = []) -> any: + output = await self._write_command(f"account-utxos {utxo_type} {with_locked} {' '.join(utxo_states)}\n") return json.loads(output) - async def list_multisig_utxos(self, utxo_types: str = '', with_locked: str = '', utxo_states: List[str] = []) -> List[UtxoOutpoint]: - output = await self._write_command(f"standalone-multisig-utxos {utxo_types} {with_locked} {''.join(utxo_states)}\n") - - j = json.loads(output) - - return [UtxoOutpoint(id=match["outpoint"]["source_id"]["content"]["tx_id"], index=int(match["outpoint"]["index"])) for match in j] + # List multisig UTXOs of the specified kind, returning a list of UtxoOutpoint's. + # By default, all unlocked and confirmed UTXOs are returned. + # Note: the accepted parameter values differ from ones accepted by this function's RPC counterpart. + # So, controller-agnostic tests should always call it without parameters. + # TODO: make the parameters compatible. + async def list_multisig_utxos(self, utxo_type: str = '', with_locked: str = '', utxo_states: List[str] = []) -> List[UtxoOutpoint]: + output = await self.list_multisig_utxos_raw(utxo_type, with_locked, utxo_states) + return [UtxoOutpoint(id=match["outpoint"]["source_id"]["content"]["tx_id"], index=int(match["outpoint"]["index"])) for match in output] + + # Same as list_multisig_utxos, but return a raw dict. + async def list_multisig_utxos_raw(self, utxo_type: str = '', with_locked: str = '', utxo_states: List[str] = []) -> List[UtxoOutpoint]: + output = await self._write_command(f"standalone-multisig-utxos {utxo_type} {with_locked} {' '.join(utxo_states)}\n") + return json.loads(output) async def get_transaction(self, tx_id: str): out = await self._write_command(f"transaction-get {tx_id}\n") @@ -313,7 +320,7 @@ async def sweep_addresses(self, destination_address: str, from_addresses: List[s async def sweep_delegation(self, destination_address: str, delegation_id: str) -> str: return await self._write_command(f"staking-sweep-delegation {destination_address} {delegation_id}\n") - async def send_to_address(self, address: str, amount: Union[int, float, str], selected_utxos: List[UtxoOutpoint] = []) -> str: + async def send_to_address(self, address: str, amount: int | float | Decimal | str, selected_utxos: List[UtxoOutpoint] = []) -> str: return await self._write_command(f"address-send {address} {amount} {' '.join(map(str, selected_utxos))}\n") async def compose_transaction(self, outputs: List[TxOutput], selected_utxos: List[UtxoOutpoint], only_transaction: bool = False) -> str: @@ -321,11 +328,11 @@ async def compose_transaction(self, outputs: List[TxOutput], selected_utxos: Lis utxos = f"--utxos {' --utxos '.join(map(str, selected_utxos))}" if selected_utxos else "" return await self._write_command(f"transaction-compose {' '.join(map(str, outputs))} {utxos} {only_tx}\n") - async def send_tokens_to_address(self, token_id: str, address: str, amount: Union[float, str]): + async def send_tokens_to_address(self, token_id: str, address: str, amount: int | float | Decimal | str): return await self._write_command(f"token-send {token_id} {address} {amount}\n") # Note: unlike send_tokens_to_address, this function behaves identically both for wallet_cli_controller and wallet_rpc_controller. - async def send_tokens_to_address_or_fail(self, token_id: str, address: str, amount: Union[float, str]): + async def send_tokens_to_address_or_fail(self, token_id: str, address: str, amount: int | float | Decimal | str): output = await self.send_tokens_to_address(token_id, address, amount) assert_in("The transaction was submitted successfully", output) @@ -572,7 +579,7 @@ async def make_tx_to_send_tokens_from_multisig_address_expect_partially_signed( return (tx, siginfo) async def make_tx_to_send_tokens_with_intent( - self, token_id: str, destination: str, amount: Union[float, str], intent: str): + self, token_id: str, destination: str, amount: int | float | Decimal | str, intent: str): output = await self._write_command( f"token-make-tx-to-send-with-intent {token_id} {destination} {amount} {intent}\n") diff --git a/test/functional/test_framework/wallet_controller_common.py b/test/functional/test_framework/wallet_controller_common.py index 50d5212aa..e2dc72302 100644 --- a/test/functional/test_framework/wallet_controller_common.py +++ b/test/functional/test_framework/wallet_controller_common.py @@ -16,6 +16,7 @@ # limitations under the License. from dataclasses import dataclass +from decimal import Decimal import re @@ -70,3 +71,11 @@ async def submit_transaction_return_id(self, transaction: str, do_not_store: boo match = re.search(pattern, output) assert match is not None return match.group(1) + + async def get_coins_balance(self, with_locked: str = "unlocked"): + balance_response = await self.get_balance(with_locked=with_locked) + match = re.search(r'Coins amount: (\d+(\.\d+)?)', balance_response) + assert match is not None + + balance = Decimal(match.group(1)) + return balance diff --git a/test/functional/test_framework/wallet_rpc_controller.py b/test/functional/test_framework/wallet_rpc_controller.py index 13fc85d06..1dbba4471 100644 --- a/test/functional/test_framework/wallet_rpc_controller.py +++ b/test/functional/test_framework/wallet_rpc_controller.py @@ -16,16 +16,16 @@ # limitations under the License. """A wrapper around a RPC wallet instance""" -import os import asyncio +import base64 import http.client import json +import os from dataclasses import dataclass -from tempfile import NamedTemporaryFile -import base64 +from decimal import Decimal from operator import itemgetter - -from typing import Optional, List, Union, TypedDict +from tempfile import NamedTemporaryFile +from typing import Optional, List, TypedDict from test_framework.util import assert_in, rpc_port from test_framework.wallet_controller_common import ( @@ -274,9 +274,42 @@ async def new_address(self) -> str: async def add_standalone_multisig_address(self, min_required_signatures: int, pub_keys: List[str], label: Optional[str] = None) -> str: return self._write_command("standalone_add_multisig", [self.account, min_required_signatures, pub_keys, label, None])['result'] - async def list_utxos(self, utxo_types: str = '', with_locked: str = '', utxo_states: List[str] = []) -> List[UtxoOutpoint]: - outputs = self._write_command("account_utxos", [self.account, utxo_types, with_locked, ''.join(utxo_states)])['result'] - return [UtxoOutpoint(id=match["outpoint"]["source_id"]["content"]['tx_id'], index=int(match["outpoint"]['index'])) for match in outputs] + # Return unlocked UTXOs with any type and any state, returning a list of UtxoOutpoint's. + # Note: unlike CLI controller's counterpart, this function doesn't accept the utxo_types/with_locked/utxo_states + # parameters. But both functions behave identically when no parameters are passed. + async def list_utxos(self) -> List[UtxoOutpoint]: + output = await self.list_utxos_raw() + return [UtxoOutpoint(id=match["outpoint"]["source_id"]["content"]['tx_id'], index=int(match["outpoint"]['index'])) for match in output] + + # Same as list_utxos, but return a raw dict. + async def list_utxos_raw(self,) -> any: + return self._write_command("account_utxos", [self.account])['result'] + + # List multisig UTXOs of the specified kind, returning a list of UtxoOutpoint's. + # By default, all unlocked and confirmed UTXOs are returned. + # Note: the accepted parameter values differ from ones accepted by this function's CLI counterpart. + # So, controller-agnostic tests should always call it without parameters. + # TODO: make the parameters compatible. + async def list_multisig_utxos(self, utxo_type: str = '', with_locked: str = '', utxo_states: List[str] = []) -> List[UtxoOutpoint]: + output = await self.list_multisig_utxos_raw(utxo_type, with_locked, utxo_states) + return [UtxoOutpoint(id=match["outpoint"]["source_id"]["content"]["tx_id"], index=int(match["outpoint"]["index"])) for match in output] + + # Same as list_multisig_utxos, but return a raw dict. + async def list_multisig_utxos_raw(self, utxo_type: str = '', with_locked: str = '', utxo_states: List[str] = []) -> List[UtxoOutpoint]: + if utxo_type == "": + utxo_types = [] + else: + utxo_types = [utxo_type] + + if with_locked == "": + with_locked = None + + result = self._write_command( + "standalone_multisig_utxos", + {"account": self.account, "utxo_states": utxo_states, "utxo_types": utxo_types, "with_locked": with_locked } + ) + + return result['result'] async def get_transaction(self, tx_id: str) -> str: return self._write_command("transaction_get", [self.account, tx_id])['result'] @@ -288,11 +321,11 @@ async def send_to_address(self, address: str, amount: int, selected_utxos: List[ self._write_command("address_send", [self.account, address, {'decimal': str(amount)}, selected_utxos, {'in_top_x_mb': 5}]) return "The transaction was submitted successfully" - async def send_tokens_to_address(self, token_id: str, address: str, amount: Union[float, str]): + async def send_tokens_to_address(self, token_id: str, address: str, amount: int | float | Decimal | str): return self._write_command("token_send", [self.account, token_id, address, {'decimal': str(amount)}, {'in_top_x_mb': 5}])['result'] # Note: unlike send_tokens_to_address, this function behaves identically both for wallet_cli_controller and wallet_rpc_controller. - async def send_tokens_to_address_or_fail(self, token_id: str, address: str, amount: Union[float, str]): + async def send_tokens_to_address_or_fail(self, token_id: str, address: str, amount: int | float | Decimal | str): # send_tokens_to_address already fails on error. await self.send_tokens_to_address(token_id, address, amount) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index e5e707476..2c493f0a9 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -182,6 +182,8 @@ class UnicodeOnWindowsError(ValueError): 'wallet_htlc_spend.py', 'wallet_htlc_refund_multisig.py', 'wallet_htlc_refund_single_sig.py', + 'wallet_list_utxos.py', + 'wallet_list_utxos_rpc.py', 'framework_tests.py', # Don't append tests at the end to avoid merge conflicts diff --git a/test/functional/wallet_account_info.py b/test/functional/wallet_account_info.py index 09fe2d050..a23f133ee 100644 --- a/test/functional/wallet_account_info.py +++ b/test/functional/wallet_account_info.py @@ -27,15 +27,13 @@ * get wallet info """ -import json from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random import string diff --git a/test/functional/wallet_cold_wallet_send.py b/test/functional/wallet_cold_wallet_send.py index 46e99119b..aabd333a7 100644 --- a/test/functional/wallet_cold_wallet_send.py +++ b/test/functional/wallet_cold_wallet_send.py @@ -39,7 +39,6 @@ from test_framework.wallet_cli_controller import UtxoOutpoint, WalletCliController import asyncio -import sys def get_destination(dest): if 'Address' in dest: diff --git a/test/functional/wallet_conflict.py b/test/functional/wallet_conflict.py index ab6554eeb..1161ffcdf 100644 --- a/test/functional/wallet_conflict.py +++ b/test/functional/wallet_conflict.py @@ -27,17 +27,13 @@ * put the freeze tx in a block, the transfer should be rejected and conflicting in the wallet """ -import json -from scalecodec.base import ScaleBytes from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN, signed_tx_obj) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import WalletCliController, DEFAULT_ACCOUNT_INDEX import asyncio -import sys -import random class WalletConflictTransaction(BitcoinTestFramework): diff --git a/test/functional/wallet_connect_to_rpc.py b/test/functional/wallet_connect_to_rpc.py index dc8d11abc..20df42582 100644 --- a/test/functional/wallet_connect_to_rpc.py +++ b/test/functional/wallet_connect_to_rpc.py @@ -29,13 +29,12 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import WalletCliController from test_framework.wallet_rpc_controller import WalletRpcController from test_framework.util import rpc_port import asyncio -import sys import random import os diff --git a/test/functional/wallet_create_pool_for_another_wallet.py b/test/functional/wallet_create_pool_for_another_wallet.py index d5bb114c4..b406c7412 100644 --- a/test/functional/wallet_create_pool_for_another_wallet.py +++ b/test/functional/wallet_create_pool_for_another_wallet.py @@ -36,15 +36,6 @@ def set_test_params(self): super().set_test_params() self.wallet_controller = WalletCliController - async def coin_balance(self, wallet, with_locked) -> Decimal: - balance_response = await wallet.get_balance(with_locked=with_locked) - match = re.search(r'Coins amount: (\d+(\.\d+)?)', balance_response) - assert match is not None - - balance = Decimal(match.group(1)) - self.log.info(f"wallet balance = {balance}") - return balance - # Assert that the wallet's coin balance equals the specified value minus some portion of a coin # (which is assumed to have been spent on fees). def assert_approximate_balance(self, balance, expected_balance_without_fee): @@ -92,7 +83,7 @@ async def async_test(self): best_block_height = await wallet.get_best_block_height() assert_in(best_block_height, '1') - balance = await self.coin_balance(wallet, 'any') + balance = await wallet.get_coins_balance('any') assert_equal(balance, initial_balance) result = await wallet.create_stake_pool(pool_pledge, 0, 0.5, decommission_address, staker_pub_key_address, staker_vrf_pub_key) @@ -101,7 +92,7 @@ async def async_test(self): self.gen_pos_block(node.mempool_transactions(), 2) assert_in("Success", await wallet.sync()) - balance = await self.coin_balance(wallet, 'any') + balance = await wallet.get_coins_balance('any') self.assert_approximate_balance(balance, initial_balance - pool_pledge) # No pools in the creating wallet. @@ -157,13 +148,13 @@ async def async_test(self): self.gen_pos_block(node.mempool_transactions(), tip_height + 1, self.hex_to_dec_array(tip_id_with_genesis_pool)) assert_in("Success", await wallet.sync()) - unlocked_balance = await self.coin_balance(wallet, 'unlocked') + unlocked_balance = await wallet.get_coins_balance('unlocked') self.assert_approximate_balance(unlocked_balance, initial_balance - pool_pledge) - locked_balance = await self.coin_balance(wallet, 'locked') + locked_balance = await wallet.get_coins_balance('locked') self.assert_approximate_balance(locked_balance, pool_pledge + total_staking_reward) - total_balance = await self.coin_balance(wallet, 'any') + total_balance = await wallet.get_coins_balance('any') self.assert_approximate_balance(total_balance, initial_balance + total_staking_reward) diff --git a/test/functional/wallet_data_deposit.py b/test/functional/wallet_data_deposit.py index 7cfb0db4d..20062ff76 100644 --- a/test/functional/wallet_data_deposit.py +++ b/test/functional/wallet_data_deposit.py @@ -26,15 +26,13 @@ * deposit some random data """ -import json from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random diff --git a/test/functional/wallet_decommission_genesis.py b/test/functional/wallet_decommission_genesis.py index 57a8771c5..e715552e9 100644 --- a/test/functional/wallet_decommission_genesis.py +++ b/test/functional/wallet_decommission_genesis.py @@ -38,7 +38,6 @@ from test_framework.wallet_cli_controller import DEFAULT_ACCOUNT_INDEX, WalletCliController import asyncio -import sys import time GENESIS_POOL_ID = "123c4c600097c513e088b9be62069f0c74c7671c523c8e3469a1c3f14b7ea2c4" diff --git a/test/functional/wallet_generate_addresses.py b/test/functional/wallet_generate_addresses.py index 2deb6d55e..f33277fdd 100644 --- a/test/functional/wallet_generate_addresses.py +++ b/test/functional/wallet_generate_addresses.py @@ -25,16 +25,14 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import subprocess import os -import re class WalletAddressGenerator(BitcoinTestFramework): diff --git a/test/functional/wallet_get_address_usage.py b/test/functional/wallet_get_address_usage.py index 2fd62a3ca..7ed4577f0 100644 --- a/test/functional/wallet_get_address_usage.py +++ b/test/functional/wallet_get_address_usage.py @@ -31,7 +31,6 @@ from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys class WalletGetAddressUsage(BitcoinTestFramework): diff --git a/test/functional/wallet_high_fee.py b/test/functional/wallet_high_fee.py index 2535ecbf4..58308db72 100644 --- a/test/functional/wallet_high_fee.py +++ b/test/functional/wallet_high_fee.py @@ -26,16 +26,14 @@ * try to spend coins from the wallet should fail """ -from time import time import scalecodec from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (calc_tx_id, make_tx_dict, reward_input, tx_input, ATOMS_PER_COIN, tx_output) +from test_framework.mintlayer import (calc_tx_id, make_tx_dict, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_greater_than, assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys class WalletSubmitTransaction(BitcoinTestFramework): diff --git a/test/functional/wallet_list_txs.py b/test/functional/wallet_list_txs.py index eb49d4c45..baa0bd07d 100644 --- a/test/functional/wallet_list_txs.py +++ b/test/functional/wallet_list_txs.py @@ -27,15 +27,13 @@ * list the txs for that address """ -import json from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random diff --git a/test/functional/wallet_list_utxos.py b/test/functional/wallet_list_utxos.py new file mode 100644 index 000000000..f69110ce5 --- /dev/null +++ b/test/functional/wallet_list_utxos.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 RBB S.r.l +# Copyright (c) 2017-2021 The Bitcoin Core developers +# 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. +"""Wallet utxo listing test. + +* Issue and mint two tokens with non-default decimals. +* Send some amounts of the tokens and coins to normal addresses and to a multisig address + inside the wallet. +* Check the result of account-utxos and standalone-multisig-utxos; in particular, check that + the returned decimal amounts are correct. +""" + +import asyncio +import random +from decimal import Decimal + +from test_framework.mintlayer import ( + ATOMS_PER_COIN, COINS_NUM_DECIMALS, + block_input_data_obj, make_tx, reward_input, random_decimal_amount +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_in +from test_framework.wallet_cli_controller import WalletCliController +from test_framework.wallet_rpc_controller import WalletRpcController + +class WalletListUtxos(BitcoinTestFramework): + def set_test_params(self): + self.wallet_controller = WalletCliController + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [[ + "--blockprod-min-peers-to-produce-blocks=0", + ]] + + def setup_network(self): + self.setup_nodes() + self.sync_all(self.nodes[0:1]) + + def generate_block(self): + node = self.nodes[0] + + block_input_data = { "PoW": { "reward_destination": "AnyoneCanSpend" } } + block_input_data = block_input_data_obj.encode(block_input_data).to_hex()[2:] + + # Create a new block, taking transactions from mempool + block = node.blockprod_generate_block(block_input_data, [], [], "FillSpaceFromMempool") + node.chainstate_submit_block(block) + block_id = node.chainstate_best_block_id() + + # Wait for mempool to sync + self.wait_until(lambda: node.mempool_local_best_block_id() == block_id, timeout = 5) + + return block_id + + async def switch_to_wallet(self, wallet, wallet_name): + await wallet.close_wallet() + await wallet.open_wallet(wallet_name) + + def run_test(self): + asyncio.run(self.async_test()) + + async def issue_and_mint_tokens( + self, wallet: WalletCliController | WalletRpcController, dest_addr: str, num_decimals: int, to_mint: int + ) -> str: + ticker = f"TKN{num_decimals}" + token_id, _, _ = await wallet.issue_new_token(ticker, num_decimals, "http://uri", dest_addr) + assert token_id is not None + self.log.info(f"New token issued: {token_id}") + + self.generate_block() + assert_in("Success", await wallet.sync()) + + await wallet.mint_tokens_or_fail(token_id, dest_addr, to_mint) + self.log.info(f"Minted {to_mint} of {token_id}") + + self.generate_block() + assert_in("Success", await wallet.sync()) + + return token_id + + async def async_test(self): + node = self.nodes[0] + + async with self.wallet_controller(node, self.config, self.log) as wallet: + await wallet.create_wallet() + + genesis_id = node.chainstate_best_block_id() + + address0 = await wallet.new_address() + address1 = await wallet.new_address() + address2 = await wallet.new_address() + + pub_key_bytes = await wallet.new_public_key(address0) + + outputs = [{ + 'Transfer': [ + { 'Coin': random.randint(1000, 2000) * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } + ], + }] + encoded_tx, _ = make_tx([reward_input(genesis_id)], outputs, 0) + await wallet.submit_transaction(encoded_tx) + + self.generate_block() + assert_in("Success", await wallet.sync()) + + multisig_pub_keys = [] + for _ in range(0, 2): + pub_key = await wallet.reveal_public_key_as_address(await wallet.new_address()) + multisig_pub_keys.append(pub_key) + + multisig_address = await wallet.add_standalone_multisig_address_get_result(1, multisig_pub_keys, "label") + + token1_decimals = 2 + token1_total_amount = random.randint(1000, 2000) + token1_id = await self.issue_and_mint_tokens(wallet, address0, token1_decimals, token1_total_amount) + + token2_decimals = 18 + token2_total_amount = random.randint(1000, 2000) + token2_id = await self.issue_and_mint_tokens(wallet, address0, token2_decimals, token2_total_amount) + + atoms_per_token1 = 10**token1_decimals + atoms_per_token2 = 10**token2_decimals + + ######################################################################################## + # Send tokens and coins to various addresses + + coins_on_ms_addr = random_decimal_amount(100, 200, COINS_NUM_DECIMALS) + await wallet.send_to_address(multisig_address, coins_on_ms_addr) + + token1_on_ms_addr = random_decimal_amount(100, 200, token1_decimals) + await wallet.send_tokens_to_address(token1_id, multisig_address, token1_on_ms_addr) + token1_on_addr1 = random_decimal_amount(100, 200, token1_decimals) + await wallet.send_tokens_to_address(token1_id, address1, token1_on_addr1) + token1_on_change_addr = token1_total_amount - token1_on_ms_addr - token1_on_addr1 + + token2_on_ms_addr = random_decimal_amount(100, 200, token2_decimals) + await wallet.send_tokens_to_address(token2_id, multisig_address, token2_on_ms_addr) + token2_on_addr2 = random_decimal_amount(100, 200, token2_decimals) + await wallet.send_tokens_to_address(token2_id, address2, token2_on_addr2) + token2_on_change_addr = token2_total_amount - token2_on_ms_addr - token2_on_addr2 + + # Note: this must be the last tx to ensure that this coins UTXO is not spent. + coins_on_addr0 = random_decimal_amount(100, 200, COINS_NUM_DECIMALS) + await wallet.send_to_address(address0, coins_on_addr0) + + self.generate_block() + assert_in("Success", await wallet.sync()) + + # There should be 2 normal UTXOs for coins - one on addr0 (coins_on_addr0) and one + # on a change address (the rest) + coins_balance = await wallet.get_coins_balance() + coins_on_change_addr = coins_balance - coins_on_addr0 + + atoms_per_asset = { + "coin": ATOMS_PER_COIN, + token1_id: atoms_per_token1, + token2_id: atoms_per_token2 + } + + # Return a list of tuples (asset_name, dest, decimal_amount), where asset_name is "coin" + # or a token id. + # Check that the decimal amount is consistent with the amount of atoms. + def check_and_simplify_utxos(utxos): + simplified_utxo = [] + for utxo in utxos: + assert_equal(utxo["output"]["type"], "Transfer") + + dest = utxo["output"]["content"]["destination"] + asset_type = utxo["output"]["content"]["value"]["type"] + amount = utxo["output"]["content"]["value"]["content"]["amount"] + asset_name = "coin" if asset_type == "Coin" else utxo["output"]["content"]["value"]["content"]["id"] + + decimal_amount = Decimal(amount["decimal"]) + atoms_amount = int(amount["atoms"]) + assert_equal(decimal_amount * atoms_per_asset[asset_name], atoms_amount) + + simplified_utxo.append((asset_name, dest, decimal_amount)) + return simplified_utxo + + ######################################################################################## + # Check normal UTXOs + + utxos = await wallet.list_utxos_raw() + utxos = check_and_simplify_utxos(utxos) + assert_equal(len(utxos), 6) + + # Check normal coin UTXOs + coin_utxos = [utxo for utxo in utxos if utxo[0] == "coin"] + assert_equal(len(coin_utxos), 2) + coin_addr0_utxo = next(utxo for utxo in coin_utxos if utxo[1] == address0) + coin_change_utxo = next(utxo for utxo in coin_utxos if utxo[1] != address0) + assert_equal(coin_addr0_utxo[2], coins_on_addr0) + assert_equal(coin_change_utxo[2], coins_on_change_addr) + + # Check normal token1 UTXOs + token1_utxos = [utxo for utxo in utxos if utxo[0] == token1_id] + assert_equal(len(token1_utxos), 2) + token1_addr1_utxo = next(utxo for utxo in token1_utxos if utxo[1] == address1) + token1_change_utxo = next(utxo for utxo in token1_utxos if utxo[1] != address1) + assert_equal(token1_addr1_utxo[2], token1_on_addr1) + assert_equal(token1_change_utxo[2], token1_on_change_addr) + + # Check normal token2 UTXOs + token2_utxos = [utxo for utxo in utxos if utxo[0] == token2_id] + assert_equal(len(token2_utxos), 2) + token2_addr2_utxo = next(utxo for utxo in token2_utxos if utxo[1] == address2) + token2_change_utxo = next(utxo for utxo in token2_utxos if utxo[1] != address2) + assert_equal(token2_addr2_utxo[2], token2_on_addr2) + assert_equal(token2_change_utxo[2], token2_on_change_addr) + + ######################################################################################## + # Check multisig UTXOs + + ms_utxos = await wallet.list_multisig_utxos_raw() + ms_utxos = check_and_simplify_utxos(ms_utxos) + assert_equal(len(ms_utxos), 3) + + # Check multisig coin UTXOs + coin_utxos = [utxo for utxo in ms_utxos if utxo[0] == "coin"] + assert_equal(len(coin_utxos), 1) + assert_equal(coin_utxos[0][1], multisig_address) + assert_equal(coin_utxos[0][2], coins_on_ms_addr) + + # Check multisig token1 UTXOs + token1_utxos = [utxo for utxo in ms_utxos if utxo[0] == token1_id] + assert_equal(len(token1_utxos), 1) + assert_equal(token1_utxos[0][1], multisig_address) + assert_equal(token1_utxos[0][2], token1_on_ms_addr) + + # Check multisig token2 UTXOs + token2_utxos = [utxo for utxo in ms_utxos if utxo[0] == token2_id] + assert_equal(len(token2_utxos), 1) + assert_equal(token2_utxos[0][1], multisig_address) + assert_equal(token2_utxos[0][2], token2_on_ms_addr) + + +if __name__ == '__main__': + WalletListUtxos().main() diff --git a/test/functional/wallet_list_utxos_rpc.py b/test/functional/wallet_list_utxos_rpc.py new file mode 100644 index 000000000..5d7172075 --- /dev/null +++ b/test/functional/wallet_list_utxos_rpc.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 RBB S.r.l +# Copyright (c) 2017-2021 The Bitcoin Core developers +# 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. +"""Wallet utxo listing test. + +Same as 'wallet_list_utxos', but for the corresponding RPC calls. +""" + +from wallet_list_utxos import WalletListUtxos +from test_framework.wallet_rpc_controller import WalletRpcController + + +class WalletListUtxosRpc(WalletListUtxos): + def set_test_params(self): + super().set_test_params() + self.wallet_controller = WalletRpcController + + def run_test(self): + super().run_test() + + +if __name__ == '__main__': + WalletListUtxosRpc().main() + diff --git a/test/functional/wallet_multisig_address.py b/test/functional/wallet_multisig_address.py index 482a82e3f..c160049c6 100644 --- a/test/functional/wallet_multisig_address.py +++ b/test/functional/wallet_multisig_address.py @@ -32,7 +32,6 @@ from test_framework.wallet_cli_controller import TxOutput, WalletCliController import asyncio -import sys import random diff --git a/test/functional/wallet_nfts.py b/test/functional/wallet_nfts.py index e61c7838a..47876abc8 100644 --- a/test/functional/wallet_nfts.py +++ b/test/functional/wallet_nfts.py @@ -28,13 +28,12 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random import string diff --git a/test/functional/wallet_order_double_fill_with_same_dest_impl.py b/test/functional/wallet_order_double_fill_with_same_dest_impl.py index 85723f28d..155192de3 100644 --- a/test/functional/wallet_order_double_fill_with_same_dest_impl.py +++ b/test/functional/wallet_order_double_fill_with_same_dest_impl.py @@ -30,7 +30,6 @@ from test_framework.wallet_rpc_controller import WalletRpcController import asyncio -import sys import random ATOMS_PER_TOKEN = 100 diff --git a/test/functional/wallet_orders_impl.py b/test/functional/wallet_orders_impl.py index 0bd7cd07a..449fcb285 100644 --- a/test/functional/wallet_orders_impl.py +++ b/test/functional/wallet_orders_impl.py @@ -33,7 +33,6 @@ from test_framework.wallet_rpc_controller import WalletRpcController import asyncio -import sys import random ATOMS_PER_TOKEN = 100 diff --git a/test/functional/wallet_pos_test_base.py b/test/functional/wallet_pos_test_base.py index 4a16d5443..58b8742aa 100644 --- a/test/functional/wallet_pos_test_base.py +++ b/test/functional/wallet_pos_test_base.py @@ -28,7 +28,6 @@ from test_framework.util import assert_equal import asyncio -import sys import time diff --git a/test/functional/wallet_recover_accounts.py b/test/functional/wallet_recover_accounts.py index 8376343af..6559d98ae 100644 --- a/test/functional/wallet_recover_accounts.py +++ b/test/functional/wallet_recover_accounts.py @@ -29,13 +29,12 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import DEFAULT_ACCOUNT_INDEX, WalletCliController import asyncio -import sys from random import randint diff --git a/test/functional/wallet_select_utxos.py b/test/functional/wallet_select_utxos.py index ceffc5b19..17c9ddf1b 100644 --- a/test/functional/wallet_select_utxos.py +++ b/test/functional/wallet_select_utxos.py @@ -27,13 +27,12 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input) +from test_framework.mintlayer import (make_tx, reward_input) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import UtxoOutpoint, WalletCliController import asyncio -import sys import random diff --git a/test/functional/wallet_set_lookahead_size.py b/test/functional/wallet_set_lookahead_size.py index 3569ef33f..f24ce601c 100644 --- a/test/functional/wallet_set_lookahead_size.py +++ b/test/functional/wallet_set_lookahead_size.py @@ -28,15 +28,13 @@ * check balance is back """ -import json from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random diff --git a/test/functional/wallet_sign_message.py b/test/functional/wallet_sign_message.py index ab14a1e88..06f5137aa 100644 --- a/test/functional/wallet_sign_message.py +++ b/test/functional/wallet_sign_message.py @@ -25,11 +25,10 @@ from random import choice, randint from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_in +from test_framework.util import assert_in from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import string class WalletSignMessage(BitcoinTestFramework): diff --git a/test/functional/wallet_submit_tx.py b/test/functional/wallet_submit_tx.py index a1a70e863..0217dc0d4 100644 --- a/test/functional/wallet_submit_tx.py +++ b/test/functional/wallet_submit_tx.py @@ -24,15 +24,13 @@ * check balance """ -import json from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random diff --git a/test/functional/wallet_sweep_address.py b/test/functional/wallet_sweep_address.py index 593bf37f8..0b53e20c5 100644 --- a/test/functional/wallet_sweep_address.py +++ b/test/functional/wallet_sweep_address.py @@ -26,13 +26,12 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (ATOMS_PER_COIN, make_tx, reward_input, tx_input) +from test_framework.mintlayer import (ATOMS_PER_COIN, make_tx, reward_input) from test_framework.util import assert_in, assert_equal, assert_not_in -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj -from test_framework.wallet_cli_controller import UtxoOutpoint, WalletCliController +from test_framework.mintlayer import block_input_data_obj +from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random diff --git a/test/functional/wallet_tokens.py b/test/functional/wallet_tokens.py index fef70023f..5a810d088 100644 --- a/test/functional/wallet_tokens.py +++ b/test/functional/wallet_tokens.py @@ -28,16 +28,14 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import DEFAULT_ACCOUNT_INDEX, WalletCliController import asyncio -import sys import random import string -import math class WalletTokens(BitcoinTestFramework): diff --git a/test/functional/wallet_tokens_change_authority.py b/test/functional/wallet_tokens_change_authority.py index eb1016e84..0897fd256 100644 --- a/test/functional/wallet_tokens_change_authority.py +++ b/test/functional/wallet_tokens_change_authority.py @@ -31,13 +31,12 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import DEFAULT_ACCOUNT_INDEX, WalletCliController import asyncio -import sys import random class WalletTokens(BitcoinTestFramework): diff --git a/test/functional/wallet_tokens_change_metadata_uri.py b/test/functional/wallet_tokens_change_metadata_uri.py index 555707c0c..57a1196a3 100644 --- a/test/functional/wallet_tokens_change_metadata_uri.py +++ b/test/functional/wallet_tokens_change_metadata_uri.py @@ -35,9 +35,7 @@ from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random -import string class WalletTokens(BitcoinTestFramework): diff --git a/test/functional/wallet_tokens_change_metadata_uri_rpc.py b/test/functional/wallet_tokens_change_metadata_uri_rpc.py index 5b50bc681..6b11cbb58 100644 --- a/test/functional/wallet_tokens_change_metadata_uri_rpc.py +++ b/test/functional/wallet_tokens_change_metadata_uri_rpc.py @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Wallet tokens change metadta uri test""" +"""Wallet tokens change metadata uri test""" from wallet_tokens_change_metadata_uri import WalletTokens from test_framework.wallet_rpc_controller import WalletRpcController diff --git a/test/functional/wallet_tokens_change_supply.py b/test/functional/wallet_tokens_change_supply.py index 0e6dfd999..def96b713 100644 --- a/test/functional/wallet_tokens_change_supply.py +++ b/test/functional/wallet_tokens_change_supply.py @@ -29,13 +29,12 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal, assert_not_in -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj -from test_framework.wallet_cli_controller import DEFAULT_ACCOUNT_INDEX, WalletCliController +from test_framework.mintlayer import block_input_data_obj +from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random class WalletTokens(BitcoinTestFramework): diff --git a/test/functional/wallet_tokens_freeze.py b/test/functional/wallet_tokens_freeze.py index 37cc70f18..cd75c0cc8 100644 --- a/test/functional/wallet_tokens_freeze.py +++ b/test/functional/wallet_tokens_freeze.py @@ -31,13 +31,12 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import DEFAULT_ACCOUNT_INDEX, WalletCliController import asyncio -import sys import random class WalletTokens(BitcoinTestFramework): diff --git a/test/functional/wallet_tokens_transfer_from_multisig_addr.py b/test/functional/wallet_tokens_transfer_from_multisig_addr.py index 8846d01c6..89652ce85 100644 --- a/test/functional/wallet_tokens_transfer_from_multisig_addr.py +++ b/test/functional/wallet_tokens_transfer_from_multisig_addr.py @@ -19,12 +19,10 @@ from test_framework.mintlayer import (block_input_data_obj, make_tx, reward_input, ATOMS_PER_COIN) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_in, assert_not_in, assert_equal -from test_framework.wallet_cli_controller import (TokenTxOutput, WalletCliController, DEFAULT_ACCOUNT_INDEX) +from test_framework.wallet_cli_controller import (TokenTxOutput, WalletCliController) import asyncio import random -import string -import sys from typing import List, Tuple diff --git a/test/functional/wallet_tx_compose.py b/test/functional/wallet_tx_compose.py index 527766518..5dbf6cb3d 100644 --- a/test/functional/wallet_tx_compose.py +++ b/test/functional/wallet_tx_compose.py @@ -28,15 +28,13 @@ * sign and submit the transaction """ -import json from test_framework.test_framework import BitcoinTestFramework -from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal -from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.mintlayer import block_input_data_obj from test_framework.wallet_cli_controller import DEFAULT_ACCOUNT_INDEX, TxOutput, WalletCliController import asyncio -import sys import random diff --git a/test/functional/wallet_tx_intent.py b/test/functional/wallet_tx_intent.py index bb5d965fa..9ec4f0188 100644 --- a/test/functional/wallet_tx_intent.py +++ b/test/functional/wallet_tx_intent.py @@ -31,7 +31,6 @@ from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random diff --git a/test/functional/wallet_watch_address.py b/test/functional/wallet_watch_address.py index fd63e3ebc..f6ff1ddad 100644 --- a/test/functional/wallet_watch_address.py +++ b/test/functional/wallet_watch_address.py @@ -33,7 +33,6 @@ from test_framework.wallet_cli_controller import WalletCliController import asyncio -import sys import random diff --git a/wallet/wallet-controller/src/helpers/mod.rs b/wallet/wallet-controller/src/helpers/mod.rs index f062e1c1a..238e51654 100644 --- a/wallet/wallet-controller/src/helpers/mod.rs +++ b/wallet/wallet-controller/src/helpers/mod.rs @@ -26,6 +26,7 @@ use common::{ address::RpcAddress, chain::{ htlc::HtlcSecret, + output_values_holder::collect_token_v1_ids_from_output_values_holder, tokens::{RPCTokenInfo, TokenId}, AccountCommand, ChainConfig, Destination, OrderAccountCommand, OrderId, PoolId, RpcOrderInfo, Transaction, TxInput, TxOutput, UtxoOutPoint, @@ -403,61 +404,8 @@ async fn fetch_order_additional_info( pub fn get_referenced_token_ids_from_partially_signed_transaction( ptx: &PartiallySignedTransaction, ) -> BTreeSet { - let mut result = BTreeSet::new(); - collect_referenced_token_ids_from_ptx(ptx, &mut result); - result -} - -fn collect_referenced_token_ids_from_ptx( - ptx: &PartiallySignedTransaction, - dest: &mut BTreeSet, -) { - for input_utxo in ptx.input_utxos().iter().flatten() { - collect_referenced_token_ids_from_tx_output(input_utxo, dest); - } - - for tx_output in ptx.tx().outputs() { - collect_referenced_token_ids_from_tx_output(tx_output, dest); - } - - for (_, order_info) in ptx.additional_info().order_info_iter() { - if let Some(token_id) = order_info.initially_asked.token_v1_id() { - dest.insert(*token_id); - } - - if let Some(token_id) = order_info.initially_given.token_v1_id() { - dest.insert(*token_id); - } - } -} - -fn collect_referenced_token_ids_from_tx_output(utxo: &TxOutput, dest: &mut BTreeSet) { - match utxo { - TxOutput::Burn(value) - | TxOutput::Transfer(value, _) - | TxOutput::LockThenTransfer(value, _, _) - | TxOutput::Htlc(value, _) => { - if let Some(token_id) = value.token_v1_id() { - dest.insert(*token_id); - } - } - TxOutput::CreateOrder(order) => { - if let Some(token_id) = order.ask().token_v1_id() { - dest.insert(*token_id); - } - - if let Some(token_id) = order.give().token_v1_id() { - dest.insert(*token_id); - } - } - TxOutput::ProduceBlockFromStake(_, _) - | TxOutput::IssueNft(_, _, _) - | TxOutput::IssueFungibleToken(_) - | TxOutput::CreateStakePool(_, _) - | TxOutput::DelegateStaking(_, _) - | TxOutput::CreateDelegationId(_, _) - | TxOutput::DataDeposit(_) => {} - } + // Note: currently a token can only be referenced via an OutputValue. + collect_token_v1_ids_from_output_values_holder(ptx) } #[cfg(test)] diff --git a/wallet/wallet-rpc-client/src/handles_client/mod.rs b/wallet/wallet-rpc-client/src/handles_client/mod.rs index e9f37a407..18e2bfeed 100644 --- a/wallet/wallet-rpc-client/src/handles_client/mod.rs +++ b/wallet/wallet-rpc-client/src/handles_client/mod.rs @@ -17,10 +17,12 @@ use std::{collections::BTreeMap, fmt::Debug, num::NonZeroUsize, path::PathBuf, s use chainstate::{rpc::RpcOutputValueIn, ChainInfo}; use common::{ - address::{dehexify::dehexify_all_addresses, AddressError}, + address::dehexify::dehexify_all_addresses, chain::{ - block::timestamp::BlockTimestamp, tokens::IsTokenUnfreezable, Block, GenBlock, - SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, + block::timestamp::BlockTimestamp, + output_values_holder::collect_token_v1_ids_from_output_values_holders, + tokens::IsTokenUnfreezable, Block, GenBlock, SignedTransaction, SignedTransactionIntent, + Transaction, TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id, Idable, H256}, }; @@ -78,7 +80,7 @@ pub enum WalletRpcHandlesClientError { HexEncodingError(#[from] hex::FromHexError), #[error(transparent)] - AddressError(#[from] AddressError), + ChainstateRpcTypeError(#[from] chainstate::rpc::RpcTypeError), } impl WalletRpcHandlesClient @@ -422,13 +424,21 @@ where .await .map_err(WalletRpcHandlesClientError::WalletRpcError)?; - utxos + let token_ids = + collect_token_v1_ids_from_output_values_holders(utxos.iter().map(|(_, output)| output)); + let token_decimals = self.wallet_rpc.get_tokens_decimals(token_ids).await?; + + Ok(utxos .into_iter() .map(|(utxo_outpoint, tx_ouput)| { - UtxoInfo::new(utxo_outpoint, tx_ouput, self.wallet_rpc.chain_config()) + UtxoInfo::new( + utxo_outpoint, + tx_ouput, + self.wallet_rpc.chain_config(), + &token_decimals, + ) }) - .collect::, _>>() - .map_err(WalletRpcHandlesClientError::AddressError) + .collect::, _>>()?) } async fn get_utxos( @@ -449,13 +459,21 @@ where .await .map_err(WalletRpcHandlesClientError::WalletRpcError)?; - utxos + let token_ids = + collect_token_v1_ids_from_output_values_holders(utxos.iter().map(|(_, output)| output)); + let token_decimals = self.wallet_rpc.get_tokens_decimals(token_ids).await?; + + Ok(utxos .into_iter() .map(|(utxo_outpoint, tx_ouput)| { - UtxoInfo::new(utxo_outpoint, tx_ouput, self.wallet_rpc.chain_config()) + UtxoInfo::new( + utxo_outpoint, + tx_ouput, + self.wallet_rpc.chain_config(), + &token_decimals, + ) }) - .collect::, _>>() - .map_err(WalletRpcHandlesClientError::AddressError) + .collect::, _>>()?) } async fn submit_raw_transaction( diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index a7ea04c44..9d7682b9d 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -27,7 +27,9 @@ use std::{ }; use chainstate::{ - rpc::RpcOutputValueIn, tx_verifier::check_transaction, ChainInfo, TokenIssuanceError, + rpc::{RpcOutputValueIn, TokenDecimals}, + tx_verifier::check_transaction, + ChainInfo, TokenIssuanceError, }; use common::{ address::Address, @@ -39,7 +41,9 @@ use common::{ signature::inputsig::arbitrary_message::{ produce_message_challenge, ArbitraryMessageSignature, }, - tokens::{IsTokenFreezable, IsTokenUnfreezable, Metadata, TokenId, TokenTotalSupply}, + tokens::{ + IsTokenFreezable, IsTokenUnfreezable, Metadata, RPCTokenInfo, TokenId, TokenTotalSupply, + }, Block, ChainConfig, DelegationId, Destination, GenBlock, OrderId, PoolId, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, @@ -756,6 +760,38 @@ where .await? } + pub async fn get_token_infos( + &self, + token_ids: BTreeSet, + ) -> WRpcResult, N> { + let mut result = BTreeMap::new(); + + // TODO: consider introducing a separate node RPC call that would fetch all token infos at once. + for token_id in token_ids { + let token_info = self + .node + .get_token_info(token_id) + .await + .map_err(RpcError::RpcError)? + .ok_or(RpcError::MissingTokenInfo(token_id))?; + result.insert(token_id, token_info); + } + + Ok(result) + } + + pub async fn get_tokens_decimals( + &self, + token_ids: BTreeSet, + ) -> WRpcResult, N> { + Ok(self + .get_token_infos(token_ids) + .await? + .iter() + .map(|(id, info)| (*id, TokenDecimals(info.token_number_of_decimals()))) + .collect()) + } + pub async fn get_transaction( &self, account_index: U31, diff --git a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs index 611572099..3788c4842 100644 --- a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs +++ b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs @@ -20,6 +20,7 @@ use common::{ address::dehexify::dehexify_all_addresses, chain::{ block::timestamp::BlockTimestamp, + output_values_holder::collect_token_v1_ids_from_output_values_holders, tokens::{IsTokenUnfreezable, TokenId}, Block, DelegationId, Destination, GenBlock, OrderId, PoolId, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, @@ -492,10 +493,15 @@ where ) .await?; + let token_ids = + collect_token_v1_ids_from_output_values_holders(utxos.iter().map(|(_, output)| output)); + let token_decimals = self.get_tokens_decimals(token_ids).await?; + let result = utxos .into_iter() .map(|(utxo_outpoint, tx_ouput)| { - let result = UtxoInfo::new(utxo_outpoint, tx_ouput, &self.chain_config); + let result = + UtxoInfo::new(utxo_outpoint, tx_ouput, &self.chain_config, &token_decimals); rpc::handle_result(result) }) .collect::, _>>(); @@ -513,10 +519,15 @@ where ) .await?; + let token_ids = + collect_token_v1_ids_from_output_values_holders(utxos.iter().map(|(_, output)| output)); + let token_decimals = self.get_tokens_decimals(token_ids).await?; + let result = utxos .into_iter() .map(|(utxo_outpoint, tx_ouput)| { - let result = UtxoInfo::new(utxo_outpoint, tx_ouput, &self.chain_config); + let result = + UtxoInfo::new(utxo_outpoint, tx_ouput, &self.chain_config, &token_decimals); rpc::handle_result(result) }) .collect::, _>>(); diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 3ff4dbbab..478f99b70 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -15,6 +15,7 @@ //! Types supporting the RPC interface +use chainstate::rpc::{RpcTypeError, TokenDecimalsProvider}; use common::{ address::{pubkeyhash::PublicKeyHash, Address, AddressError}, chain::{ @@ -171,6 +172,9 @@ pub enum RpcError { #[error("Wallet recovery requires mnemonic to be specified")] WalletRecoveryWithoutMnemonic, + + #[error("Token info missing for token {0:x}")] + MissingTokenInfo(TokenId), } impl From> for rpc::Error { @@ -389,9 +393,10 @@ impl UtxoInfo { outpoint: UtxoOutPoint, output: TxOutput, chain_config: &ChainConfig, - ) -> Result { + token_decimals_provider: &impl TokenDecimalsProvider, + ) -> Result { Ok(Self { - output: RpcTxOutput::new(chain_config, output)?, + output: RpcTxOutput::new(chain_config, token_decimals_provider, output)?, outpoint: RpcUtxoOutpoint::new(outpoint), }) }