From 8e929b93bd0969a4460bc1bedb9d99c340283733 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Tue, 2 Dec 2025 21:44:47 +0200 Subject: [PATCH 01/10] Test concluding and re-creating an order in the same tx --- .../test-suite/src/tests/orders_tests.rs | 373 +++++++++++++++++- 1 file changed, 370 insertions(+), 3 deletions(-) diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index e57718e83..e56aa3d82 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -39,9 +39,9 @@ use common::{ verify_signature, DestinationSigError, EvaluatedInputWitness, }, tokens::{IsTokenFreezable, TokenId, TokenTotalSupply}, - AccountCommand, AccountNonce, ChainstateUpgradeBuilder, Destination, OrderAccountCommand, - OrderData, OrderId, OrdersVersion, SignedTransaction, Transaction, TxInput, TxOutput, - UtxoOutPoint, + AccountCommand, AccountNonce, ChainstateUpgradeBuilder, Destination, IdCreationError, + OrderAccountCommand, OrderData, OrderId, OrdersVersion, SignedTransaction, Transaction, + TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Idable, H256}, }; @@ -3958,3 +3958,370 @@ fn fill_order_twice_in_same_block( ); }); } + +// Create and (optionally) partially fill an order. +// Then conclude it, while creating another order in the same tx, with balances equal to the +// remaining balances of the original order. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn conclude_and_recreate_in_same_tx_with_same_balances( + #[case] seed: Seed, + #[values(false, true)] fill_after_creation: bool, +) { + utils::concurrency::model(move || { + let version = OrdersVersion::V1; + let mut rng = make_seedable_rng(seed); + let mut tf = create_test_framework_with_orders(&mut rng, version); + + let tokens_amount = Amount::from_atoms(rng.gen_range(1000..1_000_000)); + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_amount_from_genesis(&mut rng, &mut tf, tokens_amount); + let coins_amount = tf.coin_amount_from_utxo(&coins_outpoint); + + let orig_ask_amount = Amount::from_atoms(rng.gen_range(10u128..10_000)); + let orig_give_amount = + Amount::from_atoms(rng.gen_range(10u128..=tokens_amount.into_atoms() / 2)); + let tokens_amount_after_order_creation = (tokens_amount - orig_give_amount).unwrap(); + + let orig_order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(orig_ask_amount), + OutputValue::TokenV1(token_id, orig_give_amount), + ); + let (orig_order_id, orig_order_creation_tx_id) = { + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(orig_order_data.clone()))) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, tokens_amount_after_order_creation), + Destination::AnyoneCanSpend, + )) + .build(); + let order_id = make_order_id(tx.inputs()).unwrap(); + let tx_id = tx.transaction().get_id(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + (order_id, tx_id) + }; + let tokens_outpoint = UtxoOutPoint::new(orig_order_creation_tx_id.into(), 1); + let tokens_amount = tokens_amount_after_order_creation; + + let (fill_amount, filled_amount, coins_outpoint, coins_amount) = if fill_after_creation { + // Fill the order partially. + let min_fill_amount = order_min_non_zero_fill_amount(&tf, &orig_order_id, version); + let fill_amount = Amount::from_atoms( + rng.gen_range(min_fill_amount.into_atoms()..orig_ask_amount.into_atoms()), + ); + let filled_amount = calculate_fill_order(&tf, &orig_order_id, fill_amount, version); + let coins_amount_after_fill = (coins_amount - fill_amount).unwrap(); + + let tx = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + orig_order_id, + fill_amount, + )), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, filled_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(coins_amount_after_fill), + Destination::AnyoneCanSpend, + )) + .build(); + let tx_id = tx.transaction().get_id(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + let coins_outpoint = UtxoOutPoint::new(tx_id.into(), 1); + let coins_amount = coins_amount_after_fill; + + (fill_amount, filled_amount, coins_outpoint, coins_amount) + } else { + (Amount::ZERO, Amount::ZERO, coins_outpoint, coins_amount) + }; + + let remaining_tokens_amount_to_trade = (orig_give_amount - filled_amount).unwrap(); + let remaining_coins_amount_to_trade = (orig_ask_amount - fill_amount).unwrap(); + + let new_order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(remaining_coins_amount_to_trade), + OutputValue::TokenV1(token_id, remaining_tokens_amount_to_trade), + ); + + // Try concluding the order and creating a new one, using only the conclusion account + // command as an input. + // This will fail, because order creation needs at least one UTXO input. + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(orig_order_id)), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(fill_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::CreateOrder(Box::new(new_order_data.clone()))) + .build(); + let err = tf + .make_block_builder() + .add_transaction(tx) + .build_and_process(&mut rng) + .unwrap_err(); + assert_eq!( + err, + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::IdCreationError( + IdCreationError::NoUtxoInputsForOrderIdCreation + ) + )) + ); + } + + // Check the original order data - it's still there + assert_eq!( + tf.chainstate.get_order_data(&orig_order_id).unwrap(), + Some(orig_order_data.clone().into()), + ); + assert_eq!( + tf.chainstate.get_order_ask_balance(&orig_order_id).unwrap(), + Some(remaining_coins_amount_to_trade), + ); + assert_eq!( + tf.chainstate.get_order_give_balance(&orig_order_id).unwrap(), + Some(remaining_tokens_amount_to_trade), + ); + + let new_order_id = { + let tx_builder = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(orig_order_id)), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(fill_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::CreateOrder(Box::new(new_order_data.clone()))); + // Add coins or tokens to inputs and transfer the same amount in outputs. + let tx_builder = if rng.gen_bool(0.5) { + tx_builder + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, tokens_amount), + Destination::AnyoneCanSpend, + )) + } else { + tx_builder + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::Transfer( + OutputValue::Coin(coins_amount), + Destination::AnyoneCanSpend, + )) + }; + let tx = tx_builder.build(); + let order_id = make_order_id(tx.inputs()).unwrap(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + order_id + }; + + // The original order is no longer there + assert_eq!(None, tf.chainstate.get_order_data(&orig_order_id).unwrap()); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&orig_order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&orig_order_id).unwrap() + ); + + // The new order exists and has the same balances as the original one before the conclusion. + assert_eq!( + tf.chainstate.get_order_data(&new_order_id).unwrap(), + Some(new_order_data.into()), + ); + assert_eq!( + tf.chainstate.get_order_ask_balance(&new_order_id).unwrap(), + Some(remaining_coins_amount_to_trade), + ); + assert_eq!( + tf.chainstate.get_order_give_balance(&new_order_id).unwrap(), + Some(remaining_tokens_amount_to_trade), + ); + }); +} + +// Create and (optionally) fill an order; the fill may be a complete fill if the give balance +// is supposed to be increased on the next step, otherwise it'll always be a partial fill. +// Then conclude the order, while creating another one in the same tx, with balances different +// from the remaining balances of the original order. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn conclude_and_recreate_in_same_tx_with_different_balances( + #[case] seed: Seed, + #[values(false, true)] fill_after_creation: bool, + #[values(false, true)] increase_give_balance: bool, +) { + utils::concurrency::model(move || { + let version = OrdersVersion::V1; + let mut rng = make_seedable_rng(seed); + let mut tf = create_test_framework_with_orders(&mut rng, version); + + let tokens_amount = Amount::from_atoms(rng.gen_range(1000..1_000_000)); + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_amount_from_genesis(&mut rng, &mut tf, tokens_amount); + let coins_amount = tf.coin_amount_from_utxo(&coins_outpoint); + + let orig_ask_amount = Amount::from_atoms(rng.gen_range(10u128..10_000)); + let orig_give_amount = + Amount::from_atoms(rng.gen_range(10u128..=tokens_amount.into_atoms() / 2)); + let tokens_amount_after_order_creation = (tokens_amount - orig_give_amount).unwrap(); + + let orig_order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(orig_ask_amount), + OutputValue::TokenV1(token_id, orig_give_amount), + ); + let (orig_order_id, orig_order_creation_tx_id) = { + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(orig_order_data.clone()))) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, tokens_amount_after_order_creation), + Destination::AnyoneCanSpend, + )) + .build(); + let order_id = make_order_id(tx.inputs()).unwrap(); + let tx_id = tx.transaction().get_id(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + (order_id, tx_id) + }; + let tokens_outpoint = UtxoOutPoint::new(orig_order_creation_tx_id.into(), 1); + let tokens_amount = tokens_amount_after_order_creation; + + let (fill_amount, filled_amount) = if fill_after_creation { + let fill_amount = if increase_give_balance && rng.gen_bool(0.5) { + // Fill the order completely. + orig_ask_amount + } else { + // Fill the order partially. + let min_fill_amount = order_min_non_zero_fill_amount(&tf, &orig_order_id, version); + Amount::from_atoms( + rng.gen_range(min_fill_amount.into_atoms()..orig_ask_amount.into_atoms()), + ) + }; + let filled_amount = calculate_fill_order(&tf, &orig_order_id, fill_amount, version); + let coins_amount_after_fill = (coins_amount - fill_amount).unwrap(); + + let tx = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + orig_order_id, + fill_amount, + )), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, filled_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(coins_amount_after_fill), + Destination::AnyoneCanSpend, + )) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + (fill_amount, filled_amount) + } else { + (Amount::ZERO, Amount::ZERO) + }; + + let remaining_tokens_amount_to_trade = (orig_give_amount - filled_amount).unwrap(); + let remaining_coins_amount_to_trade = (orig_ask_amount - fill_amount).unwrap(); + + let new_tokens_amount_to_trade = if increase_give_balance { + (remaining_tokens_amount_to_trade + + Amount::from_atoms(rng.gen_range(1u128..tokens_amount.into_atoms()))) + .unwrap() + } else { + Amount::from_atoms(rng.gen_range(1..=remaining_tokens_amount_to_trade.into_atoms())) + }; + + let new_coins_amount_to_trade = Amount::from_atoms( + rng.gen_range(1..=remaining_coins_amount_to_trade.into_atoms() * 2 + 100), + ); + + let new_order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(new_coins_amount_to_trade), + OutputValue::TokenV1(token_id, new_tokens_amount_to_trade), + ); + + let new_order_id = { + let tokens_atoms_change = new_tokens_amount_to_trade.into_atoms() as i128 + - remaining_tokens_amount_to_trade.into_atoms() as i128; + + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(orig_order_id)), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(fill_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::CreateOrder(Box::new(new_order_data.clone()))) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1( + token_id, + Amount::from_atoms( + (tokens_amount.into_atoms() as i128 - tokens_atoms_change) as u128, + ), + ), + Destination::AnyoneCanSpend, + )) + .build(); + + let order_id = make_order_id(tx.inputs()).unwrap(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + order_id + }; + + // The original order is no longer there + assert_eq!(None, tf.chainstate.get_order_data(&orig_order_id).unwrap()); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&orig_order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&orig_order_id).unwrap() + ); + + // The new order exists with the correct balances. + assert_eq!( + tf.chainstate.get_order_data(&new_order_id).unwrap(), + Some(new_order_data.into()), + ); + assert_eq!( + tf.chainstate.get_order_ask_balance(&new_order_id).unwrap(), + Some(new_coins_amount_to_trade), + ); + assert_eq!( + tf.chainstate.get_order_give_balance(&new_order_id).unwrap(), + Some(new_tokens_amount_to_trade), + ); + }); +} From c9e738178f4ed5ce55fead80e0e34b91e6ccacc2 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Fri, 12 Dec 2025 13:29:52 +0200 Subject: [PATCH 02/10] Edd wallet CLI commands to manage orders; add CLI/RPC command to list own orders --- CHANGELOG.md | 14 +- Cargo.lock | 2 + chainstate/src/detail/query.rs | 1 + chainstate/src/rpc/mod.rs | 2 +- chainstate/src/rpc/types/mod.rs | 3 + chainstate/src/rpc/types/output.rs | 52 ++++- common/src/address/mod.rs | 18 +- common/src/chain/order/rpc.rs | 13 +- .../chain/transaction/output/output_value.rs | 11 + .../output/output_values_holder.rs | 10 +- common/src/primitives/amount/decimal.rs | 27 +++ wallet/src/account/mod.rs | 31 ++- wallet/src/account/output_cache/mod.rs | 124 +++++++--- wallet/src/wallet/mod.rs | 12 +- wallet/src/wallet/tests.rs | 101 ++++---- wallet/types/src/currency.rs | 7 + wallet/wallet-cli-commands/Cargo.toml | 18 +- .../src/command_handler/mod.rs | 215 ++++++++++++++++-- wallet/wallet-cli-commands/src/errors.rs | 26 +++ .../wallet-cli-commands/src/helper_types.rs | 212 +++++++++++++++-- wallet/wallet-cli-commands/src/lib.rs | 84 +++++++ wallet/wallet-controller/src/helpers/tests.rs | 1 + wallet/wallet-controller/src/lib.rs | 9 +- wallet/wallet-controller/src/read.rs | 19 +- .../wallet-controller/src/runtime_wallet.rs | 23 +- .../src/synced_controller.rs | 2 +- wallet/wallet-controller/src/types/mod.rs | 21 +- .../src/handles_client/mod.rs | 24 +- .../src/rpc_client/client_impl.rs | 25 +- .../src/wallet_rpc_traits.rs | 19 +- wallet/wallet-rpc-lib/Cargo.toml | 1 + wallet/wallet-rpc-lib/src/rpc/interface.rs | 38 +++- wallet/wallet-rpc-lib/src/rpc/mod.rs | 149 +++++++++++- wallet/wallet-rpc-lib/src/rpc/server_impl.rs | 23 +- wallet/wallet-rpc-lib/src/rpc/types.rs | 39 ++++ 35 files changed, 1141 insertions(+), 235 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ae8ed84b..9c827dee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,24 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ ### Added - Node RPC: new method added - `chainstate_tokens_info`. + - Wallet RPC: + - new methods added: `node_get_tokens_info`, `order_list_own`; + + - Wallet CLI: + - the commands `order-create`, `order-fill`, `order-freeze`, `order-conclude` were added, + mirroring their existing RPC counterparts; + - other new commands added: `order-list-own`; + ### Changed - Wallet RPC: - `wallet_info`: the structure of the returned field `extra_info` was changed. + - `wallet_info`: the structure of the returned field `extra_info` was changed. + - `create_order`, `conclude_order`, `fill_order`, `freeze_order` were renamed to + `order_create`, `order_conclude`, `order_fill`, `order_freeze`. - The format of `PartiallySignedTransaction was changed again. + - Node RPC: the result of `chainstate_order_info` now also indicates whether the order is frozen. + ### Fixed - p2p: when a peer sends a message that can't be decoded, it will now be discouraged (which is what is normally done for misbehaving peers) and the node won't try connecting to it again.\ diff --git a/Cargo.lock b/Cargo.lock index c2f550c62..0287ab0f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9158,6 +9158,7 @@ dependencies = [ "consensus", "crossterm", "crypto", + "ctor", "derive_more 1.0.0", "directories", "dyn-clone", @@ -9349,6 +9350,7 @@ dependencies = [ "enum-iterator", "futures", "hex", + "itertools 0.14.0", "jsonrpsee", "logging", "mempool", diff --git a/chainstate/src/detail/query.rs b/chainstate/src/detail/query.rs index 24118b3f7..a2fc149ea 100644 --- a/chainstate/src/detail/query.rs +++ b/chainstate/src/detail/query.rs @@ -462,6 +462,7 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat give_balance, ask_balance, nonce, + is_frozen: order_data.is_frozen(), }; Ok(info) diff --git a/chainstate/src/rpc/mod.rs b/chainstate/src/rpc/mod.rs index 4bc3628db..058849d2c 100644 --- a/chainstate/src/rpc/mod.rs +++ b/chainstate/src/rpc/mod.rs @@ -47,7 +47,7 @@ use self::types::{block::RpcBlock, event::RpcEvent}; pub use types::{ input::RpcUtxoOutpoint, - output::{RpcOutputValueIn, RpcOutputValueOut, RpcTxOutput}, + output::{make_rpc_amount_out, RpcOutputValueIn, RpcOutputValueOut, RpcTxOutput}, signed_transaction::RpcSignedTransaction, token_decimals_provider::{TokenDecimals, TokenDecimalsProvider}, RpcTypeError, diff --git a/chainstate/src/rpc/types/mod.rs b/chainstate/src/rpc/types/mod.rs index ba4ed937f..2844b18d5 100644 --- a/chainstate/src/rpc/types/mod.rs +++ b/chainstate/src/rpc/types/mod.rs @@ -33,4 +33,7 @@ pub enum RpcTypeError { #[error("Token decimals unavailable for token {0:x}")] TokenDecimalsUnavailable(TokenId), + + #[error("Token V0 encountered")] + TokenV0Encountered, } diff --git a/chainstate/src/rpc/types/output.rs b/chainstate/src/rpc/types/output.rs index dbbadeac4..22021a95c 100644 --- a/chainstate/src/rpc/types/output.rs +++ b/chainstate/src/rpc/types/output.rs @@ -20,7 +20,10 @@ use common::{ timelock::OutputTimeLock, tokens::TokenId, ChainConfig, DelegationId, Destination, PoolId, TxOutput, }, - primitives::amount::{RpcAmountIn, RpcAmountOut}, + primitives::{ + amount::{RpcAmountIn, RpcAmountOut}, + Amount, + }, }; use crypto::vrf::VRFPublicKey; use rpc::types::RpcHexString; @@ -65,20 +68,57 @@ impl RpcOutputValueOut { OutputValue::Coin(amount) => RpcOutputValueOut::Coin { amount: RpcAmountOut::from_amount(amount, chain_config.coin_decimals()), }, - OutputValue::TokenV0(_) => unimplemented!(), + OutputValue::TokenV0(_) => return Err(RpcTypeError::TokenV0Encountered), OutputValue::TokenV1(token_id, amount) => RpcOutputValueOut::Token { id: RpcAddress::new(chain_config, token_id)?, amount: RpcAmountOut::from_amount( amount, - token_decimals_provider - .get_token_decimals(&token_id) - .ok_or(RpcTypeError::TokenDecimalsUnavailable(token_id))? - .0, + token_decimals(&token_id, token_decimals_provider)?, ), }, }; Ok(result) } + + pub fn amount(&self) -> &RpcAmountOut { + match self { + Self::Coin { amount } => amount, + Self::Token { id: _, amount } => amount, + } + } + + pub fn token_id(&self) -> Option<&RpcAddress> { + match self { + Self::Coin { amount: _ } => None, + Self::Token { id, amount: _ } => Some(id), + } + } +} + +fn token_decimals( + token_id: &TokenId, + token_decimals_provider: &impl TokenDecimalsProvider, +) -> Result { + Ok(token_decimals_provider + .get_token_decimals(token_id) + .ok_or(RpcTypeError::TokenDecimalsUnavailable(*token_id))? + .0) +} + +// FIXME put elsewhere? +pub fn make_rpc_amount_out( + amount: Amount, + token_id: Option<&TokenId>, + chain_config: &ChainConfig, + token_decimals_provider: &impl TokenDecimalsProvider, +) -> Result { + let decimals = if let Some(token_id) = token_id { + token_decimals(&token_id, token_decimals_provider)? + } else { + chain_config.coin_decimals() + }; + + Ok(RpcAmountOut::from_amount(amount, decimals)) } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rpc_description::HasValueHint)] diff --git a/common/src/address/mod.rs b/common/src/address/mod.rs index 0ec9cb7e0..f28a911e1 100644 --- a/common/src/address/mod.rs +++ b/common/src/address/mod.rs @@ -61,12 +61,7 @@ impl Address { address: impl Into, ) -> Result { let address = address.into(); - let data = bech32_encoding::bech32m_decode(&address)?; - let object = T::decode_from_bytes_from_address(data.data()) - .map_err(|e| AddressError::DecodingError(e.to_string()))?; - - let hrp_ok = data.hrp() == object.address_prefix(cfg); - ensure!(hrp_ok, AddressError::InvalidPrefix(data.hrp().to_owned())); + let object = decode_address(cfg, &address)?; Ok(Self { address, object }) } @@ -113,6 +108,17 @@ impl Display for Address { } } +pub fn decode_address(cfg: &ChainConfig, address: &str) -> Result { + let data = bech32_encoding::bech32m_decode(address)?; + let object = T::decode_from_bytes_from_address(data.data()) + .map_err(|e| AddressError::DecodingError(e.to_string()))?; + + let hrp_ok = data.hrp() == object.address_prefix(cfg); + ensure!(hrp_ok, AddressError::InvalidPrefix(data.hrp().to_owned())); + + Ok(object) +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/src/chain/order/rpc.rs b/common/src/chain/order/rpc.rs index 1da7002d6..6fca91f0c 100644 --- a/common/src/chain/order/rpc.rs +++ b/common/src/chain/order/rpc.rs @@ -16,7 +16,10 @@ use rpc_description::HasValueHint; use crate::{ - chain::{output_value::RpcOutputValue, AccountNonce, Destination}, + chain::{ + output_value::RpcOutputValue, output_values_holder::RpcOutputValuesHolder, AccountNonce, + Destination, + }, primitives::Amount, }; @@ -33,4 +36,12 @@ pub struct RpcOrderInfo { pub ask_balance: Amount, pub nonce: Option, + + pub is_frozen: bool, +} + +impl RpcOutputValuesHolder for RpcOrderInfo { + fn rpc_output_values_iter(&self) -> impl Iterator { + [&self.initially_asked, &self.initially_given].into_iter() + } } diff --git a/common/src/chain/transaction/output/output_value.rs b/common/src/chain/transaction/output/output_value.rs index cf3f381e8..ab1177646 100644 --- a/common/src/chain/transaction/output/output_value.rs +++ b/common/src/chain/transaction/output/output_value.rs @@ -126,6 +126,7 @@ impl RpcOutputValue { }), } } + pub fn amount(&self) -> Amount { match self { RpcOutputValue::Coin { amount } | RpcOutputValue::Token { id: _, amount } => *amount, @@ -138,6 +139,16 @@ impl RpcOutputValue { RpcOutputValue::Token { id, amount: _ } => Some(id), } } + + pub fn with_amount(self, new_amount: Amount) -> Self { + match self { + Self::Coin { amount: _ } => Self::Coin { amount: new_amount }, + Self::Token { id, amount: _ } => Self::Token { + id, + amount: new_amount, + }, + } + } } impl From for OutputValue { diff --git a/common/src/chain/transaction/output/output_values_holder.rs b/common/src/chain/transaction/output/output_values_holder.rs index 021585005..10b256f21 100644 --- a/common/src/chain/transaction/output/output_values_holder.rs +++ b/common/src/chain/transaction/output/output_values_holder.rs @@ -15,7 +15,10 @@ use std::collections::BTreeSet; -use crate::chain::{output_value::OutputValue, tokens::TokenId}; +use crate::chain::{ + output_value::{OutputValue, RpcOutputValue}, + 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. @@ -23,6 +26,11 @@ pub trait OutputValuesHolder { fn output_values_iter(&self) -> impl Iterator; } +/// A trait that will be implemented by types that contain one or more `RpcOutputValue`'s. +pub trait RpcOutputValuesHolder { + fn rpc_output_values_iter(&self) -> impl Iterator; +} + pub fn collect_token_v1_ids_from_output_values_holder_into( holder: &impl OutputValuesHolder, dest: &mut BTreeSet, diff --git a/common/src/primitives/amount/decimal.rs b/common/src/primitives/amount/decimal.rs index 2699f9b46..91d7fde94 100644 --- a/common/src/primitives/amount/decimal.rs +++ b/common/src/primitives/amount/decimal.rs @@ -100,6 +100,33 @@ impl DecimalAmount { } } +// FIXME tests, including one for with_decimals calls +/// Subtract two DecimalAmount's. +/// +/// This function is intended to be used with amounts that represent the same currency (i.e. when +/// the original `decimals` of both amounts were the same). +/// In this case it will only return None if the result would be negative. +/// +/// If the function is used with arbitrary unrelated amounts, it may also return None is a loss +/// of precision would occur, either during the final or an intermediate value calculation. +pub fn subtract_decimal_amounts_of_same_currency( + lhs: &DecimalAmount, + rhs: &DecimalAmount, +) -> Option { + // Remove extra zeroes, in case one of the amounts' decimals were artificially increased via `with_decimals` + // (in which case the later call "with_decimals(max_decimals)") may fail even if both amounts refer to the same + // currency). + let lhs = lhs.without_padding(); + let rhs = rhs.without_padding(); + + let max_decimals = std::cmp::max(lhs.decimals(), rhs.decimals()); + let lhs = lhs.with_decimals(max_decimals)?; + let rhs = rhs.with_decimals(max_decimals)?; + + let mantissa_diff = lhs.mantissa().checked_sub(rhs.mantissa())?; + Some(DecimalAmount::from_uint_decimal(mantissa_diff, max_decimals).without_padding()) +} + fn empty_to_zero(s: &str) -> &str { match s { "" => "0", diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 6675f2d31..bdb10515e 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -884,25 +884,24 @@ impl Account { Ok(req) } - pub fn get_pool_ids(&self, filter: WalletPoolsFilter) -> Vec<(PoolId, PoolData)> { - self.output_cache - .pool_ids() - .into_iter() - .filter(|(_, pool_data)| match filter { - WalletPoolsFilter::All => true, - WalletPoolsFilter::Decommission => { - self.key_chain.has_private_key_for_destination(&pool_data.decommission_key) - } - WalletPoolsFilter::Stake => { - self.key_chain.has_private_key_for_destination(&pool_data.stake_destination) - } - }) - .collect() + pub fn get_pools( + &self, + filter: WalletPoolsFilter, + ) -> impl Iterator { + self.output_cache.pools_iter().filter(move |(_, pool_data)| match filter { + WalletPoolsFilter::All => true, + WalletPoolsFilter::Decommission => { + self.key_chain.has_private_key_for_destination(&pool_data.decommission_key) + } + WalletPoolsFilter::Stake => { + self.key_chain.has_private_key_for_destination(&pool_data.stake_destination) + } + }) } pub fn get_delegations(&self) -> impl Iterator { self.output_cache - .delegation_ids() + .delegations_iter() .filter(|(_, data)| self.is_destination_mine(&data.destination)) } @@ -922,7 +921,7 @@ impl Account { pub fn get_orders(&self) -> impl Iterator { self.output_cache - .orders() + .orders_iter() .filter(|(_, data)| self.is_destination_mine(&data.conclude_key)) } diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index d5141e860..a1f404d93 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -23,16 +23,17 @@ use common::{ chain::{ block::timestamp::BlockTimestamp, make_delegation_id, make_order_id, make_token_id, make_token_id_with_version, - output_value::OutputValue, + output_value::{OutputValue, RpcOutputValue}, stakelock::StakePoolData, tokens::{ get_referenced_token_ids_ignore_issuance, IsTokenFreezable, IsTokenUnfreezable, RPCFungibleTokenInfo, RPCIsTokenFrozen, RPCNonFungibleTokenInfo, RPCTokenTotalSupply, TokenId, TokenIssuance, TokenTotalSupply, }, - AccountCommand, AccountNonce, AccountSpending, AccountType, ChainConfig, DelegationId, - Destination, GenBlock, OrderAccountCommand, OrderId, OutPointSourceId, PoolId, - TokenIdGenerationVersion, Transaction, TxInput, TxOutput, UtxoOutPoint, + AccountCommand, AccountCommandTag, AccountNonce, AccountSpending, AccountType, ChainConfig, + DelegationId, Destination, GenBlock, OrderAccountCommand, OrderAccountCommandTag, OrderId, + OutPointSourceId, PoolId, TokenIdGenerationVersion, Transaction, TxInput, TxOutput, + UtxoOutPoint, }, primitives::{id::WithId, per_thousand::PerThousand, Amount, BlockHeight, Id, Idable}, }; @@ -42,7 +43,6 @@ use rpc_description::HasValueHint; use tx_verifier::transaction_verifier::calculate_tokens_burned_in_outputs; use utils::ensure; use wallet_types::{ - currency::Currency, utxo_types::{get_utxo_state, UtxoState, UtxoStates}, wallet_tx::{TxData, TxState}, with_locked::WithLocked, @@ -526,24 +526,39 @@ impl TokenIssuanceData { } } +#[derive(Clone, Debug)] pub struct OrderData { pub conclude_key: Destination, - pub give_currency: Currency, - pub ask_currency: Currency, + pub initially_asked: RpcOutputValue, + pub initially_given: RpcOutputValue, + + /// Timestamp of order creation tx, if it is confirmed. + pub creation_timestamp: Option, pub last_nonce: Option, - /// last parent transaction if the parent is unconfirmed + /// Last parent transaction if the parent is unconfirmed. pub last_parent: Option, + + pub is_concluded: bool, + pub is_frozen: bool, } impl OrderData { - pub fn new(conclude_key: Destination, give_currency: Currency, ask_currency: Currency) -> Self { + pub fn new( + conclude_key: Destination, + initially_asked: RpcOutputValue, + initially_given: RpcOutputValue, + creation_timestamp: Option, + ) -> Self { Self { conclude_key, - give_currency, - ask_currency, + initially_asked, + initially_given, + creation_timestamp, last_nonce: None, last_parent: None, + is_concluded: false, + is_frozen: false, } } } @@ -625,14 +640,11 @@ impl OutputCache { .and_then(|tx| tx.outputs().get(outpoint.output_index() as usize)) } - pub fn pool_ids(&self) -> Vec<(PoolId, PoolData)> { + /// Return an iterator over pools that haven't been decommissioned yet. + pub fn pools_iter(&self) -> impl Iterator { self.pools .iter() - .filter_map(|(pool_id, pool_data)| { - (!self.consumed.contains_key(&pool_data.utxo_outpoint)) - .then_some((*pool_id, (*pool_data).clone())) - }) - .collect() + .filter(|(_, pool_data)| !self.consumed.contains_key(&pool_data.utxo_outpoint)) } fn is_txo_for_pool_id(pool_id_to_find: PoolId, output: &TxOutput) -> bool { @@ -672,7 +684,7 @@ impl OutputCache { self.pools.get(&pool_id).ok_or(WalletError::UnknownPoolId(pool_id)) } - pub fn delegation_ids(&self) -> impl Iterator { + pub fn delegations_iter(&self) -> impl Iterator { self.delegations.iter() } @@ -684,7 +696,8 @@ impl OutputCache { self.token_issuance.get(token_id) } - pub fn orders(&self) -> impl Iterator { + /// Return an iterator over all orders, including concluded ones. + pub fn orders_iter(&self) -> impl Iterator { self.orders.iter() } @@ -898,6 +911,14 @@ impl OutputCache { get_referenced_token_ids_ignore_issuance(output).contains(frozen_token_id) }; + let order_violates_frozen_token = |order_id| { + self.order_data(order_id).is_some_and(|data| { + [&data.initially_asked, &data.initially_given] + .into_iter() + .any(|v| v.token_id() == Some(frozen_token_id)) + }) + }; + let in_inputs = unconfirmed_tx.inputs().iter().any(|input| match input { TxInput::Utxo(outpoint) => self.txs.get(&outpoint.source_id()).is_some_and(|tx| { let output = @@ -915,23 +936,13 @@ impl OutputCache { AccountCommand::UnfreezeToken(_) => false, // Frozen token can be unfrozen AccountCommand::ConcludeOrder(order_id) | AccountCommand::FillOrder(order_id, _, _) => { - self.order_data(order_id).is_some_and(|data| { - [data.ask_currency, data.give_currency].iter().any(|v| match v { - Currency::Coin => false, - Currency::Token(token_id) => frozen_token_id == token_id, - }) - }) + order_violates_frozen_token(order_id) } }, TxInput::OrderAccountCommand(cmd) => match cmd { OrderAccountCommand::FillOrder(order_id, _) | OrderAccountCommand::ConcludeOrder(order_id) => { - self.order_data(order_id).is_some_and(|data| { - [data.ask_currency, data.give_currency].iter().any(|v| match v { - Currency::Coin => false, - Currency::Token(token_id) => frozen_token_id == token_id, - }) - }) + order_violates_frozen_token(order_id) } OrderAccountCommand::FreezeOrder(_) => false, }, @@ -1063,16 +1074,17 @@ impl OutputCache { TxOutput::IssueNft(_, _, _) => {} TxOutput::CreateOrder(order_data) => { let order_id = make_order_id(tx.inputs())?; - let give_currency = Currency::from_output_value(order_data.give()) + let initially_asked = RpcOutputValue::from_output_value(order_data.ask()) .ok_or(WalletError::TokenV0(tx.id()))?; - let ask_currency = Currency::from_output_value(order_data.ask()) + let initially_given = RpcOutputValue::from_output_value(order_data.give()) .ok_or(WalletError::TokenV0(tx.id()))?; self.orders.insert( order_id, OrderData::new( order_data.conclude_key().clone(), - give_currency, - ask_currency, + initially_asked, + initially_given, + block_info.map(|info| info.timestamp), ), ); } @@ -1169,11 +1181,18 @@ impl OutputCache { AccountCommand::ConcludeOrder(order_id) | AccountCommand::FillOrder(order_id, _, _) => { if !already_present { + let op_tag: AccountCommandTag = op.into(); + let cmd_tag = if op_tag == AccountCommandTag::ConcludeOrder { + OrderAccountCommandTag::ConcludeOrder + } else { + OrderAccountCommandTag::FillOrder + }; if let Some(data) = self.orders.get_mut(order_id) { Self::update_order_state( &mut self.unconfirmed_descendants, data, order_id, + cmd_tag, Some(*nonce), tx_id, )?; @@ -1191,6 +1210,7 @@ impl OutputCache { &mut self.unconfirmed_descendants, data, order_id, + cmd.into(), None, tx_id, )?; @@ -1270,6 +1290,7 @@ impl OutputCache { unconfirmed_descendants: &mut BTreeMap>, data: &mut OrderData, order_id: &OrderId, + command_tag: OrderAccountCommandTag, nonce: Option, tx_id: &OutPointSourceId, ) -> Result<(), WalletError> { @@ -1296,6 +1317,20 @@ impl OutputCache { descendants.insert(tx_id.clone()); } data.last_parent = Some(tx_id.clone()); + + match command_tag { + OrderAccountCommandTag::FillOrder => {} + OrderAccountCommandTag::FreezeOrder => { + // FIXME revise debug_asserts here and on revert + debug_assert!(!data.is_frozen); + data.is_frozen = true; + } + OrderAccountCommandTag::ConcludeOrder => { + debug_assert!(!data.is_concluded); + data.is_concluded = true; + } + } + Ok(()) } @@ -1569,6 +1604,12 @@ impl OutputCache { if let Some(data) = self.orders.get_mut(order_id) { data.last_nonce = nonce.decrement(); data.last_parent = find_parent(&self.unconfirmed_descendants, &tx_id); + + let op_tag: AccountCommandTag = op.into(); + if op_tag == AccountCommandTag::ConcludeOrder { + debug_assert!(data.is_concluded); + data.is_concluded = false; + } } } }, @@ -1578,6 +1619,19 @@ impl OutputCache { | OrderAccountCommand::ConcludeOrder(order_id) => { if let Some(data) = self.orders.get_mut(order_id) { data.last_parent = find_parent(&self.unconfirmed_descendants, &tx_id); + + let cmd_tag: OrderAccountCommandTag = cmd.into(); + match cmd_tag { + OrderAccountCommandTag::FillOrder => {} + OrderAccountCommandTag::FreezeOrder => { + debug_assert!(data.is_frozen); + data.is_frozen = false; + } + OrderAccountCommandTag::ConcludeOrder => { + debug_assert!(data.is_concluded); + data.is_concluded = false; + } + } } } }, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index cf95222be..22301345e 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -1421,13 +1421,16 @@ where }) } - pub fn get_pool_ids( + pub fn get_pools( &self, account_index: U31, filter: WalletPoolsFilter, ) -> WalletResult> { - let pool_ids = self.get_account(account_index)?.get_pool_ids(filter); - Ok(pool_ids) + Ok(self + .get_account(account_index)? + .get_pools(filter) + .map(|(id, data)| (*id, data.clone())) + .collect()) } pub fn get_delegations( @@ -2271,8 +2274,7 @@ where &self, account_index: U31, ) -> WalletResult> { - let orders = self.get_account(account_index)?.get_orders(); - Ok(orders) + Ok(self.get_account(account_index)?.get_orders()) } #[allow(clippy::too_many_arguments)] diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 23d0c6c4b..c03c9ffdf 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -1980,7 +1980,7 @@ async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let coin_balance = get_coin_balance(&wallet); @@ -2043,12 +2043,12 @@ async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); assert_eq!(pool_ids.len(), 1); let pool_ids = wallet - .get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) + .get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) .unwrap(); assert_eq!(pool_ids.len(), 1); @@ -2102,7 +2102,7 @@ async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { scan_wallet(&mut wallet, BlockHeight::new(2), vec![block3.clone()]); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); let (_pool_id, pool_data) = pool_ids.first().unwrap(); assert_eq!( @@ -2112,7 +2112,7 @@ async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { // do a reorg back to block 2 scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2.clone()]); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); let (pool_id, pool_data) = pool_ids.first().unwrap(); assert_eq!( @@ -2139,12 +2139,12 @@ async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { Amount::ZERO, 2, ); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); assert!(pool_ids.is_empty()); let pool_ids = wallet - .get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) + .get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) .unwrap(); assert!(pool_ids.is_empty()); @@ -2178,9 +2178,9 @@ async fn create_stake_pool_for_different_wallet_and_list_pool_ids(#[case] seed: let (_, block1) = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0); scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]); - let pool_ids1 = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids1 = wallet1.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids1.is_empty()); - let pool_ids2 = wallet2.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids2 = wallet2.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids2.is_empty()); let coin_balance1 = get_coin_balance(&wallet1); @@ -2283,19 +2283,19 @@ async fn create_stake_pool_for_different_wallet_and_list_pool_ids(#[case] seed: assert_eq!(coin_balance2, Amount::ZERO); let pool_ids_for_staking1 = - wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); + wallet1.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); assert!(pool_ids_for_staking1.is_empty()); let pool_ids_for_decommission1 = wallet1 - .get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) + .get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) .unwrap(); assert_eq!(pool_ids_for_decommission1.len(), 1); let pool_ids_for_decommission2 = wallet2 - .get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) + .get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) .unwrap(); assert!(pool_ids_for_decommission2.is_empty()); let pool_ids_for_staking2 = - wallet2.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); + wallet2.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); assert_eq!(pool_ids_for_staking2.len(), 1); assert_eq!(pool_ids_for_decommission1[0], pool_ids_for_staking2[0]); @@ -2364,19 +2364,19 @@ async fn create_stake_pool_for_different_wallet_and_list_pool_ids(#[case] seed: scan_wallet(&mut wallet2, BlockHeight::new(2), vec![block3.clone()]); let pool_ids_for_staking1 = - wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); + wallet1.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); assert!(pool_ids_for_staking1.is_empty()); let pool_ids_for_decommission1 = wallet1 - .get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) + .get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) .unwrap(); assert_eq!(pool_ids_for_decommission1.len(), 1); let pool_ids_for_decommission2 = wallet2 - .get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) + .get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) .unwrap(); assert!(pool_ids_for_decommission2.is_empty()); let pool_ids_for_staking2 = - wallet2.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); + wallet2.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); assert_eq!(pool_ids_for_staking2.len(), 1); assert_eq!(pool_ids_for_decommission1[0], pool_ids_for_staking2[0]); @@ -2417,9 +2417,9 @@ async fn create_stake_pool_for_different_wallet_and_list_pool_ids(#[case] seed: ); scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]); - let pool_ids1 = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids1 = wallet1.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids1.is_empty()); - let pool_ids2 = wallet2.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids2 = wallet2.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids2.is_empty()); let coin_balance1 = get_coin_balance(&wallet1); @@ -2668,7 +2668,7 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { let block1_amount = (chain_config.min_stake_pool_pledge() + delegation_amount).unwrap(); let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let coin_balance = get_coin_balance(&wallet); @@ -2705,7 +2705,7 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, (block1_amount - pool_amount).unwrap(),); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); let pool_id = pool_ids.first().unwrap().0; @@ -5024,7 +5024,7 @@ async fn decommission_pool_wrong_account(#[case] seed: Seed) { let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); - let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let coin_balance = get_coin_balance(&wallet); @@ -5062,7 +5062,7 @@ async fn decommission_pool_wrong_account(#[case] seed: Seed) { 1, ); - let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); // Try to decommission the pool with default account @@ -5126,7 +5126,7 @@ async fn decommission_pool_request_wrong_account(#[case] seed: Seed) { let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); - let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let coin_balance = get_coin_balance(&wallet); @@ -5164,7 +5164,7 @@ async fn decommission_pool_request_wrong_account(#[case] seed: Seed) { 1, ); - let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); // Try to create decommission request from account that holds the key @@ -5221,7 +5221,7 @@ async fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { let (addr, _) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); let utxo = make_address_output(addr.clone().into_object(), block1_amount); - let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let coin_balance = get_coin_balance(&wallet); @@ -5282,7 +5282,7 @@ async fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { assert_eq!(get_coin_balance(&wallet), Amount::ZERO); - let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); let pool_id = pool_ids.first().unwrap().0; @@ -5353,7 +5353,7 @@ async fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); let _ = create_block(&chain_config, &mut hot_wallet, vec![], block1_amount, 0); - let pool_ids = hot_wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = hot_wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let coin_balance = get_coin_balance(&hot_wallet); @@ -5389,7 +5389,7 @@ async fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { 1, ); - let pool_ids = hot_wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = hot_wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); let pool_id = pool_ids.first().unwrap().0; @@ -5468,10 +5468,10 @@ async fn filter_pools(#[case] seed: Seed) { let _ = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0); let _ = create_block(&chain_config, &mut wallet2, vec![], block1_amount, 0); - let pool_ids = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet1.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); - let pool_ids = wallet2.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet2.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let pool_amount = block1_amount; @@ -5511,27 +5511,27 @@ async fn filter_pools(#[case] seed: Seed) { ); // check wallet1 filter - let pool_ids = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet1.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); let pool_ids = wallet1 - .get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) + .get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) .unwrap(); assert_eq!(pool_ids.len(), 0); - let pool_ids = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); + let pool_ids = wallet1.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); assert_eq!(pool_ids.len(), 1); // check wallet2 filter - let pool_ids = wallet2.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet2.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); let pool_ids = wallet2 - .get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) + .get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Decommission) .unwrap(); assert_eq!(pool_ids.len(), 1); - let pool_ids = wallet2.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); + let pool_ids = wallet2.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); assert_eq!(pool_ids.len(), 0); } @@ -6418,6 +6418,7 @@ async fn create_order_and_conclude(#[case] seed: Seed) { give_balance: token_amount_to_mint, ask_balance: Amount::from_atoms(111), nonce: None, + is_frozen: false, }; let _ = create_block( @@ -6630,6 +6631,7 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { give_balance: sell_amount, ask_balance: token_amount_to_mint, nonce: None, + is_frozen: false, }; let (_, block4) = create_block( @@ -6734,6 +6736,7 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { give_balance: (sell_amount - Amount::from_atoms(100)).unwrap(), ask_balance: (token_amount_to_mint - Amount::from_atoms(10)).unwrap(), nonce: Some(AccountNonce::new(0)), + is_frozen: false, }; let additional_info = TxAdditionalInfo::new() @@ -6804,6 +6807,7 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { give_balance: Amount::ZERO, ask_balance: Amount::ZERO, nonce: Some(AccountNonce::new(1)), + is_frozen: false, }; let additional_info = TxAdditionalInfo::new() .with_token_info( @@ -7014,6 +7018,7 @@ async fn create_order_fill_partially_conclude(#[case] seed: Seed) { give_balance: sell_amount, ask_balance: token_amount_to_mint, nonce: None, + is_frozen: false, }; let (_, block4) = create_block( @@ -7118,6 +7123,7 @@ async fn create_order_fill_partially_conclude(#[case] seed: Seed) { give_balance: (sell_amount - Amount::from_atoms(100)).unwrap(), ask_balance: (token_amount_to_mint - Amount::from_atoms(10)).unwrap(), nonce: Some(AccountNonce::new(0)), + is_frozen: false, }; let additional_info = TxAdditionalInfo::new() @@ -7211,7 +7217,7 @@ async fn conflicting_delegation_account_nonce(#[case] seed: Seed) { let (_, block1) = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0); scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]); - let pool_ids = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet1.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let coin_balance = get_coin_balance(&wallet1); @@ -7250,7 +7256,7 @@ async fn conflicting_delegation_account_nonce(#[case] seed: Seed) { let coin_balance = get_coin_balance(&wallet1); assert_eq!(coin_balance, (block1_amount - pool_amount).unwrap(),); - let pool_ids = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet1.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); // Create a delegation @@ -7512,7 +7518,7 @@ async fn conflicting_delegation_account_nonce_same_wallet(#[case] seed: Seed) { let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); // Create a pool - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let coin_balance = get_coin_balance(&wallet); @@ -7549,7 +7555,7 @@ async fn conflicting_delegation_account_nonce_same_wallet(#[case] seed: Seed) { let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, (block1_amount - pool_amount).unwrap(),); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); // Create a delegation @@ -7888,6 +7894,7 @@ async fn conflicting_order_account_nonce(#[case] seed: Seed) { give_balance: sell_amount, ask_balance: buy_amount, nonce: None, + is_frozen: false, }; let order_additional_info_for_ptx = OrderAdditionalInfo { initially_asked: order_info.initially_asked.into(), @@ -8048,7 +8055,7 @@ async fn conflicting_delegation_account_nonce_multiple_inputs(#[case] seed: Seed let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let coin_balance = get_coin_balance(&wallet); @@ -8087,7 +8094,7 @@ async fn conflicting_delegation_account_nonce_multiple_inputs(#[case] seed: Seed let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, (block1_amount - pool_amount).unwrap(),); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); // Create a delegation @@ -8334,7 +8341,7 @@ async fn conflicting_delegation_account_with_reorg(#[case] seed: Seed) { let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let coin_balance = get_coin_balance(&wallet); @@ -8373,7 +8380,7 @@ async fn conflicting_delegation_account_with_reorg(#[case] seed: Seed) { let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, (block1_amount - pool_amount).unwrap(),); - let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); // Create a delegation diff --git a/wallet/types/src/currency.rs b/wallet/types/src/currency.rs index 1f4395978..42e3972e8 100644 --- a/wallet/types/src/currency.rs +++ b/wallet/types/src/currency.rs @@ -51,4 +51,11 @@ impl Currency { Currency::Token(id) => OutputValue::TokenV1(*id, amount), } } + + pub fn token_id(&self) -> Option<&TokenId> { + match self { + Currency::Coin => None, + Currency::Token(id) => Some(id), + } + } } diff --git a/wallet/wallet-cli-commands/Cargo.toml b/wallet/wallet-cli-commands/Cargo.toml index 55134d535..478ebda3f 100644 --- a/wallet/wallet-cli-commands/Cargo.toml +++ b/wallet/wallet-cli-commands/Cargo.toml @@ -14,40 +14,39 @@ consensus = { path = "../../consensus" } crypto = { path = "../../crypto" } logging = { path = "../../logging" } mempool = { path = "../../mempool" } -p2p-types = { path = "../../p2p/types" } node-comm = { path = "../wallet-node-client" } -rpc = { path = "../../rpc" } +p2p-types = { path = "../../p2p/types" } randomness = { path = "../../randomness" } +rpc = { path = "../../rpc" } serialization = { path = "../../serialization" } utils = { path = "../../utils" } utils-networking = { path = "../../utils/networking" } wallet = { path = ".." } wallet-controller = { path = "../wallet-controller" } -wallet-storage = { path = "../storage" } -wallet-types = { path = "../types" } wallet-rpc-lib = { path = "../wallet-rpc-lib" } wallet-rpc-client = { path = "../wallet-rpc-client" } +wallet-storage = { path = "../storage" } +wallet-types = { path = "../types" } -clap = { workspace = true, features = ["derive"] } async-trait.workspace = true +clap = { workspace = true, features = ["derive"] } crossterm.workspace = true derive_more.workspace = true directories.workspace = true dyn-clone.workspace = true +futures.workspace = true humantime.workspace = true hex.workspace = true itertools.workspace = true lazy_static.workspace = true -regex.workspace = true +prettytable-rs = "0.10" reedline = { workspace = true, features = ["external_printer"] } +regex.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true shlex.workspace = true thiserror.workspace = true tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } -futures.workspace = true - -prettytable-rs = "0.10" [dev-dependencies] blockprod = { path = "../../blockprod" } @@ -60,6 +59,7 @@ subsystem = { path = "../../subsystem" } test-utils = { path = "../../test-utils" } wallet-test-node = { path = "../wallet-test-node" } +ctor.workspace = true jsonrpsee.workspace = true rstest.workspace = true diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index f19c8a4a9..68152b177 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -17,38 +17,41 @@ mod local_state; use std::{fmt::Write, str::FromStr}; +use itertools::Itertools; + +use chainstate::rpc::RpcOutputValueOut; use common::{ - address::Address, + address::{Address, RpcAddress}, chain::{ - config::checkpoints_data::print_block_heights_ids_as_checkpoints_data, ChainConfig, - Destination, SignedTransaction, TxOutput, UtxoOutPoint, + config::checkpoints_data::print_block_heights_ids_as_checkpoints_data, tokens::TokenId, + ChainConfig, Destination, SignedTransaction, TxOutput, UtxoOutPoint, }, primitives::{Idable as _, H256}, text_summary::TextSummary, }; use crypto::key::hdkd::u31::U31; -use itertools::Itertools; use mempool::tx_options::TxOptionsOverrides; use node_comm::node_traits::NodeInterface; use serialization::{hex::HexEncode, hex_encoded::HexEncoded}; use utils::{ ensure, qrcode::{QrCode, QrCodeError}, + sorted::Sorted as _, }; use wallet::version::get_version; use wallet_controller::types::{GenericTokenTransfer, WalletExtraInfo}; use wallet_rpc_client::wallet_rpc_traits::{PartialOrSignedTx, WalletInterface}; use wallet_rpc_lib::types::{ Balances, ComposedTransaction, ControllerConfig, HardwareWalletType, MnemonicInfo, - NewSubmittedTransaction, NftMetadata, RpcInspectTransaction, RpcNewTransaction, - RpcSignatureStats, RpcSignatureStatus, RpcStandaloneAddressDetails, RpcValidatedSignatures, - TokenMetadata, + NewOrderTransaction, NewSubmittedTransaction, NftMetadata, RpcInspectTransaction, + RpcNewTransaction, RpcSignatureStats, RpcSignatureStatus, RpcStandaloneAddressDetails, + RpcValidatedSignatures, TokenMetadata, }; - -use wallet_types::partially_signed_transaction::PartiallySignedTransaction; +use wallet_types::{partially_signed_transaction::PartiallySignedTransaction, Currency}; use crate::{ - errors::WalletCliCommandError, helper_types::parse_generic_token_transfer, + errors::WalletCliCommandError, + helper_types::{format_token_name, parse_currency, parse_generic_token_transfer}, CreateWalletDeviceSelectMenu, ManageableWalletCommand, OpenWalletDeviceSelectMenu, OpenWalletSubCommand, WalletManagementCommand, }; @@ -57,8 +60,8 @@ use self::local_state::WalletWithState; use super::{ helper_types::{ - format_delegation_info, format_pool_info, parse_coin_output, parse_token_supply, - parse_utxo_outpoint, CliForceReduce, CliUtxoState, + format_delegation_info, format_own_order_info, format_pool_info, parse_coin_output, + parse_token_supply, parse_utxo_outpoint, CliForceReduce, CliUtxoState, }, ColdWalletCommand, ConsoleCommand, WalletCommand, }; @@ -125,8 +128,8 @@ where Ok(status) } - pub fn new_tx_command(new_tx: RpcNewTransaction, chain_config: &ChainConfig) -> ConsoleCommand { - let status_text = if new_tx.broadcasted { + fn new_tx_command_status_text(new_tx: RpcNewTransaction, chain_config: &ChainConfig) -> String { + if new_tx.broadcasted { let mut summary = new_tx.tx.take().transaction().text_summary(chain_config); format_fees(&mut summary, &new_tx.fees); @@ -136,9 +139,25 @@ where ) } else { format_tx_to_be_broadcasted(new_tx.tx, &new_tx.fees, chain_config) - }; + } + } - ConsoleCommand::Print(status_text) + pub fn new_tx_command(new_tx: RpcNewTransaction, chain_config: &ChainConfig) -> ConsoleCommand { + ConsoleCommand::Print(Self::new_tx_command_status_text(new_tx, chain_config)) + } + + pub fn new_order_tx_command( + new_tx: NewOrderTransaction, + chain_config: &ChainConfig, + ) -> Result> { + let (order_id, new_tx) = new_tx.into_order_id_and_new_tx(); + let order_id = order_id + .into_address(chain_config) + .map_err(|_| WalletCliCommandError::InvalidAddressInNewlyCreatedTransaction)?; + let status_text = Self::new_tx_command_status_text(new_tx, chain_config); + let status_text = format!("New order id: {order_id}\n\n{status_text}"); + + Ok(ConsoleCommand::Print(status_text)) } pub fn new_tx_submitted_command(new_tx: NewSubmittedTransaction) -> ConsoleCommand { @@ -1315,12 +1334,23 @@ where .await? .into_coins_and_tokens(); + let token_ids_str_vec = + tokens.iter().map(|(token_id, _)| token_id.as_str().to_owned()).collect_vec(); + + let token_infos = wallet + .node_get_tokens_info(token_ids_str_vec) + .await? + .into_iter() + .map(|info| (info.token_id(), info)) + .collect(); + let coins = coins.decimal(); let mut output = format!("Coins amount: {coins}\n"); for (token_id, amount) in tokens { + let token_name = format_token_name(&token_id, chain_config, &token_infos)?; let amount = amount.decimal(); - writeln!(&mut output, "Token: {token_id} amount: {amount}") + writeln!(&mut output, "Token: {token_name}, amount: {amount}") .expect("Writing to a memory buffer should not fail"); } output.pop(); @@ -1823,24 +1853,30 @@ where WalletCommand::ListPools => { let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; - let pool_ids: Vec<_> = wallet + let pool_infos: Vec<_> = wallet .list_staking_pools(selected_account) .await? .into_iter() .map(format_pool_info) .collect(); - Ok(ConsoleCommand::Print(format!("{}\n", pool_ids.join("\n")))) + Ok(ConsoleCommand::Print(format!( + "{}\n", + pool_infos.join("\n") + ))) } WalletCommand::ListOwnedPoolsForDecommission => { let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; - let pool_ids: Vec<_> = wallet + let pool_infos: Vec<_> = wallet .list_pools_for_decommission(selected_account) .await? .into_iter() .map(format_pool_info) .collect(); - Ok(ConsoleCommand::Print(format!("{}\n", pool_ids.join("\n")))) + Ok(ConsoleCommand::Print(format!( + "{}\n", + pool_infos.join("\n") + ))) } WalletCommand::ListDelegationIds => { @@ -1955,6 +1991,124 @@ where self.wallet().await?.remove_reserved_peer(address).await?; Ok(ConsoleCommand::Print("Success".to_owned())) } + WalletCommand::CreateOrder { + ask_currency, + ask_amount, + give_currency, + give_amount, + conclude_address, + } => { + let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; + + let parse_token_id = |curency_str: String| -> Result<_, WalletCliCommandError> { + let parsed_currency = parse_currency(&curency_str, chain_config)?; + match parsed_currency { + Currency::Coin => Ok(None), + Currency::Token(_) => Ok(Some(curency_str)), + } + }; + + let ask_token_id = parse_token_id(ask_currency)?; + let give_token_id = parse_token_id(give_currency)?; + let new_tx = wallet + .create_order( + selected_account, + ask_token_id, + ask_amount, + give_token_id, + give_amount, + conclude_address, + self.config, + ) + .await?; + Ok(Self::new_order_tx_command(new_tx, chain_config)?) + } + WalletCommand::FillOrder { + order_id, + amount, + output_address, + } => { + let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; + + let new_tx = wallet + .fill_order( + selected_account, + order_id, + amount, + output_address, + self.config, + ) + .await?; + Ok(Self::new_tx_command(new_tx, chain_config)) + } + WalletCommand::FreezeOrder { order_id } => { + let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; + + let new_tx = wallet.freeze_order(selected_account, order_id, self.config).await?; + Ok(Self::new_tx_command(new_tx, chain_config)) + } + WalletCommand::ConcludeOrder { + order_id, + output_address, + } => { + let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; + + let new_tx = wallet + .conclude_order(selected_account, order_id, output_address, self.config) + .await?; + Ok(Self::new_tx_command(new_tx, chain_config)) + } + WalletCommand::ListOwnOrders => { + let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; + + let order_infos = wallet.list_own_orders(selected_account).await?; + let token_ids_str_vec = get_token_ids_from_order_infos_as_str( + order_infos.iter().map(|info| (&info.initially_asked, &info.initially_given)), + ); + + let token_infos = wallet + .node_get_tokens_info(token_ids_str_vec) + .await? + .into_iter() + .map(|info| (info.token_id(), info)) + .collect(); + + // Sort the orders, so that the newer ones appear later. + let order_infos = order_infos.sorted_by(|info1, info2| { + let ts1 = + info1.existing_order_data.as_ref().map(|data| data.creation_timestamp); + let ts2 = + info2.existing_order_data.as_ref().map(|data| data.creation_timestamp); + + use std::cmp::Ordering; + + let ts_cmp_result = match (ts1, ts2) { + (Some(ts1), Some(ts2)) => ts1.cmp(&ts2), + // Note: the logic here is opposite to the normal comparison of Option's - + // we want None to be bigger than Some, so that "unconfirmed" orders + // appear later in the list. + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => Ordering::Equal, + }; + + if ts_cmp_result == Ordering::Equal { + info1.order_id.as_str().cmp(info2.order_id.as_str()) + } else { + ts_cmp_result + } + }); + + let order_infos = order_infos + .iter() + .map(|info| format_own_order_info(info, chain_config, &token_infos)) + .collect::, _>>()?; + + Ok(ConsoleCommand::Print(format!( + "{}\n", + order_infos.join("\n") + ))) + } } } @@ -1995,7 +2149,8 @@ fn format_signature_status((idx, status): (usize, &RpcSignatureStatus)) -> Strin RpcSignatureStatus::NotSigned => "NotSigned".to_owned(), RpcSignatureStatus::InvalidSignature => "InvalidSignature".to_owned(), RpcSignatureStatus::UnknownSignature => "UnknownSignature".to_owned(), - RpcSignatureStatus::PartialMultisig { required_signatures, num_signatures } => format!("PartialMultisig having {num_signatures} out of {required_signatures} required signatures"), + RpcSignatureStatus::PartialMultisig { required_signatures, num_signatures } => + format!("PartialMultisig having {num_signatures} out of {required_signatures} required signatures"), }; format!("Signature for input {idx}: {status}") @@ -2041,3 +2196,19 @@ where { wallet.get_wallet_with_acc().await } + +fn get_token_ids_from_order_infos<'a>( + initial_balances_iter: impl Iterator + Clone, +) -> impl Iterator> + Clone { + initial_balances_iter + .flat_map(|(balance1, balance2)| [balance1, balance2].into_iter()) + .flat_map(|balance| balance.token_id().into_iter()) +} + +fn get_token_ids_from_order_infos_as_str<'a>( + initial_balances_iter: impl Iterator + Clone, +) -> Vec { + get_token_ids_from_order_infos(initial_balances_iter) + .map(|rpc_addr| rpc_addr.as_str().to_owned()) + .collect_vec() +} diff --git a/wallet/wallet-cli-commands/src/errors.rs b/wallet/wallet-cli-commands/src/errors.rs index faf486fa9..da9361120 100644 --- a/wallet/wallet-cli-commands/src/errors.rs +++ b/wallet/wallet-cli-commands/src/errors.rs @@ -13,6 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use common::{ + address::{AddressError, RpcAddress}, + chain::OrderId, +}; use crypto::key::hdkd::u31::U31; use node_comm::node_traits::NodeInterface; use utils::qrcode::QrCodeError; @@ -24,30 +28,52 @@ use wallet_rpc_lib::RpcError; pub enum WalletCliCommandError { #[error("Invalid quoting")] InvalidQuoting, + #[error("{0}")] InvalidCommandInput(clap::Error), + #[error("Invalid input: {0}")] InvalidInput(String), + #[error("Please open or create a wallet file first")] NoWallet, + #[error("Account not found for index: {0}")] AccountNotFound(U31), + #[error("QR Code encoding error: {0}")] QrCodeEncoding(#[from] QrCodeError), + #[error("Error converting to json: {0}")] SerdeJsonFormatError(#[from] serde_json::Error), + #[error("{0}")] WalletRpcError(#[from] RpcError), + #[error("{0}")] WalletHandlessRpcError(#[from] WalletRpcHandlesClientError), + #[error("{0}")] WalletClientRpcError(#[from] WalletRpcError), + #[error("A new wallet has been opened between commands")] NewWalletWasOpened, + #[error("A different wallet than the existing one has been opened between commands")] DifferentWalletWasOpened, + #[error("The wallet has been closed between commands")] ExistingWalletWasClosed, + #[error("Invalid tx output: {0}")] InvalidTxOutput(GenericCurrencyTransferToTxOutputConversionError), + + #[error("Invalid address in a newly created transaction")] + InvalidAddressInNewlyCreatedTransaction, + + #[error("Error decoding token id: {0}")] + TokenIdDecodingError(AddressError), + + #[error("Accumulated ask amount for order {0} is negative")] + OrderNegativeAccumulatedAskAmount(RpcAddress), } diff --git a/wallet/wallet-cli-commands/src/helper_types.rs b/wallet/wallet-cli-commands/src/helper_types.rs index 90890e60c..35de5c95a 100644 --- a/wallet/wallet-cli-commands/src/helper_types.rs +++ b/wallet/wallet-cli-commands/src/helper_types.rs @@ -13,21 +13,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{fmt::Display, str::FromStr}; +use std::{collections::BTreeMap, fmt::Display, str::FromStr}; +use chainstate::rpc::RpcOutputValueOut; use clap::ValueEnum; use common::{ - address::Address, - chain::{ChainConfig, OutPointSourceId, TxOutput, UtxoOutPoint}, - primitives::{DecimalAmount, Id, H256}, + address::{decode_address, Address, RpcAddress}, + chain::{ + tokens::{RPCTokenInfo, TokenId}, + ChainConfig, OutPointSourceId, TxOutput, UtxoOutPoint, + }, + primitives::{ + amount::decimal::subtract_decimal_amounts_of_same_currency, DecimalAmount, Id, H256, + }, }; +use itertools::Itertools; use wallet_controller::types::{GenericCurrencyTransfer, GenericTokenTransfer}; -use wallet_rpc_lib::types::{NodeInterface, PoolInfo, TokenTotalSupply}; +use wallet_rpc_lib::types::{NodeInterface, OwnOrderInfo, PoolInfo, TokenTotalSupply}; use wallet_types::{ seed_phrase::StoreSeedPhrase, utxo_types::{UtxoState, UtxoType}, with_locked::WithLocked, + Currency, }; use crate::errors::WalletCliCommandError; @@ -100,15 +108,123 @@ impl CliUtxoState { } } -pub fn format_pool_info(pool_info: PoolInfo) -> String { +pub fn format_pool_info(info: PoolInfo) -> String { format!( - "Pool Id: {}, Pledge: {}, Balance: {}, Creation Block Height: {}, Creation block timestamp: {}, Staker: {}, Decommission Key: {}, VRF Public Key: {}", - pool_info.pool_id, pool_info.pledge.decimal(), pool_info.balance.decimal(), pool_info.height, pool_info.block_timestamp, pool_info.staker, pool_info.decommission_key, pool_info.vrf_public_key + concat!( + "Pool Id: {}, Pledge: {}, Balance: {}, Creation Block Height: {}, Creation block timestamp: {}, ", + "Staker: {}, Decommission Key: {}, VRF Public Key: {}" + ), + info.pool_id, info.pledge.decimal(), info.balance.decimal(), info.height, info.block_timestamp, + info.staker, info.decommission_key, info.vrf_public_key ) } +pub fn format_own_order_info( + order_info: &OwnOrderInfo, + chain_config: &ChainConfig, + token_infos: &BTreeMap, +) -> Result> { + if let Some(existing_order_data) = &order_info.existing_order_data { + let accumulated_ask_amount = subtract_decimal_amounts_of_same_currency( + &order_info.initially_asked.amount().decimal(), + &existing_order_data.ask_balance.decimal(), + ) + .ok_or_else(|| { + WalletCliCommandError::OrderNegativeAccumulatedAskAmount(order_info.order_id.clone()) + })?; + let status = if !existing_order_data.is_frozen + && !order_info.is_marked_as_frozen_in_wallet + && !order_info.is_marked_as_concluded_in_wallet + { + "Active".to_owned() + } else { + let frozen_status = match ( + existing_order_data.is_frozen, + order_info.is_marked_as_frozen_in_wallet, + ) { + // Note: it's technically possible for the order to be frozen but not marked as such + // in the wallet, e.g. when the wallet hasn't scanned the corresponding block yet. + (true, _) => Some("Frozen"), + (false, true) => Some("Frozen (unconfirmed)"), + (false, false) => None, + }; + let concluded_status = + order_info.is_marked_as_concluded_in_wallet.then_some("Concluded (unconfirmed)"); + + frozen_status.iter().chain(concluded_status.iter()).join(", ") + }; + Ok(format!( + concat!( + "Order Id: {id}, ", + "Initially asked: {ia}, ", + "Initially given: {ig}, ", + "Remaining ask amount: {ra}, ", + "Remaining give amount: {rg}, ", + "Accumulated ask amount: {aa}, ", + "Created at: {ts}, ", + "Status: {st}" + ), + id = order_info.order_id, + ia = format_output_value(&order_info.initially_asked, chain_config, token_infos)?, + ig = format_output_value(&order_info.initially_given, chain_config, token_infos)?, + ra = existing_order_data.ask_balance.decimal(), + rg = existing_order_data.give_balance.decimal(), + aa = accumulated_ask_amount, + ts = existing_order_data.creation_timestamp.into_time(), + st = status, + )) + } else { + Ok(format!( + concat!( + "Order Id: {id}, ", + "Initially asked: {ia}, ", + "Initially given: {ig}, ", + "Status: Unconfirmed" + ), + id = order_info.order_id, + ia = format_output_value(&order_info.initially_asked, chain_config, token_infos)?, + ig = format_output_value(&order_info.initially_given, chain_config, token_infos)?, + )) + } +} + +pub fn format_output_value( + value: &RpcOutputValueOut, + chain_config: &ChainConfig, + token_infos: &BTreeMap, +) -> Result> { + let asset_name = if let Some(token_id) = value.token_id() { + format_token_name(token_id, chain_config, token_infos)? + } else { + chain_config.coin_ticker().to_owned() + }; + + Ok(format!("{} {}", value.amount().decimal(), asset_name)) +} + +pub fn format_token_name( + token_id: &RpcAddress, + chain_config: &ChainConfig, + token_infos: &BTreeMap, +) -> Result> { + let decoded_token_id = token_id + .decode_object(chain_config) + .map_err(WalletCliCommandError::TokenIdDecodingError)?; + + let result = if let Some(token_ticker) = token_infos + .get(&decoded_token_id) + .and_then(|token_info| str::from_utf8(token_info.token_ticker()).ok()) + { + format!("{} ({token_ticker})", token_id.as_str()) + } else { + token_id.as_str().to_owned() + }; + + Ok(result) +} + pub fn format_delegation_info(delegation_id: String, balance: String) -> String { - format!("Delegation Id: {}, Balance: {}", delegation_id, balance,) + format!("Delegation Id: {}, Balance: {}", delegation_id, balance) } #[derive(Debug, Clone, Copy, ValueEnum)] @@ -228,14 +344,10 @@ pub fn parse_generic_currency_transfer( let destination = Address::from_string(chain_config, dest_str) .map_err(|err| { - WalletCliCommandError::::InvalidInput(format!("Invalid address {dest_str} {err}")) + WalletCliCommandError::::InvalidInput(format!("Invalid address '{dest_str}': {err}")) })? .into_object(); - - let amount = DecimalAmount::from_str(amount_str).map_err(|err| { - WalletCliCommandError::::InvalidInput(format!("Invalid amount {amount_str} {err}")) - })?; - + let amount = parse_decimal_amount(amount_str)?; let output = match name { "transfer" => GenericCurrencyTransfer { amount, @@ -276,21 +388,17 @@ pub fn parse_generic_token_transfer( let token_id = Address::from_string(chain_config, token_id_str) .map_err(|err| { WalletCliCommandError::::InvalidInput(format!( - "Invalid token id {token_id_str} {err}" + "Invalid token id '{token_id_str}': {err}" )) })? .into_object(); let destination = Address::from_string(chain_config, dest_str) .map_err(|err| { - WalletCliCommandError::::InvalidInput(format!("Invalid address {dest_str} {err}")) + WalletCliCommandError::::InvalidInput(format!("Invalid address '{dest_str}': {err}")) })? .into_object(); - - let amount = DecimalAmount::from_str(amount_str).map_err(|err| { - WalletCliCommandError::::InvalidInput(format!("Invalid amount {amount_str} {err}")) - })?; - + let amount = parse_decimal_amount(amount_str)?; let output = match name { "transfer" => GenericTokenTransfer { token_id, @@ -380,7 +488,7 @@ fn parse_fixed_token_supply( )?)) } else { Err(WalletCliCommandError::::InvalidInput(format!( - "Failed to parse token supply from {input}" + "Failed to parse token supply from '{input}'" ))) } } @@ -394,6 +502,31 @@ fn parse_token_amount( Ok(amount.into()) } +/// Parse a decimal amount +pub fn parse_decimal_amount( + input: &str, +) -> Result> { + DecimalAmount::from_str(input).map_err(|_| { + WalletCliCommandError::InvalidInput(format!("Invalid decimal amount: '{input}'")) + }) +} + +/// Try parsing the passed input as coins (case-insensitive "coin" is accepted) or +/// as a token id. +pub fn parse_currency( + input: &str, + chain_config: &ChainConfig, +) -> Result> { + if input.eq_ignore_ascii_case("coin") { + Ok(Currency::Coin) + } else { + let token_id = decode_address::(chain_config, input).map_err(|_| { + WalletCliCommandError::InvalidInput(format!("Invalid currency: '{input}'")) + })?; + Ok(Currency::Token(token_id)) + } +} + #[derive(Debug, Clone, Copy, ValueEnum)] pub enum CliIsFreezable { NotFreezable, @@ -653,4 +786,37 @@ mod tests { parse_assert_error(&format!("transfer {token_id_as_addr},{addr},{amount}")); } } + + #[test] + fn test_parse_currency() { + let chain_config = chain::config::create_unit_test_config(); + + let currency = parse_currency::("coin", &chain_config).unwrap(); + assert_eq!(currency, Currency::Coin); + let currency = parse_currency::("cOiN", &chain_config).unwrap(); + assert_eq!(currency, Currency::Coin); + + let err = parse_currency::("coins", &chain_config).unwrap_err(); + assert_matches!(err, WalletCliCommandError::InvalidInput(_)); + + let currency = parse_currency::( + "rmltk1ktt2slkqdy607kzhueudqucqphjzl7kl506xf78f9w7v00ydythqzgwlyp", + &chain_config, + ) + .unwrap(); + assert_eq!( + currency, + Currency::Token(TokenId::new( + H256::from_str("b2d6a87ec06934ff5857e678d073000de42ffadfa3f464f8e92bbcc7bc8d22ee") + .unwrap() + )) + ); + + let err = parse_currency::( + "rpool1zg7yccqqjlz38cyghxlxyp5lp36vwecu2g7gudrf58plzjm75tzq99fr6v", + &chain_config, + ) + .unwrap_err(); + assert_matches!(err, WalletCliCommandError::InvalidInput(_)); + } } diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index 59672720c..edf7501f7 100644 --- a/wallet/wallet-cli-commands/src/lib.rs +++ b/wallet/wallet-cli-commands/src/lib.rs @@ -1066,6 +1066,85 @@ pub enum WalletCommand { /// Transaction id, encoded in hex transaction_id: HexEncoded>, }, + + /// Create an order for exchanging an amount of one ("given") currency for a certain amount of + /// another ("asked") currency. + /// + /// Either of the currencies can be coins or tokens. + /// The ratio between the asked and given amounts determines the exchange rate. + /// + /// The entire given amount of the "given" currency will be locked inside the order, so that + /// it can be given away when the order is filled. + /// + /// The accumulated "ask" and the remaining "give" amounts can be withdrawn by concluding + /// the order. + #[clap(name = "order-create")] + CreateOrder { + /// The currency you are asking for - a token id or "coin" for coins. + ask_currency: String, + /// The asked amount. + ask_amount: DecimalAmount, + /// The currency you will be giving - a token id or "coin" for coins. + give_currency: String, + /// The given amount. + give_amount: DecimalAmount, + /// The address (key) that can authorize the conclusion and freezing of the order. + conclude_address: String, + }, + + /// Fill the specified order with the specified amount of the order's "asked" currency, + /// receiving the corresponding amount of the "given" currency. + #[clap(name = "order-fill")] + FillOrder { + /// The id of the order to fill. + order_id: String, + + /// The amount of the "asked" currency to fill the order with. + amount: DecimalAmount, + + /// The address where the corresponding amount of the order's "given" currency should be + /// transferred to. + /// If not specified, a new receive address will be generated for this purpose. + output_address: Option, + }, + + /// Freeze the order. + /// + /// Once an order is frozen, it can no longer be filled. The only possible operation for + /// a frozen order is conclusion. + /// + /// Note that freezing an order is optional, you can conclude a non-frozen order too. + /// However it may not succeed if the order is still being actively filled. In such a case + /// you may want to freeze the order first, wait for the corresponding transaction to be + /// included in a block and then conclude the order. + /// + /// To be able to freeze an order, the order's conclude key must be owned by the selected + /// account. + #[clap(name = "order-freeze")] + FreezeOrder { + /// The id of the order to freeze. + order_id: String, + }, + + /// Conclude the order, withdrawing the accumulated "asked" and the remaining "given" + /// amounts. + /// + /// To be able to conclude an order, the order's conclude key must be owned by the selected + /// account. + #[clap(name = "order-conclude")] + ConcludeOrder { + /// The id of the order to conclude. + order_id: String, + + /// The address where the accumulated "asked" amount and the remaining "given" amount + /// should be transferred to. If not specified, a new receive address will be generated + /// for this purpose. + output_address: Option, + }, + + /// List orders whose conclude key is owned by the selected account. + #[clap(name = "order-list-own")] + ListOwnOrders, } #[derive(Debug, Parser)] @@ -1378,6 +1457,11 @@ mod tests { use super::*; + #[ctor::ctor] + fn init() { + logging::init_logging(); + } + #[rstest] fn ensure_commands_have_description( #[values(false, true)] cold_wallet: bool, diff --git a/wallet/wallet-controller/src/helpers/tests.rs b/wallet/wallet-controller/src/helpers/tests.rs index eaad6fe66..77f882003 100644 --- a/wallet/wallet-controller/src/helpers/tests.rs +++ b/wallet/wallet-controller/src/helpers/tests.rs @@ -834,6 +834,7 @@ mod tx_to_partially_signed_tx_general_test { give_balance: data.give_balance, ask_balance: data.ask_balance, nonce: Some(AccountNonce::new(rng.gen())), + is_frozen: false, } } } diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 3d08f48d5..c5c58a415 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -591,13 +591,6 @@ where } } - pub async fn get_token_number_of_decimals( - &self, - token_id: TokenId, - ) -> Result> { - Ok(self.get_token_info(token_id).await?.token_number_of_decimals()) - } - pub async fn get_token_info( &self, token_id: TokenId, @@ -655,7 +648,7 @@ where ) -> Result> { let pools = self .wallet - .get_pool_ids(account_index, WalletPoolsFilter::Stake) + .get_pools(account_index, WalletPoolsFilter::Stake) .map_err(ControllerError::WalletError)?; let mut last_error = ControllerError::NoStakingPool; diff --git a/wallet/wallet-controller/src/read.rs b/wallet/wallet-controller/src/read.rs index 494a24cf8..aeebbe38d 100644 --- a/wallet/wallet-controller/src/read.rs +++ b/wallet/wallet-controller/src/read.rs @@ -17,9 +17,14 @@ use std::collections::BTreeMap; +use futures::{stream::FuturesUnordered, FutureExt, TryStreamExt}; + use common::{ address::Address, - chain::{ChainConfig, DelegationId, Destination, PoolId, Transaction, TxOutput, UtxoOutPoint}, + chain::{ + ChainConfig, DelegationId, Destination, OrderId, PoolId, Transaction, TxOutput, + UtxoOutPoint, + }, primitives::{id::WithId, Amount, Id}, }; use crypto::{ @@ -29,7 +34,6 @@ use crypto::{ }, vrf::VRFPublicKey, }; -use futures::{stream::FuturesUnordered, FutureExt, TryStreamExt}; use node_comm::node_traits::NodeInterface; use utils::tap_log::TapLog; use wallet::{ @@ -313,12 +317,12 @@ where ) -> Result, ControllerError> { let pools = self .wallet - .get_pool_ids(self.account_index, filter) + .get_pools(self.account_index, filter) .map_err(ControllerError::WalletError)?; let tasks: FuturesUnordered<_> = pools .into_iter() - .map(|(pool_id, block_info)| self.get_pool_info(pool_id, block_info)) + .map(|(pool_id, pool_data)| self.get_pool_info(pool_id, pool_data)) .collect(); tasks.try_collect().await @@ -418,4 +422,11 @@ where .map(|opt_balance| opt_balance.map(|balance| (delegation_id, balance))) .log_err() } + + /// Return info about all orders owned by the selected account. + pub async fn get_own_orders( + &self, + ) -> Result, ControllerError> { + self.wallet.get_orders(self.account_index).map_err(ControllerError::WalletError) + } } diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs index 4b91db661..c093158ae 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -40,8 +40,8 @@ use crypto::{ use mempool::FeeRate; use wallet::{ account::{ - transaction_list::TransactionList, CoinSelectionAlgo, DelegationData, PoolData, TxInfo, - UnconfirmedTokenInfo, + transaction_list::TransactionList, CoinSelectionAlgo, DelegationData, OrderData, PoolData, + TxInfo, UnconfirmedTokenInfo, }, destination_getters::HtlcSpendingCondition, send_request::{SelectedInputs, StakePoolCreationArguments}, @@ -243,15 +243,15 @@ where } } - pub fn get_pool_ids( + pub fn get_pools( &self, account_index: U31, filter: WalletPoolsFilter, ) -> WalletResult> { match self { - RuntimeWallet::Software(w) => w.get_pool_ids(account_index, filter), + RuntimeWallet::Software(w) => w.get_pools(account_index, filter), #[cfg(feature = "trezor")] - RuntimeWallet::Trezor(w) => w.get_pool_ids(account_index, filter), + RuntimeWallet::Trezor(w) => w.get_pools(account_index, filter), } } @@ -1445,6 +1445,19 @@ where } } + pub fn get_orders(&self, account_index: U31) -> WalletResult> { + let orders = match self { + RuntimeWallet::Software(w) => { + w.get_orders(account_index)?.map(|(id, data)| (*id, data.clone())).collect() + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.get_orders(account_index)?.map(|(id, data)| (*id, data.clone())).collect() + } + }; + Ok(orders) + } + pub async fn sign_raw_transaction( &mut self, account_index: U31, diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index 22698e55f..d0541b78a 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -1420,7 +1420,7 @@ where pub fn start_staking(&mut self) -> Result<(), ControllerError> { utils::ensure!(!self.wallet.is_locked(), ControllerError::WalletIsLocked); // Make sure that account_index is valid and that pools exist - let pool_ids = self.wallet.get_pool_ids(self.account_index, WalletPoolsFilter::Stake)?; + let pool_ids = self.wallet.get_pools(self.account_index, WalletPoolsFilter::Stake)?; utils::ensure!(!pool_ids.is_empty(), ControllerError::NoStakingPool); log::info!("Start staking, account_index: {}", self.account_index); self.staking_started.insert(self.account_index); diff --git a/wallet/wallet-controller/src/types/mod.rs b/wallet/wallet-controller/src/types/mod.rs index 79132bfe6..f55551550 100644 --- a/wallet/wallet-controller/src/types/mod.rs +++ b/wallet/wallet-controller/src/types/mod.rs @@ -23,10 +23,8 @@ mod transaction; use std::collections::BTreeSet; -pub use balances::Balances; use bip39::{Language, Mnemonic}; -pub use block_info::{BlockInfo, CreatedBlockInfo}; -pub use common::primitives::amount::RpcAmountOut; + use common::{ chain::{ output_value::OutputValue, @@ -35,12 +33,6 @@ use common::{ }, primitives::{DecimalAmount, H256}, }; -pub use seed_phrase::SeedWithPassPhrase; -pub use standalone_key::AccountStandaloneKeyDetails; -pub use transaction::{ - InspectTransaction, NewTransaction, PreparedTransaction, SignatureStats, TransactionToInspect, - ValidatedSignatures, -}; use utils::ensure; use wallet::signer::trezor_signer::FoundDevice; use wallet_types::{ @@ -51,6 +43,17 @@ use wallet_types::{ use crate::mnemonic; +pub use common::primitives::amount::RpcAmountOut; + +pub use balances::Balances; +pub use block_info::{BlockInfo, CreatedBlockInfo}; +pub use seed_phrase::SeedWithPassPhrase; +pub use standalone_key::AccountStandaloneKeyDetails; +pub use transaction::{ + InspectTransaction, NewTransaction, PreparedTransaction, SignatureStats, TransactionToInspect, + ValidatedSignatures, +}; + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rpc_description::HasValueHint)] pub struct WalletInfo { pub wallet_id: H256, diff --git a/wallet/wallet-rpc-client/src/handles_client/mod.rs b/wallet/wallet-rpc-client/src/handles_client/mod.rs index 18e2bfeed..456115366 100644 --- a/wallet/wallet-rpc-client/src/handles_client/mod.rs +++ b/wallet/wallet-rpc-client/src/handles_client/mod.rs @@ -21,8 +21,9 @@ use common::{ chain::{ block::timestamp::BlockTimestamp, output_values_holder::collect_token_v1_ids_from_output_values_holders, - tokens::IsTokenUnfreezable, Block, GenBlock, SignedTransaction, SignedTransactionIntent, - Transaction, TxOutput, UtxoOutPoint, + tokens::{IsTokenUnfreezable, RPCTokenInfo}, + Block, GenBlock, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, + UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id, Idable, H256}, }; @@ -46,7 +47,7 @@ use wallet_rpc_lib::{ ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegationTransaction, NewOrderTransaction, NewSubmittedTransaction, NewTokenTransaction, NftMetadata, NodeVersion, OpenedWallet, - PoolInfo, PublicKeyInfo, RpcHashedTimelockContract, RpcInspectTransaction, + OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcHashedTimelockContract, RpcInspectTransaction, RpcNewTransaction, RpcPreparedTransaction, RpcStandaloneAddresses, SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, TxOptionsOverrides, UtxoInfo, @@ -1209,6 +1210,13 @@ where .map_err(WalletRpcHandlesClientError::WalletRpcError) } + async fn list_own_orders(&self, account_index: U31) -> Result, Self::Error> { + self.wallet_rpc + .list_own_orders(account_index) + .await + .map_err(WalletRpcHandlesClientError::WalletRpcError) + } + async fn node_version(&self) -> Result { self.wallet_rpc .node_version() @@ -1532,4 +1540,14 @@ where .await .map_err(WalletRpcHandlesClientError::WalletRpcError) } + + async fn node_get_tokens_info( + &self, + token_ids: Vec, + ) -> Result, Self::Error> { + self.wallet_rpc + .node_get_tokens_info(token_ids.into_iter().map(Into::into)) + .await + .map_err(WalletRpcHandlesClientError::WalletRpcError) + } } diff --git a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs index eaca9d165..23976bd9a 100644 --- a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs @@ -24,7 +24,7 @@ use super::{ClientWalletRpc, WalletRpcError}; use chainstate::{rpc::RpcOutputValueIn, ChainInfo}; use common::{ chain::{ - block::timestamp::BlockTimestamp, Block, GenBlock, SignedTransaction, + block::timestamp::BlockTimestamp, tokens::RPCTokenInfo, Block, GenBlock, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id}, @@ -49,7 +49,7 @@ use wallet_rpc_lib::{ ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegationTransaction, NewOrderTransaction, NewSubmittedTransaction, NewTokenTransaction, NftMetadata, NodeVersion, OpenedWallet, - PoolInfo, PublicKeyInfo, RpcHashedTimelockContract, RpcInspectTransaction, + OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcHashedTimelockContract, RpcInspectTransaction, RpcNewTransaction, RpcPreparedTransaction, RpcStandaloneAddresses, SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, TransactionOptions, TransactionRequestOptions, @@ -1121,6 +1121,12 @@ impl WalletInterface for ClientWalletRpc { .map_err(WalletRpcError::ResponseError) } + async fn list_own_orders(&self, account_index: U31) -> Result, Self::Error> { + WalletRpcClient::list_own_orders(&self.http_client, account_index.into()) + .await + .map_err(WalletRpcError::ResponseError) + } + async fn node_version(&self) -> Result { WalletRpcClient::node_version(&self.http_client) .await @@ -1517,4 +1523,19 @@ impl WalletInterface for ClientWalletRpc { .await .map_err(WalletRpcError::ResponseError) } + + async fn node_get_tokens_info( + &self, + token_ids: Vec, + ) -> Result, Self::Error> { + // TODO: instead of repacking a vec of String's into a vec of `RpcAddress`'s, + // we could make WalletInterface accept `RpcAddress`'s in the first place. + // Though for consistency it should be done for all address parameters. + WalletRpcClient::node_get_tokens_info( + &self.http_client, + token_ids.into_iter().map(Into::into).collect(), + ) + .await + .map_err(WalletRpcError::ResponseError) + } } diff --git a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs index 9d24e3969..e2e544e3c 100644 --- a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs +++ b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs @@ -18,7 +18,7 @@ use std::{collections::BTreeMap, num::NonZeroUsize, path::PathBuf}; use chainstate::{rpc::RpcOutputValueIn, ChainInfo}; use common::{ chain::{ - block::timestamp::BlockTimestamp, Block, GenBlock, SignedTransaction, + block::timestamp::BlockTimestamp, tokens::RPCTokenInfo, Block, GenBlock, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id}, @@ -38,11 +38,11 @@ use wallet_rpc_lib::types::{ AccountExtendedPublicKey, AddressInfo, AddressWithUsageInfo, Balances, BlockInfo, ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegationTransaction, NewOrderTransaction, NewSubmittedTransaction, - NewTokenTransaction, NftMetadata, NodeVersion, OpenedWallet, PoolInfo, PublicKeyInfo, - RpcHashedTimelockContract, RpcInspectTransaction, RpcNewTransaction, RpcPreparedTransaction, - RpcSignatureStatus, RpcStandaloneAddresses, SendTokensFromMultisigAddressResult, - StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, - TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, + NewTokenTransaction, NftMetadata, NodeVersion, OpenedWallet, OwnOrderInfo, PoolInfo, + PublicKeyInfo, RpcHashedTimelockContract, RpcInspectTransaction, RpcNewTransaction, + RpcPreparedTransaction, RpcSignatureStatus, RpcStandaloneAddresses, + SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, + StandaloneAddressWithDetails, TokenMetadata, TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, }; use wallet_types::{ partially_signed_transaction::PartiallySignedTransaction, with_locked::WithLocked, @@ -546,6 +546,8 @@ pub trait WalletInterface { config: ControllerConfig, ) -> Result; + async fn list_own_orders(&self, account_index: U31) -> Result, Self::Error>; + async fn node_version(&self) -> Result; async fn node_shutdown(&self) -> Result<(), Self::Error>; @@ -669,6 +671,11 @@ pub trait WalletInterface { end_height: BlockHeight, step: NonZeroUsize, ) -> Result)>, Self::Error>; + + async fn node_get_tokens_info( + &self, + token_ids: Vec, + ) -> Result, Self::Error>; } pub(crate) trait FromRpcInput { diff --git a/wallet/wallet-rpc-lib/Cargo.toml b/wallet/wallet-rpc-lib/Cargo.toml index 5419863cf..04b97eaf5 100644 --- a/wallet/wallet-rpc-lib/Cargo.toml +++ b/wallet/wallet-rpc-lib/Cargo.toml @@ -33,6 +33,7 @@ derive_more.workspace = true enum-iterator.workspace = true futures.workspace = true hex.workspace = true +itertools.workspace = true jsonrpsee.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/wallet/wallet-rpc-lib/src/rpc/interface.rs b/wallet/wallet-rpc-lib/src/rpc/interface.rs index 57425082b..7f5384973 100644 --- a/wallet/wallet-rpc-lib/src/rpc/interface.rs +++ b/wallet/wallet-rpc-lib/src/rpc/interface.rs @@ -23,9 +23,10 @@ use chainstate::rpc::RpcOutputValueIn; use common::{ address::RpcAddress, chain::{ - block::timestamp::BlockTimestamp, tokens::TokenId, Block, DelegationId, Destination, - GenBlock, OrderId, PoolId, SignedTransaction, SignedTransactionIntent, Transaction, - TxOutput, + block::timestamp::BlockTimestamp, + tokens::{RPCTokenInfo, TokenId}, + Block, DelegationId, Destination, GenBlock, OrderId, PoolId, SignedTransaction, + SignedTransactionIntent, Transaction, TxOutput, }, primitives::{BlockHeight, Id}, }; @@ -46,7 +47,7 @@ use crate::types::{ ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, HexEncoded, LegacyVrfPublicKeyInfo, MaybeSignedTransaction, NewAccountInfo, NewDelegationTransaction, NewOrderTransaction, NewSubmittedTransaction, NewTokenTransaction, NftMetadata, NodeVersion, - OpenedWallet, PoolInfo, PublicKeyInfo, RpcAmountIn, RpcHashedTimelockContract, + OpenedWallet, OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcAmountIn, RpcHashedTimelockContract, RpcInspectTransaction, RpcNewTransaction, RpcPreparedTransaction, RpcStandaloneAddresses, RpcUtxoOutpoint, RpcUtxoState, RpcUtxoType, SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, @@ -814,12 +815,12 @@ trait WalletRpc { options: TransactionRequestOptions, ) -> rpc::RpcResult; - /// Create an order for exchanging "given" amount of an arbitrary currency (coins or tokens) for - /// an arbitrary amount of "asked" currency. + /// Create an order for exchanging an amount of one ("given") currency for a certain amount of + /// another ("asked") currency. Either of the currencies can be coins or tokens. /// - /// Conclude key is the key that can authorize a conclude order command, closing the order and withdrawing - /// all the remaining funds from it. - #[method(name = "create_order")] + /// Conclude key is the key that can authorize the conclude order command, closing the order + /// and withdrawing all the remaining funds from it. + #[method(name = "order_create")] async fn create_order( &self, account: AccountArg, @@ -834,7 +835,8 @@ trait WalletRpc { /// This assumes that the conclude key is owned by the selected account in this wallet. /// /// Optionally, an output address can be provided where remaining funds from the order are transferred. - #[method(name = "conclude_order")] + /// If not specified, a new receive address will be generated for this purpose. + #[method(name = "order_conclude")] async fn conclude_order( &self, account: AccountArg, @@ -846,7 +848,8 @@ trait WalletRpc { /// Fill order completely or partially given its id and an amount in the order's "asked" currency. /// /// Optionally, an output address can be provided where the exchanged funds from the order are transferred. - #[method(name = "fill_order")] + /// If not specified, a new receive address will be generated for this purpose. + #[method(name = "order_fill")] async fn fill_order( &self, account: AccountArg, @@ -858,7 +861,7 @@ trait WalletRpc { /// Freeze an order given its id. This prevents an order from being filled. /// Only a conclude operation is allowed afterwards. - #[method(name = "freeze_order")] + #[method(name = "order_freeze")] async fn freeze_order( &self, account: AccountArg, @@ -866,6 +869,10 @@ trait WalletRpc { options: TransactionOptions, ) -> rpc::RpcResult; + /// List orders whose conclude key is owned by the given account. + #[method(name = "order_list_own")] + async fn list_own_orders(&self, account: AccountArg) -> rpc::RpcResult>; + /// Obtain the node version #[method(name = "node_version")] async fn node_version(&self) -> rpc::RpcResult; @@ -1074,4 +1081,11 @@ trait WalletRpc { end_height: BlockHeight, step: NonZeroUsize, ) -> rpc::RpcResult)>>; + + /// Return token infos for the given token ids. + #[method(name = "node_get_tokens_info")] + async fn node_get_tokens_info( + &self, + token_ids: Vec>, + ) -> rpc::RpcResult>; } diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index 4a3224e10..e7839d612 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -27,7 +27,7 @@ use std::{ }; use chainstate::{ - rpc::{RpcOutputValueIn, TokenDecimals}, + rpc::{make_rpc_amount_out, RpcOutputValueIn, RpcOutputValueOut, TokenDecimals}, tx_verifier::check_transaction, ChainInfo, TokenIssuanceError, }; @@ -41,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, }, @@ -53,6 +55,7 @@ use crypto::{ key::{hdkd::u31::U31, PrivateKey, PublicKey}, vrf::VRFPublicKey, }; +use futures::{stream::FuturesUnordered, TryStreamExt as _}; use mempool::tx_accumulator::PackingStrategy; use mempool_types::tx_options::TxOptionsOverrides; use p2p_types::{bannable_address::BannableAddress, socket_address::SocketAddress, PeerId}; @@ -85,11 +88,11 @@ use wallet_types::{ SignedTxWithFees, }; -use crate::{WalletHandle, WalletRpcConfig}; +use crate::{types::ExistingOwnOrderData, WalletHandle, WalletRpcConfig}; use self::types::{ AddressInfo, AddressWithUsageInfo, DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, - NewAccountInfo, PoolInfo, PublicKeyInfo, RpcAddress, RpcAmountIn, RpcHexString, + NewAccountInfo, OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcAddress, RpcAmountIn, RpcHexString, RpcStandaloneAddress, RpcStandaloneAddressDetails, RpcStandaloneAddresses, RpcStandalonePrivateKeyAddress, RpcUtxoOutpoint, StakingStatus, StandaloneAddressWithDetails, VrfPublicKeyInfo, @@ -758,12 +761,28 @@ where .await? } + pub async fn node_get_tokens_info( + &self, + token_ids: impl IntoIterator>, + ) -> WRpcResult, N> { + let token_ids = token_ids + .into_iter() + .map(|token_id_addr| -> WRpcResult<_, N> { + Ok(token_id_addr + .decode_object(&self.chain_config) + .map_err(|_| RpcError::InvalidAddress)?) + }) + .collect::>()?; + let infos = self.node.get_tokens_info(token_ids).await.map_err(RpcError::RpcError)?; + Ok(infos) + } + pub async fn get_tokens_decimals( &self, token_ids: BTreeSet, ) -> WRpcResult, N> { let infos = self.node.get_tokens_info(token_ids).await.map_err(RpcError::RpcError)?; - let desimals = infos + let decimals = infos .iter() .map(|info| { ( @@ -773,7 +792,7 @@ where }) .collect(); - Ok(desimals) + Ok(decimals) } pub async fn get_transaction( @@ -1699,6 +1718,124 @@ where .await? } + pub async fn list_own_orders(&self, account_index: U31) -> WRpcResult, N> { + let wallet_orders_data = self + .wallet + .call_async(move |controller| { + Box::pin(async move { + controller.readonly_controller(account_index).get_own_orders().await + }) + }) + .await??; + let token_ids = wallet_orders_data + .iter() + .flat_map(|(_, order_data)| { + order_data + .initially_asked + .token_id() + .into_iter() + .chain(order_data.initially_given.token_id().into_iter()) + }) + .copied() + .collect::>(); + let orders_data = wallet_orders_data + .into_iter() + .map(async |(order_id, wallet_order_data)| -> WRpcResult<_, N> { + let node_rpc_order_info = + self.node.get_order_info(order_id).await.map_err(RpcError::RpcError)?; + + Ok((order_id, wallet_order_data, node_rpc_order_info)) + }) + .collect::>() + .try_collect::>() + .await?; + let token_decimals = self.get_tokens_decimals(token_ids).await?; + + let result_iter = orders_data.into_iter().map( + |(order_id, wallet_order_data, node_rpc_order_info)| -> WRpcResult<_, N> { + let order_id_as_rpc_addr = RpcAddress::new(&self.chain_config, order_id)?; + let initially_asked = RpcOutputValueOut::new( + &self.chain_config, + &token_decimals, + wallet_order_data.initially_asked.into(), + )?; + let initially_given = RpcOutputValueOut::new( + &self.chain_config, + &token_decimals, + wallet_order_data.initially_given.into(), + )?; + + // Note: if node_rpc_order_info is unset, the order data doesn't exist in the chainstate db, + // which means that either the order creation tx hasn't been included in a block yet, + // or that the order has been concluded and the conclusion tx has been included in a block. + let existing_order_data = + match (node_rpc_order_info, wallet_order_data.creation_timestamp) { + (Some(node_rpc_order_info), Some(creation_timestamp)) => { + let ask_balance = make_rpc_amount_out( + node_rpc_order_info.ask_balance, + node_rpc_order_info.initially_asked.token_id(), + &self.chain_config, + &token_decimals, + )?; + let give_balance = make_rpc_amount_out( + node_rpc_order_info.give_balance, + node_rpc_order_info.initially_given.token_id(), + &self.chain_config, + &token_decimals, + )?; + + Some(ExistingOwnOrderData { + ask_balance, + give_balance, + creation_timestamp, + is_frozen: node_rpc_order_info.is_frozen, + }) + } + (None, None) => None, + (Some(_), None) => { + // The wallet may not yet have handled the block containing the order creation tx. + // This is a normal (though rare) situation. Consider the order to not yet exist in this case. + None + } + (None, Some(_)) => { + // If wallet_order_data.is_concluded is true, this is a normal situation - the order has been + // concluded and the conclusion has been confirmed. + // If it's false, this situation is still possible, e.g. if another instance of this wallet + // issued a conclusion tx, the tx has been included in a block, but this instance of + // the wallet hasn't seen it yet. + None + } + }; + + // TODO: consider storing order creation timestamp in the chainstate db; this will make + // the timestamp available for non-own orders too, and also allow to simplify the logic above. + // (note that `output_cache::OrderData::creation_timestamp` will no longer be needed in this + // case and can be removed). + + Ok(OwnOrderInfo { + order_id: order_id_as_rpc_addr, + initially_asked, + initially_given, + existing_order_data, + is_marked_as_frozen_in_wallet: wallet_order_data.is_frozen, + is_marked_as_concluded_in_wallet: wallet_order_data.is_concluded, + }) + }, + ); + + Ok(itertools::process_results(result_iter, |iter| { + // Filter out concluded orders whose conclusion has been confirmed. + // Note that this will also filter out orders that were concluded right after creation, + // so that the creation tx has not been included in a block yet. Technically this + // is incorrect, but it's a degenerate case, so we just consider the conclusion to + // be confirmed. + iter.filter(|info| { + !(info.is_marked_as_concluded_in_wallet && info.existing_order_data.is_none()) + }) + .collect() + })?) + } + pub async fn compose_transaction( &self, inputs: Vec, diff --git a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs index 3788c4842..387055cba 100644 --- a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs +++ b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs @@ -21,7 +21,7 @@ use common::{ chain::{ block::timestamp::BlockTimestamp, output_values_holder::collect_token_v1_ids_from_output_values_holders, - tokens::{IsTokenUnfreezable, TokenId}, + tokens::{IsTokenUnfreezable, RPCTokenInfo, TokenId}, Block, DelegationId, Destination, GenBlock, OrderId, PoolId, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, }, @@ -50,11 +50,11 @@ use crate::{ AccountArg, AddressInfo, AddressWithUsageInfo, Balances, ChainInfo, ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, HexEncoded, LegacyVrfPublicKeyInfo, MaybeSignedTransaction, NewAccountInfo, NewDelegationTransaction, NewSubmittedTransaction, - NftMetadata, NodeVersion, OpenedWallet, PoolInfo, PublicKeyInfo, RpcAddress, RpcAmountIn, - RpcHexString, RpcInspectTransaction, RpcStandaloneAddresses, RpcUtxoOutpoint, RpcUtxoState, - RpcUtxoType, SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, - StandaloneAddressWithDetails, TokenMetadata, TransactionOptions, TransactionRequestOptions, - TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, + NftMetadata, NodeVersion, OpenedWallet, OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcAddress, + RpcAmountIn, RpcHexString, RpcInspectTransaction, RpcStandaloneAddresses, RpcUtxoOutpoint, + RpcUtxoState, RpcUtxoType, SendTokensFromMultisigAddressResult, StakePoolBalance, + StakingStatus, StandaloneAddressWithDetails, TokenMetadata, TransactionOptions, + TransactionRequestOptions, TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, }, RpcError, }; @@ -1126,6 +1126,10 @@ where ) } + async fn list_own_orders(&self, account_arg: AccountArg) -> rpc::RpcResult> { + rpc::handle_result(self.list_own_orders(account_arg.index::()?).await) + } + async fn stake_pool_balance( &self, pool_id: RpcAddress, @@ -1376,4 +1380,11 @@ where self.node_get_block_ids_as_checkpoints(start_height, end_height, step).await, ) } + + async fn node_get_tokens_info( + &self, + token_ids: Vec>, + ) -> rpc::RpcResult> { + rpc::handle_result(self.node_get_tokens_info(token_ids).await) + } } diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 9e5a9a541..693e2c53d 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -172,6 +172,9 @@ pub enum RpcError { #[error("Wallet recovery requires mnemonic to be specified")] WalletRecoveryWithoutMnemonic, + + #[error(transparent)] + ChainstateRpcTypeError(#[from] chainstate::rpc::RpcTypeError), } impl From> for rpc::Error { @@ -527,6 +530,30 @@ impl PoolInfo { } } +/// Additional data for an already/still existing order. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] +pub struct ExistingOwnOrderData { + pub ask_balance: RpcAmountOut, + pub give_balance: RpcAmountOut, + pub creation_timestamp: BlockTimestamp, + pub is_frozen: bool, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] +pub struct OwnOrderInfo { + pub order_id: RpcAddress, + + pub initially_asked: RpcOutputValueOut, + pub initially_given: RpcOutputValueOut, + + /// If this is unset, the order creation tx hasn't been included in a block yet, + /// or the order has been concluded and the conclusion tx has been included in a block. + pub existing_order_data: Option, + + pub is_marked_as_frozen_in_wallet: bool, + pub is_marked_as_concluded_in_wallet: bool, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] pub struct NewDelegationTransaction { pub delegation_id: RpcAddress, @@ -1038,6 +1065,18 @@ impl NewOrderTransaction { broadcasted: tx.broadcasted, } } + + pub fn into_order_id_and_new_tx(self) -> (RpcAddress, RpcNewTransaction) { + ( + self.order_id, + RpcNewTransaction { + tx_id: self.tx_id, + tx: self.tx, + fees: self.fees, + broadcasted: self.broadcasted, + }, + ) + } } #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] From 50f91d0ac00970abef0939a457e38b55b7edbd11 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Fri, 12 Dec 2025 13:01:16 +0200 Subject: [PATCH 03/10] Add wallet CLI/RPC command to retrieve all active orders --- CHANGELOG.md | 8 +- Cargo.lock | 14 + Cargo.toml | 1 + .../scanner-lib/src/sync/tests/simulation.rs | 6 +- .../chainstateref/tx_verifier_storage.rs | 7 +- chainstate/src/detail/error_classification.rs | 3 +- chainstate/src/detail/query.rs | 123 ++- .../src/interface/chainstate_interface.rs | 14 +- .../interface/chainstate_interface_impl.rs | 31 +- .../chainstate_interface_impl_delegation.rs | 18 +- chainstate/src/rpc/mod.rs | 30 +- .../src/internal/store_tx/read_impls.rs | 14 + chainstate/storage/src/mock/mock_impl.rs | 3 + .../helpers/in_memory_storage_wrapper.rs | 6 +- .../test-suite/src/tests/orders_tests.rs | 778 ++++++++++-------- .../src/tests/tx_verifier_among_threads.rs | 6 + .../src/transaction_verifier/hierarchy.rs | 6 +- .../src/transaction_verifier/tests/mock.rs | 3 +- chainstate/types/src/error.rs | 2 - common/src/chain/mod.rs | 2 + common/src/chain/order/rpc.rs | 2 +- .../src/chain/rpc_currency.rs | 39 +- .../output/output_values_holder.rs | 28 + .../tx_pool/tx_verifier/chainstate_handle.rs | 10 +- mocks/src/chainstate.rs | 12 +- orders-accounting/src/cache.rs | 25 + orders-accounting/src/data.rs | 4 + orders-accounting/src/storage/db.rs | 13 +- orders-accounting/src/storage/in_memory.rs | 6 +- orders-accounting/src/storage/mod.rs | 13 +- orders-accounting/src/view.rs | 7 +- wallet/src/account/currency_grouper/mod.rs | 2 +- wallet/src/account/output_cache/mod.rs | 7 + wallet/src/account/transaction_list/mod.rs | 3 +- wallet/src/send_request/mod.rs | 42 +- wallet/types/src/lib.rs | 4 +- wallet/wallet-cli-commands/Cargo.toml | 1 + .../src/command_handler/mod.rs | 169 +++- .../wallet-cli-commands/src/helper_types.rs | 69 +- wallet/wallet-cli-commands/src/lib.rs | 13 + .../wallet-controller/src/sync/tests/mod.rs | 8 + .../src/handles_client/mod.rs | 25 +- wallet/wallet-node-client/src/mock.rs | 21 +- wallet/wallet-node-client/src/node_traits.rs | 11 +- .../src/rpc_client/client_impl.rs | 20 +- .../src/rpc_client/cold_wallet_client.rs | 14 +- .../src/handles_client/mod.rs | 16 +- .../src/rpc_client/client_impl.rs | 18 +- .../src/wallet_rpc_traits.rs | 19 +- wallet/wallet-rpc-lib/src/rpc/interface.rs | 29 +- wallet/wallet-rpc-lib/src/rpc/mod.rs | 93 ++- wallet/wallet-rpc-lib/src/rpc/server_impl.rs | 29 +- wallet/wallet-rpc-lib/src/rpc/types.rs | 22 +- 53 files changed, 1331 insertions(+), 538 deletions(-) rename wallet/types/src/currency.rs => common/src/chain/rpc_currency.rs (61%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c827dee5..5d03b9a58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,15 +12,15 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ ## [Unreleased] ### Added - - Node RPC: new method added - `chainstate_tokens_info`. + - Node RPC: new method added - `chainstate_tokens_info`, `chainstate_orders_info_by_currencies`. - Wallet RPC: - - new methods added: `node_get_tokens_info`, `order_list_own`; + - new methods added: `node_get_tokens_info`, `order_list_own`, `order_list_all_active`. - Wallet CLI: - the commands `order-create`, `order-fill`, `order-freeze`, `order-conclude` were added, mirroring their existing RPC counterparts; - - other new commands added: `order-list-own`; + - other new commands added: `order-list-own`, `order-list-all-active`; ### Changed - Wallet RPC: @@ -40,6 +40,8 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ - 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. + - Node RPC: `chainstate_order_info` will no longer fail if one of the order's balances became zero. + ## [1.2.0] - 2025-10-27 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 0287ab0f5..52991ecab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -743,6 +743,19 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +[[package]] +name = "bigdecimal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bincode" version = "1.3.3" @@ -9150,6 +9163,7 @@ name = "wallet-cli-commands" version = "1.2.0" dependencies = [ "async-trait", + "bigdecimal", "blockprod", "chainstate", "chainstate-storage", diff --git a/Cargo.toml b/Cargo.toml index 2caaa0fc8..36ace3a58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,6 +142,7 @@ atomic-traits = "0.4" axum = "0.7" base64 = "0.22" bech32 = "0.11" +bigdecimal = "0.4" bip39 = { version = "2.0", default-features = false } bitcoin-bech32 = "0.13" blake2 = "0.10" diff --git a/api-server/scanner-lib/src/sync/tests/simulation.rs b/api-server/scanner-lib/src/sync/tests/simulation.rs index 156b32378..890fbbd39 100644 --- a/api-server/scanner-lib/src/sync/tests/simulation.rs +++ b/api-server/scanner-lib/src/sync/tests/simulation.rs @@ -169,6 +169,10 @@ impl OrdersAccountingView for OrderAccountingAdapterToCheckFees<'_> { fn get_give_balance(&self, id: &OrderId) -> Result { Ok(self.chainstate.get_order_give_balance(id).unwrap().unwrap_or(Amount::ZERO)) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + Ok(self.chainstate.get_all_order_ids().unwrap()) + } } #[rstest] @@ -996,7 +1000,7 @@ async fn check_orders( let tx = local_state.storage().transaction_ro().await.unwrap(); let scanner_data = tx.get_order(order_id).await.unwrap().unwrap(); - if let Some(node_data) = tf.chainstate.get_order_info_for_rpc(order_id).unwrap() { + if let Some(node_data) = tf.chainstate.get_order_info_for_rpc(&order_id).unwrap() { assert_eq!(scanner_data.conclude_destination, node_data.conclude_key); assert_eq!( scanner_data.next_nonce, diff --git a/chainstate/src/detail/chainstateref/tx_verifier_storage.rs b/chainstate/src/detail/chainstateref/tx_verifier_storage.rs index 5bed3675b..87f75fe84 100644 --- a/chainstate/src/detail/chainstateref/tx_verifier_storage.rs +++ b/chainstate/src/detail/chainstateref/tx_verifier_storage.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use crate::detail::{ chainstateref::ChainstateRef, @@ -570,6 +570,11 @@ impl OrdersAccount fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { self.db_tx.get_give_balance(id) } + + #[log_error] + fn get_all_order_ids(&self) -> Result, Self::Error> { + self.db_tx.get_all_order_ids() + } } impl FlushableOrdersAccountingView diff --git a/chainstate/src/detail/error_classification.rs b/chainstate/src/detail/error_classification.rs index 9449a9149..bbbe2aeb1 100644 --- a/chainstate/src/detail/error_classification.rs +++ b/chainstate/src/detail/error_classification.rs @@ -457,8 +457,7 @@ impl BlockProcessingErrorClassification for PropertyQueryError { // For now, since their p2p ban score is 0, let's consider them General. PropertyQueryError::StakePoolDataNotFound(_) | PropertyQueryError::StakerBalanceOverflow(_) - | PropertyQueryError::PoolBalanceNotFound(_) - | PropertyQueryError::OrderBalanceNotFound(_) => BlockProcessingErrorClass::General, + | PropertyQueryError::PoolBalanceNotFound(_) => BlockProcessingErrorClass::General, PropertyQueryError::StorageError(err) => err.classify(), PropertyQueryError::GetAncestorError(err) => err.classify(), diff --git a/chainstate/src/detail/query.rs b/chainstate/src/detail/query.rs index a2fc149ea..9e2b0c10e 100644 --- a/chainstate/src/detail/query.rs +++ b/chainstate/src/detail/query.rs @@ -13,7 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeSet, num::NonZeroUsize}; +use std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZeroUsize, +}; use chainstate_storage::BlockchainStorageRead; use chainstate_types::{BlockIndex, GenBlockIndex, Locator, PropertyQueryError}; @@ -25,10 +28,11 @@ use common::{ NftIssuance, RPCFungibleTokenInfo, RPCIsTokenFrozen, RPCNonFungibleTokenInfo, RPCTokenInfo, TokenAuxiliaryData, TokenId, }, - AccountType, Block, GenBlock, OrderId, RpcOrderInfo, Transaction, TxOutput, + AccountType, Block, GenBlock, OrderId, RpcCurrency, RpcOrderInfo, Transaction, TxOutput, }, primitives::{Amount, BlockDistance, BlockHeight, Id, Idable}, }; +use logging::log; use orders_accounting::{OrderData, OrdersAccountingStorageRead}; use tokens_accounting::TokensAccountingStorageRead; use utils::ensure; @@ -436,37 +440,92 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat pub fn get_order_info_for_rpc( &self, - order_id: OrderId, + order_id: &OrderId, ) -> Result, PropertyQueryError> { - self.get_order_data(&order_id)? - .map(|order_data| { - let ask_balance = self - .get_order_ask_balance(&order_id)? - .ok_or(PropertyQueryError::OrderBalanceNotFound(order_id))?; - let give_balance = self - .get_order_give_balance(&order_id)? - .ok_or(PropertyQueryError::OrderBalanceNotFound(order_id))?; - - let nonce = - self.chainstate_ref.get_account_nonce_count(AccountType::Order(order_id))?; - - let initially_asked = RpcOutputValue::from_output_value(order_data.ask()) - .ok_or(PropertyQueryError::UnsupportedTokenV0InOrder(order_id))?; - let initially_given = RpcOutputValue::from_output_value(order_data.give()) - .ok_or(PropertyQueryError::UnsupportedTokenV0InOrder(order_id))?; - - let info = RpcOrderInfo { - conclude_key: order_data.conclude_key().clone(), - initially_asked, - initially_given, - give_balance, - ask_balance, - nonce, - is_frozen: order_data.is_frozen(), - }; - - Ok(info) - }) + self.get_order_data(order_id)? + .map(|order_data| self.order_data_to_rpc_info(order_id, &order_data)) .transpose() } + + pub fn get_all_order_ids(&self) -> Result, PropertyQueryError> { + self.chainstate_ref.get_all_order_ids().map_err(PropertyQueryError::from) + } + + pub fn get_orders_info_for_rpc_by_currencies( + &self, + ask_currency: Option<&RpcCurrency>, + give_currency: Option<&RpcCurrency>, + ) -> Result, PropertyQueryError> { + let order_ids = self.get_all_order_ids()?; + + // FIXME add separate test with many orders to test various combinations + + let orders_info = order_ids + .into_iter() + .map(|order_id| -> Result<_, PropertyQueryError> { + match self.get_order_data(&order_id)? { + Some(order_data) => { + let rpc_info = self.order_data_to_rpc_info(&order_id, &order_data)?; + let actual_ask_currency = + RpcCurrency::from_rpc_output_value(&rpc_info.initially_asked); + let actual_give_currency = + RpcCurrency::from_rpc_output_value(&rpc_info.initially_given); + + if ask_currency + .is_none_or(|ask_currency| ask_currency == &actual_ask_currency) + && give_currency + .is_none_or(|give_currency| give_currency == &actual_give_currency) + { + Ok(Some((order_id, rpc_info))) + } else { + Ok(None) + } + } + None => { + // This should never happen. + log::error!("Order data missing for existing order {order_id:x}"); + Ok(None) + } + } + }) + .filter_map(|res_of_opt| res_of_opt.transpose()) + .collect::>()?; + + Ok(orders_info) + } + + fn order_data_to_rpc_info( + &self, + order_id: &OrderId, + order_data: &OrderData, + ) -> Result { + let order_id_addr = + common::address::Address::new(self.chainstate_ref.chain_config(), *order_id).unwrap(); + logging::log::warn!( + "order_id = {order_id:x}, order_id_addr = {order_id_addr}, order_data = {order_data:?}" + ); // FIXME + + // Note: the balances are deleted from the chainstate db once they reach zero. + let ask_balance = self.get_order_ask_balance(order_id)?.unwrap_or(Amount::ZERO); + let give_balance = self.get_order_give_balance(order_id)?.unwrap_or(Amount::ZERO); + + let nonce = self.chainstate_ref.get_account_nonce_count(AccountType::Order(*order_id))?; + + let initially_asked = RpcOutputValue::from_output_value(order_data.ask()) + .ok_or(PropertyQueryError::UnsupportedTokenV0InOrder(*order_id))?; + let initially_given = RpcOutputValue::from_output_value(order_data.give()) + .ok_or(PropertyQueryError::UnsupportedTokenV0InOrder(*order_id))?; + + let info = RpcOrderInfo { + conclude_key: order_data.conclude_key().clone(), + initially_asked, + initially_given, + give_balance, + ask_balance, + nonce, + is_frozen: order_data.is_frozen(), + }; + + Ok(info) + } } diff --git a/chainstate/src/interface/chainstate_interface.rs b/chainstate/src/interface/chainstate_interface.rs index d5c2323ff..370a35a0a 100644 --- a/chainstate/src/interface/chainstate_interface.rs +++ b/chainstate/src/interface/chainstate_interface.rs @@ -31,8 +31,8 @@ use common::{ GenBlock, }, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, ChainConfig, DelegationId, OrderId, PoolId, RpcOrderInfo, - Transaction, TxInput, UtxoOutPoint, + AccountNonce, AccountType, ChainConfig, DelegationId, OrderId, PoolId, RpcCurrency, + RpcOrderInfo, Transaction, TxInput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, }; @@ -240,8 +240,16 @@ pub trait ChainstateInterface: Send + Sync { fn get_order_give_balance(&self, id: &OrderId) -> Result, ChainstateError>; fn get_order_info_for_rpc( &self, - order_id: OrderId, + order_id: &OrderId, ) -> Result, ChainstateError>; + fn get_all_order_ids(&self) -> Result, ChainstateError>; + /// Return infos for all orders that match the given currencies. Passing None for a currency + /// means "any currency". + fn get_orders_info_for_rpc_by_currencies( + &self, + ask_currency: Option<&RpcCurrency>, + give_currency: Option<&RpcCurrency>, + ) -> Result, ChainstateError>; /// Returns the coin amounts of the outpoints spent by a transaction. /// If a utxo for an input was not found or contains tokens the result is `None`. diff --git a/chainstate/src/interface/chainstate_interface_impl.rs b/chainstate/src/interface/chainstate_interface_impl.rs index 68fb9ed2f..e61576d31 100644 --- a/chainstate/src/interface/chainstate_interface_impl.rs +++ b/chainstate/src/interface/chainstate_interface_impl.rs @@ -39,8 +39,8 @@ use common::{ block::{signed_block_header::SignedBlockHeader, Block, BlockReward, GenBlock}, config::ChainConfig, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, DelegationId, OrderId, PoolId, RpcOrderInfo, Transaction, - TxInput, TxOutput, UtxoOutPoint, + AccountNonce, AccountType, DelegationId, OrderId, PoolId, RpcCurrency, RpcOrderInfo, + Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{id::WithId, Amount, BlockHeight, Id, Idable}, }; @@ -840,13 +840,38 @@ where } #[tracing::instrument(skip_all, fields(id = %id))] - fn get_order_info_for_rpc(&self, id: OrderId) -> Result, ChainstateError> { + fn get_order_info_for_rpc( + &self, + id: &OrderId, + ) -> Result, ChainstateError> { self.chainstate .query() .map_err(ChainstateError::from)? .get_order_info_for_rpc(id) .map_err(ChainstateError::from) } + + #[tracing::instrument(skip_all)] + fn get_all_order_ids(&self) -> Result, ChainstateError> { + self.chainstate + .query() + .map_err(ChainstateError::from)? + .get_all_order_ids() + .map_err(ChainstateError::from) + } + + #[tracing::instrument(skip_all, fields(ask_currency, give_currency))] + fn get_orders_info_for_rpc_by_currencies( + &self, + ask_currency: Option<&RpcCurrency>, + give_currency: Option<&RpcCurrency>, + ) -> Result, ChainstateError> { + self.chainstate + .query() + .map_err(ChainstateError::from)? + .get_orders_info_for_rpc_by_currencies(ask_currency, give_currency) + .map_err(ChainstateError::from) + } } // TODO: remove this function. The value of an output cannot be generalized and exposed from ChainstateInterface in such way diff --git a/chainstate/src/interface/chainstate_interface_impl_delegation.rs b/chainstate/src/interface/chainstate_interface_impl_delegation.rs index c146fc9a8..69dbb91bc 100644 --- a/chainstate/src/interface/chainstate_interface_impl_delegation.rs +++ b/chainstate/src/interface/chainstate_interface_impl_delegation.rs @@ -26,8 +26,8 @@ use common::{ block::{signed_block_header::SignedBlockHeader, timestamp::BlockTimestamp, BlockReward}, config::ChainConfig, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, Block, DelegationId, GenBlock, OrderId, PoolId, RpcOrderInfo, - Transaction, TxInput, UtxoOutPoint, + AccountNonce, AccountType, Block, DelegationId, GenBlock, OrderId, PoolId, RpcCurrency, + RpcOrderInfo, Transaction, TxInput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, }; @@ -432,10 +432,22 @@ where fn get_order_info_for_rpc( &self, - order_id: OrderId, + order_id: &OrderId, ) -> Result, ChainstateError> { self.deref().get_order_info_for_rpc(order_id) } + + fn get_all_order_ids(&self) -> Result, ChainstateError> { + self.deref().get_all_order_ids() + } + + fn get_orders_info_for_rpc_by_currencies( + &self, + ask_currency: Option<&RpcCurrency>, + give_currency: Option<&RpcCurrency>, + ) -> Result, ChainstateError> { + self.deref().get_orders_info_for_rpc_by_currencies(ask_currency, give_currency) + } } #[cfg(test)] diff --git a/chainstate/src/rpc/mod.rs b/chainstate/src/rpc/mod.rs index 058849d2c..02967cc1b 100644 --- a/chainstate/src/rpc/mod.rs +++ b/chainstate/src/rpc/mod.rs @@ -31,7 +31,8 @@ use common::{ chain::{ output_values_holder::collect_token_v1_ids_from_output_values_holder, tokens::{RPCTokenInfo, TokenId}, - ChainConfig, DelegationId, Destination, OrderId, PoolId, RpcOrderInfo, TxOutput, + ChainConfig, DelegationId, Destination, OrderId, PoolId, RpcCurrency, RpcOrderInfo, + TxOutput, }, primitives::{Amount, BlockHeight, Id}, }; @@ -184,6 +185,15 @@ trait ChainstateRpc { #[method(name = "order_info")] async fn order_info(&self, order_id: String) -> RpcResult>; + /// Return infos for all orders that match the given currencies. Passing None for a currency + /// means "any currency". + #[method(name = "orders_info_by_currencies")] + async fn orders_info_by_currencies( + &self, + ask_currency: Option, + give_currency: Option, + ) -> RpcResult>; + /// Exports a "bootstrap file", which contains all blocks #[method(name = "export_bootstrap_file")] async fn export_bootstrap_file( @@ -473,7 +483,7 @@ impl ChainstateRpcServer for super::ChainstateHandle { let result: Result, _> = dynamize_err(Address::::from_string(chain_config, order_id)) .map(|address| address.into_object()) - .and_then(|order_id| dynamize_err(this.get_order_info_for_rpc(order_id))); + .and_then(|order_id| dynamize_err(this.get_order_info_for_rpc(&order_id))); result }) @@ -481,6 +491,22 @@ impl ChainstateRpcServer for super::ChainstateHandle { ) } + async fn orders_info_by_currencies( + &self, + ask_currency: Option, + give_currency: Option, + ) -> RpcResult> { + rpc::handle_result( + self.call(move |this| { + this.get_orders_info_for_rpc_by_currencies( + ask_currency.as_ref(), + give_currency.as_ref(), + ) + }) + .await, + ) + } + async fn export_bootstrap_file( &self, file_path: &std::path::Path, diff --git a/chainstate/storage/src/internal/store_tx/read_impls.rs b/chainstate/storage/src/internal/store_tx/read_impls.rs index 32ce5ba7a..c07524829 100644 --- a/chainstate/storage/src/internal/store_tx/read_impls.rs +++ b/chainstate/storage/src/internal/store_tx/read_impls.rs @@ -395,6 +395,13 @@ impl OrdersAccountingStorageRead for super::StoreTxRo fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { self.read::(id) } + + #[log_error] + fn get_all_order_ids(&self) -> crate::Result> { + let map = self.0.get::(); + let iter = map.prefix_iter_keys(&())?; + Ok(iter.collect::>()) + } } /// Blockchain data storage transaction @@ -712,4 +719,11 @@ impl OrdersAccountingStorageRead for super::StoreTxRw fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { self.read::(id) } + + #[log_error] + fn get_all_order_ids(&self) -> crate::Result> { + let map = self.get_map::()?; + let iter = map.prefix_iter_keys(&())?; + Ok(iter.collect::>()) + } } diff --git a/chainstate/storage/src/mock/mock_impl.rs b/chainstate/storage/src/mock/mock_impl.rs index 960cc221f..f02505574 100644 --- a/chainstate/storage/src/mock/mock_impl.rs +++ b/chainstate/storage/src/mock/mock_impl.rs @@ -173,6 +173,7 @@ mockall::mock! { fn get_order_data(&self, id: &OrderId) -> crate::Result>; fn get_ask_balance(&self, id: &OrderId) -> crate::Result>; fn get_give_balance(&self, id: &OrderId) -> crate::Result>; + fn get_all_order_ids(&self) -> crate::Result>; } impl crate::BlockchainStorageWrite for Store { @@ -470,6 +471,7 @@ mockall::mock! { fn get_order_data(&self, id: &OrderId) -> crate::Result>; fn get_ask_balance(&self, id: &OrderId) -> crate::Result>; fn get_give_balance(&self, id: &OrderId) -> crate::Result>; + fn get_all_order_ids(&self) -> crate::Result>; } impl crate::TransactionRo for StoreTxRo { @@ -596,6 +598,7 @@ mockall::mock! { fn get_order_data(&self, id: &OrderId) -> crate::Result>; fn get_ask_balance(&self, id: &OrderId) -> crate::Result>; fn get_give_balance(&self, id: &OrderId) -> crate::Result>; + fn get_all_order_ids(&self) -> crate::Result>; } impl crate::BlockchainStorageWrite for StoreTxRw { diff --git a/chainstate/test-suite/src/tests/helpers/in_memory_storage_wrapper.rs b/chainstate/test-suite/src/tests/helpers/in_memory_storage_wrapper.rs index 4743ce9be..10d082dd3 100644 --- a/chainstate/test-suite/src/tests/helpers/in_memory_storage_wrapper.rs +++ b/chainstate/test-suite/src/tests/helpers/in_memory_storage_wrapper.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use ::tx_verifier::transaction_verifier::storage::{ TransactionVerifierStorageError, TransactionVerifierStorageRef, @@ -268,4 +268,8 @@ impl OrdersAccountingStorageRead for InMemoryStorageWrapper { fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { self.storage.transaction_ro().unwrap().get_give_balance(id) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + self.storage.transaction_ro().unwrap().get_all_order_ids() + } } diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index e56aa3d82..bcc63469c 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -32,7 +32,7 @@ use common::{ address::pubkeyhash::PublicKeyHash, chain::{ make_order_id, make_token_id, - output_value::OutputValue, + output_value::{OutputValue, RpcOutputValue}, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, sighash::{input_commitments::SighashInputCommitment, sighashtype::SigHashType}, @@ -40,8 +40,8 @@ use common::{ }, tokens::{IsTokenFreezable, TokenId, TokenTotalSupply}, AccountCommand, AccountNonce, ChainstateUpgradeBuilder, Destination, IdCreationError, - OrderAccountCommand, OrderData, OrderId, OrdersVersion, SignedTransaction, Transaction, - TxInput, TxOutput, UtxoOutPoint, + OrderAccountCommand, OrderData, OrderId, OrdersVersion, RpcCurrency, RpcOrderInfo, + SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Idable, H256}, }; @@ -119,6 +119,160 @@ fn issue_and_mint_token_amount_from_best_block( ) } +type InitialOrderData = common::chain::OrderData; + +struct ExpectedOrderData { + initial_data: InitialOrderData, + ask_balance: Option, + give_balance: Option, + nonce: Option, + is_frozen: bool, +} + +fn assert_order_exists( + tf: &TestFramework, + rng: &mut (impl Rng + CryptoRng), + order_id: &OrderId, + expected_data: &ExpectedOrderData, +) { + let current_data = tf.chainstate.get_order_data(order_id).unwrap().unwrap(); + let (current_data_conclude_key, current_data_ask, current_data_give, current_data_is_frozen) = + current_data.consume(); + assert_eq!( + ¤t_data_conclude_key, + expected_data.initial_data.conclude_key() + ); + assert_eq!(¤t_data_ask, expected_data.initial_data.ask()); + assert_eq!(¤t_data_give, expected_data.initial_data.give()); + assert_eq!(current_data_is_frozen, expected_data.is_frozen); + + let current_ask_balance = tf.chainstate.get_order_ask_balance(order_id).unwrap(); + assert_eq!(current_ask_balance, expected_data.ask_balance); + let current_give_balance = tf.chainstate.get_order_give_balance(order_id).unwrap(); + assert_eq!(current_give_balance, expected_data.give_balance); + + let expected_info_for_rpc = RpcOrderInfo { + conclude_key: expected_data.initial_data.conclude_key().clone(), + initially_asked: RpcOutputValue::from_output_value(expected_data.initial_data.ask()) + .unwrap(), + initially_given: RpcOutputValue::from_output_value(expected_data.initial_data.give()) + .unwrap(), + ask_balance: expected_data.ask_balance.unwrap_or(Amount::ZERO), + give_balance: expected_data.give_balance.unwrap_or(Amount::ZERO), + nonce: expected_data.nonce.clone(), + is_frozen: expected_data.is_frozen, + }; + + let actual_info_for_rpc = tf.chainstate.get_order_info_for_rpc(order_id).unwrap().unwrap(); + assert_eq!(actual_info_for_rpc, expected_info_for_rpc); + + let all_order_ids = tf.chainstate.get_all_order_ids().unwrap(); + assert!(all_order_ids.contains(order_id)); + + let all_orders = tf.chainstate.get_orders_info_for_rpc_by_currencies(None, None).unwrap(); + assert_eq!(all_orders.get(order_id).unwrap(), &expected_info_for_rpc); + + let ask_currency = RpcCurrency::from_output_value(expected_data.initial_data.ask()).unwrap(); + let give_currency = RpcCurrency::from_output_value(expected_data.initial_data.give()).unwrap(); + + // Check get_orders_info_for_rpc_by_currencies when all currency filters match or are None - + // the order should be present in the result + for (ask_currency_filter, give_currency_filter) in [ + (None, None), + (Some(&ask_currency), None), + (None, Some(&give_currency)), + (Some(&ask_currency), Some(&give_currency)), + ] { + let orders_rpc_infos = tf + .chainstate + .get_orders_info_for_rpc_by_currencies(ask_currency_filter, give_currency_filter) + .unwrap(); + assert_eq!( + orders_rpc_infos.get(order_id).unwrap(), + &expected_info_for_rpc + ); + } + + let mut make_different_currency = |currency, other_currency| { + if currency != other_currency && rng.gen_bool(0.5) { + return other_currency; + } + + match currency { + RpcCurrency::Coin => RpcCurrency::Token(TokenId::random_using(rng)), + RpcCurrency::Token(_) => { + if rng.gen_bool(0.5) { + RpcCurrency::Coin + } else { + RpcCurrency::Token(TokenId::random_using(rng)) + } + } + } + }; + + let different_ask_currency = make_different_currency(ask_currency, give_currency); + let different_give_currency = make_different_currency(give_currency, ask_currency); + + // Check get_orders_info_for_rpc_by_currencies when at least one currency filter doesn't match - + // the order should not be present in the result. + for (ask_currency_filter, give_currency_filter) in [ + (Some(&different_ask_currency), None), + (Some(&different_ask_currency), Some(&give_currency)), + (None, Some(&different_give_currency)), + (Some(&ask_currency), Some(&different_give_currency)), + ( + Some(&different_ask_currency), + Some(&different_give_currency), + ), + ] { + let orders_rpc_infos = tf + .chainstate + .get_orders_info_for_rpc_by_currencies(ask_currency_filter, give_currency_filter) + .unwrap(); + assert_eq!(orders_rpc_infos.get(order_id), None); + } + + let actual_nonce = tf + .chainstate + .get_account_nonce_count(common::chain::AccountType::Order(*order_id)) + .unwrap(); + assert_eq!(actual_nonce, expected_data.nonce); +} + +trait OrdersVersionExt { + fn v0_then_some(&self, val: T) -> Option; +} + +impl OrdersVersionExt for OrdersVersion { + fn v0_then_some(&self, val: T) -> Option { + match self { + OrdersVersion::V0 => Some(val), + OrdersVersion::V1 => None, + } + } +} + +fn assert_order_missing(tf: &TestFramework, order_id: &OrderId) { + let data = tf.chainstate.get_order_data(order_id).unwrap(); + assert_eq!(data, None); + + let ask_balance = tf.chainstate.get_order_ask_balance(order_id).unwrap(); + assert_eq!(ask_balance, None); + + let give_balance = tf.chainstate.get_order_give_balance(order_id).unwrap(); + assert_eq!(give_balance, None); + + let info_for_rpc = tf.chainstate.get_order_info_for_rpc(order_id).unwrap(); + assert_eq!(info_for_rpc, None); + + let all_infos_for_rpc = + tf.chainstate.get_orders_info_for_rpc_by_currencies(None, None).unwrap(); + assert_eq!(all_infos_for_rpc.get(order_id), None); + + let all_order_ids = tf.chainstate.get_all_order_ids().unwrap(); + assert!(!all_order_ids.contains(order_id)); +} + #[rstest] #[trace] #[case(Seed::from_entropy())] @@ -147,17 +301,17 @@ fn create_order_check_storage(#[case] seed: Seed) { let order_id = make_order_id(tx.inputs()).unwrap(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_eq!( - Some(order_data.into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - Some(ask_amount), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some(give_amount), - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: Some(ask_amount), + give_balance: Some(give_amount), + nonce: None, + is_frozen: false, + }, ); }); } @@ -529,15 +683,7 @@ fn conclude_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersi .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + assert_order_missing(&tf, &order_id); }); } @@ -703,17 +849,17 @@ fn fill_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersion) let partial_fill_tx_id = tx.transaction().get_id(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_eq!( - Some(order_data.clone().into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - left_to_fill.as_non_zero(), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - (give_amount - filled_amount).unwrap().as_non_zero(), - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data.clone(), + ask_balance: left_to_fill.as_non_zero(), + give_balance: (give_amount - filled_amount).unwrap().as_non_zero(), + nonce: version.v0_then_some(AccountNonce::new(0)), + is_frozen: false, + }, ); // Note: even though zero fills are allowed in orders V0 in general, we can't do a zero @@ -748,21 +894,8 @@ fn fill_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersion) .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_eq!( - Some(order_data.into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - match version { - OrdersVersion::V0 => { - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); - } + let expected_give_balance = match version { + OrdersVersion::V0 => None, OrdersVersion::V1 => { let filled1 = (give_amount.into_atoms() * fill_amount.into_atoms()) / ask_amount.into_atoms(); @@ -771,12 +904,22 @@ fn fill_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersion) let remainder = (give_amount - Amount::from_atoms(filled1 + filled2)) .unwrap() .as_non_zero(); - assert_eq!( - remainder, - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + remainder } - } + }; + + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data.clone(), + ask_balance: None, + give_balance: expected_give_balance, + nonce: version.v0_then_some(AccountNonce::new(1)), + is_frozen: false, + }, + ); } }); } @@ -938,15 +1081,7 @@ fn fill_then_conclude(#[case] seed: Seed, #[case] version: OrdersVersion) { .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + assert_order_missing(&tf, &order_id); { // Try filling concluded order @@ -1309,15 +1444,7 @@ fn fill_completely_then_conclude(#[case] seed: Seed, #[case] version: OrdersVers .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + assert_order_missing(&tf, &order_id); }); } @@ -1526,33 +1653,22 @@ fn reorg_before_create(#[case] seed: Seed, #[case] version: OrdersVersion) { .build_and_process(&mut rng) .unwrap(); - assert_eq!( - Some(order_data.clone().into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - Some(left_to_fill), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some((give_amount - filled_amount).unwrap()), - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + let expected_order_data = ExpectedOrderData { + initial_data: order_data, + ask_balance: Some(left_to_fill), + give_balance: Some((give_amount - filled_amount).unwrap()), + nonce: version.v0_then_some(AccountNonce::new(0)), + is_frozen: false, + }; + + assert_order_exists(&tf, &mut rng, &order_id, &expected_order_data); // Create alternative chain and trigger the reorg let new_best_block = tf.create_chain_with_empty_blocks(&reorg_common_ancestor, 3, &mut rng).unwrap(); assert_eq!(tf.best_block_id(), new_best_block); - assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + assert_order_missing(&tf, &order_id); // Reapply txs again tf.make_block_builder() @@ -1560,18 +1676,7 @@ fn reorg_before_create(#[case] seed: Seed, #[case] version: OrdersVersion) { .build_and_process(&mut rng) .unwrap(); - assert_eq!( - Some(order_data.into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - Some(left_to_fill), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some((give_amount - filled_amount).unwrap()), - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + assert_order_exists(&tf, &mut rng, &order_id, &expected_order_data); }); } @@ -1665,32 +1770,24 @@ fn reorg_after_create(#[case] seed: Seed, #[case] version: OrdersVersion) { .build_and_process(&mut rng) .unwrap(); - assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + assert_order_missing(&tf, &order_id); // Create alternative chain and trigger the reorg let new_best_block = tf.create_chain_with_empty_blocks(&reorg_common_ancestor, 3, &mut rng).unwrap(); assert_eq!(tf.best_block_id(), new_best_block); - assert_eq!( - Some(output_value_amount(order_data.ask())), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some(output_value_amount(order_data.give())), - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); - assert_eq!( - Some(order_data.into()), - tf.chainstate.get_order_data(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data.clone(), + ask_balance: Some(output_value_amount(order_data.ask())), + give_balance: Some(output_value_amount(order_data.give())), + nonce: None, + is_frozen: false, + }, ); // Reapply txs again @@ -1699,15 +1796,7 @@ fn reorg_after_create(#[case] seed: Seed, #[case] version: OrdersVersion) { .build_and_process(&mut rng) .unwrap(); - assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + assert_order_missing(&tf, &order_id); }); } @@ -1856,17 +1945,17 @@ fn create_order_with_nft(#[case] seed: Seed, #[case] version: OrdersVersion) { let order_id = make_order_id(tx.inputs()).unwrap(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_eq!( - Some(order_data.clone().into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - Some(ask_amount), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some(give_amount), - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data.clone(), + ask_balance: Some(ask_amount), + give_balance: Some(give_amount), + nonce: None, + is_frozen: false, + }, ); // Try get 2 nfts out of order @@ -1932,17 +2021,17 @@ fn create_order_with_nft(#[case] seed: Seed, #[case] version: OrdersVersion) { .build_and_process(&mut rng) .unwrap(); - assert_eq!( - Some(order_data.into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: None, + give_balance: None, + nonce: version.v0_then_some(AccountNonce::new(0)), + is_frozen: false, + }, ); }); } @@ -2018,17 +2107,17 @@ fn partially_fill_order_with_nft_v0(#[case] seed: Seed) { let order_id = make_order_id(tx.inputs()).unwrap(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_eq!( - Some(order_data.clone().into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - Some(ask_amount), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some(give_amount), - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data.clone(), + ask_balance: Some(ask_amount), + give_balance: Some(give_amount), + nonce: None, + is_frozen: false, + }, ); // Try get an nft out of order with 1 atom less @@ -2100,17 +2189,17 @@ fn partially_fill_order_with_nft_v0(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - assert_eq!( - Some(order_data.clone().into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - Some(Amount::from_atoms(1)), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some(give_amount), - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data.clone(), + ask_balance: Some(Amount::from_atoms(1)), + give_balance: Some(give_amount), + nonce: Some(AccountNonce::new(0)), + is_frozen: false, + }, ); // Fill order only with proper amount spent @@ -2141,17 +2230,17 @@ fn partially_fill_order_with_nft_v0(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - assert_eq!( - Some(order_data.into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: None, + give_balance: None, + nonce: Some(AccountNonce::new(1)), + is_frozen: false, + }, ); }); } @@ -2227,17 +2316,17 @@ fn partially_fill_order_with_nft_v1(#[case] seed: Seed) { let order_id = make_order_id(tx.inputs()).unwrap(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_eq!( - Some(order_data.clone().into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - Some(ask_amount), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some(give_amount), - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data.clone(), + ask_balance: Some(ask_amount), + give_balance: Some(give_amount), + nonce: None, + is_frozen: false, + }, ); // Try to get nft by filling order with 1 atom less, getting 0 nfts @@ -2300,17 +2389,17 @@ fn partially_fill_order_with_nft_v1(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - assert_eq!( - Some(order_data.into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: None, + give_balance: None, + nonce: None, + is_frozen: false, + }, ); }); } @@ -2365,25 +2454,20 @@ fn fill_order_with_zero(#[case] seed: Seed, #[case] version: OrdersVersion) { OrdersVersion::V0 => { // Check that order has not changed except nonce assert!(result.is_ok()); - assert_eq!( - Some(AccountNonce::new(0)), - tf.chainstate - .get_account_nonce_count(common::chain::AccountType::Order(order_id)) - .unwrap() - ); - assert_eq!( - Some(order_data.into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - Some(ask_amount), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some(give_amount), - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: Some(ask_amount), + give_balance: Some(give_amount), + nonce: Some(AccountNonce::new(0)), + is_frozen: false, + }, ); } + OrdersVersion::V1 => { assert_eq!( result.unwrap_err(), @@ -2472,23 +2556,17 @@ fn fill_order_underbid(#[case] seed: Seed, #[case] version: OrdersVersion) { // The ask balance must have changed, but the give balance must not. let expected_ask_balance = (ask_amount - fill_amount).unwrap(); assert!(result.is_ok()); - assert_eq!( - tf.chainstate - .get_account_nonce_count(common::chain::AccountType::Order(order_id)) - .unwrap(), - Some(AccountNonce::new(0)) - ); - assert_eq!( - tf.chainstate.get_order_data(&order_id).unwrap(), - Some(order_data.into()) - ); - assert_eq!( - tf.chainstate.get_order_ask_balance(&order_id).unwrap(), - Some(expected_ask_balance), - ); - assert_eq!( - tf.chainstate.get_order_give_balance(&order_id).unwrap(), - Some(give_amount), + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: Some(expected_ask_balance), + give_balance: Some(give_amount), + nonce: Some(AccountNonce::new(0)), + is_frozen: false, + }, ); } OrdersVersion::V1 => { @@ -2579,17 +2657,17 @@ fn fill_orders_shuffle(#[case] seed: Seed, #[case] fills: Vec) { .build_and_process(&mut rng) .unwrap(); - assert_eq!( - Some(order_data.into()), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some(Amount::from_atoms(1)), - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: None, + give_balance: Some(Amount::from_atoms(1)), + nonce: None, + is_frozen: false, + }, ); }); } @@ -2911,15 +2989,7 @@ fn create_order_fill_activate_fork_fill_conclude(#[case] seed: Seed) { .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + assert_order_missing(&tf, &order_id); }); } @@ -3031,19 +3101,17 @@ fn freeze_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersion OrdersVersion::V1 => { assert!(result.is_ok()); - let expecter_order_data = - orders_accounting::OrderData::from(order_data).try_freeze().unwrap(); - assert_eq!( - Some(expecter_order_data), - tf.chainstate.get_order_data(&order_id).unwrap() - ); - assert_eq!( - Some(ask_amount), - tf.chainstate.get_order_ask_balance(&order_id).unwrap() - ); - assert_eq!( - Some(give_amount), - tf.chainstate.get_order_give_balance(&order_id).unwrap() + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: Some(ask_amount), + give_balance: Some(give_amount), + nonce: None, + is_frozen: true, + }, ); } } @@ -3625,17 +3693,17 @@ fn fill_order_v0_destination_irrelevancy(#[case] seed: Seed) { let expected_ask_balance = (initial_ask_amount - total_fill_amount).unwrap(); let expected_give_balance = (initial_give_amount - total_filled_amount).unwrap(); - assert_eq!( - tf.chainstate.get_order_data(&order_id).unwrap(), - Some(order_data.into()), - ); - assert_eq!( - tf.chainstate.get_order_ask_balance(&order_id).unwrap(), - Some(expected_ask_balance), - ); - assert_eq!( - tf.chainstate.get_order_give_balance(&order_id).unwrap(), - Some(expected_give_balance), + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: Some(expected_ask_balance), + give_balance: Some(expected_give_balance), + nonce: Some(AccountNonce::new(2)), + is_frozen: false, + }, ); }); } @@ -3781,17 +3849,17 @@ fn fill_order_v1_must_not_be_signed(#[case] seed: Seed) { let expected_ask_balance = (initial_ask_amount - fill_amount).unwrap(); let expected_give_balance = (initial_give_amount - filled_amount).unwrap(); - assert_eq!( - tf.chainstate.get_order_data(&order_id).unwrap(), - Some(order_data.into()), - ); - assert_eq!( - tf.chainstate.get_order_ask_balance(&order_id).unwrap(), - Some(expected_ask_balance), - ); - assert_eq!( - tf.chainstate.get_order_give_balance(&order_id).unwrap(), - Some(expected_give_balance), + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: Some(expected_ask_balance), + give_balance: Some(expected_give_balance), + nonce: None, + is_frozen: false, + }, ); }); } @@ -3944,17 +4012,17 @@ fn fill_order_twice_in_same_block( let expected_ask_balance = (initial_ask_amount - total_fill_amount).unwrap(); let expected_give_balance = (initial_give_amount - total_filled_amount).unwrap(); - assert_eq!( - tf.chainstate.get_order_data(&order_id).unwrap(), - Some(order_data.into()), - ); - assert_eq!( - tf.chainstate.get_order_ask_balance(&order_id).unwrap(), - Some(expected_ask_balance), - ); - assert_eq!( - tf.chainstate.get_order_give_balance(&order_id).unwrap(), - Some(expected_give_balance), + assert_order_exists( + &tf, + &mut rng, + &order_id, + &ExpectedOrderData { + initial_data: order_data, + ask_balance: Some(expected_ask_balance), + give_balance: Some(expected_give_balance), + nonce: version.v0_then_some(AccountNonce::new(1)), + is_frozen: false, + }, ); }); } @@ -4085,17 +4153,17 @@ fn conclude_and_recreate_in_same_tx_with_same_balances( } // Check the original order data - it's still there - assert_eq!( - tf.chainstate.get_order_data(&orig_order_id).unwrap(), - Some(orig_order_data.clone().into()), - ); - assert_eq!( - tf.chainstate.get_order_ask_balance(&orig_order_id).unwrap(), - Some(remaining_coins_amount_to_trade), - ); - assert_eq!( - tf.chainstate.get_order_give_balance(&orig_order_id).unwrap(), - Some(remaining_tokens_amount_to_trade), + assert_order_exists( + &tf, + &mut rng, + &orig_order_id, + &ExpectedOrderData { + initial_data: orig_order_data, + ask_balance: Some(remaining_coins_amount_to_trade), + give_balance: Some(remaining_tokens_amount_to_trade), + nonce: None, + is_frozen: false, + }, ); let new_order_id = { @@ -4131,29 +4199,23 @@ fn conclude_and_recreate_in_same_tx_with_same_balances( order_id }; + assert_order_missing(&tf, &orig_order_id); + // The original order is no longer there - assert_eq!(None, tf.chainstate.get_order_data(&orig_order_id).unwrap()); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&orig_order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&orig_order_id).unwrap() - ); + assert_order_missing(&tf, &orig_order_id); // The new order exists and has the same balances as the original one before the conclusion. - assert_eq!( - tf.chainstate.get_order_data(&new_order_id).unwrap(), - Some(new_order_data.into()), - ); - assert_eq!( - tf.chainstate.get_order_ask_balance(&new_order_id).unwrap(), - Some(remaining_coins_amount_to_trade), - ); - assert_eq!( - tf.chainstate.get_order_give_balance(&new_order_id).unwrap(), - Some(remaining_tokens_amount_to_trade), + assert_order_exists( + &tf, + &mut rng, + &new_order_id, + &ExpectedOrderData { + initial_data: new_order_data, + ask_balance: Some(remaining_coins_amount_to_trade), + give_balance: Some(remaining_tokens_amount_to_trade), + nonce: None, + is_frozen: false, + }, ); }); } @@ -4300,28 +4362,20 @@ fn conclude_and_recreate_in_same_tx_with_different_balances( }; // The original order is no longer there - assert_eq!(None, tf.chainstate.get_order_data(&orig_order_id).unwrap()); - assert_eq!( - None, - tf.chainstate.get_order_ask_balance(&orig_order_id).unwrap() - ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&orig_order_id).unwrap() - ); + assert_order_missing(&tf, &orig_order_id); // The new order exists with the correct balances. - assert_eq!( - tf.chainstate.get_order_data(&new_order_id).unwrap(), - Some(new_order_data.into()), - ); - assert_eq!( - tf.chainstate.get_order_ask_balance(&new_order_id).unwrap(), - Some(new_coins_amount_to_trade), - ); - assert_eq!( - tf.chainstate.get_order_give_balance(&new_order_id).unwrap(), - Some(new_tokens_amount_to_trade), + assert_order_exists( + &tf, + &mut rng, + &new_order_id, + &ExpectedOrderData { + initial_data: new_order_data, + ask_balance: Some(new_coins_amount_to_trade), + give_balance: Some(new_tokens_amount_to_trade), + nonce: None, + is_frozen: false, + }, ); }); } diff --git a/chainstate/test-suite/src/tests/tx_verifier_among_threads.rs b/chainstate/test-suite/src/tests/tx_verifier_among_threads.rs index b30ae23e4..79766fbe9 100644 --- a/chainstate/test-suite/src/tests/tx_verifier_among_threads.rs +++ b/chainstate/test-suite/src/tests/tx_verifier_among_threads.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::BTreeSet; + use chainstate_test_framework::{TestFramework, TestStore}; use common::chain::config::Builder as ConfigBuilder; use common::primitives::Amount; @@ -139,6 +141,10 @@ impl OrdersAccountingView for EmptyOrdersAccountingView { fn get_give_balance(&self, _id: &OrderId) -> Result { Ok(Amount::ZERO) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + Ok(BTreeSet::new()) + } } /// This test proves that a transaction verifier with this structure can be moved among threads diff --git a/chainstate/tx-verifier/src/transaction_verifier/hierarchy.rs b/chainstate/tx-verifier/src/transaction_verifier/hierarchy.rs index 3fab13a53..7bbe65673 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/hierarchy.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/hierarchy.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use super::{ accounting_undo_cache::CachedBlockUndo, @@ -449,6 +449,10 @@ impl OrdersAccountingStorageRead fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { self.orders_accounting_cache.get_give_balance(id).map(Some) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + self.orders_accounting_cache.get_all_order_ids() + } } impl FlushableOrdersAccountingView for TransactionVerifier { diff --git a/chainstate/tx-verifier/src/transaction_verifier/tests/mock.rs b/chainstate/tx-verifier/src/transaction_verifier/tests/mock.rs index e4dc54359..59e5c7f17 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/tests/mock.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/tests/mock.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use crate::transaction_verifier::TransactionSource; @@ -223,6 +223,7 @@ mockall::mock! { fn get_order_data(&self, id: &OrderId) -> Result, orders_accounting::Error>; fn get_ask_balance(&self, id: &OrderId) -> Result, orders_accounting::Error>; fn get_give_balance(&self, id: &OrderId) -> Result, orders_accounting::Error>; + fn get_all_order_ids(&self) -> Result, orders_accounting::Error>; } impl FlushableOrdersAccountingView for Store { diff --git a/chainstate/types/src/error.rs b/chainstate/types/src/error.rs index a68b96021..2a27e188d 100644 --- a/chainstate/types/src/error.rs +++ b/chainstate/types/src/error.rs @@ -49,8 +49,6 @@ pub enum PropertyQueryError { StakerBalanceOverflow(PoolId), #[error("Balance of pool {0} not found")] PoolBalanceNotFound(PoolId), - #[error("Balance of order {0} not found")] - OrderBalanceNotFound(OrderId), #[error("Unsupported token V0 in order {0}")] UnsupportedTokenV0InOrder(OrderId), #[error("Invalid starting block height: {0}")] diff --git a/common/src/chain/mod.rs b/common/src/chain/mod.rs index 021dde7a6..e4bcf18a7 100644 --- a/common/src/chain/mod.rs +++ b/common/src/chain/mod.rs @@ -27,6 +27,7 @@ mod make_id; mod order; mod pos; mod pow; +mod rpc_currency; mod upgrades; pub use signed_transaction::SignedTransaction; @@ -47,4 +48,5 @@ pub use pos::{ get_initial_randomness, pool_id::PoolId, pos_initial_difficulty, PoSConsensusVersion, }; pub use pow::{PoWChainConfig, PoWChainConfigBuilder}; +pub use rpc_currency::RpcCurrency; pub use upgrades::*; diff --git a/common/src/chain/order/rpc.rs b/common/src/chain/order/rpc.rs index 6fca91f0c..25dd943d8 100644 --- a/common/src/chain/order/rpc.rs +++ b/common/src/chain/order/rpc.rs @@ -23,7 +23,7 @@ use crate::{ primitives::Amount, }; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] +#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, HasValueHint)] pub struct RpcOrderInfo { pub conclude_key: Destination, diff --git a/wallet/types/src/currency.rs b/common/src/chain/rpc_currency.rs similarity index 61% rename from wallet/types/src/currency.rs rename to common/src/chain/rpc_currency.rs index 42e3972e8..1d11f2f6c 100644 --- a/wallet/types/src/currency.rs +++ b/common/src/chain/rpc_currency.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 RBB S.r.l +// Copyright (c) 2021-2025 RBB S.r.l // opensource@mintlayer.org // SPDX-License-Identifier: MIT // Licensed under the MIT License; @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::{ +use crate::{ chain::{ output_value::{OutputValue, RpcOutputValue}, tokens::TokenId, @@ -22,40 +22,43 @@ use common::{ }; #[derive( - PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + Copy, + Clone, + Debug, + serde::Serialize, + serde::Deserialize, + rpc_description::HasValueHint, )] -pub enum Currency { +#[serde(tag = "type", content = "content")] +pub enum RpcCurrency { Coin, Token(TokenId), } -impl Currency { +impl RpcCurrency { pub fn from_output_value(output_value: &OutputValue) -> Option { match output_value { - OutputValue::Coin(_) => Some(Currency::Coin), + OutputValue::Coin(_) => Some(Self::Coin), OutputValue::TokenV0(_) => None, - OutputValue::TokenV1(id, _) => Some(Currency::Token(*id)), + OutputValue::TokenV1(id, _) => Some(Self::Token(*id)), } } pub fn from_rpc_output_value(output_value: &RpcOutputValue) -> Self { match output_value { - RpcOutputValue::Coin { .. } => Currency::Coin, - RpcOutputValue::Token { id, .. } => Currency::Token(*id), + RpcOutputValue::Coin { .. } => Self::Coin, + RpcOutputValue::Token { id, .. } => Self::Token(*id), } } pub fn into_output_value(&self, amount: Amount) -> OutputValue { match self { - Currency::Coin => OutputValue::Coin(amount), - Currency::Token(id) => OutputValue::TokenV1(*id, amount), - } - } - - pub fn token_id(&self) -> Option<&TokenId> { - match self { - Currency::Coin => None, - Currency::Token(id) => Some(id), + Self::Coin => OutputValue::Coin(amount), + Self::Token(id) => OutputValue::TokenV1(*id, amount), } } } diff --git a/common/src/chain/transaction/output/output_values_holder.rs b/common/src/chain/transaction/output/output_values_holder.rs index 10b256f21..658c30594 100644 --- a/common/src/chain/transaction/output/output_values_holder.rs +++ b/common/src/chain/transaction/output/output_values_holder.rs @@ -58,3 +58,31 @@ pub fn collect_token_v1_ids_from_output_values_holders<'a, H: OutputValuesHolder } result } + +pub fn collect_token_v1_ids_from_rpc_output_values_holder_into( + holder: &impl RpcOutputValuesHolder, + dest: &mut BTreeSet, +) { + for token_id in holder + .rpc_output_values_iter() + .flat_map(|output_value| output_value.token_id().into_iter()) + { + dest.insert(*token_id); + } +} + +pub fn collect_token_v1_ids_from_rpc_output_values_holder( + holder: &impl RpcOutputValuesHolder, +) -> BTreeSet { + collect_token_v1_ids_from_rpc_output_values_holders(std::iter::once(holder)) +} + +pub fn collect_token_v1_ids_from_rpc_output_values_holders<'a, H: RpcOutputValuesHolder + 'a>( + holders: impl IntoIterator, +) -> BTreeSet { + let mut result = BTreeSet::new(); + for holder in holders { + collect_token_v1_ids_from_rpc_output_values_holder_into(holder, &mut result); + } + result +} diff --git a/mempool/src/pool/tx_pool/tx_verifier/chainstate_handle.rs b/mempool/src/pool/tx_pool/tx_verifier/chainstate_handle.rs index 4f678ab32..64fa21fa2 100644 --- a/mempool/src/pool/tx_pool/tx_verifier/chainstate_handle.rs +++ b/mempool/src/pool/tx_pool/tx_verifier/chainstate_handle.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use chainstate::{ chainstate_interface::ChainstateInterface, @@ -372,6 +372,10 @@ impl OrdersAccountingView for ChainstateHandle { let id = *id; self.call(move |c| c.get_order_give_balance(&id).map(|v| v.unwrap_or(Amount::ZERO))) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + self.call(|c| c.get_all_order_ids()) + } } impl OrdersAccountingStorageRead for ChainstateHandle { @@ -391,4 +395,8 @@ impl OrdersAccountingStorageRead for ChainstateHandle { let id = *id; self.call(move |c| c.get_order_give_balance(&id)) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + self.call(|c| c.get_all_order_ids()) + } } diff --git a/mocks/src/chainstate.rs b/mocks/src/chainstate.rs index 243ca26b9..1d6b84c1f 100644 --- a/mocks/src/chainstate.rs +++ b/mocks/src/chainstate.rs @@ -30,8 +30,8 @@ use common::{ GenBlock, }, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, ChainConfig, DelegationId, OrderId, PoolId, RpcOrderInfo, - TxInput, UtxoOutPoint, + AccountNonce, AccountType, ChainConfig, DelegationId, OrderId, PoolId, RpcCurrency, + RpcOrderInfo, TxInput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, }; @@ -209,7 +209,13 @@ mockall::mock! { fn get_order_data(&self, id: &OrderId) -> Result, ChainstateError>; fn get_order_ask_balance(&self, id: &OrderId) -> Result, ChainstateError>; fn get_order_give_balance(&self, id: &OrderId) -> Result, ChainstateError>; - fn get_order_info_for_rpc(&self, id: OrderId) -> Result, ChainstateError>; + fn get_order_info_for_rpc(&self, id: &OrderId) -> Result, ChainstateError>; + fn get_all_order_ids(&self) -> Result, ChainstateError>; + fn get_orders_info_for_rpc_by_currencies<'a>( + &self, + ask_currency: Option<&'a RpcCurrency>, + give_currency: Option<&'a RpcCurrency>, + ) -> Result, ChainstateError>; } } diff --git a/orders-accounting/src/cache.rs b/orders-accounting/src/cache.rs index b9f071a0b..4f625ef56 100644 --- a/orders-accounting/src/cache.rs +++ b/orders-accounting/src/cache.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::BTreeSet; + use accounting::combine_amount_delta; use common::{ chain::{OrderId, OrdersVersion}, @@ -140,6 +142,29 @@ impl OrdersAccountingView for OrdersAccountingCache

let local_delta = self.data.give_balances.data().get(id).cloned(); combine_amount_delta(parent_supply, local_delta).map_err(Error::AccountingError) } + + fn get_all_order_ids(&self) -> Result> { + // FIXME break this and ensure tests fail + + Ok(self + .parent + .get_all_order_ids() + .map_err(|_| Error::ViewFail)? + .into_iter() + .filter(|id| match self.data.order_data.get_data(id) { + accounting::GetDataResult::Present(_) | accounting::GetDataResult::Missing => true, + accounting::GetDataResult::Deleted => false, + }) + .chain(self.data.order_data.data().keys().copied().filter(|id| { + match self.data.order_data.get_data(&id) { + accounting::GetDataResult::Present(_) => true, + accounting::GetDataResult::Missing | accounting::GetDataResult::Deleted => { + false + } + } + })) + .collect()) + } } impl OrdersAccountingOperations for OrdersAccountingCache

{ diff --git a/orders-accounting/src/data.rs b/orders-accounting/src/data.rs index 5a403d37a..d07ba8d12 100644 --- a/orders-accounting/src/data.rs +++ b/orders-accounting/src/data.rs @@ -68,6 +68,10 @@ impl OrderData { }) } } + + pub fn consume(self) -> (Destination, OutputValue, OutputValue, bool) { + (self.conclude_key, self.ask, self.give, self.is_frozen) + } } impl From for OrderData { diff --git a/orders-accounting/src/storage/db.rs b/orders-accounting/src/storage/db.rs index d55170d8d..cb3ebe1d6 100644 --- a/orders-accounting/src/storage/db.rs +++ b/orders-accounting/src/storage/db.rs @@ -13,7 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, ops::Neg}; +use std::{ + collections::{BTreeMap, BTreeSet}, + ops::Neg, +}; use accounting::{ combine_amount_delta, combine_data_with_delta, DeltaAmountCollection, DeltaDataUndoCollection, @@ -137,6 +140,10 @@ impl OrdersAccountingView for OrdersAccountingDB fn get_give_balance(&self, id: &OrderId) -> Result { self.0.get_give_balance(id).map(|v| v.unwrap_or(Amount::ZERO)) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + self.0.get_all_order_ids() + } } impl OrdersAccountingStorageRead for OrdersAccountingDB { @@ -153,6 +160,10 @@ impl OrdersAccountingStorageRead for OrdersAccou fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { self.0.get_give_balance(id) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + self.0.get_all_order_ids() + } } impl OrdersAccountingStorageWrite for OrdersAccountingDB { diff --git a/orders-accounting/src/storage/in_memory.rs b/orders-accounting/src/storage/in_memory.rs index 417cb02a4..9d1abf749 100644 --- a/orders-accounting/src/storage/in_memory.rs +++ b/orders-accounting/src/storage/in_memory.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use common::{chain::OrderId, primitives::Amount}; @@ -77,6 +77,10 @@ impl OrdersAccountingStorageRead for InMemoryOrdersAccounting { fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { Ok(self.give_balances.get(id).cloned()) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + Ok(self.orders_data.keys().copied().collect()) + } } impl OrdersAccountingStorageWrite for InMemoryOrdersAccounting { diff --git a/orders-accounting/src/storage/mod.rs b/orders-accounting/src/storage/mod.rs index c1fa70feb..ed99a88d4 100644 --- a/orders-accounting/src/storage/mod.rs +++ b/orders-accounting/src/storage/mod.rs @@ -13,8 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::{ + collections::BTreeSet, + ops::{Deref, DerefMut}, +}; + use common::{chain::OrderId, primitives::Amount}; -use std::ops::{Deref, DerefMut}; use crate::OrderData; @@ -44,6 +48,9 @@ pub trait OrdersAccountingStorageRead { /// /// It's represented by `Amount` to simplify accounting math and the currency can be enquired from OrderData. fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error>; + + /// Return ids of all orders that exist in the storage. + fn get_all_order_ids(&self) -> Result, Self::Error>; } pub trait OrdersAccountingStorageWrite: OrdersAccountingStorageRead { @@ -75,6 +82,10 @@ where fn get_give_balance(&self, id: &OrderId) -> Result, Self::Error> { self.deref().get_give_balance(id) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + self.deref().get_all_order_ids() + } } impl OrdersAccountingStorageWrite for T diff --git a/orders-accounting/src/view.rs b/orders-accounting/src/view.rs index 78ae75132..74e374d83 100644 --- a/orders-accounting/src/view.rs +++ b/orders-accounting/src/view.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::ops::Deref; +use std::{collections::BTreeSet, ops::Deref}; use common::{chain::OrderId, primitives::Amount}; @@ -26,6 +26,7 @@ pub trait OrdersAccountingView { fn get_order_data(&self, id: &OrderId) -> Result, Self::Error>; fn get_ask_balance(&self, id: &OrderId) -> Result; fn get_give_balance(&self, id: &OrderId) -> Result; + fn get_all_order_ids(&self) -> Result, Self::Error>; } pub trait FlushableOrdersAccountingView { @@ -57,4 +58,8 @@ where fn get_give_balance(&self, id: &OrderId) -> Result { self.deref().get_give_balance(id) } + + fn get_all_order_ids(&self) -> Result, Self::Error> { + self.deref().get_all_order_ids() + } } diff --git a/wallet/src/account/currency_grouper/mod.rs b/wallet/src/account/currency_grouper/mod.rs index 8d92fc39b..ea09a9b90 100644 --- a/wallet/src/account/currency_grouper/mod.rs +++ b/wallet/src/account/currency_grouper/mod.rs @@ -24,7 +24,7 @@ use common::{ chain::{output_value::OutputValue, ChainConfig, Destination, TxOutput}, primitives::{Amount, BlockHeight}, }; -use wallet_types::currency::Currency; +use wallet_types::Currency; use super::UtxoSelectorError; diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index a1f404d93..ba750a6e4 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -24,6 +24,7 @@ use common::{ block::timestamp::BlockTimestamp, make_delegation_id, make_order_id, make_token_id, make_token_id_with_version, output_value::{OutputValue, RpcOutputValue}, + output_values_holder::RpcOutputValuesHolder, stakelock::StakePoolData, tokens::{ get_referenced_token_ids_ignore_issuance, IsTokenFreezable, IsTokenUnfreezable, @@ -563,6 +564,12 @@ impl OrderData { } } +impl RpcOutputValuesHolder for OrderData { + fn rpc_output_values_iter(&self) -> impl Iterator { + [&self.initially_asked, &self.initially_given].into_iter() + } +} + /// A helper structure for the UTXO search. /// /// All transactions and blocks from the DB are cached here. If a transaction diff --git a/wallet/src/account/transaction_list/mod.rs b/wallet/src/account/transaction_list/mod.rs index 16123031b..8ba857736 100644 --- a/wallet/src/account/transaction_list/mod.rs +++ b/wallet/src/account/transaction_list/mod.rs @@ -21,9 +21,8 @@ use common::{ }; use serde::Serialize; use wallet_types::{ - currency::Currency, wallet_tx::{TxData, TxState}, - WalletTx, + Currency, WalletTx, }; use crate::{key_chain::AccountKeyChains, WalletError, WalletResult}; diff --git a/wallet/src/send_request/mod.rs b/wallet/src/send_request/mod.rs index ec9220f49..1f3a89b1e 100644 --- a/wallet/src/send_request/mod.rs +++ b/wallet/src/send_request/mod.rs @@ -13,30 +13,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeMap; -use std::mem::take; - -use common::address::Address; -use common::chain::htlc::HtlcSecret; -use common::chain::output_value::OutputValue; -use common::chain::stakelock::StakePoolData; -use common::chain::timelock::OutputTimeLock::ForBlockCount; -use common::chain::tokens::{Metadata, TokenId, TokenIssuance}; -use common::chain::{ - ChainConfig, Destination, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, +use std::{collections::BTreeMap, mem::take}; + +use common::{ + address::Address, + chain::{ + htlc::HtlcSecret, + output_value::OutputValue, + stakelock::StakePoolData, + timelock::OutputTimeLock::ForBlockCount, + tokens::{Metadata, TokenId, TokenIssuance}, + ChainConfig, Destination, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, + }, + primitives::{per_thousand::PerThousand, Amount, BlockHeight}, }; -use common::primitives::per_thousand::PerThousand; -use common::primitives::{Amount, BlockHeight}; use crypto::vrf::VRFPublicKey; use utils::ensure; -use wallet_types::currency::Currency; -use wallet_types::partially_signed_transaction::{ - PartiallySignedTransaction, PartiallySignedTransactionWalletExt as _, PtxAdditionalInfo, +use wallet_types::{ + partially_signed_transaction::{ + PartiallySignedTransaction, PartiallySignedTransactionWalletExt as _, PtxAdditionalInfo, + }, + Currency, }; -use crate::account::PoolData; -use crate::destination_getters::{get_tx_output_destination, HtlcSpendingCondition}; -use crate::{WalletError, WalletResult}; +use crate::{ + account::PoolData, + destination_getters::{get_tx_output_destination, HtlcSpendingCondition}, + WalletError, WalletResult, +}; /// The `SendRequest` struct provides the necessary information to the wallet /// on the precise method of sending funds to a designated destination. diff --git a/wallet/types/src/lib.rs b/wallet/types/src/lib.rs index 6e834cd17..c3274c536 100644 --- a/wallet/types/src/lib.rs +++ b/wallet/types/src/lib.rs @@ -16,7 +16,6 @@ pub mod account_id; pub mod account_info; pub mod chain_info; -pub mod currency; pub mod generic_transaction; pub mod hw_data; pub mod keys; @@ -34,7 +33,6 @@ pub use account_id::{ AccountWalletTxId, }; pub use account_info::AccountInfo; -pub use currency::Currency; pub use keys::{KeyPurpose, KeychainUsageState, RootKeys}; pub use wallet_tx::{BlockInfo, WalletTx}; @@ -47,6 +45,8 @@ use common::{ use crate::scan_blockchain::ScanBlockchain; +pub type Currency = common::chain::RpcCurrency; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SignedTxWithFees { pub tx: SignedTransaction, diff --git a/wallet/wallet-cli-commands/Cargo.toml b/wallet/wallet-cli-commands/Cargo.toml index 478ebda3f..c7cbad7f8 100644 --- a/wallet/wallet-cli-commands/Cargo.toml +++ b/wallet/wallet-cli-commands/Cargo.toml @@ -29,6 +29,7 @@ wallet-storage = { path = "../storage" } wallet-types = { path = "../types" } async-trait.workspace = true +bigdecimal.workspace = true clap = { workspace = true, features = ["derive"] } crossterm.workspace = true derive_more.workspace = true diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 68152b177..5e91a872a 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -15,8 +15,9 @@ mod local_state; -use std::{fmt::Write, str::FromStr}; +use std::{collections::BTreeMap, fmt::Write, str::FromStr}; +use bigdecimal::BigDecimal; use itertools::Itertools; use chainstate::rpc::RpcOutputValueOut; @@ -51,7 +52,9 @@ use wallet_types::{partially_signed_transaction::PartiallySignedTransaction, Cur use crate::{ errors::WalletCliCommandError, - helper_types::{format_token_name, parse_currency, parse_generic_token_transfer}, + helper_types::{ + active_order_infos_header, format_token_name, parse_currency, parse_generic_token_transfer, + }, CreateWalletDeviceSelectMenu, ManageableWalletCommand, OpenWalletDeviceSelectMenu, OpenWalletSubCommand, WalletManagementCommand, }; @@ -60,8 +63,8 @@ use self::local_state::WalletWithState; use super::{ helper_types::{ - format_delegation_info, format_own_order_info, format_pool_info, parse_coin_output, - parse_token_supply, parse_utxo_outpoint, CliForceReduce, CliUtxoState, + format_active_order_info, format_delegation_info, format_own_order_info, format_pool_info, + parse_coin_output, parse_token_supply, parse_utxo_outpoint, CliForceReduce, CliUtxoState, }, ColdWalletCommand, ConsoleCommand, WalletCommand, }; @@ -2109,6 +2112,164 @@ where order_infos.join("\n") ))) } + WalletCommand::ListActiveOrders { + ask_currency, + give_currency, + } => { + let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; + + let ask_currency = ask_currency + .map(|ask_currency| parse_currency(&ask_currency, chain_config)) + .transpose()?; + let give_currency = give_currency + .map(|give_currency| parse_currency(&give_currency, chain_config)) + .transpose()?; + + let order_infos = wallet + .list_all_active_orders(selected_account, ask_currency, give_currency) + .await?; + let token_ids_iter = get_token_ids_from_order_infos( + order_infos.iter().map(|info| (&info.initially_asked, &info.initially_given)), + ); + let token_ids_str_vec = token_ids_iter + .clone() + .map(|rpc_addr| rpc_addr.as_str().to_owned()) + .collect_vec(); + let token_ids_map = token_ids_iter + .map(|rpc_addr| -> Result<_, WalletCliCommandError> { + Ok(( + rpc_addr.clone(), + rpc_addr + .decode_object(chain_config) + .map_err(WalletCliCommandError::TokenIdDecodingError)?, + )) + }) + .collect::, _>>()?; + let token_infos = wallet + .node_get_tokens_info(token_ids_str_vec) + .await? + .into_iter() + .map(|info| (info.token_id(), info)) + .collect::>(); + + let currency_decimals = |token_id_opt: Option<&TokenId>| { + if let Some(token_id) = token_id_opt { + // FIXME + token_infos + .get(token_id) + .expect("Token id is known to be present") + .token_number_of_decimals() + } else { + chain_config.coin_decimals() + } + }; + let token_ticker_str_by_id = |token_id| { + let ticker_bytes = + token_infos.get(token_id).expect("Token info must exist").token_ticker(); + // FIXME allow only alpha num tickers?? + str::from_utf8(ticker_bytes).ok() + }; + + let order_infos_with_price = order_infos + .into_iter() + .map(|info| { + let asked_token_id = info.initially_asked.token_id().map(|token_id| { + token_ids_map.get(token_id).expect("Token id is known to be present") + }); + let given_token_id = info.initially_given.token_id().map(|token_id| { + token_ids_map.get(token_id).expect("Token id is known to be present") + }); + let ask_currency_decimals = currency_decimals(asked_token_id); + let give_currency_decimals = currency_decimals(given_token_id); + + let give_ask_price = BigDecimal::new( + info.initially_given.amount().amount().into_atoms().into(), + give_currency_decimals as i64, + ) / BigDecimal::new( + info.initially_asked.amount().amount().into_atoms().into(), + ask_currency_decimals as i64, + ); + // FIXME check that it works correctly + let give_ask_price = give_ask_price.with_scale_round( + give_currency_decimals.into(), + bigdecimal::rounding::RoundingMode::Down, + ); + + (info, give_ask_price) + }) + .collect_vec(); + + use std::cmp::Ordering; + + let compare_currencies = + |token1_id: Option<&RpcAddress>, + token2_id: Option<&RpcAddress>| { + match (token1_id, token2_id) { + (Some(token1_id_as_addr), Some(token2_id_as_addr)) => { + let token1_id = token_ids_map + .get(token1_id_as_addr) + .expect("Token id is known to be present"); + let token2_id = token_ids_map + .get(token2_id_as_addr) + .expect("Token id is known to be present"); + + if token1_id == token2_id { + Ordering::Equal + } else { + let token1_ticker = token_ticker_str_by_id(token1_id); + let token2_ticker = token_ticker_str_by_id(token2_id); + let token_ids_as_addr_cmp = + token1_id_as_addr.cmp(token2_id_as_addr); + + // If tickers are valid strings, compare them first; if they are + // equal, compare the ids. + // Otherwise, put tokens with bad tickers later on the list. + // If both tickers are bad, compare the ids. + match (token1_ticker, token2_ticker) { + (Some(token1_ticker), Some(token2_ticker)) => token1_ticker + .cmp(token2_ticker) + .then(token_ids_as_addr_cmp), + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => token_ids_as_addr_cmp, + } + } + } + // The coin should come first, so it's "less" than tokens. + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + } + }; + + let order_infos_with_give_ask_price = order_infos_with_price.sorted_by( + |(order1_info, order1_price), (order2_info, order2_price)| { + let order1_asked_token_id = order1_info.initially_asked.token_id(); + let order1_given_token_id = order1_info.initially_given.token_id(); + let order2_asked_token_id = order2_info.initially_asked.token_id(); + let order2_given_token_id = order2_info.initially_given.token_id(); + + compare_currencies(order1_given_token_id, order2_given_token_id) + .then_with(|| { + compare_currencies(order1_asked_token_id, order2_asked_token_id) + }) + .then_with(|| order2_price.cmp(order1_price)) + }, + ); + + let formatted_order_infos = order_infos_with_give_ask_price + .iter() + .map(|(info, give_ask_price)| { + format_active_order_info(info, give_ask_price, chain_config, &token_infos) + }) + .collect::, _>>()?; + + Ok(ConsoleCommand::Print(format!( + "{}\n{}\n", + active_order_infos_header(), + formatted_order_infos.join("\n") + ))) + } } } diff --git a/wallet/wallet-cli-commands/src/helper_types.rs b/wallet/wallet-cli-commands/src/helper_types.rs index 35de5c95a..a00b38b0d 100644 --- a/wallet/wallet-cli-commands/src/helper_types.rs +++ b/wallet/wallet-cli-commands/src/helper_types.rs @@ -15,6 +15,7 @@ use std::{collections::BTreeMap, fmt::Display, str::FromStr}; +use bigdecimal::BigDecimal; use chainstate::rpc::RpcOutputValueOut; use clap::ValueEnum; @@ -30,7 +31,9 @@ use common::{ }; use itertools::Itertools; use wallet_controller::types::{GenericCurrencyTransfer, GenericTokenTransfer}; -use wallet_rpc_lib::types::{NodeInterface, OwnOrderInfo, PoolInfo, TokenTotalSupply}; +use wallet_rpc_lib::types::{ + ActiveOrderInfo, NodeInterface, OwnOrderInfo, PoolInfo, TokenTotalSupply, +}; use wallet_types::{ seed_phrase::StoreSeedPhrase, utxo_types::{UtxoState, UtxoType}, @@ -155,30 +158,27 @@ pub fn format_own_order_info( }; Ok(format!( concat!( - "Order Id: {id}, ", - "Initially asked: {ia}, ", - "Initially given: {ig}, ", - "Remaining ask amount: {ra}, ", - "Remaining give amount: {rg}, ", - "Accumulated ask amount: {aa}, ", + "Id: {id}, ", + "Asked: {ia} [left: {ra}, can withdraw: {aa}], ", + "Given: {ig} [left: {rg}], ", "Created at: {ts}, ", "Status: {st}" ), id = order_info.order_id, ia = format_output_value(&order_info.initially_asked, chain_config, token_infos)?, - ig = format_output_value(&order_info.initially_given, chain_config, token_infos)?, ra = existing_order_data.ask_balance.decimal(), - rg = existing_order_data.give_balance.decimal(), aa = accumulated_ask_amount, + ig = format_output_value(&order_info.initially_given, chain_config, token_infos)?, + rg = existing_order_data.give_balance.decimal(), ts = existing_order_data.creation_timestamp.into_time(), st = status, )) } else { Ok(format!( concat!( - "Order Id: {id}, ", - "Initially asked: {ia}, ", - "Initially given: {ig}, ", + "Id: {id}, ", + "Asked: {ia}, ", + "Given: {ig}, ", "Status: Unconfirmed" ), id = order_info.order_id, @@ -188,17 +188,58 @@ pub fn format_own_order_info( } } -pub fn format_output_value( +pub fn active_order_infos_header() -> &'static str { + concat!( + "The list of active orders goes below, orders belonging to this account are marked with '*'.\n", + "WARNING: token tickers are not unique, always check the token id when buying a token." + ) +} + +pub fn format_active_order_info( + order_info: &ActiveOrderInfo, + give_ask_price: &BigDecimal, + chain_config: &ChainConfig, + token_infos: &BTreeMap, +) -> Result> { + // Note: we show what's given first because the orders are sorted by the given currency first + // by the caller code. + Ok(format!( + concat!( + "{marker} ", + "Id: {id}, ", + "Given: {g} [left: {rg}], ", + "Asked: {a} [left: {ra}], ", + "Give/Ask: {price}, " + ), + marker = if order_info.is_own { "*" } else { " " }, + id = order_info.order_id, + g = format_asset_name(&order_info.initially_given, chain_config, token_infos)?, + a = format_asset_name(&order_info.initially_asked, chain_config, token_infos)?, + rg = order_info.give_balance.decimal(), + ra = order_info.ask_balance.decimal(), + price = give_ask_price.normalized(), + )) +} + +pub fn format_asset_name( value: &RpcOutputValueOut, chain_config: &ChainConfig, token_infos: &BTreeMap, ) -> Result> { - let asset_name = if let Some(token_id) = value.token_id() { + let result = if let Some(token_id) = value.token_id() { format_token_name(token_id, chain_config, token_infos)? } else { chain_config.coin_ticker().to_owned() }; + Ok(result) +} +pub fn format_output_value( + value: &RpcOutputValueOut, + chain_config: &ChainConfig, + token_infos: &BTreeMap, +) -> Result> { + let asset_name = format_asset_name(value, chain_config, token_infos)?; Ok(format!("{} {}", value.amount().decimal(), asset_name)) } diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index edf7501f7..275722a18 100644 --- a/wallet/wallet-cli-commands/src/lib.rs +++ b/wallet/wallet-cli-commands/src/lib.rs @@ -1145,6 +1145,19 @@ pub enum WalletCommand { /// List orders whose conclude key is owned by the selected account. #[clap(name = "order-list-own")] ListOwnOrders, + + /// List all active (i.e. not concluded and not frozen) orders, optionally filtering them + /// by the "asked" or "given" currency. + #[clap(name = "order-list-all-active")] + ListActiveOrders { + /// Filter orders by the specified "asked" currency - pass a token id or "coin" for coins. + #[arg(long = "ask-currency")] + ask_currency: Option, + + /// Filter orders by the specified "given" currency - pass a token id or "coin" for coins. + #[arg(long = "give-currency")] + give_currency: Option, + }, } #[derive(Debug, Parser)] diff --git a/wallet/wallet-controller/src/sync/tests/mod.rs b/wallet/wallet-controller/src/sync/tests/mod.rs index d1ebbd1fb..b6e9639be 100644 --- a/wallet/wallet-controller/src/sync/tests/mod.rs +++ b/wallet/wallet-controller/src/sync/tests/mod.rs @@ -312,6 +312,14 @@ impl NodeInterface for MockNode { unreachable!() } + async fn get_orders_info_by_currencies( + &self, + _ask_currency: Option, + _give_currency: Option, + ) -> Result, Self::Error> { + unreachable!() + } + async fn generate_block_e2e( &self, _encrypted_input_data: Vec, diff --git a/wallet/wallet-node-client/src/handles_client/mod.rs b/wallet/wallet-node-client/src/handles_client/mod.rs index a9d9d98a1..8bb5b359b 100644 --- a/wallet/wallet-node-client/src/handles_client/mod.rs +++ b/wallet/wallet-node-client/src/handles_client/mod.rs @@ -13,7 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeSet, num::NonZeroUsize, time::Duration}; +use std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZeroUsize, + time::Duration, +}; use blockprod::{BlockProductionError, BlockProductionHandle, TimestampSearchData}; use chainstate::{BlockSource, ChainInfo, ChainstateError, ChainstateHandle}; @@ -242,7 +246,24 @@ impl NodeInterface for WalletHandlesClient { async fn get_order_info(&self, order_id: OrderId) -> Result, Self::Error> { let result = self .chainstate - .call(move |this| this.get_order_info_for_rpc(order_id)) + .call(move |this| this.get_order_info_for_rpc(&order_id)) + .await??; + Ok(result) + } + + async fn get_orders_info_by_currencies( + &self, + ask_currency: Option, // FIXME use RpcCurrency directly? + give_currency: Option, + ) -> Result, Self::Error> { + let result = self + .chainstate + .call(move |this| { + this.get_orders_info_for_rpc_by_currencies( + ask_currency.as_ref(), + give_currency.as_ref(), + ) + }) .await??; Ok(result) } diff --git a/wallet/wallet-node-client/src/mock.rs b/wallet/wallet-node-client/src/mock.rs index 9b2b3cd81..01f6e8024 100644 --- a/wallet/wallet-node-client/src/mock.rs +++ b/wallet/wallet-node-client/src/mock.rs @@ -13,7 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeSet, num::NonZeroUsize, sync::Arc, time::Duration}; +use std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZeroUsize, + sync::Arc, + time::Duration, +}; + +use tokio::sync::{Mutex, MutexGuard}; use chainstate::ChainInfo; use common::{ @@ -31,7 +38,6 @@ use p2p::{ interface::types::ConnectedPeer, types::{bannable_address::BannableAddress, socket_address::SocketAddress, PeerId}, }; -use tokio::sync::{Mutex, MutexGuard}; use utils_networking::IpOrSocketAddress; use wallet_types::wallet_type::WalletControllerMode; @@ -151,6 +157,17 @@ impl NodeInterface for ClonableMockNodeInterface { self.lock().await.get_order_info(order_id).await } + async fn get_orders_info_by_currencies( + &self, + ask_currency: Option, + give_currency: Option, + ) -> Result, Self::Error> { + self.lock() + .await + .get_orders_info_by_currencies(ask_currency, give_currency) + .await + } + async fn blockprod_e2e_public_key(&self) -> Result { self.lock().await.blockprod_e2e_public_key().await } diff --git a/wallet/wallet-node-client/src/node_traits.rs b/wallet/wallet-node-client/src/node_traits.rs index 8375b3f2c..3903b86b8 100644 --- a/wallet/wallet-node-client/src/node_traits.rs +++ b/wallet/wallet-node-client/src/node_traits.rs @@ -13,7 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeSet, num::NonZeroUsize, time::Duration}; +use std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZeroUsize, + time::Duration, +}; use chainstate::ChainInfo; use common::{ @@ -82,6 +86,11 @@ pub trait NodeInterface { token_ids: BTreeSet, ) -> Result, Self::Error>; async fn get_order_info(&self, order_id: OrderId) -> Result, Self::Error>; + async fn get_orders_info_by_currencies( + &self, + ask_currency: Option, + give_currency: Option, + ) -> Result, Self::Error>; async fn blockprod_e2e_public_key(&self) -> Result; async fn generate_block( &self, diff --git a/wallet/wallet-node-client/src/rpc_client/client_impl.rs b/wallet/wallet-node-client/src/rpc_client/client_impl.rs index a3545a4a3..1fb06bec3 100644 --- a/wallet/wallet-node-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-node-client/src/rpc_client/client_impl.rs @@ -13,7 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeSet, num::NonZeroUsize, time::Duration}; +use std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZeroUsize, + time::Duration, +}; use blockprod::{rpc::BlockProductionRpcClient, TimestampSearchData}; use chainstate::{rpc::ChainstateRpcClient, ChainInfo}; @@ -195,6 +199,20 @@ impl NodeInterface for NodeRpcClient { .map_err(NodeRpcError::ResponseError) } + async fn get_orders_info_by_currencies( + &self, + ask_currency: Option, + give_currency: Option, + ) -> Result, Self::Error> { + ChainstateRpcClient::orders_info_by_currencies( + &self.http_client, + ask_currency, + give_currency, + ) + .await + .map_err(NodeRpcError::ResponseError) + } + async fn blockprod_e2e_public_key(&self) -> Result { BlockProductionRpcClient::e2e_public_key(&self.http_client) .await diff --git a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs index f2d9ba9c2..8a8b42af9 100644 --- a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs +++ b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs @@ -13,7 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeSet, num::NonZeroUsize, time::Duration}; +use std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZeroUsize, + time::Duration, +}; use blockprod::TimestampSearchData; use chainstate::ChainInfo; @@ -155,6 +159,14 @@ impl NodeInterface for ColdWalletClient { Err(ColdWalletRpcError::NotAvailable) } + async fn get_orders_info_by_currencies( + &self, + _ask_currency: Option, + _give_currency: Option, + ) -> Result, Self::Error> { + Err(ColdWalletRpcError::NotAvailable) + } + async fn blockprod_e2e_public_key(&self) -> Result { Err(ColdWalletRpcError::NotAvailable) } diff --git a/wallet/wallet-rpc-client/src/handles_client/mod.rs b/wallet/wallet-rpc-client/src/handles_client/mod.rs index 456115366..bc2061bd4 100644 --- a/wallet/wallet-rpc-client/src/handles_client/mod.rs +++ b/wallet/wallet-rpc-client/src/handles_client/mod.rs @@ -43,8 +43,8 @@ use wallet_controller::{ }; use wallet_rpc_lib::{ types::{ - AccountExtendedPublicKey, AddressInfo, AddressWithUsageInfo, Balances, BlockInfo, - ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, + AccountExtendedPublicKey, ActiveOrderInfo, AddressInfo, AddressWithUsageInfo, Balances, + BlockInfo, ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegationTransaction, NewOrderTransaction, NewSubmittedTransaction, NewTokenTransaction, NftMetadata, NodeVersion, OpenedWallet, OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcHashedTimelockContract, RpcInspectTransaction, @@ -1217,6 +1217,18 @@ where .map_err(WalletRpcHandlesClientError::WalletRpcError) } + async fn list_all_active_orders( + &self, + account_index: U31, + ask_curency: Option, + give_curency: Option, + ) -> Result, Self::Error> { + self.wallet_rpc + .list_all_active_orders(account_index, ask_curency, give_curency) + .await + .map_err(WalletRpcHandlesClientError::WalletRpcError) + } + async fn node_version(&self) -> Result { self.wallet_rpc .node_version() diff --git a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs index 23976bd9a..eff37dbec 100644 --- a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs @@ -45,7 +45,7 @@ use wallet_controller::{ }; use wallet_rpc_lib::{ types::{ - AccountExtendedPublicKey, AddressInfo, AddressWithUsageInfo, BlockInfo, + AccountExtendedPublicKey, ActiveOrderInfo, AddressInfo, AddressWithUsageInfo, BlockInfo, ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegationTransaction, NewOrderTransaction, NewSubmittedTransaction, NewTokenTransaction, NftMetadata, NodeVersion, OpenedWallet, @@ -1127,6 +1127,22 @@ impl WalletInterface for ClientWalletRpc { .map_err(WalletRpcError::ResponseError) } + async fn list_all_active_orders( + &self, + account_index: U31, + ask_curency: Option, + give_curency: Option, + ) -> Result, Self::Error> { + WalletRpcClient::list_all_active_orders( + &self.http_client, + account_index.into(), + ask_curency, + give_curency, + ) + .await + .map_err(WalletRpcError::ResponseError) + } + async fn node_version(&self) -> Result { WalletRpcClient::node_version(&self.http_client) .await diff --git a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs index e2e544e3c..340854631 100644 --- a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs +++ b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs @@ -35,12 +35,12 @@ use wallet_controller::{ ConnectedPeer, ControllerConfig, UtxoState, UtxoType, }; use wallet_rpc_lib::types::{ - AccountExtendedPublicKey, AddressInfo, AddressWithUsageInfo, Balances, BlockInfo, - ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, - NewAccountInfo, NewDelegationTransaction, NewOrderTransaction, NewSubmittedTransaction, - NewTokenTransaction, NftMetadata, NodeVersion, OpenedWallet, OwnOrderInfo, PoolInfo, - PublicKeyInfo, RpcHashedTimelockContract, RpcInspectTransaction, RpcNewTransaction, - RpcPreparedTransaction, RpcSignatureStatus, RpcStandaloneAddresses, + AccountExtendedPublicKey, ActiveOrderInfo, AddressInfo, AddressWithUsageInfo, Balances, + BlockInfo, ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, + LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegationTransaction, NewOrderTransaction, + NewSubmittedTransaction, NewTokenTransaction, NftMetadata, NodeVersion, OpenedWallet, + OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcHashedTimelockContract, RpcInspectTransaction, + RpcNewTransaction, RpcPreparedTransaction, RpcSignatureStatus, RpcStandaloneAddresses, SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, }; @@ -548,6 +548,13 @@ pub trait WalletInterface { async fn list_own_orders(&self, account_index: U31) -> Result, Self::Error>; + async fn list_all_active_orders( + &self, + account_index: U31, + ask_curency: Option, + give_curency: Option, + ) -> Result, Self::Error>; + async fn node_version(&self) -> Result; async fn node_shutdown(&self) -> Result<(), Self::Error>; diff --git a/wallet/wallet-rpc-lib/src/rpc/interface.rs b/wallet/wallet-rpc-lib/src/rpc/interface.rs index 7f5384973..9dd347e9c 100644 --- a/wallet/wallet-rpc-lib/src/rpc/interface.rs +++ b/wallet/wallet-rpc-lib/src/rpc/interface.rs @@ -43,15 +43,16 @@ use wallet_types::{ }; use crate::types::{ - AccountArg, AccountExtendedPublicKey, AddressInfo, AddressWithUsageInfo, Balances, ChainInfo, - ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, HexEncoded, - LegacyVrfPublicKeyInfo, MaybeSignedTransaction, NewAccountInfo, NewDelegationTransaction, - NewOrderTransaction, NewSubmittedTransaction, NewTokenTransaction, NftMetadata, NodeVersion, - OpenedWallet, OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcAmountIn, RpcHashedTimelockContract, - RpcInspectTransaction, RpcNewTransaction, RpcPreparedTransaction, RpcStandaloneAddresses, - RpcUtxoOutpoint, RpcUtxoState, RpcUtxoType, SendTokensFromMultisigAddressResult, - StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, - TransactionOptions, TransactionRequestOptions, TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, + AccountArg, AccountExtendedPublicKey, ActiveOrderInfo, AddressInfo, AddressWithUsageInfo, + Balances, ChainInfo, ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, + HexEncoded, LegacyVrfPublicKeyInfo, MaybeSignedTransaction, NewAccountInfo, + NewDelegationTransaction, NewOrderTransaction, NewSubmittedTransaction, NewTokenTransaction, + NftMetadata, NodeVersion, OpenedWallet, OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcAmountIn, + RpcHashedTimelockContract, RpcInspectTransaction, RpcNewTransaction, RpcPreparedTransaction, + RpcStandaloneAddresses, RpcUtxoOutpoint, RpcUtxoState, RpcUtxoType, + SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, + StandaloneAddressWithDetails, TokenMetadata, TransactionOptions, TransactionRequestOptions, + TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, }; #[rpc::rpc(server)] @@ -873,6 +874,16 @@ trait WalletRpc { #[method(name = "order_list_own")] async fn list_own_orders(&self, account: AccountArg) -> rpc::RpcResult>; + /// List all active (i.e. non-concluded, non-frozen) orders whose currencies match the passed ones. + /// If a passed currency is None, any order will match. + #[method(name = "order_list_all_active")] + async fn list_all_active_orders( + &self, + account: AccountArg, + ask_currency: Option, + give_currency: Option, + ) -> rpc::RpcResult>; + /// Obtain the node version #[method(name = "node_version")] async fn node_version(&self) -> rpc::RpcResult; diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index e7839d612..fc493b6ea 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -38,6 +38,7 @@ use common::{ classic_multisig::ClassicMultisigChallenge, htlc::{HashedTimelockContract, HtlcSecret, HtlcSecretHash}, output_value::OutputValue, + output_values_holder::collect_token_v1_ids_from_rpc_output_values_holders, signature::inputsig::arbitrary_message::{ produce_message_challenge, ArbitraryMessageSignature, }, @@ -88,7 +89,10 @@ use wallet_types::{ SignedTxWithFees, }; -use crate::{types::ExistingOwnOrderData, WalletHandle, WalletRpcConfig}; +use crate::{ + types::{ActiveOrderInfo, ExistingOwnOrderData}, + WalletHandle, WalletRpcConfig, +}; use self::types::{ AddressInfo, AddressWithUsageInfo, DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, @@ -1727,17 +1731,9 @@ where }) }) .await??; - let token_ids = wallet_orders_data - .iter() - .flat_map(|(_, order_data)| { - order_data - .initially_asked - .token_id() - .into_iter() - .chain(order_data.initially_given.token_id().into_iter()) - }) - .copied() - .collect::>(); + let token_ids = collect_token_v1_ids_from_rpc_output_values_holders( + wallet_orders_data.iter().map(|(_, order_data)| order_data), + ); let orders_data = wallet_orders_data .into_iter() .map(async |(order_id, wallet_order_data)| -> WRpcResult<_, N> { @@ -1836,6 +1832,79 @@ where })?) } + pub async fn list_all_active_orders( + &self, + account_index: U31, + ask_currency: Option, + give_currency: Option, + ) -> WRpcResult, N> { + let wallet_order_ids = self + .wallet + .call_async(move |controller| { + Box::pin(async move { + controller.readonly_controller(account_index).get_own_orders().await + }) + }) + .await?? + .into_iter() + .map(|(order_id, _)| order_id) + .collect::>(); + + let node_rpc_order_infos = self + .node + .get_orders_info_by_currencies(ask_currency, give_currency) + .await + .map_err(RpcError::RpcError)?; + + let token_ids = collect_token_v1_ids_from_rpc_output_values_holders( + node_rpc_order_infos.iter().map(|(_, order_node_rpc_info)| order_node_rpc_info), + ); + let token_decimals = self.get_tokens_decimals(token_ids).await?; + + let result = node_rpc_order_infos + .into_iter() + .filter_map(|(order_id, node_rpc_order_info)| { + (!node_rpc_order_info.is_frozen).then(|| -> WRpcResult<_, N> { + let order_id_as_rpc_addr = RpcAddress::new(&self.chain_config, order_id)?; + let initially_asked = RpcOutputValueOut::new( + &self.chain_config, + &token_decimals, + node_rpc_order_info.initially_asked.into(), + )?; + let initially_given = RpcOutputValueOut::new( + &self.chain_config, + &token_decimals, + node_rpc_order_info.initially_given.into(), + )?; + let ask_balance = make_rpc_amount_out( + node_rpc_order_info.ask_balance, + node_rpc_order_info.initially_asked.token_id(), + &self.chain_config, + &token_decimals, + )?; + let give_balance = make_rpc_amount_out( + node_rpc_order_info.give_balance, + node_rpc_order_info.initially_given.token_id(), + &self.chain_config, + &token_decimals, + )?; + let is_own = wallet_order_ids.contains(&order_id); + + Ok(ActiveOrderInfo { + order_id: order_id_as_rpc_addr, + initially_asked, + initially_given, + ask_balance, + give_balance, + is_own, + }) + }) + }) + .collect::>()?; + + Ok(result) + } + pub async fn compose_transaction( &self, inputs: Vec, diff --git a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs index 387055cba..be9a02a3d 100644 --- a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs +++ b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs @@ -47,14 +47,15 @@ use wallet_types::{ use crate::{ rpc::{ColdWalletRpcServer, WalletEventsRpcServer, WalletRpc, WalletRpcServer}, types::{ - AccountArg, AddressInfo, AddressWithUsageInfo, Balances, ChainInfo, ComposedTransaction, - CreatedWallet, DelegationInfo, HardwareWalletType, HexEncoded, LegacyVrfPublicKeyInfo, - MaybeSignedTransaction, NewAccountInfo, NewDelegationTransaction, NewSubmittedTransaction, - NftMetadata, NodeVersion, OpenedWallet, OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcAddress, - RpcAmountIn, RpcHexString, RpcInspectTransaction, RpcStandaloneAddresses, RpcUtxoOutpoint, - RpcUtxoState, RpcUtxoType, SendTokensFromMultisigAddressResult, StakePoolBalance, - StakingStatus, StandaloneAddressWithDetails, TokenMetadata, TransactionOptions, - TransactionRequestOptions, TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, + AccountArg, ActiveOrderInfo, AddressInfo, AddressWithUsageInfo, Balances, ChainInfo, + ComposedTransaction, CreatedWallet, DelegationInfo, HardwareWalletType, HexEncoded, + LegacyVrfPublicKeyInfo, MaybeSignedTransaction, NewAccountInfo, NewDelegationTransaction, + NewSubmittedTransaction, NftMetadata, NodeVersion, OpenedWallet, OwnOrderInfo, PoolInfo, + PublicKeyInfo, RpcAddress, RpcAmountIn, RpcHexString, RpcInspectTransaction, + RpcStandaloneAddresses, RpcUtxoOutpoint, RpcUtxoState, RpcUtxoType, + SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, + StandaloneAddressWithDetails, TokenMetadata, TransactionOptions, TransactionRequestOptions, + TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, }, RpcError, }; @@ -1130,6 +1131,18 @@ where rpc::handle_result(self.list_own_orders(account_arg.index::()?).await) } + async fn list_all_active_orders( + &self, + account_arg: AccountArg, + ask_currency: Option, + give_currency: Option, + ) -> rpc::RpcResult> { + rpc::handle_result( + self.list_all_active_orders(account_arg.index::()?, ask_currency, give_currency) + .await, + ) + } + async fn stake_pool_balance( &self, pool_id: RpcAddress, diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 693e2c53d..535398288 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -539,6 +539,7 @@ pub struct ExistingOwnOrderData { pub is_frozen: bool, } +/// This represents an order that is owned by the wallet. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] pub struct OwnOrderInfo { pub order_id: RpcAddress, @@ -554,6 +555,20 @@ pub struct OwnOrderInfo { pub is_marked_as_concluded_in_wallet: bool, } +/// This represents an arbitrary order that is active - i.e. not concluded, not frozen. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] +pub struct ActiveOrderInfo { + pub order_id: RpcAddress, + + pub initially_asked: RpcOutputValueOut, + pub initially_given: RpcOutputValueOut, + + pub ask_balance: RpcAmountOut, + pub give_balance: RpcAmountOut, + + pub is_own: bool, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] pub struct NewDelegationTransaction { pub delegation_id: RpcAddress, @@ -1079,13 +1094,6 @@ impl NewOrderTransaction { } } -#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] -#[serde(tag = "type", content = "content")] -pub enum RpcCurrency { - Coin, - Token { token_id: RpcAddress }, -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] #[serde(tag = "type", content = "content")] pub enum HardwareWalletType { From 79aadfdbe97e1531d15a7ceeb729377452bded7f Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Fri, 12 Dec 2025 20:04:09 +0200 Subject: [PATCH 04/10] Writing tests and applying FIXMEs - WIP --- Cargo.lock | 2 + chainstate/src/detail/query.rs | 24 +- chainstate/test-framework/src/helpers.rs | 49 +- .../test-suite/src/tests/orders_tests.rs | 798 ++++++++++++++---- .../test-suite/src/tests/tokens_misc_tests.rs | 9 +- common/src/chain/order/rpc.rs | 7 +- common/src/primitives/amount/decimal.rs | 59 +- node-daemon/docs/RPC.md | 66 +- orders-accounting/src/data.rs | 18 +- p2p/src/peer_manager/tests/connections.rs | 11 +- p2p/src/peer_manager/tests/mod.rs | 11 +- test-utils/src/lib.rs | 14 +- utxo/src/cache.rs | 9 +- utxo/src/tests/mod.rs | 37 +- utxo/src/tests/simulation.rs | 14 +- utxo/src/tests/simulation_with_undo.rs | 21 +- utxo/src/tests/test_helper.rs | 12 - wallet/Cargo.toml | 1 + wallet/src/account/output_cache/mod.rs | 10 +- wallet/src/account/output_cache/tests.rs | 428 +++++++++- wallet/types/Cargo.toml | 1 + wallet/types/src/wallet_tx.rs | 15 +- .../wallet-cli-commands/src/helper_types.rs | 1 + wallet/wallet-rpc-daemon/docs/RPC.md | 275 +++++- 24 files changed, 1618 insertions(+), 274 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52991ecab..0b550060a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9105,6 +9105,7 @@ dependencies = [ "serial_test", "serialization", "storage", + "strum 0.26.3", "tempfile", "test-utils", "thiserror 1.0.69", @@ -9449,6 +9450,7 @@ dependencies = [ "serde", "serialization", "storage", + "strum 0.26.3", "test-utils", "thiserror 1.0.69", "tx-verifier", diff --git a/chainstate/src/detail/query.rs b/chainstate/src/detail/query.rs index 9e2b0c10e..a6754b196 100644 --- a/chainstate/src/detail/query.rs +++ b/chainstate/src/detail/query.rs @@ -23,7 +23,7 @@ use chainstate_types::{BlockIndex, GenBlockIndex, Locator, PropertyQueryError}; use common::{ chain::{ block::{signed_block_header::SignedBlockHeader, BlockReward}, - output_value::RpcOutputValue, + output_value::{OutputValue, RpcOutputValue}, tokens::{ NftIssuance, RPCFungibleTokenInfo, RPCIsTokenFrozen, RPCNonFungibleTokenInfo, RPCTokenInfo, TokenAuxiliaryData, TokenId, @@ -458,24 +458,22 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat ) -> Result, PropertyQueryError> { let order_ids = self.get_all_order_ids()?; - // FIXME add separate test with many orders to test various combinations - let orders_info = order_ids .into_iter() .map(|order_id| -> Result<_, PropertyQueryError> { match self.get_order_data(&order_id)? { Some(order_data) => { - let rpc_info = self.order_data_to_rpc_info(&order_id, &order_data)?; let actual_ask_currency = - RpcCurrency::from_rpc_output_value(&rpc_info.initially_asked); + Self::order_currency(&order_id, order_data.ask())?; let actual_give_currency = - RpcCurrency::from_rpc_output_value(&rpc_info.initially_given); + Self::order_currency(&order_id, order_data.give())?; if ask_currency .is_none_or(|ask_currency| ask_currency == &actual_ask_currency) && give_currency .is_none_or(|give_currency| give_currency == &actual_give_currency) { + let rpc_info = self.order_data_to_rpc_info(&order_id, &order_data)?; Ok(Some((order_id, rpc_info))) } else { Ok(None) @@ -494,17 +492,19 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat Ok(orders_info) } + fn order_currency( + order_id: &OrderId, + value: &OutputValue, + ) -> Result { + RpcCurrency::from_output_value(value) + .ok_or(PropertyQueryError::UnsupportedTokenV0InOrder(*order_id)) + } + fn order_data_to_rpc_info( &self, order_id: &OrderId, order_data: &OrderData, ) -> Result { - let order_id_addr = - common::address::Address::new(self.chainstate_ref.chain_config(), *order_id).unwrap(); - logging::log::warn!( - "order_id = {order_id:x}, order_id_addr = {order_id_addr}, order_data = {order_data:?}" - ); // FIXME - // Note: the balances are deleted from the chainstate db once they reach zero. let ask_balance = self.get_order_ask_balance(order_id)?.unwrap_or(Amount::ZERO); let give_balance = self.get_order_give_balance(order_id)?.unwrap_or(Amount::ZERO); diff --git a/chainstate/test-framework/src/helpers.rs b/chainstate/test-framework/src/helpers.rs index b5975e832..96187dcb1 100644 --- a/chainstate/test-framework/src/helpers.rs +++ b/chainstate/test-framework/src/helpers.rs @@ -28,7 +28,7 @@ use common::{ }; use orders_accounting::{OrdersAccountingDB, OrdersAccountingView as _}; use randomness::{CryptoRng, Rng, SliceRandom as _}; -use test_utils::random_ascii_alphanumeric_string; +use test_utils::{random_ascii_alphanumeric_string, token_utils::random_nft_issuance}; use crate::{get_output_value, TestFramework, TransactionBuilder}; @@ -98,12 +98,7 @@ pub fn issue_token_from_block( /*change_outpoint*/ UtxoOutPoint, ) { let token_issuance_fee = tf.chainstate.get_chain_config().fungible_token_issuance_fee(); - - let fee_utxo_coins = - get_output_value(tf.chainstate.utxo(&utxo_to_pay_fee).unwrap().unwrap().output()) - .unwrap() - .coin_amount() - .unwrap(); + let fee_utxo_coins = tf.coin_amount_from_utxo(&utxo_to_pay_fee); let tx = TransactionBuilder::new() .add_input(utxo_to_pay_fee.into(), InputWitness::NoSignature(None)) @@ -241,6 +236,46 @@ pub fn mint_tokens_in_block( (block_id, tx_id) } +pub fn issue_random_nft_from_best_block( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, + utxo_to_pay_fee: UtxoOutPoint, +) -> ( + TokenId, + /*nft*/ UtxoOutPoint, + /*coins change*/ UtxoOutPoint, +) { + let nft_issuance_fee = tf.chainstate.get_chain_config().nft_issuance_fee(BlockHeight::zero()); + let fee_utxo_coins = tf.coin_amount_from_utxo(&utxo_to_pay_fee); + + let nft_tx_first_input = TxInput::Utxo(utxo_to_pay_fee); + let nft_id = TokenId::from_tx_input(&nft_tx_first_input); + let nft_issuance = random_nft_issuance(tf.chain_config().as_ref(), rng); + + let ntf_issuance_tx = TransactionBuilder::new() + .add_input(nft_tx_first_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::IssueNft( + nft_id, + Box::new(nft_issuance.clone().into()), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin((fee_utxo_coins - nft_issuance_fee).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + let nft_issuance_tx_id = ntf_issuance_tx.transaction().get_id(); + let nft_utxo = UtxoOutPoint::new(nft_issuance_tx_id.into(), 0); + let change_utxo = UtxoOutPoint::new(nft_issuance_tx_id.into(), 1); + + tf.make_block_builder() + .add_transaction(ntf_issuance_tx) + .build_and_process(rng) + .unwrap(); + + (nft_id, nft_utxo, change_utxo) +} + /// Given the fill amount in the "ask" currency, return the filled amount in the "give" currency. pub fn calculate_fill_order( tf: &TestFramework, diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index bcc63469c..b76a1f167 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -13,8 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::borrow::Cow; +use std::{borrow::Cow, collections::BTreeMap}; +use chainstate_storage::Transactional as _; +use orders_accounting::OrdersAccountingStorageRead as _; use rstest::rstest; use chainstate::{ @@ -24,14 +26,14 @@ use chainstate::{ use chainstate_test_framework::{ helpers::{ calculate_fill_order, issue_and_mint_random_token_from_best_block, - order_min_non_zero_fill_amount, + issue_random_nft_from_best_block, order_min_non_zero_fill_amount, }, output_value_amount, TestFramework, TransactionBuilder, }; use common::{ address::pubkeyhash::PublicKeyHash, chain::{ - make_order_id, make_token_id, + make_order_id, output_value::{OutputValue, RpcOutputValue}, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, @@ -39,19 +41,16 @@ use common::{ verify_signature, DestinationSigError, EvaluatedInputWitness, }, tokens::{IsTokenFreezable, TokenId, TokenTotalSupply}, - AccountCommand, AccountNonce, ChainstateUpgradeBuilder, Destination, IdCreationError, - OrderAccountCommand, OrderData, OrderId, OrdersVersion, RpcCurrency, RpcOrderInfo, - SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, + AccountCommand, AccountNonce, AccountType, ChainstateUpgradeBuilder, Destination, + IdCreationError, OrderAccountCommand, OrderData, OrderId, OrdersVersion, RpcCurrency, + RpcOrderInfo, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Idable, H256}, }; use crypto::key::{KeyKind, PrivateKey}; use logging::log; use randomness::{CryptoRng, Rng, SliceRandom}; -use test_utils::{ - random::{gen_random_bytes, make_seedable_rng, Seed}, - token_utils::random_nft_issuance, -}; +use test_utils::random::{gen_random_bytes, make_seedable_rng, Seed}; use tx_verifier::{ error::{InputCheckError, InputCheckErrorPayload, ScriptError, TranslationError}, input_check::signature_only_check::verify_tx_signature, @@ -80,7 +79,11 @@ fn create_test_framework_with_orders( fn issue_and_mint_token_from_genesis( rng: &mut (impl Rng + CryptoRng), tf: &mut TestFramework, -) -> (TokenId, UtxoOutPoint, UtxoOutPoint) { +) -> ( + TokenId, + /*tokens*/ UtxoOutPoint, + /*coins change*/ UtxoOutPoint, +) { let to_mint = Amount::from_atoms(rng.gen_range(100..100_000_000)); issue_and_mint_token_amount_from_genesis(rng, tf, to_mint) } @@ -89,14 +92,18 @@ fn issue_and_mint_token_amount_from_genesis( rng: &mut (impl Rng + CryptoRng), tf: &mut TestFramework, to_mint: Amount, -) -> (TokenId, UtxoOutPoint, UtxoOutPoint) { +) -> ( + TokenId, + /*tokens*/ UtxoOutPoint, + /*coins change*/ UtxoOutPoint, +) { let genesis_block_id = tf.genesis().get_id(); - let utxo = UtxoOutPoint::new(genesis_block_id.into(), 0); + let utxo_to_pay_fee = UtxoOutPoint::new(genesis_block_id.into(), 0); issue_and_mint_random_token_from_best_block( rng, tf, - utxo, + utxo_to_pay_fee, to_mint, TokenTotalSupply::Unlimited, IsTokenFreezable::Yes, @@ -106,23 +113,25 @@ fn issue_and_mint_token_amount_from_genesis( fn issue_and_mint_token_amount_from_best_block( rng: &mut (impl Rng + CryptoRng), tf: &mut TestFramework, - utxo_outpoint: UtxoOutPoint, + utxo_to_pay_fee: UtxoOutPoint, to_mint: Amount, -) -> (TokenId, UtxoOutPoint, UtxoOutPoint) { +) -> ( + TokenId, + /*tokens*/ UtxoOutPoint, + /*coins change*/ UtxoOutPoint, +) { issue_and_mint_random_token_from_best_block( rng, tf, - utxo_outpoint, + utxo_to_pay_fee, to_mint, TokenTotalSupply::Unlimited, IsTokenFreezable::Yes, ) } -type InitialOrderData = common::chain::OrderData; - struct ExpectedOrderData { - initial_data: InitialOrderData, + initial_data: common::chain::OrderData, ask_balance: Option, give_balance: Option, nonce: Option, @@ -134,22 +143,37 @@ fn assert_order_exists( rng: &mut (impl Rng + CryptoRng), order_id: &OrderId, expected_data: &ExpectedOrderData, + no_other_orders_present: bool, ) { - let current_data = tf.chainstate.get_order_data(order_id).unwrap().unwrap(); - let (current_data_conclude_key, current_data_ask, current_data_give, current_data_is_frozen) = - current_data.consume(); + let random_order_id = OrderId::random_using(rng); + + let expected_order_data = orders_accounting::OrderData::new_generic( + expected_data.initial_data.conclude_key().clone(), + expected_data.initial_data.ask().clone(), + expected_data.initial_data.give().clone(), + expected_data.is_frozen, + ); + + let actual_order_data = tf.chainstate.get_order_data(order_id).unwrap().unwrap(); + assert_eq!(actual_order_data, expected_order_data); assert_eq!( - ¤t_data_conclude_key, - expected_data.initial_data.conclude_key() + tf.chainstate.get_order_data(&random_order_id).unwrap(), + None ); - assert_eq!(¤t_data_ask, expected_data.initial_data.ask()); - assert_eq!(¤t_data_give, expected_data.initial_data.give()); - assert_eq!(current_data_is_frozen, expected_data.is_frozen); - let current_ask_balance = tf.chainstate.get_order_ask_balance(order_id).unwrap(); - assert_eq!(current_ask_balance, expected_data.ask_balance); - let current_give_balance = tf.chainstate.get_order_give_balance(order_id).unwrap(); - assert_eq!(current_give_balance, expected_data.give_balance); + let actual_ask_balance = tf.chainstate.get_order_ask_balance(order_id).unwrap(); + assert_eq!(actual_ask_balance, expected_data.ask_balance); + assert_eq!( + tf.chainstate.get_order_ask_balance(&random_order_id).unwrap(), + None + ); + + let actual_give_balance = tf.chainstate.get_order_give_balance(order_id).unwrap(); + assert_eq!(actual_give_balance, expected_data.give_balance); + assert_eq!( + tf.chainstate.get_order_give_balance(&random_order_id).unwrap(), + None + ); let expected_info_for_rpc = RpcOrderInfo { conclude_key: expected_data.initial_data.conclude_key().clone(), @@ -165,12 +189,25 @@ fn assert_order_exists( let actual_info_for_rpc = tf.chainstate.get_order_info_for_rpc(order_id).unwrap().unwrap(); assert_eq!(actual_info_for_rpc, expected_info_for_rpc); + assert_eq!( + tf.chainstate.get_order_info_for_rpc(&random_order_id).unwrap(), + None + ); let all_order_ids = tf.chainstate.get_all_order_ids().unwrap(); assert!(all_order_ids.contains(order_id)); - let all_orders = tf.chainstate.get_orders_info_for_rpc_by_currencies(None, None).unwrap(); - assert_eq!(all_orders.get(order_id).unwrap(), &expected_info_for_rpc); + let all_infos_for_rpc = + tf.chainstate.get_orders_info_for_rpc_by_currencies(None, None).unwrap(); + assert_eq!( + all_infos_for_rpc.get(order_id).unwrap(), + &expected_info_for_rpc + ); + + if no_other_orders_present { + assert_eq!(all_order_ids.len(), 1); + assert_eq!(all_infos_for_rpc.len(), 1); + } let ask_currency = RpcCurrency::from_output_value(expected_data.initial_data.ask()).unwrap(); let give_currency = RpcCurrency::from_output_value(expected_data.initial_data.give()).unwrap(); @@ -191,6 +228,10 @@ fn assert_order_exists( orders_rpc_infos.get(order_id).unwrap(), &expected_info_for_rpc ); + + if no_other_orders_present { + assert_eq!(orders_rpc_infos.len(), 1); + } } let mut make_different_currency = |currency, other_currency| { @@ -232,11 +273,70 @@ fn assert_order_exists( assert_eq!(orders_rpc_infos.get(order_id), None); } - let actual_nonce = tf - .chainstate - .get_account_nonce_count(common::chain::AccountType::Order(*order_id)) - .unwrap(); + let actual_nonce = + tf.chainstate.get_account_nonce_count(AccountType::Order(*order_id)).unwrap(); assert_eq!(actual_nonce, expected_data.nonce); + assert_eq!( + tf.chainstate + .get_account_nonce_count(AccountType::Order(random_order_id)) + .unwrap(), + None + ); + + // Check the storage directly + { + let storage_tx = tf.storage.transaction_ro().unwrap(); + + let actual_order_data = storage_tx.get_order_data(order_id).unwrap().unwrap(); + assert_eq!(actual_order_data, expected_order_data); + assert_eq!(storage_tx.get_order_data(&random_order_id).unwrap(), None); + + let actual_ask_balance = storage_tx.get_ask_balance(order_id).unwrap(); + assert_eq!(actual_ask_balance, expected_data.ask_balance); + assert_eq!(storage_tx.get_ask_balance(&random_order_id).unwrap(), None); + + let actual_give_balance = storage_tx.get_give_balance(order_id).unwrap(); + assert_eq!(actual_give_balance, expected_data.give_balance); + assert_eq!(storage_tx.get_give_balance(&random_order_id).unwrap(), None); + + let all_order_ids = storage_tx.get_all_order_ids().unwrap(); + assert!(all_order_ids.contains(order_id)); + + let orders_accounting_data = storage_tx.read_orders_accounting_data().unwrap(); + assert_eq!( + orders_accounting_data.order_data.get(order_id).unwrap(), + &expected_order_data + ); + assert_eq!( + orders_accounting_data.ask_balances.get(order_id), + expected_data.ask_balance.as_ref() + ); + assert_eq!( + orders_accounting_data.give_balances.get(order_id), + expected_data.give_balance.as_ref() + ); + + if no_other_orders_present { + assert_eq!(all_order_ids.len(), 1); + assert_eq!(orders_accounting_data.order_data.len(), 1); + assert_eq!( + orders_accounting_data.ask_balances.len(), + if expected_data.ask_balance.is_some() { + 1 + } else { + 0 + } + ); + assert_eq!( + orders_accounting_data.give_balances.len(), + if expected_data.give_balance.is_some() { + 1 + } else { + 0 + } + ); + } + } } trait OrdersVersionExt { @@ -252,18 +352,20 @@ impl OrdersVersionExt for OrdersVersion { } } -fn assert_order_missing(tf: &TestFramework, order_id: &OrderId) { - let data = tf.chainstate.get_order_data(order_id).unwrap(); - assert_eq!(data, None); +fn assert_order_missing(tf: &TestFramework, order_id: &OrderId, no_other_orders_present: bool) { + assert_eq!(tf.chainstate.get_order_data(order_id).unwrap(), None); - let ask_balance = tf.chainstate.get_order_ask_balance(order_id).unwrap(); - assert_eq!(ask_balance, None); + assert_eq!(tf.chainstate.get_order_ask_balance(order_id).unwrap(), None); - let give_balance = tf.chainstate.get_order_give_balance(order_id).unwrap(); - assert_eq!(give_balance, None); + assert_eq!( + tf.chainstate.get_order_give_balance(order_id).unwrap(), + None + ); - let info_for_rpc = tf.chainstate.get_order_info_for_rpc(order_id).unwrap(); - assert_eq!(info_for_rpc, None); + assert_eq!( + tf.chainstate.get_order_info_for_rpc(order_id).unwrap(), + None + ); let all_infos_for_rpc = tf.chainstate.get_orders_info_for_rpc_by_currencies(None, None).unwrap(); @@ -271,6 +373,35 @@ fn assert_order_missing(tf: &TestFramework, order_id: &OrderId) { let all_order_ids = tf.chainstate.get_all_order_ids().unwrap(); assert!(!all_order_ids.contains(order_id)); + + if no_other_orders_present { + assert_eq!(all_infos_for_rpc.len(), 0); + assert_eq!(all_order_ids.len(), 0); + } + + // Check the storage directly + { + let storage_tx = tf.storage.transaction_ro().unwrap(); + + assert_eq!(storage_tx.get_order_data(&order_id).unwrap(), None); + assert_eq!(storage_tx.get_ask_balance(&order_id).unwrap(), None); + assert_eq!(storage_tx.get_give_balance(&order_id).unwrap(), None); + + let all_order_ids = storage_tx.get_all_order_ids().unwrap(); + assert!(!all_order_ids.contains(order_id)); + + let orders_accounting_data = storage_tx.read_orders_accounting_data().unwrap(); + assert_eq!(orders_accounting_data.order_data.get(order_id), None); + assert_eq!(orders_accounting_data.ask_balances.get(order_id), None); + assert_eq!(orders_accounting_data.give_balances.get(order_id), None); + + if no_other_orders_present { + assert_eq!(all_order_ids.len(), 0); + assert_eq!(orders_accounting_data.order_data.len(), 0); + assert_eq!(orders_accounting_data.ask_balances.len(), 0); + assert_eq!(orders_accounting_data.give_balances.len(), 0); + } + } } #[rstest] @@ -312,6 +443,7 @@ fn create_order_check_storage(#[case] seed: Seed) { nonce: None, is_frozen: false, }, + true, ); }); } @@ -683,7 +815,7 @@ fn conclude_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersi .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_order_missing(&tf, &order_id); + assert_order_missing(&tf, &order_id, true); }); } @@ -860,6 +992,7 @@ fn fill_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersion) nonce: version.v0_then_some(AccountNonce::new(0)), is_frozen: false, }, + true, ); // Note: even though zero fills are allowed in orders V0 in general, we can't do a zero @@ -919,6 +1052,7 @@ fn fill_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersion) nonce: version.v0_then_some(AccountNonce::new(1)), is_frozen: false, }, + true, ); } }); @@ -1081,7 +1215,7 @@ fn fill_then_conclude(#[case] seed: Seed, #[case] version: OrdersVersion) { .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_order_missing(&tf, &order_id); + assert_order_missing(&tf, &order_id, true); { // Try filling concluded order @@ -1444,7 +1578,7 @@ fn fill_completely_then_conclude(#[case] seed: Seed, #[case] version: OrdersVers .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_order_missing(&tf, &order_id); + assert_order_missing(&tf, &order_id, true); }); } @@ -1661,14 +1795,14 @@ fn reorg_before_create(#[case] seed: Seed, #[case] version: OrdersVersion) { is_frozen: false, }; - assert_order_exists(&tf, &mut rng, &order_id, &expected_order_data); + assert_order_exists(&tf, &mut rng, &order_id, &expected_order_data, true); // Create alternative chain and trigger the reorg let new_best_block = tf.create_chain_with_empty_blocks(&reorg_common_ancestor, 3, &mut rng).unwrap(); assert_eq!(tf.best_block_id(), new_best_block); - assert_order_missing(&tf, &order_id); + assert_order_missing(&tf, &order_id, true); // Reapply txs again tf.make_block_builder() @@ -1676,7 +1810,7 @@ fn reorg_before_create(#[case] seed: Seed, #[case] version: OrdersVersion) { .build_and_process(&mut rng) .unwrap(); - assert_order_exists(&tf, &mut rng, &order_id, &expected_order_data); + assert_order_exists(&tf, &mut rng, &order_id, &expected_order_data, true); }); } @@ -1770,7 +1904,7 @@ fn reorg_after_create(#[case] seed: Seed, #[case] version: OrdersVersion) { .build_and_process(&mut rng) .unwrap(); - assert_order_missing(&tf, &order_id); + assert_order_missing(&tf, &order_id, true); // Create alternative chain and trigger the reorg let new_best_block = @@ -1788,6 +1922,7 @@ fn reorg_after_create(#[case] seed: Seed, #[case] version: OrdersVersion) { nonce: None, is_frozen: false, }, + true, ); // Reapply txs again @@ -1796,7 +1931,7 @@ fn reorg_after_create(#[case] seed: Seed, #[case] version: OrdersVersion) { .build_and_process(&mut rng) .unwrap(); - assert_order_missing(&tf, &order_id); + assert_order_missing(&tf, &order_id, true); }); } @@ -1895,40 +2030,14 @@ fn create_order_with_nft(#[case] seed: Seed, #[case] version: OrdersVersion) { let mut rng = make_seedable_rng(seed); let mut tf = create_test_framework_with_orders(&mut rng, version); - let genesis_input = TxInput::from_utxo(tf.genesis().get_id().into(), 0); - let token_id = make_token_id( - tf.chain_config(), - tf.next_block_height(), - std::slice::from_ref(&genesis_input), - ) - .unwrap(); - let nft_issuance = random_nft_issuance(tf.chain_config(), &mut rng); - let token_min_issuance_fee = - tf.chainstate.get_chain_config().nft_issuance_fee(BlockHeight::zero()); - - let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let genesis_utxo = UtxoOutPoint::new(tf.genesis().get_id().into(), 0); // Issue an NFT - let issue_nft_tx = TransactionBuilder::new() - .add_input(genesis_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::IssueNft( - token_id, - Box::new(nft_issuance.into()), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::Transfer( - OutputValue::Coin(ask_amount), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) - .build(); - let issue_nft_tx_id = issue_nft_tx.transaction().get_id(); - tf.make_block_builder() - .add_transaction(issue_nft_tx) - .build_and_process(&mut rng) - .unwrap(); + let (token_id, nft_outpoint, coins_outpoint) = + issue_random_nft_from_best_block(&mut rng, &mut tf, genesis_utxo); // Create order selling NFT for coins + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); let give_amount = Amount::from_atoms(1); let order_data = OrderData::new( Destination::AnyoneCanSpend, @@ -1936,8 +2045,6 @@ fn create_order_with_nft(#[case] seed: Seed, #[case] version: OrdersVersion) { OutputValue::TokenV1(token_id, give_amount), ); - let nft_outpoint = UtxoOutPoint::new(issue_nft_tx_id.into(), 0); - let tx = TransactionBuilder::new() .add_input(nft_outpoint.into(), InputWitness::NoSignature(None)) .add_output(TxOutput::CreateOrder(Box::new(order_data.clone()))) @@ -1956,6 +2063,7 @@ fn create_order_with_nft(#[case] seed: Seed, #[case] version: OrdersVersion) { nonce: None, is_frozen: false, }, + true, ); // Try get 2 nfts out of order @@ -1971,7 +2079,7 @@ fn create_order_with_nft(#[case] seed: Seed, #[case] version: OrdersVersion) { }; let tx = TransactionBuilder::new() .add_input( - TxInput::from_utxo(issue_nft_tx_id.into(), 1), + TxInput::Utxo(coins_outpoint.clone()), InputWitness::NoSignature(None), ) .add_input(fill_input, InputWitness::NoSignature(None)) @@ -2008,7 +2116,7 @@ fn create_order_with_nft(#[case] seed: Seed, #[case] version: OrdersVersion) { .add_transaction( TransactionBuilder::new() .add_input( - TxInput::from_utxo(issue_nft_tx_id.into(), 1), + TxInput::Utxo(coins_outpoint), InputWitness::NoSignature(None), ) .add_input(fill_input, InputWitness::NoSignature(None)) @@ -2032,6 +2140,7 @@ fn create_order_with_nft(#[case] seed: Seed, #[case] version: OrdersVersion) { nonce: version.v0_then_some(AccountNonce::new(0)), is_frozen: false, }, + true, ); }); } @@ -2058,40 +2167,14 @@ fn partially_fill_order_with_nft_v0(#[case] seed: Seed) { ) .build(); - let genesis_input = TxInput::from_utxo(tf.genesis().get_id().into(), 0); - let token_id = make_token_id( - tf.chain_config(), - tf.next_block_height(), - std::slice::from_ref(&genesis_input), - ) - .unwrap(); - let nft_issuance = random_nft_issuance(tf.chain_config(), &mut rng); - let token_min_issuance_fee = - tf.chainstate.get_chain_config().nft_issuance_fee(BlockHeight::zero()); - - let ask_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); + let genesis_utxo = UtxoOutPoint::new(tf.genesis().get_id().into(), 0); // Issue an NFT - let issue_nft_tx = TransactionBuilder::new() - .add_input(genesis_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::IssueNft( - token_id, - Box::new(nft_issuance.into()), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::Transfer( - OutputValue::Coin(ask_amount), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) - .build(); - let issue_nft_tx_id = issue_nft_tx.transaction().get_id(); - tf.make_block_builder() - .add_transaction(issue_nft_tx) - .build_and_process(&mut rng) - .unwrap(); + let (token_id, nft_outpoint, coins_outpoint) = + issue_random_nft_from_best_block(&mut rng, &mut tf, genesis_utxo); // Create order selling NFT for coins + let ask_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); let give_amount = Amount::from_atoms(1); let order_data = OrderData::new( Destination::AnyoneCanSpend, @@ -2099,7 +2182,6 @@ fn partially_fill_order_with_nft_v0(#[case] seed: Seed) { OutputValue::TokenV1(token_id, give_amount), ); - let nft_outpoint = UtxoOutPoint::new(issue_nft_tx_id.into(), 0); let tx = TransactionBuilder::new() .add_input(nft_outpoint.into(), InputWitness::NoSignature(None)) .add_output(TxOutput::CreateOrder(Box::new(order_data.clone()))) @@ -2118,13 +2200,14 @@ fn partially_fill_order_with_nft_v0(#[case] seed: Seed) { nonce: None, is_frozen: false, }, + true, ); // Try get an nft out of order with 1 atom less { let tx = TransactionBuilder::new() .add_input( - TxInput::from_utxo(issue_nft_tx_id.into(), 1), + TxInput::Utxo(coins_outpoint.clone()), InputWitness::NoSignature(None), ) .add_input( @@ -2160,7 +2243,7 @@ fn partially_fill_order_with_nft_v0(#[case] seed: Seed) { // Fill order with 1 atom less, getting 0 nfts let partially_fill_tx = TransactionBuilder::new() .add_input( - TxInput::from_utxo(issue_nft_tx_id.into(), 1), + TxInput::Utxo(coins_outpoint), InputWitness::NoSignature(None), ) .add_input( @@ -2200,6 +2283,7 @@ fn partially_fill_order_with_nft_v0(#[case] seed: Seed) { nonce: Some(AccountNonce::new(0)), is_frozen: false, }, + true, ); // Fill order only with proper amount spent @@ -2241,6 +2325,7 @@ fn partially_fill_order_with_nft_v0(#[case] seed: Seed) { nonce: Some(AccountNonce::new(1)), is_frozen: false, }, + true, ); }); } @@ -2267,40 +2352,14 @@ fn partially_fill_order_with_nft_v1(#[case] seed: Seed) { ) .build(); - let genesis_input = TxInput::from_utxo(tf.genesis().get_id().into(), 0); - let token_id = make_token_id( - tf.chain_config(), - tf.next_block_height(), - std::slice::from_ref(&genesis_input), - ) - .unwrap(); - let nft_issuance = random_nft_issuance(tf.chain_config(), &mut rng); - let token_min_issuance_fee = - tf.chainstate.get_chain_config().nft_issuance_fee(BlockHeight::zero()); - - let ask_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); + let genesis_utxo = UtxoOutPoint::new(tf.genesis().get_id().into(), 0); // Issue an NFT - let issue_nft_tx = TransactionBuilder::new() - .add_input(genesis_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::IssueNft( - token_id, - Box::new(nft_issuance.into()), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::Transfer( - OutputValue::Coin(ask_amount), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) - .build(); - let issue_nft_tx_id = issue_nft_tx.transaction().get_id(); - tf.make_block_builder() - .add_transaction(issue_nft_tx) - .build_and_process(&mut rng) - .unwrap(); + let (token_id, nft_outpoint, coins_outpoint) = + issue_random_nft_from_best_block(&mut rng, &mut tf, genesis_utxo); // Create order selling NFT for coins + let ask_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); let give_amount = Amount::from_atoms(1); let order_data = OrderData::new( Destination::AnyoneCanSpend, @@ -2308,7 +2367,6 @@ fn partially_fill_order_with_nft_v1(#[case] seed: Seed) { OutputValue::TokenV1(token_id, give_amount), ); - let nft_outpoint = UtxoOutPoint::new(issue_nft_tx_id.into(), 0); let tx = TransactionBuilder::new() .add_input(nft_outpoint.into(), InputWitness::NoSignature(None)) .add_output(TxOutput::CreateOrder(Box::new(order_data.clone()))) @@ -2327,6 +2385,7 @@ fn partially_fill_order_with_nft_v1(#[case] seed: Seed) { nonce: None, is_frozen: false, }, + true, ); // Try to get nft by filling order with 1 atom less, getting 0 nfts @@ -2334,7 +2393,7 @@ fn partially_fill_order_with_nft_v1(#[case] seed: Seed) { let underbid_amount = (ask_amount - Amount::from_atoms(1)).unwrap(); let tx = TransactionBuilder::new() .add_input( - TxInput::from_utxo(issue_nft_tx_id.into(), 1), + TxInput::Utxo(coins_outpoint.clone()), InputWitness::NoSignature(None), ) .add_input( @@ -2371,7 +2430,7 @@ fn partially_fill_order_with_nft_v1(#[case] seed: Seed) { .add_transaction( TransactionBuilder::new() .add_input( - TxInput::from_utxo(issue_nft_tx_id.into(), 1), + TxInput::Utxo(coins_outpoint), InputWitness::NoSignature(None), ) .add_input( @@ -2400,6 +2459,7 @@ fn partially_fill_order_with_nft_v1(#[case] seed: Seed) { nonce: None, is_frozen: false, }, + true, ); }); } @@ -2465,6 +2525,7 @@ fn fill_order_with_zero(#[case] seed: Seed, #[case] version: OrdersVersion) { nonce: Some(AccountNonce::new(0)), is_frozen: false, }, + true, ); } @@ -2567,6 +2628,7 @@ fn fill_order_underbid(#[case] seed: Seed, #[case] version: OrdersVersion) { nonce: Some(AccountNonce::new(0)), is_frozen: false, }, + true, ); } OrdersVersion::V1 => { @@ -2668,6 +2730,7 @@ fn fill_orders_shuffle(#[case] seed: Seed, #[case] fills: Vec) { nonce: None, is_frozen: false, }, + true, ); }); } @@ -2989,7 +3052,7 @@ fn create_order_fill_activate_fork_fill_conclude(#[case] seed: Seed) { .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - assert_order_missing(&tf, &order_id); + assert_order_missing(&tf, &order_id, true); }); } @@ -3112,6 +3175,7 @@ fn freeze_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersion nonce: None, is_frozen: true, }, + true, ); } } @@ -3704,6 +3768,7 @@ fn fill_order_v0_destination_irrelevancy(#[case] seed: Seed) { nonce: Some(AccountNonce::new(2)), is_frozen: false, }, + true, ); }); } @@ -3860,6 +3925,7 @@ fn fill_order_v1_must_not_be_signed(#[case] seed: Seed) { nonce: None, is_frozen: false, }, + true, ); }); } @@ -4023,6 +4089,7 @@ fn fill_order_twice_in_same_block( nonce: version.v0_then_some(AccountNonce::new(1)), is_frozen: false, }, + true, ); }); } @@ -4164,6 +4231,7 @@ fn conclude_and_recreate_in_same_tx_with_same_balances( nonce: None, is_frozen: false, }, + true, ); let new_order_id = { @@ -4199,10 +4267,8 @@ fn conclude_and_recreate_in_same_tx_with_same_balances( order_id }; - assert_order_missing(&tf, &orig_order_id); - // The original order is no longer there - assert_order_missing(&tf, &orig_order_id); + assert_order_missing(&tf, &orig_order_id, false); // The new order exists and has the same balances as the original one before the conclusion. assert_order_exists( @@ -4216,6 +4282,7 @@ fn conclude_and_recreate_in_same_tx_with_same_balances( nonce: None, is_frozen: false, }, + true, ); }); } @@ -4362,7 +4429,7 @@ fn conclude_and_recreate_in_same_tx_with_different_balances( }; // The original order is no longer there - assert_order_missing(&tf, &orig_order_id); + assert_order_missing(&tf, &orig_order_id, false); // The new order exists with the correct balances. assert_order_exists( @@ -4376,6 +4443,417 @@ fn conclude_and_recreate_in_same_tx_with_different_balances( nonce: None, is_frozen: false, }, + true, + ); + }); +} + +// Test get_orders_info_for_rpc_by_currencies when multiple orders are available. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn get_orders_info_for_rpc_by_currencies_test(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + // Create 2 fungible tokens and 2 NFTs + + let token1_mint_amount = Amount::from_atoms(rng.gen_range(1000..100_000)); + let (token1_id, token1_utxo, coins_utxo) = + issue_and_mint_token_amount_from_genesis(&mut rng, &mut tf, token1_mint_amount); + + let token2_mint_amount = Amount::from_atoms(rng.gen_range(1000..100_000)); + let (token2_id, _, coins_utxo) = issue_and_mint_token_amount_from_best_block( + &mut rng, + &mut tf, + coins_utxo, + token2_mint_amount, ); + + let (nft1_id, nft1_utxo, coins_utxo) = + issue_random_nft_from_best_block(&mut rng, &mut tf, coins_utxo); + + let (nft2_id, nft2_utxo, coins_utxo) = + issue_random_nft_from_best_block(&mut rng, &mut tf, coins_utxo); + + let coins_change_amount = tf.coin_amount_from_utxo(&coins_utxo); + + // Now create the orders; we won't be doing anything with them in this test, + // so all non-initial data will have the default values. + + fn make_expected_order_info(initial_data: &OrderData) -> RpcOrderInfo { + RpcOrderInfo { + conclude_key: initial_data.conclude_key().clone(), + initially_asked: RpcOutputValue::from_output_value(initial_data.ask()).unwrap(), + initially_given: RpcOutputValue::from_output_value(initial_data.give()).unwrap(), + ask_balance: initial_data.ask().amount(), + give_balance: initial_data.give().amount(), + nonce: None, + is_frozen: false, + } + } + + // Create order 1, which gives token1 for coins + + let order1_coin_ask_amount = Amount::from_atoms(rng.gen_range(100..200)); + let order1_token1_give_amount = Amount::from_atoms(rng.gen_range(100..200)); + + let token1_change_amount = (token1_mint_amount - order1_token1_give_amount).unwrap(); + let order1_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(order1_coin_ask_amount), + OutputValue::TokenV1(token1_id, order1_token1_give_amount), + ); + let order1_expected_info = make_expected_order_info(&order1_data); + let order1_tx = TransactionBuilder::new() + .add_input(token1_utxo.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order1_data))) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token1_id, token1_change_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let token1_utxo = UtxoOutPoint::new(order1_tx.transaction().get_id().into(), 1); + let order1_id = make_order_id(order1_tx.inputs()).unwrap(); + tf.make_block_builder() + .add_transaction(order1_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Create order 2, which gives NFT1 for coins + + let order2_coin_ask_amount = Amount::from_atoms(rng.gen_range(100..200)); + + let order2_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(order2_coin_ask_amount), + OutputValue::TokenV1(nft1_id, Amount::from_atoms(1)), + ); + let order2_expected_info = make_expected_order_info(&order2_data); + let order2_tx = TransactionBuilder::new() + .add_input(nft1_utxo.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order2_data))) + .build(); + let order2_id = make_order_id(order2_tx.inputs()).unwrap(); + tf.make_block_builder() + .add_transaction(order2_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Create order 3, which gives NFT2 for token2 + + let order3_token2_ask_amount = Amount::from_atoms(rng.gen_range(100..200)); + + let order3_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token2_id, order3_token2_ask_amount), + OutputValue::TokenV1(nft2_id, Amount::from_atoms(1)), + ); + let order3_expected_info = make_expected_order_info(&order3_data); + let order3_tx = TransactionBuilder::new() + .add_input(nft2_utxo.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order3_data))) + .build(); + let order3_id = make_order_id(order3_tx.inputs()).unwrap(); + tf.make_block_builder() + .add_transaction(order3_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Create order 4, which gives token1 for NFT1 + + let order4_token1_give_amount = Amount::from_atoms(rng.gen_range(100..200)); + + let token1_change_amount = (token1_change_amount - order4_token1_give_amount).unwrap(); + let order4_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(nft1_id, Amount::from_atoms(1)), + OutputValue::TokenV1(token1_id, order4_token1_give_amount), + ); + let order4_expected_info = make_expected_order_info(&order4_data); + let order4_tx = TransactionBuilder::new() + .add_input(token1_utxo.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order4_data))) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token1_id, token1_change_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let token1_utxo = UtxoOutPoint::new(order4_tx.transaction().get_id().into(), 1); + let order4_id = make_order_id(order4_tx.inputs()).unwrap(); + tf.make_block_builder() + .add_transaction(order4_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Create order 5, which gives coins for NFT2 + + let order5_coins_give_amount = Amount::from_atoms(rng.gen_range(100..200)); + let coins_change_amount = (coins_change_amount - order5_coins_give_amount).unwrap(); + + let order5_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(nft2_id, Amount::from_atoms(1)), + OutputValue::Coin(order5_coins_give_amount), + ); + let order5_expected_info = make_expected_order_info(&order5_data); + let order5_tx = TransactionBuilder::new() + .add_input(coins_utxo.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order5_data))) + .add_output(TxOutput::Transfer( + OutputValue::Coin(coins_change_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let coins_utxo = UtxoOutPoint::new(order5_tx.transaction().get_id().into(), 1); + let order5_id = make_order_id(order5_tx.inputs()).unwrap(); + tf.make_block_builder() + .add_transaction(order5_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Create order 6, which gives token1 for token2 + + let order6_token2_ask_amount = Amount::from_atoms(rng.gen_range(100..200)); + let order6_token1_give_amount = Amount::from_atoms(rng.gen_range(100..200)); + + let token1_change_amount = (token1_change_amount - order6_token1_give_amount).unwrap(); + let order6_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token2_id, order6_token2_ask_amount), + OutputValue::TokenV1(token1_id, order6_token1_give_amount), + ); + let order6_expected_info = make_expected_order_info(&order6_data); + let order6_tx = TransactionBuilder::new() + .add_input(token1_utxo.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order6_data))) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token1_id, token1_change_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let order6_id = make_order_id(order6_tx.inputs()).unwrap(); + tf.make_block_builder() + .add_transaction(order6_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Create order 7, which gives coins for token2 + + let order7_token2_ask_amount = Amount::from_atoms(rng.gen_range(100..200)); + let order7_coins_give_amount = Amount::from_atoms(rng.gen_range(100..200)); + + let coins_change_amount = (coins_change_amount - order7_coins_give_amount).unwrap(); + let order7_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token2_id, order7_token2_ask_amount), + OutputValue::Coin(order7_coins_give_amount), + ); + let order7_expected_info = make_expected_order_info(&order7_data); + let order7_tx = TransactionBuilder::new() + .add_input(coins_utxo.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order7_data))) + .add_output(TxOutput::Transfer( + OutputValue::Coin(coins_change_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let order7_id = make_order_id(order7_tx.inputs()).unwrap(); + tf.make_block_builder() + .add_transaction(order7_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Now we have: + // 2 orders that ask for coins - orders 1 and 2; + // 3 orders that ask for token2 - orders 3, 6 and 7; + // 1 order that asks for NFT1 - order 4; + // 1 order that asks for NFT2 - order 5; + // 2 orders that give coins - orders 5 and 7; + // 3 orders that give token1 - orders 1, 4 and 6; + // 1 order that gives NFT1 - order 2; + // 1 order that gives NFT2 - order 3; + + let check_ask = |ask_currency, expected_infos| { + let actual_infos = tf + .chainstate + .get_orders_info_for_rpc_by_currencies(Some(&ask_currency), None) + .unwrap(); + assert_eq!(actual_infos, expected_infos); + }; + + let check_give = |give_currency, expected_infos| { + let actual_infos = tf + .chainstate + .get_orders_info_for_rpc_by_currencies(None, Some(&give_currency)) + .unwrap(); + assert_eq!(actual_infos, expected_infos); + }; + + let check_both = |ask_currency, give_currency, expected_infos| { + let actual_infos = tf + .chainstate + .get_orders_info_for_rpc_by_currencies(Some(&ask_currency), Some(&give_currency)) + .unwrap(); + assert_eq!(actual_infos, expected_infos); + }; + + // Use unqualified names of the currencies to prevent rustfmt from turning a one-liner check + // into 5 lines (note that having a shorter alias like Curr doesn't always help). + use RpcCurrency::*; + + // Get all orders + { + let expected_infos = BTreeMap::from_iter([ + (order1_id, order1_expected_info.clone()), + (order2_id, order2_expected_info.clone()), + (order3_id, order3_expected_info.clone()), + (order4_id, order4_expected_info.clone()), + (order5_id, order5_expected_info.clone()), + (order6_id, order6_expected_info.clone()), + (order7_id, order7_expected_info.clone()), + ]); + let actual_infos = + tf.chainstate.get_orders_info_for_rpc_by_currencies(None, None).unwrap(); + assert_eq!(actual_infos, expected_infos); + } + + // Check currencies that are never given or asked for + { + // No one asks for token1 + check_ask(Token(token1_id), BTreeMap::new()); + check_both(Token(token1_id), Coin, BTreeMap::new()); + check_both(Token(token1_id), Token(token1_id), BTreeMap::new()); + check_both(Token(token1_id), Token(token2_id), BTreeMap::new()); + check_both(Token(token1_id), Token(nft1_id), BTreeMap::new()); + check_both(Token(token1_id), Token(nft2_id), BTreeMap::new()); + + // No one gives token2 + check_give(Token(token2_id), BTreeMap::new()); + check_both(Coin, Token(token2_id), BTreeMap::new()); + check_both(Token(token1_id), Token(token2_id), BTreeMap::new()); + check_both(Token(token2_id), Token(token2_id), BTreeMap::new()); + check_both(Token(nft1_id), Token(token2_id), BTreeMap::new()); + check_both(Token(nft2_id), Token(token2_id), BTreeMap::new()); + } + + // Get all orders that ask for a specific currency. + { + // Get all orders that ask for coins + let expected_infos = BTreeMap::from_iter([ + (order1_id, order1_expected_info.clone()), + (order2_id, order2_expected_info.clone()), + ]); + check_ask(Coin, expected_infos); + + // Get all orders that ask for token2 + let expected_infos = BTreeMap::from_iter([ + (order3_id, order3_expected_info.clone()), + (order6_id, order6_expected_info.clone()), + (order7_id, order7_expected_info.clone()), + ]); + check_ask(Token(token2_id), expected_infos); + + // Get all orders that ask for NFT1 + let expected_infos = BTreeMap::from_iter([(order4_id, order4_expected_info.clone())]); + check_ask(Token(nft1_id), expected_infos); + + // Get all orders that ask for NFT2 + let expected_infos = BTreeMap::from_iter([(order5_id, order5_expected_info.clone())]); + check_ask(Token(nft2_id), expected_infos); + } + + // Get all orders that give specific currency. + { + // Get all orders that give coins + let expected_infos = BTreeMap::from_iter([ + (order5_id, order5_expected_info.clone()), + (order7_id, order7_expected_info.clone()), + ]); + check_give(Coin, expected_infos); + + // Get all orders that give token1 + let expected_infos = BTreeMap::from_iter([ + (order1_id, order1_expected_info.clone()), + (order4_id, order4_expected_info.clone()), + (order6_id, order6_expected_info.clone()), + ]); + check_give(Token(token1_id), expected_infos); + + // Get all orders that give NFT1 + let expected_infos = BTreeMap::from_iter([(order2_id, order2_expected_info.clone())]); + check_give(Token(nft1_id), expected_infos); + + // Get all orders that give NFT2 + let expected_infos = BTreeMap::from_iter([(order3_id, order3_expected_info.clone())]); + check_give(Token(nft2_id), expected_infos); + } + + // Get all orders with specific give/ask currencies. + { + // Asking for coins + + // Get all orders that ask for coins and give coins + check_both(Coin, Coin, BTreeMap::new()); + + // Get all orders that ask for coins and give token1 + let expected_infos = BTreeMap::from_iter([(order1_id, order1_expected_info.clone())]); + check_both(Coin, Token(token1_id), expected_infos); + + // Get all orders that ask for coins and give NFT1 + let expected_infos = BTreeMap::from_iter([(order2_id, order2_expected_info.clone())]); + check_both(Coin, Token(nft1_id), expected_infos); + + // Get all orders that ask for coins and give NFT2 + check_both(Coin, Token(nft2_id), BTreeMap::new()); + + // Asking for token2 + + // Get all orders that ask for token2 and give coins + let expected_infos = BTreeMap::from_iter([(order7_id, order7_expected_info.clone())]); + check_both(Token(token2_id), Coin, expected_infos); + + // Get all orders that ask for token2 and give token1 + let expected_infos = BTreeMap::from_iter([(order6_id, order6_expected_info.clone())]); + check_both(Token(token2_id), Token(token1_id), expected_infos); + + // Get all orders that ask for token2 and give NFT1 + check_both(Token(token2_id), Token(nft1_id), BTreeMap::new()); + + // Get all orders that ask for token2 and give NFT2 + let expected_infos = BTreeMap::from_iter([(order3_id, order3_expected_info.clone())]); + check_both(Token(token2_id), Token(nft2_id), expected_infos); + + // Asking for NFT1 + + // Get all orders that ask for NFT1 and give coins + check_both(Token(nft1_id), Coin, BTreeMap::new()); + + // Get all orders that ask for NFT1 and give token1 + let expected_infos = BTreeMap::from_iter([(order4_id, order4_expected_info.clone())]); + check_both(Token(nft1_id), Token(token1_id), expected_infos); + + // Get all orders that ask for NFT1 and give NFT1 + check_both(Token(nft1_id), Token(nft1_id), BTreeMap::new()); + + // Get all orders that ask for NFT1 and give NFT2 + check_both(Token(nft1_id), Token(nft2_id), BTreeMap::new()); + + // Asking for NFT2 + + // Get all orders that ask for NFT2 and give coins + let expected_infos = BTreeMap::from_iter([(order5_id, order5_expected_info.clone())]); + check_both(Token(nft2_id), Coin, expected_infos); + + // Get all orders that ask for NFT2 and give token1 + check_both(Token(nft2_id), Token(token1_id), BTreeMap::new()); + + // Get all orders that ask for NFT2 and give NFT1 + check_both(Token(nft2_id), Token(nft1_id), BTreeMap::new()); + + // Get all orders that ask for NFT2 and give NFT2 + check_both(Token(nft2_id), Token(nft2_id), BTreeMap::new()); + } }); } diff --git a/chainstate/test-suite/src/tests/tokens_misc_tests.rs b/chainstate/test-suite/src/tests/tokens_misc_tests.rs index 1cc6e587d..48bdfd4f5 100644 --- a/chainstate/test-suite/src/tests/tokens_misc_tests.rs +++ b/chainstate/test-suite/src/tests/tokens_misc_tests.rs @@ -21,7 +21,6 @@ use rstest::rstest; use chainstate::{ChainstateError, PropertyQueryError}; use chainstate_test_framework::{ - get_output_value, helpers::{issue_token_from_block, issue_token_from_genesis, make_token_issuance}, TestFramework, TransactionBuilder, }; @@ -96,11 +95,7 @@ fn get_tokens_info_for_rpc_test(#[case] seed: Seed) { let nft_issuance_fee = tf.chainstate.get_chain_config().nft_issuance_fee(BlockHeight::zero()); - let change_amount = - get_output_value(tf.chainstate.utxo(&utxo_with_change).unwrap().unwrap().output()) - .unwrap() - .coin_amount() - .unwrap(); + let change_amount = tf.coin_amount_from_utxo(&utxo_with_change); let nft_tx1_first_input = TxInput::Utxo(utxo_with_change); let nft1_id = TokenId::from_tx_input(&nft_tx1_first_input); @@ -120,7 +115,7 @@ fn get_tokens_info_for_rpc_test(#[case] seed: Seed) { )) .build(); let nft1_issuance_tx_id = ntf1_issuance_tx.transaction().get_id(); - let utxo_with_change = UtxoOutPoint::new(ntf1_issuance_tx.transaction().get_id().into(), 1); + let utxo_with_change = UtxoOutPoint::new(nft1_issuance_tx_id.into(), 1); let change_amount = next_change_amount; let nft1_issuance_block_id = *tf diff --git a/common/src/chain/order/rpc.rs b/common/src/chain/order/rpc.rs index 25dd943d8..b041b8b09 100644 --- a/common/src/chain/order/rpc.rs +++ b/common/src/chain/order/rpc.rs @@ -30,10 +30,11 @@ pub struct RpcOrderInfo { pub initially_asked: RpcOutputValue, pub initially_given: RpcOutputValue, - // left to offer - pub give_balance: Amount, - // how much more is expected to get in return + // The remaining amount of the "ask" currency that the order is expected to receive in + // exchange for the remaining amount of the "give" currency. pub ask_balance: Amount, + // The remaining amount of the "give" currency. + pub give_balance: Amount, pub nonce: Option, diff --git a/common/src/primitives/amount/decimal.rs b/common/src/primitives/amount/decimal.rs index 91d7fde94..19a2ce2fd 100644 --- a/common/src/primitives/amount/decimal.rs +++ b/common/src/primitives/amount/decimal.rs @@ -100,7 +100,6 @@ impl DecimalAmount { } } -// FIXME tests, including one for with_decimals calls /// Subtract two DecimalAmount's. /// /// This function is intended to be used with amounts that represent the same currency (i.e. when @@ -116,7 +115,8 @@ pub fn subtract_decimal_amounts_of_same_currency( // Remove extra zeroes, in case one of the amounts' decimals were artificially increased via `with_decimals` // (in which case the later call "with_decimals(max_decimals)") may fail even if both amounts refer to the same // currency). - let lhs = lhs.without_padding(); + // Note that since this function can only succeed if lhs is bigger than or equal to rhs, + // any padding that lhs has should be ok for rhs too. I.e. removing padding for lhs is redundant. let rhs = rhs.without_padding(); let max_decimals = std::cmp::max(lhs.decimals(), rhs.decimals()); @@ -376,4 +376,59 @@ mod test { let amount = dec_amount2.to_amount(4); assert_eq!(amount, Some(Amount::from_atoms(1234500))); } + + #[test] + fn subtract_same_currency() { + let res = subtract_decimal_amounts_of_same_currency( + &DecimalAmount::from_uint_decimal(12345, 3), + &DecimalAmount::from_uint_decimal(1234, 3), + ) + .unwrap(); + assert!(res.is_same(&DecimalAmount::from_uint_decimal(11111, 3))); + + let res = subtract_decimal_amounts_of_same_currency( + &DecimalAmount::from_uint_decimal(12345, 3), + &DecimalAmount::from_uint_decimal(123400, 5), + ) + .unwrap(); + assert!(res.is_same(&DecimalAmount::from_uint_decimal(11111, 3))); + + let res = subtract_decimal_amounts_of_same_currency( + &DecimalAmount::from_uint_decimal(1234500, 5), + &DecimalAmount::from_uint_decimal(1234, 3), + ) + .unwrap(); + assert!(res.is_same(&DecimalAmount::from_uint_decimal(11111, 3))); + + let res = subtract_decimal_amounts_of_same_currency( + &DecimalAmount::from_uint_decimal(12345, 3), + &DecimalAmount::from_uint_decimal(2345, 3), + ) + .unwrap(); + assert!(res.is_same(&DecimalAmount::from_uint_decimal(10, 0))); + + let res = subtract_decimal_amounts_of_same_currency( + &DecimalAmount::from_uint_decimal(12345, 3), + &DecimalAmount::from_uint_decimal(234500, 5), + ) + .unwrap(); + assert!(res.is_same(&DecimalAmount::from_uint_decimal(10, 0))); + + let res = subtract_decimal_amounts_of_same_currency( + &DecimalAmount::from_uint_decimal(1234500, 5), + &DecimalAmount::from_uint_decimal(2345, 3), + ) + .unwrap(); + assert!(res.is_same(&DecimalAmount::from_uint_decimal(10, 0))); + + // The difference between the decimals is 35 and 12345*10^35 won't fit into u128 + // (but 2345*10^35 still fits into u128, so we can construct it). + assert!(DecimalAmount::from_uint_decimal(12345, 3).with_decimals(38).is_none()); // sanity check + let res = subtract_decimal_amounts_of_same_currency( + &DecimalAmount::from_uint_decimal(12345, 3), + &DecimalAmount::from_uint_decimal(2345, 3).with_decimals(38).unwrap(), + ) + .unwrap(); + assert!(res.is_same(&DecimalAmount::from_uint_decimal(10, 0))); + } } diff --git a/node-daemon/docs/RPC.md b/node-daemon/docs/RPC.md index d3292de64..d4a427157 100644 --- a/node-daemon/docs/RPC.md +++ b/node-daemon/docs/RPC.md @@ -645,15 +645,79 @@ EITHER OF "amount": { "atoms": number string }, }, }, - "give_balance": { "atoms": number string }, "ask_balance": { "atoms": number string }, + "give_balance": { "atoms": number string }, "nonce": EITHER OF 1) number 2) null, + "is_frozen": bool, } 2) null ``` +### Method `chainstate_orders_info_by_currencies` + +Return infos for all orders that match the given currencies. Passing None for a currency +means "any currency". + + +Parameters: +``` +{ + "ask_currency": EITHER OF + 1) { "type": "Coin" } + 2) { + "type": "Token", + "content": hex string, + } + 3) null, + "give_currency": EITHER OF + 1) { "type": "Coin" } + 2) { + "type": "Token", + "content": hex string, + } + 3) null, +} +``` + +Returns: +``` +{ hex string: { + "conclude_key": bech32 string, + "initially_asked": EITHER OF + 1) { + "type": "Coin", + "content": { "amount": { "atoms": number string } }, + } + 2) { + "type": "Token", + "content": { + "id": hex string, + "amount": { "atoms": number string }, + }, + }, + "initially_given": EITHER OF + 1) { + "type": "Coin", + "content": { "amount": { "atoms": number string } }, + } + 2) { + "type": "Token", + "content": { + "id": hex string, + "amount": { "atoms": number string }, + }, + }, + "ask_balance": { "atoms": number string }, + "give_balance": { "atoms": number string }, + "nonce": EITHER OF + 1) number + 2) null, + "is_frozen": bool, +}, .. } +``` + ### Method `chainstate_export_bootstrap_file` Exports a "bootstrap file", which contains all blocks diff --git a/orders-accounting/src/data.rs b/orders-accounting/src/data.rs index d07ba8d12..21a91b0c2 100644 --- a/orders-accounting/src/data.rs +++ b/orders-accounting/src/data.rs @@ -40,6 +40,20 @@ impl OrderData { } } + pub fn new_generic( + conclude_key: Destination, + ask: OutputValue, + give: OutputValue, + is_frozen: bool, + ) -> Self { + Self { + conclude_key, + ask, + give, + is_frozen, + } + } + pub fn conclude_key(&self) -> &Destination { &self.conclude_key } @@ -68,10 +82,6 @@ impl OrderData { }) } } - - pub fn consume(self) -> (Destination, OutputValue, OutputValue, bool) { - (self.conclude_key, self.ask, self.give, self.is_frozen) - } } impl From for OrderData { diff --git a/p2p/src/peer_manager/tests/connections.rs b/p2p/src/peer_manager/tests/connections.rs index 41134ee64..cc8230d64 100644 --- a/p2p/src/peer_manager/tests/connections.rs +++ b/p2p/src/peer_manager/tests/connections.rs @@ -887,9 +887,14 @@ async fn connection_timeout_rpc_notified( ) .unwrap(); - logging::spawn_in_current_span(async move { - peer_manager.run().await.unwrap(); - }); + logging::spawn_in_current_span( + // Rust 1.92 thinks that the unwrap call here is unreachable, even though the function + // returns a normal error. + #[allow(unreachable_code)] + async move { + peer_manager.run().await.unwrap(); + }, + ); let (response_sender, response_receiver) = oneshot_nofail::channel(); peer_mgr_event_sender diff --git a/p2p/src/peer_manager/tests/mod.rs b/p2p/src/peer_manager/tests/mod.rs index d06812f24..5711f79a1 100644 --- a/p2p/src/peer_manager/tests/mod.rs +++ b/p2p/src/peer_manager/tests/mod.rs @@ -209,9 +209,14 @@ where { let (peer_manager, peer_mgr_event_sender, shutdown_sender, subscribers_sender) = make_peer_manager_custom::(transport, addr, chain_config, p2p_config, time_getter).await; - logging::spawn_in_current_span(async move { - peer_manager.run().await.unwrap(); - }); + logging::spawn_in_current_span( + // Rust 1.92 thinks that the unwrap call here is unreachable, even though the function + // returns a normal error. + #[allow(unreachable_code)] + async move { + peer_manager.run().await.unwrap(); + }, + ); (peer_mgr_event_sender, shutdown_sender, subscribers_sender) } diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index bbbd84021..bd68d454e 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -20,7 +20,7 @@ pub mod test_dir; pub mod threading; pub mod token_utils; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, convert::Infallible}; use hex::ToHex; use itertools::Itertools; @@ -30,6 +30,18 @@ use randomness::Rng; pub use basic_test_time_getter::BasicTestTimeGetter; +pub trait UnwrapInfallible { + type Output; + fn unwrap_infallible(self) -> Self::Output; +} + +impl UnwrapInfallible for Result { + type Output = T; + fn unwrap_infallible(self) -> Self::Output { + self.unwrap_or_else(|inf| match inf {}) + } +} + /// Assert that the encoded object matches the expected hex string. pub fn assert_encoded_eq(to_encode: &E, expected_hex: &str) { assert_eq!(to_encode.encode().encode_hex::(), expected_hex); diff --git a/utxo/src/cache.rs b/utxo/src/cache.rs index 15645604b..75749040e 100644 --- a/utxo/src/cache.rs +++ b/utxo/src/cache.rs @@ -522,12 +522,13 @@ fn should_include_in_utxo_set(output: &TxOutput) -> bool { #[cfg(test)] mod unit_test { use super::*; - use crate::tests::test_helper::{ - empty_test_utxos_view, insert_single_entry, Presence, UnwrapInfallible, - }; + use crate::tests::test_helper::{empty_test_utxos_view, insert_single_entry, Presence}; use common::primitives::H256; use rstest::rstest; - use test_utils::random::{make_seedable_rng, Seed}; + use test_utils::{ + random::{make_seedable_rng, Seed}, + UnwrapInfallible as _, + }; #[rstest] #[trace] diff --git a/utxo/src/tests/mod.rs b/utxo/src/tests/mod.rs index 4d8c15f77..29c7ede10 100644 --- a/utxo/src/tests/mod.rs +++ b/utxo/src/tests/mod.rs @@ -17,18 +17,11 @@ mod simulation; mod simulation_with_undo; pub mod test_helper; -use crate::{ - flush_to_base, - tests::test_helper::{ - create_tx_outputs, empty_test_utxos_view, - Presence::{self, *}, - UnwrapInfallible, - }, - utxo_entry::{IsDirty, IsFresh, UtxoEntry}, - ConsumedUtxoCache, - Error::{self, *}, - FlushableUtxoView, Utxo, UtxoSource, UtxosCache, UtxosTxUndo, UtxosView, -}; +use std::{collections::BTreeMap, convert::Infallible}; + +use itertools::Itertools; +use rstest::rstest; + use common::{ chain::{ block::{ @@ -44,11 +37,23 @@ use common::{ primitives::{Amount, BlockHeight, Compact, Id, Idable, H256}, }; use crypto::vrf::VRFKeyKind; -use itertools::Itertools; use randomness::{seq, CryptoRng, Rng}; -use rstest::rstest; -use std::{collections::BTreeMap, convert::Infallible}; -use test_utils::random::{make_seedable_rng, Seed}; +use test_utils::{ + random::{make_seedable_rng, Seed}, + UnwrapInfallible as _, +}; + +use crate::{ + flush_to_base, + tests::test_helper::{ + create_tx_outputs, empty_test_utxos_view, + Presence::{self, *}, + }, + utxo_entry::{IsDirty, IsFresh, UtxoEntry}, + ConsumedUtxoCache, + Error::{self, *}, + FlushableUtxoView, Utxo, UtxoSource, UtxosCache, UtxosTxUndo, UtxosView, +}; fn make_pool_id(rng: &mut impl Rng) -> PoolId { H256::random_using(rng).into() diff --git a/utxo/src/tests/simulation.rs b/utxo/src/tests/simulation.rs index 9a389d1a8..a331792e7 100644 --- a/utxo/src/tests/simulation.rs +++ b/utxo/src/tests/simulation.rs @@ -15,12 +15,18 @@ use std::convert::Infallible; -use super::test_helper::{create_utxo, empty_test_utxos_view, UnwrapInfallible}; -use crate::{ConsumedUtxoCache, FlushableUtxoView, UtxosCache, UtxosView}; +use rstest::rstest; + use common::chain::UtxoOutPoint; use randomness::{CryptoRng, Rng}; -use rstest::rstest; -use test_utils::random::{make_seedable_rng, Seed}; +use test_utils::{ + random::{make_seedable_rng, Seed}, + UnwrapInfallible as _, +}; + +use crate::{ConsumedUtxoCache, FlushableUtxoView, UtxosCache, UtxosView}; + +use super::test_helper::{create_utxo, empty_test_utxos_view}; // This test creates an arbitrary long chain of caches. // Every new cache is populated with random utxo values which can be created/spend/removed. diff --git a/utxo/src/tests/simulation_with_undo.rs b/utxo/src/tests/simulation_with_undo.rs index c9a2176bb..f50e1833f 100644 --- a/utxo/src/tests/simulation_with_undo.rs +++ b/utxo/src/tests/simulation_with_undo.rs @@ -13,18 +13,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::test_helper::{create_tx_outputs, empty_test_utxos_view, UnwrapInfallible}; -use crate::{ - ConsumedUtxoCache, FlushableUtxoView, UtxoSource, UtxosCache, UtxosTxUndoWithSources, UtxosView, -}; +use std::{collections::BTreeMap, convert::Infallible}; + +use rstest::rstest; + use common::{ chain::{block::BlockReward, OutPointSourceId, Transaction, TxInput, UtxoOutPoint}, primitives::{BlockHeight, Id, Idable, H256}, }; use randomness::{CryptoRng, Rng}; -use rstest::rstest; -use std::{collections::BTreeMap, convert::Infallible}; -use test_utils::random::{make_seedable_rng, Seed}; +use test_utils::{ + random::{make_seedable_rng, Seed}, + UnwrapInfallible as _, +}; + +use crate::{ + ConsumedUtxoCache, FlushableUtxoView, UtxoSource, UtxosCache, UtxosTxUndoWithSources, UtxosView, +}; + +use super::test_helper::{create_tx_outputs, empty_test_utxos_view}; // Structure to store outpoints of current utxo set and info for undo #[derive(Default)] diff --git a/utxo/src/tests/test_helper.rs b/utxo/src/tests/test_helper.rs index 5921cee07..b2096c36d 100644 --- a/utxo/src/tests/test_helper.rs +++ b/utxo/src/tests/test_helper.rs @@ -30,18 +30,6 @@ use crypto::key::{KeyKind, PrivateKey}; use itertools::Itertools; use randomness::{seq, CryptoRng, Rng}; -pub trait UnwrapInfallible { - type Output; - fn unwrap_infallible(self) -> Self::Output; -} - -impl UnwrapInfallible for Result { - type Output = T; - fn unwrap_infallible(self) -> Self::Output { - self.unwrap_or_else(|inf| match inf {}) - } -} - struct EmptyUtxosView { best_block_hash: Id, } diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index d14cb2ea0..2c4711048 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -37,6 +37,7 @@ itertools.workspace = true parity-scale-codec.workspace = true semver.workspace = true serde.workspace = true +strum.workspace = true thiserror.workspace = true trezor-client = { workspace = true, optional = true } zeroize.workspace = true diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index ba750a6e4..58a479835 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -75,7 +75,7 @@ pub struct DelegationData { pub pool_id: PoolId, pub destination: Destination, pub last_nonce: Option, - /// last parent transaction if the parent is unconfirmed + /// Last transaction that changed the delegation state. pub last_parent: Option, pub not_staked_yet: bool, } @@ -509,7 +509,7 @@ pub struct TokenIssuanceData { pub authority: Destination, pub last_nonce: Option, - /// last parent transaction if the parent is unconfirmed + /// Last transaction that changed the token issuance state. pub last_parent: Option, /// unconfirmed transactions that modify the total supply or frozen state of this token @@ -527,7 +527,7 @@ impl TokenIssuanceData { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct OrderData { pub conclude_key: Destination, pub initially_asked: RpcOutputValue, @@ -537,7 +537,7 @@ pub struct OrderData { pub creation_timestamp: Option, pub last_nonce: Option, - /// Last parent transaction if the parent is unconfirmed. + /// Last transaction that changed the order state. pub last_parent: Option, pub is_concluded: bool, @@ -1101,7 +1101,7 @@ impl OutputCache { } /// Update the inputs for a new transaction, mark them as consumed and update delegation account - /// balances + /// balances, token issuance states and order states. fn update_inputs( &mut self, tx: &WalletTx, diff --git a/wallet/src/account/output_cache/tests.rs b/wallet/src/account/output_cache/tests.rs index 18474d313..bddcaadb9 100644 --- a/wallet/src/account/output_cache/tests.rs +++ b/wallet/src/account/output_cache/tests.rs @@ -15,15 +15,25 @@ use rstest::rstest; +use strum::IntoEnumIterator as _; + use chainstate_test_framework::{empty_witness, TransactionBuilder}; -use common::chain::{ - config::{create_unit_test_config, create_unit_test_config_builder}, - signature::inputsig::InputWitness, - timelock::OutputTimeLock, - OrderData, +use common::{ + address::pubkeyhash::PublicKeyHash, + chain::{ + config::{create_unit_test_config, create_unit_test_config_builder}, + make_token_id_with_version, + signature::inputsig::InputWitness, + timelock::OutputTimeLock, + tokens::TokenIssuanceV1, + AccountOutPoint, ChainstateUpgradeBuilder, OrderData, TokenIdGenerationVersion, + }, }; -use randomness::Rng; +use randomness::{seq::IteratorRandom as _, Rng}; use test_utils::random::{make_seedable_rng, Seed}; +use wallet_types::wallet_tx::TxStateTag; + +use crate::account::output_cache; use super::*; @@ -448,11 +458,6 @@ fn update_conflicting_txs_frozen_token_only_in_outputs(#[case] seed: Seed) { #[trace] #[case(Seed::from_entropy())] fn token_id_in_add_tx(#[case] seed: Seed) { - use common::chain::{ - make_token_id_with_version, tokens::TokenIssuanceV1, AccountOutPoint, - ChainstateUpgradeBuilder, TokenIdGenerationVersion, - }; - let mut rng = make_seedable_rng(seed); let fork_height = BlockHeight::new(rng.gen_range(1000..1_000_000)); @@ -741,3 +746,404 @@ fn abandon_transaction(#[case] seed: Seed) { ]) ); } + +// Create, fill, freeze and conclude 2 orders, checking the contents of the `orders` map +// inside the cache. +// The txs related to the 1st order are Confirmed, and those related to the 2nd one are Inactive. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn orders_state_update(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let chain_config = create_unit_test_config(); + + let token_id = TokenId::random_using(&mut rng); + + let conclude_key1 = Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)); + let conclude_key2 = Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)); + let coins1 = OutputValue::Coin(Amount::from_atoms(rng.gen_range(1000..100_1000))); + let coins2 = OutputValue::Coin(Amount::from_atoms(rng.gen_range(1000..100_1000))); + let tokens1 = OutputValue::TokenV1(token_id, Amount::from_atoms(rng.gen_range(1000..100_1000))); + let tokens2 = OutputValue::TokenV1(token_id, Amount::from_atoms(rng.gen_range(1000..100_1000))); + + let parent_tx_1_id = Id::::random_using(&mut rng); + let order1_creation_tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(parent_tx_1_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::CreateOrder(Box::new(OrderData::new( + conclude_key1.clone(), + coins1.clone(), + tokens1.clone(), + )))) + .build(); + let order1_creation_tx_id = order1_creation_tx.transaction().get_id(); + let order1_creation_timestamp = BlockTimestamp::from_int_seconds(rng.gen_range(0..10)); + let order1_id = make_order_id(order1_creation_tx.inputs()).unwrap(); + let order1_creation_tx_confirmation_height = BlockHeight::new(rng.gen_range(0..10)); + + let mut output_cache = OutputCache::empty(); + + // Create order 1 + + if rng.gen_bool(0.5) { + add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + } + + output_cache + .add_tx( + &chain_config, + BlockHeight::new(10), + order1_creation_tx_id.into(), + WalletTx::Tx(TxData::new( + order1_creation_tx, + TxState::Confirmed( + order1_creation_tx_confirmation_height, + order1_creation_timestamp, + rng.gen_range(0..10), + ), + )), + ) + .unwrap(); + + if rng.gen_bool(0.5) { + add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + } + + let mut expected_cached_order1_data = output_cache::OrderData { + conclude_key: conclude_key1.clone(), + initially_asked: RpcOutputValue::from_output_value(&coins1).unwrap(), + initially_given: RpcOutputValue::from_output_value(&tokens1).unwrap(), + creation_timestamp: Some(order1_creation_timestamp), + last_nonce: None, + last_parent: None, + is_concluded: false, + is_frozen: false, + }; + + assert_eq!( + output_cache.orders, + BTreeMap::from_iter([(order1_id, expected_cached_order1_data.clone())]) + ); + + // Create order 2 + + let parent_tx_2_id = Id::::random_using(&mut rng); + let order2_creation_tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(parent_tx_2_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::CreateOrder(Box::new(OrderData::new( + conclude_key2.clone(), + tokens2.clone(), + coins2.clone(), + )))) + .build(); + let order2_creation_tx_id = order2_creation_tx.transaction().get_id(); + let order2_id = make_order_id(order2_creation_tx.inputs()).unwrap(); + + output_cache + .add_tx( + &chain_config, + BlockHeight::new(20), + order2_creation_tx_id.into(), + WalletTx::Tx(TxData::new( + order2_creation_tx, + TxState::Inactive(rng.gen()), + )), + ) + .unwrap(); + + if rng.gen_bool(0.5) { + add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + } + + let mut expected_cached_order2_data = output_cache::OrderData { + conclude_key: conclude_key2, + initially_asked: RpcOutputValue::from_output_value(&tokens2).unwrap(), + initially_given: RpcOutputValue::from_output_value(&coins2).unwrap(), + creation_timestamp: None, + last_nonce: None, + last_parent: None, + is_concluded: false, + is_frozen: false, + }; + + assert_eq!( + output_cache.orders, + BTreeMap::from_iter([ + (order1_id, expected_cached_order1_data.clone()), + (order2_id, expected_cached_order2_data.clone()) + ]) + ); + + // Fill order 1 + + let order1_fill_tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order1_id, + Amount::from_atoms(rng.gen_range(100..200)), + )), + InputWitness::NoSignature(None), + ) + .build(); + let order1_fill_tx_id = order1_fill_tx.transaction().get_id(); + + output_cache + .add_tx( + &chain_config, + BlockHeight::new(30), + order1_fill_tx_id.into(), + WalletTx::Tx(TxData::new( + order1_fill_tx, + TxState::Confirmed( + BlockHeight::new(rng.gen_range(20..30)), + BlockTimestamp::from_int_seconds(rng.gen_range(20..30)), + rng.gen_range(0..10), + ), + )), + ) + .unwrap(); + + if rng.gen_bool(0.5) { + add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + } + + expected_cached_order1_data.last_parent = Some(order1_fill_tx_id.into()); + assert_eq!( + output_cache.orders, + BTreeMap::from_iter([ + (order1_id, expected_cached_order1_data.clone()), + (order2_id, expected_cached_order2_data.clone()) + ]) + ); + + // Fill order 2 + + let order2_fill_tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order2_id, + Amount::from_atoms(rng.gen_range(100..200)), + )), + InputWitness::NoSignature(None), + ) + .build(); + let order2_fill_tx_id = order2_fill_tx.transaction().get_id(); + + output_cache + .add_tx( + &chain_config, + BlockHeight::new(40), + order2_fill_tx_id.into(), + WalletTx::Tx(TxData::new(order2_fill_tx, TxState::Inactive(rng.gen()))), + ) + .unwrap(); + + if rng.gen_bool(0.5) { + add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + } + + expected_cached_order2_data.last_parent = Some(order2_fill_tx_id.into()); + assert_eq!( + output_cache.orders, + BTreeMap::from_iter([ + (order1_id, expected_cached_order1_data.clone()), + (order2_id, expected_cached_order2_data.clone()) + ]) + ); + + // Freeze order 1 + + let order1_freeze_tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(order1_id)), + InputWitness::NoSignature(None), + ) + .build(); + let order1_freeze_tx_id = order1_freeze_tx.transaction().get_id(); + + output_cache + .add_tx( + &chain_config, + BlockHeight::new(50), + order1_freeze_tx_id.into(), + WalletTx::Tx(TxData::new( + order1_freeze_tx, + TxState::Confirmed( + BlockHeight::new(rng.gen_range(40..50)), + BlockTimestamp::from_int_seconds(rng.gen_range(40..50)), + rng.gen_range(0..10), + ), + )), + ) + .unwrap(); + + if rng.gen_bool(0.5) { + add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + } + + expected_cached_order1_data.last_parent = Some(order1_freeze_tx_id.into()); + expected_cached_order1_data.is_frozen = true; + assert_eq!( + output_cache.orders, + BTreeMap::from_iter([ + (order1_id, expected_cached_order1_data.clone()), + (order2_id, expected_cached_order2_data.clone()) + ]) + ); + + // Freeze order 2 + + let order2_freeze_tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(order2_id)), + InputWitness::NoSignature(None), + ) + .build(); + let order2_freeze_tx_id = order2_freeze_tx.transaction().get_id(); + + output_cache + .add_tx( + &chain_config, + BlockHeight::new(60), + order2_freeze_tx_id.into(), + WalletTx::Tx(TxData::new(order2_freeze_tx, TxState::Inactive(rng.gen()))), + ) + .unwrap(); + + if rng.gen_bool(0.5) { + add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + } + + expected_cached_order2_data.last_parent = Some(order2_freeze_tx_id.into()); + expected_cached_order2_data.is_frozen = true; + assert_eq!( + output_cache.orders, + BTreeMap::from_iter([ + (order1_id, expected_cached_order1_data.clone()), + (order2_id, expected_cached_order2_data.clone()) + ]) + ); + + // Conclude order 1 + + let order1_conclude_tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order1_id)), + InputWitness::NoSignature(None), + ) + .build(); + let order1_conclude_tx_id = order1_conclude_tx.transaction().get_id(); + + output_cache + .add_tx( + &chain_config, + BlockHeight::new(70), + order1_conclude_tx_id.into(), + WalletTx::Tx(TxData::new( + order1_conclude_tx, + TxState::Confirmed( + BlockHeight::new(rng.gen_range(60..70)), + BlockTimestamp::from_int_seconds(rng.gen_range(60..70)), + rng.gen_range(0..10), + ), + )), + ) + .unwrap(); + + if rng.gen_bool(0.5) { + add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + } + + expected_cached_order1_data.last_parent = Some(order1_conclude_tx_id.into()); + expected_cached_order1_data.is_concluded = true; + assert_eq!( + output_cache.orders, + BTreeMap::from_iter([ + (order1_id, expected_cached_order1_data.clone()), + (order2_id, expected_cached_order2_data.clone()) + ]) + ); + + // Conclude order 2 + + let order2_conclude_tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order2_id)), + InputWitness::NoSignature(None), + ) + .build(); + let order2_conclude_tx_id = order2_conclude_tx.transaction().get_id(); + + output_cache + .add_tx( + &chain_config, + BlockHeight::new(80), + order2_conclude_tx_id.into(), + WalletTx::Tx(TxData::new( + order2_conclude_tx, + TxState::Inactive(rng.gen()), + )), + ) + .unwrap(); + + if rng.gen_bool(0.5) { + add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + } + + expected_cached_order2_data.last_parent = Some(order2_conclude_tx_id.into()); + expected_cached_order2_data.is_concluded = true; + assert_eq!( + output_cache.orders, + BTreeMap::from_iter([ + (order1_id, expected_cached_order1_data.clone()), + (order2_id, expected_cached_order2_data.clone()) + ]) + ); +} + +fn add_random_transfer_tx( + output_cache: &mut OutputCache, + chain_config: &ChainConfig, + mut rng: impl Rng, +) { + let random_tx_id = Id::::random_using(&mut rng); + let random_block_id = Id::::random_using(&mut rng); + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(random_tx_id.into(), rng.gen()), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(rng.gen())), + Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)), + )) + .build(); + let tx_id = tx.transaction().get_id(); + + let tx_state = match TxStateTag::iter().choose(&mut rng).unwrap() { + TxStateTag::Confirmed => TxState::Confirmed( + BlockHeight::new(rng.gen_range(0..100)), + BlockTimestamp::from_int_seconds(rng.gen_range(0..100)), + rng.gen_range(0..100), + ), + TxStateTag::InMempool => TxState::InMempool(rng.gen()), + TxStateTag::Conflicted => TxState::Conflicted(random_block_id), + TxStateTag::Inactive => TxState::Inactive(rng.gen()), + TxStateTag::Abandoned => TxState::Abandoned, + }; + + output_cache + .add_tx( + &chain_config, + BlockHeight::new(rng.gen_range(0..100)), + tx_id.into(), + WalletTx::Tx(TxData::new(tx, tx_state)), + ) + .unwrap(); +} diff --git a/wallet/types/Cargo.toml b/wallet/types/Cargo.toml index 7c8fac78c..ad2285a22 100644 --- a/wallet/types/Cargo.toml +++ b/wallet/types/Cargo.toml @@ -24,6 +24,7 @@ itertools.workspace = true parity-scale-codec.workspace = true semver.workspace = true serde.workspace = true +strum.workspace = true thiserror.workspace = true zeroize.workspace = true diff --git a/wallet/types/src/wallet_tx.rs b/wallet/types/src/wallet_tx.rs index b8c15f987..8f068cfb3 100644 --- a/wallet/types/src/wallet_tx.rs +++ b/wallet/types/src/wallet_tx.rs @@ -25,20 +25,27 @@ use common::chain::{ use common::primitives::id::WithId; use common::primitives::{BlockHeight, Id, Idable}; -#[derive(Debug, PartialEq, Eq, Clone, Copy, Decode, Encode, serde::Serialize)] +#[derive( + Debug, PartialEq, Eq, Clone, Copy, Decode, Encode, serde::Serialize, strum::EnumDiscriminants, +)] +#[strum_discriminants(name(TxStateTag), derive(strum::EnumIter))] pub enum TxState { /// Confirmed transaction in a block #[codec(index = 0)] - Confirmed(BlockHeight, BlockTimestamp, u64), + Confirmed( + BlockHeight, + BlockTimestamp, + /*tx index inside the block*/ u64, + ), /// Unconfirmed transaction in the mempool #[codec(index = 1)] - InMempool(u64), + InMempool(/*unconfirmed tx counter*/ u64), /// Conflicted transaction with a confirmed block #[codec(index = 2)] Conflicted(Id), /// Transaction that is not confirmed or conflicted and is not in the mempool. #[codec(index = 3)] - Inactive(u64), + Inactive(/*unconfirmed tx counter*/ u64), /// Transaction that is not confirmed or conflicted and is not in the mempool and marked as /// abandoned by the user #[codec(index = 4)] diff --git a/wallet/wallet-cli-commands/src/helper_types.rs b/wallet/wallet-cli-commands/src/helper_types.rs index a00b38b0d..fef48f541 100644 --- a/wallet/wallet-cli-commands/src/helper_types.rs +++ b/wallet/wallet-cli-commands/src/helper_types.rs @@ -252,6 +252,7 @@ pub fn format_token_name( .decode_object(chain_config) .map_err(WalletCliCommandError::TokenIdDecodingError)?; + // FIXME ticker is supposed to be alphanum only; at least print an error to log; same for orders sorting. let result = if let Some(token_ticker) = token_infos .get(&decoded_token_id) .and_then(|token_info| str::from_utf8(token_info.token_ticker()).ok()) diff --git a/wallet/wallet-rpc-daemon/docs/RPC.md b/wallet/wallet-rpc-daemon/docs/RPC.md index 06ab43217..edaeda3a5 100644 --- a/wallet/wallet-rpc-daemon/docs/RPC.md +++ b/wallet/wallet-rpc-daemon/docs/RPC.md @@ -2347,13 +2347,13 @@ Returns: } ``` -### Method `create_order` +### Method `order_create` -Create an order for exchanging "given" amount of an arbitrary currency (coins or tokens) for -an arbitrary amount of "asked" currency. +Create an order for exchanging an amount of one ("given") currency for a certain amount of +another ("asked") currency. Either of the currencies can be coins or tokens. -Conclude key is the key that can authorize a conclude order command, closing the order and withdrawing -all the remaining funds from it. +Conclude key is the key that can authorize the conclude order command, closing the order +and withdrawing all the remaining funds from it. Parameters: @@ -2424,13 +2424,14 @@ Returns: } ``` -### Method `conclude_order` +### Method `order_conclude` Conclude an order, given its id. This assumes that the conclude key is owned by the selected account in this wallet. Optionally, an output address can be provided where remaining funds from the order are transferred. +If not specified, a new receive address will be generated for this purpose. Parameters: @@ -2471,11 +2472,12 @@ Returns: } ``` -### Method `fill_order` +### Method `order_fill` Fill order completely or partially given its id and an amount in the order's "asked" currency. Optionally, an output address can be provided where the exchanged funds from the order are transferred. +If not specified, a new receive address will be generated for this purpose. Parameters: @@ -2519,7 +2521,7 @@ Returns: } ``` -### Method `freeze_order` +### Method `order_freeze` Freeze an order given its id. This prevents an order from being filled. Only a conclude operation is allowed afterwards. @@ -2560,6 +2562,154 @@ Returns: } ``` +### Method `order_list_own` + +List orders whose conclude key is owned by the given account. + + +Parameters: +``` +{ "account": number } +``` + +Returns: +``` +[ { + "order_id": bech32 string, + "initially_asked": EITHER OF + 1) { + "type": "Coin", + "content": { "amount": { + "atoms": number string, + "decimal": decimal string, + } }, + } + 2) { + "type": "Token", + "content": { + "id": bech32 string, + "amount": { + "atoms": number string, + "decimal": decimal string, + }, + }, + }, + "initially_given": EITHER OF + 1) { + "type": "Coin", + "content": { "amount": { + "atoms": number string, + "decimal": decimal string, + } }, + } + 2) { + "type": "Token", + "content": { + "id": bech32 string, + "amount": { + "atoms": number string, + "decimal": decimal string, + }, + }, + }, + "existing_order_data": EITHER OF + 1) { + "ask_balance": { + "atoms": number string, + "decimal": decimal string, + }, + "give_balance": { + "atoms": number string, + "decimal": decimal string, + }, + "creation_timestamp": { "timestamp": number }, + "is_frozen": bool, + } + 2) null, + "is_marked_as_frozen_in_wallet": bool, + "is_marked_as_concluded_in_wallet": bool, +}, .. ] +``` + +### Method `order_list_all_active` + +List all active (i.e. non-concluded, non-frozen) orders whose currencies match the passed ones. +If a passed currency is None, any order will match. + + +Parameters: +``` +{ + "account": number, + "ask_currency": EITHER OF + 1) { "type": "Coin" } + 2) { + "type": "Token", + "content": hex string, + } + 3) null, + "give_currency": EITHER OF + 1) { "type": "Coin" } + 2) { + "type": "Token", + "content": hex string, + } + 3) null, +} +``` + +Returns: +``` +[ { + "order_id": bech32 string, + "initially_asked": EITHER OF + 1) { + "type": "Coin", + "content": { "amount": { + "atoms": number string, + "decimal": decimal string, + } }, + } + 2) { + "type": "Token", + "content": { + "id": bech32 string, + "amount": { + "atoms": number string, + "decimal": decimal string, + }, + }, + }, + "initially_given": EITHER OF + 1) { + "type": "Coin", + "content": { "amount": { + "atoms": number string, + "decimal": decimal string, + } }, + } + 2) { + "type": "Token", + "content": { + "id": bech32 string, + "amount": { + "atoms": number string, + "decimal": decimal string, + }, + }, + }, + "ask_balance": { + "atoms": number string, + "decimal": decimal string, + }, + "give_balance": { + "atoms": number string, + "decimal": decimal string, + }, + "is_own": bool, +}, .. ] +``` + ### Method `node_version` Obtain the node version @@ -3186,6 +3336,115 @@ Returns: ], .. ] ``` +### Method `node_get_tokens_info` + +Return token infos for the given token ids. + + +Parameters: +``` +{ "token_ids": [ bech32 string, .. ] } +``` + +Returns: +``` +[ EITHER OF + 1) { + "type": "FungibleToken", + "content": { + "token_id": hex string, + "token_ticker": { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + }, + "number_of_decimals": number, + "metadata_uri": { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + }, + "circulating_supply": { "atoms": number string }, + "total_supply": EITHER OF + 1) { + "type": "Fixed", + "content": { "amount": { "atoms": number string } }, + } + 2) { "type": "Lockable" } + 3) { "type": "Unlimited" }, + "is_locked": bool, + "frozen": EITHER OF + 1) { + "type": "NotFrozen", + "content": { "freezable": bool }, + } + 2) { + "type": "Frozen", + "content": { "unfreezable": bool }, + }, + "authority": bech32 string, + }, + } + 2) { + "type": "NonFungibleToken", + "content": { + "token_id": hex string, + "creation_tx_id": hex string, + "creation_block_id": hex string, + "metadata": { + "creator": EITHER OF + 1) hex string + 2) null, + "name": { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + }, + "description": { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + }, + "ticker": { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + }, + "icon_uri": EITHER OF + 1) { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + } + 2) null, + "additional_metadata_uri": EITHER OF + 1) { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + } + 2) null, + "media_uri": EITHER OF + 1) { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + } + 2) null, + "media_hash": hex string, + }, + }, + }, .. ] +``` + ## Module `ColdWalletRpc` RPC methods available in the cold wallet mode. From a2724087d955c0e3fc2b4259a03fb1b1e784c6c7 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Sat, 13 Dec 2025 21:10:53 +0200 Subject: [PATCH 05/10] Fix functional test failures due to account-balance now also mentioning token tickers and order-related rpc calls having been renamed --- .../test_framework/wallet_rpc_controller.py | 16 ++++++---- test/functional/wallet_conflict.py | 7 ++-- .../functional/wallet_htlc_refund_multisig.py | 4 +-- .../wallet_htlc_refund_single_sig.py | 6 ++-- test/functional/wallet_htlc_spend.py | 22 ++++++++----- test/functional/wallet_nfts.py | 19 ++++++----- ...t_order_double_fill_with_same_dest_impl.py | 22 ++++++++----- test/functional/wallet_orders_impl.py | 32 ++++++++++++------- test/functional/wallet_sweep_address.py | 20 ++++++++---- test/functional/wallet_tokens.py | 4 +-- .../wallet_tokens_change_authority.py | 8 ++--- .../functional/wallet_tokens_change_supply.py | 24 ++++++++++---- test/functional/wallet_tokens_freeze.py | 9 ++++-- ...llet_tokens_transfer_from_multisig_addr.py | 9 ++++-- test/functional/wallet_tx_intent.py | 13 +++++--- .../src/command_handler/mod.rs | 4 ++- 16 files changed, 139 insertions(+), 80 deletions(-) diff --git a/test/functional/test_framework/wallet_rpc_controller.py b/test/functional/test_framework/wallet_rpc_controller.py index 1dbba4471..5e5bbbd7f 100644 --- a/test/functional/test_framework/wallet_rpc_controller.py +++ b/test/functional/test_framework/wallet_rpc_controller.py @@ -504,10 +504,14 @@ async def get_balance(self, with_locked: str = 'unlocked', utxo_states: List[str if 'tokens' in result: for (hexified_token_id, balance) in result['tokens'].items(): token_id_as_addr = self.node.test_functions_dehexify_all_addresses(hexified_token_id) - tokens[token_id_as_addr] = balance['decimal'] + token_info = self.node.chainstate_token_info(token_id_as_addr) + ticker = token_info["content"]["token_ticker"]["text"] + + tokens[token_id_as_addr] = (ticker, balance['decimal']) # Mimic the output of wallet_cli_controller's 'get_balance' - return "\n".join([f"Coins amount: {coins}"] + [f"Token: {token} amount: {amount}" for token, amount in tokens.items()]) + return "\n".join([f"Coins amount: {coins}"] + [f"Token: {token} ({ticker}), amount: {amount}" + for token, (ticker, amount) in tokens.items()]) async def new_vrf_public_key(self) -> str: result = self._write_command("staking_new_vrf_public_key", [self.account]) @@ -660,7 +664,7 @@ async def create_order(self, give = {"type": "Coin", "content": {"amount": {'decimal': str(give_amount)}}} object = [self.account, ask, give, conclude_address, {'in_top_x_mb': 5}] - result = self._write_command("create_order", object) + result = self._write_command("order_create", object) return result async def fill_order(self, @@ -668,17 +672,17 @@ async def fill_order(self, fill_amount: int, output_address: Optional[str] = None) -> str: object = [self.account, order_id, {'decimal': str(fill_amount)}, output_address, {'in_top_x_mb': 5}] - result = self._write_command("fill_order", object) + result = self._write_command("order_fill", object) return result async def freeze_order(self, order_id: str) -> str: object = [self.account, order_id, {'in_top_x_mb': 5}] - result = self._write_command("freeze_order", object) + result = self._write_command("order_freeze", object) return result async def conclude_order(self, order_id: str, output_address: Optional[str] = None) -> str: object = [self.account, order_id, output_address, {'in_top_x_mb': 5}] - result = self._write_command("conclude_order", object) + result = self._write_command("order_conclude", object) return result diff --git a/test/functional/wallet_conflict.py b/test/functional/wallet_conflict.py index 1161ffcdf..097350b9c 100644 --- a/test/functional/wallet_conflict.py +++ b/test/functional/wallet_conflict.py @@ -113,7 +113,8 @@ async def async_test(self): assert_in(f"Coins amount: {coins_to_send * 2 + token_fee}", await wallet.get_balance()) address = await wallet.new_address() - token_id, tx_id, err = await wallet.issue_new_token("XXX", 2, "http://uri", address) + token_ticker = "XXX" + token_id, tx_id, err = await wallet.issue_new_token(token_ticker, 2, "http://uri", address) assert token_id is not None assert tx_id is not None assert err is None @@ -138,7 +139,7 @@ async def async_test(self): # now send tokens from acc1 and freeze the tokens from default acc assert_in("Success", await wallet.select_account(1)) - assert_in(f"{token_id} amount: {tokens_to_mint}", await wallet.get_balance()) + assert_in(f"{token_id} ({token_ticker}), amount: {tokens_to_mint}", await wallet.get_balance()) assert_in("The transaction was submitted successfully", await wallet.send_tokens_to_address(token_id, address, tokens_to_mint)) transactions = node.mempool_transactions() assert_equal(len(transactions), 1) @@ -189,7 +190,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) assert_in("Success", await wallet.select_account(DEFAULT_ACCOUNT_INDEX)) - assert_in(f"{token_id} amount: 10", await wallet.get_balance()) + assert_in(f"{token_id} ({token_ticker}), amount: 10", await wallet.get_balance()) # check we cannot abandon an already confirmed transaction assert_in("Success", await wallet.select_account(1)) diff --git a/test/functional/wallet_htlc_refund_multisig.py b/test/functional/wallet_htlc_refund_multisig.py index fcf31d79d..178bac27c 100644 --- a/test/functional/wallet_htlc_refund_multisig.py +++ b/test/functional/wallet_htlc_refund_multisig.py @@ -155,7 +155,7 @@ async def async_test(self): balance = await wallet.get_balance() print(balance) assert_in("Coins amount: 0", balance) - assert_in(f"Token: {token_id} amount: {amount_to_mint}", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {amount_to_mint}", balance) ######################################################################################## # Setup Alice's htlc @@ -317,7 +317,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() assert_in("Coins amount: 0", balance) - assert_in(f"Token: {token_id} amount: {alice_amount_to_swap}", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {alice_amount_to_swap}", balance) await self.switch_to_wallet(wallet, 'bob_wallet') assert_in("Success", await wallet.sync()) diff --git a/test/functional/wallet_htlc_refund_single_sig.py b/test/functional/wallet_htlc_refund_single_sig.py index 664e09e72..812c1310e 100644 --- a/test/functional/wallet_htlc_refund_single_sig.py +++ b/test/functional/wallet_htlc_refund_single_sig.py @@ -157,7 +157,7 @@ async def async_test(self): # Check Alice's balance balance = await wallet.get_balance() assert_in("Coins amount: 0", balance) - assert_in(f"Token: {token_id} amount: {amount_to_mint}", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {amount_to_mint}", balance) ######################################################################################## # Setup Alice's htlc @@ -315,8 +315,8 @@ async def async_test(self): unlocked_balance = await wallet.get_balance(with_locked='unlocked') assert_in("Coins amount: 0", all_balance) assert_in("Coins amount: 0", unlocked_balance) - assert_in(f"Token: {token_id} amount: {alice_amount_to_swap}", all_balance) - assert_in(f"Token: {token_id} amount: {alice_amount_to_swap}", unlocked_balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {alice_amount_to_swap}", all_balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {alice_amount_to_swap}", unlocked_balance) # Switch to Bob's wallet and check balance - he got his coins back, but the're locked # because he used LockThenTransfer. diff --git a/test/functional/wallet_htlc_spend.py b/test/functional/wallet_htlc_spend.py index a9e7bd6e4..c2a74ab07 100644 --- a/test/functional/wallet_htlc_spend.py +++ b/test/functional/wallet_htlc_spend.py @@ -108,11 +108,16 @@ async def async_test(self): self.log.debug(f'Tip: {tip_id}') # Submit a valid transaction - outputs = [{ - 'Transfer': [ { 'Coin': 151 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': alice_pub_key_bytes}}} } ], - }, { - 'Transfer': [ { 'Coin': 151 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': bob_pub_key_bytes}}} } ], - }] + outputs = [ + {'Transfer': [ + { 'Coin': 151 * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': alice_pub_key_bytes}}} } + ]}, + {'Transfer': [ + { 'Coin': 151 * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': bob_pub_key_bytes}}} } + ]} + ] encoded_tx, tx_id = make_tx([reward_input(tip_id)], outputs, 0) node.mempool_submit_transaction(encoded_tx, {}) @@ -133,7 +138,8 @@ async def async_test(self): assert_not_in("Tokens", balance) # issue a valid token - token_id, _, _ = (await wallet.issue_new_token("XXXX", 2, "http://uri", alice_address)) + token_ticker = "XXXX" + token_id, _, _ = (await wallet.issue_new_token(token_ticker, 2, "http://uri", alice_address)) assert token_id is not None self.log.info(f"new token id: {token_id}") @@ -151,7 +157,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() assert_in("Coins amount: 0", balance) - assert_in(f"Token: {token_id} amount: {amount_to_mint}", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {amount_to_mint}", balance) ######################################################################################## # Setup Alice's htlc @@ -276,7 +282,7 @@ async def async_test(self): balance = await wallet.get_balance() assert_in("Coins amount: 0", balance) - assert_in(f"Token: {token_id} amount: {alice_amount_to_swap}", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {alice_amount_to_swap}", balance) if __name__ == '__main__': diff --git a/test/functional/wallet_nfts.py b/test/functional/wallet_nfts.py index 47876abc8..428d33094 100644 --- a/test/functional/wallet_nfts.py +++ b/test/functional/wallet_nfts.py @@ -89,7 +89,10 @@ async def async_test(self): # Submit a valid transaction output = { - 'Transfer': [ { 'Coin': 1000 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], + 'Transfer': [ + { 'Coin': 1000 * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } + ], } encoded_tx, tx_id = make_tx([reward_input(tip_id)], [output], 0) @@ -128,13 +131,14 @@ async def async_test(self): # invalid name # > max len invalid_name = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(random.randint(11, 20))) - nft_id, err = await wallet.issue_new_nft(address, "123456", invalid_name, "SomeNFT", "XXX") + nft_ticker = "XXX" + nft_id, err = await wallet.issue_new_nft(address, "123456", invalid_name, "SomeNFT", nft_ticker) assert nft_id is None assert err is not None assert_in("Invalid name length", err) # non alphanumeric invalid_name = "asd" + random.choice(r"#$%&'()*+,-./:;<=>?@[]^_`{|}~") - nft_id, err = await wallet.issue_new_nft(address, "123456", invalid_name, "SomeNFT", "XXX") + nft_id, err = await wallet.issue_new_nft(address, "123456", invalid_name, "SomeNFT", nft_ticker) assert nft_id is None assert err is not None assert_in("Invalid character in token name", err) @@ -142,13 +146,13 @@ async def async_test(self): # invalid description # > max len invalid_desc = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(random.randint(101, 200))) - nft_id, err = await wallet.issue_new_nft(address, "123456", "Name", invalid_desc, "XXX") + nft_id, err = await wallet.issue_new_nft(address, "123456", "Name", invalid_desc, nft_ticker) assert nft_id is None assert err is not None assert_in("Invalid description length", err) # issue a valid NFT - nft_id, err = await wallet.issue_new_nft(address, "123456", "Name", "SomeNFT", "XXX") + nft_id, err = await wallet.issue_new_nft(address, "123456", "Name", "SomeNFT", nft_ticker) assert err is None assert nft_id is not None self.log.info(f"new nft id: '{nft_id}'") @@ -157,9 +161,8 @@ async def async_test(self): assert_in("Success", await wallet.sync()) assert_in(f"Coins amount: 994", await wallet.get_balance()) - self.log.info(await wallet.get_balance()) - assert_in(f"{nft_id} amount: 1", await wallet.get_balance()) + assert_in(f"{nft_id} ({nft_ticker}), amount: 1", await wallet.get_balance()) # create a new account and send some tokens to it await wallet.create_new_account() @@ -167,7 +170,7 @@ async def async_test(self): address = await wallet.new_address() await wallet.select_account(0) - assert_in(f"{nft_id} amount: 1", await wallet.get_balance()) + assert_in(f"{nft_id} ({nft_ticker}), amount: 1", await wallet.get_balance()) output = await wallet.send_tokens_to_address(nft_id, address, 1) self.log.info(output) assert_in("The transaction was submitted successfully", output) 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 155192de3..6c4a9e9b5 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 @@ -105,11 +105,16 @@ async def async_test(self): self.log.debug(f'Tip: {tip_id}') # Submit a valid transaction - outputs = [{ - 'Transfer': [ { 'Coin': 151 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': alice_pub_key_bytes}}} } ], - }, { - 'Transfer': [ { 'Coin': 151 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': bob_pub_key_bytes}}} } ], - }] + outputs = [ + {'Transfer': [ + { 'Coin': 151 * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': alice_pub_key_bytes}}} } + ]}, + {'Transfer': [ + { 'Coin': 151 * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': bob_pub_key_bytes}}} } + ]} + ] encoded_tx, tx_id = make_tx([reward_input(tip_id)], outputs, 0) node.mempool_submit_transaction(encoded_tx, {}) @@ -130,7 +135,8 @@ async def async_test(self): assert_not_in("Tokens", balance) # issue a valid token - token_id, _, _ = (await wallet.issue_new_token("XXXX", 2, "http://uri", alice_address)) + token_ticker = "XXXX" + token_id, _, _ = (await wallet.issue_new_token(token_ticker, 2, "http://uri", alice_address)) assert token_id is not None self.log.info(f"new token id: {token_id}") @@ -148,7 +154,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() assert_in(f"Coins amount: 0", balance) - assert_in(f"Token: {token_id} amount: {amount_to_mint}", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {amount_to_mint}", balance) ######################################################################################## # Alice creates an order selling tokens for coins @@ -208,4 +214,4 @@ async def async_test(self): balance = await wallet.get_balance() assert_in(f"Coins amount: 146.99", balance) - assert_in(f"Token: {token_id} amount: 2", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: 2", balance) diff --git a/test/functional/wallet_orders_impl.py b/test/functional/wallet_orders_impl.py index 449fcb285..8618ae05f 100644 --- a/test/functional/wallet_orders_impl.py +++ b/test/functional/wallet_orders_impl.py @@ -115,13 +115,20 @@ async def async_test(self): self.log.debug(f'Tip: {tip_id}') # Submit a valid transaction - outputs = [{ - 'Transfer': [ { 'Coin': 151 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': alice_pub_key_bytes}}} } ], - }, { - 'Transfer': [ { 'Coin': 151 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': bob_pub_key_bytes}}} } ], - }, { - 'Transfer': [ { 'Coin': 151 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': carol_pub_key_bytes}}} } ], - }] + outputs = [ + {'Transfer': [ + { 'Coin': 151 * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': alice_pub_key_bytes}}} } + ]}, + {'Transfer': [ + { 'Coin': 151 * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': bob_pub_key_bytes}}} } + ]}, + {'Transfer': [ + { 'Coin': 151 * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': carol_pub_key_bytes}}} } + ]} + ] encoded_tx, tx_id = make_tx([reward_input(tip_id)], outputs, 0) node.mempool_submit_transaction(encoded_tx, {}) @@ -142,7 +149,8 @@ async def async_test(self): assert_not_in("Tokens", balance) # issue a valid token - token_id, _, _ = (await wallet.issue_new_token("XXXX", 2, "http://uri", alice_address)) + token_ticker = "XXXX" + token_id, _, _ = (await wallet.issue_new_token(token_ticker, 2, "http://uri", alice_address)) assert token_id is not None self.log.info(f"new token id: {token_id}") @@ -160,7 +168,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() assert_in(f"Coins amount: 0", balance) - assert_in(f"Token: {token_id} amount: {amount_to_mint}", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {amount_to_mint}", balance) ######################################################################################## # Alice creates an order selling tokens for coins @@ -188,7 +196,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() assert_in(f"Coins amount: 148.99", balance) - assert_in(f"Token: {token_id} amount: 1", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: 1", balance) # try conclude order conclude_order_result = await wallet.conclude_order(order_id) @@ -208,7 +216,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() assert_in(f"Coins amount: 140", balance) - assert_in(f"Token: {token_id} amount: 5", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: 5", balance) # try freeze order freeze_order_result = await wallet.freeze_order(order_id) @@ -246,7 +254,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() assert_in(f"Coins amount: 12", balance) - assert_in(f"Token: {token_id} amount: {amount_to_mint - 5 - 1}", balance) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {amount_to_mint - 5 - 1}", balance) ######################################################################################## # Carol tries filling again diff --git a/test/functional/wallet_sweep_address.py b/test/functional/wallet_sweep_address.py index 0b53e20c5..3548be691 100644 --- a/test/functional/wallet_sweep_address.py +++ b/test/functional/wallet_sweep_address.py @@ -107,11 +107,17 @@ async def async_test(self): # Submit a valid transaction def make_output(pub_key_bytes): return { - 'Transfer': [ { 'Coin': coins_per_utxo * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], + 'Transfer': [ + { 'Coin': coins_per_utxo * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } + ], } def make_locked_output(pub_key_bytes): return { - 'LockThenTransfer': [ { 'Coin': coins_per_utxo * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} }, { 'ForBlockCount': 99 } ], + 'LockThenTransfer': [ + { 'Coin': coins_per_utxo * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} }, { 'ForBlockCount': 99 } + ], } encoded_tx, tx_id = make_tx([reward_input(tip_id)], [make_output(pk) for pk in pks] + [make_locked_output(locked_pub_key_bytes)], 0) @@ -169,7 +175,8 @@ def make_locked_output(pub_key_bytes): # issue some tokens to also transfer tokens_address = await wallet.new_address() - token_id, tx_id, err = await wallet.issue_new_token("XXX", 2, "http://uri", tokens_address) + token_ticker = "XXX" + token_id, tx_id, err = await wallet.issue_new_token(token_ticker, 2, "http://uri", tokens_address) assert token_id is not None assert tx_id is not None assert err is None @@ -182,7 +189,8 @@ def make_locked_output(pub_key_bytes): # issue some more tokens but freeze them frozen_tokens_address = await wallet.new_address() - frozen_token_id, frozen_tx_id, err = await wallet.issue_new_token("XXX", 2, "http://uri", frozen_tokens_address) + frozen_token_ticker = "YYY" + frozen_token_id, frozen_tx_id, err = await wallet.issue_new_token(frozen_token_ticker, 2, "http://uri", frozen_tokens_address) assert frozen_token_id is not None assert frozen_tx_id is not None assert err is None @@ -210,7 +218,7 @@ def make_locked_output(pub_key_bytes): # check the balance balance = await wallet.get_balance() # frozen tokens can't be transferred so they should be here - assert_in(f"{frozen_token_id} amount: 10000", balance) + assert_in(f"{frozen_token_id} ({frozen_token_ticker}), amount: 10000", balance) # the other token should not be in the acc1 balance any more assert_not_in(f"{token_id}", balance) @@ -219,7 +227,7 @@ def make_locked_output(pub_key_bytes): # frozen tokens can't be transferred so they are not in acc0 balance assert_not_in(f"{frozen_token_id}", balance) # the other token should be fully transferred in the acc0 balance - assert_in(f"{token_id} amount: 10000", balance) + assert_in(f"{token_id} ({token_ticker}), amount: 10000", balance) if __name__ == '__main__': diff --git a/test/functional/wallet_tokens.py b/test/functional/wallet_tokens.py index 5a810d088..af14c5176 100644 --- a/test/functional/wallet_tokens.py +++ b/test/functional/wallet_tokens.py @@ -160,7 +160,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) assert_in("Coins amount: 50", await wallet.get_balance()) - assert_in(f"{token_id} amount: {amount_to_mint}", await wallet.get_balance()) + assert_in(f"{token_id} ({valid_ticker}), amount: {amount_to_mint}", await wallet.get_balance()) ## create a new account and send some tokens to it await wallet.create_new_account() @@ -184,7 +184,7 @@ async def async_test(self): token_balance_decimals = 10**len(str(amount_to_send_decimals)) - amount_to_send_decimals num_zeroes = len(str(amount_to_send_decimals)) - len(str(token_balance_decimals)) token_balance_str = f"{token_balance}.{'0'*num_zeroes}{token_balance_decimals}".rstrip('0').rstrip('.') - assert_in(f"{token_id} amount: {token_balance_str}", await wallet.get_balance()) + assert_in(f"{token_id} ({valid_ticker}), amount: {token_balance_str}", await wallet.get_balance()) ## try to issue a new token, should fail with not enough coins token_id, tx_id, err = await wallet.issue_new_token("XXX", 2, "http://uri", address) diff --git a/test/functional/wallet_tokens_change_authority.py b/test/functional/wallet_tokens_change_authority.py index 0897fd256..f5e095996 100644 --- a/test/functional/wallet_tokens_change_authority.py +++ b/test/functional/wallet_tokens_change_authority.py @@ -113,7 +113,8 @@ async def async_test(self): address = await wallet.new_address() # issue a valid token - token_id, tx_id, err = await wallet.issue_new_token("XXX", 2, "http://uri", address, token_supply='lockable') + token_ticker = "XXX" + token_id, tx_id, err = await wallet.issue_new_token(token_ticker, 2, "http://uri", address, token_supply='lockable') assert token_id is not None assert tx_id is not None assert err is None @@ -128,7 +129,7 @@ async def async_test(self): self.generate_block() assert_in("Success", await wallet.sync()) - assert_in(f"{token_id} amount: 10000", await wallet.get_balance()) + assert_in(f"{token_id} ({token_ticker}), amount: 10000", await wallet.get_balance()) assert_in("Coins amount: 850", await wallet.get_balance()) ## create a new account and send some tokens to it @@ -146,8 +147,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) ## check the new balance - assert_in(f"{token_id} amount: 9989.99", await wallet.get_balance()) - + assert_in(f"{token_id} ({token_ticker}), amount: 9989.99", await wallet.get_balance()) assert_in("The transaction was submitted successfully", await wallet.change_token_authority(token_id, new_acc_address)) diff --git a/test/functional/wallet_tokens_change_supply.py b/test/functional/wallet_tokens_change_supply.py index def96b713..9aa13e713 100644 --- a/test/functional/wallet_tokens_change_supply.py +++ b/test/functional/wallet_tokens_change_supply.py @@ -89,7 +89,10 @@ async def async_test(self): # Submit a valid transaction output = { - 'Transfer': [ { 'Coin': 2001 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], + 'Transfer': [ + { 'Coin': 2001 * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } + ], } encoded_tx, tx_id = make_tx([reward_input(tip_id)], [output], 0) @@ -125,14 +128,15 @@ async def async_test(self): assert_in("Invalid character in token ticker", err) # invalid url - token_id, tx_id, err = await wallet.issue_new_token("XXX", 2, "123 123", address) + valid_ticker = "XXX" + token_id, tx_id, err = await wallet.issue_new_token(valid_ticker, 2, "123 123", address) assert token_id is None assert tx_id is None assert err is not None assert_in("Incorrect metadata URI", err) # invalid num decimals - token_id, tx_id, err = await wallet.issue_new_token("XXX", 99, "http://uri", address) + token_id, tx_id, err = await wallet.issue_new_token(valid_ticker, 99, "http://uri", address) assert token_id is None assert tx_id is None assert err is not None @@ -140,7 +144,7 @@ async def async_test(self): # issue a valid token number_of_decimals = random.randrange(0, 4) - token_id, tx_id, err = await wallet.issue_new_token("XXX", number_of_decimals, "http://uri", address, 'lockable') + token_id, tx_id, err = await wallet.issue_new_token(valid_ticker, number_of_decimals, "http://uri", address, 'lockable') assert token_id is not None assert tx_id is not None assert err is None @@ -175,7 +179,10 @@ async def async_test(self): expected_coins_balance -= 50 assert_in("The transaction was submitted successfully", await wallet.unmint_tokens(token_id, tokens_to_unmint)) else: - assert_in(f"Trying to unmint Amount {{ atoms: {tokens_to_unmint * 10**number_of_decimals} }} but the current supply is Amount {{ atoms: {total_tokens_supply * 10**number_of_decimals} }}", await wallet.unmint_tokens(token_id, tokens_to_unmint)) + assert_in( + f"Trying to unmint Amount {{ atoms: {tokens_to_unmint * 10**number_of_decimals} }} but the current supply is Amount {{ atoms: {total_tokens_supply * 10**number_of_decimals} }}", + await wallet.unmint_tokens(token_id, tokens_to_unmint) + ) continue # either generate a new block or leave the transaction as in-memory state @@ -186,7 +193,10 @@ async def async_test(self): # check total supply is correct if total_tokens_supply > 0: - assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance(utxo_states=['confirmed', 'inactive'])) + assert_in( + f"{token_id} ({valid_ticker}), amount: {total_tokens_supply}", + await wallet.get_balance(utxo_states=['confirmed', 'inactive']) + ) else: assert_not_in(f"{token_id}", await wallet.get_balance(utxo_states=['confirmed', 'inactive'])) @@ -197,7 +207,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) assert_in(f"Coins amount: {expected_coins_balance - 50}", await wallet.get_balance()) if total_tokens_supply > 0: - assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) + assert_in(f"{token_id} ({valid_ticker}), amount: {total_tokens_supply}", await wallet.get_balance()) else: assert_not_in(f"{token_id}", await wallet.get_balance()) diff --git a/test/functional/wallet_tokens_freeze.py b/test/functional/wallet_tokens_freeze.py index cd75c0cc8..fc07f8dd7 100644 --- a/test/functional/wallet_tokens_freeze.py +++ b/test/functional/wallet_tokens_freeze.py @@ -91,7 +91,10 @@ async def async_test(self): # Submit a valid transaction output = { - 'Transfer': [ { 'Coin': 501 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], + 'Transfer': [ + { 'Coin': 501 * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } + ], } encoded_tx, tx_id = make_tx([reward_input(tip_id)], [output], 0) @@ -139,7 +142,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) assert_in("Coins amount: 350", await wallet.get_balance()) - assert_in(f"{token_id} amount: 10000", await wallet.get_balance()) + assert_in(f"{token_id} ({ticker}), amount: 10000", await wallet.get_balance()) ## create a new account and send some tokens to it await wallet.create_new_account() @@ -154,7 +157,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) ## check the new balance - assert_in(f"{token_id} amount: 9989.99", await wallet.get_balance()) + assert_in(f"{token_id} ({ticker}), amount: 9989.99", await wallet.get_balance()) assert_in("The transaction was submitted successfully", await wallet.freeze_token(token_id, 'unfreezable')) diff --git a/test/functional/wallet_tokens_transfer_from_multisig_addr.py b/test/functional/wallet_tokens_transfer_from_multisig_addr.py index 89652ce85..8c01996c2 100644 --- a/test/functional/wallet_tokens_transfer_from_multisig_addr.py +++ b/test/functional/wallet_tokens_transfer_from_multisig_addr.py @@ -72,7 +72,10 @@ async def setup_coins_and_tokens(self, node, wallet, coin_amount, foo_amount, ba # This function will spend 2x100 coins on issuing tokens and 2x50 on minting; # also, a portion of a coin will be spent for the transaction fee. output = { - 'Transfer': [ { 'Coin': (coin_amount + 301) * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], + 'Transfer': [ + { 'Coin': (coin_amount + 301) * ATOMS_PER_COIN }, + { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } + ], } encoded_tx, tx_id = make_tx([reward_input(tip_id)], [output], 0) @@ -163,12 +166,12 @@ async def assert_balances(coin, foo, bar): assert_in(f"Coins amount: {coin}", balances) if foo: - assert_in(f"Token: {token_foo_id} amount: {foo}", balances) + assert_in(f"Token: {token_foo_id} (FOO), amount: {foo}", balances) else: assert_not_in(token_foo_id, balances) if bar: - assert_in(f"Token: {token_bar_id} amount: {bar}", balances) + assert_in(f"Token: {token_bar_id} (BAR), amount: {bar}", balances) else: assert_not_in(token_bar_id, balances) diff --git a/test/functional/wallet_tx_intent.py b/test/functional/wallet_tx_intent.py index 9ec4f0188..516bc3973 100644 --- a/test/functional/wallet_tx_intent.py +++ b/test/functional/wallet_tx_intent.py @@ -73,7 +73,10 @@ def run_test(self): # Create a token and mint the specified amount of it spread across several utxos. # The resulting coin balance's integer part will equal coin_amount. - async def setup_currency(self, node, wallet, coin_amount, min_token_amount_per_utxo, max_token_amount_per_utxo, token_utxo_count): + async def setup_currency( + self, node, wallet, coin_amount, token_ticker, min_token_amount_per_utxo, + max_token_amount_per_utxo, token_utxo_count + ): pub_key_bytes = await wallet.new_public_key() tip_id = node.chainstate_best_block_id() @@ -104,7 +107,7 @@ async def setup_currency(self, node, wallet, coin_amount, min_token_amount_per_u self.log.debug(f'token_issuer_address = {token_issuer_address}') # Create the token. - token_id, tx_id, err = await wallet.issue_new_token('FOO', 5, "http://uri", token_issuer_address) + token_id, tx_id, err = await wallet.issue_new_token(token_ticker, 5, "http://uri", token_issuer_address) assert token_id is not None assert tx_id is not None assert err is None @@ -150,8 +153,10 @@ async def async_test(self): await wallet.create_wallet('test_wallet') coin_amount = random.randint(100, 200) + token_ticker = "FOO" token_utxos_count = random.randint(5, 10) - (token_id, token_amount) = await self.setup_currency(node, wallet, coin_amount, 50, 250, token_utxos_count) + (token_id, token_amount) = await self.setup_currency( + node, wallet, coin_amount, token_ticker, 50, 250, token_utxos_count) async def assert_balances(coin, token): await self.sync_wallet(wallet) @@ -159,7 +164,7 @@ async def assert_balances(coin, token): assert_in(f"Coins amount: {coin}", balances) if token: - assert_in(f"Token: {token_id} amount: {token}", balances) + assert_in(f"Token: {token_id} ({token_ticker}), amount: {token}", balances) else: assert_not_in(token_id, balances) diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 5e91a872a..f89a192f4 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -636,7 +636,9 @@ where for (token_id, amount) in tokens { let amount = amount.decimal(); - writeln!(&mut output, "Token: {token_id} amount: {amount}") + // TODO: it'd be nice to print token tickers here too (as in GetBalance), + // when the wallet is in the hot mode. + writeln!(&mut output, "Token: {token_id}, amount: {amount}") .expect("Writing to a memory buffer should not fail"); } output.pop(); From f0f6010c09f170fb3a00beceb5e7bb843de65ac5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Sat, 13 Dec 2025 22:05:42 +0200 Subject: [PATCH 06/10] Chainstate RPC: use RpcAddress instead of String's in parameters (this only affects RPC docs); pool_decommission_destination now returns RpcAddress instead of Destination. General: a large TODO about RPC types was added; RpcCurrency was renamed to Currency and an actual RpcCurrency was introduced; Destination's value hint is now HEXIFIED_DEST instead of BECH32_STRING; `impl HasValueHint` for `Id` was removed, nowindividual Ids implement it separately, specifying the appropriate hint. --- CHANGELOG.md | 20 +- chainstate/src/detail/query.rs | 10 +- .../src/interface/chainstate_interface.rs | 6 +- .../interface/chainstate_interface_impl.rs | 6 +- .../chainstate_interface_impl_delegation.rs | 6 +- chainstate/src/rpc/mod.rs | 117 ++++++----- .../test-suite/src/tests/orders_tests.rs | 20 +- common/src/address/rpc.rs | 1 + common/src/chain/block/mod.rs | 4 + common/src/chain/currency.rs | 149 +++++++++++++ common/src/chain/gen_block.rs | 4 + common/src/chain/mod.rs | 4 +- common/src/chain/order/order_id.rs | 4 + common/src/chain/pos/delegation_id.rs | 4 + common/src/chain/pos/pool_id.rs | 4 + common/src/chain/rpc_currency.rs | 64 ------ common/src/chain/tokens/rpc.rs | 1 + common/src/chain/tokens/token_id.rs | 4 + common/src/chain/transaction/mod.rs | 4 + common/src/chain/transaction/output/mod.rs | 2 +- common/src/lib.rs | 196 ++++++++++++++++++ common/src/primitives/id/mod.rs | 4 - crypto/src/vrf/mod.rs | 2 + mocks/src/chainstate.rs | 6 +- node-daemon/docs/RPC.md | 48 ++--- rpc/description/src/value_hint.rs | 9 + wallet/src/account/currency_grouper/mod.rs | 3 +- wallet/src/account/mod.rs | 8 +- wallet/src/account/transaction_list/mod.rs | 4 +- wallet/src/send_request/mod.rs | 9 +- wallet/src/signer/tests/generic_tests.rs | 4 +- wallet/src/wallet/mod.rs | 11 +- wallet/src/wallet/tests.rs | 5 +- wallet/types/src/lib.rs | 4 +- .../src/command_handler/mod.rs | 9 +- wallet/wallet-cli-commands/src/errors.rs | 3 + .../wallet-cli-commands/src/helper_types.rs | 10 +- wallet/wallet-controller/src/helpers/mod.rs | 11 +- wallet/wallet-controller/src/lib.rs | 3 +- wallet/wallet-controller/src/read.rs | 4 +- .../wallet-controller/src/runtime_wallet.rs | 8 +- .../wallet-controller/src/sync/tests/mod.rs | 7 +- .../src/synced_controller.rs | 6 +- .../wallet-controller/src/tests/test_utils.rs | 5 +- .../src/handles_client/mod.rs | 6 +- wallet/wallet-node-client/src/mock.rs | 6 +- wallet/wallet-node-client/src/node_traits.rs | 6 +- .../src/rpc_client/client_impl.rs | 36 ++-- .../src/rpc_client/cold_wallet_client.rs | 6 +- .../src/handles_client/mod.rs | 8 +- .../src/rpc_client/client_impl.rs | 8 +- .../src/wallet_rpc_traits.rs | 8 +- wallet/wallet-rpc-daemon/docs/RPC.md | 10 +- wallet/wallet-rpc-lib/src/rpc/interface.rs | 8 +- wallet/wallet-rpc-lib/src/rpc/mod.rs | 20 +- wallet/wallet-rpc-lib/src/rpc/server_impl.rs | 8 +- 56 files changed, 653 insertions(+), 290 deletions(-) create mode 100644 common/src/chain/currency.rs delete mode 100644 common/src/chain/rpc_currency.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d03b9a58..a14572b6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,15 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ - The format of `PartiallySignedTransaction was changed again. - - Node RPC: the result of `chainstate_order_info` now also indicates whether the order is frozen. + - Node RPC: + - The result of `chainstate_order_info` now also indicates whether the order is frozen. + + - `chainstate_pool_decommission_destination` now returns a bech32 string instead of a hexified + destination (note that in the generated documentation its result was already incorrectly + designated as "bech32 string"; now the description is correct). + + - Documentation-only changes: + - Certain parameters that were designated as "string" are now designated as "bech32 string". ### Fixed - p2p: when a peer sends a message that can't be decoded, it will now be discouraged (which is what @@ -40,7 +48,15 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ - 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. - - Node RPC: `chainstate_order_info` will no longer fail if one of the order's balances became zero. + - Node RPC: + - `chainstate_order_info` will no longer fail if one of the order's balances became zero. + + - Documentation-only changes: + - Certain parameters and/or returned values that were previously (incorrectly) designated as + "hex string" are now designated as "hexified xxx id". + + - Parameters and/or returned values having the "plain" `Destination` type were incorrectly + designated as "bech32 string", while in reality they are "hexified destination". ## [1.2.0] - 2025-10-27 diff --git a/chainstate/src/detail/query.rs b/chainstate/src/detail/query.rs index a6754b196..12fafd2b4 100644 --- a/chainstate/src/detail/query.rs +++ b/chainstate/src/detail/query.rs @@ -28,7 +28,7 @@ use common::{ NftIssuance, RPCFungibleTokenInfo, RPCIsTokenFrozen, RPCNonFungibleTokenInfo, RPCTokenInfo, TokenAuxiliaryData, TokenId, }, - AccountType, Block, GenBlock, OrderId, RpcCurrency, RpcOrderInfo, Transaction, TxOutput, + AccountType, Block, Currency, GenBlock, OrderId, RpcOrderInfo, Transaction, TxOutput, }, primitives::{Amount, BlockDistance, BlockHeight, Id, Idable}, }; @@ -453,8 +453,8 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat pub fn get_orders_info_for_rpc_by_currencies( &self, - ask_currency: Option<&RpcCurrency>, - give_currency: Option<&RpcCurrency>, + ask_currency: Option<&Currency>, + give_currency: Option<&Currency>, ) -> Result, PropertyQueryError> { let order_ids = self.get_all_order_ids()?; @@ -495,8 +495,8 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat fn order_currency( order_id: &OrderId, value: &OutputValue, - ) -> Result { - RpcCurrency::from_output_value(value) + ) -> Result { + Currency::from_output_value(value) .ok_or(PropertyQueryError::UnsupportedTokenV0InOrder(*order_id)) } diff --git a/chainstate/src/interface/chainstate_interface.rs b/chainstate/src/interface/chainstate_interface.rs index 370a35a0a..ad7abcc0f 100644 --- a/chainstate/src/interface/chainstate_interface.rs +++ b/chainstate/src/interface/chainstate_interface.rs @@ -31,7 +31,7 @@ use common::{ GenBlock, }, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, ChainConfig, DelegationId, OrderId, PoolId, RpcCurrency, + AccountNonce, AccountType, ChainConfig, Currency, DelegationId, OrderId, PoolId, RpcOrderInfo, Transaction, TxInput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, @@ -247,8 +247,8 @@ pub trait ChainstateInterface: Send + Sync { /// means "any currency". fn get_orders_info_for_rpc_by_currencies( &self, - ask_currency: Option<&RpcCurrency>, - give_currency: Option<&RpcCurrency>, + ask_currency: Option<&Currency>, + give_currency: Option<&Currency>, ) -> Result, ChainstateError>; /// Returns the coin amounts of the outpoints spent by a transaction. diff --git a/chainstate/src/interface/chainstate_interface_impl.rs b/chainstate/src/interface/chainstate_interface_impl.rs index e61576d31..df3afef17 100644 --- a/chainstate/src/interface/chainstate_interface_impl.rs +++ b/chainstate/src/interface/chainstate_interface_impl.rs @@ -39,7 +39,7 @@ use common::{ block::{signed_block_header::SignedBlockHeader, Block, BlockReward, GenBlock}, config::ChainConfig, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, DelegationId, OrderId, PoolId, RpcCurrency, RpcOrderInfo, + AccountNonce, AccountType, Currency, DelegationId, OrderId, PoolId, RpcOrderInfo, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{id::WithId, Amount, BlockHeight, Id, Idable}, @@ -863,8 +863,8 @@ where #[tracing::instrument(skip_all, fields(ask_currency, give_currency))] fn get_orders_info_for_rpc_by_currencies( &self, - ask_currency: Option<&RpcCurrency>, - give_currency: Option<&RpcCurrency>, + ask_currency: Option<&Currency>, + give_currency: Option<&Currency>, ) -> Result, ChainstateError> { self.chainstate .query() diff --git a/chainstate/src/interface/chainstate_interface_impl_delegation.rs b/chainstate/src/interface/chainstate_interface_impl_delegation.rs index 69dbb91bc..a0fae05bb 100644 --- a/chainstate/src/interface/chainstate_interface_impl_delegation.rs +++ b/chainstate/src/interface/chainstate_interface_impl_delegation.rs @@ -26,7 +26,7 @@ use common::{ block::{signed_block_header::SignedBlockHeader, timestamp::BlockTimestamp, BlockReward}, config::ChainConfig, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, Block, DelegationId, GenBlock, OrderId, PoolId, RpcCurrency, + AccountNonce, AccountType, Block, Currency, DelegationId, GenBlock, OrderId, PoolId, RpcOrderInfo, Transaction, TxInput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, @@ -443,8 +443,8 @@ where fn get_orders_info_for_rpc_by_currencies( &self, - ask_currency: Option<&RpcCurrency>, - give_currency: Option<&RpcCurrency>, + ask_currency: Option<&Currency>, + give_currency: Option<&Currency>, ) -> Result, ChainstateError> { self.deref().get_orders_info_for_rpc_by_currencies(ask_currency, give_currency) } diff --git a/chainstate/src/rpc/mod.rs b/chainstate/src/rpc/mod.rs index 02967cc1b..d1e2a8b2b 100644 --- a/chainstate/src/rpc/mod.rs +++ b/chainstate/src/rpc/mod.rs @@ -27,7 +27,7 @@ use std::{ use chainstate_types::BlockIndex; use common::{ - address::{dehexify::to_dehexified_json, Address}, + address::{dehexify::to_dehexified_json, Address, RpcAddress}, chain::{ output_values_holder::collect_token_v1_ids_from_output_values_holder, tokens::{RPCTokenInfo, TokenId}, @@ -146,14 +146,17 @@ trait ChainstateRpc { /// The balance contains both delegated balance and staker balance. /// Returns `None` (null) if the pool is not found. #[method(name = "stake_pool_balance")] - async fn stake_pool_balance(&self, pool_address: String) -> RpcResult>; + async fn stake_pool_balance( + &self, + pool_address: RpcAddress, + ) -> RpcResult>; /// Returns the balance of the staker (pool owner) of the pool associated with the given pool address. /// /// This excludes the delegation balances. /// Returns `None` (null) if the pool is not found. #[method(name = "staker_balance")] - async fn staker_balance(&self, pool_address: String) -> RpcResult>; + async fn staker_balance(&self, pool_address: RpcAddress) -> RpcResult>; /// Returns the pool's decommission destination associated with the given pool address. /// @@ -161,29 +164,32 @@ trait ChainstateRpc { #[method(name = "pool_decommission_destination")] async fn pool_decommission_destination( &self, - pool_address: String, - ) -> RpcResult>; + pool_address: RpcAddress, + ) -> RpcResult>>; /// Given a pool defined by a pool address, and a delegation address, /// returns the amount of coins owned by that delegation in that pool. #[method(name = "delegation_share")] async fn delegation_share( &self, - pool_address: String, - delegation_address: String, + pool_address: RpcAddress, + delegation_address: RpcAddress, ) -> RpcResult>; /// Get token information, given a token id in address form. #[method(name = "token_info")] - async fn token_info(&self, token_id: String) -> RpcResult>; + async fn token_info(&self, token_id: RpcAddress) -> RpcResult>; /// Get tokens information, given multiple token ids in address form. #[method(name = "tokens_info")] - async fn tokens_info(&self, token_ids: Vec) -> RpcResult>; + async fn tokens_info( + &self, + token_ids: Vec>, + ) -> RpcResult>; /// Get order information, given an order id, in address form. #[method(name = "order_info")] - async fn order_info(&self, order_id: String) -> RpcResult>; + async fn order_info(&self, order_id: RpcAddress) -> RpcResult>; /// Return infos for all orders that match the given currencies. Passing None for a currency /// means "any currency". @@ -363,24 +369,26 @@ impl ChainstateRpcServer for super::ChainstateHandle { ) } - async fn stake_pool_balance(&self, pool_address: String) -> RpcResult> { + async fn stake_pool_balance( + &self, + pool_address: RpcAddress, + ) -> RpcResult> { rpc::handle_result( self.call(move |this| { let chain_config = this.get_chain_config(); - let id_result = Address::::from_string(chain_config, pool_address); - id_result.map(|address| this.get_stake_pool_balance(address.into_object())) + let id_result = pool_address.decode_object(chain_config); + id_result.map(|address| this.get_stake_pool_balance(address)) }) .await, ) } - async fn staker_balance(&self, pool_address: String) -> RpcResult> { + async fn staker_balance(&self, pool_address: RpcAddress) -> RpcResult> { rpc::handle_result( self.call(move |this| { let chain_config = this.get_chain_config(); let result: Result, _> = - dynamize_err(Address::::from_string(chain_config, pool_address)) - .map(|address| address.into_object()) + dynamize_err(pool_address.decode_object(chain_config)) .and_then(|pool_id| dynamize_err(this.get_stake_pool_data(pool_id))) .and_then(|pool_data| { dynamize_err(pool_data.map(|d| d.staker_balance()).transpose()) @@ -394,18 +402,24 @@ impl ChainstateRpcServer for super::ChainstateHandle { async fn pool_decommission_destination( &self, - pool_address: String, - ) -> RpcResult> { + pool_address: RpcAddress, + ) -> RpcResult>> { rpc::handle_result( - self.call(move |this| { + self.call(move |this| -> Result<_, DynamizedError> { let chain_config = this.get_chain_config(); - let result: Result, _> = - dynamize_err(Address::::from_string(chain_config, pool_address)) - .map(|address| address.into_object()) - .and_then(|pool_id| dynamize_err(this.get_stake_pool_data(pool_id))) - .map(|pool_data| pool_data.map(|d| d.decommission_destination().clone())); + let pool_id = dynamize_err(pool_address.decode_object(chain_config))?; + let pool_data = dynamize_err(this.get_stake_pool_data(pool_id))?; - result + pool_data + .map(|d| -> Result<_, DynamizedError> { + let addr = dynamize_err(Address::new( + &chain_config, + d.decommission_destination().clone(), + ))?; + + Ok(addr.into()) + }) + .transpose() }) .await, ) @@ -413,22 +427,16 @@ impl ChainstateRpcServer for super::ChainstateHandle { async fn delegation_share( &self, - pool_address: String, - delegation_address: String, + pool_address: RpcAddress, + delegation_address: RpcAddress, ) -> RpcResult> { rpc::handle_result( self.call(move |this| { let chain_config = this.get_chain_config(); - let pool_id_result = - dynamize_err(Address::::from_string(chain_config, &pool_address)) - .map(|address| address.into_object()); - - let delegation_id_result = dynamize_err(Address::::from_string( - chain_config, - &delegation_address, - )) - .map(|address| address.into_object()); + let pool_id_result = dynamize_err(pool_address.decode_object(chain_config)); + let delegation_id_result = + dynamize_err(delegation_address.decode_object(chain_config)); let ids = pool_id_result.and_then(|x| delegation_id_result.map(|y| (x, y))); @@ -440,13 +448,12 @@ impl ChainstateRpcServer for super::ChainstateHandle { ) } - async fn token_info(&self, token_id: String) -> RpcResult> { + async fn token_info(&self, token_id: RpcAddress) -> RpcResult> { rpc::handle_result( self.call(move |this| { let chain_config = this.get_chain_config(); let token_info_result: Result, _> = - dynamize_err(Address::::from_string(chain_config, token_id)) - .map(|address| address.into_object()) + dynamize_err(token_id.decode_object(&chain_config)) .and_then(|token_id| dynamize_err(this.get_token_info_for_rpc(token_id))); token_info_result @@ -455,7 +462,10 @@ impl ChainstateRpcServer for super::ChainstateHandle { ) } - async fn tokens_info(&self, token_ids: Vec) -> RpcResult> { + async fn tokens_info( + &self, + token_ids: Vec>, + ) -> RpcResult> { rpc::handle_result( self.call(move |this| -> Result<_, DynamizedError> { let chain_config = this.get_chain_config(); @@ -463,10 +473,7 @@ impl ChainstateRpcServer for super::ChainstateHandle { let token_ids = token_ids .into_iter() .map(|token_id| -> Result<_, DynamizedError> { - Ok( - dynamize_err(Address::::from_string(chain_config, token_id))? - .into_object(), - ) + Ok(token_id.decode_object(&chain_config)?) }) .collect::>()?; @@ -476,13 +483,12 @@ impl ChainstateRpcServer for super::ChainstateHandle { ) } - async fn order_info(&self, order_id: String) -> RpcResult> { + async fn order_info(&self, order_id: RpcAddress) -> RpcResult> { rpc::handle_result( self.call(move |this| { let chain_config = this.get_chain_config(); let result: Result, _> = - dynamize_err(Address::::from_string(chain_config, order_id)) - .map(|address| address.into_object()) + dynamize_err(order_id.decode_object(&chain_config)) .and_then(|order_id| dynamize_err(this.get_order_info_for_rpc(&order_id))); result @@ -497,11 +503,18 @@ impl ChainstateRpcServer for super::ChainstateHandle { give_currency: Option, ) -> RpcResult> { rpc::handle_result( - self.call(move |this| { - this.get_orders_info_for_rpc_by_currencies( - ask_currency.as_ref(), - give_currency.as_ref(), - ) + self.call(move |this| -> Result<_, DynamizedError> { + let chain_config = this.get_chain_config(); + Ok(this.get_orders_info_for_rpc_by_currencies( + ask_currency + .map(|rpc_currency| dynamize_err(rpc_currency.to_currency(chain_config))) + .transpose()? + .as_ref(), + give_currency + .map(|rpc_currency| dynamize_err(rpc_currency.to_currency(chain_config))) + .transpose()? + .as_ref(), + )) }) .await, ) diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index b76a1f167..d0bbb6d9b 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -41,9 +41,9 @@ use common::{ verify_signature, DestinationSigError, EvaluatedInputWitness, }, tokens::{IsTokenFreezable, TokenId, TokenTotalSupply}, - AccountCommand, AccountNonce, AccountType, ChainstateUpgradeBuilder, Destination, - IdCreationError, OrderAccountCommand, OrderData, OrderId, OrdersVersion, RpcCurrency, - RpcOrderInfo, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, + AccountCommand, AccountNonce, AccountType, ChainstateUpgradeBuilder, Currency, Destination, + IdCreationError, OrderAccountCommand, OrderData, OrderId, OrdersVersion, RpcOrderInfo, + SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Idable, H256}, }; @@ -209,8 +209,8 @@ fn assert_order_exists( assert_eq!(all_infos_for_rpc.len(), 1); } - let ask_currency = RpcCurrency::from_output_value(expected_data.initial_data.ask()).unwrap(); - let give_currency = RpcCurrency::from_output_value(expected_data.initial_data.give()).unwrap(); + let ask_currency = Currency::from_output_value(expected_data.initial_data.ask()).unwrap(); + let give_currency = Currency::from_output_value(expected_data.initial_data.give()).unwrap(); // Check get_orders_info_for_rpc_by_currencies when all currency filters match or are None - // the order should be present in the result @@ -240,12 +240,12 @@ fn assert_order_exists( } match currency { - RpcCurrency::Coin => RpcCurrency::Token(TokenId::random_using(rng)), - RpcCurrency::Token(_) => { + Currency::Coin => Currency::Token(TokenId::random_using(rng)), + Currency::Token(_) => { if rng.gen_bool(0.5) { - RpcCurrency::Coin + Currency::Coin } else { - RpcCurrency::Token(TokenId::random_using(rng)) + Currency::Token(TokenId::random_using(rng)) } } } @@ -4701,7 +4701,7 @@ fn get_orders_info_for_rpc_by_currencies_test(#[case] seed: Seed) { // Use unqualified names of the currencies to prevent rustfmt from turning a one-liner check // into 5 lines (note that having a shorter alias like Curr doesn't always help). - use RpcCurrency::*; + use Currency::*; // Get all orders { diff --git a/common/src/address/rpc.rs b/common/src/address/rpc.rs index 923d52d49..b2537850a 100644 --- a/common/src/address/rpc.rs +++ b/common/src/address/rpc.rs @@ -48,6 +48,7 @@ impl RpcAddress { } impl RpcAddress { + // TODO: this function should accept the object by ref, to avoid redundant cloning. /// Construct from an addressable object pub fn new(cfg: &ChainConfig, object: T) -> Result { Ok(Self::from_address(Address::new(cfg, object)?)) diff --git a/common/src/chain/block/mod.rs b/common/src/chain/block/mod.rs index 92b9e54fb..94ac70514 100644 --- a/common/src/chain/block/mod.rs +++ b/common/src/chain/block/mod.rs @@ -252,6 +252,10 @@ impl OutputValuesHolder for Block { } } +impl rpc_description::HasValueHint for Id { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEX_STRING; +} + #[cfg(test)] mod tests { use crate::{ diff --git a/common/src/chain/currency.rs b/common/src/chain/currency.rs new file mode 100644 index 000000000..1429270f3 --- /dev/null +++ b/common/src/chain/currency.rs @@ -0,0 +1,149 @@ +// 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 crate::{ + address::{AddressError, RpcAddress}, + chain::{ + output_value::{OutputValue, RpcOutputValue}, + tokens::TokenId, + ChainConfig, + }, + primitives::Amount, +}; + +// TODO: currently out RPC types are a bit of a mess and we need to revamp them. +// The reason for having RPC types in the first place is that in RPC we'd like for certain things to have a more +// human-readable representation, namely: +// 1) Destinations, VRF public keys and ids of pools/delegations/tokens/orders should be bech32-encoded instead +// of hex-encoded or "hexified" via `HexifiedAddress`. For this purpose we have the `RpcAddress` wrapper +// (which holds a bech-32 encoded string), so e.g. `RpcAddress` should be used instead of the plain +// PoolId in RPC types. +// 2) Amounts are more readable when they are in the decimal form instead of the plain number of atoms. But we also +// have to differentiate between input and output amounts, because for inputs we want the amount to be *either* atoms +// or a decimal value, but for outputs we want *both* the atoms and the decimal value. For this reason we have +// `RpcAmountIn`/`RpcOutputValueIn` and `RpcAmountOut`/`RpcOutputValueOut`. +// +// About the mess: +// 1) We also have the `RpcOutputValue` type which is just the normal `OutputValue` but without the deprecated +// `TokenV0` variant, i.e. it has a plain `TokenId` and plain `Amount`'s. Normally, this type should have been named +// `OutputValueV1` (and it could become just `OutputValue` if we decide to get rid of TokenV0 completely, though this +// would require to restart the testnet). However, it actually implements `rpc_description::HasValueHint` and is used +// in RPC types. +// 2) Many RPC types contain plain `Destination`'s and/or ids, e.g. `RPCFungibleTokenInfo` and `RpcOrderInfo`. +// Many RPC types also contain plain `Amount`'s and `RpcOutputValue`'s, e.g. `RpcOrderInfo` and RPCTokenTotalSupply`. +// 3) In the wallet some RPC types have the "Rpc" prefix and some don't. E.g. `RpcStandaloneAddressDetail`s and +// `StandaloneAddressWithDetails` are both RPC types. Also, sometimes they use plain String's instead of `RpcAddress`. +// Also, some of the types are quite generic, e.g. `TokenTotalSupply` (which should have been named +// `RpcTokenTotalSupplyIn` and moved out of the wallet). +// +// What should we do: +// 1) The "RPC" prefix should be replace with "Rpc" to honor Rust's naming conventions. +// 2) `RpcOutputValue` should be renamed to `OutputValueV1` and it should *not* implement `HasValueHint`. +// Also, all functions that take `RpcOutputValue` and are named as such (e.g. `from_rpc_output_value` below) +// should be renamed as well. +// 3) `Destination` and ids of pools/delegations/tokens/orders should *not* implement `HasValueHint` either +// (see also the TODO inside `impl ValueHint` in `rpc/description/src/value_hint.rs`); `RpcAddress` should be used +// in all RPC types instead. +// 4) RPC types should not contain plain `Amount`'s and `OutputValue`'s; instead, they should contain the corresponding +// "Rpc...In" or "...Out" type. Which in turn means that RPC types that contain an amount or an output value must +// themselves be designated as "In" or "Out" and have the corresponding suffix. +// 5) We also need to reconsider where the RPC types live. Currently many of them live in `common`, even though they're +// only used in the chainstate rpc, but lots of others live in `chainstate`, see the contents of the `chainstate/src/rpc/types/` +// folder. One approach would be to put an RPC type into the same crate as its non-RPC counterpart. Another approach +// is to put them all to chainstate (note that blockprod and mempool depend on chainstate, so we'll be able to use +// chainstate types in their RPC interfaces if needed). +// 6) All types that implement `HasValueHint` should have a specific prefix designating them as RPC types. Normally, +// it would be "Rpc", but for wallet types we could consider a different prefix, e.g. "WalletRpc", "Wrpc" or something +// like that. The reason is that wallet RPC may potentially need a special wallet-only version of a generic RPC type. +// Also, in general, wallet RPC types are rather specific, so it might be better to differentiate them from the generic ones. +// 7) Some of the wallet's RPC types (like the above-mentioned `TokenTotalSupply`) should be moved outside the wallet. +// 8) We should also ensure that RPC types don't contain plain String's where an RpcAddress can be used (this affects +// how the corresponding value is referred to in the generated RPC documentation; also, it has less potential for mis-use). + +#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)] +pub enum Currency { + Coin, + Token(TokenId), +} + +impl Currency { + pub fn from_output_value(output_value: &OutputValue) -> Option { + match output_value { + OutputValue::Coin(_) => Some(Self::Coin), + OutputValue::TokenV0(_) => None, + OutputValue::TokenV1(id, _) => Some(Self::Token(*id)), + } + } + + pub fn from_rpc_output_value(output_value: &RpcOutputValue) -> Self { + match output_value { + RpcOutputValue::Coin { .. } => Self::Coin, + RpcOutputValue::Token { id, .. } => Self::Token(*id), + } + } + + pub fn into_output_value(&self, amount: Amount) -> OutputValue { + match self { + Self::Coin => OutputValue::Coin(amount), + Self::Token(id) => OutputValue::TokenV1(*id, amount), + } + } + + pub fn to_rpc_currency(&self, chain_config: &ChainConfig) -> Result { + RpcCurrency::from_currency(self, chain_config) + } +} + +#[derive( + PartialEq, + Eq, + PartialOrd, + Ord, + Clone, + Debug, + serde::Serialize, + serde::Deserialize, + rpc_description::HasValueHint, +)] +#[serde(tag = "type", content = "content")] +pub enum RpcCurrency { + Coin, + Token(RpcAddress), +} + +impl RpcCurrency { + pub fn to_currency(&self, chain_config: &ChainConfig) -> Result { + let result = match self { + RpcCurrency::Coin => Currency::Coin, + RpcCurrency::Token(rpc_address) => { + Currency::Token(rpc_address.decode_object(chain_config)?) + } + }; + + Ok(result) + } + + pub fn from_currency( + currency: &Currency, + chain_config: &ChainConfig, + ) -> Result { + let result = match currency { + Currency::Coin => Self::Coin, + Currency::Token(id) => Self::Token(RpcAddress::new(chain_config, *id)?), + }; + + Ok(result) + } +} diff --git a/common/src/chain/gen_block.rs b/common/src/chain/gen_block.rs index f951b38a1..6c40b9d61 100644 --- a/common/src/chain/gen_block.rs +++ b/common/src/chain/gen_block.rs @@ -99,6 +99,10 @@ impl Id { } } +impl rpc_description::HasValueHint for Id { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEX_STRING; +} + /// Classified generalized block #[derive(Eq, PartialEq, Clone, Debug)] pub enum GenBlockId { diff --git a/common/src/chain/mod.rs b/common/src/chain/mod.rs index e4bcf18a7..8585b6a84 100644 --- a/common/src/chain/mod.rs +++ b/common/src/chain/mod.rs @@ -23,11 +23,11 @@ pub mod tokens; pub mod transaction; mod coin_unit; +mod currency; mod make_id; mod order; mod pos; mod pow; -mod rpc_currency; mod upgrades; pub use signed_transaction::SignedTransaction; @@ -36,6 +36,7 @@ pub use transaction::*; pub use block::Block; pub use coin_unit::CoinUnit; pub use config::ChainConfig; +pub use currency::{Currency, RpcCurrency}; pub use gen_block::{GenBlock, GenBlockId}; pub use genesis::Genesis; pub use make_id::{ @@ -48,5 +49,4 @@ pub use pos::{ get_initial_randomness, pool_id::PoolId, pos_initial_difficulty, PoSConsensusVersion, }; pub use pow::{PoWChainConfig, PoWChainConfigBuilder}; -pub use rpc_currency::RpcCurrency; pub use upgrades::*; diff --git a/common/src/chain/order/order_id.rs b/common/src/chain/order/order_id.rs index 73dd2ab55..473826aed 100644 --- a/common/src/chain/order/order_id.rs +++ b/common/src/chain/order/order_id.rs @@ -61,3 +61,7 @@ impl<'de> serde::Deserialize<'de> for OrderId { HexifiedAddress::::serde_deserialize(deserializer) } } + +impl rpc_description::HasValueHint for OrderId { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEXIFIED_ORDER_ID; +} diff --git a/common/src/chain/pos/delegation_id.rs b/common/src/chain/pos/delegation_id.rs index 8a85d5d98..d041bdffa 100644 --- a/common/src/chain/pos/delegation_id.rs +++ b/common/src/chain/pos/delegation_id.rs @@ -61,3 +61,7 @@ impl<'de> serde::Deserialize<'de> for DelegationId { HexifiedAddress::::serde_deserialize(deserializer) } } + +impl rpc_description::HasValueHint for DelegationId { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEXIFIED_DELEGATION_ID; +} diff --git a/common/src/chain/pos/pool_id.rs b/common/src/chain/pos/pool_id.rs index b5e36a427..d1961aff2 100644 --- a/common/src/chain/pos/pool_id.rs +++ b/common/src/chain/pos/pool_id.rs @@ -62,3 +62,7 @@ impl Addressable for PoolId { "HexifiedPoolId" } } + +impl rpc_description::HasValueHint for PoolId { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEXIFIED_POOL_ID; +} diff --git a/common/src/chain/rpc_currency.rs b/common/src/chain/rpc_currency.rs deleted file mode 100644 index 1d11f2f6c..000000000 --- a/common/src/chain/rpc_currency.rs +++ /dev/null @@ -1,64 +0,0 @@ -// 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 crate::{ - chain::{ - output_value::{OutputValue, RpcOutputValue}, - tokens::TokenId, - }, - primitives::Amount, -}; - -#[derive( - PartialEq, - Eq, - PartialOrd, - Ord, - Copy, - Clone, - Debug, - serde::Serialize, - serde::Deserialize, - rpc_description::HasValueHint, -)] -#[serde(tag = "type", content = "content")] -pub enum RpcCurrency { - Coin, - Token(TokenId), -} - -impl RpcCurrency { - pub fn from_output_value(output_value: &OutputValue) -> Option { - match output_value { - OutputValue::Coin(_) => Some(Self::Coin), - OutputValue::TokenV0(_) => None, - OutputValue::TokenV1(id, _) => Some(Self::Token(*id)), - } - } - - pub fn from_rpc_output_value(output_value: &RpcOutputValue) -> Self { - match output_value { - RpcOutputValue::Coin { .. } => Self::Coin, - RpcOutputValue::Token { id, .. } => Self::Token(*id), - } - } - - pub fn into_output_value(&self, amount: Amount) -> OutputValue { - match self { - Self::Coin => OutputValue::Coin(amount), - Self::Token(id) => OutputValue::TokenV1(*id, amount), - } - } -} diff --git a/common/src/chain/tokens/rpc.rs b/common/src/chain/tokens/rpc.rs index 3d24804c1..194956ff1 100644 --- a/common/src/chain/tokens/rpc.rs +++ b/common/src/chain/tokens/rpc.rs @@ -151,6 +151,7 @@ impl RPCFungibleTokenInfo { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, HasValueHint)] pub struct RPCNonFungibleTokenInfo { + // TODO: same as in RPCFungibleTokenInfo, use RpcAddress here. pub token_id: TokenId, pub creation_tx_id: Id, pub creation_block_id: Id, diff --git a/common/src/chain/tokens/token_id.rs b/common/src/chain/tokens/token_id.rs index bff6a1655..46067f2f4 100644 --- a/common/src/chain/tokens/token_id.rs +++ b/common/src/chain/tokens/token_id.rs @@ -61,3 +61,7 @@ impl<'de> serde::Deserialize<'de> for TokenId { HexifiedAddress::::serde_deserialize(deserializer) } } + +impl rpc_description::HasValueHint for TokenId { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEXIFIED_TOKEN_ID; +} diff --git a/common/src/chain/transaction/mod.rs b/common/src/chain/transaction/mod.rs index 7172d16f3..ea6532bd1 100644 --- a/common/src/chain/transaction/mod.rs +++ b/common/src/chain/transaction/mod.rs @@ -195,6 +195,10 @@ impl OutputValuesHolder for Transaction { } } +impl rpc_description::HasValueHint for Id { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEX_STRING; +} + #[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 db5fcd6d3..9e090b120 100644 --- a/common/src/chain/transaction/output/mod.rs +++ b/common/src/chain/transaction/output/mod.rs @@ -76,7 +76,7 @@ impl<'de> serde::Deserialize<'de> for Destination { } impl rpc_description::HasValueHint for Destination { - const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::BECH32_STRING; + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEXIFIED_DEST; } impl Addressable for Destination { diff --git a/common/src/lib.rs b/common/src/lib.rs index 0a70bc6db..932c116c9 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -25,8 +25,204 @@ pub use uint::{Uint128, Uint256, Uint512, UintConversionError}; #[cfg(test)] mod tests { + use std::str::FromStr as _; + + use crypto::vrf::VRFPublicKey; + use hex::FromHex; + use rpc_description::HasValueHint; + use serialization::DecodeAll as _; + + use crate::{ + address::pubkeyhash::PublicKeyHash, + chain::{ + tokens::TokenId, Block, DelegationId, Destination, GenBlock, OrderId, PoolId, + Transaction, + }, + primitives::{Id, H256}, + }; + #[ctor::ctor] fn init() { logging::init_logging(); } + + #[test] + fn basic_serialization_test() { + let hash256 = + H256::from_str("00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF") + .unwrap(); + + // Destination + { + type TypeToTest = Destination; + let val = Destination::PublicKeyHash( + PublicKeyHash::from_str("0011223344556677889900112233445566778899").unwrap(), + ); + + let serialized_val = serde_json::to_string(&val).unwrap(); + assert_eq!( + serialized_val, + r#""HexifiedDestination{0x010011223344556677889900112233445566778899}""# + ); + + let expected_value_hint = rpc_description::ValueHint::Prim("hexified destination"); + assert_eq!(TypeToTest::HINT_SER, expected_value_hint); + assert_eq!(TypeToTest::HINT_DE, expected_value_hint); + + let deserialized_val = serde_json::from_str::(&serialized_val).unwrap(); + assert_eq!(deserialized_val, val); + } + + // Pool id + { + type TypeToTest = PoolId; + let val = TypeToTest::new(hash256); + + let serialized_val = serde_json::to_string(&val).unwrap(); + assert_eq!( + serialized_val, + r#""HexifiedPoolId{0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff}""# + ); + + let expected_value_hint = rpc_description::ValueHint::Prim("hexified pool id"); + assert_eq!(TypeToTest::HINT_SER, expected_value_hint); + assert_eq!(TypeToTest::HINT_DE, expected_value_hint); + + let deserialized_val = serde_json::from_str::(&serialized_val).unwrap(); + assert_eq!(deserialized_val, val); + } + + // Delegation id + { + type TypeToTest = DelegationId; + let val = TypeToTest::new(hash256); + + let serialized_val = serde_json::to_string(&val).unwrap(); + assert_eq!( + serialized_val, + r#""HexifiedDelegationId{0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff}""# + ); + + let expected_value_hint = rpc_description::ValueHint::Prim("hexified delegation id"); + assert_eq!(TypeToTest::HINT_SER, expected_value_hint); + assert_eq!(TypeToTest::HINT_DE, expected_value_hint); + + let deserialized_val = serde_json::from_str::(&serialized_val).unwrap(); + assert_eq!(deserialized_val, val); + } + + // Token id + { + type TypeToTest = TokenId; + let val = TypeToTest::new(hash256); + + let serialized_val = serde_json::to_string(&val).unwrap(); + assert_eq!( + serialized_val, + r#""HexifiedTokenId{0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff}""# + ); + + let expected_value_hint = rpc_description::ValueHint::Prim("hexified token id"); + assert_eq!(TypeToTest::HINT_SER, expected_value_hint); + assert_eq!(TypeToTest::HINT_DE, expected_value_hint); + + let deserialized_val = serde_json::from_str::(&serialized_val).unwrap(); + assert_eq!(deserialized_val, val); + } + + // Order id + { + type TypeToTest = OrderId; + let val = TypeToTest::new(hash256); + + let serialized_val = serde_json::to_string(&val).unwrap(); + assert_eq!( + serialized_val, + r#""HexifiedOrderId{0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff}""# + ); + + let expected_value_hint = rpc_description::ValueHint::Prim("hexified order id"); + assert_eq!(TypeToTest::HINT_SER, expected_value_hint); + assert_eq!(TypeToTest::HINT_DE, expected_value_hint); + + let deserialized_val = serde_json::from_str::(&serialized_val).unwrap(); + assert_eq!(deserialized_val, val); + } + + // Block id + { + type TypeToTest = Id; + let val = TypeToTest::new(hash256); + + let serialized_val = serde_json::to_string(&val).unwrap(); + assert_eq!( + serialized_val, + r#""00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff""# + ); + + let expected_value_hint = rpc_description::ValueHint::Prim("hex string"); + assert_eq!(TypeToTest::HINT_SER, expected_value_hint); + assert_eq!(TypeToTest::HINT_DE, expected_value_hint); + + let deserialized_val = serde_json::from_str::(&serialized_val).unwrap(); + assert_eq!(deserialized_val, val); + } + + // GenBlock id + { + type TypeToTest = Id; + let val = TypeToTest::new(hash256); + + let serialized_val = serde_json::to_string(&val).unwrap(); + assert_eq!( + serialized_val, + r#""00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff""# + ); + + let expected_value_hint = rpc_description::ValueHint::Prim("hex string"); + assert_eq!(TypeToTest::HINT_SER, expected_value_hint); + assert_eq!(TypeToTest::HINT_DE, expected_value_hint); + + let deserialized_val = serde_json::from_str::(&serialized_val).unwrap(); + assert_eq!(deserialized_val, val); + } + + // Transaction id + { + type TypeToTest = Id; + let val = TypeToTest::new(hash256); + + let serialized_val = serde_json::to_string(&val).unwrap(); + assert_eq!( + serialized_val, + r#""00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff""# + ); + + let expected_value_hint = rpc_description::ValueHint::Prim("hex string"); + assert_eq!(TypeToTest::HINT_SER, expected_value_hint); + assert_eq!(TypeToTest::HINT_DE, expected_value_hint); + + let deserialized_val = serde_json::from_str::(&serialized_val).unwrap(); + assert_eq!(deserialized_val, val); + } + + // Additionally check VRFPublicKey, which is currently serialized as plain hex + // and doesn't have a ValueHint. + { + let pk_encoded: Vec = FromHex::from_hex( + "00c0158e93e3904b404a12f56493802f3a325939fa780dc0fc415370599be27c68", + ) + .unwrap(); + let pk = VRFPublicKey::decode_all(&mut pk_encoded.as_slice()).unwrap(); + + let serialized_pk = serde_json::to_string(&pk).unwrap(); + assert_eq!( + serialized_pk, + r#""00c0158e93e3904b404a12f56493802f3a325939fa780dc0fc415370599be27c68""# + ); + + let deserialized_pk = serde_json::from_str::(&serialized_pk).unwrap(); + assert_eq!(deserialized_pk, pk); + } + } } diff --git a/common/src/primitives/id/mod.rs b/common/src/primitives/id/mod.rs index 2072cd0a9..114972631 100644 --- a/common/src/primitives/id/mod.rs +++ b/common/src/primitives/id/mod.rs @@ -236,10 +236,6 @@ impl AsRef<[u8]> for Id { } } -impl rpc_description::HasValueHint for Id { - const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEX_STRING; -} - /// a trait for objects that deserve having a unique id with implementations to how to ID them pub trait Idable { type Tag: TypeName; diff --git a/crypto/src/vrf/mod.rs b/crypto/src/vrf/mod.rs index 98269fc52..96bb92346 100644 --- a/crypto/src/vrf/mod.rs +++ b/crypto/src/vrf/mod.rs @@ -72,6 +72,8 @@ pub struct VRFPublicKey { pub_key: VRFPublicKeyHolder, } +// TODO: since VRFPublicKey implements Addressable (see common/src/address/traits.rs), +// perhaps it should be serialized as HexifiedAddress instead of plain hex. impl serde::Serialize for VRFPublicKey { fn serialize(&self, serializer: S) -> Result { HexEncoded::new(self).serialize(serializer) diff --git a/mocks/src/chainstate.rs b/mocks/src/chainstate.rs index 1d6b84c1f..b63ade8fe 100644 --- a/mocks/src/chainstate.rs +++ b/mocks/src/chainstate.rs @@ -30,7 +30,7 @@ use common::{ GenBlock, }, tokens::{RPCTokenInfo, TokenAuxiliaryData, TokenId}, - AccountNonce, AccountType, ChainConfig, DelegationId, OrderId, PoolId, RpcCurrency, + AccountNonce, AccountType, ChainConfig, Currency, DelegationId, OrderId, PoolId, RpcOrderInfo, TxInput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Id}, @@ -213,8 +213,8 @@ mockall::mock! { fn get_all_order_ids(&self) -> Result, ChainstateError>; fn get_orders_info_for_rpc_by_currencies<'a>( &self, - ask_currency: Option<&'a RpcCurrency>, - give_currency: Option<&'a RpcCurrency>, + ask_currency: Option<&'a Currency>, + give_currency: Option<&'a Currency>, ) -> Result, ChainstateError>; } } diff --git a/node-daemon/docs/RPC.md b/node-daemon/docs/RPC.md index d4a427157..affe56fca 100644 --- a/node-daemon/docs/RPC.md +++ b/node-daemon/docs/RPC.md @@ -317,7 +317,7 @@ Returns `None` (null) if the pool is not found. Parameters: ``` -{ "pool_address": string } +{ "pool_address": bech32 string } ``` Returns: @@ -337,7 +337,7 @@ Returns `None` (null) if the pool is not found. Parameters: ``` -{ "pool_address": string } +{ "pool_address": bech32 string } ``` Returns: @@ -356,7 +356,7 @@ Returns `None` (null) if the pool is not found. Parameters: ``` -{ "pool_address": string } +{ "pool_address": bech32 string } ``` Returns: @@ -375,8 +375,8 @@ returns the amount of coins owned by that delegation in that pool. Parameters: ``` { - "pool_address": string, - "delegation_address": string, + "pool_address": bech32 string, + "delegation_address": bech32 string, } ``` @@ -394,7 +394,7 @@ Get token information, given a token id in address form. Parameters: ``` -{ "token_id": string } +{ "token_id": bech32 string } ``` Returns: @@ -403,7 +403,7 @@ EITHER OF 1) { "type": "FungibleToken", "content": { - "token_id": hex string, + "token_id": hexified token id, "token_ticker": { "text": EITHER OF 1) string @@ -435,13 +435,13 @@ EITHER OF "type": "Frozen", "content": { "unfreezable": bool }, }, - "authority": bech32 string, + "authority": hexified destination, }, } 2) { "type": "NonFungibleToken", "content": { - "token_id": hex string, + "token_id": hexified token id, "creation_tx_id": hex string, "creation_block_id": hex string, "metadata": { @@ -504,7 +504,7 @@ Get tokens information, given multiple token ids in address form. Parameters: ``` -{ "token_ids": [ string, .. ] } +{ "token_ids": [ bech32 string, .. ] } ``` Returns: @@ -513,7 +513,7 @@ Returns: 1) { "type": "FungibleToken", "content": { - "token_id": hex string, + "token_id": hexified token id, "token_ticker": { "text": EITHER OF 1) string @@ -545,13 +545,13 @@ Returns: "type": "Frozen", "content": { "unfreezable": bool }, }, - "authority": bech32 string, + "authority": hexified destination, }, } 2) { "type": "NonFungibleToken", "content": { - "token_id": hex string, + "token_id": hexified token id, "creation_tx_id": hex string, "creation_block_id": hex string, "metadata": { @@ -613,14 +613,14 @@ Get order information, given an order id, in address form. Parameters: ``` -{ "order_id": string } +{ "order_id": bech32 string } ``` Returns: ``` EITHER OF 1) { - "conclude_key": bech32 string, + "conclude_key": hexified destination, "initially_asked": EITHER OF 1) { "type": "Coin", @@ -629,7 +629,7 @@ EITHER OF 2) { "type": "Token", "content": { - "id": hex string, + "id": hexified token id, "amount": { "atoms": number string }, }, }, @@ -641,7 +641,7 @@ EITHER OF 2) { "type": "Token", "content": { - "id": hex string, + "id": hexified token id, "amount": { "atoms": number string }, }, }, @@ -668,14 +668,14 @@ Parameters: 1) { "type": "Coin" } 2) { "type": "Token", - "content": hex string, + "content": bech32 string, } 3) null, "give_currency": EITHER OF 1) { "type": "Coin" } 2) { "type": "Token", - "content": hex string, + "content": bech32 string, } 3) null, } @@ -683,8 +683,8 @@ Parameters: Returns: ``` -{ hex string: { - "conclude_key": bech32 string, +{ hexified order id: { + "conclude_key": hexified destination, "initially_asked": EITHER OF 1) { "type": "Coin", @@ -693,7 +693,7 @@ Returns: 2) { "type": "Token", "content": { - "id": hex string, + "id": hexified token id, "amount": { "atoms": number string }, }, }, @@ -705,7 +705,7 @@ Returns: 2) { "type": "Token", "content": { - "id": hex string, + "id": hexified token id, "amount": { "atoms": number string }, }, }, @@ -1457,7 +1457,7 @@ the parameters. Parameters: ``` { - "pool_id": hex string, + "pool_id": hexified pool id, "min_height": number, "max_height": EITHER OF 1) number diff --git a/rpc/description/src/value_hint.rs b/rpc/description/src/value_hint.rs index 6641e4aef..f8dad1c40 100644 --- a/rpc/description/src/value_hint.rs +++ b/rpc/description/src/value_hint.rs @@ -64,6 +64,15 @@ impl ValueHint { pub const NUMBER_STRING: VH = VH::Prim("number string"); pub const DECIMAL_STRING: VH = VH::Prim("decimal string"); pub const BECH32_STRING: VH = VH::Prim("bech32 string"); + // TODO: perhaps Destination and pool/delegation/token/order ids should not have + // an RPC ValueHint at all and their RpcAddress<> equivalent should be used in all RPC calls. + // Also see the TODO in `common/src/chain/currency.rs` for the info about current inconsistencies + // in RPC types. + pub const HEXIFIED_DEST: VH = VH::Prim("hexified destination"); + pub const HEXIFIED_POOL_ID: VH = VH::Prim("hexified pool id"); + pub const HEXIFIED_DELEGATION_ID: VH = VH::Prim("hexified delegation id"); + pub const HEXIFIED_TOKEN_ID: VH = VH::Prim("hexified token id"); + pub const HEXIFIED_ORDER_ID: VH = VH::Prim("hexified order id"); pub const HEX_STRING: VH = VH::Prim("hex string"); pub const GENERIC_OBJECT: VH = VH::Prim("object"); pub const JSON: VH = VH::Prim("json"); diff --git a/wallet/src/account/currency_grouper/mod.rs b/wallet/src/account/currency_grouper/mod.rs index ea09a9b90..acf9827f4 100644 --- a/wallet/src/account/currency_grouper/mod.rs +++ b/wallet/src/account/currency_grouper/mod.rs @@ -21,10 +21,9 @@ use crate::{ use std::collections::BTreeMap; use common::{ - chain::{output_value::OutputValue, ChainConfig, Destination, TxOutput}, + chain::{output_value::OutputValue, ChainConfig, Currency, Destination, TxOutput}, primitives::{Amount, BlockHeight}, }; -use wallet_types::Currency; use super::UtxoSelectorError; diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index bdb10515e..027d1d046 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -63,8 +63,8 @@ use common::chain::tokens::{ IsTokenUnfreezable, NftIssuance, NftIssuanceV0, RPCFungibleTokenInfo, TokenId, TokenIssuance, }; use common::chain::{ - make_token_id, AccountNonce, Block, ChainConfig, DelegationId, Destination, GenBlock, PoolId, - SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, + make_token_id, AccountNonce, Block, ChainConfig, Currency, DelegationId, Destination, GenBlock, + PoolId, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, }; use common::primitives::{Amount, BlockHeight, Id}; use consensus::PoSGenerateBlockInputData; @@ -84,8 +84,8 @@ use wallet_storage::{ use wallet_types::utxo_types::{get_utxo_type, UtxoState, UtxoStates, UtxoType, UtxoTypes}; use wallet_types::wallet_tx::{BlockData, TxData, TxState}; use wallet_types::{ - AccountId, AccountInfo, AccountWalletCreatedTxId, AccountWalletTxId, BlockInfo, Currency, - KeyPurpose, KeychainUsageState, WalletTx, + AccountId, AccountInfo, AccountWalletCreatedTxId, AccountWalletTxId, BlockInfo, KeyPurpose, + KeychainUsageState, WalletTx, }; pub use self::output_cache::{ diff --git a/wallet/src/account/transaction_list/mod.rs b/wallet/src/account/transaction_list/mod.rs index 8ba857736..84d2a873c 100644 --- a/wallet/src/account/transaction_list/mod.rs +++ b/wallet/src/account/transaction_list/mod.rs @@ -16,13 +16,13 @@ use std::{cmp::Ordering, ops::Add}; use common::{ - chain::{block::timestamp::BlockTimestamp, Transaction, TxInput, TxOutput}, + chain::{block::timestamp::BlockTimestamp, Currency, Transaction, TxInput, TxOutput}, primitives::{Amount, BlockHeight, Id, Idable}, }; use serde::Serialize; use wallet_types::{ wallet_tx::{TxData, TxState}, - Currency, WalletTx, + WalletTx, }; use crate::{key_chain::AccountKeyChains, WalletError, WalletResult}; diff --git a/wallet/src/send_request/mod.rs b/wallet/src/send_request/mod.rs index 1f3a89b1e..e684f7021 100644 --- a/wallet/src/send_request/mod.rs +++ b/wallet/src/send_request/mod.rs @@ -23,17 +23,14 @@ use common::{ stakelock::StakePoolData, timelock::OutputTimeLock::ForBlockCount, tokens::{Metadata, TokenId, TokenIssuance}, - ChainConfig, Destination, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, + ChainConfig, Currency, Destination, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{per_thousand::PerThousand, Amount, BlockHeight}, }; use crypto::vrf::VRFPublicKey; use utils::ensure; -use wallet_types::{ - partially_signed_transaction::{ - PartiallySignedTransaction, PartiallySignedTransactionWalletExt as _, PtxAdditionalInfo, - }, - Currency, +use wallet_types::partially_signed_transaction::{ + PartiallySignedTransaction, PartiallySignedTransactionWalletExt as _, PtxAdditionalInfo, }; use crate::{ diff --git a/wallet/src/signer/tests/generic_tests.rs b/wallet/src/signer/tests/generic_tests.rs index df7c97ebf..dff8967a3 100644 --- a/wallet/src/signer/tests/generic_tests.rs +++ b/wallet/src/signer/tests/generic_tests.rs @@ -40,7 +40,7 @@ use common::{ TokenIssuance, TokenIssuanceV1, TokenTotalSupply, }, AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, ChainConfig, - ChainstateUpgradeBuilder, DelegationId, Destination, GenBlock, NetUpgrades, + ChainstateUpgradeBuilder, Currency, DelegationId, Destination, GenBlock, NetUpgrades, OrderAccountCommand, OrderData, OrderId, OutPointSourceId, PoolId, SighashInputCommitmentVersion, SignedTransactionIntent, Transaction, TxInput, TxOutput, UtxoOutPoint, @@ -66,7 +66,7 @@ use wallet_types::{ OrderAdditionalInfo, PoolAdditionalInfo, PtxAdditionalInfo, TokenAdditionalInfo, TokensAdditionalInfo, }, - BlockInfo, Currency, KeyPurpose, + BlockInfo, KeyPurpose, }; use crate::{ diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 22301345e..53096123a 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -51,10 +51,10 @@ use common::chain::tokens::{ }; use common::chain::{ make_delegation_id, make_order_id, make_token_id, AccountCommand, AccountNonce, - AccountOutPoint, Block, ChainConfig, DelegationId, Destination, GenBlock, IdCreationError, - OrderAccountCommand, OrderId, OutPointSourceId, PoolId, RpcOrderInfo, SignedTransaction, - SignedTransactionIntent, Transaction, TransactionCreationError, TxInput, TxOutput, - UtxoOutPoint, + AccountOutPoint, Block, ChainConfig, Currency, DelegationId, Destination, GenBlock, + IdCreationError, OrderAccountCommand, OrderId, OutPointSourceId, PoolId, RpcOrderInfo, + SignedTransaction, SignedTransactionIntent, Transaction, TransactionCreationError, TxInput, + TxOutput, UtxoOutPoint, }; use common::primitives::id::{hash_encoded, WithId}; use common::primitives::{Amount, BlockHeight, Id, H256}; @@ -90,8 +90,7 @@ use wallet_types::wallet_tx::{TxData, TxState}; use wallet_types::wallet_type::{WalletControllerMode, WalletType}; use wallet_types::with_locked::WithLocked; use wallet_types::{ - AccountId, AccountKeyPurposeId, BlockInfo, Currency, KeyPurpose, KeychainUsageState, - SignedTxWithFees, + AccountId, AccountKeyPurposeId, BlockInfo, KeyPurpose, KeychainUsageState, SignedTxWithFees, }; pub const WALLET_VERSION_UNINITIALIZED: u32 = 0; diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index c03c9ffdf..a8a2045c6 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -31,7 +31,8 @@ use common::{ stakelock::StakePoolData, timelock::OutputTimeLock, tokens::{RPCIsTokenFrozen, TokenData, TokenIssuanceV0, TokenIssuanceV1}, - AccountSpending, ChainstateUpgradeBuilder, Destination, Genesis, OutPointSourceId, TxInput, + AccountSpending, ChainstateUpgradeBuilder, Currency, Destination, Genesis, + OutPointSourceId, TxInput, }, primitives::{per_thousand::PerThousand, Idable, H256}, }; @@ -60,7 +61,7 @@ use wallet_types::{ }, seed_phrase::{PassPhrase, StoreSeedPhrase}, utxo_types::{UtxoState, UtxoType}, - AccountWalletTxId, Currency, WalletTx, + AccountWalletTxId, WalletTx, }; use crate::{ diff --git a/wallet/types/src/lib.rs b/wallet/types/src/lib.rs index c3274c536..b1ae8cb98 100644 --- a/wallet/types/src/lib.rs +++ b/wallet/types/src/lib.rs @@ -39,14 +39,12 @@ pub use wallet_tx::{BlockInfo, WalletTx}; use std::collections::BTreeMap; use common::{ - chain::{SignedTransaction, Transaction}, + chain::{Currency, SignedTransaction, Transaction}, primitives::Amount, }; use crate::scan_blockchain::ScanBlockchain; -pub type Currency = common::chain::RpcCurrency; - #[derive(Debug, Clone, PartialEq, Eq)] pub struct SignedTxWithFees { pub tx: SignedTransaction, diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index f89a192f4..df00d25e5 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -25,7 +25,7 @@ use common::{ address::{Address, RpcAddress}, chain::{ config::checkpoints_data::print_block_heights_ids_as_checkpoints_data, tokens::TokenId, - ChainConfig, Destination, SignedTransaction, TxOutput, UtxoOutPoint, + ChainConfig, Currency, Destination, SignedTransaction, TxOutput, UtxoOutPoint, }, primitives::{Idable as _, H256}, text_summary::TextSummary, @@ -48,12 +48,13 @@ use wallet_rpc_lib::types::{ RpcNewTransaction, RpcSignatureStats, RpcSignatureStatus, RpcStandaloneAddressDetails, RpcValidatedSignatures, TokenMetadata, }; -use wallet_types::{partially_signed_transaction::PartiallySignedTransaction, Currency}; +use wallet_types::partially_signed_transaction::PartiallySignedTransaction; use crate::{ errors::WalletCliCommandError, helper_types::{ active_order_infos_header, format_token_name, parse_currency, parse_generic_token_transfer, + parse_rpc_currency, }, CreateWalletDeviceSelectMenu, ManageableWalletCommand, OpenWalletDeviceSelectMenu, OpenWalletSubCommand, WalletManagementCommand, @@ -2121,10 +2122,10 @@ where let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; let ask_currency = ask_currency - .map(|ask_currency| parse_currency(&ask_currency, chain_config)) + .map(|ask_currency| parse_rpc_currency(&ask_currency, chain_config)) .transpose()?; let give_currency = give_currency - .map(|give_currency| parse_currency(&give_currency, chain_config)) + .map(|give_currency| parse_rpc_currency(&give_currency, chain_config)) .transpose()?; let order_infos = wallet diff --git a/wallet/wallet-cli-commands/src/errors.rs b/wallet/wallet-cli-commands/src/errors.rs index da9361120..87467dee1 100644 --- a/wallet/wallet-cli-commands/src/errors.rs +++ b/wallet/wallet-cli-commands/src/errors.rs @@ -76,4 +76,7 @@ pub enum WalletCliCommandError { #[error("Accumulated ask amount for order {0} is negative")] OrderNegativeAccumulatedAskAmount(RpcAddress), + + #[error("Address error: {0}")] + AddressError(#[from] AddressError), } diff --git a/wallet/wallet-cli-commands/src/helper_types.rs b/wallet/wallet-cli-commands/src/helper_types.rs index fef48f541..6c0487947 100644 --- a/wallet/wallet-cli-commands/src/helper_types.rs +++ b/wallet/wallet-cli-commands/src/helper_types.rs @@ -23,7 +23,7 @@ use common::{ address::{decode_address, Address, RpcAddress}, chain::{ tokens::{RPCTokenInfo, TokenId}, - ChainConfig, OutPointSourceId, TxOutput, UtxoOutPoint, + ChainConfig, Currency, OutPointSourceId, RpcCurrency, TxOutput, UtxoOutPoint, }, primitives::{ amount::decimal::subtract_decimal_amounts_of_same_currency, DecimalAmount, Id, H256, @@ -38,7 +38,6 @@ use wallet_types::{ seed_phrase::StoreSeedPhrase, utxo_types::{UtxoState, UtxoType}, with_locked::WithLocked, - Currency, }; use crate::errors::WalletCliCommandError; @@ -569,6 +568,13 @@ pub fn parse_currency( } } +pub fn parse_rpc_currency( + input: &str, + chain_config: &ChainConfig, +) -> Result> { + Ok(parse_currency(input, chain_config)?.to_rpc_currency(chain_config)?) +} + #[derive(Debug, Clone, Copy, ValueEnum)] pub enum CliIsFreezable { NotFreezable, diff --git a/wallet/wallet-controller/src/helpers/mod.rs b/wallet/wallet-controller/src/helpers/mod.rs index 238e51654..7c95cc303 100644 --- a/wallet/wallet-controller/src/helpers/mod.rs +++ b/wallet/wallet-controller/src/helpers/mod.rs @@ -28,7 +28,7 @@ use common::{ htlc::HtlcSecret, output_values_holder::collect_token_v1_ids_from_output_values_holder, tokens::{RPCTokenInfo, TokenId}, - AccountCommand, ChainConfig, Destination, OrderAccountCommand, OrderId, PoolId, + AccountCommand, ChainConfig, Currency, Destination, OrderAccountCommand, OrderId, PoolId, RpcOrderInfo, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{amount::RpcAmountOut, Amount}, @@ -39,12 +39,9 @@ use wallet::{ destination_getters::{get_tx_output_destination, HtlcSpendingCondition}, WalletError, }; -use wallet_types::{ - partially_signed_transaction::{ - OrderAdditionalInfo, PartiallySignedTransaction, PartiallySignedTransactionWalletExt as _, - PoolAdditionalInfo, PtxAdditionalInfo, TokenAdditionalInfo, TokensAdditionalInfo, - }, - Currency, +use wallet_types::partially_signed_transaction::{ + OrderAdditionalInfo, PartiallySignedTransaction, PartiallySignedTransactionWalletExt as _, + PoolAdditionalInfo, PtxAdditionalInfo, TokenAdditionalInfo, TokensAdditionalInfo, }; use crate::{runtime_wallet::RuntimeWallet, types::Balances, ControllerError}; diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index c5c58a415..62f96fe54 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -68,7 +68,7 @@ use common::{ DestinationSigError, Transactable, }, tokens::{RPCTokenInfo, TokenId}, - Block, ChainConfig, Destination, GenBlock, PoolId, SighashInputCommitmentVersion, + Block, ChainConfig, Currency, Destination, GenBlock, PoolId, SighashInputCommitmentVersion, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{ @@ -117,7 +117,6 @@ use wallet_types::{ signature_status::SignatureStatus, wallet_type::{WalletControllerMode, WalletType}, with_locked::WithLocked, - Currency, }; #[cfg(feature = "trezor")] diff --git a/wallet/wallet-controller/src/read.rs b/wallet/wallet-controller/src/read.rs index aeebbe38d..0b18e519e 100644 --- a/wallet/wallet-controller/src/read.rs +++ b/wallet/wallet-controller/src/read.rs @@ -22,7 +22,7 @@ use futures::{stream::FuturesUnordered, FutureExt, TryStreamExt}; use common::{ address::Address, chain::{ - ChainConfig, DelegationId, Destination, OrderId, PoolId, Transaction, TxOutput, + ChainConfig, Currency, DelegationId, Destination, OrderId, PoolId, Transaction, TxOutput, UtxoOutPoint, }, primitives::{id::WithId, Amount, Id}, @@ -45,7 +45,7 @@ use wallet_types::{ utxo_types::{UtxoStates, UtxoTypes}, wallet_tx::TxData, with_locked::WithLocked, - Currency, KeyPurpose, KeychainUsageState, + KeyPurpose, KeychainUsageState, }; use crate::{ diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs index c093158ae..271fda0c1 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -23,9 +23,9 @@ use common::{ output_value::OutputValue, signature::inputsig::arbitrary_message::ArbitraryMessageSignature, tokens::{IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance}, - AccountCommand, AccountOutPoint, DelegationId, Destination, GenBlock, OrderAccountCommand, - OrderId, PoolId, RpcOrderInfo, SignedTransaction, SignedTransactionIntent, Transaction, - TxOutput, UtxoOutPoint, + AccountCommand, AccountOutPoint, Currency, DelegationId, Destination, GenBlock, + OrderAccountCommand, OrderId, PoolId, RpcOrderInfo, SignedTransaction, + SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, primitives::{id::WithId, Amount, BlockHeight, Id, H256}, }; @@ -61,7 +61,7 @@ use wallet_types::{ utxo_types::{UtxoState, UtxoStates, UtxoTypes}, wallet_tx::TxData, with_locked::WithLocked, - Currency, KeyPurpose, KeychainUsageState, SignedTxWithFees, + KeyPurpose, KeychainUsageState, SignedTxWithFees, }; #[cfg(feature = "trezor")] diff --git a/wallet/wallet-controller/src/sync/tests/mod.rs b/wallet/wallet-controller/src/sync/tests/mod.rs index b6e9639be..7a17df8e6 100644 --- a/wallet/wallet-controller/src/sync/tests/mod.rs +++ b/wallet/wallet-controller/src/sync/tests/mod.rs @@ -26,7 +26,8 @@ use chainstate_test_framework::TestFramework; use common::{ chain::{ tokens::{RPCTokenInfo, TokenId}, - DelegationId, Destination, OrderId, PoolId, RpcOrderInfo, SignedTransaction, Transaction, + Currency, DelegationId, Destination, OrderId, PoolId, RpcOrderInfo, SignedTransaction, + Transaction, }, primitives::{time::Time, Amount}, }; @@ -314,8 +315,8 @@ impl NodeInterface for MockNode { async fn get_orders_info_by_currencies( &self, - _ask_currency: Option, - _give_currency: Option, + _ask_currency: Option, + _give_currency: Option, ) -> Result, Self::Error> { unreachable!() } diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index d0541b78a..e37f597ec 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -30,8 +30,8 @@ use common::{ Metadata, RPCFungibleTokenInfo, RPCTokenInfo, TokenId, TokenIssuance, TokenIssuanceV1, TokenTotalSupply, }, - ChainConfig, DelegationId, Destination, OrderId, PoolId, RpcOrderInfo, SignedTransaction, - SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, + ChainConfig, Currency, DelegationId, Destination, OrderId, PoolId, RpcOrderInfo, + SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, primitives::{amount::RpcAmountIn, per_thousand::PerThousand, Amount, Id}, }; @@ -65,7 +65,7 @@ use wallet_types::{ signature_status::SignatureStatus, utxo_types::{UtxoState, UtxoType}, with_locked::WithLocked, - Currency, SignedTxWithFees, + SignedTxWithFees, }; use crate::{ diff --git a/wallet/wallet-controller/src/tests/test_utils.rs b/wallet/wallet-controller/src/tests/test_utils.rs index 9c1e4162a..bfaf53b88 100644 --- a/wallet/wallet-controller/src/tests/test_utils.rs +++ b/wallet/wallet-controller/src/tests/test_utils.rs @@ -25,7 +25,8 @@ use common::{ RPCFungibleTokenInfo, RPCIsTokenFrozen, RPCTokenTotalSupply, TokenCreator, TokenId, TokenTotalSupply, }, - Block, ChainConfig, Destination, OrderId, SignedTransaction, Transaction, TxOutput, + Block, ChainConfig, Currency, Destination, OrderId, SignedTransaction, Transaction, + TxOutput, }, primitives::{amount::RpcAmountOut, Amount, BlockHeight}, }; @@ -36,7 +37,7 @@ use crypto::{ use randomness::{CryptoRng, Rng}; use test_utils::random::{gen_random_alnum_string, gen_random_bytes}; use wallet::{signer::SignerProvider, wallet::test_helpers::scan_wallet, DefaultWallet, Wallet}; -use wallet_types::{account_info::DEFAULT_ACCOUNT_INDEX, Currency}; +use wallet_types::account_info::DEFAULT_ACCOUNT_INDEX; use crate::types::Balances; diff --git a/wallet/wallet-node-client/src/handles_client/mod.rs b/wallet/wallet-node-client/src/handles_client/mod.rs index 8bb5b359b..8ef524c47 100644 --- a/wallet/wallet-node-client/src/handles_client/mod.rs +++ b/wallet/wallet-node-client/src/handles_client/mod.rs @@ -24,7 +24,7 @@ use chainstate::{BlockSource, ChainInfo, ChainstateError, ChainstateHandle}; use common::{ chain::{ tokens::{RPCTokenInfo, TokenId}, - Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, + Block, Currency, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, Transaction, }, primitives::{time::Time, Amount, BlockHeight, Id}, @@ -253,8 +253,8 @@ impl NodeInterface for WalletHandlesClient { async fn get_orders_info_by_currencies( &self, - ask_currency: Option, // FIXME use RpcCurrency directly? - give_currency: Option, + ask_currency: Option, + give_currency: Option, ) -> Result, Self::Error> { let result = self .chainstate diff --git a/wallet/wallet-node-client/src/mock.rs b/wallet/wallet-node-client/src/mock.rs index 01f6e8024..c234cce18 100644 --- a/wallet/wallet-node-client/src/mock.rs +++ b/wallet/wallet-node-client/src/mock.rs @@ -26,7 +26,7 @@ use chainstate::ChainInfo; use common::{ chain::{ tokens::{RPCTokenInfo, TokenId}, - Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, + Block, Currency, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, Transaction, TxOutput, UtxoOutPoint, }, primitives::{time::Time, Amount, BlockHeight, Id}, @@ -159,8 +159,8 @@ impl NodeInterface for ClonableMockNodeInterface { async fn get_orders_info_by_currencies( &self, - ask_currency: Option, - give_currency: Option, + ask_currency: Option, + give_currency: Option, ) -> Result, Self::Error> { self.lock() .await diff --git a/wallet/wallet-node-client/src/node_traits.rs b/wallet/wallet-node-client/src/node_traits.rs index 3903b86b8..d6bceb473 100644 --- a/wallet/wallet-node-client/src/node_traits.rs +++ b/wallet/wallet-node-client/src/node_traits.rs @@ -23,7 +23,7 @@ use chainstate::ChainInfo; use common::{ chain::{ tokens::{RPCTokenInfo, TokenId}, - Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, + Block, Currency, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, Transaction, TxOutput, UtxoOutPoint, }, primitives::{time::Time, Amount, BlockHeight, Id}, @@ -88,8 +88,8 @@ pub trait NodeInterface { async fn get_order_info(&self, order_id: OrderId) -> Result, Self::Error>; async fn get_orders_info_by_currencies( &self, - ask_currency: Option, - give_currency: Option, + ask_currency: Option, + give_currency: Option, ) -> Result, Self::Error>; async fn blockprod_e2e_public_key(&self) -> Result; async fn generate_block( diff --git a/wallet/wallet-node-client/src/rpc_client/client_impl.rs b/wallet/wallet-node-client/src/rpc_client/client_impl.rs index 1fb06bec3..d0e78120f 100644 --- a/wallet/wallet-node-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-node-client/src/rpc_client/client_impl.rs @@ -25,7 +25,7 @@ use common::{ address::Address, chain::{ tokens::{RPCTokenInfo, TokenId}, - Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, + Block, Currency, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, Transaction, TxOutput, UtxoOutPoint, }, primitives::{time::Time, Amount, BlockHeight, Id}, @@ -133,14 +133,14 @@ impl NodeInterface for NodeRpcClient { async fn get_stake_pool_balance(&self, pool_id: PoolId) -> Result, Self::Error> { let pool_address = Address::new(&self.chain_config, pool_id)?; - ChainstateRpcClient::stake_pool_balance(&self.http_client, pool_address.into_string()) + ChainstateRpcClient::stake_pool_balance(&self.http_client, pool_address.into()) .await .map_err(NodeRpcError::ResponseError) } async fn get_staker_balance(&self, pool_id: PoolId) -> Result, Self::Error> { let pool_address = Address::new(&self.chain_config, pool_id)?; - ChainstateRpcClient::staker_balance(&self.http_client, pool_address.into_string()) + ChainstateRpcClient::staker_balance(&self.http_client, pool_address.into()) .await .map_err(NodeRpcError::ResponseError) } @@ -150,12 +150,14 @@ impl NodeInterface for NodeRpcClient { pool_id: PoolId, ) -> Result, Self::Error> { let pool_address = Address::new(&self.chain_config, pool_id)?; - ChainstateRpcClient::pool_decommission_destination( + let dest_as_address = ChainstateRpcClient::pool_decommission_destination( &self.http_client, - pool_address.into_string(), + pool_address.into(), ) .await - .map_err(NodeRpcError::ResponseError) + .map_err(NodeRpcError::ResponseError)?; + + Ok(dest_as_address.map(|addr| addr.decode_object(&self.chain_config)).transpose()?) } async fn get_delegation_share( @@ -163,15 +165,15 @@ impl NodeInterface for NodeRpcClient { pool_id: PoolId, delegation_id: DelegationId, ) -> Result, Self::Error> { - let pool_address = Address::new(&self.chain_config, pool_id)?.into_string(); - let delegation_address = Address::new(&self.chain_config, delegation_id)?.into_string(); + let pool_address = Address::new(&self.chain_config, pool_id)?.into(); + let delegation_address = Address::new(&self.chain_config, delegation_id)?.into(); ChainstateRpcClient::delegation_share(&self.http_client, pool_address, delegation_address) .await .map_err(NodeRpcError::ResponseError) } async fn get_token_info(&self, token_id: TokenId) -> Result, Self::Error> { - let token_id = Address::new(&self.chain_config, token_id)?.into_string(); + let token_id = Address::new(&self.chain_config, token_id)?.into(); ChainstateRpcClient::token_info(&self.http_client, token_id) .await .map_err(NodeRpcError::ResponseError) @@ -184,7 +186,7 @@ impl NodeInterface for NodeRpcClient { let token_ids = token_ids .into_iter() .map(|token_id| { - Ok::<_, Self::Error>(Address::new(&self.chain_config, token_id)?.into_string()) + Ok::<_, Self::Error>(Address::new(&self.chain_config, token_id)?.into()) }) .collect::>()?; ChainstateRpcClient::tokens_info(&self.http_client, token_ids) @@ -193,7 +195,7 @@ impl NodeInterface for NodeRpcClient { } async fn get_order_info(&self, order_id: OrderId) -> Result, Self::Error> { - let order_id = Address::new(&self.chain_config, order_id)?.into_string(); + let order_id = Address::new(&self.chain_config, order_id)?.into(); ChainstateRpcClient::order_info(&self.http_client, order_id) .await .map_err(NodeRpcError::ResponseError) @@ -201,13 +203,17 @@ impl NodeInterface for NodeRpcClient { async fn get_orders_info_by_currencies( &self, - ask_currency: Option, - give_currency: Option, + ask_currency: Option, + give_currency: Option, ) -> Result, Self::Error> { ChainstateRpcClient::orders_info_by_currencies( &self.http_client, - ask_currency, - give_currency, + ask_currency + .map(|currency| currency.to_rpc_currency(&self.chain_config)) + .transpose()?, + give_currency + .map(|currency| currency.to_rpc_currency(&self.chain_config)) + .transpose()?, ) .await .map_err(NodeRpcError::ResponseError) diff --git a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs index 8a8b42af9..d7dacaf3d 100644 --- a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs +++ b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs @@ -24,7 +24,7 @@ use chainstate::ChainInfo; use common::{ chain::{ tokens::{RPCTokenInfo, TokenId}, - Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, + Block, Currency, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, Transaction, }, primitives::{time::Time, Amount, BlockHeight, Id}, @@ -161,8 +161,8 @@ impl NodeInterface for ColdWalletClient { async fn get_orders_info_by_currencies( &self, - _ask_currency: Option, - _give_currency: Option, + _ask_currency: Option, + _give_currency: Option, ) -> Result, Self::Error> { Err(ColdWalletRpcError::NotAvailable) } diff --git a/wallet/wallet-rpc-client/src/handles_client/mod.rs b/wallet/wallet-rpc-client/src/handles_client/mod.rs index bc2061bd4..23e5ee876 100644 --- a/wallet/wallet-rpc-client/src/handles_client/mod.rs +++ b/wallet/wallet-rpc-client/src/handles_client/mod.rs @@ -22,8 +22,8 @@ use common::{ block::timestamp::BlockTimestamp, output_values_holder::collect_token_v1_ids_from_output_values_holders, tokens::{IsTokenUnfreezable, RPCTokenInfo}, - Block, GenBlock, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, - UtxoOutPoint, + Block, GenBlock, RpcCurrency, SignedTransaction, SignedTransactionIntent, Transaction, + TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id, Idable, H256}, }; @@ -1220,8 +1220,8 @@ where async fn list_all_active_orders( &self, account_index: U31, - ask_curency: Option, - give_curency: Option, + ask_curency: Option, + give_curency: Option, ) -> Result, Self::Error> { self.wallet_rpc .list_all_active_orders(account_index, ask_curency, give_curency) diff --git a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs index eff37dbec..bcedf93d2 100644 --- a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs @@ -24,8 +24,8 @@ use super::{ClientWalletRpc, WalletRpcError}; use chainstate::{rpc::RpcOutputValueIn, ChainInfo}; use common::{ chain::{ - block::timestamp::BlockTimestamp, tokens::RPCTokenInfo, Block, GenBlock, SignedTransaction, - SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, + block::timestamp::BlockTimestamp, tokens::RPCTokenInfo, Block, GenBlock, RpcCurrency, + SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id}, }; @@ -1130,8 +1130,8 @@ impl WalletInterface for ClientWalletRpc { async fn list_all_active_orders( &self, account_index: U31, - ask_curency: Option, - give_curency: Option, + ask_curency: Option, + give_curency: Option, ) -> Result, Self::Error> { WalletRpcClient::list_all_active_orders( &self.http_client, diff --git a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs index 340854631..1a7c9426b 100644 --- a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs +++ b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs @@ -18,8 +18,8 @@ use std::{collections::BTreeMap, num::NonZeroUsize, path::PathBuf}; use chainstate::{rpc::RpcOutputValueIn, ChainInfo}; use common::{ chain::{ - block::timestamp::BlockTimestamp, tokens::RPCTokenInfo, Block, GenBlock, SignedTransaction, - SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, + block::timestamp::BlockTimestamp, tokens::RPCTokenInfo, Block, GenBlock, RpcCurrency, + SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id}, }; @@ -551,8 +551,8 @@ pub trait WalletInterface { async fn list_all_active_orders( &self, account_index: U31, - ask_curency: Option, - give_curency: Option, + ask_curency: Option, + give_curency: Option, ) -> Result, Self::Error>; async fn node_version(&self) -> Result; diff --git a/wallet/wallet-rpc-daemon/docs/RPC.md b/wallet/wallet-rpc-daemon/docs/RPC.md index edaeda3a5..2993889d0 100644 --- a/wallet/wallet-rpc-daemon/docs/RPC.md +++ b/wallet/wallet-rpc-daemon/docs/RPC.md @@ -2645,14 +2645,14 @@ Parameters: 1) { "type": "Coin" } 2) { "type": "Token", - "content": hex string, + "content": bech32 string, } 3) null, "give_currency": EITHER OF 1) { "type": "Coin" } 2) { "type": "Token", - "content": hex string, + "content": bech32 string, } 3) null, } @@ -3352,7 +3352,7 @@ Returns: 1) { "type": "FungibleToken", "content": { - "token_id": hex string, + "token_id": hexified token id, "token_ticker": { "text": EITHER OF 1) string @@ -3384,13 +3384,13 @@ Returns: "type": "Frozen", "content": { "unfreezable": bool }, }, - "authority": bech32 string, + "authority": hexified destination, }, } 2) { "type": "NonFungibleToken", "content": { - "token_id": hex string, + "token_id": hexified token id, "creation_tx_id": hex string, "creation_block_id": hex string, "metadata": { diff --git a/wallet/wallet-rpc-lib/src/rpc/interface.rs b/wallet/wallet-rpc-lib/src/rpc/interface.rs index 9dd347e9c..4b68861d6 100644 --- a/wallet/wallet-rpc-lib/src/rpc/interface.rs +++ b/wallet/wallet-rpc-lib/src/rpc/interface.rs @@ -25,8 +25,8 @@ use common::{ chain::{ block::timestamp::BlockTimestamp, tokens::{RPCTokenInfo, TokenId}, - Block, DelegationId, Destination, GenBlock, OrderId, PoolId, SignedTransaction, - SignedTransactionIntent, Transaction, TxOutput, + Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcCurrency, + SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, }, primitives::{BlockHeight, Id}, }; @@ -880,8 +880,8 @@ trait WalletRpc { async fn list_all_active_orders( &self, account: AccountArg, - ask_currency: Option, - give_currency: Option, + ask_currency: Option, + give_currency: Option, ) -> rpc::RpcResult>; /// Obtain the node version diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index fc493b6ea..252e1d103 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -45,8 +45,9 @@ use common::{ tokens::{ IsTokenFreezable, IsTokenUnfreezable, Metadata, RPCTokenInfo, TokenId, TokenTotalSupply, }, - Block, ChainConfig, DelegationId, Destination, GenBlock, OrderId, PoolId, - SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, + Block, ChainConfig, Currency, DelegationId, Destination, GenBlock, OrderId, PoolId, + RpcCurrency, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, + UtxoOutPoint, }, primitives::{ id::WithId, per_thousand::PerThousand, time::Time, Amount, BlockHeight, Id, Idable, @@ -85,7 +86,7 @@ use wallet_types::wallet_type::WalletType; use wallet_types::{ account_info::StandaloneAddressDetails, generic_transaction::GenericTransaction, partially_signed_transaction::PartiallySignedTransaction, scan_blockchain::ScanBlockchain, - signature_status::SignatureStatus, wallet_tx::TxData, with_locked::WithLocked, Currency, + signature_status::SignatureStatus, wallet_tx::TxData, with_locked::WithLocked, SignedTxWithFees, }; @@ -1835,8 +1836,8 @@ where pub async fn list_all_active_orders( &self, account_index: U31, - ask_currency: Option, - give_currency: Option, + ask_currency: Option, + give_currency: Option, ) -> WRpcResult, N> { let wallet_order_ids = self .wallet @@ -1852,7 +1853,14 @@ where let node_rpc_order_infos = self .node - .get_orders_info_by_currencies(ask_currency, give_currency) + .get_orders_info_by_currencies( + ask_currency + .map(|rpc_currency| rpc_currency.to_currency(&self.chain_config)) + .transpose()?, + give_currency + .map(|rpc_currency| rpc_currency.to_currency(&self.chain_config)) + .transpose()?, + ) .await .map_err(RpcError::RpcError)?; diff --git a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs index be9a02a3d..815c517b6 100644 --- a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs +++ b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs @@ -22,8 +22,8 @@ use common::{ block::timestamp::BlockTimestamp, output_values_holder::collect_token_v1_ids_from_output_values_holders, tokens::{IsTokenUnfreezable, RPCTokenInfo, TokenId}, - Block, DelegationId, Destination, GenBlock, OrderId, PoolId, SignedTransaction, - SignedTransactionIntent, Transaction, TxOutput, + Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcCurrency, + SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, }, primitives::{time::Time, BlockHeight, Id, Idable}, }; @@ -1134,8 +1134,8 @@ where async fn list_all_active_orders( &self, account_arg: AccountArg, - ask_currency: Option, - give_currency: Option, + ask_currency: Option, + give_currency: Option, ) -> rpc::RpcResult> { rpc::handle_result( self.list_all_active_orders(account_arg.index::()?, ask_currency, give_currency) From a9eab8554d59798d5a71a57ed8c9e7db22c81653 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Sun, 14 Dec 2025 20:16:21 +0200 Subject: [PATCH 07/10] Writing tests and applying FIXMEs - WIP --- .../src/delta/delta_data_collection/mod.rs | 1 + chainstate/src/rpc/mod.rs | 4 +- chainstate/src/rpc/types/block.rs | 4 +- chainstate/src/rpc/types/block_reward.rs | 7 +- chainstate/src/rpc/types/mod.rs | 9 +- chainstate/src/rpc/types/output.rs | 35 +- .../src/rpc/types/signed_transaction.rs | 6 +- .../chain/transaction/output/output_value.rs | 12 +- common/src/lib.rs | 4 + common/src/primitives/amount/rpc.rs | 18 ++ .../src}/token_decimals_provider.rs | 18 +- orders-accounting/src/cache.rs | 22 +- orders-accounting/src/tests/operations.rs | 110 ++++++- wallet/src/account/mod.rs | 4 +- wallet/src/account/output_cache/mod.rs | 62 +++- wallet/src/wallet/mod.rs | 23 +- wallet/src/wallet/tests.rs | 2 +- .../src/command_handler/mod.rs | 305 +++++++++--------- .../wallet-cli-commands/src/helper_types.rs | 12 +- wallet/wallet-rpc-lib/src/rpc/mod.rs | 24 +- wallet/wallet-rpc-lib/src/rpc/types.rs | 6 +- 21 files changed, 439 insertions(+), 249 deletions(-) rename {chainstate/src/rpc/types => common/src}/token_decimals_provider.rs (60%) diff --git a/accounting/src/delta/delta_data_collection/mod.rs b/accounting/src/delta/delta_data_collection/mod.rs index b81b7f596..0adbc4a7a 100644 --- a/accounting/src/delta/delta_data_collection/mod.rs +++ b/accounting/src/delta/delta_data_collection/mod.rs @@ -60,6 +60,7 @@ impl DataDelta { /// `GetDataResult` is represented by 3 states instead of typical 2 states, because it is /// important to distinguish the case when data was explicitly deleted from the case when the data is just not there. +#[derive(Clone, Debug, PartialEq, Eq)] pub enum GetDataResult { Present(T), Deleted, diff --git a/chainstate/src/rpc/mod.rs b/chainstate/src/rpc/mod.rs index d1e2a8b2b..6dc47e1d0 100644 --- a/chainstate/src/rpc/mod.rs +++ b/chainstate/src/rpc/mod.rs @@ -35,6 +35,7 @@ use common::{ TxOutput, }, primitives::{Amount, BlockHeight, Id}, + TokenDecimals, }; use rpc::{subscription, RpcResult}; use serialization::hex_encoded::HexEncoded; @@ -48,9 +49,8 @@ use self::types::{block::RpcBlock, event::RpcEvent}; pub use types::{ input::RpcUtxoOutpoint, - output::{make_rpc_amount_out, RpcOutputValueIn, RpcOutputValueOut, RpcTxOutput}, + output::{RpcOutputValueIn, RpcOutputValueOut, RpcTxOutput}, signed_transaction::RpcSignedTransaction, - token_decimals_provider::{TokenDecimals, TokenDecimalsProvider}, RpcTypeError, }; diff --git a/chainstate/src/rpc/types/block.rs b/chainstate/src/rpc/types/block.rs index 8723eccb6..09d605be5 100644 --- a/chainstate/src/rpc/types/block.rs +++ b/chainstate/src/rpc/types/block.rs @@ -17,13 +17,13 @@ use chainstate_types::BlockIndex; use common::{ chain::{block::timestamp::BlockTimestamp, Block, ChainConfig, GenBlock}, primitives::{BlockHeight, Id, Idable}, + TokenDecimalsProvider, }; use serialization::hex_encoded::HexEncoded; use super::{ block_reward::RpcBlockReward, consensus_data::RpcConsensusData, - signed_transaction::RpcSignedTransaction, token_decimals_provider::TokenDecimalsProvider, - RpcTypeError, + signed_transaction::RpcSignedTransaction, RpcTypeError, }; #[derive(Debug, Clone, serde::Serialize)] diff --git a/chainstate/src/rpc/types/block_reward.rs b/chainstate/src/rpc/types/block_reward.rs index 487a10b40..84c46c203 100644 --- a/chainstate/src/rpc/types/block_reward.rs +++ b/chainstate/src/rpc/types/block_reward.rs @@ -13,9 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::chain::{block::BlockReward, ChainConfig}; +use common::{ + chain::{block::BlockReward, ChainConfig}, + TokenDecimalsProvider, +}; -use super::{output::RpcTxOutput, token_decimals_provider::TokenDecimalsProvider, RpcTypeError}; +use super::{output::RpcTxOutput, RpcTypeError}; #[derive(Debug, Clone, serde::Serialize)] pub struct RpcBlockReward { diff --git a/chainstate/src/rpc/types/mod.rs b/chainstate/src/rpc/types/mod.rs index 2844b18d5..a7aacace6 100644 --- a/chainstate/src/rpc/types/mod.rs +++ b/chainstate/src/rpc/types/mod.rs @@ -22,18 +22,17 @@ 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}; +use common::{address::AddressError, TokenDecimalsUnavailableError}; #[derive(thiserror::Error, Debug)] pub enum RpcTypeError { #[error("Address error: {0}")] Address(#[from] AddressError), - #[error("Token decimals unavailable for token {0:x}")] - TokenDecimalsUnavailable(TokenId), - #[error("Token V0 encountered")] TokenV0Encountered, + + #[error(transparent)] + TokenDecimalsUnavailableError(#[from] TokenDecimalsUnavailableError), } diff --git a/chainstate/src/rpc/types/output.rs b/chainstate/src/rpc/types/output.rs index 22021a95c..33e69395c 100644 --- a/chainstate/src/rpc/types/output.rs +++ b/chainstate/src/rpc/types/output.rs @@ -20,17 +20,14 @@ use common::{ timelock::OutputTimeLock, tokens::TokenId, ChainConfig, DelegationId, Destination, PoolId, TxOutput, }, - primitives::{ - amount::{RpcAmountIn, RpcAmountOut}, - Amount, - }, + primitives::amount::{RpcAmountIn, RpcAmountOut}, + TokenDecimalsProvider, }; use crypto::vrf::VRFPublicKey; use rpc::types::RpcHexString; use super::{ token::{RpcNftIssuance, RpcTokenIssuance}, - token_decimals_provider::TokenDecimalsProvider, RpcTypeError, }; @@ -73,7 +70,7 @@ impl RpcOutputValueOut { id: RpcAddress::new(chain_config, token_id)?, amount: RpcAmountOut::from_amount( amount, - token_decimals(&token_id, token_decimals_provider)?, + token_decimals_provider.get_token_decimals(&token_id)?.0, ), }, }; @@ -95,32 +92,6 @@ impl RpcOutputValueOut { } } -fn token_decimals( - token_id: &TokenId, - token_decimals_provider: &impl TokenDecimalsProvider, -) -> Result { - Ok(token_decimals_provider - .get_token_decimals(token_id) - .ok_or(RpcTypeError::TokenDecimalsUnavailable(*token_id))? - .0) -} - -// FIXME put elsewhere? -pub fn make_rpc_amount_out( - amount: Amount, - token_id: Option<&TokenId>, - chain_config: &ChainConfig, - token_decimals_provider: &impl TokenDecimalsProvider, -) -> Result { - let decimals = if let Some(token_id) = token_id { - token_decimals(&token_id, token_decimals_provider)? - } else { - chain_config.coin_decimals() - }; - - Ok(RpcAmountOut::from_amount(amount, decimals)) -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rpc_description::HasValueHint)] pub struct RpcStakePoolData { pledge: RpcAmountOut, diff --git a/chainstate/src/rpc/types/signed_transaction.rs b/chainstate/src/rpc/types/signed_transaction.rs index a33015164..be30a4549 100644 --- a/chainstate/src/rpc/types/signed_transaction.rs +++ b/chainstate/src/rpc/types/signed_transaction.rs @@ -16,13 +16,11 @@ use common::{ chain::{ChainConfig, SignedTransaction, Transaction}, primitives::{Id, Idable}, + TokenDecimalsProvider, }; use serialization::hex_encoded::HexEncoded; -use super::{ - input::RpcTxInput, output::RpcTxOutput, token_decimals_provider::TokenDecimalsProvider, - RpcTypeError, -}; +use super::{input::RpcTxInput, output::RpcTxOutput, RpcTypeError}; #[derive(Debug, Clone, serde::Serialize, rpc_description::HasValueHint)] pub struct RpcSignedTransaction { diff --git a/common/src/chain/transaction/output/output_value.rs b/common/src/chain/transaction/output/output_value.rs index ab1177646..d9f039b10 100644 --- a/common/src/chain/transaction/output/output_value.rs +++ b/common/src/chain/transaction/output/output_value.rs @@ -16,7 +16,10 @@ use serialization::{Decode, Encode}; use crate::{ - chain::tokens::{NftIssuanceV0, TokenData, TokenId, TokenIssuanceV0, TokenTransfer}, + chain::{ + tokens::{NftIssuanceV0, TokenData, TokenId, TokenIssuanceV0, TokenTransfer}, + Currency, + }, primitives::Amount, }; @@ -140,6 +143,13 @@ impl RpcOutputValue { } } + pub fn currency(&self) -> Currency { + match self { + Self::Coin { amount: _ } => Currency::Coin, + Self::Token { id, amount: _ } => Currency::Token(*id), + } + } + pub fn with_amount(self, new_amount: Amount) -> Self { match self { Self::Coin { amount: _ } => Self::Coin { amount: new_amount }, diff --git a/common/src/lib.rs b/common/src/lib.rs index 932c116c9..01950f832 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -19,8 +19,12 @@ pub mod primitives; pub mod size_estimation; pub mod text_summary; pub mod time_getter; +mod token_decimals_provider; pub mod uint; +pub use token_decimals_provider::{ + TokenDecimals, TokenDecimalsProvider, TokenDecimalsUnavailableError, +}; pub use uint::{Uint128, Uint256, Uint512, UintConversionError}; #[cfg(test)] diff --git a/common/src/primitives/amount/rpc.rs b/common/src/primitives/amount/rpc.rs index 966257d23..238307f98 100644 --- a/common/src/primitives/amount/rpc.rs +++ b/common/src/primitives/amount/rpc.rs @@ -13,6 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::{ + chain::{ChainConfig, Currency}, + TokenDecimalsProvider, TokenDecimalsUnavailableError, +}; + use super::{Amount, DecimalAmount, RpcAmountInSerde, RpcAmountOutSerde}; /// Amount type suitable for getting user input supporting both decimal and atom formats in Json. @@ -109,6 +114,19 @@ impl RpcAmountOut { Self::from_amount_no_padding(amount, decimals) } + pub fn from_currency_amount( + amount: Amount, + currency: &Currency, + chain_config: &ChainConfig, + token_decimals_provider: &impl TokenDecimalsProvider, + ) -> Result { + let decimals = match currency { + Currency::Coin => chain_config.coin_decimals(), + Currency::Token(token_id) => token_decimals_provider.get_token_decimals(token_id)?.0, + }; + Ok(Self::from_amount(amount, decimals)) + } + /// Construct from amount, keeping all decimal digits pub fn from_amount_full_padding(amount: Amount, decimals: u8) -> Self { let decimal = DecimalAmount::from_amount_full_padding(amount, decimals); diff --git a/chainstate/src/rpc/types/token_decimals_provider.rs b/common/src/token_decimals_provider.rs similarity index 60% rename from chainstate/src/rpc/types/token_decimals_provider.rs rename to common/src/token_decimals_provider.rs index 1998b7326..7018a4e49 100644 --- a/chainstate/src/rpc/types/token_decimals_provider.rs +++ b/common/src/token_decimals_provider.rs @@ -15,17 +15,27 @@ use std::collections::BTreeMap; -use common::chain::tokens::TokenId; +use crate::chain::tokens::TokenId; #[derive(Clone, Copy, Debug)] pub struct TokenDecimals(pub u8); pub trait TokenDecimalsProvider { - fn get_token_decimals(&self, token_id: &TokenId) -> Option; + fn get_token_decimals( + &self, + token_id: &TokenId, + ) -> Result; } impl TokenDecimalsProvider for BTreeMap { - fn get_token_decimals(&self, token_id: &TokenId) -> Option { - self.get(token_id).copied() + fn get_token_decimals( + &self, + token_id: &TokenId, + ) -> Result { + self.get(token_id).copied().ok_or(TokenDecimalsUnavailableError(*token_id)) } } + +#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)] +#[error("Token decimals unavailable for token {0:x}")] +pub struct TokenDecimalsUnavailableError(TokenId); diff --git a/orders-accounting/src/cache.rs b/orders-accounting/src/cache.rs index 4f625ef56..a7dd4ae64 100644 --- a/orders-accounting/src/cache.rs +++ b/orders-accounting/src/cache.rs @@ -21,7 +21,7 @@ use common::{ primitives::Amount, }; use logging::log; -use utils::ensure; +use utils::{debug_panic_or_log, ensure}; use crate::{ calculate_fill_order, @@ -144,23 +144,35 @@ impl OrdersAccountingView for OrdersAccountingCache

} fn get_all_order_ids(&self) -> Result> { - // FIXME break this and ensure tests fail - Ok(self .parent .get_all_order_ids() .map_err(|_| Error::ViewFail)? .into_iter() .filter(|id| match self.data.order_data.get_data(id) { - accounting::GetDataResult::Present(_) | accounting::GetDataResult::Missing => true, + accounting::GetDataResult::Missing => true, accounting::GetDataResult::Deleted => false, + accounting::GetDataResult::Present(_) => { + // No need to include orders that are present in `self.data.order_data`, + // because they'll be included by the code below anyway. + false + } }) .chain(self.data.order_data.data().keys().copied().filter(|id| { match self.data.order_data.get_data(&id) { accounting::GetDataResult::Present(_) => true, - accounting::GetDataResult::Missing | accounting::GetDataResult::Deleted => { + accounting::GetDataResult::Missing => { + // This shouldn't happen. + debug_panic_or_log!( + concat!( + "Got GetDataResult::Missing for order {id:x} ", + "even though the id came from the collection itself" + ), + id = id + ); false } + accounting::GetDataResult::Deleted => false, } })) .collect()) diff --git a/orders-accounting/src/tests/operations.rs b/orders-accounting/src/tests/operations.rs index e98f6d675..10dc1b347 100644 --- a/orders-accounting/src/tests/operations.rs +++ b/orders-accounting/src/tests/operations.rs @@ -13,8 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; +use accounting::GetDataResult; use common::{ chain::{output_value::OutputValue, tokens::TokenId, Destination, OrderId, OrdersVersion}, primitives::Amount, @@ -881,3 +882,110 @@ fn conclude_freezed_order_and_undo(#[case] seed: Seed) { ); assert_eq!(expected_storage, storage); } + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn get_all_order_ids(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let orig_order1_id = OrderId::random_using(&mut rng); + let orig_order2_id = OrderId::random_using(&mut rng); + let orig_order3_id = OrderId::random_using(&mut rng); + let orig_data1 = make_order_data(&mut rng); + let orig_data2 = make_order_data(&mut rng); + let orig_data3 = make_order_data(&mut rng); + + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([ + (orig_order1_id, orig_data1.clone()), + (orig_order2_id, orig_data2.clone()), + (orig_order3_id, orig_data3.clone()), + ]), + BTreeMap::from_iter([ + (orig_order1_id, output_value_amount(orig_data1.ask())), + (orig_order2_id, output_value_amount(orig_data2.ask())), + (orig_order3_id, output_value_amount(orig_data3.ask())), + ]), + BTreeMap::from_iter([ + (orig_order1_id, output_value_amount(orig_data1.give())), + (orig_order2_id, output_value_amount(orig_data2.give())), + (orig_order3_id, output_value_amount(orig_data3.give())), + ]), + ); + let db = OrdersAccountingDB::new(&mut storage); + + let mut cache = OrdersAccountingCache::new(&db); + + // All 3 orders are returned + assert_eq!( + cache.get_all_order_ids().unwrap(), + BTreeSet::from_iter([orig_order1_id, orig_order2_id, orig_order3_id]) + ); + + // Conclude one of the orders + assert_eq!( + cache.data().order_data.get_data(&orig_order3_id), + GetDataResult::Missing + ); + let _undo = cache.conclude_order(orig_order3_id).unwrap(); + assert_eq!( + cache.data().order_data.get_data(&orig_order3_id), + GetDataResult::Deleted + ); + + // The concluded order should no longer be returned + assert_eq!( + cache.get_all_order_ids().unwrap(), + BTreeSet::from_iter([orig_order1_id, orig_order2_id]) + ); + + // Freeze one of the orders so that it's "Present" in the cache. + assert_eq!( + cache.data().order_data.get_data(&orig_order2_id), + GetDataResult::Missing + ); + let _undo = cache.freeze_order(orig_order2_id).unwrap(); + assert_eq!( + cache.data().order_data.get_data(&orig_order2_id), + GetDataResult::Present(&orig_data2.try_freeze().unwrap()) + ); + + // The result should remain the same + assert_eq!( + cache.get_all_order_ids().unwrap(), + BTreeSet::from_iter([orig_order1_id, orig_order2_id]) + ); + + let new_order1_id = OrderId::random_using(&mut rng); + let new_order2_id = OrderId::random_using(&mut rng); + let new_data1 = make_order_data(&mut rng); + let new_data2 = make_order_data(&mut rng); + + // Create 2 new orders + let _undo = cache.create_order(new_order1_id, new_data1.clone()).unwrap(); + let _undo = cache.create_order(new_order2_id, new_data2.clone()).unwrap(); + + // The result should include the new orders + assert_eq!( + cache.get_all_order_ids().unwrap(), + BTreeSet::from_iter([orig_order1_id, orig_order2_id, new_order1_id, new_order2_id]) + ); + + // Conclude one of the new orders + assert_eq!( + cache.data().order_data.get_data(&new_order2_id), + GetDataResult::Present(&new_data2) + ); + let _undo = cache.conclude_order(new_order2_id).unwrap(); + assert_eq!( + cache.data().order_data.get_data(&new_order2_id), + GetDataResult::Deleted + ); + + // The concluded order should no longer be returned + assert_eq!( + cache.get_all_order_ids().unwrap(), + BTreeSet::from_iter([orig_order1_id, orig_order2_id, new_order1_id]) + ); +} diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 027d1d046..55ae68f79 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -89,8 +89,8 @@ use wallet_types::{ }; pub use self::output_cache::{ - DelegationData, OrderData, OwnFungibleTokenInfo, PoolData, TxInfo, UnconfirmedTokenInfo, - UtxoWithTxOutput, + DelegationData, OrderData, OutputCacheInconsistencyError, OwnFungibleTokenInfo, PoolData, + TxInfo, UnconfirmedTokenInfo, UtxoWithTxOutput, }; use self::output_cache::{OutputCache, TokenIssuanceData}; use self::transaction_list::{get_transaction_list, TransactionList}; diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index 58a479835..da4328b68 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -1118,7 +1118,9 @@ impl OutputCache { { ensure!( is_unconfirmed, - WalletError::ConfirmedTxAmongUnconfirmedDescendants(tx_id.clone()) + OutputCacheInconsistencyError::ConfirmedTxAmongUnconfirmedDescendants( + tx_id.clone() + ) ); descendants.insert(tx_id.clone()); } @@ -1245,7 +1247,10 @@ impl OutputCache { ensure!( delegation_nonce == next_nonce, - WalletError::InconsistentDelegationDuplicateNonce(*delegation_id, delegation_nonce) + OutputCacheInconsistencyError::InconsistentDelegationDuplicateNonce( + *delegation_id, + delegation_nonce + ) ); data.last_nonce = Some(delegation_nonce); @@ -1276,7 +1281,10 @@ impl OutputCache { ensure!( token_nonce == next_nonce, - WalletError::InconsistentTokenIssuanceDuplicateNonce(*delegation_id, token_nonce) + OutputCacheInconsistencyError::InconsistentTokenIssuanceDuplicateNonce( + *delegation_id, + token_nonce + ) ); data.last_nonce = Some(token_nonce); @@ -1309,7 +1317,7 @@ impl OutputCache { ensure!( nonce == next_nonce, - WalletError::InconsistentOrderDuplicateNonce(*order_id, nonce) + OutputCacheInconsistencyError::InconsistentOrderDuplicateNonce(*order_id, nonce) ); data.last_nonce = Some(nonce); @@ -1328,12 +1336,17 @@ impl OutputCache { match command_tag { OrderAccountCommandTag::FillOrder => {} OrderAccountCommandTag::FreezeOrder => { - // FIXME revise debug_asserts here and on revert - debug_assert!(!data.is_frozen); + ensure!( + !data.is_frozen, + OutputCacheInconsistencyError::OrderAlreadyFrozen(*order_id) + ); data.is_frozen = true; } OrderAccountCommandTag::ConcludeOrder => { - debug_assert!(!data.is_concluded); + ensure!( + !data.is_concluded, + OutputCacheInconsistencyError::OrderAlreadyConcluded(*order_id) + ); data.is_concluded = true; } } @@ -1353,7 +1366,7 @@ impl OutputCache { ensure!( !self.unconfirmed_descendants.contains_key(tx_id), - WalletError::ConfirmedTxAmongUnconfirmedDescendants(tx_id.clone()) + OutputCacheInconsistencyError::ConfirmedTxAmongUnconfirmedDescendants(tx_id.clone()) ); Ok(()) @@ -1614,7 +1627,7 @@ impl OutputCache { let op_tag: AccountCommandTag = op.into(); if op_tag == AccountCommandTag::ConcludeOrder { - debug_assert!(data.is_concluded); + ensure!(data.is_concluded, OutputCacheInconsistencyError::OrderMustBeConcludedToRevertConclude(*order_id)); data.is_concluded = false; } } @@ -1631,11 +1644,11 @@ impl OutputCache { match cmd_tag { OrderAccountCommandTag::FillOrder => {} OrderAccountCommandTag::FreezeOrder => { - debug_assert!(data.is_frozen); + ensure!(data.is_frozen, OutputCacheInconsistencyError::OrderMustBeFrozenToRevertFreeze(*order_id)); data.is_frozen = false; } OrderAccountCommandTag::ConcludeOrder => { - debug_assert!(data.is_concluded); + ensure!(data.is_concluded, OutputCacheInconsistencyError::OrderMustBeConcludedToRevertConclude(*order_id)); data.is_concluded = false; } } @@ -2015,5 +2028,32 @@ fn uses_conflicting_nonce( }) } +#[derive(thiserror::Error, Debug, Eq, PartialEq)] +pub enum OutputCacheInconsistencyError { + #[error("Transaction from {0:?} is confirmed and among unconfirmed descendants")] + ConfirmedTxAmongUnconfirmedDescendants(OutPointSourceId), + + #[error("Delegation {0:x} has duplicate AccountNonce: {1}")] + InconsistentDelegationDuplicateNonce(DelegationId, AccountNonce), + + #[error("Token {0:x} has duplicate AccountNonce: {1}")] + InconsistentTokenIssuanceDuplicateNonce(TokenId, AccountNonce), + + #[error("Order {0:x} has duplicate AccountNonce: {1}")] + InconsistentOrderDuplicateNonce(OrderId, AccountNonce), + + #[error("Order {0:x} is already frozen")] + OrderAlreadyFrozen(OrderId), + + #[error("Order {0:x} is already concluded")] + OrderAlreadyConcluded(OrderId), + + #[error("Order {0:x} must already be concluded to revert the conclusion")] + OrderMustBeConcludedToRevertConclude(OrderId), + + #[error("Order {0:x} must already be frozen to revert the freezing")] + OrderMustBeFrozenToRevertFreeze(OrderId), +} + #[cfg(test)] mod tests; diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 53096123a..6f45246eb 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -19,7 +19,8 @@ use std::sync::Arc; use crate::account::{ transaction_list::TransactionList, CoinSelectionAlgo, CurrentFeeRate, DelegationData, - OrderData, PoolData, TxInfo, UnconfirmedTokenInfo, UtxoSelectorError, + OrderData, OutputCacheInconsistencyError, PoolData, TxInfo, UnconfirmedTokenInfo, + UtxoSelectorError, }; use crate::destination_getters::HtlcSpendingCondition; use crate::key_chain::{ @@ -50,11 +51,11 @@ use common::chain::tokens::{ IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance, }; use common::chain::{ - make_delegation_id, make_order_id, make_token_id, AccountCommand, AccountNonce, - AccountOutPoint, Block, ChainConfig, Currency, DelegationId, Destination, GenBlock, - IdCreationError, OrderAccountCommand, OrderId, OutPointSourceId, PoolId, RpcOrderInfo, - SignedTransaction, SignedTransactionIntent, Transaction, TransactionCreationError, TxInput, - TxOutput, UtxoOutPoint, + make_delegation_id, make_order_id, make_token_id, AccountCommand, AccountOutPoint, Block, + ChainConfig, Currency, DelegationId, Destination, GenBlock, IdCreationError, + OrderAccountCommand, OrderId, OutPointSourceId, PoolId, RpcOrderInfo, SignedTransaction, + SignedTransactionIntent, Transaction, TransactionCreationError, TxInput, TxOutput, + UtxoOutPoint, }; use common::primitives::id::{hash_encoded, WithId}; use common::primitives::{Amount, BlockHeight, Id, H256}; @@ -144,8 +145,6 @@ pub enum WalletError { OutputAmountOverflow, #[error("Fee amounts overflow")] FeeAmountOverflow, - #[error("Delegation with id: {0} with duplicate AccountNonce: {1}")] - InconsistentDelegationDuplicateNonce(DelegationId, AccountNonce), #[error("Inconsistent produce block from stake for pool id: {0}, missing CreateStakePool")] InconsistentProduceBlockFromStake(PoolId), #[error("Delegation nonce overflow for id: {0}")] @@ -154,10 +153,6 @@ pub enum WalletError { TokenIssuanceNonceOverflow(TokenId), #[error("Order nonce overflow for id: {0}")] OrderNonceOverflow(OrderId), - #[error("Token with id: {0} with duplicate AccountNonce: {1}")] - InconsistentTokenIssuanceDuplicateNonce(TokenId, AccountNonce), - #[error("Order with id: {0} with duplicate AccountNonce: {1}")] - InconsistentOrderDuplicateNonce(OrderId, AccountNonce), #[error("Unknown token with Id {0}")] UnknownTokenId(TokenId), #[error("Unknown order with Id {0}")] @@ -278,10 +273,10 @@ pub enum WalletError { MismatchedTokenAdditionalData(TokenId), #[error("Unsupported operation for a Hardware wallet")] UnsupportedHardwareWalletOperation, - #[error("Transaction from {0:?} is confirmed and among unconfirmed descendants")] - ConfirmedTxAmongUnconfirmedDescendants(OutPointSourceId), #[error("Id creation error: {0}")] IdCreationError(#[from] IdCreationError), + #[error("Output cache inconsistency error: {0}")] + OutputCacheInconsistencyError(#[from] OutputCacheInconsistencyError), } /// Result type used for the wallet diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index a8a2045c6..ffac00a8f 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -31,7 +31,7 @@ use common::{ stakelock::StakePoolData, timelock::OutputTimeLock, tokens::{RPCIsTokenFrozen, TokenData, TokenIssuanceV0, TokenIssuanceV1}, - AccountSpending, ChainstateUpgradeBuilder, Currency, Destination, Genesis, + AccountNonce, AccountSpending, ChainstateUpgradeBuilder, Currency, Destination, Genesis, OutPointSourceId, TxInput, }, primitives::{per_thousand::PerThousand, Idable, H256}, diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index df00d25e5..164f08ba3 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -31,6 +31,7 @@ use common::{ text_summary::TextSummary, }; use crypto::key::hdkd::u31::U31; +use logging::log; use mempool::tx_options::TxOptionsOverrides; use node_comm::node_traits::NodeInterface; use serialization::{hex::HexEncode, hex_encoded::HexEncoded}; @@ -54,7 +55,7 @@ use crate::{ errors::WalletCliCommandError, helper_types::{ active_order_infos_header, format_token_name, parse_currency, parse_generic_token_transfer, - parse_rpc_currency, + parse_rpc_currency, token_ticker_from_rpc_token_info, }, CreateWalletDeviceSelectMenu, ManageableWalletCommand, OpenWalletDeviceSelectMenu, OpenWalletSubCommand, WalletManagementCommand, @@ -2118,162 +2119,172 @@ where WalletCommand::ListActiveOrders { ask_currency, give_currency, - } => { - let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; - - let ask_currency = ask_currency - .map(|ask_currency| parse_rpc_currency(&ask_currency, chain_config)) - .transpose()?; - let give_currency = give_currency - .map(|give_currency| parse_rpc_currency(&give_currency, chain_config)) - .transpose()?; + } => self.list_all_active_orders(chain_config, ask_currency, give_currency).await, + } + } - let order_infos = wallet - .list_all_active_orders(selected_account, ask_currency, give_currency) - .await?; - let token_ids_iter = get_token_ids_from_order_infos( - order_infos.iter().map(|info| (&info.initially_asked, &info.initially_given)), - ); - let token_ids_str_vec = token_ids_iter - .clone() - .map(|rpc_addr| rpc_addr.as_str().to_owned()) - .collect_vec(); - let token_ids_map = token_ids_iter - .map(|rpc_addr| -> Result<_, WalletCliCommandError> { - Ok(( - rpc_addr.clone(), - rpc_addr - .decode_object(chain_config) - .map_err(WalletCliCommandError::TokenIdDecodingError)?, - )) - }) - .collect::, _>>()?; - let token_infos = wallet - .node_get_tokens_info(token_ids_str_vec) - .await? - .into_iter() - .map(|info| (info.token_id(), info)) - .collect::>(); + async fn list_all_active_orders( + &mut self, + chain_config: &ChainConfig, + ask_currency: Option, + give_currency: Option, + ) -> Result> + where + WalletCliCommandError: From, + { + let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; + + let ask_currency = ask_currency + .map(|ask_currency| parse_rpc_currency(&ask_currency, chain_config)) + .transpose()?; + let give_currency = give_currency + .map(|give_currency| parse_rpc_currency(&give_currency, chain_config)) + .transpose()?; + + let order_infos = wallet + .list_all_active_orders(selected_account, ask_currency, give_currency) + .await?; + let (token_ids_map, token_infos) = { + let token_ids_iter = get_token_ids_from_order_infos( + order_infos.iter().map(|info| (&info.initially_asked, &info.initially_given)), + ); + let token_ids_str_vec = token_ids_iter + .clone() + .map(|rpc_addr| rpc_addr.as_str().to_owned()) + .collect_vec(); + let token_ids_map = token_ids_iter + .map(|rpc_addr| -> Result<_, WalletCliCommandError> { + Ok(( + rpc_addr.clone(), + rpc_addr + .decode_object(chain_config) + .map_err(WalletCliCommandError::TokenIdDecodingError)?, + )) + }) + .collect::, _>>()?; + let token_infos = wallet + .node_get_tokens_info(token_ids_str_vec) + .await? + .into_iter() + .map(|info| (info.token_id(), info)) + .collect::>(); + + (token_ids_map, token_infos) + }; - let currency_decimals = |token_id_opt: Option<&TokenId>| { + // Calculate give/ask price for each order. Note that the `filter_map` call shouldn't filter + // anything out normally; if it does, then the implementation of `node_get_tokens_info` + // has a bug. + let order_infos_with_price = order_infos + .into_iter() + .filter_map(|info| { + let get_decimals = |token_id_opt: Option<&TokenId>| { if let Some(token_id) = token_id_opt { - // FIXME - token_infos - .get(token_id) - .expect("Token id is known to be present") - .token_number_of_decimals() + if let Some(info) = token_infos.get(token_id) { + Some(info.token_number_of_decimals()) + } + else { + log::error!( + "Token {token_id:x} is missing in the result of node_get_tokens_info" + ); + None + } } else { - chain_config.coin_decimals() + Some(chain_config.coin_decimals()) } }; - let token_ticker_str_by_id = |token_id| { - let ticker_bytes = - token_infos.get(token_id).expect("Token info must exist").token_ticker(); - // FIXME allow only alpha num tickers?? - str::from_utf8(ticker_bytes).ok() - }; - let order_infos_with_price = order_infos - .into_iter() - .map(|info| { - let asked_token_id = info.initially_asked.token_id().map(|token_id| { - token_ids_map.get(token_id).expect("Token id is known to be present") - }); - let given_token_id = info.initially_given.token_id().map(|token_id| { - token_ids_map.get(token_id).expect("Token id is known to be present") - }); - let ask_currency_decimals = currency_decimals(asked_token_id); - let give_currency_decimals = currency_decimals(given_token_id); - - let give_ask_price = BigDecimal::new( - info.initially_given.amount().amount().into_atoms().into(), - give_currency_decimals as i64, - ) / BigDecimal::new( - info.initially_asked.amount().amount().into_atoms().into(), - ask_currency_decimals as i64, - ); - // FIXME check that it works correctly - let give_ask_price = give_ask_price.with_scale_round( - give_currency_decimals.into(), - bigdecimal::rounding::RoundingMode::Down, - ); - - (info, give_ask_price) - }) - .collect_vec(); - - use std::cmp::Ordering; - - let compare_currencies = - |token1_id: Option<&RpcAddress>, - token2_id: Option<&RpcAddress>| { - match (token1_id, token2_id) { - (Some(token1_id_as_addr), Some(token2_id_as_addr)) => { - let token1_id = token_ids_map - .get(token1_id_as_addr) - .expect("Token id is known to be present"); - let token2_id = token_ids_map - .get(token2_id_as_addr) - .expect("Token id is known to be present"); - - if token1_id == token2_id { - Ordering::Equal - } else { - let token1_ticker = token_ticker_str_by_id(token1_id); - let token2_ticker = token_ticker_str_by_id(token2_id); - let token_ids_as_addr_cmp = - token1_id_as_addr.cmp(token2_id_as_addr); - - // If tickers are valid strings, compare them first; if they are - // equal, compare the ids. - // Otherwise, put tokens with bad tickers later on the list. - // If both tickers are bad, compare the ids. - match (token1_ticker, token2_ticker) { - (Some(token1_ticker), Some(token2_ticker)) => token1_ticker - .cmp(token2_ticker) - .then(token_ids_as_addr_cmp), - (Some(_), None) => Ordering::Less, - (None, Some(_)) => Ordering::Greater, - (None, None) => token_ids_as_addr_cmp, - } - } - } - // The coin should come first, so it's "less" than tokens. - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - (None, None) => Ordering::Equal, - } - }; - - let order_infos_with_give_ask_price = order_infos_with_price.sorted_by( - |(order1_info, order1_price), (order2_info, order2_price)| { - let order1_asked_token_id = order1_info.initially_asked.token_id(); - let order1_given_token_id = order1_info.initially_given.token_id(); - let order2_asked_token_id = order2_info.initially_asked.token_id(); - let order2_given_token_id = order2_info.initially_given.token_id(); - - compare_currencies(order1_given_token_id, order2_given_token_id) - .then_with(|| { - compare_currencies(order1_asked_token_id, order2_asked_token_id) - }) - .then_with(|| order2_price.cmp(order1_price)) - }, + let asked_token_id = info.initially_asked.token_id().map(|token_id| { + token_ids_map.get(token_id).expect("Token id is known to be present") + }); + let given_token_id = info.initially_given.token_id().map(|token_id| { + token_ids_map.get(token_id).expect("Token id is known to be present") + }); + let ask_currency_decimals = get_decimals(asked_token_id)?; + let give_currency_decimals = get_decimals(given_token_id)?; + + let give_ask_price = BigDecimal::new( + info.initially_given.amount().amount().into_atoms().into(), + give_currency_decimals as i64, + ) / BigDecimal::new( + info.initially_asked.amount().amount().into_atoms().into(), + ask_currency_decimals as i64, + ); + // FIXME check that it works correctly + let give_ask_price = give_ask_price.with_scale_round( + give_currency_decimals.into(), + bigdecimal::rounding::RoundingMode::Down, ); - let formatted_order_infos = order_infos_with_give_ask_price - .iter() - .map(|(info, give_ask_price)| { - format_active_order_info(info, give_ask_price, chain_config, &token_infos) - }) - .collect::, _>>()?; + Some((info, give_ask_price)) + }) + .collect_vec(); - Ok(ConsoleCommand::Print(format!( - "{}\n{}\n", - active_order_infos_header(), - formatted_order_infos.join("\n") - ))) - } - } + use std::cmp::Ordering; + + // This will be used with order_infos_with_price, so we know that the token_infos map + // contains all tokens that we may encounter here. + let compare_currencies = + |token1_id: Option<&RpcAddress>, token2_id: Option<&RpcAddress>| { + let ticker_by_id = |token_id| { + token_ticker_from_rpc_token_info( + token_infos.get(token_id).expect("Token info is known to be present"), + ) + }; + + match (token1_id, token2_id) { + (Some(token1_id_as_addr), Some(token2_id_as_addr)) => { + let token1_id = token_ids_map + .get(token1_id_as_addr) + .expect("Token id is known to be present"); + let token2_id = token_ids_map + .get(token2_id_as_addr) + .expect("Token id is known to be present"); + + if token1_id == token2_id { + Ordering::Equal + } else { + let token1_ticker = ticker_by_id(token1_id); + let token2_ticker = ticker_by_id(token2_id); + + // Compare the tickers first; if they are equal, compare the ids. + token1_ticker + .cmp(token2_ticker) + .then(token1_id_as_addr.cmp(token2_id_as_addr)) + } + } + // The coin should come first, so it's "less" than tokens. + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + } + }; + + let order_infos_with_give_ask_price = order_infos_with_price.sorted_by( + |(order1_info, order1_price), (order2_info, order2_price)| { + let order1_asked_token_id = order1_info.initially_asked.token_id(); + let order1_given_token_id = order1_info.initially_given.token_id(); + let order2_asked_token_id = order2_info.initially_asked.token_id(); + let order2_given_token_id = order2_info.initially_given.token_id(); + + compare_currencies(order1_given_token_id, order2_given_token_id) + .then_with(|| compare_currencies(order1_asked_token_id, order2_asked_token_id)) + .then_with(|| order2_price.cmp(order1_price)) + }, + ); + + let formatted_order_infos = order_infos_with_give_ask_price + .iter() + .map(|(info, give_ask_price)| { + format_active_order_info(info, give_ask_price, chain_config, &token_infos) + }) + .collect::, _>>()?; + + Ok(ConsoleCommand::Print(format!( + "{}\n{}\n", + active_order_infos_header(), + formatted_order_infos.join("\n") + ))) } pub async fn handle_manageable_wallet_command( diff --git a/wallet/wallet-cli-commands/src/helper_types.rs b/wallet/wallet-cli-commands/src/helper_types.rs index 6c0487947..dd4287b7a 100644 --- a/wallet/wallet-cli-commands/src/helper_types.rs +++ b/wallet/wallet-cli-commands/src/helper_types.rs @@ -251,10 +251,8 @@ pub fn format_token_name( .decode_object(chain_config) .map_err(WalletCliCommandError::TokenIdDecodingError)?; - // FIXME ticker is supposed to be alphanum only; at least print an error to log; same for orders sorting. - let result = if let Some(token_ticker) = token_infos - .get(&decoded_token_id) - .and_then(|token_info| str::from_utf8(token_info.token_ticker()).ok()) + let result = if let Some(token_ticker) = + token_infos.get(&decoded_token_id).map(token_ticker_from_rpc_token_info) { format!("{} ({token_ticker})", token_id.as_str()) } else { @@ -264,6 +262,12 @@ pub fn format_token_name( Ok(result) } +pub fn token_ticker_from_rpc_token_info(info: &RPCTokenInfo) -> &str { + // Note: all token tickers must be alphanumeric strings, so this "???" should not be possible + // in reality. + str::from_utf8(info.token_ticker()).unwrap_or("???") +} + pub fn format_delegation_info(delegation_id: String, balance: String) -> String { format!("Delegation Id: {}, Balance: {}", delegation_id, balance) } diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index 252e1d103..3a7e70329 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -27,7 +27,7 @@ use std::{ }; use chainstate::{ - rpc::{make_rpc_amount_out, RpcOutputValueIn, RpcOutputValueOut, TokenDecimals}, + rpc::{RpcOutputValueIn, RpcOutputValueOut}, tx_verifier::check_transaction, ChainInfo, TokenIssuanceError, }; @@ -52,6 +52,7 @@ use common::{ primitives::{ id::WithId, per_thousand::PerThousand, time::Time, Amount, BlockHeight, Id, Idable, }, + TokenDecimals, }; use crypto::{ key::{hdkd::u31::U31, PrivateKey, PublicKey}, @@ -75,8 +76,9 @@ use wallet::{ use wallet_controller::{ types::{ Balances, BlockInfo, CreatedBlockInfo, CreatedWallet, GenericTokenTransfer, - InspectTransaction, NewTransaction, OpenedWallet, SeedWithPassPhrase, SweepFromAddresses, - TransactionToInspect, WalletCreationOptions, WalletInfo, WalletTypeArgs, + InspectTransaction, NewTransaction, OpenedWallet, RpcAmountOut, SeedWithPassPhrase, + SweepFromAddresses, TransactionToInspect, WalletCreationOptions, WalletInfo, + WalletTypeArgs, }, ConnectedPeer, ControllerConfig, ControllerError, NodeInterface, UtxoState, UtxoStates, UtxoType, UtxoTypes, DEFAULT_ACCOUNT_INDEX, @@ -1768,15 +1770,15 @@ where let existing_order_data = match (node_rpc_order_info, wallet_order_data.creation_timestamp) { (Some(node_rpc_order_info), Some(creation_timestamp)) => { - let ask_balance = make_rpc_amount_out( + let ask_balance = RpcAmountOut::from_currency_amount( node_rpc_order_info.ask_balance, - node_rpc_order_info.initially_asked.token_id(), + &node_rpc_order_info.initially_asked.currency(), &self.chain_config, &token_decimals, )?; - let give_balance = make_rpc_amount_out( + let give_balance = RpcAmountOut::from_currency_amount( node_rpc_order_info.give_balance, - node_rpc_order_info.initially_given.token_id(), + &node_rpc_order_info.initially_given.currency(), &self.chain_config, &token_decimals, )?; @@ -1884,15 +1886,15 @@ where &token_decimals, node_rpc_order_info.initially_given.into(), )?; - let ask_balance = make_rpc_amount_out( + let ask_balance = RpcAmountOut::from_currency_amount( node_rpc_order_info.ask_balance, - node_rpc_order_info.initially_asked.token_id(), + &node_rpc_order_info.initially_asked.currency(), &self.chain_config, &token_decimals, )?; - let give_balance = make_rpc_amount_out( + let give_balance = RpcAmountOut::from_currency_amount( node_rpc_order_info.give_balance, - node_rpc_order_info.initially_given.token_id(), + &node_rpc_order_info.initially_given.currency(), &self.chain_config, &token_decimals, )?; diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 535398288..6b6714fca 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -15,7 +15,7 @@ //! Types supporting the RPC interface -use chainstate::rpc::{RpcTypeError, TokenDecimalsProvider}; +use chainstate::rpc::RpcTypeError; use common::{ address::{pubkeyhash::PublicKeyHash, Address, AddressError}, chain::{ @@ -28,6 +28,7 @@ use common::{ TxOutput, UtxoOutPoint, }, primitives::{per_thousand::PerThousand, Amount, BlockHeight, Id, Idable}, + TokenDecimalsProvider, TokenDecimalsUnavailableError, }; use crypto::{ key::{ @@ -175,6 +176,9 @@ pub enum RpcError { #[error(transparent)] ChainstateRpcTypeError(#[from] chainstate::rpc::RpcTypeError), + + #[error(transparent)] + TokenDecimalsUnavailableError(#[from] TokenDecimalsUnavailableError), } impl From> for rpc::Error { From 084b902eb1437375074bac4aca9ff836c29b13fb Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 15 Dec 2025 22:25:03 +0200 Subject: [PATCH 08/10] Add functional tests for order management via CLI and tests for order listing for RPC and CLI --- .../test_framework/test_framework.py | 1 - .../test_framework/wallet_cli_controller.py | 71 +++ .../test_framework/wallet_rpc_controller.py | 125 +++-- test/functional/test_runner.py | 23 +- ...t_order_double_fill_with_same_dest_impl.py | 18 +- .../wallet_order_list_all_active.py | 391 +++++++++++++++ test/functional/wallet_order_list_own_cli.py | 215 +++++++++ test/functional/wallet_order_list_own_rpc.py | 212 ++++++++ .../wallet_order_listing_test_utils.py | 455 ++++++++++++++++++ test/functional/wallet_orders_impl.py | 32 +- ...t_orders_v0.py => wallet_orders_v0_cli.py} | 3 +- test/functional/wallet_orders_v0_rpc.py | 28 ++ ...t_orders_v1.py => wallet_orders_v1_cli.py} | 3 +- test/functional/wallet_orders_v1_rpc.py | 28 ++ .../src/command_handler/mod.rs | 13 +- .../wallet-cli-commands/src/helper_types.rs | 2 +- wallet/wallet-rpc-lib/src/rpc/mod.rs | 24 +- 17 files changed, 1554 insertions(+), 90 deletions(-) create mode 100644 test/functional/wallet_order_list_all_active.py create mode 100644 test/functional/wallet_order_list_own_cli.py create mode 100644 test/functional/wallet_order_list_own_rpc.py create mode 100644 test/functional/wallet_order_listing_test_utils.py rename test/functional/{wallet_orders_v0.py => wallet_orders_v0_cli.py} (87%) create mode 100644 test/functional/wallet_orders_v0_rpc.py rename test/functional/{wallet_orders_v1.py => wallet_orders_v1_cli.py} (87%) create mode 100644 test/functional/wallet_orders_v1_rpc.py diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 7a803bae7..e67e74ab7 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -20,7 +20,6 @@ import time from typing import List -from .address import create_deterministic_address_bcrt1_p2tr_op_true from .authproxy import JSONRPCException from . import coverage from .p2p import NetworkThread diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index f8874e4f8..3247fa701 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -593,3 +593,74 @@ async def make_tx_to_send_tokens_with_intent( assert match is not None return (match.group(1), match.group(2), match.group(3)) + + async def create_order( + self, + ask_token_id: Optional[str], + ask_amount: int | float | Decimal | str, + give_token_id: Optional[str], + give_amount: int | float | Decimal | str, + conclude_address: str + ) -> str: + if ask_token_id is None: + ask_token_id = "coin" + if give_token_id is None: + give_token_id = "coin" + + output = await self._write_command( + f"order-create {ask_token_id} {ask_amount} {give_token_id} {give_amount} {conclude_address}\n") + + match = re.search("New order id: (.*)\n", output) + assert match is not None + return match.group(1) + + async def fill_order( + self, + order_id: str, + fill_amount: int | float | Decimal | str, + output_address: Optional[str] = None + ) -> str: + if output_address is None: + output_address = "" + + output = await self._write_command(f"order-fill {order_id} {fill_amount} {output_address}\n") + return output + + async def freeze_order(self, order_id: str) -> str: + output = await self._write_command(f"order-freeze {order_id}\n") + return output + + async def conclude_order( + self, + order_id: str, + output_address: Optional[str] = None + ) -> str: + if output_address is None: + output_address = "" + + output = await self._write_command(f"order-conclude {order_id} {output_address}\n") + return output + + # Note: the result of this function is incompatible with RPC controller's list_own_orders. + async def list_own_orders(self) -> List[str]: + output = await self._write_command(f"order-list-own\n") + return output.strip().split("\n") + + # Note: the result of this function is incompatible with RPC controller's list_all_active_orders. + async def list_all_active_orders(self, ask_currency_filter: str | None, give_currency_filter: str | None): + if ask_currency_filter is None: + ask_currency_filter = "" + else: + ask_currency_filter = f"--ask-currency {ask_currency_filter}" + + if give_currency_filter is None: + give_currency_filter = "" + else: + give_currency_filter = f"--give-currency {give_currency_filter}" + + output = await self._write_command(f"order-list-all-active {ask_currency_filter} {give_currency_filter}\n") + + result = output.split("\n") + assert_in("The list of active orders goes below", result[0]) + assert_in("WARNING: token tickers are not unique", result[1]) + return result[2:] diff --git a/test/functional/test_framework/wallet_rpc_controller.py b/test/functional/test_framework/wallet_rpc_controller.py index 5e5bbbd7f..c3e8556e7 100644 --- a/test/functional/test_framework/wallet_rpc_controller.py +++ b/test/functional/test_framework/wallet_rpc_controller.py @@ -360,7 +360,8 @@ async def issue_new_token(self, return None, None, result['error'] async def mint_tokens(self, token_id: str, address: str, amount: int) -> NewTxResult: - return self._write_command("token_mint", [self.account, token_id, address, {'decimal': str(amount)}, {'in_top_x_mb': 5}])['result'] + result = self._write_command("token_mint", [self.account, token_id, address, {'decimal': str(amount)}, {'in_top_x_mb': 5}]) + return result['result'] # Note: unlike mint_tokens, this function behaves identically both for wallet_cli_controller and wallet_rpc_controller. async def mint_tokens_or_fail(self, token_id: str, address: str, amount: int): @@ -622,11 +623,13 @@ async def make_tx_to_send_tokens_from_multisig_address_expect_partially_signed( return (result['transaction'], siginfo_to_return) - async def compose_transaction(self, - outputs: List[TransferTxOutput], - selected_utxos: List[UtxoOutpoint], - htlc_secrets: Optional[List[Optional[str]]] = None, - only_transaction: bool = False) -> str: + async def compose_transaction( + self, + outputs: List[TransferTxOutput], + selected_utxos: List[UtxoOutpoint], + htlc_secrets: Optional[List[Optional[str]]] = None, + only_transaction: bool = False + ) -> str: utxos = [to_json(utxo) for utxo in selected_utxos] outputs = [to_json(output) for output in outputs] print(f"outputs = {outputs}") @@ -635,24 +638,27 @@ async def compose_transaction(self, return result async def create_htlc_transaction(self, - amount: int, - token_id: Optional[str], - secret_hash: str, - spend_address: str, - refund_address: str, - refund_lock_for_blocks: int) -> NewTxResult: + amount: int, + token_id: Optional[str], + secret_hash: str, + spend_address: str, + refund_address: str, + refund_lock_for_blocks: int + ) -> NewTxResult: timelock = { "type": "ForBlockCount", "content": refund_lock_for_blocks } htlc = { "secret_hash": secret_hash, "spend_address": spend_address, "refund_address": refund_address, "refund_timelock": timelock } - object = [self.account, {'decimal': str(amount)}, token_id, htlc, {'in_top_x_mb': 5}] - result = self._write_command("create_htlc_transaction", object) + params = [self.account, {'decimal': str(amount)}, token_id, htlc, {'in_top_x_mb': 5}] + result = self._write_command("create_htlc_transaction", params) return result['result'] - async def create_order(self, - ask_token_id: Optional[str], - ask_amount: int, - give_token_id: Optional[str], - give_amount: int, - conclude_address: str) -> str: + async def create_order( + self, + ask_token_id: Optional[str], + ask_amount: int | float | Decimal | str, + give_token_id: Optional[str], + give_amount: int | float | Decimal | str, + conclude_address: str + ) -> str: if ask_token_id is not None: ask = {"type": "Token", "content": {"id": ask_token_id, "amount": {'decimal': str(ask_amount)}}} else: @@ -663,26 +669,67 @@ async def create_order(self, else: give = {"type": "Coin", "content": {"amount": {'decimal': str(give_amount)}}} - object = [self.account, ask, give, conclude_address, {'in_top_x_mb': 5}] - result = self._write_command("order_create", object) - return result + params = [self.account, ask, give, conclude_address, {'in_top_x_mb': 5}] - async def fill_order(self, - order_id: str, - fill_amount: int, - output_address: Optional[str] = None) -> str: - object = [self.account, order_id, {'decimal': str(fill_amount)}, output_address, {'in_top_x_mb': 5}] - result = self._write_command("order_fill", object) - return result + result = self._write_command("order_create", params) + assert result['result']['tx_id'] is not None + order_id = result['result']['order_id'] + + return order_id + + async def fill_order( + self, + order_id: str, + fill_amount: int | float | Decimal | str, + output_address: Optional[str] = None + ) -> str: + params = [self.account, order_id, {'decimal': str(fill_amount)}, output_address, {'in_top_x_mb': 5}] + result = self._write_command("order_fill", params) + return get_tx_submission_success_or_error(result) async def freeze_order(self, order_id: str) -> str: - object = [self.account, order_id, {'in_top_x_mb': 5}] - result = self._write_command("order_freeze", object) - return result + params = [self.account, order_id, {'in_top_x_mb': 5}] + result = self._write_command("order_freeze", params) + return get_tx_submission_success_or_error(result) + + async def conclude_order( + self, + order_id: str, + output_address: Optional[str] = None + ) -> str: + params = [self.account, order_id, output_address, {'in_top_x_mb': 5}] + result = self._write_command("order_conclude", params) + return get_tx_submission_success_or_error(result) + + # Note: the result of this function is incompatible with CLI controller's list_own_orders. + async def list_own_orders(self) -> List[dict]: + params = [self.account, {'in_top_x_mb': 5}] + result = self._write_command("order_list_own", params) + return result['result'] - async def conclude_order(self, - order_id: str, - output_address: Optional[str] = None) -> str: - object = [self.account, order_id, output_address, {'in_top_x_mb': 5}] - result = self._write_command("order_conclude", object) - return result + # Note: the result of this function is incompatible with CLI controller's list_all_active_orders. + async def list_all_active_orders(self, ask_currency: str | None, give_currency: str | None): + def make_currency(currency): + if currency is None: + return None + elif currency == "coin": + return { "type": "Coin" } + else: + return { "type": "Token", "content": currency } + + params = [self.account, make_currency(ask_currency), make_currency(give_currency), {'in_top_x_mb': 5}] + + result = self._write_command("order_list_all_active", params) + return result['result'] + + +def get_tx_submission_success_or_error(submission_result: dict) -> str: + success_res = submission_result.get('result') + error_res = submission_result.get('error') + assert success_res is not None or error_res is not None + + if success_res is not None: + assert success_res['tx_id'] is not None + return "The transaction was submitted successfully" + elif error_res is not None: + return error_res['message'] diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 2c493f0a9..7a4d409eb 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -112,8 +112,17 @@ class UnicodeOnWindowsError(ValueError): # vv Tests less than 2m vv + 'wallet_order_list_all_active.py', + # vv Tests less than 60s vv + 'p2p_submit_orphan.py', + 'wallet_cold_wallet_send_rpc.py', + 'wallet_delegations.py', + 'wallet_get_address_usage.py', + 'wallet_high_fee.py', + 'wallet_multisig_address.py', + # vv Tests less than 30s vv 'blockprod_generate_blocks_all_sources.py', 'blockprod_generate_pos_blocks.py', @@ -124,7 +133,6 @@ class UnicodeOnWindowsError(ValueError): 'blockprod_ibd_genesis.py', 'example_test.py', 'p2p_ping.py', - 'p2p_submit_orphan.py', 'p2p_syncing_test.py', 'p2p_relay_transactions.py', 'feature_db_reinit.py', @@ -136,7 +144,6 @@ class UnicodeOnWindowsError(ValueError): 'wallet_sign_message.py', 'wallet_sign_message_rpc.py', 'wallet_cold_wallet_send.py', - 'wallet_cold_wallet_send_rpc.py', 'wallet_create_pool_for_another_wallet.py', 'wallet_create_pool_for_another_wallet_rpc.py', 'wallet_tx_compose.py', @@ -147,7 +154,6 @@ class UnicodeOnWindowsError(ValueError): 'wallet_sweep_address.py', 'wallet_sweep_delegation.py', 'wallet_recover_accounts.py', - 'wallet_get_address_usage.py', 'wallet_tokens.py', 'wallet_tokens_freeze.py', 'wallet_tokens_transfer_from_multisig_addr.py', @@ -159,15 +165,17 @@ class UnicodeOnWindowsError(ValueError): 'wallet_nfts.py', 'wallet_decommission_genesis.py', 'wallet_decommission_request.py', - 'wallet_delegations.py', 'wallet_delegations_rpc.py', 'wallet_generate_addresses.py', 'wallet_set_lookahead_size.py', 'wallet_connect_to_rpc.py', - 'wallet_multisig_address.py', 'wallet_watch_address.py', - 'wallet_orders_v0.py', - 'wallet_orders_v1.py', + 'wallet_order_list_own_cli.py', + 'wallet_order_list_own_rpc.py', + 'wallet_orders_v0_cli.py', + 'wallet_orders_v1_cli.py', + 'wallet_orders_v0_rpc.py', + 'wallet_orders_v1_rpc.py', 'wallet_order_double_fill_with_same_dest_v0.py', 'wallet_order_double_fill_with_same_dest_v1.py', 'mempool_basic_reorg.py', @@ -178,7 +186,6 @@ class UnicodeOnWindowsError(ValueError): 'mempool_submit_tx.py', 'mempool_timelocked_tx.py', 'mempool_feerate_points.py', - 'wallet_high_fee.py', 'wallet_htlc_spend.py', 'wallet_htlc_refund_multisig.py', 'wallet_htlc_refund_single_sig.py', 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 6c4a9e9b5..281e9ebb4 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 @@ -147,8 +147,7 @@ async def async_test(self): assert_not_in("Tokens", balance) amount_to_mint = random.randint(100, 10000) - mint_result = await wallet.mint_tokens(token_id, alice_address, amount_to_mint) - assert mint_result['tx_id'] is not None + await wallet.mint_tokens_or_fail(token_id, alice_address, amount_to_mint) self.generate_block() assert_in("Success", await wallet.sync()) @@ -158,9 +157,7 @@ async def async_test(self): ######################################################################################## # Alice creates an order selling tokens for coins - create_order_result = await wallet.create_order(None, amount_to_mint * 2, token_id, amount_to_mint, alice_address) - assert create_order_result['result']['tx_id'] is not None - order_id = create_order_result['result']['order_id'] + order_id = await wallet.create_order(None, amount_to_mint * 2, token_id, amount_to_mint, alice_address) self.generate_block() assert_in("Success", await wallet.sync()) @@ -181,14 +178,12 @@ async def async_test(self): # Perform the fill. result = await wallet.fill_order(order_id, fill_amount, fill_dest_address) - fill_tx1_id = result['result']['tx_id'] - assert fill_tx1_id is not None + assert_in("The transaction was submitted successfully", result) if self.use_orders_v1: # Perform another fill for the same amount. result = await wallet.fill_order(order_id, fill_amount, fill_dest_address) - fill_tx2_id = result['result']['tx_id'] - assert fill_tx2_id is not None + assert_in("The transaction was submitted successfully", result) # We're able to successfully mine a block, which will contain both transactions. self.generate_block() @@ -199,15 +194,14 @@ async def async_test(self): # the chainstate only, so creating another "fill" tx when the previos one hasn't been mined yet # will use the same nonce. result = await wallet.fill_order(order_id, fill_amount, fill_dest_address) - assert_in("Mempool error: Nonce is not incremental", result['error']['message']) + assert_in("Mempool error: Nonce is not incremental", result) self.generate_block() assert_in("Success", await wallet.sync()) # After the first tx has been mined, a new one will be created with the correct nonce. result = await wallet.fill_order(order_id, fill_amount, fill_dest_address) - fill_tx2_id = result['result']['tx_id'] - assert fill_tx2_id is not None + assert_in("The transaction was submitted successfully", result) self.generate_block() assert_in("Success", await wallet.sync()) diff --git a/test/functional/wallet_order_list_all_active.py b/test/functional/wallet_order_list_all_active.py new file mode 100644 index 000000000..27eaacd31 --- /dev/null +++ b/test/functional/wallet_order_list_all_active.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 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 all actrive orders listing test, both RPC and CLI +""" + +from test_framework.util import assert_in, assert_equal +from test_framework.wallet_cli_controller import WalletCliController +from test_framework.wallet_rpc_controller import WalletRpcController +from wallet_order_listing_test_utils import * + +import asyncio +import itertools +import random +from decimal import Decimal +from typing import Callable + + +class WalletOrderListAllActive(WalletOrdersListingTestBase): + def run_test(self): + asyncio.run(self.async_test()) + + async def async_test(self): + node = self.nodes[0] + + async with (WalletRpcController(node, self.config, self.log) as wallet1, + WalletCliController(node, self.config, self.log) as wallet2): + await self.setup_wallets([wallet1, wallet2]) + + tokens_info1 = await self.issue_and_mint_tokens(wallet1, [ + TokenInfo(6, "XXX"), + TokenInfo(7, "YYY"), + TokenInfo(8, "YYY"), # same ticker + ]) + w1_token_ids = list(tokens_info1.keys()) + random.shuffle(w1_token_ids) + (w1_token1_id, w1_token2_id, w1_token3_id) = tuple(w1_token_ids) + + tokens_info2 = await self.issue_and_mint_tokens(wallet2, [ + TokenInfo(16, "ABC"), + TokenInfo(17, "ABC"), # same ticker + TokenInfo(18, "XYZ"), + ]) + w2_token_ids = list(tokens_info2.keys()) + random.shuffle(w2_token_ids) + (w2_token1_id, w2_token2_id, w2_token3_id) = tuple(w2_token_ids) + + tokens_info = tokens_info1 | tokens_info2 + + w1_address = await wallet1.new_address() + w2_address = await wallet2.new_address() + + ######################################################################################## + # Some helpers + + def mk_init1( + ask_token_id: str | None, + ask_amount: Decimal | int, + give_token_id: str | None, + give_amount: Decimal | int + ) -> InitialOrderData: + return InitialOrderData(ask_token_id, ask_amount, give_token_id, give_amount, w1_address) + + def mk_init2( + ask_token_id: str | None, + ask_amount: Decimal | int, + give_token_id: str | None, + give_amount: Decimal | int + ) -> InitialOrderData: + return InitialOrderData(ask_token_id, ask_amount, give_token_id, give_amount, w2_address) + + def rand_tkn(token_id: str) -> list[str | Decimal]: + return [token_id, random_token_amount(token_id, 90, 110, tokens_info)] + + def rand_coins() -> list[str | Decimal]: + return [None, random_coins_amount(90, 110)] + + def with_is_own(data: ExpectedActiveOrderData, is_own: bool) -> ExpectedActiveOrderDataWithIsOwn: + return ExpectedActiveOrderDataWithIsOwn( + data.order_id, data.initial_data, data.ask_balance, data.give_balance, is_own + ) + + w1_order_ids = [] + w2_order_ids = [] + order_init_datas: dict[str, InitialOrderData] = {} + order_expected_datas: dict[str, ExpectedActiveOrderData] = {} + + async def mk_order_impl(wallet: WalletRpcController | WalletCliController, init_data: InitialOrderData): + order_id = await self.create_order(wallet, init_data) + self.log.info(f"New order created: {order_id}, init_data = {init_data}") + + order_init_datas[order_id] = init_data + + exp_data = ExpectedActiveOrderData( + order_id, init_data, init_data.ask_amount, init_data.give_amount + ) + order_expected_datas[order_id] = exp_data + + return order_id + + async def mk_order1(init_data: InitialOrderData): + order_id = await mk_order_impl(wallet1, init_data) + w1_order_ids.append(order_id) + return order_id + + async def mk_order2(init_data: InitialOrderData): + order_id = await mk_order_impl(wallet2, init_data) + w2_order_ids.append(order_id) + return order_id + + async def gen_block_and_sync(): + self.generate_block() + assert_in("Success", await wallet1.sync()) + assert_in("Success", await wallet2.sync()) + + ######################################################################################## + # Setup orders in wallet1 (2 orders for each currency pair) + + # Ask for w2_token1, give coins + await mk_order1(mk_init1(*rand_tkn(w2_token1_id), *rand_coins())) + await mk_order1(mk_init1(*rand_tkn(w2_token1_id), *rand_coins())) + + # Ask for w2_token2, give w1_token1 + await mk_order1(mk_init1(*rand_tkn(w2_token2_id), *rand_tkn(w1_token1_id))) + await mk_order1(mk_init1(*rand_tkn(w2_token2_id), *rand_tkn(w1_token1_id))) + + # Ask for w2_token3, give w1_token2 + await mk_order1(mk_init1(*rand_tkn(w2_token3_id), *rand_tkn(w1_token2_id))) + await mk_order1(mk_init1(*rand_tkn(w2_token3_id), *rand_tkn(w1_token2_id))) + + # Ask for coins, give w1_token3 + await mk_order1(mk_init1(*rand_coins(), *rand_tkn(w1_token3_id))) + await mk_order1(mk_init1(*rand_coins(), *rand_tkn(w1_token3_id))) + + ######################################################################################## + # Setup orders in wallet2 (2 orders for each currency pair) + + # Ask for w1_token1, give coins + await mk_order2(mk_init2(*rand_tkn(w1_token1_id), *rand_coins())) + await mk_order2(mk_init2(*rand_tkn(w1_token1_id), *rand_coins())) + + # Ask for w1_token2, give w2_token1 + await mk_order2(mk_init2(*rand_tkn(w1_token2_id), *rand_tkn(w2_token1_id))) + await mk_order2(mk_init2(*rand_tkn(w1_token2_id), *rand_tkn(w2_token1_id))) + + # Ask for w1_token3, give w2_token2 + await mk_order2(mk_init2(*rand_tkn(w1_token3_id), *rand_tkn(w2_token2_id))) + await mk_order2(mk_init2(*rand_tkn(w1_token3_id), *rand_tkn(w2_token2_id))) + + # Ask for coins, give w2_token3 + await mk_order2(mk_init2(*rand_coins(), *rand_tkn(w2_token3_id))) + await mk_order2(mk_init2(*rand_coins(), *rand_tkn(w2_token3_id))) + + ######################################################################################## + + # Before the txs are mined, there are no active orders + w1_actual_active_orders = await wallet1.list_all_active_orders(None, None) + assert_equal(w1_actual_active_orders, []) + + w2_actual_active_orders = await wallet2.list_all_active_orders(None, None) + assert_equal(w2_actual_active_orders, []) + + ######################################################################################## + # Some helper functions to check for specific order sets + + def mk_currency_filter(token_id: str | None) -> str: + return "coin" if token_id is None else token_id + + async def rpc_check_orders_with_filters_impl( + exp_data_filter: Callable[[InitialOrderData], bool], + ask_filter: str | None, + give_filter: str | None, + ): + exp_datas = [ + with_is_own(exp_data, order_id in w1_order_ids) + for order_id, exp_data in order_expected_datas.items() + if exp_data_filter(exp_data.initial_data) + ] + + self.log.info( + f"Checking orders via rpc; ask_filter = {ask_filter}, " + + f"give_filter = {give_filter}, expected items count: {len(exp_datas)}" + ) + + expected = make_expected_rpc_active_order_datas(exp_datas, tokens_info) + actual = await wallet1.list_all_active_orders(ask_filter, give_filter) + assert_equal(actual, expected) + + async def rpc_check_all_orders(): + await rpc_check_orders_with_filters_impl(lambda _: True, None, None) + + async def rpc_check_orders_with_ask_filter(ask_token_id: str | None): + await rpc_check_orders_with_filters_impl( + lambda init_data: init_data.ask_token_id == ask_token_id, + mk_currency_filter(ask_token_id), + None + ) + + async def rpc_check_orders_with_give_filter(give_token_id: str | None): + await rpc_check_orders_with_filters_impl( + lambda init_data: init_data.give_token_id == give_token_id, + None, + mk_currency_filter(give_token_id), + ) + + async def rpc_check_orders_with_filters(ask_token_id: str | None, give_token_id: str | None): + await rpc_check_orders_with_filters_impl( + lambda init_data: + init_data.ask_token_id == ask_token_id and + init_data.give_token_id == give_token_id, + mk_currency_filter(ask_token_id), + mk_currency_filter(give_token_id), + ) + + async def cli_check_orders_with_filters_impl( + exp_data_filter: Callable[[InitialOrderData], bool], + ask_filter: str | None, + give_filter: str | None, + ): + exp_datas = [ + with_is_own(exp_data, order_id in w2_order_ids) + for order_id, exp_data in order_expected_datas.items() + if exp_data_filter(exp_data.initial_data) + ] + + self.log.info( + f"Checking orders via cli; ask_filter = {ask_filter}, " + + f"give_filter = {give_filter}, expected items count: {len(exp_datas)}" + ) + + expected = make_expected_cli_active_order_datas(exp_datas, tokens_info) + actual = await wallet2.list_all_active_orders(ask_filter, give_filter) + assert_equal(actual, expected) + + async def cli_check_all_orders(): + await cli_check_orders_with_filters_impl(lambda _: True, None, None) + + async def cli_check_orders_with_ask_filter(ask_token_id: str | None): + await cli_check_orders_with_filters_impl( + lambda init_data: init_data.ask_token_id == ask_token_id, + mk_currency_filter(ask_token_id), + None + ) + + async def cli_check_orders_with_give_filter(give_token_id: str | None): + await cli_check_orders_with_filters_impl( + lambda init_data: init_data.give_token_id == give_token_id, + None, + mk_currency_filter(give_token_id), + ) + + async def cli_check_orders_with_filters(ask_token_id: str | None, give_token_id: str | None): + await cli_check_orders_with_filters_impl( + lambda init_data: + init_data.ask_token_id == ask_token_id and + init_data.give_token_id == give_token_id, + mk_currency_filter(ask_token_id), + mk_currency_filter(give_token_id), + ) + + async def check_all(): + # All orders + await rpc_check_all_orders() + await cli_check_all_orders() + + # Orders asking for or giving a specific asset + for filter in list(tokens_info.keys()) + [None]: + # Orders asking for the asset + await rpc_check_orders_with_ask_filter(filter) + await cli_check_orders_with_ask_filter(filter) + + # Orders giving the asset + await rpc_check_orders_with_give_filter(filter) + await cli_check_orders_with_give_filter(filter) + + # Orders asking for and giving a specific asset + for filter1, filter2 in itertools.combinations(list(tokens_info.keys()) + [None], 2): + await rpc_check_orders_with_filters(filter1, filter2) + + ######################################################################################## + # Generate a block, the orders should exist now + + await gen_block_and_sync() + + await check_all() + + ######################################################################################## + # Fill some orders randomly + + fill_amounts = {} + + async def random_fill(wallet: WalletRpcController | WalletCliController, order_id: str, address: str): + order_init = order_init_datas[order_id] + fill_decimals = currency_decimals(order_init.ask_token_id, tokens_info) + fill_amount = random_decimal_amount(10, 80, fill_decimals) + + self.log.info(f"Filling order {order_id} with amount {fill_amount}") + result = await wallet.fill_order(order_id, fill_amount, address) + assert_in("The transaction was submitted successfully", result) + + fill_amounts[order_id] = fill_amount + + # Fill some wallet2's orders via wallet1 + for order_id in random.sample(w2_order_ids, random.randint(0, len(w2_order_ids))): + await random_fill(wallet1, order_id, w1_address) + + # Fill some wallet1's orders via wallet2 + for order_id in random.sample(w1_order_ids, random.randint(0, len(w1_order_ids))): + await random_fill(wallet2, order_id, w2_address) + + # Before the txs have been mined, ther expected values stay the same. + await check_all() + + # Generate a block + await gen_block_and_sync() + + # Now we can update the expected data + for order_id, fill_amount in fill_amounts.items(): + exp_data = order_expected_datas[order_id] + filled_amount = ( + Decimal(exp_data.initial_data.give_amount) / + Decimal(exp_data.initial_data.ask_amount) * + fill_amount + ) + filled_amount = round_down_currency(exp_data.initial_data.give_token_id, filled_amount, tokens_info) + order_expected_datas[order_id].ask_balance -= fill_amount + order_expected_datas[order_id].give_balance -= filled_amount + + # Check the orders again + await check_all() + + ######################################################################################## + # Freeze and conclude some orders + + inactive_order_ids = set() + + async def freeze_order(wallet: WalletRpcController | WalletCliController, order_id: str): + self.log.info(f"Freezing order {order_id}") + await wallet.freeze_order(order_id) + inactive_order_ids.add(order_id) + + async def conclude_order(wallet: WalletRpcController | WalletCliController, order_id: str): + self.log.info(f"Concluding order {order_id}") + await wallet.conclude_order(order_id) + inactive_order_ids.add(order_id) + + # Freeze some orders in wallet1 + for order_id in random.sample(w1_order_ids, random.randint(0, len(w1_order_ids) // 2)): + await freeze_order(wallet1, order_id) + + # Conclude some orders in wallet1 + for order_id in random.sample(w1_order_ids, random.randint(0, len(w1_order_ids) // 2)): + await conclude_order(wallet1, order_id) + + # Freeze some orders in wallet2 + for order_id in random.sample(w2_order_ids, random.randint(0, len(w2_order_ids) // 2)): + await freeze_order(wallet2, order_id) + + # Conclude some orders in wallet2 + for order_id in random.sample(w2_order_ids, random.randint(0, len(w2_order_ids) // 2)): + await conclude_order(wallet2, order_id) + + # Before the txs have been mined, ther expected values stay the same. + await check_all() + + # Generate a block + await gen_block_and_sync() + + # Now we can update the expected data + for order_id in inactive_order_ids: + del order_expected_datas[order_id] + + # Check the orders again + await check_all() + + +if __name__ == "__main__": + WalletOrderListAllActive().main() diff --git a/test/functional/wallet_order_list_own_cli.py b/test/functional/wallet_order_list_own_cli.py new file mode 100644 index 000000000..bdbc727ec --- /dev/null +++ b/test/functional/wallet_order_list_own_cli.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 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 own orders listing test, CLI +""" + +from test_framework.util import assert_in, assert_equal +from test_framework.wallet_cli_controller import WalletCliController +from wallet_order_listing_test_utils import * + +import asyncio +from decimal import Decimal + + +class WalletOrderListOwnCli(WalletOrdersListingTestBase): + def run_test(self): + asyncio.run(self.async_test()) + + async def async_test(self): + node = self.nodes[0] + + async with WalletCliController(node, self.config, self.log) as wallet: + await self.setup_wallets([wallet]) + + tokens_info = await self.issue_and_mint_tokens(wallet, [ + TokenInfo(16, "FOO"), + TokenInfo(6, "BAR"), + ]) + (token1_id, token2_id) = tuple(tokens_info.keys()) + + address = await wallet.new_address() + + # Ask for token1, give coins. + order1_init_data = InitialOrderData(token1_id, 200, None, 100, address) + order1_id = await self.create_order(wallet, order1_init_data) + order1_expected_data = ExpectedCliOwnOrderData( + order1_id, order1_init_data, None, "Unconfirmed") + + # Same, but the price is different. + order2_init_data = InitialOrderData(token1_id, 200, None, 111, address) + order2_id = await self.create_order(wallet, order2_init_data) + order2_expected_data = ExpectedCliOwnOrderData( + order2_id, order2_init_data, None, "Unconfirmed") + + expected_own_orders = make_expected_cli_own_order_datas( + [order1_expected_data, order2_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Generate a block; after this, both orders are on the chain and existing_order_data + # is not None anymore. + self.generate_block() + assert_in("Success", await wallet.sync()) + + order12_timestamp = self.best_block_timestamp() + + order1_expected_data.existing_data = ExpectedExistingOwnOrderData( + order1_init_data.ask_amount, order1_init_data.give_amount, + order12_timestamp, False + ) + order1_expected_data.status_in_cli = "Active" + + order2_expected_data.existing_data = ExpectedExistingOwnOrderData( + order2_init_data.ask_amount, order2_init_data.give_amount, + order12_timestamp, False + ) + order2_expected_data.status_in_cli = "Active" + + # Create one more order, asking for token2 and giving coins. + order3_init_data = InitialOrderData(token2_id, 200, None, 222, address) + order3_id = await self.create_order(wallet, order3_init_data) + order3_expected_data = ExpectedCliOwnOrderData( + order3_id, order3_init_data, None, "Unconfirmed") + + expected_own_orders = make_expected_cli_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Generate a block; after this, order3 is also on the chain. + self.generate_block() + assert_in("Success", await wallet.sync()) + + order3_timestamp = self.best_block_timestamp() + order3_expected_data.existing_data = ExpectedExistingOwnOrderData( + order3_init_data.ask_amount, order3_init_data.give_amount, + order3_timestamp, False + ) + order3_expected_data.status_in_cli = "Active" + + expected_own_orders = make_expected_cli_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Fill order3 + result = await wallet.fill_order(order3_id, 10, address) + assert_in("The transaction was submitted successfully", result) + + # The fill tx hasn't been mined yet, so the expected order data remains the same. + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Generate a block, now the fill should take effect. + self.generate_block() + assert_in("Success", await wallet.sync()) + + order3_expected_data.existing_data.ask_balance = 190 + order3_expected_data.existing_data.give_balance = Decimal("210.9") + + expected_own_orders = make_expected_cli_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Freeze order3 + result = await wallet.freeze_order(order3_id) + assert_in("The transaction was submitted successfully", result) + + # For now, order3 is just marked as frozen in the wallet + order3_expected_data.status_in_cli = "Frozen (unconfirmed)" + + expected_own_orders = make_expected_cli_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Generate a block, now the freeze should take effect. + self.generate_block() + assert_in("Success", await wallet.sync()) + + # Now order3 is actually frozen + order3_expected_data.status_in_cli = "Frozen" + + expected_own_orders = make_expected_cli_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Conclude order3 + result = await wallet.conclude_order(order3_id) + assert_in("The transaction was submitted successfully", result) + + # For now, order3 is just marked as concluded in the wallet + order3_expected_data.status_in_cli = "Frozen, Concluded (unconfirmed)" + + expected_own_orders = make_expected_cli_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Generate a block, now the conclusion should take effect. + self.generate_block() + assert_in("Success", await wallet.sync()) + + # Order3 is no longer there + expected_own_orders = make_expected_cli_own_order_datas( + [order1_expected_data, order2_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Conclude order2 + result = await wallet.conclude_order(order2_id) + assert_in("The transaction was submitted successfully", result) + + # Freeze and conclude order1 + result = await wallet.freeze_order(order1_id) + assert_in("The transaction was submitted successfully", result) + result = await wallet.conclude_order(order1_id) + assert_in("The transaction was submitted successfully", result) + + # Check the orders without generating a block, both "frozen" and "concluded" + # status should be unconfirmed. + order2_expected_data.status_in_cli = "Concluded (unconfirmed)" + order1_expected_data.status_in_cli = "Frozen (unconfirmed), Concluded (unconfirmed)" + + expected_own_orders = make_expected_cli_own_order_datas( + [order1_expected_data, order2_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + +if __name__ == "__main__": + WalletOrderListOwnCli().main() diff --git a/test/functional/wallet_order_list_own_rpc.py b/test/functional/wallet_order_list_own_rpc.py new file mode 100644 index 000000000..22470fa02 --- /dev/null +++ b/test/functional/wallet_order_list_own_rpc.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 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 own orders listing test, RPC +""" + +from test_framework.util import assert_in, assert_equal +from test_framework.wallet_rpc_controller import WalletRpcController +from wallet_order_listing_test_utils import * + +import asyncio +from decimal import Decimal + +class WalletOrderListOwnRpc(WalletOrdersListingTestBase): + def run_test(self): + asyncio.run(self.async_test()) + + async def async_test(self): + node = self.nodes[0] + + async with WalletRpcController(node, self.config, self.log) as wallet: + await self.setup_wallets([wallet]) + + tokens_info = await self.issue_and_mint_tokens(wallet, [ + TokenInfo(16, "FOO"), + TokenInfo(6, "BAR"), + ]) + (token1_id, token2_id) = tuple(tokens_info.keys()) + + address = await wallet.new_address() + + # Ask for token1, give coins. + order1_init_data = InitialOrderData(token1_id, 200, None, 100, address) + order1_id = await self.create_order(wallet, order1_init_data) + order1_expected_data = ExpectedRpcOwnOrderData( + order1_id, order1_init_data, None, False, False) + + # Same, but the price is different. + order2_init_data = InitialOrderData(token1_id, 200, None, 111, address) + order2_id = await self.create_order(wallet, order2_init_data) + order2_expected_data = ExpectedRpcOwnOrderData( + order2_id, order2_init_data, None, False, False) + + expected_own_orders = make_expected_rpc_own_order_datas( + [order1_expected_data, order2_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Generate a block; after this, both orders are on the chain and existing_order_data + # is not None anymore. + self.generate_block() + assert_in("Success", await wallet.sync()) + + order12_timestamp = self.best_block_timestamp() + + order1_expected_data.existing_data = ExpectedExistingOwnOrderData( + order1_init_data.ask_amount, order1_init_data.give_amount, + order12_timestamp, False + ) + + order2_expected_data.existing_data = ExpectedExistingOwnOrderData( + order2_init_data.ask_amount, order2_init_data.give_amount, + order12_timestamp, False + ) + + # Create one more order, asking for token2 and giving coins. + order3_init_data = InitialOrderData(token2_id, 200, None, 222, address) + order3_id = await self.create_order(wallet, order3_init_data) + order3_expected_data = ExpectedRpcOwnOrderData( + order3_id, order3_init_data, None, False, False) + + expected_own_orders = make_expected_rpc_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Generate a block; after this, order3 is also on the chain. + self.generate_block() + assert_in("Success", await wallet.sync()) + + order3_timestamp = self.best_block_timestamp() + order3_expected_data.existing_data = ExpectedExistingOwnOrderData( + order3_init_data.ask_amount, order3_init_data.give_amount, + order3_timestamp, False + ) + + expected_own_orders = make_expected_rpc_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Fill order3 + result = await wallet.fill_order(order3_id, 10, address) + assert_in("The transaction was submitted successfully", result) + + # The fill tx hasn't been mined yet, so the expected order data remains the same. + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Generate a block, now the fill should take effect. + self.generate_block() + assert_in("Success", await wallet.sync()) + + order3_expected_data.existing_data.ask_balance = 190 + order3_expected_data.existing_data.give_balance = Decimal("210.9") + + expected_own_orders = make_expected_rpc_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Freeze order3 + result = await wallet.freeze_order(order3_id) + assert_in("The transaction was submitted successfully", result) + + # For now, order3 is just marked as frozen in the wallet + order3_expected_data.is_marked_as_frozen_in_wallet = True + + expected_own_orders = make_expected_rpc_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Generate a block, now the freeze should take effect. + self.generate_block() + assert_in("Success", await wallet.sync()) + + # Now order3 is actually frozen + order3_expected_data.existing_data.is_frozen = True + + expected_own_orders = make_expected_rpc_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Conclude order3 + result = await wallet.conclude_order(order3_id) + assert_in("The transaction was submitted successfully", result) + + # For now, order3 is just marked as concluded in the wallet + order3_expected_data.is_marked_as_concluded_in_wallet = True + + expected_own_orders = make_expected_rpc_own_order_datas( + [order1_expected_data, order2_expected_data, order3_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Generate a block, now the conclusion should take effect. + self.generate_block() + assert_in("Success", await wallet.sync()) + + # Order3 is no longer there + expected_own_orders = make_expected_rpc_own_order_datas( + [order1_expected_data, order2_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + # Conclude order2 + result = await wallet.conclude_order(order2_id) + assert_in("The transaction was submitted successfully", result) + + # Freeze and conclude order1 + result = await wallet.freeze_order(order1_id) + assert_in("The transaction was submitted successfully", result) + result = await wallet.conclude_order(order1_id) + assert_in("The transaction was submitted successfully", result) + + # Check the orders without generating a block, the orders should still exist and + # the appropriate "is_marked_as_xxx_in_wallet" should be set. + order2_expected_data.is_marked_as_concluded_in_wallet = True + order1_expected_data.is_marked_as_concluded_in_wallet = True + order1_expected_data.is_marked_as_frozen_in_wallet = True + + expected_own_orders = make_expected_rpc_own_order_datas( + [order1_expected_data, order2_expected_data], + tokens_info + ) + actual_own_orders = await wallet.list_own_orders() + assert_equal(actual_own_orders, expected_own_orders) + + +if __name__ == "__main__": + WalletOrderListOwnRpc().main() diff --git a/test/functional/wallet_order_listing_test_utils.py b/test/functional/wallet_order_listing_test_utils.py new file mode 100644 index 000000000..6c5d20138 --- /dev/null +++ b/test/functional/wallet_order_listing_test_utils.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 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. +"""Utilities and the base class for order listing tests +""" + +from test_framework.test_framework import BitcoinTestFramework +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 COINS_NUM_DECIMALS, block_input_data_obj, random_decimal_amount +from test_framework.wallet_cli_controller import WalletCliController +from test_framework.wallet_rpc_controller import WalletRpcController + +import random +from dataclasses import dataclass +from datetime import datetime, timezone +from decimal import Decimal, ROUND_DOWN + + +MIN_TOKENS_TO_MINT = 10000 + + +def random_mint_amount(): + return random.randint(MIN_TOKENS_TO_MINT, MIN_TOKENS_TO_MINT*10) + + +@dataclass +class TokenInfo: + decimals: int + ticker: str + +@dataclass +class InitialOrderData: + ask_token_id: str | None + ask_amount: Decimal | int + give_token_id: str | None + give_amount: Decimal | int + conclude_key: str + +@dataclass +class ExpectedExistingOwnOrderData: + ask_balance: Decimal | int + give_balance: Decimal | int + creation_timestamp: int + is_frozen: bool + +@dataclass +class ExpectedRpcOwnOrderData: + order_id: str + initial_data: InitialOrderData + existing_data: ExpectedExistingOwnOrderData + is_marked_as_frozen_in_wallet: bool + is_marked_as_concluded_in_wallet: bool + +@dataclass +class ExpectedCliOwnOrderData: + order_id: str + initial_data: InitialOrderData + existing_data: ExpectedExistingOwnOrderData + status_in_cli: str + +@dataclass +class ExpectedActiveOrderData: + order_id: str + initial_data: InitialOrderData + ask_balance: Decimal | int + give_balance: Decimal | int + +@dataclass +class ExpectedActiveOrderDataWithIsOwn: + order_id: str + initial_data: InitialOrderData + ask_balance: Decimal | int + give_balance: Decimal | int + is_own: bool + +class WalletOrdersListingTestBase(BitcoinTestFramework): + def set_test_params(self): + 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]) + + self.node = self.nodes[0] + + def generate_block(self): + 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 = self.node.blockprod_generate_block( + block_input_data, [], [], "FillSpaceFromMempool") + self.node.chainstate_submit_block(block) + block_id = self.node.chainstate_best_block_id() + + # Wait for mempool to sync + self.wait_until(lambda: self.node.mempool_local_best_block_id() + == block_id, timeout=5) + + return block_id + + async def setup_wallets( + self, wallets: list[WalletRpcController | WalletCliController] + ): + pub_keys_bytes = [] + for i, wallet in enumerate(wallets): + await wallet.create_wallet(f"wallet{i}") + assert_equal("0", await wallet.get_best_block_height()) + + address = await wallet.new_address() + pub_key_bytes = await wallet.new_public_key(address) + assert_equal(len(pub_key_bytes), 33) + pub_keys_bytes.append(pub_key_bytes) + + tip_id = self.node.chainstate_best_block_id() + + # Submit a valid transaction + coins_to_send = 1000 + outputs = [ + {"Transfer": [ + {"Coin": coins_to_send * ATOMS_PER_COIN}, + {"PublicKey": {"key": {"Secp256k1Schnorr": { + "pubkey_data": pub_key_bytes}}}} + ]} + for pub_key_bytes in pub_keys_bytes + ] + encoded_tx, tx_id = make_tx([reward_input(tip_id)], outputs, 0) + + self.node.mempool_submit_transaction(encoded_tx, {}) + assert self.node.mempool_contains_tx(tx_id) + + block_id = self.generate_block() + assert not self.node.mempool_contains_tx(tx_id) + + # Sync the wallets and check best block and balance + for wallet in wallets: + assert_in("Success", await wallet.sync()) + assert_equal(await wallet.get_best_block_height(), "1") + assert_equal(await wallet.get_best_block(), block_id) + balance = await wallet.get_balance() + assert_in(f"Coins amount: {coins_to_send}", balance) + + async def issue_and_mint_tokens( + self, wallet: WalletRpcController | WalletCliController, token_infos: list[TokenInfo] + ) -> dict[str, TokenInfo]: + result = {} + address = await wallet.new_address() + + # Issue tokens + for info in token_infos: + token_id, _, _ = await wallet.issue_new_token( + info.ticker, info.decimals, "http://uri", address + ) + assert token_id is not None + self.log.info(f"New token issued: {token_id}") + result[token_id] = info + + self.generate_block() + assert_in("Success", await wallet.sync()) + + # Mint tokens + minted_amounts = [] + for id, info in result.items(): + amount_to_mint = random_mint_amount() + await wallet.mint_tokens_or_fail(id, address, amount_to_mint) + minted_amounts.append(amount_to_mint) + + self.generate_block() + assert_in("Success", await wallet.sync()) + + # Check balances + balance = await wallet.get_balance() + for (id, info), minted_amount in zip(result.items(), minted_amounts): + assert_in(f"Token: {id} ({info.ticker}), amount: {minted_amount}", balance) + + return result + + async def create_order(self, wallet: WalletRpcController | WalletCliController, data: InitialOrderData): + order1_id = await wallet.create_order( + data.ask_token_id, data.ask_amount, data.give_token_id, data.give_amount, data.conclude_key) + return order1_id + + def best_block_timestamp(self) -> int : + tip_id = self.node.chainstate_best_block_id() + tip = self.node.chainstate_get_block_json(tip_id) + return tip["timestamp"]["timestamp"] + + +def make_expected_rpc_amount( + token_id: str | None, + amount: int | float | Decimal | str, + tokens_info: dict[str, TokenInfo], +): + num_decimals = currency_decimals(token_id, tokens_info) + amount = Decimal(amount) + amount_atoms = amount.scaleb(num_decimals) + return {"atoms": f"{amount_atoms:f}", "decimal": decimal_to_str(amount)} + + +def make_expected_rpc_output_val( + token_id: str | None, + amount: int | float | Decimal | str, + tokens_info: dict[str, TokenInfo], +): + amount_obj = make_expected_rpc_amount(token_id, amount, tokens_info) + + if token_id is not None: + return {"type": "Token", "content": {"id": token_id, "amount": amount_obj}} + else: + return {"type": "Coin", "content": {"amount": amount_obj}} + + +def sort_expected_own_order_datas_for_rpc(datas: list[ExpectedRpcOwnOrderData]) -> list[ExpectedRpcOwnOrderData]: + # Own orders returned via RPC are sorted by order id + return sorted(datas, key=lambda item: item.order_id) + + +def make_expected_rpc_own_order_data( + data: ExpectedRpcOwnOrderData, + tokens_info: dict[str, TokenInfo], +) -> dict: + initially_asked = make_expected_rpc_output_val( + data.initial_data.ask_token_id, data.initial_data.ask_amount, tokens_info) + initially_given = make_expected_rpc_output_val( + data.initial_data.give_token_id, data.initial_data.give_amount, tokens_info) + + existing_order_data = data.existing_data + if existing_order_data is not None: + ask_balance = make_expected_rpc_amount( + data.initial_data.ask_token_id, existing_order_data.ask_balance, tokens_info) + give_balance = make_expected_rpc_amount( + data.initial_data.give_token_id, existing_order_data.give_balance, tokens_info) + + existing_order_data = { + "ask_balance": ask_balance, + "give_balance": give_balance, + "creation_timestamp": {"timestamp": existing_order_data.creation_timestamp}, + "is_frozen": existing_order_data.is_frozen + } + + return { + "order_id": data.order_id, + "initially_asked": initially_asked, + "initially_given": initially_given, + "existing_order_data": existing_order_data, + "is_marked_as_frozen_in_wallet": data.is_marked_as_frozen_in_wallet, + "is_marked_as_concluded_in_wallet": data.is_marked_as_concluded_in_wallet + } + + +def make_expected_rpc_own_order_datas(datas: list[ExpectedRpcOwnOrderData], tokens_info: dict[str, TokenInfo]): + sorted_datas = sort_expected_own_order_datas_for_rpc(datas) + return [ + make_expected_rpc_own_order_data(data, tokens_info) + for data in sorted_datas + ] + + +def sort_expected_active_order_datas_for_rpc(datas: list[ExpectedActiveOrderDataWithIsOwn]) -> list[ExpectedRpcOwnOrderData]: + # Active orders returned via RPC are sorted by order id + return sorted(datas, key=lambda item: item.order_id) + + +def make_expected_rpc_active_order_data( + data: ExpectedActiveOrderDataWithIsOwn, + tokens_info: dict[str, TokenInfo], +) -> dict: + initially_asked = make_expected_rpc_output_val( + data.initial_data.ask_token_id, data.initial_data.ask_amount, tokens_info) + initially_given = make_expected_rpc_output_val( + data.initial_data.give_token_id, data.initial_data.give_amount, tokens_info) + + ask_balance = make_expected_rpc_amount( + data.initial_data.ask_token_id, data.ask_balance, tokens_info) + give_balance = make_expected_rpc_amount( + data.initial_data.give_token_id, data.give_balance, tokens_info) + + return { + "order_id": data.order_id, + "initially_asked": initially_asked, + "initially_given": initially_given, + "ask_balance": ask_balance, + "give_balance": give_balance, + "is_own": data.is_own, + } + + +def make_expected_rpc_active_order_datas(datas: list[ExpectedActiveOrderDataWithIsOwn], tokens_info: dict[str, TokenInfo]): + sorted_datas = sort_expected_active_order_datas_for_rpc(datas) + return [ + make_expected_rpc_active_order_data(data, tokens_info) + for data in sorted_datas + ] + + +def sort_expected_own_order_datas_for_cli(datas: list[ExpectedCliOwnOrderData]) -> list[ExpectedCliOwnOrderData]: + # Own orders returned via CLI are first sorted by timestamp and then by order id. + # Orders without timestamp (i.e. unconfirmed ones) appear last. + def sort_key(data: ExpectedCliOwnOrderData): + effective_ts = data.existing_data.creation_timestamp if data.existing_data is not None else 2**64 + return (effective_ts, data.order_id) + + return sorted(datas, key=sort_key) + + +def make_currency_name_for_cli(token_id: str | None, tokens_info: dict[str, TokenInfo]): + if token_id is None: + return "RML" + else: + return f"{token_id} ({tokens_info[token_id].ticker})" + + +def make_expected_cli_own_order_data( + data: ExpectedCliOwnOrderData, + tokens_info: dict[str, TokenInfo], +): + ask_currency_name = make_currency_name_for_cli(data.initial_data.ask_token_id, tokens_info) + give_currency_name = make_currency_name_for_cli(data.initial_data.give_token_id, tokens_info) + + ask_extra_info = "" + give_extra_info = "" + created_at = "" + + if data.existing_data is not None: + ask_left = data.existing_data.ask_balance + ask_can_withdraw = data.initial_data.ask_amount - ask_left + ask_extra_info = f" [left: {decimal_to_str(ask_left)}, can withdraw: {decimal_to_str(ask_can_withdraw)}]" + + give_extra_info = f" [left: {decimal_to_str(data.existing_data.give_balance)}]" + + created_at = datetime.fromtimestamp(data.existing_data.creation_timestamp, timezone.utc) + created_at = created_at.strftime('%Y-%m-%d %H:%M:%S UTC') + created_at = f"Created at: {created_at}, " + + result = ( + f"Id: {data.order_id}, " + + f"Asked: {decimal_to_str(data.initial_data.ask_amount)} {ask_currency_name}{ask_extra_info}, " + + f"Given: {decimal_to_str(data.initial_data.give_amount)} {give_currency_name}{give_extra_info}, " + + created_at + + f"Status: {data.status_in_cli}" + ) + return result + + +def make_expected_cli_own_order_datas(datas: list[ExpectedCliOwnOrderData], tokens_info: dict[str, TokenInfo]): + sorted_datas = sort_expected_own_order_datas_for_cli(datas) + return [ + make_expected_cli_own_order_data(data, tokens_info) + for data in sorted_datas + ] + + +def sort_expected_active_order_datas_for_cli( + datas: list[ExpectedActiveOrderDataWithIsOwn], tokens_info: dict[str, TokenInfo] +) -> list[ExpectedActiveOrderDataWithIsOwn]: + # Active orders returned via CLI are first sorted by given currency, then by asked currency, + # then by give/ask price, then by order id. + # Sorting by currency means: coins come first, tokens are sorted by ticker first, then by id. + # The give/ask price sorting is in the descending order, the rest is in the ascending order. + + def make_currency_key(token_id: str | None) -> str: + if token_id is None: + return ("", "") + else: + ticker = tokens_info[token_id].ticker + return (ticker, token_id) + + def sort_key(data: ExpectedActiveOrderDataWithIsOwn): + ask_currency_key = make_currency_key(data.initial_data.ask_token_id) + give_currency_key = make_currency_key(data.initial_data.give_token_id) + + give_ask_price = Decimal(data.initial_data.give_amount) / Decimal(data.initial_data.ask_amount) + + return (give_currency_key, ask_currency_key, -give_ask_price, data.order_id) + + return sorted(datas, key=sort_key) + + +def make_expected_cli_active_order_data( + data: ExpectedActiveOrderDataWithIsOwn, + tokens_info: dict[str, TokenInfo], +): + ask_currency_name = make_currency_name_for_cli(data.initial_data.ask_token_id, tokens_info) + give_currency_name = make_currency_name_for_cli(data.initial_data.give_token_id, tokens_info) + + own_order_marker = "* " if data.is_own else " " + + give_ask_price = Decimal(data.initial_data.give_amount) / Decimal(data.initial_data.ask_amount) + give_ask_price = round_down_currency(data.initial_data.give_token_id, give_ask_price, tokens_info) + + result = ( + f"{own_order_marker}" + + f"Id: {data.order_id}, " + + f"Given: {give_currency_name} [left: {decimal_to_str(data.give_balance)}], " + + f"Asked: {ask_currency_name} [left: {decimal_to_str(data.ask_balance)}], " + + f"Give/Ask: {decimal_to_str(give_ask_price)}" + ) + return result + + +def make_expected_cli_active_order_datas(datas: list[ExpectedActiveOrderDataWithIsOwn], tokens_info: dict[str, TokenInfo]): + sorted_datas = sort_expected_active_order_datas_for_cli(datas, tokens_info) + return [ + make_expected_cli_active_order_data(data, tokens_info) + for data in sorted_datas + ] + + +def decimal_to_str(d: Decimal | int) -> str: + # Produce a fixed-point string without trailing zeros and trailing decimal point. + # TODO: is there a nicer way? + + result = f"{Decimal(d):f}" + if d.as_integer_ratio()[1] == 1: + # If it's a whole number, return it as is + return result + else: + # Strip trailing zeros, then strip the dot. + return result.rstrip('0').rstrip('.') + + +def random_token_amount(token_id: str, min: int, max: int, tokens_info: dict[str, TokenInfo]) -> Decimal: + return random_decimal_amount(min, max, tokens_info[token_id].decimals) + + +def random_coins_amount(min: int, max: int) -> Decimal: + return random_decimal_amount(min, max, COINS_NUM_DECIMALS) + + +def round_down_currency(token_id: str | None, amount: Decimal, tokens_info: dict[str, TokenInfo]) -> Decimal: + decimals = currency_decimals(token_id, tokens_info) + return amount.quantize(Decimal(10) ** -decimals, ROUND_DOWN) + + +def currency_decimals(token_id: str | None, tokens_info: dict[str, TokenInfo]) -> int: + return COINS_NUM_DECIMALS if token_id is None else tokens_info[token_id].decimals + diff --git a/test/functional/wallet_orders_impl.py b/test/functional/wallet_orders_impl.py index 8618ae05f..18ff4f8db 100644 --- a/test/functional/wallet_orders_impl.py +++ b/test/functional/wallet_orders_impl.py @@ -30,6 +30,7 @@ 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 block_input_data_obj +from test_framework.wallet_cli_controller import WalletCliController from test_framework.wallet_rpc_controller import WalletRpcController import asyncio @@ -38,8 +39,9 @@ ATOMS_PER_TOKEN = 100 class WalletOrdersImpl(BitcoinTestFramework): - def set_test_params(self, use_orders_v1): + def set_test_params(self, use_orders_v1, wallet_controller): self.use_orders_v1 = use_orders_v1 + self.wallet_controller = wallet_controller self.setup_clean_chain = True self.num_nodes = 1 @@ -83,7 +85,8 @@ async def async_test(self): node = self.nodes[0] # new wallet - async with WalletRpcController(node, self.config, self.log, [], self.chain_config_args()) as wallet: + wallet: WalletRpcController | WalletCliController + async with self.wallet_controller(node, self.config, self.log, [], self.chain_config_args()) as wallet: await wallet.create_wallet('alice_wallet') # check it is on genesis @@ -161,8 +164,7 @@ async def async_test(self): assert_not_in("Tokens", balance) amount_to_mint = random.randint(100, 10000) - mint_result = await wallet.mint_tokens(token_id, alice_address, amount_to_mint) - assert mint_result['tx_id'] is not None + await wallet.mint_tokens_or_fail(token_id, alice_address, amount_to_mint) self.generate_block() assert_in("Success", await wallet.sync()) @@ -172,9 +174,7 @@ async def async_test(self): ######################################################################################## # Alice creates an order selling tokens for coins - create_order_result = await wallet.create_order(None, amount_to_mint * 2, token_id, amount_to_mint, alice_address) - assert create_order_result['result']['tx_id'] is not None - order_id = create_order_result['result']['order_id'] + order_id = await wallet.create_order(None, amount_to_mint * 2, token_id, amount_to_mint, alice_address) self.generate_block() assert_in("Success", await wallet.sync()) @@ -191,7 +191,7 @@ async def async_test(self): # buy 1 token fill_order_result = await wallet.fill_order(order_id, 2) - assert fill_order_result['result']['tx_id'] is not None + assert_in("The transaction was submitted successfully", fill_order_result) self.generate_block() assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() @@ -200,7 +200,7 @@ async def async_test(self): # try conclude order conclude_order_result = await wallet.conclude_order(order_id) - assert_in("Failed to convert partially signed tx to signed", conclude_order_result['error']['message']) + assert_in("Failed to convert partially signed tx to signed", conclude_order_result) ######################################################################################## # Carol fills the order partially @@ -211,7 +211,7 @@ async def async_test(self): # buy 5 token fill_order_result = await wallet.fill_order(order_id, 10) - assert fill_order_result['result']['tx_id'] is not None + assert_in("The transaction was submitted successfully", fill_order_result) self.generate_block() assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() @@ -220,11 +220,11 @@ async def async_test(self): # try freeze order freeze_order_result = await wallet.freeze_order(order_id) - assert_in("Failed to convert partially signed tx to signed", freeze_order_result['error']['message']) + assert_in("Failed to convert partially signed tx to signed", freeze_order_result) # try conclude order conclude_order_result = await wallet.conclude_order(order_id) - assert_in("Failed to convert partially signed tx to signed", conclude_order_result['error']['message']) + assert_in("Failed to convert partially signed tx to signed", conclude_order_result) if self.use_orders_v1: ######################################################################################## @@ -233,7 +233,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) freeze_order_result = await wallet.freeze_order(order_id) - assert freeze_order_result['result']['tx_id'] is not None + assert_in("The transaction was submitted successfully", freeze_order_result) self.generate_block() assert_in("Success", await wallet.sync()) @@ -241,7 +241,7 @@ async def async_test(self): # Carol tries filling again await self.switch_to_wallet(wallet, 'carol_wallet') fill_order_result = await wallet.fill_order(order_id, 1) - assert_in("Attempt to fill frozen order", fill_order_result['error']['message']) + assert_in("Attempt to fill frozen order", fill_order_result) ######################################################################################## # Alice concludes the order @@ -249,7 +249,7 @@ async def async_test(self): assert_in("Success", await wallet.sync()) conclude_order_result = await wallet.conclude_order(order_id) - assert conclude_order_result['result']['tx_id'] is not None + assert_in("The transaction was submitted successfully", conclude_order_result) self.generate_block() assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() @@ -260,4 +260,4 @@ async def async_test(self): # Carol tries filling again await self.switch_to_wallet(wallet, 'carol_wallet') fill_order_result = await wallet.fill_order(order_id, 1) - assert_in("Unknown order", fill_order_result['error']['message']) + assert_in("Unknown order", fill_order_result) diff --git a/test/functional/wallet_orders_v0.py b/test/functional/wallet_orders_v0_cli.py similarity index 87% rename from test/functional/wallet_orders_v0.py rename to test/functional/wallet_orders_v0_cli.py index 60b74f6da..bb27d3fcf 100644 --- a/test/functional/wallet_orders_v0.py +++ b/test/functional/wallet_orders_v0_cli.py @@ -15,12 +15,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +from test_framework.wallet_cli_controller import WalletCliController from wallet_orders_impl import WalletOrdersImpl class WalletOrdersV0(WalletOrdersImpl): def set_test_params(self): - super().set_test_params(False) + super().set_test_params(False, WalletCliController) if __name__ == '__main__': diff --git a/test/functional/wallet_orders_v0_rpc.py b/test/functional/wallet_orders_v0_rpc.py new file mode 100644 index 000000000..79470a0fa --- /dev/null +++ b/test/functional/wallet_orders_v0_rpc.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 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. + +from test_framework.wallet_rpc_controller import WalletRpcController +from wallet_orders_impl import WalletOrdersImpl + + +class WalletOrdersV0(WalletOrdersImpl): + def set_test_params(self): + super().set_test_params(False, WalletRpcController) + + +if __name__ == '__main__': + WalletOrdersV0().main() diff --git a/test/functional/wallet_orders_v1.py b/test/functional/wallet_orders_v1_cli.py similarity index 87% rename from test/functional/wallet_orders_v1.py rename to test/functional/wallet_orders_v1_cli.py index 757491e2d..235d7ab20 100644 --- a/test/functional/wallet_orders_v1.py +++ b/test/functional/wallet_orders_v1_cli.py @@ -15,12 +15,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +from test_framework.wallet_cli_controller import WalletCliController from wallet_orders_impl import WalletOrdersImpl class WalletOrdersV1(WalletOrdersImpl): def set_test_params(self): - super().set_test_params(True) + super().set_test_params(True, WalletCliController) if __name__ == '__main__': diff --git a/test/functional/wallet_orders_v1_rpc.py b/test/functional/wallet_orders_v1_rpc.py new file mode 100644 index 000000000..74a0ac372 --- /dev/null +++ b/test/functional/wallet_orders_v1_rpc.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 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. + +from test_framework.wallet_rpc_controller import WalletRpcController +from wallet_orders_impl import WalletOrdersImpl + + +class WalletOrdersV1(WalletOrdersImpl): + def set_test_params(self): + super().set_test_params(True, WalletRpcController) + + +if __name__ == '__main__': + WalletOrdersV1().main() diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 164f08ba3..265b7cbb8 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -2100,7 +2100,7 @@ where }; if ts_cmp_result == Ordering::Equal { - info1.order_id.as_str().cmp(info2.order_id.as_str()) + info1.order_id.cmp(&info2.order_id) } else { ts_cmp_result } @@ -2172,9 +2172,11 @@ where (token_ids_map, token_infos) }; - // Calculate give/ask price for each order. Note that the `filter_map` call shouldn't filter - // anything out normally; if it does, then the implementation of `node_get_tokens_info` - // has a bug. + // Calculate give/ask price for each order. Note: + // 1) The price is based on original amounts, which is valid for orders v1 but not for v0; + // this is ok, because orders v0 will be deprecated soon. + // 2) The `filter_map` call shouldn't filter anything out normally; if it does, then + // the implementation of `node_get_tokens_info` has a bug. let order_infos_with_price = order_infos .into_iter() .filter_map(|info| { @@ -2210,7 +2212,6 @@ where info.initially_asked.amount().amount().into_atoms().into(), ask_currency_decimals as i64, ); - // FIXME check that it works correctly let give_ask_price = give_ask_price.with_scale_round( give_currency_decimals.into(), bigdecimal::rounding::RoundingMode::Down, @@ -2270,6 +2271,8 @@ where compare_currencies(order1_given_token_id, order2_given_token_id) .then_with(|| compare_currencies(order1_asked_token_id, order2_asked_token_id)) .then_with(|| order2_price.cmp(order1_price)) + // If the price is the same, sort by the order id. + .then_with(|| order1_info.order_id.cmp(&order2_info.order_id)) }, ); diff --git a/wallet/wallet-cli-commands/src/helper_types.rs b/wallet/wallet-cli-commands/src/helper_types.rs index dd4287b7a..3e695d200 100644 --- a/wallet/wallet-cli-commands/src/helper_types.rs +++ b/wallet/wallet-cli-commands/src/helper_types.rs @@ -208,7 +208,7 @@ pub fn format_active_order_info( "Id: {id}, ", "Given: {g} [left: {rg}], ", "Asked: {a} [left: {ra}], ", - "Give/Ask: {price}, " + "Give/Ask: {price}" ), marker = if order_info.is_own { "*" } else { " " }, id = order_info.order_id, diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index 3a7e70329..986c62bcd 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -26,6 +26,9 @@ use std::{ time::Duration, }; +use futures::{stream::FuturesUnordered, TryStreamExt as _}; +use itertools::Itertools as _; + use chainstate::{ rpc::{RpcOutputValueIn, RpcOutputValueOut}, tx_verifier::check_transaction, @@ -58,7 +61,6 @@ use crypto::{ key::{hdkd::u31::U31, PrivateKey, PublicKey}, vrf::VRFPublicKey, }; -use futures::{stream::FuturesUnordered, TryStreamExt as _}; use mempool::tx_accumulator::PackingStrategy; use mempool_types::tx_options::TxOptionsOverrides; use p2p_types::{bannable_address::BannableAddress, socket_address::SocketAddress, PeerId}; @@ -67,7 +69,7 @@ use types::{ AccountExtendedPublicKey, NewOrderTransaction, NewSubmittedTransaction, NewTokenTransaction, RpcHashedTimelockContract, RpcNewTransaction, RpcPreparedTransaction, }; -use utils::{ensure, shallow_clone::ShallowClone}; +use utils::{ensure, shallow_clone::ShallowClone, sorted::Sorted as _}; use utils_networking::IpOrSocketAddress; use wallet::{ account::{transaction_list::TransactionList, PoolData, TransactionToSign, TxInfo}, @@ -1822,7 +1824,7 @@ where }, ); - Ok(itertools::process_results(result_iter, |iter| { + let result = itertools::process_results(result_iter, |iter| { // Filter out concluded orders whose conclusion has been confirmed. // Note that this will also filter out orders that were concluded right after creation, // so that the creation tx has not been included in a block yet. Technically this @@ -1831,8 +1833,14 @@ where iter.filter(|info| { !(info.is_marked_as_concluded_in_wallet && info.existing_order_data.is_none()) }) - .collect() - })?) + .collect_vec() + })?; + // Note: currently the infos should be sorted by plain order id (because this is how + // they are sorted in the output cache). + // We re-sort then by the bech32 representation of the order id, to simplify testing. + let result = result.sorted_by(|info1, info2| info1.order_id.cmp(&info2.order_id)); + + Ok(result) } pub async fn list_all_active_orders( @@ -1910,7 +1918,11 @@ where }) }) }) - .collect::>()?; + .collect::, _>>()?; + // Note: currently the infos are sorted by plain order id (because node_rpc_order_infos + // is a BTreeMap). + // We re-sort then by the bech32 representation of the order id, to simplify testing. + let result = result.sorted_by(|info1, info2| info1.order_id.cmp(&info2.order_id)); Ok(result) } From d20b9e4899fb159194d242dac196b60c4ba69249 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Tue, 16 Dec 2025 17:24:21 +0200 Subject: [PATCH 09/10] Appease clippy --- CHANGELOG.md | 2 +- .../db-dumper/src/dumper_lib/tests/mod.rs | 2 +- chainstate/launcher/src/config.rs | 10 +++------ chainstate/src/rpc/mod.rs | 8 +++---- .../test-framework/src/random_tx_maker.rs | 4 ++-- .../test-suite/src/tests/orders_tests.rs | 8 +++---- common/src/address/dehexify.rs | 12 +++++------ common/src/chain/transaction/output/mod.rs | 2 +- crypto/src/key/hdkd/child_number.rs | 6 +++--- do_checks.sh | 3 +++ mempool/src/pool/tests/orphans.rs | 2 +- node-gui/src/main.rs | 2 +- orders-accounting/src/cache.rs | 2 +- supply-chain/config.toml | 4 ++++ wallet/src/account/output_cache/tests.rs | 2 +- .../tests/generic_fixed_signature_tests.rs | 2 +- .../src/command_handler/mod.rs | 12 +++++------ wallet/wallet-controller/src/read.rs | 2 +- wallet/wallet-rpc-lib/src/rpc/mod.rs | 21 ++++++------------- 19 files changed, 50 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a14572b6a..becd4c28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,7 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ - Node RPC: - `chainstate_order_info` will no longer fail if one of the order's balances became zero. - + - Documentation-only changes: - Certain parameters and/or returned values that were previously (incorrectly) designated as "hex string" are now designated as "hexified xxx id". diff --git a/chainstate/db-dumper/src/dumper_lib/tests/mod.rs b/chainstate/db-dumper/src/dumper_lib/tests/mod.rs index 6e09d2fea..95f274324 100644 --- a/chainstate/db-dumper/src/dumper_lib/tests/mod.rs +++ b/chainstate/db-dumper/src/dumper_lib/tests/mod.rs @@ -58,7 +58,7 @@ fn dump_blocks_predefined() { let chain_config = Arc::new(chain::config::create_unit_test_config_builder().genesis_custom(genesis).build()); - let block_infos = vec![ + let block_infos = [ TestBlockInfo::from_input_info(TestBlockInputInfo { height: BlockHeight::new(1), is_mainchain: true, diff --git a/chainstate/launcher/src/config.rs b/chainstate/launcher/src/config.rs index 0fb8d244c..b295f2c15 100644 --- a/chainstate/launcher/src/config.rs +++ b/chainstate/launcher/src/config.rs @@ -19,16 +19,12 @@ use chainstate::ChainstateConfig; /// Storage type to use #[must_use] -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub enum StorageBackendConfig { + #[default] Lmdb, - InMemory, -} -impl Default for StorageBackendConfig { - fn default() -> Self { - Self::Lmdb - } + InMemory, } impl StorageBackendConfig { diff --git a/chainstate/src/rpc/mod.rs b/chainstate/src/rpc/mod.rs index 6dc47e1d0..16508d0fd 100644 --- a/chainstate/src/rpc/mod.rs +++ b/chainstate/src/rpc/mod.rs @@ -413,7 +413,7 @@ impl ChainstateRpcServer for super::ChainstateHandle { pool_data .map(|d| -> Result<_, DynamizedError> { let addr = dynamize_err(Address::new( - &chain_config, + chain_config, d.decommission_destination().clone(), ))?; @@ -453,7 +453,7 @@ impl ChainstateRpcServer for super::ChainstateHandle { self.call(move |this| { let chain_config = this.get_chain_config(); let token_info_result: Result, _> = - dynamize_err(token_id.decode_object(&chain_config)) + dynamize_err(token_id.decode_object(chain_config)) .and_then(|token_id| dynamize_err(this.get_token_info_for_rpc(token_id))); token_info_result @@ -473,7 +473,7 @@ impl ChainstateRpcServer for super::ChainstateHandle { let token_ids = token_ids .into_iter() .map(|token_id| -> Result<_, DynamizedError> { - Ok(token_id.decode_object(&chain_config)?) + Ok(token_id.decode_object(chain_config)?) }) .collect::>()?; @@ -488,7 +488,7 @@ impl ChainstateRpcServer for super::ChainstateHandle { self.call(move |this| { let chain_config = this.get_chain_config(); let result: Result, _> = - dynamize_err(order_id.decode_object(&chain_config)) + dynamize_err(order_id.decode_object(chain_config)) .and_then(|order_id| dynamize_err(this.get_order_info_for_rpc(&order_id))); result diff --git a/chainstate/test-framework/src/random_tx_maker.rs b/chainstate/test-framework/src/random_tx_maker.rs index 58e5cefb8..322f54f9f 100644 --- a/chainstate/test-framework/src/random_tx_maker.rs +++ b/chainstate/test-framework/src/random_tx_maker.rs @@ -1379,14 +1379,14 @@ impl<'a> RandomTxMaker<'a> { PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); *dummy_pool_id = pool_id; - *pool_data = Box::new(StakePoolData::new( + **pool_data = StakePoolData::new( pool_data.pledge(), Destination::PublicKey(staker_pk), vrf_pk, Destination::AnyoneCanSpend, pool_data.margin_ratio_per_thousand(), pool_data.cost_per_block(), - )); + ); let _ = pos_accounting_cache .create_pool(pool_id, pool_data.as_ref().clone().into()) .unwrap(); diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index d0bbb6d9b..91b8060c7 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -183,7 +183,7 @@ fn assert_order_exists( .unwrap(), ask_balance: expected_data.ask_balance.unwrap_or(Amount::ZERO), give_balance: expected_data.give_balance.unwrap_or(Amount::ZERO), - nonce: expected_data.nonce.clone(), + nonce: expected_data.nonce, is_frozen: expected_data.is_frozen, }; @@ -383,9 +383,9 @@ fn assert_order_missing(tf: &TestFramework, order_id: &OrderId, no_other_orders_ { let storage_tx = tf.storage.transaction_ro().unwrap(); - assert_eq!(storage_tx.get_order_data(&order_id).unwrap(), None); - assert_eq!(storage_tx.get_ask_balance(&order_id).unwrap(), None); - assert_eq!(storage_tx.get_give_balance(&order_id).unwrap(), None); + assert_eq!(storage_tx.get_order_data(order_id).unwrap(), None); + assert_eq!(storage_tx.get_ask_balance(order_id).unwrap(), None); + assert_eq!(storage_tx.get_give_balance(order_id).unwrap(), None); let all_order_ids = storage_tx.get_all_order_ids().unwrap(); assert!(!all_order_ids.contains(order_id)); diff --git a/common/src/address/dehexify.rs b/common/src/address/dehexify.rs index a0885ae08..3012442ed 100644 --- a/common/src/address/dehexify.rs +++ b/common/src/address/dehexify.rs @@ -22,12 +22,12 @@ use super::hexified::HexifiedAddress; #[allow(clippy::let_and_return)] pub fn dehexify_all_addresses(conf: &ChainConfig, input: &str) -> String { - let result = HexifiedAddress::::replace_with_address(conf, input).to_string(); - let result = HexifiedAddress::::replace_with_address(conf, &result).to_string(); - let result = HexifiedAddress::::replace_with_address(conf, &result).to_string(); - let result = HexifiedAddress::::replace_with_address(conf, &result).to_string(); - let result = HexifiedAddress::::replace_with_address(conf, &result).to_string(); - let result = HexifiedAddress::::replace_with_address(conf, &result).to_string(); + let result = HexifiedAddress::::replace_with_address(conf, input).clone(); + let result = HexifiedAddress::::replace_with_address(conf, &result).clone(); + let result = HexifiedAddress::::replace_with_address(conf, &result).clone(); + let result = HexifiedAddress::::replace_with_address(conf, &result).clone(); + let result = HexifiedAddress::::replace_with_address(conf, &result).clone(); + let result = HexifiedAddress::::replace_with_address(conf, &result).clone(); result } diff --git a/common/src/chain/transaction/output/mod.rs b/common/src/chain/transaction/output/mod.rs index 9e090b120..79faa8e11 100644 --- a/common/src/chain/transaction/output/mod.rs +++ b/common/src/chain/transaction/output/mod.rs @@ -282,7 +282,7 @@ impl TextSummary for TxOutput { NftIssuance::V0(iss1) => { let md = &iss1.metadata; let creator = match &md.creator { - Some(c) => hex::encode(c.public_key.encode()).to_string(), + Some(c) => hex::encode(c.public_key.encode()).clone(), None => "Unspecified".to_string(), }; format!( diff --git a/crypto/src/key/hdkd/child_number.rs b/crypto/src/key/hdkd/child_number.rs index 42311e022..5a96a81c7 100644 --- a/crypto/src/key/hdkd/child_number.rs +++ b/crypto/src/key/hdkd/child_number.rs @@ -145,9 +145,9 @@ mod test { #[case(0, false)] #[case(1, false)] #[case(1234567, false)] - #[case(u32::MAX & (!0x80000000 - 1), false)] - #[case(u32::MAX & !0x80000000, false)] - #[case(u32::MAX & (!0x80000000 + 1), true)] + #[case(!0x80000000 - 1, false)] + #[case(!0x80000000, false)] + #[case(!0x80000000 + 1, true)] #[case(u32::MAX - 1, true)] #[case(u32::MAX, true)] fn create_child_number(#[case] encoded_num: u32, #[case] is_hardened: bool) { diff --git a/do_checks.sh b/do_checks.sh index e6d975f23..ec886ff4f 100755 --- a/do_checks.sh +++ b/do_checks.sh @@ -44,6 +44,8 @@ fi # error type instead of `Infallible` can be seen as redundant). # * "manual_is_multiple_of" - starting from v1.90 clippy insists that `x % 2 == 0` should be # replaced with `x.is_multiple_of(2)`, which is a questionable improvement. +# * "let_and_return" is disabled because having `let` before returning can be useful at least +# as a potential place for a breakpoint. EXTRA_ARGS=() if [[ $CLIPPY_VERSION -ge 1089 ]]; then EXTRA_ARGS+=(-A clippy::infallible_try_from) @@ -56,6 +58,7 @@ cargo clippy --all-features --workspace --all-targets -- \ -A clippy::unnecessary_literal_unwrap \ -A clippy::new_without_default \ -A clippy::uninlined_format_args \ + -A clippy::let-and-return \ -D clippy::implicit_saturating_sub \ -D clippy::implicit_clone \ -D clippy::map_unwrap_or \ diff --git a/mempool/src/pool/tests/orphans.rs b/mempool/src/pool/tests/orphans.rs index b7902432d..83f9ee6ba 100644 --- a/mempool/src/pool/tests/orphans.rs +++ b/mempool/src/pool/tests/orphans.rs @@ -153,7 +153,7 @@ async fn diamond_graph(#[case] seed: Seed, #[case] insertion_plan: Vec<(usize, u let tx3 = make_tx(&mut rng, &[(tx1_outpt, 0), (tx2_outpt, 0)], &[90_000_000]); - let txs = vec![tx0, tx1, tx2, tx3]; + let txs = [tx0, tx1, tx2, tx3]; let tx_ids: Vec<_> = txs.iter().map(|tx| tx.transaction().get_id()).collect(); // Set up mempool and execute the insertion plan diff --git a/node-gui/src/main.rs b/node-gui/src/main.rs index e860f4315..fffddee4a 100644 --- a/node-gui/src/main.rs +++ b/node-gui/src/main.rs @@ -497,7 +497,7 @@ fn view(state: &GuiState) -> Element<'_, Message> { column![ iced::widget::text("Mintlayer-core node initialization failed".to_string()) .size(header_font_size), - iced::widget::text(message.to_string()).size(text_font_size) + iced::widget::text(message.clone()).size(text_font_size) ] } InitializationInterruptionReason::DataDirCleanedUp => { diff --git a/orders-accounting/src/cache.rs b/orders-accounting/src/cache.rs index a7dd4ae64..e26f1cc9a 100644 --- a/orders-accounting/src/cache.rs +++ b/orders-accounting/src/cache.rs @@ -159,7 +159,7 @@ impl OrdersAccountingView for OrdersAccountingCache

} }) .chain(self.data.order_data.data().keys().copied().filter(|id| { - match self.data.order_data.get_data(&id) { + match self.data.order_data.get_data(id) { accounting::GetDataResult::Present(_) => true, accounting::GetDataResult::Missing => { // This shouldn't happen. diff --git a/supply-chain/config.toml b/supply-chain/config.toml index f68d6b393..36eea365f 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -112,6 +112,10 @@ criteria = "safe-to-deploy" version = "0.4.5" criteria = "safe-to-deploy" +[[exemptions.bigdecimal]] +version = "0.4.9" +criteria = "safe-to-deploy" + [[exemptions.bincode]] version = "1.3.3" criteria = "safe-to-deploy" diff --git a/wallet/src/account/output_cache/tests.rs b/wallet/src/account/output_cache/tests.rs index bddcaadb9..332a36a0d 100644 --- a/wallet/src/account/output_cache/tests.rs +++ b/wallet/src/account/output_cache/tests.rs @@ -1140,7 +1140,7 @@ fn add_random_transfer_tx( output_cache .add_tx( - &chain_config, + chain_config, BlockHeight::new(rng.gen_range(0..100)), tx_id.into(), WalletTx::Tx(TxData::new(tx, tx_state)), diff --git a/wallet/src/signer/tests/generic_fixed_signature_tests.rs b/wallet/src/signer/tests/generic_fixed_signature_tests.rs index d2c278830..fa85a0818 100644 --- a/wallet/src/signer/tests/generic_fixed_signature_tests.rs +++ b/wallet/src/signer/tests/generic_fixed_signature_tests.rs @@ -531,7 +531,7 @@ pub async fn test_fixed_signatures_generic2( let account2_dest2 = new_dest_from_account(&mut account2, &mut db_tx, KeyPurpose::Change); let account2_pk2 = find_pub_key_for_pkh_dest(&account2_dest2, &account2).clone(); - let utxos = vec![ + let utxos = [ TxOutput::Transfer( OutputValue::Coin(Amount::from_atoms(1000)), account1_dest4.clone(), diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 265b7cbb8..9dc441f0d 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -1342,7 +1342,7 @@ where .into_coins_and_tokens(); let token_ids_str_vec = - tokens.iter().map(|(token_id, _)| token_id.as_str().to_owned()).collect_vec(); + tokens.keys().map(|token_id| token_id.as_str().to_owned()).collect_vec(); let token_infos = wallet .node_get_tokens_info(token_ids_str_vec) @@ -1899,7 +1899,7 @@ where ) }) .collect(); - Ok(ConsoleCommand::Print(delegations.join("\n").to_string())) + Ok(ConsoleCommand::Print(delegations.join("\n").clone())) } WalletCommand::ListCreatedBlocksIds => { @@ -2082,13 +2082,13 @@ where // Sort the orders, so that the newer ones appear later. let order_infos = order_infos.sorted_by(|info1, info2| { + use std::cmp::Ordering; + let ts1 = info1.existing_order_data.as_ref().map(|data| data.creation_timestamp); let ts2 = info2.existing_order_data.as_ref().map(|data| data.creation_timestamp); - use std::cmp::Ordering; - let ts_cmp_result = match (ts1, ts2) { (Some(ts1), Some(ts2)) => ts1.cmp(&ts2), // Note: the logic here is opposite to the normal comparison of Option's - @@ -2221,12 +2221,12 @@ where }) .collect_vec(); - use std::cmp::Ordering; - // This will be used with order_infos_with_price, so we know that the token_infos map // contains all tokens that we may encounter here. let compare_currencies = |token1_id: Option<&RpcAddress>, token2_id: Option<&RpcAddress>| { + use std::cmp::Ordering; + let ticker_by_id = |token_id| { token_ticker_from_rpc_token_info( token_infos.get(token_id).expect("Token info is known to be present"), diff --git a/wallet/wallet-controller/src/read.rs b/wallet/wallet-controller/src/read.rs index 0b18e519e..be3d3d939 100644 --- a/wallet/wallet-controller/src/read.rs +++ b/wallet/wallet-controller/src/read.rs @@ -424,7 +424,7 @@ where } /// Return info about all orders owned by the selected account. - pub async fn get_own_orders( + pub fn get_own_orders( &self, ) -> Result, ControllerError> { self.wallet.get_orders(self.account_index).map_err(ControllerError::WalletError) diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index 986c62bcd..7b80f79cc 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -777,9 +777,9 @@ where let token_ids = token_ids .into_iter() .map(|token_id_addr| -> WRpcResult<_, N> { - Ok(token_id_addr + token_id_addr .decode_object(&self.chain_config) - .map_err(|_| RpcError::InvalidAddress)?) + .map_err(|_| RpcError::InvalidAddress) }) .collect::>()?; let infos = self.node.get_tokens_info(token_ids).await.map_err(RpcError::RpcError)?; @@ -1730,11 +1730,7 @@ where pub async fn list_own_orders(&self, account_index: U31) -> WRpcResult, N> { let wallet_orders_data = self .wallet - .call_async(move |controller| { - Box::pin(async move { - controller.readonly_controller(account_index).get_own_orders().await - }) - }) + .call(move |controller| controller.readonly_controller(account_index).get_own_orders()) .await??; let token_ids = collect_token_v1_ids_from_rpc_output_values_holders( wallet_orders_data.iter().map(|(_, order_data)| order_data), @@ -1851,11 +1847,7 @@ where ) -> WRpcResult, N> { let wallet_order_ids = self .wallet - .call_async(move |controller| { - Box::pin(async move { - controller.readonly_controller(account_index).get_own_orders().await - }) - }) + .call(move |controller| controller.readonly_controller(account_index).get_own_orders()) .await?? .into_iter() .map(|(order_id, _)| order_id) @@ -1874,9 +1866,8 @@ where .await .map_err(RpcError::RpcError)?; - let token_ids = collect_token_v1_ids_from_rpc_output_values_holders( - node_rpc_order_infos.iter().map(|(_, order_node_rpc_info)| order_node_rpc_info), - ); + let token_ids = + collect_token_v1_ids_from_rpc_output_values_holders(node_rpc_order_infos.values()); let token_decimals = self.get_tokens_decimals(token_ids).await?; let result = node_rpc_order_infos From 4da5d7799c24e44d1acbcd5d3925bcb03ab5084c Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Tue, 16 Dec 2025 20:10:54 +0200 Subject: [PATCH 10/10] Minor cleanup --- .../interface/chainstate_interface_impl.rs | 2 +- .../test-suite/src/tests/orders_tests.rs | 4 +- common/src/chain/currency.rs | 6 +- common/src/chain/tokens/rpc.rs | 1 - .../output/output_values_holder.rs | 6 - common/src/lib.rs | 3 +- .../wallet_order_list_all_active.py | 2 +- test/functional/wallet_order_list_own_cli.py | 2 +- test/functional/wallet_order_list_own_rpc.py | 2 +- utxo/src/cache.rs | 9 +- .../src/command_handler/mod.rs | 103 +++++++++--------- .../wallet-cli-commands/src/helper_types.rs | 31 +++--- wallet/wallet-rpc-lib/src/rpc/mod.rs | 2 +- 13 files changed, 89 insertions(+), 84 deletions(-) diff --git a/chainstate/src/interface/chainstate_interface_impl.rs b/chainstate/src/interface/chainstate_interface_impl.rs index df3afef17..3ab6ea4a0 100644 --- a/chainstate/src/interface/chainstate_interface_impl.rs +++ b/chainstate/src/interface/chainstate_interface_impl.rs @@ -860,7 +860,7 @@ where .map_err(ChainstateError::from) } - #[tracing::instrument(skip_all, fields(ask_currency, give_currency))] + #[tracing::instrument(skip(self))] fn get_orders_info_for_rpc_by_currencies( &self, ask_currency: Option<&Currency>, diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index 91b8060c7..86802fc9f 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -15,14 +15,13 @@ use std::{borrow::Cow, collections::BTreeMap}; -use chainstate_storage::Transactional as _; -use orders_accounting::OrdersAccountingStorageRead as _; use rstest::rstest; use chainstate::{ BlockError, ChainstateError, CheckBlockError, CheckBlockTransactionsError, ConnectTransactionError, }; +use chainstate_storage::Transactional as _; use chainstate_test_framework::{ helpers::{ calculate_fill_order, issue_and_mint_random_token_from_best_block, @@ -49,6 +48,7 @@ use common::{ }; use crypto::key::{KeyKind, PrivateKey}; use logging::log; +use orders_accounting::OrdersAccountingStorageRead as _; use randomness::{CryptoRng, Rng, SliceRandom}; use test_utils::random::{gen_random_bytes, make_seedable_rng, Seed}; use tx_verifier::{ diff --git a/common/src/chain/currency.rs b/common/src/chain/currency.rs index 1429270f3..be3de7797 100644 --- a/common/src/chain/currency.rs +++ b/common/src/chain/currency.rs @@ -27,7 +27,7 @@ use crate::{ // The reason for having RPC types in the first place is that in RPC we'd like for certain things to have a more // human-readable representation, namely: // 1) Destinations, VRF public keys and ids of pools/delegations/tokens/orders should be bech32-encoded instead -// of hex-encoded or "hexified" via `HexifiedAddress`. For this purpose we have the `RpcAddress` wrapper +// of being hex-encoded or "hexified" via `HexifiedAddress`. For this purpose we have the `RpcAddress` wrapper // (which holds a bech-32 encoded string), so e.g. `RpcAddress` should be used instead of the plain // PoolId in RPC types. // 2) Amounts are more readable when they are in the decimal form instead of the plain number of atoms. But we also @@ -49,7 +49,7 @@ use crate::{ // `RpcTokenTotalSupplyIn` and moved out of the wallet). // // What should we do: -// 1) The "RPC" prefix should be replace with "Rpc" to honor Rust's naming conventions. +// 1) The "RPC" prefix should be replaced with "Rpc" to honor Rust's naming conventions. // 2) `RpcOutputValue` should be renamed to `OutputValueV1` and it should *not* implement `HasValueHint`. // Also, all functions that take `RpcOutputValue` and are named as such (e.g. `from_rpc_output_value` below) // should be renamed as well. @@ -60,7 +60,7 @@ use crate::{ // "Rpc...In" or "...Out" type. Which in turn means that RPC types that contain an amount or an output value must // themselves be designated as "In" or "Out" and have the corresponding suffix. // 5) We also need to reconsider where the RPC types live. Currently many of them live in `common`, even though they're -// only used in the chainstate rpc, but lots of others live in `chainstate`, see the contents of the `chainstate/src/rpc/types/` +// only used in the chainstate RPC, but lots of others live in `chainstate`, see the contents of the `chainstate/src/rpc/types/` // folder. One approach would be to put an RPC type into the same crate as its non-RPC counterpart. Another approach // is to put them all to chainstate (note that blockprod and mempool depend on chainstate, so we'll be able to use // chainstate types in their RPC interfaces if needed). diff --git a/common/src/chain/tokens/rpc.rs b/common/src/chain/tokens/rpc.rs index 194956ff1..3d24804c1 100644 --- a/common/src/chain/tokens/rpc.rs +++ b/common/src/chain/tokens/rpc.rs @@ -151,7 +151,6 @@ impl RPCFungibleTokenInfo { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, HasValueHint)] pub struct RPCNonFungibleTokenInfo { - // TODO: same as in RPCFungibleTokenInfo, use RpcAddress here. pub token_id: TokenId, pub creation_tx_id: Id, pub creation_block_id: Id, diff --git a/common/src/chain/transaction/output/output_values_holder.rs b/common/src/chain/transaction/output/output_values_holder.rs index 658c30594..f545a90c2 100644 --- a/common/src/chain/transaction/output/output_values_holder.rs +++ b/common/src/chain/transaction/output/output_values_holder.rs @@ -71,12 +71,6 @@ pub fn collect_token_v1_ids_from_rpc_output_values_holder_into( } } -pub fn collect_token_v1_ids_from_rpc_output_values_holder( - holder: &impl RpcOutputValuesHolder, -) -> BTreeSet { - collect_token_v1_ids_from_rpc_output_values_holders(std::iter::once(holder)) -} - pub fn collect_token_v1_ids_from_rpc_output_values_holders<'a, H: RpcOutputValuesHolder + 'a>( holders: impl IntoIterator, ) -> BTreeSet { diff --git a/common/src/lib.rs b/common/src/lib.rs index 01950f832..1d2753714 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -31,8 +31,9 @@ pub use uint::{Uint128, Uint256, Uint512, UintConversionError}; mod tests { use std::str::FromStr as _; - use crypto::vrf::VRFPublicKey; use hex::FromHex; + + use crypto::vrf::VRFPublicKey; use rpc_description::HasValueHint; use serialization::DecodeAll as _; diff --git a/test/functional/wallet_order_list_all_active.py b/test/functional/wallet_order_list_all_active.py index 27eaacd31..79dbe94c6 100644 --- a/test/functional/wallet_order_list_all_active.py +++ b/test/functional/wallet_order_list_all_active.py @@ -14,7 +14,7 @@ # 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 all actrive orders listing test, both RPC and CLI +"""Test listing all actrive orders, both RPC and CLI """ from test_framework.util import assert_in, assert_equal diff --git a/test/functional/wallet_order_list_own_cli.py b/test/functional/wallet_order_list_own_cli.py index bdbc727ec..25d3f5e99 100644 --- a/test/functional/wallet_order_list_own_cli.py +++ b/test/functional/wallet_order_list_own_cli.py @@ -14,7 +14,7 @@ # 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 own orders listing test, CLI +"""Test listing own orders via CLI """ from test_framework.util import assert_in, assert_equal diff --git a/test/functional/wallet_order_list_own_rpc.py b/test/functional/wallet_order_list_own_rpc.py index 22470fa02..bdded2279 100644 --- a/test/functional/wallet_order_list_own_rpc.py +++ b/test/functional/wallet_order_list_own_rpc.py @@ -14,7 +14,7 @@ # 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 own orders listing test, RPC +"""Test listing own orders via RPC """ from test_framework.util import assert_in, assert_equal diff --git a/utxo/src/cache.rs b/utxo/src/cache.rs index 75749040e..68238e7f3 100644 --- a/utxo/src/cache.rs +++ b/utxo/src/cache.rs @@ -521,15 +521,18 @@ fn should_include_in_utxo_set(output: &TxOutput) -> bool { #[cfg(test)] mod unit_test { - use super::*; - use crate::tests::test_helper::{empty_test_utxos_view, insert_single_entry, Presence}; - use common::primitives::H256; use rstest::rstest; + + use common::primitives::H256; use test_utils::{ random::{make_seedable_rng, Seed}, UnwrapInfallible as _, }; + use super::*; + + use crate::tests::test_helper::{empty_test_utxos_view, insert_single_entry, Presence}; + #[rstest] #[trace] #[case(Seed::from_entropy())] diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 9dc441f0d..f65b0320a 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -2065,62 +2065,65 @@ where .await?; Ok(Self::new_tx_command(new_tx, chain_config)) } - WalletCommand::ListOwnOrders => { - let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; + WalletCommand::ListOwnOrders => self.list_own_orders(chain_config).await, + WalletCommand::ListActiveOrders { + ask_currency, + give_currency, + } => self.list_all_active_orders(chain_config, ask_currency, give_currency).await, + } + } - let order_infos = wallet.list_own_orders(selected_account).await?; - let token_ids_str_vec = get_token_ids_from_order_infos_as_str( - order_infos.iter().map(|info| (&info.initially_asked, &info.initially_given)), - ); + async fn list_own_orders( + &mut self, + chain_config: &ChainConfig, + ) -> Result> + where + WalletCliCommandError: From, + { + let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; - let token_infos = wallet - .node_get_tokens_info(token_ids_str_vec) - .await? - .into_iter() - .map(|info| (info.token_id(), info)) - .collect(); + let order_infos = wallet.list_own_orders(selected_account).await?; + let token_ids_str_vec = get_token_ids_from_order_infos_as_str( + order_infos.iter().map(|info| (&info.initially_asked, &info.initially_given)), + ); - // Sort the orders, so that the newer ones appear later. - let order_infos = order_infos.sorted_by(|info1, info2| { - use std::cmp::Ordering; - - let ts1 = - info1.existing_order_data.as_ref().map(|data| data.creation_timestamp); - let ts2 = - info2.existing_order_data.as_ref().map(|data| data.creation_timestamp); - - let ts_cmp_result = match (ts1, ts2) { - (Some(ts1), Some(ts2)) => ts1.cmp(&ts2), - // Note: the logic here is opposite to the normal comparison of Option's - - // we want None to be bigger than Some, so that "unconfirmed" orders - // appear later in the list. - (Some(_), None) => Ordering::Less, - (None, Some(_)) => Ordering::Greater, - (None, None) => Ordering::Equal, - }; + let token_infos = wallet + .node_get_tokens_info(token_ids_str_vec) + .await? + .into_iter() + .map(|info| (info.token_id(), info)) + .collect(); + + // Sort the orders, so that the newer ones appear later. + let order_infos = order_infos.sorted_by(|info1, info2| { + use std::cmp::Ordering; + + let ts1 = info1.existing_order_data.as_ref().map(|data| data.creation_timestamp); + let ts2 = info2.existing_order_data.as_ref().map(|data| data.creation_timestamp); + + let ts_cmp_result = match (ts1, ts2) { + (Some(ts1), Some(ts2)) => ts1.cmp(&ts2), + // Note: the logic here is opposite to the normal comparison of Option's - + // we want None to be bigger than Some, so that "unconfirmed" orders + // appear later in the list. + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => Ordering::Equal, + }; - if ts_cmp_result == Ordering::Equal { - info1.order_id.cmp(&info2.order_id) - } else { - ts_cmp_result - } - }); + // If the timestamps are equal, sort the orders by id in the bech32 encoding. + ts_cmp_result.then_with(|| info1.order_id.cmp(&info2.order_id)) + }); - let order_infos = order_infos - .iter() - .map(|info| format_own_order_info(info, chain_config, &token_infos)) - .collect::, _>>()?; + let order_infos = order_infos + .iter() + .map(|info| format_own_order_info(info, chain_config, &token_infos)) + .collect::, _>>()?; - Ok(ConsoleCommand::Print(format!( - "{}\n", - order_infos.join("\n") - ))) - } - WalletCommand::ListActiveOrders { - ask_currency, - give_currency, - } => self.list_all_active_orders(chain_config, ask_currency, give_currency).await, - } + Ok(ConsoleCommand::Print(format!( + "{}\n", + order_infos.join("\n") + ))) } async fn list_all_active_orders( diff --git a/wallet/wallet-cli-commands/src/helper_types.rs b/wallet/wallet-cli-commands/src/helper_types.rs index 3e695d200..6c6ffb6b7 100644 --- a/wallet/wallet-cli-commands/src/helper_types.rs +++ b/wallet/wallet-cli-commands/src/helper_types.rs @@ -16,20 +16,20 @@ use std::{collections::BTreeMap, fmt::Display, str::FromStr}; use bigdecimal::BigDecimal; -use chainstate::rpc::RpcOutputValueOut; use clap::ValueEnum; +use itertools::Itertools; +use chainstate::rpc::RpcOutputValueOut; use common::{ address::{decode_address, Address, RpcAddress}, chain::{ tokens::{RPCTokenInfo, TokenId}, - ChainConfig, Currency, OutPointSourceId, RpcCurrency, TxOutput, UtxoOutPoint, + ChainConfig, Currency, Destination, OutPointSourceId, RpcCurrency, TxOutput, UtxoOutPoint, }, primitives::{ amount::decimal::subtract_decimal_amounts_of_same_currency, DecimalAmount, Id, H256, }, }; -use itertools::Itertools; use wallet_controller::types::{GenericCurrencyTransfer, GenericTokenTransfer}; use wallet_rpc_lib::types::{ ActiveOrderInfo, NodeInterface, OwnOrderInfo, PoolInfo, TokenTotalSupply, @@ -127,6 +127,7 @@ pub fn format_own_order_info( token_infos: &BTreeMap, ) -> Result> { if let Some(existing_order_data) = &order_info.existing_order_data { + // The order exists on chain let accumulated_ask_amount = subtract_decimal_amounts_of_same_currency( &order_info.initially_asked.amount().decimal(), &existing_order_data.ask_balance.decimal(), @@ -173,6 +174,7 @@ pub fn format_own_order_info( st = status, )) } else { + // The order only exists in the wallet Ok(format!( concat!( "Id: {id}, ", @@ -387,11 +389,7 @@ pub fn parse_generic_currency_transfer( } }; - let destination = Address::from_string(chain_config, dest_str) - .map_err(|err| { - WalletCliCommandError::::InvalidInput(format!("Invalid address '{dest_str}': {err}")) - })? - .into_object(); + let destination = parse_destination(chain_config, dest_str)?; let amount = parse_decimal_amount(amount_str)?; let output = match name { "transfer" => GenericCurrencyTransfer { @@ -438,11 +436,7 @@ pub fn parse_generic_token_transfer( })? .into_object(); - let destination = Address::from_string(chain_config, dest_str) - .map_err(|err| { - WalletCliCommandError::::InvalidInput(format!("Invalid address '{dest_str}': {err}")) - })? - .into_object(); + let destination = parse_destination(chain_config, dest_str)?; let amount = parse_decimal_amount(amount_str)?; let output = match name { "transfer" => GenericTokenTransfer { @@ -556,6 +550,16 @@ pub fn parse_decimal_amount( }) } +/// Parse a destination +pub fn parse_destination( + chain_config: &ChainConfig, + input: &str, +) -> Result> { + decode_address(chain_config, input).map_err(|err| { + WalletCliCommandError::::InvalidInput(format!("Invalid address '{input}': {err}")) + }) +} + /// Try parsing the passed input as coins (case-insensitive "coin" is accepted) or /// as a token id. pub fn parse_currency( @@ -572,6 +576,7 @@ pub fn parse_currency( } } +/// Same as `parse_currency`, but return `RpcCurrency`. pub fn parse_rpc_currency( input: &str, chain_config: &ChainConfig, diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index 7b80f79cc..6cdaa78ed 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -1831,7 +1831,7 @@ where }) .collect_vec() })?; - // Note: currently the infos should be sorted by plain order id (because this is how + // Note: currently the infos are sorted by plain order id (because this is how // they are sorted in the output cache). // We re-sort then by the bech32 representation of the order id, to simplify testing. let result = result.sorted_by(|info1, info2| info1.order_id.cmp(&info2.order_id));