From 0b503ba2a8aa928e637ef1a671b6b5256fa465f2 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 23 Feb 2026 16:22:13 +0200 Subject: [PATCH 1/6] Refactor wallet-cli helper types; cleanup --- common/src/primitives/amount/decimal.rs | 27 + common/src/primitives/error.rs | 38 - common/src/primitives/mod.rs | 1 - .../src/command_handler/mod.rs | 83 +- wallet/wallet-cli-commands/src/errors.rs | 22 +- .../wallet-cli-commands/src/helper_types.rs | 896 ++++++++++++------ wallet/wallet-cli-commands/src/lib.rs | 63 +- wallet/wallet-controller/src/types/mod.rs | 10 +- wallet/wallet-rpc-lib/src/rpc/types.rs | 2 +- 9 files changed, 712 insertions(+), 430 deletions(-) delete mode 100644 common/src/primitives/error.rs diff --git a/common/src/primitives/amount/decimal.rs b/common/src/primitives/amount/decimal.rs index 19a2ce2fd5..b8ba9fda7a 100644 --- a/common/src/primitives/amount/decimal.rs +++ b/common/src/primitives/amount/decimal.rs @@ -186,6 +186,33 @@ impl std::fmt::Display for DecimalAmount { } } +/// A wrapper for `DecimalAmount` that uses `is_same` to implement comparison. +/// +/// This is mainly intended to be used in error types to make them comparable (which in turn +/// is mostly useful in tests). +#[derive(Clone, Copy, Debug)] +pub struct DecimalAmountWithIsSameComparison(pub DecimalAmount); + +impl PartialEq for DecimalAmountWithIsSameComparison { + fn eq(&self, other: &Self) -> bool { + self.0.is_same(&other.0) + } +} + +impl Eq for DecimalAmountWithIsSameComparison {} + +impl std::fmt::Display for DecimalAmountWithIsSameComparison { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl From for DecimalAmountWithIsSameComparison { + fn from(value: DecimalAmount) -> Self { + Self(value) + } +} + #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] pub enum ParseError { #[error("Resulting number is too big")] diff --git a/common/src/primitives/error.rs b/common/src/primitives/error.rs deleted file mode 100644 index 57ec28488a..0000000000 --- a/common/src/primitives/error.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2021-2022 RBB S.r.l -// opensource@mintlayer.org -// SPDX-License-Identifier: MIT -// Licensed under the MIT License; -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::error::Error; - -/// High-level error type that is passed between the subsystems -/// to signal serious errors, e.g., the inability to establish -/// any outbound connections. -/// -/// Each subsystem who wishes to propagate an error message to -/// the rest of the system must implement the `From` trait which -/// wraps the internal error into a `MintlayerError`. -/// -/// ```ignore -/// impl From for MintlayerError { -/// fn from(e: SocketError) -> MintlayerError { -/// MintlayerError::NetworkError(Box::new(e)) -/// } -/// } -/// ``` -#[derive(Debug)] -pub enum MintlayerError { - /// Generic network error that might have originated from P2P or RPC - #[allow(unused)] - NetworkError(Box), -} diff --git a/common/src/primitives/mod.rs b/common/src/primitives/mod.rs index fc9918f260..0eb924a5db 100644 --- a/common/src/primitives/mod.rs +++ b/common/src/primitives/mod.rs @@ -16,7 +16,6 @@ pub mod amount; pub mod bech32_encoding; pub mod compact; -pub mod error; pub mod height; pub mod id; pub mod per_thousand; diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 75858d8d73..533e9aaffc 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, Currency, Destination, SignedTransaction, TxOutput, UtxoOutPoint, + ChainConfig, Destination, RpcCurrency, SignedTransaction, }, primitives::{Idable as _, H256}, text_summary::TextSummary, @@ -41,7 +41,7 @@ use utils::{ sorted::Sorted as _, }; use wallet::version::get_version; -use wallet_controller::types::{GenericTokenTransfer, WalletExtraInfo}; +use wallet_controller::types::WalletExtraInfo; use wallet_rpc_client::wallet_rpc_traits::{PartialOrSignedTx, WalletInterface}; use wallet_rpc_lib::types::{ Balances, ComposedTransaction, ControllerConfig, HardwareWalletType, MnemonicInfo, @@ -54,8 +54,7 @@ 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, token_ticker_from_rpc_token_info, + active_order_infos_header, format_token_name, token_ticker_from_rpc_token_info, CliCurrency, }, CreateWalletDeviceSelectMenu, ManageableWalletCommand, OpenWalletDeviceSelectMenu, OpenWalletSubCommand, WalletManagementCommand, @@ -66,7 +65,7 @@ use self::local_state::WalletWithState; use super::{ helper_types::{ 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, + CliForceReduceLookaheadSize, CliUtxoState, }, ColdWalletCommand, ConsoleCommand, WalletCommand, }; @@ -465,7 +464,7 @@ where i_know_what_i_am_doing, } => { let force_reduce = match i_know_what_i_am_doing { - Some(CliForceReduce::IKnowWhatIAmDoing) => true, + Some(CliForceReduceLookaheadSize::IKnowWhatIAmDoing) => true, None => false, }; @@ -1139,21 +1138,17 @@ where utxos, only_transaction, } => { - let outputs: Vec = outputs - .iter() - .map(|input| parse_coin_output(input, chain_config)) - .collect::, WalletCliCommandError>>()?; + let outputs = outputs + .into_iter() + .map(|output| output.to_coin_tx_output(chain_config)) + .collect::, _>>()?; - let input_utxos: Vec = utxos - .iter() - .map(|s| parse_utxo_outpoint(s)) - .collect::, WalletCliCommandError>>( - )?; + let utxos = utxos.into_iter().map(Into::into).collect(); let ComposedTransaction { hex, fees } = self .non_empty_wallet() .await? - .compose_transaction(input_utxos, outputs, None, only_transaction) + .compose_transaction(utxos, outputs, None, only_transaction) .await?; let mut output = format!("The hex encoded transaction is:\n{hex}\n"); @@ -1178,7 +1173,7 @@ where token_supply, is_freezable, } => { - let token_supply = parse_token_supply(&token_supply, number_of_decimals)?; + let token_supply = token_supply.to_fully_parsed(number_of_decimals)?; let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; let new_token = wallet @@ -1464,14 +1459,10 @@ where amount, utxos, } => { - let input_utxos: Vec = utxos - .iter() - .map(|s| parse_utxo_outpoint(s)) - .collect::, WalletCliCommandError>>( - )?; + let utxos = utxos.into_iter().map(Into::into).collect(); let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; let new_tx = wallet - .send_coins(selected_account, address, amount, input_utxos, self.config) + .send_coins(selected_account, address, amount, utxos, self.config) .await?; Ok(Self::new_tx_command(new_tx, chain_config)) } @@ -1527,14 +1518,13 @@ where utxo, change_address, } => { - let selected_input = parse_utxo_outpoint(&utxo)?; let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; let ComposedTransaction { hex, fees } = wallet .transaction_from_cold_input( selected_account, address, amount, - selected_input, + utxo.into(), change_address, self.config, ) @@ -1670,10 +1660,10 @@ where fee_change_address, outputs, } => { - let outputs: Vec = outputs + let outputs = outputs .into_iter() - .map(|input| parse_generic_token_transfer(&input, chain_config)) - .collect::, WalletCliCommandError>>()?; + .map(|ouput| ouput.to_fully_parsed(chain_config)) + .collect::, _>>()?; let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; let result = wallet @@ -2020,16 +2010,19 @@ where } => { 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 parse_token_id = + |currency: &CliCurrency| -> Result<_, WalletCliCommandError> { + let parsed_currency = currency + .to_fully_parsed(chain_config)? + .to_rpc_currency(chain_config)?; + match parsed_currency { + RpcCurrency::Coin => Ok(None), + RpcCurrency::Token(token_id) => Ok(Some(token_id.into_string())), + } + }; - let ask_token_id = parse_token_id(ask_currency)?; - let give_token_id = parse_token_id(give_currency)?; + 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, @@ -2142,20 +2135,20 @@ where async fn list_all_active_orders( &mut self, chain_config: &ChainConfig, - ask_currency: Option, - give_currency: Option, + 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 parse_currency = |currency: CliCurrency| -> Result<_, WalletCliCommandError> { + Ok(currency.to_fully_parsed(chain_config)?.to_rpc_currency(chain_config)?) + }; + + let ask_currency = ask_currency.map(parse_currency).transpose()?; + let give_currency = give_currency.map(parse_currency).transpose()?; let order_infos = wallet .list_all_active_orders(selected_account, ask_currency, give_currency) diff --git a/wallet/wallet-cli-commands/src/errors.rs b/wallet/wallet-cli-commands/src/errors.rs index 87467dee1b..752b9e1520 100644 --- a/wallet/wallet-cli-commands/src/errors.rs +++ b/wallet/wallet-cli-commands/src/errors.rs @@ -13,17 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::{ - address::{AddressError, RpcAddress}, - chain::OrderId, -}; +use common::address::AddressError; use crypto::key::hdkd::u31::U31; use node_comm::node_traits::NodeInterface; use utils::qrcode::QrCodeError; -use wallet_controller::types::GenericCurrencyTransferToTxOutputConversionError; use wallet_rpc_client::{handles_client::WalletRpcHandlesClientError, rpc_client::WalletRpcError}; use wallet_rpc_lib::RpcError; +use crate::helper_types; + #[derive(thiserror::Error, derive_more::Debug)] pub enum WalletCliCommandError { #[error("Invalid quoting")] @@ -51,7 +49,7 @@ pub enum WalletCliCommandError { WalletRpcError(#[from] RpcError), #[error("{0}")] - WalletHandlessRpcError(#[from] WalletRpcHandlesClientError), + WalletHandlesRpcError(#[from] WalletRpcHandlesClientError), #[error("{0}")] WalletClientRpcError(#[from] WalletRpcError), @@ -65,18 +63,18 @@ pub enum WalletCliCommandError { #[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), - #[error("Address error: {0}")] AddressError(#[from] AddressError), + + #[error(transparent)] + FormatError(#[from] helper_types::FormatError), + + #[error(transparent)] + ParseError(#[from] helper_types::ParseError), } diff --git a/wallet/wallet-cli-commands/src/helper_types.rs b/wallet/wallet-cli-commands/src/helper_types.rs index 6c6ffb6b7b..0947bfa42d 100644 --- a/wallet/wallet-cli-commands/src/helper_types.rs +++ b/wallet/wallet-cli-commands/src/helper_types.rs @@ -21,27 +21,29 @@ use itertools::Itertools; use chainstate::rpc::RpcOutputValueOut; use common::{ - address::{decode_address, Address, RpcAddress}, + address::{decode_address, Address, AddressError, RpcAddress}, chain::{ tokens::{RPCTokenInfo, TokenId}, - ChainConfig, Currency, Destination, OutPointSourceId, RpcCurrency, TxOutput, UtxoOutPoint, + ChainConfig, Currency, Destination, OrderId, OutPointSourceId, TxOutput, UtxoOutPoint, }, primitives::{ - amount::decimal::subtract_decimal_amounts_of_same_currency, DecimalAmount, Id, H256, + amount::decimal::{ + subtract_decimal_amounts_of_same_currency, DecimalAmountWithIsSameComparison, + }, + DecimalAmount, Id, H256, }, }; -use wallet_controller::types::{GenericCurrencyTransfer, GenericTokenTransfer}; -use wallet_rpc_lib::types::{ - ActiveOrderInfo, NodeInterface, OwnOrderInfo, PoolInfo, TokenTotalSupply, +use utils::ensure; +use wallet_controller::types::{ + GenericCurrencyTransfer, GenericCurrencyTransferToTxOutputConversionError, GenericTokenTransfer, }; +use wallet_rpc_lib::types::{ActiveOrderInfo, OwnOrderInfo, PoolInfo, TokenTotalSupply}; use wallet_types::{ seed_phrase::StoreSeedPhrase, utxo_types::{UtxoState, UtxoType}, with_locked::WithLocked, }; -use crate::errors::WalletCliCommandError; - #[derive(Debug, Clone, Copy, ValueEnum)] pub enum CliUtxoTypes { All, @@ -121,11 +123,11 @@ pub fn format_pool_info(info: PoolInfo) -> String { ) } -pub fn format_own_order_info( +pub fn format_own_order_info( order_info: &OwnOrderInfo, chain_config: &ChainConfig, token_infos: &BTreeMap, -) -> Result> { +) -> 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( @@ -133,7 +135,7 @@ pub fn format_own_order_info( &existing_order_data.ask_balance.decimal(), ) .ok_or_else(|| { - WalletCliCommandError::OrderNegativeAccumulatedAskAmount(order_info.order_id.clone()) + FormatError::OrderNegativeAccumulatedAskAmount(order_info.order_id.clone()) })?; let status = if !existing_order_data.is_frozen && !order_info.is_marked_as_frozen_in_wallet @@ -196,12 +198,12 @@ pub fn active_order_infos_header() -> &'static str { ) } -pub fn format_active_order_info( +pub fn format_active_order_info( order_info: &ActiveOrderInfo, give_ask_price: &BigDecimal, chain_config: &ChainConfig, token_infos: &BTreeMap, -) -> Result> { +) -> Result { // Note: we show what's given first because the orders are sorted by the given currency first // by the caller code. Ok(format!( @@ -222,11 +224,11 @@ pub fn format_active_order_info( )) } -pub fn format_asset_name( +pub fn format_asset_name( value: &RpcOutputValueOut, chain_config: &ChainConfig, token_infos: &BTreeMap, -) -> Result> { +) -> Result { let result = if let Some(token_id) = value.token_id() { format_token_name(token_id, chain_config, token_infos)? } else { @@ -235,23 +237,23 @@ pub fn format_asset_name( Ok(result) } -pub fn format_output_value( +pub fn format_output_value( value: &RpcOutputValueOut, chain_config: &ChainConfig, token_infos: &BTreeMap, -) -> Result> { +) -> Result { let asset_name = format_asset_name(value, chain_config, token_infos)?; Ok(format!("{} {}", value.amount().decimal(), asset_name)) } -pub fn format_token_name( +pub fn format_token_name( token_id: &RpcAddress, chain_config: &ChainConfig, token_infos: &BTreeMap, -) -> Result> { +) -> Result { let decoded_token_id = token_id .decode_object(chain_config) - .map_err(WalletCliCommandError::TokenIdDecodingError)?; + .map_err(FormatError::TokenIdDecodingError)?; let result = if let Some(token_ticker) = token_infos.get(&decoded_token_id).map(token_ticker_from_rpc_token_info) @@ -316,12 +318,12 @@ impl From for StoreSeedPhrase { } #[derive(Debug, Clone, Copy, ValueEnum)] -pub enum EnableOrDisable { +pub enum CliEnableOrDisable { Enable, Disable, } -impl EnableOrDisable { +impl CliEnableOrDisable { pub fn is_enable(self) -> bool { match self { Self::Enable => true, @@ -330,129 +332,165 @@ impl EnableOrDisable { } } -/// Parses a string into UtxoOutPoint -/// The string format is expected to be -/// tx(H256,u32) or block(H256,u32) -/// -/// e.g tx(000000000000000000059fa50103b9683e51e5aba83b8a34c9b98ce67d66136c,1) -/// e.g block(000000000000000000059fa50103b9683e51e5aba83b8a34c9b98ce67d66136c,2) -pub fn parse_utxo_outpoint( - input: &str, -) -> Result> { - let (name, mut args) = parse_funclike_expr(input).ok_or( - WalletCliCommandError::::InvalidInput("Invalid input format".into()), - )?; - - let (h256_str, output_index_str) = match (args.next(), args.next(), args.next()) { - (Some(h256_str), Some(output_index_str), None) => (h256_str, output_index_str), - (_, _, _) => { - return Err(WalletCliCommandError::::InvalidInput( - "Invalid input format".into(), - )); - } - }; +/// A UtxoOutPoint that can be parsed from strings of the form `tx(H256,u32)` or `block(H256,u32)`, +/// e.g. `tx(000000000000000000059fa50103b9683e51e5aba83b8a34c9b98ce67d66136c,1)`, +/// `block(000000000000000000059fa50103b9683e51e5aba83b8a34c9b98ce67d66136c,2)` +#[derive(Debug, Clone)] +pub struct CliUtxoOutPoint { + pub source_id: OutPointSourceId, + pub output_index: u32, +} - let h256 = H256::from_str(h256_str) - .map_err(|err| WalletCliCommandError::::InvalidInput(err.to_string()))?; - let output_index = u32::from_str(output_index_str) - .map_err(|err| WalletCliCommandError::::InvalidInput(err.to_string()))?; - let source_id = match name { - "tx" => OutPointSourceId::Transaction(Id::new(h256)), - "block" => OutPointSourceId::BlockReward(Id::new(h256)), - _ => { - return Err(WalletCliCommandError::::InvalidInput( - "Invalid input: unknown ID type".into(), - )); - } - }; +impl FromStr for CliUtxoOutPoint { + type Err = ParseError; - Ok(UtxoOutPoint::new(source_id, output_index)) -} + fn from_str(input: &str) -> Result { + let (name, mut args) = parse_funclike_expr(input).ok_or(ParseError::InvalidInputFormat)?; -/// Parses a string into `GenericCurrencyTransfer`. -/// The string format is expected to be `transfer(address,amount)` -/// e.g `transfer(tmt1qy7y8ra99sgmt97lu2kn249yds23pnp7xsv62p77,10.1)`. -pub fn parse_generic_currency_transfer( - input: &str, - chain_config: &ChainConfig, -) -> Result> { - let (name, mut args) = parse_funclike_expr(input).ok_or( - WalletCliCommandError::::InvalidInput("Invalid input format".into()), - )?; - - let (dest_str, amount_str) = match (args.next(), args.next(), args.next()) { - (Some(dest_str), Some(amount_str), None) => (dest_str, amount_str), - (_, _, _) => { - return Err(WalletCliCommandError::::InvalidInput( - "Invalid input format".into(), - )); - } - }; + let (h256_str, output_index_str) = match (args.next(), args.next(), args.next()) { + (Some(h256_str), Some(output_index_str), None) => (h256_str, output_index_str), + (_, _, _) => { + return Err(ParseError::InvalidInputFormat); + } + }; - let destination = parse_destination(chain_config, dest_str)?; - let amount = parse_decimal_amount(amount_str)?; - let output = match name { - "transfer" => GenericCurrencyTransfer { - amount, - destination, - }, - _ => { - return Err(WalletCliCommandError::::InvalidInput( - "Invalid input: unknown type".into(), - )); - } - }; + let h256 = + H256::from_str(h256_str).map_err(|_| ParseError::InvalidHash(h256_str.to_owned()))?; + + let output_index = u32::from_str(output_index_str) + .map_err(|_| ParseError::InvalidOutputIndex(output_index_str.to_owned()))?; + + let source_id = if name.eq_ignore_ascii_case("tx") { + OutPointSourceId::Transaction(Id::new(h256)) + } else if name.eq_ignore_ascii_case("block") { + OutPointSourceId::BlockReward(Id::new(h256)) + } else { + return Err(ParseError::UnknownSourceIdType(name.to_owned())); + }; - Ok(output) + Ok(Self { + source_id, + output_index, + }) + } } -/// Parses a string into `GenericTokenTransfer`. -/// The string format is expected to be `transfer(token_id,address,amount)` -pub fn parse_generic_token_transfer( - input: &str, - chain_config: &ChainConfig, -) -> Result> { - let (name, mut args) = parse_funclike_expr(input).ok_or( - WalletCliCommandError::::InvalidInput("Invalid input format".into()), - )?; - - let (token_id_str, dest_str, amount_str) = - match (args.next(), args.next(), args.next(), args.next()) { - (Some(dest_str), Some(amount_str), Some(token_id_str), None) => { - (dest_str, amount_str, token_id_str) +impl From for UtxoOutPoint { + fn from(value: CliUtxoOutPoint) -> Self { + Self::new(value.source_id, value.output_index) + } +} + +/// This represents a transfer of an amount of an unspecified currency and can be parsed +/// from strings of the form `transfer(address,amount)`. +#[derive(Debug, Clone)] +pub struct CliUnspecifiedCurrencyTransfer { + pub amount: DecimalAmount, + pub destination: String, +} + +impl FromStr for CliUnspecifiedCurrencyTransfer { + type Err = ParseError; + + fn from_str(input: &str) -> Result { + let (name, mut args) = parse_funclike_expr(input).ok_or(ParseError::InvalidInputFormat)?; + + let (dest_str, amount_str) = match (args.next(), args.next(), args.next()) { + (Some(dest_str), Some(amount_str), None) => (dest_str, amount_str), + (_, _, _) => { + return Err(ParseError::InvalidInputFormat); } - (_, _, _, _) => { - return Err(WalletCliCommandError::::InvalidInput( - "Invalid input format".into(), - )); + }; + + let amount = parse_decimal_amount(amount_str)?; + + let result = if name.eq_ignore_ascii_case("transfer") { + Self { + amount, + destination: dest_str.to_owned(), } + } else { + return Err(ParseError::UnknownAction(name.to_owned())); }; - let token_id = Address::from_string(chain_config, token_id_str) - .map_err(|err| { - WalletCliCommandError::::InvalidInput(format!( - "Invalid token id '{token_id_str}': {err}" - )) - })? - .into_object(); + Ok(result) + } +} - let destination = parse_destination(chain_config, dest_str)?; - let amount = parse_decimal_amount(amount_str)?; - let output = match name { - "transfer" => GenericTokenTransfer { - token_id, - amount, - destination, - }, +impl CliUnspecifiedCurrencyTransfer { + pub fn to_fully_parsed( + &self, + chain_config: &ChainConfig, + ) -> Result { + Ok(GenericCurrencyTransfer { + amount: self.amount, + destination: parse_destination(chain_config, &self.destination)?, + }) + } - _ => { - return Err(WalletCliCommandError::::InvalidInput( - "Invalid input: unknown type".into(), - )); - } - }; + pub fn to_coin_tx_output(&self, chain_config: &ChainConfig) -> Result { + Ok(self.to_fully_parsed(chain_config)?.into_coin_tx_output(chain_config)?) + } +} - Ok(output) +/// This represents a transfer of an amount of a token and can be parsed +/// from strings of the form `transfer(token_id,address,amount)`. +#[derive(Debug, Clone)] +pub struct CliTokenTransfer { + pub token_id: String, + pub amount: DecimalAmount, + pub destination: String, +} + +impl FromStr for CliTokenTransfer { + type Err = ParseError; + + fn from_str(input: &str) -> Result { + let (name, mut args) = parse_funclike_expr(input).ok_or(ParseError::InvalidInputFormat)?; + + let (token_id_str, dest_str, amount_str) = + match (args.next(), args.next(), args.next(), args.next()) { + (Some(dest_str), Some(amount_str), Some(token_id_str), None) => { + (dest_str, amount_str, token_id_str) + } + (_, _, _, _) => { + return Err(ParseError::InvalidInputFormat); + } + }; + + let amount = parse_decimal_amount(amount_str)?; + + let result = if name.eq_ignore_ascii_case("transfer") { + Self { + token_id: token_id_str.to_owned(), + amount, + destination: dest_str.to_owned(), + } + } else { + return Err(ParseError::UnknownAction(name.to_owned())); + }; + + Ok(result) + } +} + +impl CliTokenTransfer { + pub fn to_fully_parsed( + &self, + chain_config: &ChainConfig, + ) -> Result { + let token_id = Address::from_string(chain_config, &self.token_id) + .map_err(|_| ParseError::InvalidTokenId(self.token_id.clone()))? + .into_object(); + + let destination = parse_destination(chain_config, &self.destination)?; + + Ok(GenericTokenTransfer { + token_id, + amount: self.amount, + destination, + }) + } } /// Parse simple strings of the form "foo(x,y,z)". @@ -492,96 +530,113 @@ fn pop_char_from_str(s: &str) -> (Option, &str) { (last_ch, chars.as_str()) } -/// Same as `parse_generic_output`, but produce a concrete TxOutput that transfers coins. -pub fn parse_coin_output( - input: &str, - chain_config: &ChainConfig, -) -> Result> { - parse_generic_currency_transfer(input, chain_config)? - .into_coin_tx_output(chain_config) - .map_err(WalletCliCommandError::::InvalidTxOutput) +/// A TokenTotalSupply that can be parsed from strings "unlimited", "lockable" and "fixed(amount)". +#[derive(Debug, Clone)] +pub enum CliTokenTotalSupply { + Unlimited, + Lockable, + Fixed(DecimalAmount), } -/// Try to parse a total token supply from a string -/// Valid values are "unlimited", "lockable" and "fixed(Amount)" -pub fn parse_token_supply( - input: &str, - token_number_of_decimals: u8, -) -> Result> { - match input { - "unlimited" => Ok(TokenTotalSupply::Unlimited), - "lockable" => Ok(TokenTotalSupply::Lockable), - _ => parse_fixed_token_supply(input, token_number_of_decimals), - } -} +impl FromStr for CliTokenTotalSupply { + type Err = ParseError; -/// Try to parse a fixed total token supply in the format of "fixed(Amount)" -fn parse_fixed_token_supply( - input: &str, - token_number_of_decimals: u8, -) -> Result> { - if let Some(inner) = input.strip_prefix("fixed(").and_then(|str| str.strip_suffix(')')) { - Ok(TokenTotalSupply::Fixed(parse_token_amount( - token_number_of_decimals, - inner, - )?)) - } else { - Err(WalletCliCommandError::::InvalidInput(format!( - "Failed to parse token supply from '{input}'" - ))) + fn from_str(input: &str) -> Result { + if input.eq_ignore_ascii_case("unlimited") { + Ok(Self::Unlimited) + } else if input.eq_ignore_ascii_case("lockable") { + Ok(Self::Lockable) + } else { + let (name, mut args) = + parse_funclike_expr(input).ok_or(ParseError::InvalidInputFormat)?; + + ensure!( + name.eq_ignore_ascii_case("fixed"), + ParseError::UnknownTokenSupplyType(name.to_owned()) + ); + + let amount_str = match (args.next(), args.next()) { + (Some(amount_str), None) => amount_str, + (_, _) => { + return Err(ParseError::InvalidInputFormat); + } + }; + + Ok(Self::Fixed(parse_decimal_amount(amount_str)?)) + } } } -fn parse_token_amount( - token_number_of_decimals: u8, - value: &str, -) -> Result> { - let amount = common::primitives::Amount::from_fixedpoint_str(value, token_number_of_decimals) - .ok_or_else(|| WalletCliCommandError::::InvalidInput(value.to_owned()))?; - Ok(amount.into()) +impl CliTokenTotalSupply { + pub fn to_fully_parsed( + &self, + token_number_of_decimals: u8, + ) -> Result { + let result = match self { + Self::Unlimited => TokenTotalSupply::Unlimited, + Self::Lockable => TokenTotalSupply::Lockable, + Self::Fixed(amount) => { + // Note: even though `RpcAmountIn` can be constructed from `DecimalAmount` directly, + // we want to do the conversion to atoms early, to produce a nicer error message. + let amount = amount.to_amount(token_number_of_decimals).ok_or( + ParseError::DecimalAmountNotConvertibleToAtoms((*amount).into()), + )?; + TokenTotalSupply::Fixed(amount.into()) + } + }; + + Ok(result) + } } /// 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}'")) - }) +pub fn parse_decimal_amount(input: &str) -> Result { + DecimalAmount::from_str(input).map_err(|_| ParseError::InvalidDecimalAmount(input.to_owned())) } /// Parse a destination -pub fn parse_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}")) - }) +) -> Result { + decode_address(chain_config, input) + .map_err(|_| ParseError::InvalidDestination(input.to_owned())) } -/// 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)] +pub enum CliCurrency { + Coin, + Token(String), +} + +impl FromStr for CliCurrency { + type Err = ParseError; + + /// Try parsing the passed input as coins (case-insensitive "coin" is accepted), otherwise + /// treat it as a token id. + fn from_str(input: &str) -> Result { + if input.eq_ignore_ascii_case("coin") { + Ok(Self::Coin) + } else { + Ok(Self::Token(input.to_owned())) + } } } -/// Same as `parse_currency`, but return `RpcCurrency`. -pub fn parse_rpc_currency( - input: &str, - chain_config: &ChainConfig, -) -> Result> { - Ok(parse_currency(input, chain_config)?.to_rpc_currency(chain_config)?) +impl CliCurrency { + pub fn to_fully_parsed(&self, chain_config: &ChainConfig) -> Result { + let result = match self { + Self::Coin => Currency::Coin, + Self::Token(token_id_str) => { + let token_id = decode_address::(chain_config, token_id_str) + .map_err(|_| ParseError::InvalidCurrency(token_id_str.to_owned()))?; + + Currency::Token(token_id) + } + }; + + Ok(result) + } } #[derive(Debug, Clone, Copy, ValueEnum)] @@ -615,17 +670,17 @@ impl CliIsUnfreezable { } #[derive(Debug, Clone, Copy, ValueEnum)] -pub enum CliForceReduce { +pub enum CliForceReduceLookaheadSize { IKnowWhatIAmDoing, } #[derive(Debug, Clone, Copy, ValueEnum)] -pub enum YesNo { +pub enum CliYesNo { Yes, No, } -impl YesNo { +impl CliYesNo { pub fn to_bool(self) -> bool { match self { Self::Yes => true, @@ -634,20 +689,73 @@ impl YesNo { } } +#[derive(thiserror::Error, Clone, Debug)] +pub enum FormatError { + #[error("Accumulated ask amount for order {0} is negative")] + OrderNegativeAccumulatedAskAmount(RpcAddress), + + #[error("Error decoding token id: {0}")] + TokenIdDecodingError(AddressError), +} + +#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq)] +pub enum ParseError { + #[error("Unknown source id type: {0}")] + UnknownSourceIdType(String), + + #[error("Unknown action: {0}")] + UnknownAction(String), + + #[error("Invalid input format")] + InvalidInputFormat, + + #[error("Invalid token id: {0}")] + InvalidTokenId(String), + + #[error("Invalid decimal amount: {0}")] + InvalidDecimalAmount(String), + + #[error("Invalid destination: {0}")] + InvalidDestination(String), + + #[error("Invalid hash: {0}")] + InvalidHash(String), + + #[error("Invalid output index: {0}")] + InvalidOutputIndex(String), + + #[error("Unknown token supply type: {0}")] + UnknownTokenSupplyType(String), + + #[error("Invalid currency: {0}")] + InvalidCurrency(String), + + #[error("Decimal amount cannot be converted to atoms: {0}")] + DecimalAmountNotConvertibleToAtoms(DecimalAmountWithIsSameComparison), + + #[error("Address error: {0}")] + AddressError(#[from] AddressError), + + #[error(transparent)] + GenericCurrencyTransferToTxOutputConversionError( + #[from] GenericCurrencyTransferToTxOutputConversionError, + ), +} + #[cfg(test)] mod tests { use rstest::rstest; use common::{ address::pubkeyhash::PublicKeyHash, - chain::{self, Destination}, + chain::{self, tokens::TokenId, Destination}, }; - use node_comm::rpc_client::ColdWalletClient; use randomness::Rng; use test_utils::{ - assert_matches, + assert_matches, assert_matches_return_val, random::{make_seedable_rng, Seed}, }; + use wallet_rpc_lib::types::RpcAmountIn; use super::*; @@ -715,53 +823,61 @@ mod tests { #[trace] #[case(Seed::from_entropy())] fn test_parse_utxo_outpoint(#[case] seed: Seed) { - fn check(input: &str, is_tx: bool, idx: u32, hash: H256) { - let utxo_outpoint = parse_utxo_outpoint::(input).unwrap(); - - match utxo_outpoint.source_id() { - OutPointSourceId::Transaction(id) => { - assert_eq!(id.to_hash(), hash); - assert!(is_tx); - } - OutPointSourceId::BlockReward(id) => { - assert_eq!(id.to_hash(), hash); - assert!(!is_tx); - } - } - - assert_eq!(utxo_outpoint.output_index(), idx); - } - let mut rng = make_seedable_rng(seed); for _ in 0..10 { let h256 = H256::random_using(&mut rng); let idx = rng.gen::(); - let (id, is_tx) = if rng.gen::() { - ("tx", true) - } else { - ("block", false) - }; - check(&format!("{id}({h256:x},{idx})"), is_tx, idx, h256); + + for tag in ["tx", "Tx", "tX"] { + let parsed_outpoint: UtxoOutPoint = + CliUtxoOutPoint::from_str(&format!("{tag}({h256:x},{idx})")).unwrap().into(); + assert_eq!( + parsed_outpoint, + UtxoOutPoint::new(OutPointSourceId::Transaction(h256.into()), idx) + ); + } + + for tag in ["block", "Block", "bLOck"] { + let parsed_outpoint: UtxoOutPoint = + CliUtxoOutPoint::from_str(&format!("{tag}({h256:x},{idx})")).unwrap().into(); + assert_eq!( + parsed_outpoint, + UtxoOutPoint::new(OutPointSourceId::BlockReward(h256.into()), idx) + ); + } + + let err = CliUtxoOutPoint::from_str(&format!("foo({h256:x},{idx})")).unwrap_err(); + assert_eq!(err, ParseError::UnknownSourceIdType("foo".to_owned())); + + let tag = if rng.gen_bool(0.5) { "tx" } else { "block" }; + // Sanity check + CliUtxoOutPoint::from_str(&format!("{tag}({h256:x},{idx})")).unwrap(); + + let err = CliUtxoOutPoint::from_str(&format!("{tag} {h256:x},{idx})")).unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = CliUtxoOutPoint::from_str(&format!("{tag}({h256:x},{idx}")).unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = CliUtxoOutPoint::from_str(&format!("{tag} {h256:x},{idx}")).unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = CliUtxoOutPoint::from_str(&format!("{tag}({h256:x})")).unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = CliUtxoOutPoint::from_str(&format!("{tag}()")).unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); } } #[rstest] #[trace] #[case(Seed::from_entropy())] - fn test_parse_generic_output(#[case] seed: Seed) { + fn test_parse_unspecified_currency_transfer(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = chain::config::create_unit_test_config(); - let parse_assert_error = |str_to_parse: &str| { - let err = parse_generic_token_transfer::(str_to_parse, &chain_config) - .unwrap_err(); - assert_matches!( - err, - WalletCliCommandError::::InvalidInput(_) - ); - }; - for _ in 0..10 { let pkh = PublicKeyHash::random_using(&mut rng); let addr = Address::new(&chain_config, Destination::PublicKeyHash(pkh)).unwrap(); @@ -769,46 +885,66 @@ mod tests { rng.gen_range(0..=u128::MAX), rng.gen_range(0..=u8::MAX), ); - let GenericCurrencyTransfer { - amount: parsed_amount, - destination: parsed_dest, - } = parse_generic_currency_transfer::( - &format!("transfer({addr},{amount})"), - &chain_config, - ) - .unwrap(); - - assert_eq!(parsed_amount.mantissa(), amount.mantissa()); - assert_eq!(parsed_amount.decimals(), amount.decimals()); - assert_eq!(parsed_dest, *addr.as_object()); - - parse_assert_error(&format!("foo({addr},{amount})")); - parse_assert_error(&format!("transfer(foo,{amount})")); - parse_assert_error(&format!("transfer({addr},foo)")); - parse_assert_error(&format!("transfer {addr},{amount})")); - parse_assert_error(&format!("transfer({addr},{amount}")); - parse_assert_error(&format!("transfer {addr},{amount}")); + + for tag in ["transfer", "Transfer", "traNSfer"] { + let GenericCurrencyTransfer { + amount: parsed_amount, + destination: parsed_dest, + } = CliUnspecifiedCurrencyTransfer::from_str(&format!("{tag}({addr},{amount})")) + .unwrap() + .to_fully_parsed(&chain_config) + .unwrap(); + + assert_eq!(parsed_amount.mantissa(), amount.mantissa()); + assert_eq!(parsed_amount.decimals(), amount.decimals()); + assert_eq!(parsed_dest, *addr.as_object()); + } + + let err = CliUnspecifiedCurrencyTransfer::from_str(&format!("foo({addr},{amount})")) + .unwrap_err(); + assert_eq!(err, ParseError::UnknownAction("foo".to_owned())); + + let err = + CliUnspecifiedCurrencyTransfer::from_str(&format!("transfer {addr},{amount})")) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = + CliUnspecifiedCurrencyTransfer::from_str(&format!("transfer({addr},{amount}")) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = + CliUnspecifiedCurrencyTransfer::from_str(&format!("transfer {addr},{amount}")) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = + CliUnspecifiedCurrencyTransfer::from_str(&format!("transfer({addr})")).unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = CliUnspecifiedCurrencyTransfer::from_str("transfer()").unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = CliUnspecifiedCurrencyTransfer::from_str(&format!("transfer({addr},foo)")) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidDecimalAmount("foo".to_owned())); + + let err = CliUnspecifiedCurrencyTransfer::from_str(&format!("transfer(foo,{amount})")) + .unwrap() + .to_fully_parsed(&chain_config) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidDestination("foo".to_owned())); } } #[rstest] #[trace] #[case(Seed::from_entropy())] - fn test_parse_generic_token_output(#[case] seed: Seed) { - use common::chain::tokens::TokenId; - + fn test_parse_token_transfer(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = chain::config::create_unit_test_config(); - let parse_assert_error = |str_to_parse: &str| { - let err = parse_generic_token_transfer::(str_to_parse, &chain_config) - .unwrap_err(); - assert_matches!( - err, - WalletCliCommandError::::InvalidInput(_) - ); - }; - for _ in 0..10 { let token_id = TokenId::new(H256::random_using(&mut rng)); let token_id_as_addr = Address::new(&chain_config, token_id).unwrap(); @@ -818,29 +954,169 @@ mod tests { rng.gen_range(0..=u128::MAX), rng.gen_range(0..=u8::MAX), ); - let GenericTokenTransfer { - token_id: parsed_token_id, - amount: parsed_amount, - destination: parsed_dest, - } = parse_generic_token_transfer::( - &format!("transfer({token_id_as_addr},{addr},{amount})"), - &chain_config, - ) - .unwrap(); - - assert_eq!(parsed_token_id, token_id); - - assert_eq!(parsed_amount.mantissa(), amount.mantissa()); - assert_eq!(parsed_amount.decimals(), amount.decimals()); - assert_eq!(parsed_dest, *addr.as_object()); - - parse_assert_error(&format!("foo({token_id_as_addr},{addr},{amount})")); - parse_assert_error(&format!("transfer(foo,{addr},{amount})")); - parse_assert_error(&format!("transfer({token_id_as_addr},foo,{amount})")); - parse_assert_error(&format!("transfer({token_id_as_addr},{addr},foo)")); - parse_assert_error(&format!("transfer {token_id_as_addr},{addr},{amount})")); - parse_assert_error(&format!("transfer({token_id_as_addr},{addr},{amount}")); - parse_assert_error(&format!("transfer {token_id_as_addr},{addr},{amount}")); + + for tag in ["transfer", "Transfer", "traNSfer"] { + let GenericTokenTransfer { + token_id: parsed_token_id, + amount: parsed_amount, + destination: parsed_dest, + } = CliTokenTransfer::from_str(&format!( + "{tag}({token_id_as_addr},{addr},{amount})" + )) + .unwrap() + .to_fully_parsed(&chain_config) + .unwrap(); + + assert_eq!(parsed_token_id, token_id); + + assert_eq!(parsed_amount.mantissa(), amount.mantissa()); + assert_eq!(parsed_amount.decimals(), amount.decimals()); + assert_eq!(parsed_dest, *addr.as_object()); + } + + let err = + CliTokenTransfer::from_str(&format!("foo({token_id_as_addr},{addr},{amount})")) + .unwrap_err(); + assert_eq!(err, ParseError::UnknownAction("foo".to_owned())); + + let err = CliTokenTransfer::from_str(&format!( + "transfer {token_id_as_addr},{addr},{amount})" + )) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = + CliTokenTransfer::from_str(&format!("transfer({token_id_as_addr},{addr},{amount}")) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = + CliTokenTransfer::from_str(&format!("transfer {token_id_as_addr},{addr},{amount}")) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = CliTokenTransfer::from_str(&format!("transfer({token_id_as_addr},{addr})")) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = + CliTokenTransfer::from_str(&format!("transfer({token_id_as_addr})")).unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = CliTokenTransfer::from_str("transfer()").unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = + CliTokenTransfer::from_str(&format!("transfer({token_id_as_addr},{addr},foo)")) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidDecimalAmount("foo".to_owned())); + + let err = CliTokenTransfer::from_str(&format!("transfer(foo,{addr},{amount})")) + .unwrap() + .to_fully_parsed(&chain_config) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidTokenId("foo".to_owned())); + + let err = + CliTokenTransfer::from_str(&format!("transfer({token_id_as_addr},foo,{amount})")) + .unwrap() + .to_fully_parsed(&chain_config) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidDestination("foo".to_owned())); + } + } + + #[rstest] + #[trace] + #[case(Seed::from_entropy())] + fn test_parse_token_total_supply(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + for _ in 0..10 { + let token_number_of_decimals = rng.gen_range(0..20); + + for tag in ["unlimited", "Unlimited", "unLImited"] { + let parsed_supply = CliTokenTotalSupply::from_str(tag) + .unwrap() + .to_fully_parsed(token_number_of_decimals) + .unwrap(); + assert_matches!(parsed_supply, TokenTotalSupply::Unlimited); + } + + for tag in ["lockable", "Lockable", "locKAble"] { + let parsed_supply = CliTokenTotalSupply::from_str(tag) + .unwrap() + .to_fully_parsed(token_number_of_decimals) + .unwrap(); + assert_matches!(parsed_supply, TokenTotalSupply::Lockable); + } + + let decimal_amount = DecimalAmount::from_uint_decimal( + rng.gen_range(0..=1_000_000_000_000), + rng.gen_range(0..=token_number_of_decimals), + ); + + for tag in ["fixed", "Fixed", "fIXed"] { + let parsed_supply = + CliTokenTotalSupply::from_str(&format!("{tag}({decimal_amount})")) + .unwrap() + .to_fully_parsed(token_number_of_decimals) + .unwrap(); + let parsed_amount = assert_matches_return_val!( + parsed_supply, + TokenTotalSupply::Fixed(amount), + amount + ); + let expected_atoms = decimal_amount.to_amount(token_number_of_decimals).unwrap(); + assert!(parsed_amount.is_same(&RpcAmountIn::from_atoms(expected_atoms))); + } + + let err = CliTokenTotalSupply::from_str("foo").unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = CliTokenTotalSupply::from_str("fixed").unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = CliTokenTotalSupply::from_str(&format!("foo({decimal_amount})")).unwrap_err(); + assert_eq!(err, ParseError::UnknownTokenSupplyType("foo".to_owned())); + + let err = CliTokenTotalSupply::from_str("fixed()").unwrap_err(); + assert_eq!(err, ParseError::InvalidDecimalAmount("".to_owned())); + + let err = + CliTokenTotalSupply::from_str(&format!("fixed({decimal_amount},{decimal_amount})")) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = + CliTokenTotalSupply::from_str(&format!("fixed {decimal_amount})")).unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let err = + CliTokenTotalSupply::from_str(&format!("fixed({decimal_amount}")).unwrap_err(); + assert_eq!(err, ParseError::InvalidInputFormat); + + let decimal_amount_with_too_many_decimals = { + let mut mantissa = rng.gen_range(0..=1_000_000_000_000); + // Make sure that the number has indeed more decimals than `token_number_of_decimals`. + if mantissa % 10 == 0 { + mantissa += rng.gen_range(1..=9); + } + DecimalAmount::from_uint_decimal(mantissa, token_number_of_decimals + 1) + }; + + let err = CliTokenTotalSupply::from_str(&format!( + "fixed({decimal_amount_with_too_many_decimals})", + )) + .unwrap() + .to_fully_parsed(token_number_of_decimals) + .unwrap_err(); + assert_eq!( + err, + ParseError::DecimalAmountNotConvertibleToAtoms( + decimal_amount_with_too_many_decimals.into() + ) + ); } } @@ -848,18 +1124,24 @@ mod tests { fn test_parse_currency() { let chain_config = chain::config::create_unit_test_config(); - let currency = parse_currency::("coin", &chain_config).unwrap(); + let currency = + CliCurrency::from_str("coin").unwrap().to_fully_parsed(&chain_config).unwrap(); assert_eq!(currency, Currency::Coin); - let currency = parse_currency::("cOiN", &chain_config).unwrap(); + let currency = + CliCurrency::from_str("cOiN").unwrap().to_fully_parsed(&chain_config).unwrap(); assert_eq!(currency, Currency::Coin); - let err = parse_currency::("coins", &chain_config).unwrap_err(); - assert_matches!(err, WalletCliCommandError::InvalidInput(_)); + let err = CliCurrency::from_str("coins") + .unwrap() + .to_fully_parsed(&chain_config) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidCurrency("coins".to_owned())); - let currency = parse_currency::( + let currency = CliCurrency::from_str( "rmltk1ktt2slkqdy607kzhueudqucqphjzl7kl506xf78f9w7v00ydythqzgwlyp", - &chain_config, ) + .unwrap() + .to_fully_parsed(&chain_config) .unwrap(); assert_eq!( currency, @@ -869,11 +1151,11 @@ mod tests { )) ); - let err = parse_currency::( - "rpool1zg7yccqqjlz38cyghxlxyp5lp36vwecu2g7gudrf58plzjm75tzq99fr6v", - &chain_config, - ) - .unwrap_err(); - assert_matches!(err, WalletCliCommandError::InvalidInput(_)); + let bad_token_id = "rpool1zg7yccqqjlz38cyghxlxyp5lp36vwecu2g7gudrf58plzjm75tzq99fr6v"; + let err = CliCurrency::from_str(bad_token_id) + .unwrap() + .to_fully_parsed(&chain_config) + .unwrap_err(); + assert_eq!(err, ParseError::InvalidCurrency(bad_token_id.to_owned())); } } diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index 88388beeca..55720b9114 100644 --- a/wallet/wallet-cli-commands/src/lib.rs +++ b/wallet/wallet-cli-commands/src/lib.rs @@ -20,7 +20,7 @@ mod helper_types; pub use command_handler::CommandHandler; use dyn_clone::DynClone; pub use errors::WalletCliCommandError; -use helper_types::YesNo; +use helper_types::CliYesNo; use regex::Regex; use rpc::description::{Described, Module}; use wallet_controller::types::WalletTypeArgs; @@ -42,9 +42,13 @@ use p2p_types::{bannable_address::BannableAddress, PeerId}; use serialization::hex_encoded::HexEncoded; use utils_networking::IpOrSocketAddress; +use crate::helper_types::{ + CliCurrency, CliTokenTotalSupply, CliUnspecifiedCurrencyTransfer, CliUtxoOutPoint, +}; + use self::helper_types::{ - CliForceReduce, CliIsFreezable, CliIsUnfreezable, CliStoreSeedPhrase, CliUtxoState, - CliUtxoTypes, CliWithLocked, EnableOrDisable, + CliEnableOrDisable, CliForceReduceLookaheadSize, CliIsFreezable, CliIsUnfreezable, + CliStoreSeedPhrase, CliTokenTransfer, CliUtxoState, CliUtxoTypes, CliWithLocked, }; #[derive(Debug, Subcommand, Clone)] @@ -349,7 +353,7 @@ pub enum ColdWalletCommand { /// Forces the reduction of lookahead size even below the last used address; /// this may cause the wallet to lose track of used addresses and its actual balance. - i_know_what_i_am_doing: Option, + i_know_what_i_am_doing: Option, }, /// Creates a QR code of the provided address @@ -477,7 +481,7 @@ pub enum WalletCommand { #[clap(name = "config-broadcast")] ConfigBroadcast { #[arg(value_enum)] - broadcast: YesNo, + broadcast: CliYesNo, }, #[clap(name = "account-create")] @@ -607,14 +611,21 @@ pub enum WalletCommand { IssueNewToken { /// The ticker/symbol of the token created token_ticker: String, + /// The maximum number of digits after the decimal points number_of_decimals: u8, + /// URI for data related to the token (website, media, etc) metadata_uri: String, + /// The address of the authority who will be able to manage this token authority_address: String, + /// The total supply of this token - token_supply: String, + /// + /// Valid values are "unlimited", "lockable" and "fixed(amount)" + token_supply: CliTokenTotalSupply, + /// Whether it's possible to centrally freeze this token for all users (due to migration requirements, for example) is_freezable: CliIsFreezable, }, @@ -697,6 +708,9 @@ pub enum WalletCommand { /// The utxos to pay fees from will be selected automatically; these will be normal, single-sig utxos. /// The optional "fee change address" specifies the destination for the change for the fee payment; /// If it's unset, the destination will be taken from one of existing single-sig utxos. + // TODO: as mentioned in another TODO for `TransactionCompose` below, we should have a more general + // `CliCurrencyTransfer`, which would allow to specify either token or coin transfers. Once it's implemented, + // it's better to generalize this command, so that it can perform coin transfers as well. #[clap(name = "token-make-tx-to-send-from-multisig-address")] #[clap(hide = true)] MakeTxToSendTokensFromMultisigAddress { @@ -709,7 +723,7 @@ pub enum WalletCommand { /// The transaction outputs, in the format `transfer(token_id,address,amount)` /// e.g. transfer(tmltk1e7egscactagl7e3met67658hpl4vf9ux0ralaculjvnzhtc4qmsqv9y857,tmt1q8lhgxhycm8e6yk9zpnetdwtn03h73z70c3ha4l7,0.9) - outputs: Vec, + outputs: Vec, }, #[clap(name = "address-send")] @@ -722,8 +736,7 @@ pub enum WalletCommand { /// A utxo can be from a transaction output or a block reward output: /// e.g tx(000000000000000000059fa50103b9683e51e5aba83b8a34c9b98ce67d66136c,1) or /// block(000000000000000000059fa50103b9683e51e5aba83b8a34c9b98ce67d66136c,2) - #[arg(default_values_t = Vec::::new())] - utxos: Vec, + utxos: Vec, }, /// Sweep all spendable coins or tokens from the specified (or all) addresses to the given destination address. @@ -761,7 +774,7 @@ pub enum WalletCommand { /// You can choose what utxo to spend. A utxo can be from a transaction output or a block reward output: /// e.g tx(000000000000000000059fa50103b9683e51e5aba83b8a34c9b98ce67d66136c,1) or /// block(000000000000000000059fa50103b9683e51e5aba83b8a34c9b98ce67d66136c,2) - utxo: String, + utxo: CliUtxoOutPoint, /// Optional change address; if not specified, it returns the change to the same address from the input. #[arg(long = "change")] change_address: Option, @@ -900,7 +913,7 @@ pub enum WalletCommand { /// Enable or disable p2p networking in the node #[clap(name = "node-enable-p2p-networking")] - NodeEnableNetworking { enable: EnableOrDisable }, + NodeEnableNetworking { enable: CliEnableOrDisable }, #[clap(name = "node-connect-to-peer")] Connect { address: IpOrSocketAddress }, @@ -1018,7 +1031,7 @@ pub enum WalletCommand { /// of the next block. seconds_to_check_for_height: u64, /// This determines how "seconds_to_check_for_height" will be interpreted - check_all_timestamps_between_blocks: YesNo, + check_all_timestamps_between_blocks: CliYesNo, }, /// Return mainchain block ids with heights in the given range using the given step. @@ -1042,21 +1055,25 @@ pub enum WalletCommand { step: NonZeroUsize, }, + // TODO: rework `CliUnspecifiedCurrencyTransfer` into a more general `CliCurrencyTransfer`, + // which would allow the user to say "transfer(token_id,addr,amount)" and "transfer(coin,addr,amount)" + // (while also supporting "transfer(addr,amount)" for backward compatibility). + // Then make `transaction-compose` support token transfers as well. #[clap(name = "transaction-compose")] TransactionCompose { /// The transaction outputs, in the format `transfer(address,amount)` /// e.g. transfer(tmt1q8lhgxhycm8e6yk9zpnetdwtn03h73z70c3ha4l7,0.9) - outputs: Vec, + outputs: Vec, /// You can choose what utxos to spend (space separated as additional arguments). /// /// A utxo can be from a transaction output or a block reward output: /// e.g tx(000000000000000000059fa50103b9683e51e5aba83b8a34c9b98ce67d66136c,1) or /// block(000000000000000000059fa50103b9683e51e5aba83b8a34c9b98ce67d66136c,2) - #[arg(long = "utxos", default_values_t = Vec::::new())] - utxos: Vec, + #[arg(long = "utxos")] + utxos: Vec, - /// This specifies that instead of a (hex encoded) PartiallySignedTransaction - /// the result should be a (hex encoded) "simple" transaction. + /// This specifies that instead of a (hex-encoded) PartiallySignedTransaction + /// the result should be a (hex-encoded) "simple" transaction. /// /// Producing a "simple" transaction will result in a shorter hex string, but you won't /// be able to use it with account-sign-raw-transaction in the cold wallet mode, which @@ -1118,13 +1135,17 @@ pub enum WalletCommand { #[clap(name = "order-create")] CreateOrder { /// The currency you are asking for - a token id or "coin" for coins. - ask_currency: String, + ask_currency: CliCurrency, + /// The asked amount. ask_amount: DecimalAmount, + /// The currency you will be giving - a token id or "coin" for coins. - give_currency: String, + give_currency: CliCurrency, + /// The given amount. give_amount: DecimalAmount, + /// The address (key) that can authorize the conclusion and freezing of the order. conclude_address: String, }, @@ -1189,11 +1210,11 @@ pub enum WalletCommand { ListActiveOrders { /// Filter orders by the specified "asked" currency - pass a token id or "coin" for coins. #[arg(long = "ask-currency")] - ask_currency: Option, + 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, + give_currency: Option, }, } diff --git a/wallet/wallet-controller/src/types/mod.rs b/wallet/wallet-controller/src/types/mod.rs index 1f2862d8f2..6beec191a1 100644 --- a/wallet/wallet-controller/src/types/mod.rs +++ b/wallet/wallet-controller/src/types/mod.rs @@ -31,7 +31,7 @@ use common::{ tokens::{RPCTokenInfo, TokenId}, ChainConfig, Destination, TxOutput, }, - primitives::{DecimalAmount, H256}, + primitives::{amount::decimal::DecimalAmountWithIsSameComparison, DecimalAmount, H256}, }; use utils::ensure; use wallet::signer::trezor_signer::FoundDevice; @@ -94,7 +94,7 @@ impl GenericCurrencyTransfer { let decimals = chain_config.coin_decimals(); let output_val = OutputValue::Coin(self.amount.to_amount(decimals).ok_or( GenericCurrencyTransferToTxOutputConversionError::AmountNotConvertible( - self.amount, + self.amount.into(), decimals, ), )?); @@ -111,7 +111,7 @@ impl GenericCurrencyTransfer { token_info.token_id(), self.amount.to_amount(decimals).ok_or( GenericCurrencyTransferToTxOutputConversionError::AmountNotConvertible( - self.amount, + self.amount.into(), decimals, ), )?, @@ -159,10 +159,10 @@ impl GenericTokenTransfer { } } -#[derive(thiserror::Error, Debug)] +#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] pub enum GenericCurrencyTransferToTxOutputConversionError { #[error("Decimal amount {0} can't be converted to Amount with {1} decimals")] - AmountNotConvertible(DecimalAmount, u8), + AmountNotConvertible(DecimalAmountWithIsSameComparison, u8), #[error("Unexpected token id {actual} (expecting {expected})")] UnexpectedTokenId { expected: TokenId, actual: TokenId }, } diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 68291e309e..e76fd50c51 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -122,7 +122,7 @@ pub enum RpcError { #[error("{0}")] SubmitError(#[from] SubmitError), - #[error("Invalid hex encoded transaction")] + #[error("Invalid raw transaction")] InvalidRawTransaction, #[error( From 107830dcb898e17bb26472860f9be0c9cfb6d4bd Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 23 Feb 2026 16:30:17 +0200 Subject: [PATCH 2/6] Replace "hex encoded" with "hex-encoded" in comments and docs --- serialization/src/hex_encoded.rs | 2 +- .../test_framework/wallet_cli_controller.py | 4 ++-- .../test_framework/wallet_rpc_controller.py | 4 ++-- test/functional/wallet_multisig_address.py | 2 +- test/functional/wallet_sign_message.py | 2 +- test/functional/wallet_tx_compose.py | 4 ++-- .../src/command_handler/mod.rs | 10 ++++----- wallet/wallet-cli-commands/src/lib.rs | 22 +++++++++---------- wallet/wallet-rpc-daemon/docs/RPC.md | 10 ++++----- wallet/wallet-rpc-lib/src/rpc/interface.rs | 10 ++++----- 10 files changed, 35 insertions(+), 35 deletions(-) diff --git a/serialization/src/hex_encoded.rs b/serialization/src/hex_encoded.rs index dfb1d76f56..86b62c6958 100644 --- a/serialization/src/hex_encoded.rs +++ b/serialization/src/hex_encoded.rs @@ -17,7 +17,7 @@ use std::{fmt::Display, str::FromStr}; use crate::hex::{HexDecode, HexEncode, HexError}; -/// Wrapper that serializes objects as hex encoded string for `serde` +/// Wrapper that serializes objects as hex-encoded string for `serde` #[derive(Debug, Clone, PartialEq, Eq)] pub struct HexEncoded(T); diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index 3247fa701f..2ea9cf1d3e 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -585,9 +585,9 @@ async def make_tx_to_send_tokens_with_intent( f"token-make-tx-to-send-with-intent {token_id} {destination} {amount} {intent}\n") pattern = ( - r'The hex encoded transaction is:\n([0-9a-fA-F]+)\n\n' + r'The hex-encoded transaction is:\n([0-9a-fA-F]+)\n\n' r'The transaction id is:\n([0-9a-fA-F]+)\n\n' - r'The hex encoded signed transaction intent is:\n([0-9a-fA-F]+)' + r'The hex-encoded signed transaction intent is:\n([0-9a-fA-F]+)' ) match = re.search(pattern, output) assert match is not None diff --git a/test/functional/test_framework/wallet_rpc_controller.py b/test/functional/test_framework/wallet_rpc_controller.py index c3e8556e77..49e7689c86 100644 --- a/test/functional/test_framework/wallet_rpc_controller.py +++ b/test/functional/test_framework/wallet_rpc_controller.py @@ -546,14 +546,14 @@ async def sign_raw_transaction_expect_fully_signed(self, transaction: str) -> st async def sign_challenge_plain(self, message: str, address: str) -> str: result = self._write_command('challenge_sign_plain', [self.account, message, address]) if 'result' in result: - return f"The generated hex encoded signature is\n\n{result['result']}" + return f"The generated hex-encoded signature is\n\n{result['result']}" else: return result['error']['message'] async def sign_challenge_hex(self, message: str, address: str) -> str: result = self._write_command('challenge_sign_hex', [self.account, message, address]) if 'result' in result: - return f"The generated hex encoded signature is\n\n{result['result']}" + return f"The generated hex-encoded signature is\n\n{result['result']}" else: return result['error']['message'] diff --git a/test/functional/wallet_multisig_address.py b/test/functional/wallet_multisig_address.py index 6190ea195f..3d251f4f33 100644 --- a/test/functional/wallet_multisig_address.py +++ b/test/functional/wallet_multisig_address.py @@ -195,7 +195,7 @@ async def async_test(self): coins_from_multisig = '0.1' outputs = [TxOutput(address, coins_from_multisig) ] output = await wallet.compose_transaction(outputs, utxos, True) - assert_in("The hex encoded transaction is", output) + assert_in("The hex-encoded transaction is", output) encoded_tx = output.split('\n')[1] # sign the transaction from N random wallets diff --git a/test/functional/wallet_sign_message.py b/test/functional/wallet_sign_message.py index 06f5137aae..5423a13165 100644 --- a/test/functional/wallet_sign_message.py +++ b/test/functional/wallet_sign_message.py @@ -64,7 +64,7 @@ async def async_test(self): output = await wallet.sign_challenge_hex(message, destination) else: output = await wallet.sign_challenge_plain(message, destination) - assert_in("The generated hex encoded signature is", output) + assert_in("The generated hex-encoded signature is", output) signature = output.split('\n')[2] await wallet.close_wallet() diff --git a/test/functional/wallet_tx_compose.py b/test/functional/wallet_tx_compose.py index c20b2b6b08..acd00bed0d 100644 --- a/test/functional/wallet_tx_compose.py +++ b/test/functional/wallet_tx_compose.py @@ -142,7 +142,7 @@ def make_output(pub_key_bytes): # compose a transaction with all our utxos and n outputs to the other acc and 1 as change output = await wallet.compose_transaction(outputs, utxos, True) - assert_in("The hex encoded transaction is", output) + assert_in("The hex-encoded transaction is", output) # check the fees include the 0.1 + any extra utxos assert_in(f"Coins amount: {((len(addresses) - (num_outputs + 1))*coins_to_send)}.1", output) encoded_tx = output.split('\n')[1] @@ -158,7 +158,7 @@ def make_output(pub_key_bytes): assert_in(f"Missing signatures: {len(utxos)}", output) output = await wallet.compose_transaction(outputs, utxos, False) - assert_in("The hex encoded transaction is", output) + assert_in("The hex-encoded transaction is", output) # check the fees include the 0.1 + any extra utxos assert_in(f"Coins amount: {((len(addresses) - (num_outputs + 1))*coins_to_send)}.1", output) encoded_ptx = output.split('\n')[1] diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 533e9aaffc..09814469c0 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -772,7 +772,7 @@ where }; Ok(ConsoleCommand::Print(format!( - "The generated hex encoded signature is\n\n{result}{qr_code_string}", + "The generated hex-encoded signature is\n\n{result}{qr_code_string}", ))) } @@ -791,7 +791,7 @@ where }; Ok(ConsoleCommand::Print(format!( - "The generated hex encoded signature is\n\n{result}{qr_code_string}", + "The generated hex-encoded signature is\n\n{result}{qr_code_string}", ))) } @@ -1150,7 +1150,7 @@ where .await? .compose_transaction(utxos, outputs, None, only_transaction) .await?; - let mut output = format!("The hex encoded transaction is:\n{hex}\n"); + let mut output = format!("The hex-encoded transaction is:\n{hex}\n"); format_fees(&mut output, &fees); @@ -1645,9 +1645,9 @@ where let output = format!( concat!( - "The hex encoded transaction is:\n{}\n\n", + "The hex-encoded transaction is:\n{}\n\n", "The transaction id is:\n{:x}\n\n", - "The hex encoded signed transaction intent is:\n{}\n" + "The hex-encoded signed transaction intent is:\n{}\n" ), signed_tx, tx_id, signed_intent ); diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index 55720b9114..d1f4386d98 100644 --- a/wallet/wallet-cli-commands/src/lib.rs +++ b/wallet/wallet-cli-commands/src/lib.rs @@ -365,7 +365,7 @@ pub enum ColdWalletCommand { #[clap(name = "address-new")] NewAddress, - /// Reveal the public key behind the specified "public key hash" address as a hex encoded string. + /// Reveal the public key behind the specified "public key hash" address as a hex-encoded string. #[clap(name = "address-reveal-public-key-as-hex")] RevealPublicKeyHex { public_key_hash: String, @@ -409,14 +409,14 @@ pub enum ColdWalletCommand { #[clap(name = "account-sign-raw-transaction")] SignRawTransaction { - /// Hex encoded transaction or PartiallySignedTransaction. + /// Hex-encoded transaction or PartiallySignedTransaction. transaction: String, }, #[clap(name = "challenge-sign-hex")] #[clap(hide = true)] SignChallegeHex { - /// Hex encoded message to be signed + /// Hex-encoded message to be signed message: String, /// Address with whose private key to sign the challenge address: String, @@ -433,9 +433,9 @@ pub enum ColdWalletCommand { #[clap(name = "challenge-verify-hex")] #[clap(hide = true)] VerifyChallengeHex { - /// The hex encoded message that was signed + /// The hex-encoded message that was signed message: String, - /// Hex encoded signed challenge + /// Hex-encoded signed challenge signed_challenge: String, /// Address with whose private key the challenge was signed with address: String, @@ -445,7 +445,7 @@ pub enum ColdWalletCommand { VerifyChallenge { /// The message that was signed message: String, - /// Hex encoded signed challenge + /// Hex-encoded signed challenge signed_challenge: String, /// Address with whose private key the challenge was signed with address: String, @@ -543,7 +543,7 @@ pub enum WalletCommand { #[clap(name = "standalone-add-private-key-from-hex")] AddStandalonePrivateKey { - /// The new hex encoded standalone private key to be added to the selected account + /// The new hex-encoded standalone private key to be added to the selected account hex_private_key: HexEncoded, /// Optionally specify a label to the new address @@ -597,7 +597,7 @@ pub enum WalletCommand { description: String, /// Ticker of the token ticker: String, - /// The owner, represented by a public key (hex encoded) + /// The owner, represented by a public key (hex-encoded) creator: Option>, /// URI for the icon of the NFT icon_uri: Option, @@ -782,7 +782,7 @@ pub enum WalletCommand { #[clap(name = "transaction-inspect")] InspectTransaction { - /// Hex encoded transaction or PartiallySignedTransaction. + /// Hex-encoded transaction or PartiallySignedTransaction. transaction: String, }, @@ -965,13 +965,13 @@ pub enum WalletCommand { #[clap(name = "node-submit-block")] SubmitBlock { - /// Hex encoded block + /// Hex-encoded block block: HexEncoded, }, #[clap(name = "node-submit-transaction")] SubmitTransaction { - /// Hex encoded transaction. + /// Hex-encoded transaction. transaction: HexEncoded, /// Do not store the transaction in the wallet #[arg(long = "do-not-store", default_value_t = false)] diff --git a/wallet/wallet-rpc-daemon/docs/RPC.md b/wallet/wallet-rpc-daemon/docs/RPC.md index ff67f19630..1bd8ae206e 100644 --- a/wallet/wallet-rpc-daemon/docs/RPC.md +++ b/wallet/wallet-rpc-daemon/docs/RPC.md @@ -3095,7 +3095,7 @@ json ### Method `transaction_get_raw` -Get a transaction from the wallet, if present, as hex encoded raw transaction +Get a transaction from the wallet, if present, as hex-encoded raw transaction Parameters: @@ -3113,7 +3113,7 @@ hex string ### Method `transaction_get_signed_raw` -Get a signed transaction from the wallet, if present, as hex encoded raw transaction +Get a signed transaction from the wallet, if present, as hex-encoded raw transaction Parameters: @@ -3133,7 +3133,7 @@ hex string Compose a new transaction from the specified outputs and selected utxos. -The transaction is returned in a hex encoded form that can be passed to account-sign-raw-transaction. +The transaction is returned in a hex-encoded form that can be passed to account-sign-raw-transaction. The fees that will be paid by the transaction are also returned. @@ -3298,7 +3298,7 @@ Returns: ### Method `node_get_block` -Get a block by its id, represented as hex encoded bytes +Get a block by its id, represented as hex-encoded bytes Parameters: @@ -4044,7 +4044,7 @@ Returns: Signs transaction inputs that are not yet signed. -The input is a hex encoded transaction or PartiallySignedTransaction. This format is +The input is a hex-encoded transaction or PartiallySignedTransaction. This format is automatically used in this wallet in functions such as staking-decommission-pool-request. Once all signatures are complete, the result can be broadcast to the network. diff --git a/wallet/wallet-rpc-lib/src/rpc/interface.rs b/wallet/wallet-rpc-lib/src/rpc/interface.rs index 4b68861d60..60b42a0e55 100644 --- a/wallet/wallet-rpc-lib/src/rpc/interface.rs +++ b/wallet/wallet-rpc-lib/src/rpc/interface.rs @@ -263,7 +263,7 @@ trait ColdWalletRpc { /// Signs transaction inputs that are not yet signed. /// - /// The input is a hex encoded transaction or PartiallySignedTransaction. This format is + /// The input is a hex-encoded transaction or PartiallySignedTransaction. This format is /// automatically used in this wallet in functions such as staking-decommission-pool-request. /// /// Once all signatures are complete, the result can be broadcast to the network. @@ -995,7 +995,7 @@ trait WalletRpc { transaction_id: Id, ) -> rpc::RpcResult; - /// Get a transaction from the wallet, if present, as hex encoded raw transaction + /// Get a transaction from the wallet, if present, as hex-encoded raw transaction #[method(name = "transaction_get_raw")] async fn get_raw_transaction( &self, @@ -1003,7 +1003,7 @@ trait WalletRpc { transaction_id: Id, ) -> rpc::RpcResult>; - /// Get a signed transaction from the wallet, if present, as hex encoded raw transaction + /// Get a signed transaction from the wallet, if present, as hex-encoded raw transaction #[method(name = "transaction_get_signed_raw")] async fn get_raw_signed_transaction( &self, @@ -1013,7 +1013,7 @@ trait WalletRpc { /// Compose a new transaction from the specified outputs and selected utxos. /// - /// The transaction is returned in a hex encoded form that can be passed to account-sign-raw-transaction. + /// The transaction is returned in a hex-encoded form that can be passed to account-sign-raw-transaction. /// /// The fees that will be paid by the transaction are also returned. #[method(name = "transaction_compose")] @@ -1079,7 +1079,7 @@ trait WalletRpc { check_all_timestamps_between_blocks: bool, ) -> rpc::RpcResult>>; - /// Get a block by its id, represented as hex encoded bytes + /// Get a block by its id, represented as hex-encoded bytes #[method(name = "node_get_block")] async fn node_block(&self, block_id: Id) -> rpc::RpcResult>>; From 89636ec16e5304cdbf8de55d9847584688a3848c Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 23 Feb 2026 17:58:19 +0200 Subject: [PATCH 3/6] Remove redundant (and inappropriately named) methods `process_send_request` and `process_send_request_and_sign` from `wallet::Account`, use `select_inputs_for_send_request` directly instead --- wallet/src/account/mod.rs | 55 ++------------------------------------- wallet/src/wallet/mod.rs | 23 ++++++++++------ 2 files changed, 17 insertions(+), 61 deletions(-) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 1f5db6f2f8..4840305c6a 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -42,7 +42,7 @@ use utxo_selector::SelectionResult; pub use utxo_selector::UtxoSelectorError; use wallet_types::account_id::AccountPrefixedId; use wallet_types::account_info::{StandaloneAddressDetails, StandaloneAddresses}; -use wallet_types::partially_signed_transaction::{PartiallySignedTransaction, PtxAdditionalInfo}; +use wallet_types::partially_signed_transaction::{PartiallySignedTransaction}; use wallet_types::with_locked::WithLocked; use crate::account::utxo_selector::{select_coins, OutputGroup}; @@ -201,7 +201,7 @@ impl Account { // Note: the default selection algo depends on whether input_utxos are empty. #[allow(clippy::too_many_arguments)] - fn select_inputs_for_send_request( + pub fn select_inputs_for_send_request( &mut self, mut request: SendRequest, input_utxos: SelectedInputs, @@ -660,57 +660,6 @@ impl Account { Ok(req) } - #[allow(clippy::too_many_arguments)] - pub fn process_send_request( - &mut self, - db_tx: &mut impl WalletStorageWriteLocked, - request: SendRequest, - inputs: SelectedInputs, - selection_algo: Option, - change_addresses: BTreeMap>, - median_time: BlockTimestamp, - fee_rate: CurrentFeeRate, - ptx_additional_info: PtxAdditionalInfo, - ) -> WalletResult<(PartiallySignedTransaction, BTreeMap)> { - let mut request = self.select_inputs_for_send_request( - request, - inputs, - selection_algo, - change_addresses, - db_tx, - median_time, - fee_rate, - None, - )?; - - let fees = request.get_fees(); - let ptx = request.into_partially_signed_tx(ptx_additional_info)?; - - Ok((ptx, fees)) - } - - pub fn process_send_request_and_sign( - &mut self, - db_tx: &mut impl WalletStorageWriteLocked, - request: SendRequest, - inputs: SelectedInputs, - change_addresses: BTreeMap>, - median_time: BlockTimestamp, - fee_rate: CurrentFeeRate, - ) -> WalletResult { - self.select_inputs_for_send_request( - request, - inputs, - None, - change_addresses, - db_tx, - median_time, - fee_rate, - None, - ) - // TODO: Randomize inputs and outputs - } - fn decommission_stake_pool_impl( &mut self, db_tx: &mut impl WalletStorageWriteLocked, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 86a918c15f..f9892a5837 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -1734,20 +1734,22 @@ where account_index, additional_info, |account, db_tx| { - let send_request = account.process_send_request_and_sign( - db_tx, + let request = account.select_inputs_for_send_request( request, inputs, + None, change_addresses, + db_tx, latest_median_time, CurrentFeeRate { current_fee_rate, consolidate_fee_rate, }, + None, )?; - let additional_data = additional_data_getter(&send_request); - Ok((send_request, additional_data)) + let additional_data = additional_data_getter(&request); + Ok((request, additional_data)) }, |err| err, ) @@ -1769,19 +1771,24 @@ where let request = SendRequest::new().with_outputs(outputs); let latest_median_time = self.latest_median_time; self.for_account_rw(account_index, |account, db_tx| { - account.process_send_request( - db_tx, + let mut request = account.select_inputs_for_send_request( request, inputs, selection_algo, change_addresses, + db_tx, latest_median_time, CurrentFeeRate { current_fee_rate, consolidate_fee_rate, }, - ptx_additional_info, - ) + None, + )?; + + let fees = request.get_fees(); + let ptx = request.into_partially_signed_tx(ptx_additional_info)?; + + Ok((ptx, fees)) }) } From bc4306012961288062cd0b019b5c2b748e81d5dc Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 23 Feb 2026 18:02:23 +0200 Subject: [PATCH 4/6] Minor cleanup --- wallet/src/account/mod.rs | 153 ++++++++++-------- wallet/src/wallet/mod.rs | 139 ++++++++-------- .../src/synced_controller.rs | 8 +- 3 files changed, 159 insertions(+), 141 deletions(-) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 4840305c6a..918cbddd5c 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -18,85 +18,96 @@ mod output_cache; pub mod transaction_list; mod utxo_selector; -use common::address::pubkeyhash::PublicKeyHash; -use common::chain::block::timestamp::BlockTimestamp; -use common::chain::classic_multisig::ClassicMultisigChallenge; -use common::chain::htlc::HashedTimelockContract; -use common::chain::{ - AccountCommand, AccountOutPoint, AccountSpending, OrderAccountCommand, OrderId, OrdersVersion, - RpcOrderInfo, +use std::{ + cmp::Reverse, + collections::{btree_map::Entry, BTreeMap, BTreeSet}, + ops::{Add, Sub}, + sync::Arc, }; -use common::primitives::id::WithId; -use common::primitives::{Idable, H256}; -use common::size_estimation::{ - input_signature_size, input_signature_size_from_destination, outputs_encoded_size, - tx_size_with_num_inputs_and_outputs, DestinationInfoProvider, + +use itertools::{izip, Itertools}; + +use common::{ + address::{pubkeyhash::PublicKeyHash, Address, RpcAddress}, + chain::{ + block::timestamp::BlockTimestamp, + classic_multisig::ClassicMultisigChallenge, + htlc::HashedTimelockContract, + make_token_id, + output_value::{OutputValue, RpcOutputValue}, + tokens::{ + IsTokenUnfreezable, NftIssuance, NftIssuanceV0, RPCFungibleTokenInfo, TokenId, + TokenIssuance, + }, + AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, Block, ChainConfig, + Currency, DelegationId, Destination, GenBlock, OrderAccountCommand, OrderId, OrdersVersion, + PoolId, RpcOrderInfo, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, + }, + primitives::{ + id::{Idable, WithId, H256}, + Amount, BlockHeight, Id, + }, + size_estimation::{ + input_signature_size, input_signature_size_from_destination, outputs_encoded_size, + tx_size_with_num_inputs_and_outputs, DestinationInfoProvider, + }, + Uint256, +}; +use consensus::PoSGenerateBlockInputData; +use crypto::{ + key::{ + extended::ExtendedPublicKey, + hdkd::{child_number::ChildNumber, u31::U31}, + PrivateKey, PublicKey, + }, + vrf::VRFPublicKey, }; -use common::Uint256; -use crypto::key::extended::ExtendedPublicKey; -use crypto::key::hdkd::child_number::ChildNumber; use mempool::FeeRate; use serialization::hex_encoded::HexEncoded; use utils::ensure; -use utxo_selector::SelectionResult; -pub use utxo_selector::UtxoSelectorError; -use wallet_types::account_id::AccountPrefixedId; -use wallet_types::account_info::{StandaloneAddressDetails, StandaloneAddresses}; -use wallet_types::partially_signed_transaction::{PartiallySignedTransaction}; -use wallet_types::with_locked::WithLocked; - -use crate::account::utxo_selector::{select_coins, OutputGroup}; -use crate::destination_getters::{get_tx_output_destination, HtlcSpendingCondition}; -use crate::key_chain::{AccountKeyChains, KeyChainError, VRFAccountKeyChains}; -use crate::send_request::{ - make_address_output, make_address_output_from_delegation, make_address_output_token, - make_decommission_stake_pool_output, make_mint_token_outputs, make_stake_output, - make_unmint_token_outputs, IssueNftArguments, SelectedInputs, StakePoolCreationArguments, - StakePoolCreationResolvedArguments, -}; -use crate::wallet::WalletPoolsFilter; -use crate::wallet_events::{WalletEvents, WalletEventsNoOp}; -use crate::{SendRequest, WalletError, WalletResult}; -use common::address::{Address, RpcAddress}; -use common::chain::output_value::{OutputValue, RpcOutputValue}; -use common::chain::tokens::{ - IsTokenUnfreezable, NftIssuance, NftIssuanceV0, RPCFungibleTokenInfo, TokenId, TokenIssuance, -}; -use common::chain::{ - 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; -use crypto::key::hdkd::u31::U31; -use crypto::key::{PrivateKey, PublicKey}; -use crypto::vrf::VRFPublicKey; -use itertools::{izip, Itertools}; -use std::cmp::Reverse; -use std::collections::btree_map::Entry; -use std::collections::{BTreeMap, BTreeSet}; -use std::ops::{Add, Sub}; -use std::sync::Arc; use wallet_storage::{ StoreTxRw, WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteLocked, WalletStorageWriteUnlocked, }; -use wallet_types::utxo_types::{get_utxo_type, UtxoState, UtxoStates, UtxoType, UtxoTypes}; -use wallet_types::wallet_tx::{BlockData, TxData, TxState}; use wallet_types::{ + account_id::AccountPrefixedId, + account_info::{StandaloneAddressDetails, StandaloneAddresses}, + partially_signed_transaction::PartiallySignedTransaction, + utxo_types::{get_utxo_type, UtxoState, UtxoStates, UtxoType, UtxoTypes}, + wallet_tx::{BlockData, TxData, TxState}, + with_locked::WithLocked, AccountId, AccountInfo, AccountWalletCreatedTxId, AccountWalletTxId, BlockInfo, KeyPurpose, KeychainUsageState, WalletTx, }; -pub use self::output_cache::{ - DelegationData, OrderData, OutputCacheInconsistencyError, OwnFungibleTokenInfo, PoolData, - TxChanged, TxInfo, UnconfirmedTokenInfo, UtxoWithTxOutput, +use crate::{ + account::utxo_selector::{select_coins, OutputGroup}, + destination_getters::{get_tx_output_destination, HtlcSpendingCondition}, + key_chain::{AccountKeyChains, KeyChainError, VRFAccountKeyChains}, + send_request::{ + make_address_output, make_address_output_from_delegation, make_address_output_token, + make_decommission_stake_pool_output, make_mint_token_outputs, make_stake_output, + make_unmint_token_outputs, IssueNftArguments, SelectedInputs, StakePoolCreationArguments, + StakePoolCreationResolvedArguments, + }, + wallet::WalletPoolsFilter, + wallet_events::{WalletEvents, WalletEventsNoOp}, + SendRequest, WalletError, WalletResult, }; -use self::output_cache::{OutputCache, TokenIssuanceData}; -use self::transaction_list::{get_transaction_list, TransactionList}; -use self::utxo_selector::PayFee; -pub use self::utxo_selector::CoinSelectionAlgo; +use self::{ + output_cache::{OutputCache, TokenIssuanceData}, + transaction_list::{get_transaction_list, TransactionList}, + utxo_selector::{PayFee, SelectionResult}, +}; + +pub use self::{ + output_cache::{ + DelegationData, OrderData, OutputCacheInconsistencyError, OwnFungibleTokenInfo, PoolData, + TxChanged, TxInfo, UnconfirmedTokenInfo, UtxoWithTxOutput, + }, + utxo_selector::{CoinSelectionAlgo, UtxoSelectorError}, +}; pub struct CurrentFeeRate { pub current_fee_rate: FeeRate, @@ -1212,7 +1223,7 @@ impl Account { let tx_input = TxInput::AccountCommand(nonce, AccountCommand::MintTokens(token_id, amount)); let authority = token_info.authority()?.clone(); - self.change_token_supply_transaction( + self.make_change_token_transaction( authority, tx_input, outputs, @@ -1239,7 +1250,7 @@ impl Account { let tx_input = TxInput::AccountCommand(nonce, AccountCommand::UnmintTokens(token_id)); let authority = token_info.authority()?.clone(); - self.change_token_supply_transaction( + self.make_change_token_transaction( authority, tx_input, outputs, @@ -1263,7 +1274,7 @@ impl Account { let tx_input = TxInput::AccountCommand(nonce, AccountCommand::LockTokenSupply(token_id)); let authority = token_info.authority()?.clone(); - self.change_token_supply_transaction( + self.make_change_token_transaction( authority, tx_input, vec![], @@ -1290,7 +1301,7 @@ impl Account { ); let authority = token_info.authority()?.clone(); - self.change_token_supply_transaction( + self.make_change_token_transaction( authority, tx_input, vec![], @@ -1314,7 +1325,7 @@ impl Account { TxInput::AccountCommand(nonce, AccountCommand::UnfreezeToken(token_info.token_id())); let authority = token_info.authority()?.clone(); - self.change_token_supply_transaction( + self.make_change_token_transaction( authority, tx_input, vec![], @@ -1341,7 +1352,7 @@ impl Account { ); let authority = token_info.authority()?.clone(); - self.change_token_supply_transaction( + self.make_change_token_transaction( authority, tx_input, vec![], @@ -1366,7 +1377,7 @@ impl Account { ); let authority = token_info.authority()?.clone(); - self.change_token_supply_transaction( + self.make_change_token_transaction( authority, tx_input, vec![], @@ -1376,7 +1387,7 @@ impl Account { ) } - fn change_token_supply_transaction( + fn make_change_token_transaction( &mut self, authority: Destination, tx_input: TxInput, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index f9892a5837..a19c3db88a 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -13,87 +13,94 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::{BTreeMap, BTreeSet}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use crate::account::{ - transaction_list::TransactionList, CoinSelectionAlgo, CurrentFeeRate, DelegationData, - OrderData, OutputCacheInconsistencyError, PoolData, TxInfo, UnconfirmedTokenInfo, - UtxoSelectorError, +use std::{ + collections::{BTreeMap, BTreeSet}, + path::{Path, PathBuf}, + sync::Arc, }; -use crate::destination_getters::HtlcSpendingCondition; -use crate::key_chain::{ - make_account_path, make_path_to_vrf_key, AccountKeyChainImplSoftware, KeyChainError, - MasterKeyChain, LOOKAHEAD_SIZE, VRF_INDEX, -}; -use crate::send_request::{ - make_issue_token_outputs, IssueNftArguments, SelectedInputs, StakePoolCreationArguments, -}; -#[cfg(feature = "trezor")] -use crate::signer::trezor_signer::{FoundDevice, TrezorError}; -use crate::signer::{Signer, SignerError, SignerProvider}; -use crate::wallet_events::{WalletEvents, WalletEventsNoOp}; -use crate::{Account, SendRequest}; -pub use bip39::{Language, Mnemonic}; -use common::address::pubkeyhash::PublicKeyHash; -use common::address::{Address, AddressError, RpcAddress}; -use common::chain::block::timestamp::BlockTimestamp; -use common::chain::classic_multisig::ClassicMultisigChallenge; -use common::chain::htlc::HashedTimelockContract; -use common::chain::output_value::OutputValue; -use common::chain::signature::inputsig::arbitrary_message::{ - ArbitraryMessageSignature, SignArbitraryMessageError, -}; -use common::chain::signature::DestinationSigError; -use common::chain::tokens::{ - IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance, -}; -use common::chain::{ - 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::{ + address::{pubkeyhash::PublicKeyHash, Address, AddressError, RpcAddress}, + chain::{ + block::timestamp::BlockTimestamp, + classic_multisig::ClassicMultisigChallenge, + htlc::HashedTimelockContract, + make_delegation_id, make_order_id, make_token_id, + output_value::OutputValue, + signature::{ + inputsig::arbitrary_message::{ArbitraryMessageSignature, SignArbitraryMessageError}, + DestinationSigError, + }, + tokens::{IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance}, + AccountCommand, AccountOutPoint, Block, ChainConfig, Currency, DelegationId, Destination, + GenBlock, IdCreationError, OrderAccountCommand, OrderId, OutPointSourceId, PoolId, + RpcOrderInfo, SignedTransaction, SignedTransactionIntent, Transaction, + TransactionCreationError, TxInput, TxOutput, UtxoOutPoint, + }, + primitives::{ + id::{hash_encoded, WithId}, + Amount, BlockHeight, Id, H256, + }, + size_estimation::SizeEstimationError, }; -use common::primitives::id::{hash_encoded, WithId}; -use common::primitives::{Amount, BlockHeight, Id, H256}; -use common::size_estimation::SizeEstimationError; use consensus::PoSGenerateBlockInputData; -use crypto::key::extended::ExtendedPublicKey; -use crypto::key::hdkd::child_number::ChildNumber; -use crypto::key::hdkd::derivable::Derivable; -use crypto::key::hdkd::u31::U31; -use crypto::key::{PrivateKey, PublicKey}; -use crypto::vrf::VRFPublicKey; +use crypto::{ + key::{ + extended::ExtendedPublicKey, + hdkd::{child_number::ChildNumber, derivable::Derivable, u31::U31}, + PrivateKey, PublicKey, + }, + vrf::VRFPublicKey, +}; use mempool::FeeRate; -use tx_verifier::error::TokenIssuanceError; -use tx_verifier::{check_transaction, CheckTransactionError}; +use tx_verifier::{check_transaction, error::TokenIssuanceError, CheckTransactionError}; use utils::{debug_panic_or_log, ensure}; -pub use wallet_storage::Error; use wallet_storage::{ DefaultBackend, Store, StoreTxRo, StoreTxRw, StoreTxRwUnlocked, TransactionRoLocked, TransactionRwLocked, TransactionRwUnlocked, Transactional, WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteLocked, WalletStorageWriteUnlocked, }; -use wallet_types::account_info::{StandaloneAddressDetails, StandaloneAddresses}; -use wallet_types::chain_info::ChainInfo; -use wallet_types::hw_data::HardwareWalletFullInfo; -use wallet_types::partially_signed_transaction::{ - PartiallySignedTransaction, PartiallySignedTransactionError, PoolAdditionalInfo, - PtxAdditionalInfo, TokenAdditionalInfo, TokensAdditionalInfo, TxAdditionalInfo, -}; -use wallet_types::seed_phrase::SerializableSeedPhrase; -use wallet_types::signature_status::SignatureStatus; -use wallet_types::utxo_types::{UtxoStates, UtxoTypes}; -use wallet_types::wallet_tx::{TxData, TxState}; -use wallet_types::wallet_type::{WalletControllerMode, WalletType}; -use wallet_types::with_locked::WithLocked; use wallet_types::{ + account_info::{StandaloneAddressDetails, StandaloneAddresses}, + chain_info::ChainInfo, + hw_data::HardwareWalletFullInfo, + partially_signed_transaction::{ + PartiallySignedTransaction, PartiallySignedTransactionError, PoolAdditionalInfo, + PtxAdditionalInfo, TokenAdditionalInfo, TokensAdditionalInfo, TxAdditionalInfo, + }, + seed_phrase::SerializableSeedPhrase, + signature_status::SignatureStatus, + utxo_types::{UtxoStates, UtxoTypes}, + wallet_tx::{TxData, TxState}, + wallet_type::{WalletControllerMode, WalletType}, + with_locked::WithLocked, AccountId, AccountKeyPurposeId, BlockInfo, KeyPurpose, KeychainUsageState, SignedTxWithFees, }; +#[cfg(feature = "trezor")] +use crate::signer::trezor_signer::{FoundDevice, TrezorError}; +use crate::{ + account::{ + transaction_list::TransactionList, CoinSelectionAlgo, CurrentFeeRate, DelegationData, + OrderData, OutputCacheInconsistencyError, PoolData, TxInfo, UnconfirmedTokenInfo, + UtxoSelectorError, + }, + destination_getters::HtlcSpendingCondition, + key_chain::{ + make_account_path, make_path_to_vrf_key, AccountKeyChainImplSoftware, KeyChainError, + MasterKeyChain, LOOKAHEAD_SIZE, VRF_INDEX, + }, + send_request::{ + make_issue_token_outputs, IssueNftArguments, SelectedInputs, StakePoolCreationArguments, + }, + signer::{Signer, SignerError, SignerProvider}, + wallet_events::{WalletEvents, WalletEventsNoOp}, + Account, SendRequest, +}; + +pub use bip39::{Language, Mnemonic}; +pub use wallet_storage::Error; + pub const WALLET_VERSION_UNINITIALIZED: u32 = 0; pub const WALLET_VERSION_V1: u32 = 1; pub const WALLET_VERSION_V2: u32 = 2; diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index 4ca31c5dca..12018eb94f 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -1576,7 +1576,7 @@ where &UnconfirmedTokenInfo, ) -> WalletResult, { - let token_freezable_info = self.unconfirmed_token_info(token_info)?; + let unconfirmed_token_info = self.unconfirmed_token_info(token_info)?; let (current_fee_rate, consolidate_fee_rate) = self.get_current_and_consolidation_fee_rate().await?; @@ -1586,7 +1586,7 @@ where consolidate_fee_rate, self.wallet, self.account_index, - &token_freezable_info, + &unconfirmed_token_info, ) .await .map_err(ControllerError::WalletError)?; @@ -1625,7 +1625,7 @@ where &mut self, token_info: RPCTokenInfo, ) -> Result> { - let token_freezable_info = match token_info { + let unconfirmed_token_info = match token_info { RPCTokenInfo::FungibleToken(token_info) => { self.wallet.get_token_unconfirmed_info(self.account_index, token_info)? } @@ -1633,7 +1633,7 @@ where UnconfirmedTokenInfo::NonFungibleToken(info.token_id, info.as_ref().into()) } }; - Ok(token_freezable_info) + Ok(unconfirmed_token_info) } /// Similar to create_and_send_tx but some transactions also create an ID From 590c10e3542cc484f6cc45d781bc105ba20875b9 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Tue, 24 Feb 2026 11:39:22 +0200 Subject: [PATCH 5/6] chainstate: add missing test for filling/concluding/freezing orders that use a frozen token --- .../src/tests/fungible_tokens_v1.rs | 368 +++++++++++++++++- .../src/synced_controller.rs | 6 + 2 files changed, 369 insertions(+), 5 deletions(-) diff --git a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs index ccb1a8f08e..b098ceaa02 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs @@ -30,7 +30,8 @@ use chainstate_test_framework::{ }; use common::{ chain::{ - make_token_id, + htlc::{HashedTimelockContract, HtlcSecret}, + make_order_id, make_token_id, output_value::OutputValue, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, @@ -43,8 +44,8 @@ use common::{ TokenIssuanceV1, TokenTotalSupply, }, AccountCommand, AccountNonce, AccountType, Block, ChainstateUpgradeBuilder, Destination, - GenBlock, OrderData, OutPointSourceId, SignedTransaction, Transaction, TxInput, TxOutput, - UtxoOutPoint, + GenBlock, OrderAccountCommand, OrderData, OutPointSourceId, SignedTransaction, Transaction, + TxInput, TxOutput, UtxoOutPoint, }, primitives::{amount::SignedAmount, Amount, BlockHeight, CoinOrTokenId, Id, Idable}, }; @@ -4178,8 +4179,6 @@ fn token_issue_mint_and_data_deposit_not_enough_fee(#[case] seed: Seed) { #[trace] #[case(Seed::from_entropy())] fn check_freezable_supply(#[case] seed: Seed) { - use common::chain::htlc::{HashedTimelockContract, HtlcSecret}; - utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); @@ -4573,6 +4572,365 @@ fn check_freezable_supply(#[case] seed: Seed) { }); } +// Check that orders that use a frozen token cannot be filled or concluded, but can be frozen. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_freeze_conclude_order_with_frozen_token( + #[case] seed: Seed, + #[values(false, true)] freeze_orders: bool, +) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::Yes, + ); + + // Mint some tokens + let amount_to_mint = Amount::from_atoms(rng.gen_range(100..1000)); + let best_block_id = tf.best_block_id(); + let (_, mint_tx_id) = mint_tokens_in_block( + &mut rng, + &mut tf, + best_block_id, + utxo_with_change, + token_id, + amount_to_mint, + true, + ); + let utxo_with_tokens_change = UtxoOutPoint::new(mint_tx_id.into(), 0); + let utxo_with_change = UtxoOutPoint::new(mint_tx_id.into(), 1); + let change_amount = tf.coin_amount_from_utxo(&utxo_with_change); + + // Check the token + check_fungible_token( + &tf, + &mut rng, + &token_id, + &ExpectedFungibleTokenData { + issuance: issuance.clone(), + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::Yes), + }, + true, + ); + + // Split the change utxo into 2 + let change_split_tx = TransactionBuilder::new() + .add_input(utxo_with_change.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::Transfer( + OutputValue::Coin((change_amount / 2).unwrap()), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin((change_amount / 2).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + let change_split_tx_id = change_split_tx.transaction().get_id(); + tf.make_block_builder() + .add_transaction(change_split_tx) + .build_and_process(&mut rng) + .unwrap(); + + let utxo_with_change1 = UtxoOutPoint::new(change_split_tx_id.into(), 0); + let utxo_with_change2 = UtxoOutPoint::new(change_split_tx_id.into(), 1); + + let change_amount1 = tf.coin_amount_from_utxo(&utxo_with_change1); + + // Create the orders + + let order1_token_give_amount = (amount_to_mint / 2).unwrap(); + let order1_coin_ask_amount = Amount::from_atoms(rng.gen_range(100..1000)); + + let token_remaining_amount = (amount_to_mint - order1_token_give_amount).unwrap(); + let order1_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(order1_coin_ask_amount), + OutputValue::TokenV1(token_id, order1_token_give_amount), + ); + let order1_creation_tx = TransactionBuilder::new() + .add_input( + utxo_with_tokens_change.into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::CreateOrder(Box::new(order1_data))) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, token_remaining_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let order1_creation_tx_id = order1_creation_tx.transaction().get_id(); + let order1_id = make_order_id(order1_creation_tx.inputs()).unwrap(); + tf.make_block_builder() + .add_transaction(order1_creation_tx) + .build_and_process(&mut rng) + .unwrap(); + + let utxo_with_tokens_change = UtxoOutPoint::new(order1_creation_tx_id.into(), 1); + + let order2_token_ask_amount = token_remaining_amount; + let order2_coin_give_amount = Amount::from_atoms(rng.gen_range(100..1000)); + + let order2_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id, order2_token_ask_amount), + OutputValue::Coin(order2_coin_give_amount), + ); + let order2_creation_tx = TransactionBuilder::new() + .add_input(utxo_with_change1.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order2_data))) + .add_output(TxOutput::Transfer( + OutputValue::Coin((change_amount1 - order2_coin_give_amount).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + let order2_creation_tx_id = order2_creation_tx.transaction().get_id(); + + let order2_id = make_order_id(order2_creation_tx.inputs()).unwrap(); + tf.make_block_builder() + .add_transaction(order2_creation_tx) + .build_and_process(&mut rng) + .unwrap(); + + let utxo_with_change1 = UtxoOutPoint::new(order2_creation_tx_id.into(), 1); + let change_amount1 = tf.coin_amount_from_utxo(&utxo_with_change1); + + let token_freeze_fee = + tf.chainstate.get_chain_config().token_freeze_fee(BlockHeight::zero()); + + // Freeze the token + let token_freeze_tx = TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(1), + AccountCommand::FreezeToken(token_id, IsTokenUnfreezable::Yes), + ), + InputWitness::NoSignature(None), + ) + .add_input(utxo_with_change1.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::Transfer( + OutputValue::Coin((change_amount1 - token_freeze_fee).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + let token_freeze_tx_id = token_freeze_tx.transaction().get_id(); + + tf.make_block_builder() + .add_transaction(token_freeze_tx) + .build_and_process(&mut rng) + .unwrap(); + + let utxo_with_change1 = UtxoOutPoint::new(token_freeze_tx_id.into(), 0); + + check_fungible_token( + &tf, + &mut rng, + &token_id, + &ExpectedFungibleTokenData { + issuance: issuance.clone(), + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::Yes(IsTokenUnfreezable::Yes), + }, + true, + ); + + // Try filling/concluding the orders, which should fail. + // Also optionally try freezing them, which should succeed. + // Note that the order fill/conclude txs don't have outputs: a) for simplicity (e.g. so that + // the conclude tx stays the same no matter whether the fill has succeeded or not), + // b) (in the case of a token output) to ensure that the tx is rejected not because of + // the output, but because of the order command itself. + + // Try filling order1 + let order1_fill_tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order1_id, + order1_coin_ask_amount, + )), + InputWitness::NoSignature(None), + ) + .add_input(utxo_with_change1.into(), InputWitness::NoSignature(None)) + .build(); + let result = tf + .make_block_builder() + .add_transaction(order1_fill_tx.clone()) + .build_and_process(&mut rng); + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) + )) + ); + + // Try concluding order1 + let order1_conclude_tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order1_id)), + InputWitness::NoSignature(None), + ) + .build(); + let result = tf + .make_block_builder() + .add_transaction(order1_conclude_tx.clone()) + .build_and_process(&mut rng); + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) + )) + ); + + // Try filling order2 + let order2_fill_tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order2_id, + order2_token_ask_amount, + )), + InputWitness::NoSignature(None), + ) + .add_input( + utxo_with_tokens_change.into(), + InputWitness::NoSignature(None), + ) + .build(); + let result = tf + .make_block_builder() + .add_transaction(order2_fill_tx.clone()) + .build_and_process(&mut rng); + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) + )) + ); + + // Try concluding order2 + let order2_conclude_tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order2_id)), + InputWitness::NoSignature(None), + ) + .build(); + let result = tf + .make_block_builder() + .add_transaction(order2_conclude_tx.clone()) + .build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) + )) + ); + + // The orders can still be frozen. + if freeze_orders { + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder( + order1_id, + )), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder( + order2_id, + )), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + } + + // Unfreeze the token + let unfreeze_tx = TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(2), + AccountCommand::UnfreezeToken(token_id), + ), + InputWitness::NoSignature(None), + ) + .add_input(utxo_with_change2.into(), InputWitness::NoSignature(None)) + .build(); + tf.make_block_builder() + .add_transaction(unfreeze_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Check result + check_fungible_token( + &tf, + &mut rng, + &token_id, + &ExpectedFungibleTokenData { + issuance: issuance.clone(), + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::Yes), + }, + true, + ); + + // Now it should be possible to work with the orders. + + if !freeze_orders { + // Fill order1 + tf.make_block_builder() + .add_transaction(order1_fill_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Fill order2 + tf.make_block_builder() + .add_transaction(order2_fill_tx) + .build_and_process(&mut rng) + .unwrap(); + } + + // Conclude order1 + tf.make_block_builder() + .add_transaction(order1_conclude_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Conclude order2 + tf.make_block_builder() + .add_transaction(order2_conclude_tx) + .build_and_process(&mut rng) + .unwrap(); + }); +} + // Check that if token is frozen/unfrozen by an input command it takes effect only // after tx is processed. Meaning tx outputs are not aware of state change by inputs in the same tx. #[rstest] diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index 12018eb94f..074e702deb 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -1231,6 +1231,8 @@ where let ask_value = convert_value(ask_value).await?; let give_value = convert_value(give_value).await?; + // TODO: check whether the tokens, if any, are usable (i.e. non-frozen). + self.create_and_send_tx_with_id( async move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, @@ -1261,6 +1263,8 @@ where let tx_additional_info = self.additional_info_for_order_update_tx(order_id, &order_info).await?; + // TODO: check whether the tokens, if any, are usable (i.e. non-frozen). + self.create_and_send_tx( async move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, @@ -1306,6 +1310,8 @@ where .to_amount(ask_currency_decimals) .ok_or(ControllerError::InvalidCoinAmount)?; + // TODO: check whether the tokens, if any, are usable (i.e. non-frozen). + self.create_and_send_tx( async move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, From 6f2a65ecbfb04851301cdcd33b2c4509e470bc02 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Tue, 24 Feb 2026 18:31:38 +0200 Subject: [PATCH 6/6] Add mistakenly omitted checks in the functional test wallet_order_list_all_active.py --- test/functional/wallet_order_list_all_active.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/functional/wallet_order_list_all_active.py b/test/functional/wallet_order_list_all_active.py index 79dbe94c63..f2c1a17dc6 100644 --- a/test/functional/wallet_order_list_all_active.py +++ b/test/functional/wallet_order_list_all_active.py @@ -289,6 +289,7 @@ async def check_all(): # 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) + await cli_check_orders_with_filters(filter1, filter2) ######################################################################################## # Generate a block, the orders should exist now @@ -321,7 +322,7 @@ async def random_fill(wallet: WalletRpcController | WalletCliController, order_i 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. + # Before the txs have been mined, the expected values stay the same. await check_all() # Generate a block @@ -349,12 +350,14 @@ async def random_fill(wallet: WalletRpcController | WalletCliController, order_i async def freeze_order(wallet: WalletRpcController | WalletCliController, order_id: str): self.log.info(f"Freezing order {order_id}") - await wallet.freeze_order(order_id) + result = await wallet.freeze_order(order_id) + assert_in("The transaction was submitted successfully", result) 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) + result = await wallet.conclude_order(order_id) + assert_in("The transaction was submitted successfully", result) inactive_order_ids.add(order_id) # Freeze some orders in wallet1