diff --git a/Cargo.lock b/Cargo.lock index 51251b6c1c..4ca803db1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1068,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata", "serde", ] @@ -4489,11 +4489,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -5040,16 +5040,6 @@ dependencies = [ "utils-networking", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -5616,12 +5606,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "owned_ttf_parser" version = "0.25.0" @@ -6223,7 +6207,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift 0.3.0", - "regex-syntax 0.8.5", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -6558,7 +6542,7 @@ dependencies = [ "crossterm", "fd-lock", "itertools 0.12.1", - "nu-ansi-term 0.50.1", + "nu-ansi-term", "serde", "strip-ansi-escapes", "strum", @@ -6596,17 +6580,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -6617,15 +6592,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -8157,7 +8126,6 @@ dependencies = [ "subsystem", "thiserror 1.0.69", "tokio", - "wallet-types", ] [[package]] @@ -8672,14 +8640,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", - "nu-ansi-term 0.46.0", + "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs b/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs index 1a44be46eb..19350a7611 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs @@ -16,6 +16,7 @@ use std::convert::Infallible; use common::chain::{ + partially_signed_transaction::PartiallySignedTransaction, signature::{ inputsig::InputWitness, sighash::input_commitments::SighashInputCommitment, DestinationSigError, Transactable, @@ -104,6 +105,7 @@ impl InputInfoProvider for InputVerifyContextSignature<'_, T> { // Prevent BlockRewardTransactable from being used here pub trait SignatureOnlyVerifiable {} impl SignatureOnlyVerifiable for SignedTransaction {} +impl SignatureOnlyVerifiable for PartiallySignedTransaction {} // Note: the passed `outpoint_destination` value is only used in a limited number of scenarios // (see `impl SignatureInfoProvider for InputVerifyContextSignature` above). In all other cases diff --git a/common/src/chain/mod.rs b/common/src/chain/mod.rs index 32b5b1d5ac..021dde7a6f 100644 --- a/common/src/chain/mod.rs +++ b/common/src/chain/mod.rs @@ -18,6 +18,7 @@ pub mod chaintrust; pub mod config; pub mod gen_block; pub mod genesis; +pub mod partially_signed_transaction; pub mod tokens; pub mod transaction; diff --git a/common/src/chain/partially_signed_transaction/additional_info.rs b/common/src/chain/partially_signed_transaction/additional_info.rs new file mode 100644 index 0000000000..172cdcfd2d --- /dev/null +++ b/common/src/chain/partially_signed_transaction/additional_info.rs @@ -0,0 +1,149 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeMap; + +use serialization::{Decode, Encode}; + +use crate::{ + chain::{ + output_value::OutputValue, + signature::sighash::{self}, + OrderId, PoolId, + }, + primitives::Amount, +}; + +// Note: PoolAdditionalInfo and OrderAdditionalInfo below are identical to the corresponding +// structs in `input_commitments/info_providers.rs` (except that those don't derive Encode/Decode) +// and basically serve the same purpose. +// We keep them separate because: +// 1) Technically we may want to have even more info inside partially signed transaction in +// the future, which may not be needed by the input commitments. +// 2) We want to be able to refactor input commitments structs without worrying about breaking +// PartiallySignedTransaction's backward compatibility. + +/// Pool additional info, which must be present for each ProduceBlockFromStake UTXO consumed by +/// the transaction. Transaction's signature commits to this info since SighashInputCommitments::V1. +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode, serde::Serialize)] +pub struct PoolAdditionalInfo { + pub staker_balance: Amount, +} + +/// Order additional info, which must be present for each FillOrder and ConcludeOrder input consumed +/// by the transaction. Transaction's signature commits to this info since SighashInputCommitments::V1. +/// +/// Note though that only ConcludeOrder commitments include both initial and current balances, +/// while FillOrder commitments only include the initial ones. So this info representation +/// is not ideal, as it forces the caller to provide additional info that will not actually +/// be used. +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode, serde::Serialize)] +pub struct OrderAdditionalInfo { + pub initially_asked: OutputValue, + pub initially_given: OutputValue, + pub ask_balance: Amount, + pub give_balance: Amount, +} + +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode, serde::Serialize)] +pub struct TxAdditionalInfo { + pool_info: BTreeMap, + order_info: BTreeMap, +} + +impl TxAdditionalInfo { + pub fn new() -> Self { + Self { + pool_info: BTreeMap::new(), + order_info: BTreeMap::new(), + } + } + + pub fn with_pool_info(mut self, pool_id: PoolId, info: PoolAdditionalInfo) -> Self { + self.pool_info.insert(pool_id, info); + self + } + + pub fn with_order_info(mut self, order_id: OrderId, info: OrderAdditionalInfo) -> Self { + self.order_info.insert(order_id, info); + self + } + + pub fn add_pool_info(&mut self, pool_id: PoolId, info: PoolAdditionalInfo) { + self.pool_info.insert(pool_id, info); + } + + pub fn add_order_info(&mut self, order_id: OrderId, info: OrderAdditionalInfo) { + self.order_info.insert(order_id, info); + } + + pub fn join(mut self, other: Self) -> Self { + self.pool_info.extend(other.pool_info); + self.order_info.extend(other.order_info); + Self { + pool_info: self.pool_info, + order_info: self.order_info, + } + } + + pub fn get_pool_info(&self, pool_id: &PoolId) -> Option<&PoolAdditionalInfo> { + self.pool_info.get(pool_id) + } + + pub fn get_order_info(&self, order_id: &OrderId) -> Option<&OrderAdditionalInfo> { + self.order_info.get(order_id) + } + + pub fn pool_info_iter(&self) -> impl Iterator { + self.pool_info.iter() + } + + pub fn order_info_iter(&self) -> impl Iterator { + self.order_info.iter() + } +} + +impl sighash::input_commitments::PoolInfoProvider for TxAdditionalInfo { + type Error = std::convert::Infallible; + + fn get_pool_info( + &self, + pool_id: &PoolId, + ) -> Result, Self::Error> { + Ok( + self.pool_info.get(pool_id).map(|info| sighash::input_commitments::PoolInfo { + staker_balance: info.staker_balance, + }), + ) + } +} + +impl sighash::input_commitments::OrderInfoProvider for TxAdditionalInfo { + type Error = std::convert::Infallible; + + fn get_order_info( + &self, + order_id: &OrderId, + ) -> Result, Self::Error> { + Ok( + self.order_info.get(order_id).map(|info| sighash::input_commitments::OrderInfo { + initially_asked: info.initially_asked.clone(), + initially_given: info.initially_given.clone(), + ask_balance: info.ask_balance, + give_balance: info.give_balance, + }), + ) + } +} diff --git a/common/src/chain/partially_signed_transaction/mod.rs b/common/src/chain/partially_signed_transaction/mod.rs new file mode 100644 index 0000000000..89ff6906d6 --- /dev/null +++ b/common/src/chain/partially_signed_transaction/mod.rs @@ -0,0 +1,465 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use thiserror::Error; + +use serialization::{Decode, Encode}; +use utils::ensure; + +use crate::{ + chain::{ + htlc::HtlcSecret, + signature::{ + inputsig::InputWitness, + sighash::{ + self, + input_commitments::{ + make_sighash_input_commitments_for_transaction_inputs, + make_sighash_input_commitments_for_transaction_inputs_at_height, + SighashInputCommitment, + }, + }, + Signable, Transactable, + }, + tokens::TokenId, + AccountCommand, ChainConfig, Destination, OrderAccountCommand, OrderId, PoolId, + SighashInputCommitmentVersion, SignedTransaction, Transaction, TransactionCreationError, + TxInput, TxOutput, + }, + primitives::BlockHeight, +}; + +mod additional_info; + +pub use additional_info::{OrderAdditionalInfo, PoolAdditionalInfo, TxAdditionalInfo}; + +/// This determines what should be checked when a PartiallySignedTransaction is constructed. +pub enum PartiallySignedTransactionConsistencyCheck { + /// Only do the cheap basic checks. + Basic, + + /// Also check consistency of additional info. + WithAdditionalInfo, +} + +/// A partially signed transaction, which contains the transaction itself, some of the signatures +/// and certain additional info, which is required to produce signatures. +/// +/// Note: currently PartiallySignedTransaction's consistency checks require that the additional info +/// is present even if the inputs that need it are already signed. +/// +/// Regarding the ability to refactor it, making non-backward-compatible changes. +/// Currently PartiallySignedTransaction is used: +/// 1) By the wallet. In this case the encoded transaction is supposed to be short-lived, +/// so breaking compatibility should be tolerable. +/// 2) By the bridge, whose e2m master agent puts a PartiallySignedTransaction in the db +/// to be read by the cosigner; once the cosigner handles the transaction, it is replaced +/// by the normal SignedTransaction in the db. I.e. breaking the compatibility is possible +/// provided that there are no partially signed e2m withdrawal transactions in the bridge db +/// during the update of wallet-rpc-daemon that is used by the bridge. +/// 3) By the Mojito and RioSwap teams, where the former construct a PartiallySignedTransaction +/// via the wasm call `encode_partially_signed_transaction` and the latter pass it to +/// wallet-rpc-daemon. The transaction is treated as a black box, so a breaking change is +/// technically possible, though it'll require synchronization between multiple teams. +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode, serde::Serialize)] +pub struct PartiallySignedTransaction { + tx: Transaction, + witnesses: Vec>, + + input_utxos: Vec>, + destinations: Vec>, + + htlc_secrets: Vec>, + additional_info: TxAdditionalInfo, +} + +impl PartiallySignedTransaction { + // Note: passing `None` for `htlc_secrets` is equivalent to passing a `Vec` of `None`s. + pub fn new( + tx: Transaction, + witnesses: Vec>, + input_utxos: Vec>, + destinations: Vec>, + htlc_secrets: Option>>, + additional_info: TxAdditionalInfo, + cosnsitency_check: PartiallySignedTransactionConsistencyCheck, + ) -> Result { + let this = Self::new_unchecked( + tx, + witnesses, + input_utxos, + destinations, + htlc_secrets, + additional_info, + ); + + this.ensure_consistency(cosnsitency_check)?; + Ok(this) + } + + fn new_unchecked( + tx: Transaction, + witnesses: Vec>, + input_utxos: Vec>, + destinations: Vec>, + htlc_secrets: Option>>, + additional_info: TxAdditionalInfo, + ) -> Self { + let htlc_secrets = htlc_secrets.unwrap_or_else(|| vec![None; tx.inputs().len()]); + + Self { + tx, + witnesses, + input_utxos, + destinations, + htlc_secrets, + additional_info, + } + } + + pub fn ensure_consistency( + &self, + cosnsitency_check: PartiallySignedTransactionConsistencyCheck, + ) -> Result<(), PartiallySignedTransactionError> { + ensure!( + self.tx.inputs().len() == self.witnesses.len(), + PartiallySignedTransactionError::InvalidWitnessCount + ); + + ensure!( + self.tx.inputs().len() == self.input_utxos.len(), + PartiallySignedTransactionError::InvalidInputUtxosCount, + ); + + ensure!( + self.tx.inputs().len() == self.destinations.len(), + PartiallySignedTransactionError::InvalidDestinationsCount + ); + + ensure!( + self.tx.inputs().len() == self.htlc_secrets.len(), + PartiallySignedTransactionError::InvalidHtlcSecretsCount + ); + + match cosnsitency_check { + PartiallySignedTransactionConsistencyCheck::Basic => {} + PartiallySignedTransactionConsistencyCheck::WithAdditionalInfo => { + self.ensure_additional_info_completeness()?; + } + } + + Ok(()) + } + + fn ensure_additional_info_completeness(&self) -> Result<(), PartiallySignedTransactionError> { + // TODO: try to re-use the input commitments machinery here instead of doing custom checks. + + let ensure_order_info_present = + |order_id: &OrderId| -> Result<_, PartiallySignedTransactionError> { + ensure!( + self.additional_info.get_order_info(order_id).is_some(), + PartiallySignedTransactionError::OrderAdditionalInfoMissing(*order_id) + ); + Ok(()) + }; + + let ensure_no_utxo = |input_index, + input_utxo_opt: &Option| + -> Result<_, PartiallySignedTransactionError> { + ensure!( + input_utxo_opt.is_none(), + PartiallySignedTransactionError::UtxoPresentForNonUtxoInput { input_index } + ); + Ok(()) + }; + + let check_utxo = |output: &TxOutput| -> Result<(), PartiallySignedTransactionError> { + match output { + TxOutput::ProduceBlockFromStake(_, pool_id) => { + ensure!( + self.additional_info.get_pool_info(pool_id).is_some(), + PartiallySignedTransactionError::PoolAdditionalInfoMissing(*pool_id) + ); + } + TxOutput::Transfer(_, _) + | TxOutput::LockThenTransfer(_, _, _) + | TxOutput::Burn(_) + | TxOutput::Htlc(_, _) + | TxOutput::CreateOrder(_) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::CreateStakePool(_, _) + | TxOutput::IssueNft(_, _, _) + | TxOutput::DataDeposit(_) => {} + } + Ok(()) + }; + + for (input_index, (input, input_utxo)) in + self.tx.inputs().iter().zip(self.input_utxos.iter()).enumerate() + { + match input { + TxInput::Utxo(_) => { + let input_utxo = input_utxo.as_ref().ok_or( + PartiallySignedTransactionError::MissingUtxoForUtxoInput { input_index }, + )?; + check_utxo(input_utxo)?; + } + TxInput::Account(_) => ensure_no_utxo(input_index, input_utxo)?, + TxInput::AccountCommand(_, command) => { + ensure_no_utxo(input_index, input_utxo)?; + + match command { + AccountCommand::ConcludeOrder(id) => ensure_order_info_present(id)?, + AccountCommand::FillOrder(id, _, _) => ensure_order_info_present(id)?, + + AccountCommand::MintTokens(_, _) + | AccountCommand::UnmintTokens(_) + | AccountCommand::LockTokenSupply(_) + | AccountCommand::FreezeToken(_, _) + | AccountCommand::UnfreezeToken(_) + | AccountCommand::ChangeTokenAuthority(_, _) + | AccountCommand::ChangeTokenMetadataUri(_, _) => {} + } + } + TxInput::OrderAccountCommand(command) => { + match command { + OrderAccountCommand::FillOrder(id, _) + | OrderAccountCommand::ConcludeOrder(id) => ensure_order_info_present(id)?, + + OrderAccountCommand::FreezeOrder(_) => {} + }; + } + } + } + + Ok(()) + } + + pub fn with_witnesses( + mut self, + witnesses: Vec>, + ) -> Result { + ensure!( + witnesses.len() == self.tx.inputs().len(), + PartiallySignedTransactionError::InvalidWitnessCount + ); + self.witnesses = witnesses; + Ok(self) + } + + pub fn tx(&self) -> &Transaction { + &self.tx + } + + pub fn take_tx(self) -> Transaction { + self.tx + } + + pub fn input_utxos(&self) -> &[Option] { + self.input_utxos.as_ref() + } + + /// Input destinations + pub fn destinations(&self) -> &[Option] { + self.destinations.as_ref() + } + + pub fn witnesses(&self) -> &[Option] { + self.witnesses.as_ref() + } + + pub fn htlc_secrets(&self) -> &[Option] { + self.htlc_secrets.as_ref() + } + + pub fn count_inputs(&self) -> usize { + self.tx.inputs().len() + } + + // Note: this function only checks that all inputs that require a signature have one. + // I.e. it doesn't check whether a multisig input has all required signatures. + // TODO: rename it at least or make private. + pub fn all_signatures_available(&self) -> bool { + self.witnesses + .iter() + .enumerate() + .zip(&self.destinations) + .all(|((_, witness), dest)| { + let dest_needs_signature = match dest { + Some(dest) => match dest { + Destination::AnyoneCanSpend => false, + Destination::PublicKeyHash(_) + | Destination::PublicKey(_) + | Destination::ScriptHash(_) + | Destination::ClassicMultisig(_) => true, + }, + None => false, + }; + + match (witness, dest_needs_signature) { + (Some(InputWitness::NoSignature(_)), false) => true, + (Some(InputWitness::NoSignature(_)), true) => false, + // TODO: consider returning a Result and produce an error in this case. + (Some(InputWitness::Standard(_)), false) => false, + (Some(InputWitness::Standard(_)), true) => true, + (None, _) => false, + } + }) + } + + pub fn into_signed_tx(self) -> Result { + if self.all_signatures_available() { + let witnesses = self.witnesses.into_iter().map(|w| w.expect("cannot fail")).collect(); + Ok(SignedTransaction::new(self.tx, witnesses) + .map_err(PartiallySignedTransactionError::TxCreationError)?) + } else { + Err(PartiallySignedTransactionError::FailedToConvertPartiallySignedTx(Box::new(self))) + } + } + + pub fn additional_info(&self) -> &TxAdditionalInfo { + &self.additional_info + } + + pub fn make_sighash_input_commitments( + &self, + version: SighashInputCommitmentVersion, + ) -> Result>, PartiallySignedTransactionError> { + Ok(make_sighash_input_commitments( + self.tx.inputs(), + &self.input_utxos, + &self.additional_info, + version, + )?) + } + + pub fn make_sighash_input_commitments_at_height( + &self, + chain_config: &ChainConfig, + block_height: BlockHeight, + ) -> Result>, PartiallySignedTransactionError> { + Ok(make_sighash_input_commitments_at_height( + self.tx.inputs(), + &self.input_utxos, + &self.additional_info, + chain_config, + block_height, + )?) + } +} + +pub fn make_sighash_input_commitments_at_height<'a>( + tx_inputs: &[TxInput], + input_utxos: &'a [Option], + additional_info: &TxAdditionalInfo, + chain_config: &ChainConfig, + block_height: BlockHeight, +) -> Result>, SighashInputCommitmentCreationError> { + make_sighash_input_commitments_for_transaction_inputs_at_height( + tx_inputs, + &sighash::input_commitments::TrivialUtxoProvider(input_utxos), + additional_info, + additional_info, + chain_config, + block_height, + ) +} + +pub fn make_sighash_input_commitments<'a>( + tx_inputs: &[TxInput], + input_utxos: &'a [Option], + additional_info: &TxAdditionalInfo, + version: SighashInputCommitmentVersion, +) -> Result>, SighashInputCommitmentCreationError> { + make_sighash_input_commitments_for_transaction_inputs( + tx_inputs, + &sighash::input_commitments::TrivialUtxoProvider(input_utxos), + additional_info, + additional_info, + version, + ) +} + +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum PartiallySignedTransactionError { + #[error("Failed to convert partially signed tx to signed")] + FailedToConvertPartiallySignedTx(Box), + + #[error("Failed to create transaction: {0}")] + TxCreationError(TransactionCreationError), + + #[error("The number of witnesses does not match the number of inputs")] + InvalidWitnessCount, + + #[error("The number of input utxos does not match the number of inputs")] + InvalidInputUtxosCount, + + #[error("The number of destinations does not match the number of inputs")] + InvalidDestinationsCount, + + #[error("The number of htlc secrets does not match the number of inputs")] + InvalidHtlcSecretsCount, + + #[error("Missing UTXO for input #{input_index}")] + MissingUtxoForUtxoInput { input_index: usize }, + + #[error("A UTXO for non-UTXO input #{input_index} is specified")] + UtxoPresentForNonUtxoInput { input_index: usize }, + + #[error("Additional info is missing for order {0}")] + OrderAdditionalInfoMissing(OrderId), + + #[error("Additional info is missing for token {0}")] + TokenAdditionalInfoMissing(TokenId), + + #[error("Additional info is missing for pool {0}")] + PoolAdditionalInfoMissing(PoolId), + + #[error("Error creating sighash input commitment: {0}")] + SighashInputCommitmentCreationError(#[from] SighashInputCommitmentCreationError), +} + +pub type SighashInputCommitmentCreationError = + sighash::input_commitments::SighashInputCommitmentCreationError< + std::convert::Infallible, + std::convert::Infallible, + std::convert::Infallible, + >; + +impl Signable for PartiallySignedTransaction { + fn inputs(&self) -> Option<&[TxInput]> { + Some(self.tx.inputs()) + } + + fn outputs(&self) -> Option<&[TxOutput]> { + Some(self.tx.outputs()) + } + + fn version_byte(&self) -> Option { + Some(self.tx.version_byte()) + } + + fn flags(&self) -> Option { + Some(self.tx.flags()) + } +} + +impl Transactable for PartiallySignedTransaction { + fn signatures(&self) -> Vec> { + self.witnesses.clone() + } +} diff --git a/test-rpc-functions/Cargo.toml b/test-rpc-functions/Cargo.toml index 731751a3b5..608b31801c 100644 --- a/test-rpc-functions/Cargo.toml +++ b/test-rpc-functions/Cargo.toml @@ -16,7 +16,6 @@ randomness = { path = "../randomness/" } rpc = { path = "../rpc/" } serialization = { path = "../serialization" } subsystem = { path = "../subsystem/" } -wallet-types = { path = "../wallet/types" } async-trait.workspace = true futures.workspace = true diff --git a/test-rpc-functions/src/rpc.rs b/test-rpc-functions/src/rpc.rs index 6594cee63a..93a91739d3 100644 --- a/test-rpc-functions/src/rpc.rs +++ b/test-rpc-functions/src/rpc.rs @@ -28,6 +28,7 @@ use common::{ EpochIndex, }, output_value::OutputValue, + partially_signed_transaction::PartiallySignedTransaction, signature::inputsig::{ arbitrary_message, authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, @@ -45,7 +46,6 @@ use serialization::{ hex_encoded::HexEncoded, Encode as _, }; -use wallet_types::partially_signed_transaction::PartiallySignedTransaction; use crate::{RpcTestFunctionsError, RpcTestFunctionsHandle}; diff --git a/test/functional/test_framework/__init__.py b/test/functional/test_framework/__init__.py index 54888b34a3..51a59e8b3d 100644 --- a/test/functional/test_framework/__init__.py +++ b/test/functional/test_framework/__init__.py @@ -226,7 +226,6 @@ def init_mintlayer_types(): "TxAdditionalInfo": { "type": "struct", "type_mapping": [ - ["token_info", "BTreeMap"], ["pool_info", "BTreeMap"], ["order_info", "BTreeMap"], ], diff --git a/test/functional/wallet_htlc_refund.py b/test/functional/wallet_htlc_refund.py index cf08d95e1e..05478dc72e 100644 --- a/test/functional/wallet_htlc_refund.py +++ b/test/functional/wallet_htlc_refund.py @@ -157,16 +157,6 @@ async def async_test(self): assert_in("Coins amount: 0", balance) assert_in(f"Token: {token_id} amount: {amount_to_mint}", balance) - token_additional_info_for_ptx = [ - ( - token_id_dec_array, - { - 'num_decimals': token_number_of_decimals, - 'ticker': token_ticker.encode('utf-8') - } - ) - ] - ######################################################################################## # Setup Alice's htlc alice_secret = bytes([random.randint(0, 255) for _ in range(32)]) @@ -197,7 +187,7 @@ async def async_test(self): 'input_utxos': alice_htlc_outputs, 'destinations': [refund_dest_obj, alice_htlc_change_dest], 'htlc_secrets': [None, None], - 'additional_info': {'token_info': token_additional_info_for_ptx, 'pool_info': [], 'order_info': []} + 'additional_info': {'pool_info': [], 'order_info': []} } alice_refund_tx_hex = scalecodec.base.RuntimeConfiguration().create_scale_object('PartiallySignedTransaction').encode(alice_refund_ptx).to_hex()[2:] @@ -226,7 +216,7 @@ async def async_test(self): 'input_utxos': bob_htlc_outputs, 'destinations': [refund_dest_obj, bob_htlc_change_dest], 'htlc_secrets': [None, None], - 'additional_info': {'token_info': token_additional_info_for_ptx, 'pool_info': [], 'order_info': []} + 'additional_info': {'pool_info': [], 'order_info': []} } bob_refund_tx_hex = scalecodec.base.RuntimeConfiguration().create_scale_object('PartiallySignedTransaction').encode(bob_refund_ptx).to_hex()[2:] diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 218b572216..36d6ff743c 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, TxAdditionalInfo}; +use wallet_types::partially_signed_transaction::{PartiallySignedTransaction, PtxAdditionalInfo}; use wallet_types::with_locked::WithLocked; use crate::account::utxo_selector::{select_coins, OutputGroup}; @@ -670,7 +670,7 @@ impl Account { change_addresses: BTreeMap>, median_time: BlockTimestamp, fee_rate: CurrentFeeRate, - additional_info: TxAdditionalInfo, + ptx_additional_info: PtxAdditionalInfo, ) -> WalletResult<(PartiallySignedTransaction, BTreeMap)> { let mut request = self.select_inputs_for_send_request( request, @@ -684,7 +684,7 @@ impl Account { )?; let fees = request.get_fees(); - let ptx = request.into_partially_signed_tx(additional_info)?; + let ptx = request.into_partially_signed_tx(ptx_additional_info)?; Ok((ptx, fees)) } diff --git a/wallet/src/send_request/mod.rs b/wallet/src/send_request/mod.rs index 483210048a..657a8fc578 100644 --- a/wallet/src/send_request/mod.rs +++ b/wallet/src/send_request/mod.rs @@ -30,7 +30,9 @@ use common::primitives::{Amount, BlockHeight}; use crypto::vrf::VRFPublicKey; use utils::ensure; use wallet_types::currency::Currency; -use wallet_types::partially_signed_transaction::{PartiallySignedTransaction, TxAdditionalInfo}; +use wallet_types::partially_signed_transaction::{ + PartiallySignedTransaction, PartiallySignedTransactionWalletExt as _, PtxAdditionalInfo, +}; use crate::account::PoolData; use crate::destination_getters::{get_tx_output_destination, HtlcSpendingCondition}; @@ -313,14 +315,14 @@ impl SendRequest { pub fn into_partially_signed_tx( self, - additional_info: TxAdditionalInfo, + additional_info: PtxAdditionalInfo, ) -> WalletResult { let num_inputs = self.inputs.len(); let destinations = self.destinations.into_iter().map(Some).collect(); let utxos = self.utxos; let tx = Transaction::new(self.flags, self.inputs, self.outputs)?; - let ptx = PartiallySignedTransaction::new( + let ptx = PartiallySignedTransaction::new_for_wallet( tx, vec![None; num_inputs], utxos, diff --git a/wallet/src/signer/mod.rs b/wallet/src/signer/mod.rs index 814dbc6d1c..52d7696a6c 100644 --- a/wallet/src/signer/mod.rs +++ b/wallet/src/signer/mod.rs @@ -39,7 +39,9 @@ use wallet_storage::{ }; use wallet_types::{ hw_data::HardwareWalletFullInfo, - partially_signed_transaction::{PartiallySignedTransaction, PartiallySignedTransactionError}, + partially_signed_transaction::{ + PartiallySignedTransaction, PartiallySignedTransactionError, TokensAdditionalInfo, + }, signature_status::SignatureStatus, AccountId, }; @@ -111,6 +113,7 @@ pub trait Signer { fn sign_tx( &mut self, tx: PartiallySignedTransaction, + tokens_additional_info: &TokensAdditionalInfo, key_chain: &impl AccountKeyChains, db_tx: &impl WalletStorageReadUnlocked, block_height: BlockHeight, diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index 37ddadfe61..47eb56deaf 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -55,8 +55,11 @@ use wallet_storage::{ WalletStorageWriteUnlocked, }; use wallet_types::{ - hw_data::HardwareWalletFullInfo, partially_signed_transaction::PartiallySignedTransaction, - seed_phrase::StoreSeedPhrase, signature_status::SignatureStatus, AccountId, + hw_data::HardwareWalletFullInfo, + partially_signed_transaction::{PartiallySignedTransaction, TokensAdditionalInfo}, + seed_phrase::StoreSeedPhrase, + signature_status::SignatureStatus, + AccountId, }; use crate::{ @@ -289,6 +292,7 @@ impl Signer for SoftwareSigner { fn sign_tx( &mut self, ptx: PartiallySignedTransaction, + _tokens_additional_info: &TokensAdditionalInfo, key_chain: &impl AccountKeyChains, db_tx: &impl WalletStorageReadUnlocked, block_height: BlockHeight, diff --git a/wallet/src/signer/tests/generic_fixed_signature_tests.rs b/wallet/src/signer/tests/generic_fixed_signature_tests.rs index 2353fa6f38..7ffa775b38 100644 --- a/wallet/src/signer/tests/generic_fixed_signature_tests.rs +++ b/wallet/src/signer/tests/generic_fixed_signature_tests.rs @@ -65,7 +65,8 @@ use wallet_storage::{DefaultBackend, Store, TransactionRwUnlocked, Transactional use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, partially_signed_transaction::{ - OrderAdditionalInfo, PoolAdditionalInfo, TokenAdditionalInfo, TxAdditionalInfo, + OrderAdditionalInfo, PoolAdditionalInfo, PtxAdditionalInfo, TokenAdditionalInfo, + TokensAdditionalInfo, }, seed_phrase::StoreSeedPhrase, BlockInfo, KeyPurpose, @@ -345,34 +346,40 @@ where .with_inputs_and_destinations(acc_inputs.into_iter().zip(acc_dests.clone())) .with_outputs(outputs); let destinations = req.destinations().to_vec(); - let additional_info = TxAdditionalInfo::new() - .with_token_info( - token_id, - // Note: this info doesn't influence the signature and can be random. - TokenAdditionalInfo { - num_decimals: rng.gen_range(1..10), - ticker: random_ascii_alphanumeric_string(rng, 5..10).into_bytes(), - }, - ) - .with_order_info( - order_id, - OrderAdditionalInfo { - ask_balance: Amount::from_atoms(10), - give_balance: Amount::from_atoms(100), - initially_asked: OutputValue::Coin(Amount::from_atoms(20)), - // Note: initially_given's amount isn't used by the signers in orders v0, only its - // currency matters. - initially_given: OutputValue::TokenV1( - token_id, - Amount::from_atoms(rng.gen_range(100..200)), - ), - }, - ); - let orig_ptx = req.into_partially_signed_tx(additional_info).unwrap(); + let ptx_additional_info = PtxAdditionalInfo::new().with_order_info( + order_id, + OrderAdditionalInfo { + ask_balance: Amount::from_atoms(10), + give_balance: Amount::from_atoms(100), + initially_asked: OutputValue::Coin(Amount::from_atoms(20)), + // Note: initially_given's amount isn't used by the signers in orders v0, only its + // currency matters. + initially_given: OutputValue::TokenV1( + token_id, + Amount::from_atoms(rng.gen_range(100..200)), + ), + }, + ); + let tokens_additional_info = TokensAdditionalInfo::new().with_info( + token_id, + // Note: this info doesn't influence the signature and can be random. + TokenAdditionalInfo { + num_decimals: rng.gen_range(1..10), + ticker: random_ascii_alphanumeric_string(rng, 5..10).into_bytes(), + }, + ); + let orig_ptx = req.into_partially_signed_tx(ptx_additional_info).unwrap(); let mut signer = make_signer(chain_config.clone(), account.account_index()); - let (ptx, _, _) = - signer.sign_tx(orig_ptx, account.key_chain(), &db_tx, tx_block_height).unwrap(); + let (ptx, _, _) = signer + .sign_tx( + orig_ptx, + &tokens_additional_info, + account.key_chain(), + &db_tx, + tx_block_height, + ) + .unwrap(); assert!(ptx.all_signatures_available()); let input_commitments = ptx @@ -877,15 +884,7 @@ pub fn test_fixed_signatures_generic2( .chain(acc_dests.iter().map(|_| None)) .collect::>(); - let additional_info = TxAdditionalInfo::new() - .with_token_info( - token_id, - // Note: token info doesn't influence the signature and can be random. - TokenAdditionalInfo { - num_decimals: rng.gen_range(1..10), - ticker: random_ascii_alphanumeric_string(rng, 5..10).into_bytes(), - }, - ) + let ptx_additional_info = PtxAdditionalInfo::new() .with_order_info(filled_order_v0_id, filled_order_v0_info) .with_order_info(filled_order_v1_id, filled_order_v1_info) .with_order_info(concluded_order_v0_id, concluded_order_v0_info) @@ -897,7 +896,15 @@ pub fn test_fixed_signatures_generic2( staker_balance: decommissioned_pool_balance, }, ); - let ptx = req.into_partially_signed_tx(additional_info).unwrap(); + let tokens_additional_info = TokensAdditionalInfo::new().with_info( + token_id, + // Note: token info doesn't influence the signature and can be random. + TokenAdditionalInfo { + num_decimals: rng.gen_range(1..10), + ticker: random_ascii_alphanumeric_string(rng, 5..10).into_bytes(), + }, + ); + let ptx = req.into_partially_signed_tx(ptx_additional_info).unwrap(); let input_commitments = ptx .make_sighash_input_commitments_at_height(&chain_config, tx_block_height) @@ -907,12 +914,28 @@ pub fn test_fixed_signatures_generic2( .collect_vec(); let mut signer = make_signer(chain_config.clone(), account1.account_index()); - let (ptx, _, _) = signer.sign_tx(ptx, account1.key_chain(), &db_tx, tx_block_height).unwrap(); + let (ptx, _, _) = signer + .sign_tx( + ptx, + &tokens_additional_info, + account1.key_chain(), + &db_tx, + tx_block_height, + ) + .unwrap(); assert!(ptx.all_signatures_available()); // Fully sign multisig inputs. let mut signer = make_signer(chain_config.clone(), account2.account_index()); - let (ptx, _, _) = signer.sign_tx(ptx, account2.key_chain(), &db_tx, tx_block_height).unwrap(); + let (ptx, _, _) = signer + .sign_tx( + ptx, + &tokens_additional_info, + account2.key_chain(), + &db_tx, + tx_block_height, + ) + .unwrap(); assert!(ptx.all_signatures_available()); for (i, dest) in destinations.iter().enumerate() { diff --git a/wallet/src/signer/tests/generic_tests.rs b/wallet/src/signer/tests/generic_tests.rs index ab42e6236d..73e975c385 100644 --- a/wallet/src/signer/tests/generic_tests.rs +++ b/wallet/src/signer/tests/generic_tests.rs @@ -60,7 +60,8 @@ use wallet_storage::{DefaultBackend, Store, TransactionRwUnlocked, Transactional use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, partially_signed_transaction::{ - OrderAdditionalInfo, PoolAdditionalInfo, TokenAdditionalInfo, TxAdditionalInfo, + OrderAdditionalInfo, PoolAdditionalInfo, PtxAdditionalInfo, TokenAdditionalInfo, + TokensAdditionalInfo, }, BlockInfo, Currency, KeyPurpose, }; @@ -670,14 +671,7 @@ pub fn test_sign_transaction_generic( .with_outputs(outputs); let destinations = req.destinations().to_vec(); - let additional_info = TxAdditionalInfo::new() - .with_token_info( - token_id, - TokenAdditionalInfo { - num_decimals: rng.gen_range(5..10), - ticker: random_ascii_alphanumeric_string(rng, 5..10).into_bytes(), - }, - ) + let ptx_additional_info = PtxAdditionalInfo::new() .with_order_info(filled_order1_id, filled_order1_info) .with_order_info(filled_order2_id, filled_order2_info) .with_order_info( @@ -719,12 +713,20 @@ pub fn test_sign_transaction_generic( staker_balance: decommissioned_pool_balance, }, ); - let orig_ptx = req.into_partially_signed_tx(additional_info).unwrap(); + let tokens_additional_info = TokensAdditionalInfo::new().with_info( + token_id, + TokenAdditionalInfo { + num_decimals: rng.gen_range(5..10), + ticker: random_ascii_alphanumeric_string(rng, 5..10).into_bytes(), + }, + ); + let orig_ptx = req.into_partially_signed_tx(ptx_additional_info).unwrap(); let mut signer = make_signer(chain_config.clone(), account.account_index()); let (ptx, _, _) = signer .sign_tx( orig_ptx.clone(), + &tokens_additional_info, account.key_chain(), &db_tx, tx_block_height, @@ -735,7 +737,13 @@ pub fn test_sign_transaction_generic( if let Some(make_another_signer) = &make_another_signer { let mut another_signer = make_another_signer(chain_config.clone(), account.account_index()); let (another_ptx, _, _) = another_signer - .sign_tx(orig_ptx, account.key_chain(), &db_tx, tx_block_height) + .sign_tx( + orig_ptx, + &tokens_additional_info, + account.key_chain(), + &db_tx, + tx_block_height, + ) .unwrap(); assert!(another_ptx.all_signatures_available()); @@ -796,6 +804,7 @@ pub fn test_sign_transaction_generic( let (ptx, _, _) = signer .sign_tx( orig_ptx.clone(), + &tokens_additional_info, account2.key_chain(), &db_tx, tx_block_height, @@ -807,7 +816,13 @@ pub fn test_sign_transaction_generic( let mut another_signer = make_another_signer(chain_config.clone(), account2.account_index()); let (another_ptx, _, _) = another_signer - .sign_tx(orig_ptx, account2.key_chain(), &db_tx, tx_block_height) + .sign_tx( + orig_ptx, + &tokens_additional_info, + account2.key_chain(), + &db_tx, + tx_block_height, + ) .unwrap(); assert!(another_ptx.all_signatures_available()); diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index 55cdad100d..066b3f8475 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -97,7 +97,8 @@ use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, hw_data::{HardwareWalletData, HardwareWalletFullInfo, TrezorFullInfo}, partially_signed_transaction::{ - OrderAdditionalInfo, PartiallySignedTransaction, TokenAdditionalInfo, TxAdditionalInfo, + OrderAdditionalInfo, PartiallySignedTransaction, PtxAdditionalInfo, TokenAdditionalInfo, + TokensAdditionalInfo, }, signature_status::SignatureStatus, AccountId, @@ -372,12 +373,20 @@ impl TrezorSigner { fn to_trezor_output_msgs( &self, ptx: &PartiallySignedTransaction, + tokens_additional_info: &TokensAdditionalInfo, ) -> SignerResult> { let outputs = ptx .tx() .outputs() .iter() - .map(|out| to_trezor_output_msg(&self.chain_config, out, ptx.additional_info())) + .map(|out| { + to_trezor_output_msg( + &self.chain_config, + out, + ptx.additional_info(), + tokens_additional_info, + ) + }) .collect(); outputs } @@ -504,6 +513,7 @@ impl Signer for TrezorSigner { fn sign_tx( &mut self, ptx: PartiallySignedTransaction, + tokens_additional_info: &TokensAdditionalInfo, key_chain: &impl AccountKeyChains, db_tx: &impl WalletStorageReadUnlocked, block_height: BlockHeight, @@ -512,10 +522,15 @@ impl Signer for TrezorSigner { Vec, Vec, )> { - let (inputs, standalone_inputs) = - to_trezor_input_msgs(&ptx, key_chain, &self.chain_config, db_tx)?; - let outputs = self.to_trezor_output_msgs(&ptx)?; - let utxos = to_trezor_utxo_msgs(&ptx, &self.chain_config)?; + let (inputs, standalone_inputs) = to_trezor_input_msgs( + &ptx, + tokens_additional_info, + key_chain, + &self.chain_config, + db_tx, + )?; + let outputs = self.to_trezor_output_msgs(&ptx, tokens_additional_info)?; + let utxos = to_trezor_utxo_msgs(&ptx, tokens_additional_info, &self.chain_config)?; let chain_type = to_trezor_chain_type(&self.chain_config); let input_commitment_version = self @@ -894,6 +909,7 @@ fn sign_input_with_standalone_key( fn to_trezor_input_msgs( ptx: &PartiallySignedTransaction, + tokens_additional_info: &TokensAdditionalInfo, key_chain: &impl AccountKeyChains, chain_config: &ChainConfig, db_tx: &impl WalletStorageReadUnlocked, @@ -917,12 +933,14 @@ fn to_trezor_input_msgs( nonce, command, ptx.additional_info(), + tokens_additional_info, ), TxInput::OrderAccountCommand(command) => to_trezor_order_command_input( chain_config, address_paths, command, ptx.additional_info(), + tokens_additional_info, ), }?; @@ -940,7 +958,8 @@ fn to_trezor_account_command_input( address_paths: Vec, nonce: &common::chain::AccountNonce, command: &AccountCommand, - additional_info: &TxAdditionalInfo, + ptx_additional_info: &PtxAdditionalInfo, + tokens_additional_info: &TokensAdditionalInfo, ) -> SignerResult { let mut inp_req = MintlayerAccountCommandTxInput::new(); inp_req.addresses = address_paths; @@ -1001,19 +1020,19 @@ fn to_trezor_account_command_input( initially_given, ask_balance, give_balance, - } = additional_info + } = ptx_additional_info .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; req.initially_asked = Some(to_trezor_output_value( initially_asked, - additional_info, + tokens_additional_info, chain_config, )?) .into(); req.initially_given = Some(to_trezor_output_value( initially_given, - additional_info, + tokens_additional_info, chain_config, )?) .into(); @@ -1033,19 +1052,19 @@ fn to_trezor_account_command_input( initially_given, ask_balance, give_balance, - } = additional_info + } = ptx_additional_info .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; req.initially_asked = Some(to_trezor_output_value( initially_asked, - additional_info, + tokens_additional_info, chain_config, )?) .into(); req.initially_given = Some(to_trezor_output_value( initially_given, - additional_info, + tokens_additional_info, chain_config, )?) .into(); @@ -1064,7 +1083,8 @@ fn to_trezor_order_command_input( chain_config: &ChainConfig, address_paths: Vec, command: &OrderAccountCommand, - additional_info: &TxAdditionalInfo, + ptx_additional_info: &PtxAdditionalInfo, + tokens_additional_info: &TokensAdditionalInfo, ) -> SignerResult { let mut inp_req = MintlayerOrderCommandTxInput::new(); inp_req.addresses = address_paths; @@ -1084,19 +1104,19 @@ fn to_trezor_order_command_input( initially_given, ask_balance, give_balance, - } = additional_info + } = ptx_additional_info .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; req.initially_asked = Some(to_trezor_output_value( initially_asked, - additional_info, + tokens_additional_info, chain_config, )?) .into(); req.initially_given = Some(to_trezor_output_value( initially_given, - additional_info, + tokens_additional_info, chain_config, )?) .into(); @@ -1115,20 +1135,20 @@ fn to_trezor_order_command_input( initially_given, ask_balance: _, give_balance: _, - } = additional_info + } = ptx_additional_info .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; req.initially_asked = Some(to_trezor_output_value( initially_asked, - additional_info, + tokens_additional_info, chain_config, )?) .into(); req.initially_given = Some(to_trezor_output_value( initially_given, - additional_info, + tokens_additional_info, chain_config, )?) .into(); @@ -1265,18 +1285,19 @@ fn destination_to_address_paths_impl( fn to_trezor_output_value( output_value: &OutputValue, - additional_info: &TxAdditionalInfo, + tokens_additional_info: &TokensAdditionalInfo, chain_config: &ChainConfig, ) -> SignerResult { to_trezor_output_value_with_token_info( output_value, - |token_id| additional_info.get_token_info(&token_id), + |token_id| tokens_additional_info.get_info(&token_id), chain_config, ) } fn to_trezor_utxo_msgs( ptx: &PartiallySignedTransaction, + tokens_additional_info: &TokensAdditionalInfo, chain_config: &ChainConfig, ) -> SignerResult>> { let mut utxos: BTreeMap> = BTreeMap::new(); @@ -1289,7 +1310,12 @@ fn to_trezor_utxo_msgs( OutPointSourceId::Transaction(id) => id.to_hash().0, OutPointSourceId::BlockReward(id) => id.to_hash().0, }; - let out = to_trezor_output_msg(chain_config, utxo, ptx.additional_info())?; + let out = to_trezor_output_msg( + chain_config, + utxo, + ptx.additional_info(), + tokens_additional_info, + )?; utxos.entry(id).or_default().insert(outpoint.output_index(), out); } TxInput::Account(_) @@ -1304,14 +1330,15 @@ fn to_trezor_utxo_msgs( fn to_trezor_output_msg( chain_config: &ChainConfig, out: &TxOutput, - additional_info: &TxAdditionalInfo, + ptx_additional_info: &PtxAdditionalInfo, + tokens_additional_info: &TokensAdditionalInfo, ) -> SignerResult { let res = match out { TxOutput::Transfer(value, dest) => { let mut out_req = MintlayerTransferTxOutput::new(); out_req.value = Some(to_trezor_output_value( value, - additional_info, + tokens_additional_info, chain_config, )?) .into(); @@ -1325,7 +1352,7 @@ fn to_trezor_output_msg( let mut out_req = MintlayerLockThenTransferTxOutput::new(); out_req.value = Some(to_trezor_output_value( value, - additional_info, + tokens_additional_info, chain_config, )?) .into(); @@ -1341,7 +1368,7 @@ fn to_trezor_output_msg( let mut out_req = MintlayerBurnTxOutput::new(); out_req.value = Some(to_trezor_output_value( value, - additional_info, + tokens_additional_info, chain_config, )?) .into(); @@ -1393,7 +1420,7 @@ fn to_trezor_output_msg( let mut out_req = MintlayerProduceBlockFromStakeTxOutput::new(); out_req.set_pool_id(Address::new(chain_config, *pool_id)?.into_string()); out_req.set_destination(Address::new(chain_config, dest.clone())?.into_string()); - let staker_balance = additional_info + let staker_balance = ptx_additional_info .get_pool_info(pool_id) .ok_or(SignerError::MissingTxExtraInfo)? .staker_balance; @@ -1486,7 +1513,7 @@ fn to_trezor_output_msg( let mut out_req = MintlayerHtlcTxOutput::new(); out_req.value = Some(to_trezor_output_value( value, - additional_info, + tokens_additional_info, chain_config, )?) .into(); @@ -1512,13 +1539,13 @@ fn to_trezor_output_msg( out_req.ask = Some(to_trezor_output_value( data.ask(), - additional_info, + tokens_additional_info, chain_config, )?) .into(); out_req.give = Some(to_trezor_output_value( data.give(), - additional_info, + tokens_additional_info, chain_config, )?) .into(); diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 6c7c7e14ba..a875cb9bd3 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -81,7 +81,7 @@ use wallet_types::chain_info::ChainInfo; use wallet_types::hw_data::HardwareWalletFullInfo; use wallet_types::partially_signed_transaction::{ PartiallySignedTransaction, PartiallySignedTransactionError, PoolAdditionalInfo, - TokenAdditionalInfo, TxAdditionalInfo, + PtxAdditionalInfo, TokenAdditionalInfo, TokensAdditionalInfo, TxAdditionalInfo, }; use wallet_types::seed_phrase::SerializableSeedPhrase; use wallet_types::signature_status::SignatureStatus; @@ -1144,12 +1144,18 @@ where |account, db_tx, chain_config, signer_provider| { let (mut request, additional_data) = f(account, db_tx)?; let fees = request.get_fees(); - let ptx = request.into_partially_signed_tx(additional_info)?; + let ptx = request.into_partially_signed_tx(additional_info.ptx_additional_info)?; let mut signer = signer_provider.provide(Arc::new(chain_config.clone()), account_index); let ptx = signer - .sign_tx(ptx, account.key_chain(), db_tx, next_block_height) + .sign_tx( + ptx, + &additional_info.tokens_additional_info, + account.key_chain(), + db_tx, + next_block_height, + ) .map(|(ptx, _, _)| ptx)?; let input_commitments = @@ -1664,7 +1670,7 @@ where change_addresses: BTreeMap>, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - additional_info: TxAdditionalInfo, + ptx_additional_info: PtxAdditionalInfo, ) -> WalletResult<(PartiallySignedTransaction, BTreeMap)> { let request = SendRequest::new().with_outputs(outputs); let latest_median_time = self.latest_median_time; @@ -1680,7 +1686,7 @@ where current_fee_rate, consolidate_fee_rate, }, - additional_info, + ptx_additional_info, ) }) } @@ -2110,8 +2116,8 @@ where let (_, best_block_height) = self.get_best_block_for_account(account_index)?; let next_block_height = best_block_height.next_height(); - let additional_info = - TxAdditionalInfo::new().with_pool_info(pool_id, PoolAdditionalInfo { staker_balance }); + let ptx_additional_info = + PtxAdditionalInfo::new().with_pool_info(pool_id, PoolAdditionalInfo { staker_balance }); self.for_account_rw_unlocked( account_index, |account, db_tx, chain_config, signer_provider| { @@ -2123,12 +2129,18 @@ where current_fee_rate, )?; - let ptx = request.into_partially_signed_tx(additional_info)?; + let ptx = request.into_partially_signed_tx(ptx_additional_info)?; let mut signer = signer_provider.provide(Arc::new(chain_config.clone()), account_index); let ptx = signer - .sign_tx(ptx, account.key_chain(), db_tx, next_block_height) + .sign_tx( + ptx, + &TokensAdditionalInfo::new(), + account.key_chain(), + db_tx, + next_block_height, + ) .map(|(ptx, _, _)| ptx)?; if ptx.all_signatures_available() { @@ -2304,6 +2316,7 @@ where &mut self, account_index: U31, ptx: PartiallySignedTransaction, + tokens_additional_info: &TokensAdditionalInfo, ) -> WalletResult<( PartiallySignedTransaction, Vec, @@ -2318,7 +2331,13 @@ where let mut signer = signer_provider.provide(Arc::new(chain_config.clone()), account_index); - let res = signer.sign_tx(ptx, account.key_chain(), db_tx, next_block_height)?; + let res = signer.sign_tx( + ptx, + tokens_additional_info, + account.key_chain(), + db_tx, + next_block_height, + )?; Ok(res) }, ) diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 258c6d05a1..a946a98f27 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -55,7 +55,7 @@ use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, partially_signed_transaction::{ OrderAdditionalInfo, PartiallySignedTransaction, PartiallySignedTransactionError, - TxAdditionalInfo, + PartiallySignedTransactionWalletExt as _, TxAdditionalInfo, }, seed_phrase::{PassPhrase, StoreSeedPhrase}, utxo_types::{UtxoState, UtxoType}, @@ -5069,17 +5069,17 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { // remove the signatures and try to sign it again let tx = stake_pool_transaction.transaction().clone(); let inps = tx.inputs().len(); - let ptx = PartiallySignedTransaction::new( + let ptx = PartiallySignedTransaction::new_for_wallet( tx, vec![None; inps], vec![Some(utxo)], vec![Some(addr.into_object())], None, - TxAdditionalInfo::new(), + PtxAdditionalInfo::new(), ) .unwrap(); let stake_pool_transaction = wallet - .sign_raw_transaction(acc_0_index, ptx) + .sign_raw_transaction(acc_0_index, ptx, &TokensAdditionalInfo::new()) .unwrap() .0 .into_signed_tx() @@ -5108,17 +5108,26 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) .unwrap(); + let tokens_additional_info = TokensAdditionalInfo::new(); // Try to sign decommission request with wrong account let sign_from_acc0_res = wallet - .sign_raw_transaction(acc_0_index, decommission_partial_tx.clone()) + .sign_raw_transaction( + acc_0_index, + decommission_partial_tx.clone(), + &tokens_additional_info, + ) .unwrap() .0; // the tx is still not fully signed assert!(!sign_from_acc0_res.all_signatures_available()); let signed_tx = wallet - .sign_raw_transaction(acc_1_index, decommission_partial_tx) + .sign_raw_transaction( + acc_1_index, + decommission_partial_tx, + &tokens_additional_info, + ) .unwrap() .0 .into_signed_tx() @@ -5201,10 +5210,15 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) .unwrap(); + let tokens_additional_info = TokensAdditionalInfo::new(); // sign the tx with cold wallet let partially_signed_transaction = cold_wallet - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, decommission_partial_tx) + .sign_raw_transaction( + DEFAULT_ACCOUNT_INDEX, + decommission_partial_tx, + &tokens_additional_info, + ) .unwrap() .0; assert!(partially_signed_transaction.all_signatures_available()); @@ -5212,7 +5226,11 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { // sign it with the hot wallet should leave the signatures in place even if it can't find the // destinations for the inputs let partially_signed_transaction = hot_wallet - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, partially_signed_transaction) + .sign_raw_transaction( + DEFAULT_ACCOUNT_INDEX, + partially_signed_transaction, + &tokens_additional_info, + ) .unwrap() .0; assert!(partially_signed_transaction.all_signatures_available()); @@ -5371,13 +5389,18 @@ fn sign_send_request_cold_wallet(#[case] seed: Seed) { [(Currency::Coin, cold_wallet_address.clone())].into(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), - TxAdditionalInfo::new(), + PtxAdditionalInfo::new(), ) .unwrap(); + let tokens_additional_info = TokensAdditionalInfo::new(); // Try to sign request with the hot wallet let tx = hot_wallet - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, send_req.clone()) + .sign_raw_transaction( + DEFAULT_ACCOUNT_INDEX, + send_req.clone(), + &tokens_additional_info, + ) .unwrap() .0; // the tx is not fully signed @@ -5385,7 +5408,7 @@ fn sign_send_request_cold_wallet(#[case] seed: Seed) { // sign the tx with cold wallet let signed_tx = cold_wallet - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, send_req) + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, send_req, &tokens_additional_info) .unwrap() .0 .into_signed_tx() @@ -5555,39 +5578,52 @@ fn test_add_standalone_multisig(#[case] seed: Seed) { )], ) .unwrap(); - let spend_multisig_tx = PartiallySignedTransaction::new( + let spend_multisig_tx = PartiallySignedTransaction::new_for_wallet( spend_multisig_tx, vec![None; 1], vec![Some(tx.outputs()[0].clone())], vec![Some(multisig_address.as_object().clone())], None, - TxAdditionalInfo::new(), + PtxAdditionalInfo::new(), ) .unwrap(); + let tokens_additional_info = TokensAdditionalInfo::new(); // sign it with wallet1 - let (ptx, _, statuses) = - wallet1.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, spend_multisig_tx).unwrap(); + let (ptx, _, statuses) = wallet1 + .sign_raw_transaction( + DEFAULT_ACCOUNT_INDEX, + spend_multisig_tx, + &tokens_additional_info, + ) + .unwrap(); // check it is still not fully signed assert!(ptx.all_signatures_available()); assert!(!statuses.iter().all(|s| *s == SignatureStatus::FullySigned)); // try to sign it with wallet1 again - let (ptx, _, statuses) = wallet1.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx).unwrap(); + let (ptx, _, statuses) = wallet1 + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx, &tokens_additional_info) + .unwrap(); // check it is still not fully signed assert!(ptx.all_signatures_available()); assert!(!statuses.iter().all(|s| *s == SignatureStatus::FullySigned)); // try to sign it with wallet2 but wallet2 does not have the multisig added as standalone - let ptx = wallet2.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx).unwrap().0; + let ptx = wallet2 + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx, &tokens_additional_info) + .unwrap() + .0; // add it to wallet2 as well wallet2.add_standalone_multisig(DEFAULT_ACCOUNT_INDEX, challenge, None).unwrap(); // now we can sign it - let (ptx, _, statuses) = wallet2.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx).unwrap(); + let (ptx, _, statuses) = wallet2 + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx, &tokens_additional_info) + .unwrap(); // now it is fully signed assert!(ptx.all_signatures_available()); @@ -5709,18 +5745,20 @@ fn create_htlc_and_spend(#[case] seed: Seed) { ) .unwrap(); let spend_utxos = vec![create_htlc_tx.transaction().outputs().first().cloned()]; - let spend_ptx = PartiallySignedTransaction::new( + let spend_ptx = PartiallySignedTransaction::new_for_wallet( spend_tx, vec![None], spend_utxos, vec![Some(spend_key.into_object())], Some(vec![Some(secret)]), - TxAdditionalInfo::new(), + PtxAdditionalInfo::new(), ) .unwrap(); + let tokens_additional_info = TokensAdditionalInfo::new(); - let (spend_ptx, _, new_statuses) = - wallet2.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, spend_ptx).unwrap(); + let (spend_ptx, _, new_statuses) = wallet2 + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, spend_ptx, &tokens_additional_info) + .unwrap(); assert_eq!(vec![SignatureStatus::FullySigned], new_statuses); let spend_tx = spend_ptx.into_signed_tx().unwrap(); @@ -5809,13 +5847,13 @@ fn create_htlc_and_refund(#[case] seed: Seed) { ) .unwrap(); let refund_utxos = vec![create_htlc_tx.transaction().outputs().first().cloned()]; - let refund_ptx = PartiallySignedTransaction::new( + let refund_ptx = PartiallySignedTransaction::new_for_wallet( refund_tx, vec![None], refund_utxos, vec![Some(refund_key)], None, - TxAdditionalInfo::new(), + PtxAdditionalInfo::new(), ) .unwrap(); @@ -5851,8 +5889,11 @@ fn create_htlc_and_refund(#[case] seed: Seed) { .unwrap(); assert_eq!(wallet2_multisig_utxos.len(), 1); - let (refund_ptx, prev_statuses, new_statuses) = - wallet2.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, refund_ptx).unwrap(); + let tokens_additional_info = TokensAdditionalInfo::new(); + + let (refund_ptx, prev_statuses, new_statuses) = wallet2 + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, refund_ptx, &tokens_additional_info) + .unwrap(); assert_eq!(vec![SignatureStatus::NotSigned], prev_statuses); assert_eq!( @@ -5863,8 +5904,9 @@ fn create_htlc_and_refund(#[case] seed: Seed) { new_statuses ); - let (refund_ptx, prev_statuses, new_statuses) = - wallet1.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, refund_ptx).unwrap(); + let (refund_ptx, prev_statuses, new_statuses) = wallet1 + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, refund_ptx, &tokens_additional_info) + .unwrap(); assert_eq!( vec![SignatureStatus::PartialMultisig { required_signatures: 2, @@ -7875,18 +7917,23 @@ fn conflicting_delegation_account_nonce_multiple_inputs(#[case] seed: Seed) { ) .unwrap(); - let spend_from_delegation_ptx = PartiallySignedTransaction::new( + let spend_from_delegation_ptx = PartiallySignedTransaction::new_for_wallet( spend_from_delegation_tx, vec![None; 3], vec![None; 3], vec![Some(spend_destination); 3], None, - TxAdditionalInfo::new(), + PtxAdditionalInfo::new(), ) .unwrap(); + let tokens_additional_info = TokensAdditionalInfo::new(); let spend_from_delegation_signed_tx = wallet - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, spend_from_delegation_ptx) + .sign_raw_transaction( + DEFAULT_ACCOUNT_INDEX, + spend_from_delegation_ptx, + &tokens_additional_info, + ) .unwrap() .0 .into_signed_tx() diff --git a/wallet/types/src/partially_signed_transaction.rs b/wallet/types/src/partially_signed_transaction.rs index 063d9cf11c..43962d2675 100644 --- a/wallet/types/src/partially_signed_transaction.rs +++ b/wallet/types/src/partially_signed_transaction.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2022 RBB S.r.l +// Copyright (c) 2021-2025 RBB S.r.l // opensource@mintlayer.org // SPDX-License-Identifier: MIT // Licensed under the MIT License; @@ -15,569 +15,127 @@ use std::collections::BTreeMap; -use common::{ - chain::{ - htlc::HtlcSecret, - output_value::OutputValue, - signature::{ - inputsig::InputWitness, - sighash::{ - self, - input_commitments::{ - make_sighash_input_commitments_for_transaction_inputs, - make_sighash_input_commitments_for_transaction_inputs_at_height, - SighashInputCommitment, - }, - }, - Signable, Transactable, - }, - tokens::TokenId, - AccountCommand, ChainConfig, Destination, OrderAccountCommand, OrderId, PoolId, - SighashInputCommitmentVersion, SignedTransaction, Transaction, TransactionCreationError, - TxInput, TxOutput, - }, - primitives::{Amount, BlockHeight}, +use common::chain::{ + htlc::HtlcSecret, partially_signed_transaction::PartiallySignedTransactionConsistencyCheck, + signature::inputsig::InputWitness, tokens::TokenId, Destination, OrderId, PoolId, Transaction, + TxOutput, }; -use serialization::{Decode, Encode}; -use thiserror::Error; -use tx_verifier::input_check::signature_only_check::SignatureOnlyVerifiable; -use utils::ensure; -#[derive(Error, Debug, Clone, PartialEq, Eq)] -pub enum PartiallySignedTransactionError { - #[error("Failed to convert partially signed tx to signed")] - FailedToConvertPartiallySignedTx(Box), - - #[error("Failed to create transaction: {0}")] - TxCreationError(TransactionCreationError), - - #[error("The number of witnesses does not match the number of inputs")] - InvalidWitnessCount, - - #[error("The number of input utxos does not match the number of inputs")] - InvalidInputUtxosCount, - - #[error("The number of destinations does not match the number of inputs")] - InvalidDestinationsCount, - - #[error("The number of htlc secrets does not match the number of inputs")] - InvalidHtlcSecretsCount, - - #[error("Missing UTXO for input #{input_index}")] - MissingUtxoForUtxoInput { input_index: usize }, - - #[error("A UTXO for non-UTXO input #{input_index} is specified")] - UtxoPresentForNonUtxoInput { input_index: usize }, - - #[error("Additional info is missing for order {0}")] - OrderAdditionalInfoMissing(OrderId), - - #[error("Additional info is missing for token {0}")] - TokenAdditionalInfoMissing(TokenId), - - #[error("Additional info is missing for pool {0}")] - PoolAdditionalInfoMissing(PoolId), - - #[error("Error creating sighash input commitment: {0}")] - SighashInputCommitmentCreationError(#[from] SighashInputCommitmentCreationError), -} - -#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] -pub struct TokenAdditionalInfo { - pub num_decimals: u8, - pub ticker: Vec, -} - -#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] -pub struct PoolAdditionalInfo { - pub staker_balance: Amount, -} - -#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] -pub struct OrderAdditionalInfo { - pub initially_asked: OutputValue, - pub initially_given: OutputValue, - pub ask_balance: Amount, - pub give_balance: Amount, -} - -/// Additional info for a partially signed Tx mainly used by hardware wallets to show info to the -/// user -#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] -pub struct TxAdditionalInfo { - token_info: BTreeMap, - pool_info: BTreeMap, - order_info: BTreeMap, -} - -impl TxAdditionalInfo { - pub fn new() -> Self { - Self { - token_info: BTreeMap::new(), - pool_info: BTreeMap::new(), - order_info: BTreeMap::new(), - } - } - - pub fn with_token_info(mut self, token_id: TokenId, info: TokenAdditionalInfo) -> Self { - self.token_info.insert(token_id, info); - self - } - - pub fn with_pool_info(mut self, pool_id: PoolId, info: PoolAdditionalInfo) -> Self { - self.pool_info.insert(pool_id, info); - self - } - - pub fn with_order_info(mut self, order_id: OrderId, info: OrderAdditionalInfo) -> Self { - self.order_info.insert(order_id, info); - self - } - - pub fn add_token_info(&mut self, token_id: TokenId, info: TokenAdditionalInfo) { - self.token_info.insert(token_id, info); - } - - pub fn join(mut self, other: Self) -> Self { - self.token_info.extend(other.token_info); - self.pool_info.extend(other.pool_info); - self.order_info.extend(other.order_info); - Self { - token_info: self.token_info, - pool_info: self.pool_info, - order_info: self.order_info, - } - } - - pub fn get_token_info(&self, token_id: &TokenId) -> Option<&TokenAdditionalInfo> { - self.token_info.get(token_id) - } - - pub fn get_pool_info(&self, pool_id: &PoolId) -> Option<&PoolAdditionalInfo> { - self.pool_info.get(pool_id) - } - - pub fn get_order_info(&self, order_id: &OrderId) -> Option<&OrderAdditionalInfo> { - self.order_info.get(order_id) - } - - pub fn token_info_iter(&self) -> impl Iterator { - self.token_info.iter() - } - - pub fn pool_info_iter(&self) -> impl Iterator { - self.pool_info.iter() - } - - pub fn order_info_iter(&self) -> impl Iterator { - self.order_info.iter() - } -} - -impl sighash::input_commitments::PoolInfoProvider for TxAdditionalInfo { - type Error = std::convert::Infallible; - - fn get_pool_info( - &self, - pool_id: &PoolId, - ) -> Result, Self::Error> { - Ok( - self.pool_info.get(pool_id).map(|info| sighash::input_commitments::PoolInfo { - staker_balance: info.staker_balance, - }), - ) - } -} - -impl sighash::input_commitments::OrderInfoProvider for TxAdditionalInfo { - type Error = std::convert::Infallible; - - fn get_order_info( - &self, - order_id: &OrderId, - ) -> Result, Self::Error> { - Ok( - self.order_info.get(order_id).map(|info| sighash::input_commitments::OrderInfo { - initially_asked: info.initially_asked.clone(), - initially_given: info.initially_given.clone(), - ask_balance: info.ask_balance, - give_balance: info.give_balance, - }), - ) - } -} - -#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] -pub struct PartiallySignedTransaction { - tx: Transaction, - witnesses: Vec>, - - input_utxos: Vec>, - destinations: Vec>, - - htlc_secrets: Vec>, - additional_info: TxAdditionalInfo, -} +pub use common::chain::partially_signed_transaction::{ + make_sighash_input_commitments, OrderAdditionalInfo, PartiallySignedTransaction, + PartiallySignedTransactionError, PoolAdditionalInfo, SighashInputCommitmentCreationError, + TxAdditionalInfo as PtxAdditionalInfo, +}; -impl PartiallySignedTransaction { - pub fn new_unchecked( +pub trait PartiallySignedTransactionWalletExt { + fn new_for_wallet( tx: Transaction, witnesses: Vec>, input_utxos: Vec>, destinations: Vec>, htlc_secrets: Option>>, - additional_info: TxAdditionalInfo, - ) -> Self { - let htlc_secrets = htlc_secrets.unwrap_or_else(|| vec![None; tx.inputs().len()]); - - Self { - tx, - witnesses, - input_utxos, - destinations, - htlc_secrets, - additional_info, - } - } + additional_info: PtxAdditionalInfo, + ) -> Result; +} - pub fn new( +impl PartiallySignedTransactionWalletExt for PartiallySignedTransaction { + fn new_for_wallet( tx: Transaction, witnesses: Vec>, input_utxos: Vec>, destinations: Vec>, htlc_secrets: Option>>, - additional_info: TxAdditionalInfo, + additional_info: PtxAdditionalInfo, ) -> Result { - let this = Self::new_unchecked( + let consistency_checks = if cfg!(debug_assertions) { + PartiallySignedTransactionConsistencyCheck::WithAdditionalInfo + } else { + PartiallySignedTransactionConsistencyCheck::Basic + }; + + Self::new( tx, witnesses, input_utxos, destinations, htlc_secrets, additional_info, - ); - - this.ensure_consistency(Self::need_heavy_consistency_checks())?; - Ok(this) - } - - pub fn ensure_consistency( - &self, - with_heavy_checks: bool, - ) -> Result<(), PartiallySignedTransactionError> { - ensure!( - self.tx.inputs().len() == self.witnesses.len(), - PartiallySignedTransactionError::InvalidWitnessCount - ); - - ensure!( - self.tx.inputs().len() == self.input_utxos.len(), - PartiallySignedTransactionError::InvalidInputUtxosCount, - ); - - ensure!( - self.tx.inputs().len() == self.destinations.len(), - PartiallySignedTransactionError::InvalidDestinationsCount - ); - - ensure!( - self.tx.inputs().len() == self.htlc_secrets.len(), - PartiallySignedTransactionError::InvalidHtlcSecretsCount - ); - - if with_heavy_checks { - self.ensure_additional_info_completeness()?; - } - - Ok(()) - } - - fn need_heavy_consistency_checks() -> bool { - cfg!(debug_assertions) + consistency_checks, + ) } +} - fn ensure_additional_info_completeness(&self) -> Result<(), PartiallySignedTransactionError> { - let ensure_order_info_present = - |order_id: &OrderId| -> Result<_, PartiallySignedTransactionError> { - ensure!( - self.additional_info.get_order_info(order_id).is_some(), - PartiallySignedTransactionError::OrderAdditionalInfoMissing(*order_id) - ); - Ok(()) - }; - let ensure_token_info_present = - |token_id: &TokenId| -> Result<_, PartiallySignedTransactionError> { - ensure!( - self.additional_info.get_token_info(token_id).is_some(), - PartiallySignedTransactionError::TokenAdditionalInfoMissing(*token_id) - ); - Ok(()) - }; - - let ensure_no_utxo = |input_index, - input_utxo_opt: &Option| - -> Result<_, PartiallySignedTransactionError> { - ensure!( - input_utxo_opt.is_none(), - PartiallySignedTransactionError::UtxoPresentForNonUtxoInput { input_index } - ); - Ok(()) - }; - - let check_tx_output = |output: &TxOutput| -> Result<(), PartiallySignedTransactionError> { - match output { - TxOutput::Transfer(output_value, _) - | TxOutput::LockThenTransfer(output_value, _, _) - | TxOutput::Burn(output_value) - | TxOutput::Htlc(output_value, _) => { - output_value.token_v1_id().map(ensure_token_info_present).transpose()?; - } - TxOutput::CreateOrder(order_data) => { - order_data.ask().token_v1_id().map(ensure_token_info_present).transpose()?; - order_data.give().token_v1_id().map(ensure_token_info_present).transpose()?; - } - TxOutput::ProduceBlockFromStake(_, pool_id) => { - ensure!( - self.additional_info.get_pool_info(pool_id).is_some(), - PartiallySignedTransactionError::PoolAdditionalInfoMissing(*pool_id) - ); - } - - TxOutput::CreateDelegationId(_, _) - | TxOutput::DelegateStaking(_, _) - | TxOutput::IssueFungibleToken(_) - | TxOutput::CreateStakePool(_, _) - | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => {} - } - Ok(()) - }; - - for (input_index, (input, input_utxo)) in - self.tx.inputs().iter().zip(self.input_utxos.iter()).enumerate() - { - match input { - TxInput::Utxo(_) => { - let input_utxo = input_utxo.as_ref().ok_or( - PartiallySignedTransactionError::MissingUtxoForUtxoInput { input_index }, - )?; - check_tx_output(input_utxo)?; - } - TxInput::Account(_) => ensure_no_utxo(input_index, input_utxo)?, - TxInput::AccountCommand(_, command) => { - ensure_no_utxo(input_index, input_utxo)?; - - match command { - AccountCommand::ConcludeOrder(id) => ensure_order_info_present(id)?, - AccountCommand::FillOrder(id, _, _) => ensure_order_info_present(id)?, - - AccountCommand::MintTokens(_, _) - | AccountCommand::UnmintTokens(_) - | AccountCommand::LockTokenSupply(_) - | AccountCommand::FreezeToken(_, _) - | AccountCommand::UnfreezeToken(_) - | AccountCommand::ChangeTokenAuthority(_, _) - | AccountCommand::ChangeTokenMetadataUri(_, _) => {} - } - } - TxInput::OrderAccountCommand(command) => { - let id = match command { - OrderAccountCommand::FillOrder(id, _) => id, - OrderAccountCommand::FreezeOrder(id) => id, - OrderAccountCommand::ConcludeOrder(id) => id, - }; - ensure_order_info_present(id)? - } - } - } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TokenAdditionalInfo { + pub num_decimals: u8, + pub ticker: Vec, +} - for output in self.tx.outputs() { - check_tx_output(output)?; - } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TokensAdditionalInfo { + infos: BTreeMap, +} - for (_, order_info) in self.additional_info.order_info_iter() { - order_info - .initially_asked - .token_v1_id() - .map(ensure_token_info_present) - .transpose()?; - order_info - .initially_given - .token_v1_id() - .map(ensure_token_info_present) - .transpose()?; +impl TokensAdditionalInfo { + pub fn new() -> Self { + Self { + infos: BTreeMap::new(), } - - Ok(()) } - pub fn with_witnesses( - mut self, - witnesses: Vec>, - ) -> Result { - self.witnesses = witnesses; - self.ensure_consistency(Self::need_heavy_consistency_checks())?; - Ok(self) - } - - pub fn tx(&self) -> &Transaction { - &self.tx - } - - pub fn take_tx(self) -> Transaction { - self.tx - } - - pub fn input_utxos(&self) -> &[Option] { - self.input_utxos.as_ref() - } - - pub fn destinations(&self) -> &[Option] { - self.destinations.as_ref() - } - - pub fn witnesses(&self) -> &[Option] { - self.witnesses.as_ref() - } - - pub fn htlc_secrets(&self) -> &[Option] { - self.htlc_secrets.as_ref() - } - - pub fn count_inputs(&self) -> usize { - self.tx.inputs().len() + pub fn with_info(mut self, token_id: TokenId, info: TokenAdditionalInfo) -> Self { + self.infos.insert(token_id, info); + self } - pub fn all_signatures_available(&self) -> bool { - self.witnesses - .iter() - .enumerate() - .zip(&self.destinations) - .all(|((_, witness), dest)| { - let dest_needs_signature = match dest { - Some(dest) => match dest { - Destination::AnyoneCanSpend => false, - Destination::PublicKeyHash(_) - | Destination::PublicKey(_) - | Destination::ScriptHash(_) - | Destination::ClassicMultisig(_) => true, - }, - None => false, - }; - - match (witness, dest_needs_signature) { - (Some(InputWitness::NoSignature(_)), false) => true, - (Some(InputWitness::NoSignature(_)), true) => false, - // TODO: consider returning a Result and produce an error in this case. - (Some(InputWitness::Standard(_)), false) => false, - (Some(InputWitness::Standard(_)), true) => true, - (None, _) => false, - } - }) + pub fn add_info(&mut self, token_id: TokenId, info: TokenAdditionalInfo) { + self.infos.insert(token_id, info); } - pub fn into_signed_tx(self) -> Result { - if self.all_signatures_available() { - let witnesses = self.witnesses.into_iter().map(|w| w.expect("cannot fail")).collect(); - Ok(SignedTransaction::new(self.tx, witnesses) - .map_err(PartiallySignedTransactionError::TxCreationError)?) - } else { - Err(PartiallySignedTransactionError::FailedToConvertPartiallySignedTx(Box::new(self))) - } - } - - pub fn additional_info(&self) -> &TxAdditionalInfo { - &self.additional_info + pub fn join(mut self, other: Self) -> Self { + self.infos.extend(other.infos); + self } - pub fn make_sighash_input_commitments( - &self, - version: SighashInputCommitmentVersion, - ) -> Result>, PartiallySignedTransactionError> { - Ok(make_sighash_input_commitments( - self.tx.inputs(), - &self.input_utxos, - &self.additional_info, - version, - )?) + pub fn get_info(&self, token_id: &TokenId) -> Option<&TokenAdditionalInfo> { + self.infos.get(token_id) } - pub fn make_sighash_input_commitments_at_height( - &self, - chain_config: &ChainConfig, - block_height: BlockHeight, - ) -> Result>, PartiallySignedTransactionError> { - Ok(make_sighash_input_commitments_at_height( - self.tx.inputs(), - &self.input_utxos, - &self.additional_info, - chain_config, - block_height, - )?) + pub fn info_iter(&self) -> impl Iterator { + self.infos.iter() } } -pub fn make_sighash_input_commitments_at_height<'a>( - tx_inputs: &[TxInput], - input_utxos: &'a [Option], - additional_info: &TxAdditionalInfo, - chain_config: &ChainConfig, - block_height: BlockHeight, -) -> Result>, SighashInputCommitmentCreationError> { - make_sighash_input_commitments_for_transaction_inputs_at_height( - tx_inputs, - &sighash::input_commitments::TrivialUtxoProvider(input_utxos), - additional_info, - additional_info, - chain_config, - block_height, - ) -} - -pub fn make_sighash_input_commitments<'a>( - tx_inputs: &[TxInput], - input_utxos: &'a [Option], - additional_info: &TxAdditionalInfo, - version: SighashInputCommitmentVersion, -) -> Result>, SighashInputCommitmentCreationError> { - make_sighash_input_commitments_for_transaction_inputs( - tx_inputs, - &sighash::input_commitments::TrivialUtxoProvider(input_utxos), - additional_info, - additional_info, - version, - ) +#[derive(Clone, Debug)] +pub struct TxAdditionalInfo { + pub ptx_additional_info: PtxAdditionalInfo, + pub tokens_additional_info: TokensAdditionalInfo, } -pub type SighashInputCommitmentCreationError = - sighash::input_commitments::SighashInputCommitmentCreationError< - std::convert::Infallible, - std::convert::Infallible, - std::convert::Infallible, - >; - -impl Signable for PartiallySignedTransaction { - fn inputs(&self) -> Option<&[TxInput]> { - Some(self.tx.inputs()) +impl TxAdditionalInfo { + pub fn new() -> Self { + Self { + ptx_additional_info: PtxAdditionalInfo::new(), + tokens_additional_info: TokensAdditionalInfo::new(), + } } - fn outputs(&self) -> Option<&[TxOutput]> { - Some(self.tx.outputs()) + pub fn with_token_info(mut self, token_id: TokenId, info: TokenAdditionalInfo) -> Self { + self.tokens_additional_info = self.tokens_additional_info.with_info(token_id, info); + self } - fn version_byte(&self) -> Option { - Some(self.tx.version_byte()) + pub fn with_pool_info(mut self, pool_id: PoolId, info: PoolAdditionalInfo) -> Self { + self.ptx_additional_info = self.ptx_additional_info.with_pool_info(pool_id, info); + self } - fn flags(&self) -> Option { - Some(self.tx.flags()) + pub fn with_order_info(mut self, order_id: OrderId, info: OrderAdditionalInfo) -> Self { + self.ptx_additional_info = self.ptx_additional_info.with_order_info(order_id, info); + self } -} -impl Transactable for PartiallySignedTransaction { - fn signatures(&self) -> Vec> { - self.witnesses.clone() + pub fn get_token_info(&self, token_id: &TokenId) -> Option<&TokenAdditionalInfo> { + self.tokens_additional_info.get_info(token_id) } } - -impl SignatureOnlyVerifiable for PartiallySignedTransaction {} diff --git a/wallet/wallet-controller/src/helpers/mod.rs b/wallet/wallet-controller/src/helpers/mod.rs index 23330bcadb..a35958df42 100644 --- a/wallet/wallet-controller/src/helpers/mod.rs +++ b/wallet/wallet-controller/src/helpers/mod.rs @@ -26,7 +26,6 @@ use common::{ address::RpcAddress, chain::{ htlc::HtlcSecret, - output_value::OutputValue, tokens::{RPCTokenInfo, TokenId}, AccountCommand, ChainConfig, Destination, OrderAccountCommand, OrderId, PoolId, RpcOrderInfo, Transaction, TxInput, TxOutput, UtxoOutPoint, @@ -41,8 +40,8 @@ use wallet::{ }; use wallet_types::{ partially_signed_transaction::{ - OrderAdditionalInfo, PartiallySignedTransaction, PoolAdditionalInfo, TokenAdditionalInfo, - TxAdditionalInfo, + OrderAdditionalInfo, PartiallySignedTransaction, PartiallySignedTransactionWalletExt as _, + PoolAdditionalInfo, PtxAdditionalInfo, TokenAdditionalInfo, TokensAdditionalInfo, }, Currency, }; @@ -62,31 +61,35 @@ pub async fn fetch_token_info( ))) } -pub async fn fetch_token_infos_into_tx_info( +pub async fn fetch_token_infos_into( rpc_client: &T, - token_ids: impl IntoIterator, - tx_info: &mut TxAdditionalInfo, + token_ids: &BTreeSet, + dest_info: &mut TokensAdditionalInfo, ) -> Result<(), ControllerError> { - let mut seen_token_infos = BTreeSet::new(); - for token_id in token_ids { - if !seen_token_infos.contains(&token_id) { - let token_info = fetch_token_info(rpc_client, token_id).await?; - - tx_info.add_token_info( - token_id, - TokenAdditionalInfo { - num_decimals: token_info.token_number_of_decimals(), - ticker: token_info.token_ticker().to_vec(), - }, - ); - seen_token_infos.insert(token_id); - } + let token_info = fetch_token_info(rpc_client, *token_id).await?; + + dest_info.add_info( + *token_id, + TokenAdditionalInfo { + num_decimals: token_info.token_number_of_decimals(), + ticker: token_info.token_ticker().to_vec(), + }, + ); } Ok(()) } +pub async fn fetch_token_infos( + rpc_client: &T, + token_ids: &BTreeSet, +) -> Result> { + let mut result = TokensAdditionalInfo::new(); + fetch_token_infos_into(rpc_client, token_ids, &mut result).await?; + Ok(result) +} + pub async fn fetch_order_info( rpc_client: &T, order_id: OrderId, @@ -180,67 +183,37 @@ fn pool_id_from_txo(utxo: &TxOutput) -> Option { } } -async fn fetch_token_extra_info( - rpc_client: &T, - value: &OutputValue, -) -> Result> -where - T: NodeInterface, -{ - match value { - OutputValue::Coin(_) | OutputValue::TokenV0(_) => Ok(TxAdditionalInfo::new()), - OutputValue::TokenV1(token_id, _) => { - let info = fetch_token_info(rpc_client, *token_id).await?; - Ok(TxAdditionalInfo::new().with_token_info( - *token_id, - TokenAdditionalInfo { - num_decimals: info.token_number_of_decimals(), - ticker: info.token_ticker().to_vec(), - }, - )) - } - } -} - pub async fn fetch_utxo_extra_info( rpc_client: &T, utxo: TxOutput, -) -> Result<(TxOutput, TxAdditionalInfo), ControllerError> +) -> Result<(TxOutput, PtxAdditionalInfo), ControllerError> where T: NodeInterface, { match &utxo { - TxOutput::Burn(value) - | TxOutput::Transfer(value, _) - | TxOutput::LockThenTransfer(value, _, _) - | TxOutput::Htlc(value, _) => { - let additional_info = fetch_token_extra_info(rpc_client, value).await?; - Ok((utxo, additional_info)) - } - TxOutput::CreateOrder(order) => { - let ask_info = fetch_token_extra_info(rpc_client, order.ask()).await?; - let give_info = fetch_token_extra_info(rpc_client, order.give()).await?; - let additional_info = ask_info.join(give_info); - Ok((utxo, additional_info)) - } TxOutput::ProduceBlockFromStake(_, pool_id) => { - let additional_infos = rpc_client + let ptx_additional_infos = rpc_client .get_staker_balance(*pool_id) .await .map_err(ControllerError::NodeCallError)? .map(|staker_balance| { - TxAdditionalInfo::new() + PtxAdditionalInfo::new() .with_pool_info(*pool_id, PoolAdditionalInfo { staker_balance }) }) .ok_or(WalletError::UnknownPoolId(*pool_id))?; - Ok((utxo, additional_infos)) + Ok((utxo, ptx_additional_infos)) } - TxOutput::IssueNft(_, _, _) + TxOutput::Burn(_) + | TxOutput::Transfer(_, _) + | TxOutput::LockThenTransfer(_, _, _) + | TxOutput::Htlc(_, _) + | TxOutput::CreateOrder(_) + | TxOutput::IssueNft(_, _, _) | TxOutput::IssueFungibleToken(_) | TxOutput::CreateStakePool(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::CreateDelegationId(_, _) - | TxOutput::DataDeposit(_) => Ok((utxo, TxAdditionalInfo::new())), + | TxOutput::DataDeposit(_) => Ok((utxo, PtxAdditionalInfo::new())), } } @@ -286,7 +259,7 @@ pub async fn tx_to_partially_signed_tx( ); } - let (input_utxos, additional_infos, destinations) = fetch_input_infos( + let (input_utxos, ptx_additional_info, destinations) = fetch_input_infos( rpc_client, wallet, tx.inputs().iter().enumerate().map(|(idx, inp)| { @@ -300,24 +273,13 @@ pub async fn tx_to_partially_signed_tx( let num_inputs = tx.inputs().len(); - let tasks: FuturesOrdered<_> = tx - .outputs() - .iter() - .map(|out| fetch_utxo_extra_info(rpc_client, out.clone())) - .collect(); - let additional_infos = tasks - .try_collect::>() - .await? - .into_iter() - .fold(additional_infos, |acc, (_, info)| acc.join(info)); - - let ptx = PartiallySignedTransaction::new( + let ptx = PartiallySignedTransaction::new_for_wallet( tx, vec![None; num_inputs], input_utxos, destinations, htlc_secrets, - additional_infos, + ptx_additional_info, )?; Ok(ptx) } @@ -329,7 +291,7 @@ pub async fn fetch_input_infos( ) -> Result< ( Vec>, - TxAdditionalInfo, + PtxAdditionalInfo, Vec>, ), ControllerError, @@ -340,9 +302,9 @@ pub async fn fetch_input_infos( into_utxo_and_destination(rpc_client, wallet, inp, htpc_spend_cond) }) .collect(); - let (input_utxos, additional_infos, destinations) = + let (input_utxos, ptx_additional_info, destinations) = tasks.try_collect::>().await?.into_iter().fold( - (Vec::new(), TxAdditionalInfo::new(), Vec::new()), + (Vec::new(), PtxAdditionalInfo::new(), Vec::new()), |(mut input_utxos, additional_info, mut destinations), (x, y, z)| { input_utxos.push(x); let additional_info = additional_info.join(y); @@ -351,7 +313,7 @@ pub async fn fetch_input_infos( }, ); - Ok((input_utxos, additional_infos, destinations)) + Ok((input_utxos, ptx_additional_info, destinations)) } async fn into_utxo_and_destination( @@ -359,23 +321,23 @@ async fn into_utxo_and_destination( wallet: &RuntimeWallet, tx_inp: &TxInput, htlc_spending_condition: HtlcSpendingCondition, -) -> Result<(Option, TxAdditionalInfo, Option), ControllerError> { +) -> Result<(Option, PtxAdditionalInfo, Option), ControllerError> { Ok(match tx_inp { TxInput::Utxo(outpoint) => { let (utxo, dest) = fetch_utxo_and_destination(rpc_client, wallet, outpoint, htlc_spending_condition) .await?; - let (utxo, additional_infos) = fetch_utxo_extra_info(rpc_client, utxo).await?; - (Some(utxo), additional_infos, Some(dest)) + let (utxo, ptx_additional_infos) = fetch_utxo_extra_info(rpc_client, utxo).await?; + (Some(utxo), ptx_additional_infos, Some(dest)) } TxInput::Account(acc_outpoint) => { let dest = wallet.find_account_destination(acc_outpoint); - (None, TxAdditionalInfo::new(), dest) + (None, PtxAdditionalInfo::new(), dest) } TxInput::AccountCommand(_, cmd) => { let dest = wallet.find_account_command_destination(cmd); - let additional_infos = match cmd { + let ptx_additional_infos = match cmd { AccountCommand::FillOrder(order_id, _, _) | AccountCommand::ConcludeOrder(order_id) => { fetch_order_additional_info(rpc_client, *order_id).await? @@ -386,14 +348,14 @@ async fn into_utxo_and_destination( | AccountCommand::UnfreezeToken(_) | AccountCommand::LockTokenSupply(_) | AccountCommand::ChangeTokenAuthority(_, _) - | AccountCommand::ChangeTokenMetadataUri(_, _) => TxAdditionalInfo::new(), + | AccountCommand::ChangeTokenMetadataUri(_, _) => PtxAdditionalInfo::new(), }; - (None, additional_infos, dest) + (None, ptx_additional_infos, dest) } TxInput::OrderAccountCommand(cmd) => { let dest = wallet.find_order_account_command_destination(cmd); - let additional_infos = match cmd { + let ptx_additional_info = match cmd { OrderAccountCommand::FillOrder(order_id, _) | OrderAccountCommand::FreezeOrder(order_id) | OrderAccountCommand::ConcludeOrder(order_id) => { @@ -401,7 +363,7 @@ async fn into_utxo_and_destination( } }; - (None, additional_infos, dest) + (None, ptx_additional_info, dest) } }) } @@ -409,7 +371,7 @@ async fn into_utxo_and_destination( async fn fetch_order_additional_info( rpc_client: &T, order_id: OrderId, -) -> Result> { +) -> Result> { let order_info = rpc_client .get_order_info(order_id) .await @@ -418,32 +380,75 @@ async fn fetch_order_additional_info( order_id, )))?; - let ask_token_info = fetch_token_extra_info( - rpc_client, - &Currency::from_rpc_output_value(&order_info.initially_asked) - .into_output_value(order_info.ask_balance), - ) - .await?; - let give_token_info = fetch_token_extra_info( - rpc_client, - &Currency::from_rpc_output_value(&order_info.initially_given) - .into_output_value(order_info.give_balance), - ) - .await?; + Ok(PtxAdditionalInfo::new().with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.into(), + initially_given: order_info.initially_given.into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + )) +} - let result = - ask_token_info - .join(give_token_info) - .join(TxAdditionalInfo::new().with_order_info( - order_id, - OrderAdditionalInfo { - initially_asked: order_info.initially_asked.into(), - initially_given: order_info.initially_given.into(), - ask_balance: order_info.ask_balance, - give_balance: order_info.give_balance, - }, - )); - Ok(result) +pub fn get_referenced_token_ids_from_partially_signed_transaction( + ptx: &PartiallySignedTransaction, +) -> BTreeSet { + let mut result = BTreeSet::new(); + collect_referenced_token_ids_from_ptx(ptx, &mut result); + result +} + +fn collect_referenced_token_ids_from_ptx( + ptx: &PartiallySignedTransaction, + dest: &mut BTreeSet, +) { + for input_utxo in ptx.input_utxos().iter().flatten() { + collect_referenced_token_ids_from_tx_output(input_utxo, dest); + } + + for tx_output in ptx.tx().outputs() { + collect_referenced_token_ids_from_tx_output(tx_output, dest); + } + + for (_, order_info) in ptx.additional_info().order_info_iter() { + if let Some(token_id) = order_info.initially_asked.token_v1_id() { + dest.insert(*token_id); + } + + if let Some(token_id) = order_info.initially_given.token_v1_id() { + dest.insert(*token_id); + } + } +} + +fn collect_referenced_token_ids_from_tx_output(utxo: &TxOutput, dest: &mut BTreeSet) { + match utxo { + TxOutput::Burn(value) + | TxOutput::Transfer(value, _) + | TxOutput::LockThenTransfer(value, _, _) + | TxOutput::Htlc(value, _) => { + if let Some(token_id) = value.token_v1_id() { + dest.insert(*token_id); + } + } + TxOutput::CreateOrder(order) => { + if let Some(token_id) = order.ask().token_v1_id() { + dest.insert(*token_id); + } + + if let Some(token_id) = order.give().token_v1_id() { + dest.insert(*token_id); + } + } + TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::IssueNft(_, _, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::CreateStakePool(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DataDeposit(_) => {} + } } #[cfg(test)] diff --git a/wallet/wallet-controller/src/helpers/tests.rs b/wallet/wallet-controller/src/helpers/tests.rs index 143b41e206..eaad6fe667 100644 --- a/wallet/wallet-controller/src/helpers/tests.rs +++ b/wallet/wallet-controller/src/helpers/tests.rs @@ -13,7 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, num::NonZeroU8, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZeroU8, + sync::Arc, +}; use itertools::Itertools as _; use rstest::rstest; @@ -51,20 +55,23 @@ use wallet::{ use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, partially_signed_transaction::{ - OrderAdditionalInfo, PoolAdditionalInfo, TokenAdditionalInfo, TxAdditionalInfo, + OrderAdditionalInfo, PoolAdditionalInfo, PtxAdditionalInfo, TokenAdditionalInfo, + TokensAdditionalInfo, }, }; use crate::{ tests::test_utils::{ create_block_scan_wallet, random_is_token_unfreezable, random_nft_issuance, - random_order_currencies_with_token, random_pub_key, - random_rpc_ft_info_with_id_ticker_decimals, random_rpc_is_token_frozen, + random_order_currencies_with_token, random_pub_key, random_rpc_is_token_frozen, random_token_data_with_id_and_authority, random_vrf_pub_key, tx_with_outputs, wallet_new_dest, OrderCurrencies, TestOrderData, TestTokenData, MNEMONIC, }, { - helpers::{fetch_utxo, tx_to_partially_signed_tx}, + helpers::{ + fetch_token_infos, fetch_utxo, + get_referenced_token_ids_from_partially_signed_transaction, tx_to_partially_signed_tx, + }, runtime_wallet::RuntimeWallet, }, }; @@ -79,17 +86,9 @@ mod tx_to_partially_signed_tx_general_test { async fn test(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); - let random_tokens_count = 15; - let random_tokens = (0..random_tokens_count) - .map(|_| { - use crate::tests::test_utils::random_token_data_with_id_and_authority; - - random_token_data_with_id_and_authority( - TokenId::random_using(&mut rng), - Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)), - &mut rng, - ) - }) + let random_token_ids_count = 15; + let random_token_ids = (0..random_token_ids_count) + .map(|_| TokenId::random_using(&mut rng)) .collect_vec(); let chain_config = Arc::new(create_regtest()); @@ -99,7 +98,7 @@ mod tx_to_partially_signed_tx_general_test { // Transfer to a destination belonging to the wallet. let token0_transfer_utxo_dest = wallet_new_dest(&mut wallet); let token0_transfer_utxo = TxOutput::Transfer( - OutputValue::TokenV1(random_tokens[0].id, Amount::from_atoms(rng.gen())), + OutputValue::TokenV1(random_token_ids[0], Amount::from_atoms(rng.gen())), token0_transfer_utxo_dest.clone(), ); let tx_with_token0_transfer = tx_with_outputs(vec![token0_transfer_utxo.clone()]); @@ -110,7 +109,7 @@ mod tx_to_partially_signed_tx_general_test { let token1_transfer_utxo_dest = Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)); let token1_transfer_utxo = TxOutput::Transfer( - OutputValue::TokenV1(random_tokens[1].id, Amount::from_atoms(rng.gen())), + OutputValue::TokenV1(random_token_ids[1], Amount::from_atoms(rng.gen())), token1_transfer_utxo_dest.clone(), ); let token1_transfer_outpoint = @@ -119,7 +118,7 @@ mod tx_to_partially_signed_tx_general_test { let lock_then_transfer_utxo_dest = Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)); let lock_then_transfer_utxo = TxOutput::LockThenTransfer( - OutputValue::TokenV1(random_tokens[2].id, Amount::from_atoms(rng.gen())), + OutputValue::TokenV1(random_token_ids[2], Amount::from_atoms(rng.gen())), lock_then_transfer_utxo_dest.clone(), OutputTimeLock::ForBlockCount(rng.gen()), ); @@ -187,11 +186,11 @@ mod tx_to_partially_signed_tx_general_test { &mut blocks, &chain_config, &[ - random_order_currencies_with_token(&mut rng, random_tokens[3].id), - random_order_currencies_with_token(&mut rng, random_tokens[4].id), - random_order_currencies_with_token(&mut rng, random_tokens[5].id), - random_order_currencies_with_token(&mut rng, random_tokens[6].id), - random_order_currencies_with_token(&mut rng, random_tokens[7].id), + random_order_currencies_with_token(&mut rng, random_token_ids[3]), + random_order_currencies_with_token(&mut rng, random_token_ids[4]), + random_order_currencies_with_token(&mut rng, random_token_ids[5]), + random_order_currencies_with_token(&mut rng, random_token_ids[6]), + random_order_currencies_with_token(&mut rng, random_token_ids[7]), ], &mut wallet, &mut rng, @@ -243,7 +242,7 @@ mod tx_to_partially_signed_tx_general_test { // Note: the wallet doesn't check that the secret and the secret hash are consistent. let htlc_secret = HtlcSecret::new_from_rng(&mut rng); let create_htlc_utxo = TxOutput::Htlc( - OutputValue::TokenV1(random_tokens[8].id, Amount::from_atoms(rng.gen())), + OutputValue::TokenV1(random_token_ids[8], Amount::from_atoms(rng.gen())), Box::new(HashedTimelockContract { secret_hash: HtlcSecretHash::random_using(&mut rng), spend_key: htlc_spend_key.clone(), @@ -506,16 +505,16 @@ mod tx_to_partially_signed_tx_general_test { let outputs = vec![ TxOutput::Transfer( - OutputValue::TokenV1(random_tokens[9].id, Amount::from_atoms(rng.r#gen())), + OutputValue::TokenV1(random_token_ids[9], Amount::from_atoms(rng.r#gen())), Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)), ), TxOutput::LockThenTransfer( - OutputValue::TokenV1(random_tokens[10].id, Amount::from_atoms(rng.r#gen())), + OutputValue::TokenV1(random_token_ids[10], Amount::from_atoms(rng.r#gen())), Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)), OutputTimeLock::ForBlockCount(rng.r#gen()), ), TxOutput::Burn(OutputValue::TokenV1( - random_tokens[11].id, + random_token_ids[11], Amount::from_atoms(rng.r#gen()), )), TxOutput::CreateStakePool( @@ -552,7 +551,7 @@ mod tx_to_partially_signed_tx_general_test { ), TxOutput::DataDeposit(gen_random_bytes(&mut rng, 10, 20)), TxOutput::Htlc( - OutputValue::TokenV1(random_tokens[12].id, Amount::from_atoms(rng.r#gen())), + OutputValue::TokenV1(random_token_ids[12], Amount::from_atoms(rng.r#gen())), Box::new(HashedTimelockContract { secret_hash: HtlcSecretHash::random_using(&mut rng), spend_key: Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)), @@ -562,8 +561,8 @@ mod tx_to_partially_signed_tx_general_test { ), TxOutput::CreateOrder(Box::new(OrderData::new( Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)), - OutputValue::TokenV1(random_tokens[13].id, Amount::from_atoms(rng.r#gen())), - OutputValue::TokenV1(random_tokens[14].id, Amount::from_atoms(rng.r#gen())), + OutputValue::TokenV1(random_token_ids[13], Amount::from_atoms(rng.r#gen())), + OutputValue::TokenV1(random_token_ids[14], Amount::from_atoms(rng.r#gen())), ))), ]; @@ -588,14 +587,6 @@ mod tx_to_partially_signed_tx_general_test { ), ]); - // Note: the "wallet" token infos won't be queried, because those tokens - // are only used by token-related AccountCommand's, for which we don't collect - // TokenAdditionalInfo's currently. - let token_infos_to_return = random_tokens - .iter() - .map(|token_data| (token_data.id, make_rpc_token_info(token_data, &mut rng))) - .collect::>(); - let order_infos_to_return = wallet_orders .iter() .map(|order_data| (order_data.id, make_rpc_order_info(order_data, &mut rng))) @@ -642,10 +633,6 @@ mod tx_to_partially_signed_tx_general_test { Ok(Some(utxos_to_return.get(&outpoint).unwrap().clone())) }); - node_mock.expect_get_token_info().returning(move |token_id| { - Ok(Some(token_infos_to_return.get(&token_id).unwrap().clone())) - }); - node_mock.expect_get_order_info().returning(move |token_id| { Ok(Some(order_infos_to_return.get(&token_id).unwrap().clone())) }); @@ -681,20 +668,8 @@ mod tx_to_partially_signed_tx_general_test { assert_eq!(ptx.destinations(), &expected_inputs_destinations); assert_eq!(ptx.htlc_secrets(), &htlc_secrets); - let expected_tx_additional_info = { - let mut info = TxAdditionalInfo::new(); - - // Note: the "wallet" tokens are only used by token-related AccountCommand's, for which - // we don't collect TokenAdditionalInfo's currently. So we don't append them here. - for token in random_tokens { - info = info.with_token_info( - token.id, - TokenAdditionalInfo { - num_decimals: token.num_decimals, - ticker: token.ticker.into_bytes(), - }, - ) - } + let expected_ptx_additional_info = { + let mut info = PtxAdditionalInfo::new(); for order in wallet_orders { info = info.with_order_info( @@ -724,7 +699,14 @@ mod tx_to_partially_signed_tx_general_test { }, ) }; - assert_eq!(ptx.additional_info(), &expected_tx_additional_info); + + assert_eq!(ptx.additional_info(), &expected_ptx_additional_info); + + // Note: the "wallet" tokens are only used by token-related AccountCommand's, for which + // we don't collect token ids currently. So we don't append them here. + let expected_token_ids = random_token_ids.into_iter().collect::>(); + let actual_token_ids = get_referenced_token_ids_from_partially_signed_transaction(&ptx); + assert_eq!(actual_token_ids, expected_token_ids); } // Make blocks with txs that issue tokens with authority destinations belonging to the wallet. @@ -844,20 +826,6 @@ mod tx_to_partially_signed_tx_general_test { result } - fn make_rpc_token_info(data: &TestTokenData, rng: &mut impl Rng) -> RPCTokenInfo { - RPCTokenInfo::FungibleToken(RPCFungibleTokenInfo { - token_id: data.id, - token_ticker: data.ticker.clone().into(), - number_of_decimals: data.num_decimals, - metadata_uri: data.metadata_uri.clone().into(), - circulating_supply: Amount::from_atoms(rng.gen()), - total_supply: data.total_supply.into(), - is_locked: rng.gen(), - frozen: random_rpc_is_token_frozen(rng), - authority: data.authority.clone(), - }) - } - fn make_rpc_order_info(data: &TestOrderData, rng: &mut impl Rng) -> RpcOrderInfo { RpcOrderInfo { conclude_key: data.conclude_key.clone(), @@ -938,9 +906,6 @@ async fn tx_to_partially_signed_tx_htlc_input_with_known_utxo_test( ); let last_height = 1; - let token_num_decimals = rng.gen_range(1..20); - let token_ticker = gen_random_alnum_string(&mut rng, 5, 10); - let node_mock = { let mut node_mock = MockNodeInterface::new(); @@ -955,16 +920,6 @@ async fn tx_to_partially_signed_tx_htlc_input_with_known_utxo_test( BTreeMap::from([(create_htlc_outpoint.clone(), create_htlc_output.clone())]) }; - let token_infos_to_return = BTreeMap::from([( - token_id, - RPCTokenInfo::FungibleToken(random_rpc_ft_info_with_id_ticker_decimals( - token_id, - token_ticker.clone(), - token_num_decimals, - &mut rng, - )), - )]); - let chain_info_to_return = ChainInfo { best_block_height: BlockHeight::new(last_height), best_block_id: last_block.get_id().into(), @@ -977,10 +932,6 @@ async fn tx_to_partially_signed_tx_htlc_input_with_known_utxo_test( .expect_get_utxo() .returning(move |outpoint| Ok(Some(utxos_to_return.get(&outpoint).unwrap().clone()))); - node_mock.expect_get_token_info().returning(move |token_id| { - Ok(Some(token_infos_to_return.get(&token_id).unwrap().clone())) - }); - node_mock .expect_chainstate_info() .returning(move || Ok(chain_info_to_return.clone())); @@ -1016,19 +967,80 @@ async fn tx_to_partially_signed_tx_htlc_input_with_known_utxo_test( assert_eq!(ptx.input_utxos(), &expected_inputs_utxos); assert_eq!(ptx.destinations(), &expected_inputs_destinations); assert_eq!(ptx.htlc_secrets(), &htlc_secrets); - assert_eq!( - ptx.additional_info(), - &TxAdditionalInfo::new().with_token_info( - token_id, - TokenAdditionalInfo { - num_decimals: token_num_decimals, - ticker: token_ticker.clone().into_bytes() - } - ) - ); + assert_eq!(*ptx.additional_info(), PtxAdditionalInfo::new()); + + let expected_token_ids = BTreeSet::from([token_id]); + let actual_token_ids = get_referenced_token_ids_from_partially_signed_transaction(&ptx); + assert_eq!(actual_token_ids, expected_token_ids); // Also call fetch_utxo for the same outpoint; the expectations are exactly the same: // if the wallet has cached the utxo, node interface should not be queried. let fetched_utxo = fetch_utxo(&node_mock, &wallet, &create_htlc_outpoint).await.unwrap(); assert_eq!(fetched_utxo, create_htlc_output); } + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[trace] +#[tokio::test] +async fn fetch_token_infos_test(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let tokens_count = rng.gen_range(10..20); + let tokens_data = (0..tokens_count) + .map(|_| { + use crate::tests::test_utils::random_token_data_with_id_and_authority; + + random_token_data_with_id_and_authority( + TokenId::random_using(&mut rng), + Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)), + &mut rng, + ) + }) + .collect_vec(); + let token_ids = tokens_data.iter().map(|token| token.id).collect::>(); + + let mut node_mock = MockNodeInterface::new(); + + let token_infos_to_return = tokens_data + .iter() + .map(|token_data| (token_data.id, make_rpc_token_info(token_data, &mut rng))) + .collect::>(); + + node_mock + .expect_get_token_info() + .returning(move |token_id| Ok(Some(token_infos_to_return.get(&token_id).unwrap().clone()))); + + let expected_tokens_info = { + let mut info = TokensAdditionalInfo::new(); + + for token in tokens_data { + info = info.with_info( + token.id, + TokenAdditionalInfo { + num_decimals: token.num_decimals, + ticker: token.ticker.into_bytes(), + }, + ) + } + + info + }; + let actual_token_infos = fetch_token_infos(&node_mock, &token_ids).await.unwrap(); + assert_eq!(actual_token_infos, expected_tokens_info); +} + +fn make_rpc_token_info(data: &TestTokenData, rng: &mut impl Rng) -> RPCTokenInfo { + RPCTokenInfo::FungibleToken(RPCFungibleTokenInfo { + token_id: data.id, + token_ticker: data.ticker.clone().into(), + number_of_decimals: data.num_decimals, + metadata_uri: data.metadata_uri.clone().into(), + circulating_supply: Amount::from_atoms(rng.gen()), + total_supply: data.total_supply.into(), + is_locked: rng.gen(), + frozen: random_rpc_is_token_frozen(rng), + authority: data.authority.clone(), + }) +} diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 1ca5daddcc..01c0ae7a5f 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -111,7 +111,8 @@ use wallet_types::{ hw_data::HardwareWalletFullInfo, partially_signed_transaction::{ make_sighash_input_commitments, PartiallySignedTransaction, - PartiallySignedTransactionError, SighashInputCommitmentCreationError, TxAdditionalInfo, + PartiallySignedTransactionError, PartiallySignedTransactionWalletExt as _, + PtxAdditionalInfo, SighashInputCommitmentCreationError, }, signature_status::SignatureStatus, wallet_type::{WalletControllerMode, WalletType}, @@ -1196,27 +1197,27 @@ where .collect::, WalletError>>() .map_err(ControllerError::WalletError)?; - let (input_utxos, additional_infos) = + let (input_utxos, ptx_additional_info) = self.fetch_utxos_extra_info(input_utxos).await?.into_iter().fold( - (Vec::new(), TxAdditionalInfo::new()), + (Vec::new(), PtxAdditionalInfo::new()), |(mut input_utxos, additional_info), (x, y)| { input_utxos.push(x); (input_utxos, additional_info.join(y)) }, ); - let additional_infos = self + let ptx_additional_info = self .fetch_utxos_extra_info(tx.outputs().to_vec()) .await? .into_iter() - .fold(additional_infos, |acc, (_, info)| acc.join(info)); - let tx = PartiallySignedTransaction::new( + .fold(ptx_additional_info, |acc, (_, info)| acc.join(info)); + let tx = PartiallySignedTransaction::new_for_wallet( tx, vec![None; num_inputs], input_utxos.into_iter().map(Option::Some).collect(), destinations.into_iter().map(Option::Some).collect(), htlc_secrets, - additional_infos, + ptx_additional_info, )?; TransactionToSign::Partial(tx) @@ -1303,7 +1304,7 @@ where async fn fetch_utxos_extra_info( &self, inputs: Vec, - ) -> Result, ControllerError> { + ) -> Result, ControllerError> { let tasks: FuturesOrdered<_> = inputs .into_iter() .map(|input| fetch_utxo_extra_info(&self.rpc_client, input)) diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs index af060246ab..cb583997a4 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -53,7 +53,9 @@ use wallet::{ use wallet_types::{ account_info::{StandaloneAddressDetails, StandaloneAddresses}, hw_data::HardwareWalletFullInfo, - partially_signed_transaction::{PartiallySignedTransaction, TxAdditionalInfo}, + partially_signed_transaction::{ + PartiallySignedTransaction, PtxAdditionalInfo, TokensAdditionalInfo, TxAdditionalInfo, + }, seed_phrase::SerializableSeedPhrase, signature_status::SignatureStatus, utxo_types::{UtxoState, UtxoStates, UtxoTypes}, @@ -984,7 +986,7 @@ impl RuntimeWallet { change_addresses: BTreeMap>, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - additional_info: TxAdditionalInfo, + ptx_additional_info: PtxAdditionalInfo, ) -> WalletResult<(PartiallySignedTransaction, BTreeMap)> { match self { RuntimeWallet::Software(w) => w.create_unsigned_transaction_to_addresses( @@ -995,7 +997,7 @@ impl RuntimeWallet { change_addresses, current_fee_rate, consolidate_fee_rate, - additional_info, + ptx_additional_info, ), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.create_unsigned_transaction_to_addresses( @@ -1006,7 +1008,7 @@ impl RuntimeWallet { change_addresses, current_fee_rate, consolidate_fee_rate, - additional_info, + ptx_additional_info, ), } } @@ -1312,15 +1314,20 @@ impl RuntimeWallet { &mut self, account_index: U31, ptx: PartiallySignedTransaction, + tokens_additional_info: &TokensAdditionalInfo, ) -> WalletResult<( PartiallySignedTransaction, Vec, Vec, )> { match self { - RuntimeWallet::Software(w) => w.sign_raw_transaction(account_index, ptx), + RuntimeWallet::Software(w) => { + w.sign_raw_transaction(account_index, ptx, tokens_additional_info) + } #[cfg(feature = "trezor")] - RuntimeWallet::Trezor(w) => w.sign_raw_transaction(account_index, ptx), + RuntimeWallet::Trezor(w) => { + w.sign_raw_transaction(account_index, ptx, tokens_additional_info) + } } } diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index c5b901bdff..f11b5b7a17 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -59,7 +59,8 @@ use wallet::{ }; use wallet_types::{ partially_signed_transaction::{ - OrderAdditionalInfo, PartiallySignedTransaction, TokenAdditionalInfo, TxAdditionalInfo, + OrderAdditionalInfo, PartiallySignedTransaction, PtxAdditionalInfo, TokenAdditionalInfo, + TokensAdditionalInfo, TxAdditionalInfo, }, signature_status::SignatureStatus, utxo_types::{UtxoState, UtxoType}, @@ -69,8 +70,9 @@ use wallet_types::{ use crate::{ helpers::{ - fetch_order_info, fetch_token_info, fetch_token_infos_into_tx_info, fetch_utxo, - into_balances, tx_to_partially_signed_tx, + fetch_order_info, fetch_token_info, fetch_token_infos, fetch_token_infos_into, fetch_utxo, + get_referenced_token_ids_from_partially_signed_transaction, into_balances, + tx_to_partially_signed_tx, }, runtime_wallet::RuntimeWallet, types::{ @@ -164,9 +166,9 @@ where async fn filter_out_utxos_with_frozen_tokens( &self, input_utxos: Vec<(UtxoOutPoint, TxOutput)>, - ) -> Result<(Vec<(UtxoOutPoint, TxOutput)>, TxAdditionalInfo), ControllerError> { + ) -> Result<(Vec<(UtxoOutPoint, TxOutput)>, TokensAdditionalInfo), ControllerError> { let mut result = vec![]; - let mut additional_info = TxAdditionalInfo::new(); + let mut additional_info = TokensAdditionalInfo::new(); for utxo in input_utxos { let token_ids = get_referenced_token_ids_ignore_issuance(&utxo.1); if token_ids.is_empty() { @@ -196,7 +198,7 @@ where if ok_to_use { result.push(utxo); for token_info in token_infos { - additional_info.add_token_info( + additional_info.add_info( token_info.token_id(), TokenAdditionalInfo { num_decimals: token_info.token_number_of_decimals(), @@ -590,7 +592,7 @@ where WithLocked::Unlocked, )?; - let (inputs, additional_info) = + let (inputs, tokens_additional_info) = self.filter_out_utxos_with_frozen_tokens(selected_utxos).await?; let filtered_inputs = inputs @@ -611,7 +613,10 @@ where destination_address, filtered_inputs, current_fee_rate, - additional_info, + TxAdditionalInfo { + ptx_additional_info: PtxAdditionalInfo::new(), + tokens_additional_info, + }, ) }, ) @@ -700,7 +705,7 @@ where [(Currency::Coin, change_address)].into(), current_fee_rate, consolidate_fee_rate, - TxAdditionalInfo::new(), + PtxAdditionalInfo::new(), ) .map_err(ControllerError::WalletError)?; @@ -744,19 +749,11 @@ where ControllerError::::ExpectingNonEmptyOutputs ); - let (outputs, additional_info) = { + let outputs = { let mut result = Vec::new(); - let mut additional_info = TxAdditionalInfo::new(); for (token_id, outputs_vec) in outputs { let token_info = fetch_token_info(&self.rpc_client, token_id).await?; - additional_info.add_token_info( - token_id, - TokenAdditionalInfo { - num_decimals: token_info.token_number_of_decimals(), - ticker: token_info.token_ticker().to_vec(), - }, - ); match &token_info { RPCTokenInfo::FungibleToken(token_info) => { @@ -774,7 +771,7 @@ where .map_err(ControllerError::InvalidTxOutput)?; } - (result, additional_info) + result }; let (inputs, change_addresses) = { @@ -852,7 +849,7 @@ where change_addresses, current_fee_rate, consolidate_fee_rate, - additional_info, + PtxAdditionalInfo::new(), )?; let fees = into_balances(&self.rpc_client, self.chain_config, fees).await?; @@ -1128,8 +1125,13 @@ where htlc: HashedTimelockContract, ) -> Result> { let mut tx_additional_info = TxAdditionalInfo::new(); - let output_value = - self.convert_rpc_amount_in(amount, token_id, &mut tx_additional_info).await?; + let output_value = self + .convert_rpc_amount_in( + amount, + token_id, + &mut tx_additional_info.tokens_additional_info, + ) + .await?; let (current_fee_rate, consolidate_fee_rate) = self.get_current_and_consolidation_fee_rate().await?; @@ -1165,7 +1167,12 @@ where (amount, Some(token_id)) } }; - self.convert_rpc_amount_in(amount, token_id, &mut tx_additional_info).await + self.convert_rpc_amount_in( + amount, + token_id, + &mut tx_additional_info.tokens_additional_info, + ) + .await }; let ask_value = convert_value(ask_value).await?; @@ -1292,7 +1299,7 @@ where &self, amount: RpcAmountIn, token_id: Option, - tx_additional_info: &mut TxAdditionalInfo, + tokens_additional_info: &mut TokensAdditionalInfo, ) -> Result> { let output_value = match token_id { Some(token_id) => { @@ -1300,7 +1307,7 @@ where let amount = amount .to_amount(token_info.token_number_of_decimals()) .ok_or(ControllerError::InvalidCoinAmount)?; - tx_additional_info.add_token_info( + tokens_additional_info.add_info( token_id, TokenAdditionalInfo { num_decimals: token_info.token_number_of_decimals(), @@ -1338,10 +1345,10 @@ where let token1_id = order_info.initially_asked.token_id().cloned(); let token2_id = order_info.initially_given.token_id().cloned(); - fetch_token_infos_into_tx_info( + fetch_token_infos_into( &self.rpc_client, - token1_id.into_iter().chain(token2_id.into_iter()), - &mut tx_info, + &token1_id.into_iter().chain(token2_id.into_iter()).collect(), + &mut tx_info.tokens_additional_info, ) .await?; @@ -1383,8 +1390,12 @@ where } }; + let referenced_token_ids = get_referenced_token_ids_from_partially_signed_transaction(&ptx); + let tokens_additional_info = + fetch_token_infos(&self.rpc_client, &referenced_token_ids).await?; + self.wallet - .sign_raw_transaction(self.account_index, ptx) + .sign_raw_transaction(self.account_index, ptx, &tokens_additional_info) .map_err(ControllerError::WalletError) } diff --git a/wallet/wallet-controller/src/tests/compose_transaction_tests.rs b/wallet/wallet-controller/src/tests/compose_transaction_tests.rs index a34f24d740..3dfe5fa7de 100644 --- a/wallet/wallet-controller/src/tests/compose_transaction_tests.rs +++ b/wallet/wallet-controller/src/tests/compose_transaction_tests.rs @@ -13,7 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::Arc, +}; use itertools::Itertools as _; use rstest::rstest; @@ -42,9 +45,10 @@ use wallet::{ account::TransactionToSign, wallet::test_helpers::create_wallet_with_mnemonic, wallet_events::WalletEventsNoOp, }; -use wallet_types::partially_signed_transaction::{TokenAdditionalInfo, TxAdditionalInfo}; +use wallet_types::partially_signed_transaction::PtxAdditionalInfo; use crate::{ + helpers::get_referenced_token_ids_from_partially_signed_transaction, runtime_wallet::RuntimeWallet, tests::test_utils::{ assert_fees, create_block_scan_wallet, random_rpc_ft_info_with_id_ticker_decimals, @@ -101,12 +105,6 @@ async fn general_test(#[case] seed: Seed, #[case] use_htlc_secret: bool) { let token2_outpoint = UtxoOutPoint::new(Id::::random_using(&mut rng).into(), rng.gen()); - let token1_num_decimals = rng.gen_range(1..20); - let token1_ticker = gen_random_alnum_string(&mut rng, 5, 10); - let token2_num_decimals = rng.gen_range(1..20); - let token2_ticker = gen_random_alnum_string(&mut rng, 5, 10); - let token3_num_decimals = rng.gen_range(1..20); - let token3_ticker = gen_random_alnum_string(&mut rng, 5, 10); let token4_num_decimals = rng.gen_range(1..20); let token4_ticker = gen_random_alnum_string(&mut rng, 5, 10); @@ -154,44 +152,15 @@ async fn general_test(#[case] seed: Seed, #[case] use_htlc_secret: bool) { (create_htlc_outpoint.clone(), create_htlc_output.clone()), ]); - let token_infos_to_return = BTreeMap::from([ - ( - token1_id, - RPCTokenInfo::FungibleToken(random_rpc_ft_info_with_id_ticker_decimals( - token1_id, - token1_ticker.clone(), - token1_num_decimals, - &mut rng, - )), - ), - ( - token2_id, - RPCTokenInfo::FungibleToken(random_rpc_ft_info_with_id_ticker_decimals( - token2_id, - token2_ticker.clone(), - token2_num_decimals, - &mut rng, - )), - ), - ( - token3_id, - RPCTokenInfo::FungibleToken(random_rpc_ft_info_with_id_ticker_decimals( - token3_id, - token3_ticker.clone(), - token3_num_decimals, - &mut rng, - )), - ), - ( + let token_infos_to_return = BTreeMap::from([( + token4_id, + RPCTokenInfo::FungibleToken(random_rpc_ft_info_with_id_ticker_decimals( token4_id, - RPCTokenInfo::FungibleToken(random_rpc_ft_info_with_id_ticker_decimals( - token4_id, - token4_ticker.clone(), - token4_num_decimals, - &mut rng, - )), - ), - ]); + token4_ticker.clone(), + token4_num_decimals, + &mut rng, + )), + )]); let chain_info_to_return = ChainInfo { best_block_height: BlockHeight::new(last_height), @@ -286,36 +255,9 @@ async fn general_test(#[case] seed: Seed, #[case] use_htlc_secret: bool) { ); assert_eq!(composed_tx.destinations(), &expected_inputs_destinations); assert_eq!(composed_tx.htlc_secrets(), &htlc_secrets); - assert_eq!( - composed_tx.additional_info(), - &TxAdditionalInfo::new() - .with_token_info( - token1_id, - TokenAdditionalInfo { - num_decimals: token1_num_decimals, - ticker: token1_ticker.into_bytes() - } - ) - .with_token_info( - token2_id, - TokenAdditionalInfo { - num_decimals: token2_num_decimals, - ticker: token2_ticker.into_bytes() - } - ) - .with_token_info( - token3_id, - TokenAdditionalInfo { - num_decimals: token3_num_decimals, - ticker: token3_ticker.into_bytes() - } - ) - .with_token_info( - token4_id, - TokenAdditionalInfo { - num_decimals: token4_num_decimals, - ticker: token4_ticker.into_bytes() - } - ) - ); + assert_eq!(composed_tx.additional_info(), &PtxAdditionalInfo::new()); + + let expected_token_ids = BTreeSet::from([token1_id, token2_id, token3_id, token4_id]); + let actual_token_ids = get_referenced_token_ids_from_partially_signed_transaction(&composed_tx); + assert_eq!(actual_token_ids, expected_token_ids); } diff --git a/wasm-wrappers/WASM-API.md b/wasm-wrappers/WASM-API.md index e9908aad45..11a62107d2 100644 --- a/wasm-wrappers/WASM-API.md +++ b/wasm-wrappers/WASM-API.md @@ -271,6 +271,43 @@ and a network type (mainnet, testnet, etc), this function returns a witness to b Given an unsigned transaction and signatures, this function returns a SignedTransaction object as bytes. +### Function: `encode_partially_signed_transaction` + +Return a PartiallySignedTransaction object as bytes. + +`transaction` is an encoded `Transaction` (which can be produced via `encode_transaction`). + +`signatures`, `input_utxos`, `input_destinations` and `htlc_secrets` are encoded lists of +optional objects of the corresponding type. To produce such a list, iterate over your +original list of optional objects and then: +1) emit byte 0 if the current object is null; +2) otherwise emit byte 1 followed by the object in its encoded form. + +Each individual object in each of the lists corresponds to the transaction input with the same +index and its meaning is as follows: + 1) `signatures` - the signature for the input; + 2) `input_utxos`- the utxo for the input (if it's utxo-based); + 3) `input_destinations` - the destination (address) corresponding to the input; this determines + the key(s) with which the input has to be signed. Note that for utxo-based inputs the + corresponding destination can usually be extracted from the utxo itself (the exception + being the `ProduceBlockFromStake` utxo, which doesn't contain the pool's decommission key). + However, PartiallySignedTransaction requires that *all* input destinations are provided + explicitly anyway. + 4) `htlc_secrets` - if the input is an HTLC one and if the transaction is spending the HTLC, + this should be the HTLC secret. Otherwise it should be null. + + The number of items in each list must be equal to the number of transaction inputs. + +`additional_info` has the same meaning as in `encode_witness`. + +### Function: `decode_partially_signed_transaction_to_js` + +Decodes a partially signed transaction from its binary encoding into a JavaScript object. + +### Function: `encode_destination` + +Convert the specified string address into a Destination object, encoded as bytes. + ### Function: `get_transaction_id` Given a `Transaction` encoded in bytes (not a signed transaction, but a signed transaction is tolerated by ignoring the extra bytes, by choice) @@ -338,6 +375,7 @@ Note: instead of `encode_witness`). Note that in orders v0 FillOrder inputs can technically have a signature, it's just not checked. But in orders V1 we actually require that those inputs don't have signatures. + Also, in orders V1 the provided destination is always ignored. ### Function: `encode_input_for_freeze_order` diff --git a/wasm-wrappers/js-bindings-test/tests/main.ts b/wasm-wrappers/js-bindings-test/tests/main.ts index 935c09009d..d7a10c221b 100644 --- a/wasm-wrappers/js-bindings-test/tests/main.ts +++ b/wasm-wrappers/js-bindings-test/tests/main.ts @@ -26,6 +26,7 @@ import { test_misc } from "./test_misc.js"; import { test_orders } from "./test_orders.js"; import { test_signed_transaction_intent } from "./test_signed_transaction_intent.js"; import { test_transaction_and_witness_encoding } from "./test_transaction_and_witness_encoding.js"; +import { test_partially_signed_transaction_encoding } from "./test_partially_signed_transaction_encoding.js"; /** @public */ export function run_all_tests() { @@ -38,4 +39,5 @@ export function run_all_tests() { run_one_test(test_orders); run_one_test(test_signed_transaction_intent); run_one_test(test_transaction_and_witness_encoding); + run_one_test(test_partially_signed_transaction_encoding); } diff --git a/wasm-wrappers/js-bindings-test/tests/test_encode_other_outputs.ts b/wasm-wrappers/js-bindings-test/tests/test_encode_other_outputs.ts index 17cb36915b..31c35914fb 100644 --- a/wasm-wrappers/js-bindings-test/tests/test_encode_other_outputs.ts +++ b/wasm-wrappers/js-bindings-test/tests/test_encode_other_outputs.ts @@ -380,11 +380,11 @@ function issue_fungible_token_test() { } function issue_nft_test() { - const account_pubkey = make_default_account_privkey( + const account_privkey = make_default_account_privkey( MNEMONIC, Network.Testnet ); - const receiving_privkey = make_receiving_address(account_pubkey, 0); + const receiving_privkey = make_receiving_address(account_privkey, 0); const receiving_pubkey = public_key_from_private_key(receiving_privkey); let encoded_nft = encode_output_issue_nft( diff --git a/wasm-wrappers/js-bindings-test/tests/test_htlc.ts b/wasm-wrappers/js-bindings-test/tests/test_htlc.ts index 813710c740..6320e84210 100644 --- a/wasm-wrappers/js-bindings-test/tests/test_htlc.ts +++ b/wasm-wrappers/js-bindings-test/tests/test_htlc.ts @@ -54,11 +54,11 @@ import { } from "./test_encode_other_outputs.js"; export function test_htlc() { - const account_pubkey = make_default_account_privkey( + const account_privkey = make_default_account_privkey( MNEMONIC, Network.Testnet ); - const receiving_privkey = make_receiving_address(account_pubkey, 0); + const receiving_privkey = make_receiving_address(account_privkey, 0); const htlc_coins_output = encode_output_htlc( Amount.from_atoms("40000"), diff --git a/wasm-wrappers/js-bindings-test/tests/test_partially_signed_transaction_encoding.ts b/wasm-wrappers/js-bindings-test/tests/test_partially_signed_transaction_encoding.ts new file mode 100644 index 0000000000..7197a6de1e --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/test_partially_signed_transaction_encoding.ts @@ -0,0 +1,397 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + Amount, + decode_partially_signed_transaction_to_js, + encode_destination, + encode_input_for_conclude_order, + encode_input_for_fill_order, + encode_input_for_utxo, + encode_lock_until_height, + encode_multisig_challenge, + encode_output_lock_then_transfer, + encode_output_htlc, + encode_output_produce_block_from_stake, + encode_output_transfer, + encode_partially_signed_transaction, + encode_transaction, + encode_witness, + make_default_account_privkey, + make_receiving_address, + multisig_challenge_to_address, + Network, + pubkey_to_pubkeyhash_address, + public_key_from_private_key, + SignatureHashType, + TxAdditionalInfo, +} from "../../pkg/wasm_wrappers.js"; + +import { assert_eq_vals } from "./utils.js"; + +import { + ANOTHER_ORDER_ID, + MNEMONIC, + POOL_ID, + ORDER_ID, + SIGHASH_INPUT_COMMITMENTS_V1_TESTNET_FORK_HEIGHT, + TOKEN_ID, + HTLC_SECRET_HASH, + HTLC_SECRET, +} from "./defs.js"; +import { ADDRESS } from "./test_address_generation.js"; + +export function test_partially_signed_transaction_encoding() { + const height = SIGHASH_INPUT_COMMITMENTS_V1_TESTNET_FORK_HEIGHT; + const account_privkey = make_default_account_privkey( + MNEMONIC, + Network.Testnet, + ); + function make_addr(key_index: number) { + const sk = make_receiving_address(account_privkey, key_index); + const pk = public_key_from_private_key(sk); + return pubkey_to_pubkeyhash_address(pk, Network.Testnet) + } + + const produce_block_from_stake_utxo = encode_output_produce_block_from_stake( + POOL_ID, + ADDRESS, + Network.Testnet + ); + const block_outpoint = new Uint8Array(33).fill(1); + const tx_outpoint = new Uint8Array(33).fill(0); + const produce_block_from_stake_input = encode_input_for_utxo(block_outpoint, 1); + + const fill_order_input = encode_input_for_fill_order( + ORDER_ID, + Amount.from_atoms("40000"), + ADDRESS, + BigInt(1), + BigInt(height), + Network.Testnet + ); + + const conclude_order_input = encode_input_for_conclude_order( + ANOTHER_ORDER_ID, + BigInt(1), + BigInt(height), + Network.Testnet + ); + + const transfer_input_sk = make_receiving_address(account_privkey, 0); + const transfer_input_pk = public_key_from_private_key(transfer_input_sk); + const transfer_input_addr = pubkey_to_pubkeyhash_address( + transfer_input_pk, + Network.Testnet + ); + + const transfer_utxo = encode_output_transfer(Amount.from_atoms("100"), transfer_input_addr, Network.Testnet); + + const alice_sk = make_receiving_address(account_privkey, 1); + const bob_sk = make_receiving_address(account_privkey, 2); + const alice_pk = public_key_from_private_key(alice_sk); + const bob_pk = public_key_from_private_key(bob_sk); + const htlc_challenge = encode_multisig_challenge(Uint8Array.from([...alice_pk, ...bob_pk]), 2, Network.Testnet); + const htlc_multisig_destination = multisig_challenge_to_address(htlc_challenge, Network.Testnet); + + const htlc_spend_addr = make_addr(3); + const htlc_utxo = encode_output_htlc( + Amount.from_atoms("40000"), + undefined, + HTLC_SECRET_HASH, + htlc_spend_addr, + htlc_multisig_destination, + encode_lock_until_height(BigInt(100)), + Network.Testnet + ); + const transfer_input = encode_input_for_utxo(tx_outpoint, 1); + const htlc_input = encode_input_for_utxo(tx_outpoint, 2); + + const inputs = [...produce_block_from_stake_input, ...fill_order_input, ...conclude_order_input, ...transfer_input, ...htlc_input]; + const input_utxos = [1, ...produce_block_from_stake_utxo, 0, 0, 1, ...transfer_utxo, 1, ...htlc_utxo]; + + const output_lock = encode_lock_until_height(BigInt(123)); + const lock_tehn_transfer_dest_addr = make_addr(4); + const output = encode_output_lock_then_transfer( + Amount.from_atoms("100"), + lock_tehn_transfer_dest_addr, + output_lock, + Network.Testnet + ); + const outputs = [...output]; + + const produce_block_from_stake_input_dest_addr = make_addr(5); + const conclude_order_input_dest_addr = make_addr(6); + + const tx = encode_transaction(Uint8Array.from(inputs), Uint8Array.from(outputs), BigInt(0)); + + const additional_info: TxAdditionalInfo = { + pool_info: { [POOL_ID]: { staker_balance: { atoms: "4000000000000000" } } }, + order_info: { + [ORDER_ID]: { + initially_asked: { + coins: { atoms: "3000000000000000" }, + }, + initially_given: { + tokens: { + token_id: TOKEN_ID, + amount: { atoms: "3000000000000000" } + } + }, + ask_balance: { atoms: "3000000000000000" }, + give_balance: { atoms: "3000000000000000" } + }, + [ANOTHER_ORDER_ID]: { + initially_asked: { + coins: { atoms: "4000000000000000" }, + }, + initially_given: { + tokens: { + token_id: TOKEN_ID, + amount: { atoms: "4000000000000000" } + } + }, + ask_balance: { atoms: "4000000000000000" }, + give_balance: { atoms: "4000000000000000" } + } + } + }; + + const transfer_input_sig = encode_witness( + SignatureHashType.ALL, + transfer_input_sk, + transfer_input_addr, + tx, + Uint8Array.from(input_utxos), + 0, + additional_info, + BigInt(height), + Network.Testnet + ); + const signatures = [0, 0, 0, 1, ...transfer_input_sig, 0]; + + const produce_block_from_stake_input_dest = encode_destination(produce_block_from_stake_input_dest_addr, Network.Testnet); + const conclude_order_input_dest = encode_destination(conclude_order_input_dest_addr, Network.Testnet); + const transfer_input_dest = encode_destination(transfer_input_addr, Network.Testnet); + const htlc_input_dest = encode_destination(htlc_spend_addr, Network.Testnet); + + const input_destinations = [ + 1, ...produce_block_from_stake_input_dest, 0, 1, ...conclude_order_input_dest, 1, ...transfer_input_dest, 1, ...htlc_input_dest + ]; + const htlc_secrets = [0, 0, 0, 0, 1, ...HTLC_SECRET]; + + const ptx = encode_partially_signed_transaction( + tx, Uint8Array.from(signatures), + Uint8Array.from(input_utxos), + Uint8Array.from(input_destinations), + Uint8Array.from(htlc_secrets), + additional_info, + Network.Testnet + ); + + const ptx_json = decode_partially_signed_transaction_to_js(ptx, Network.Testnet); + const expected_ptx_json = { + "tx":{ + "V1":{ + "version":1, + "flags":0, + "inputs":[ + { + "Utxo":{ + "id":{ + "BlockReward":"0101010101010101010101010101010101010101010101010101010101010101" + }, + "index":1 + } + }, + { + "OrderAccountCommand":{ + "FillOrder":[ + "tordr1xxt0avjtt4flkq0tnlyphmdm4aaj9vmkx5r2m4g863nw3lgf7nzs7mlkqc", + { + "atoms":"40000" + } + ] + } + }, + { + "OrderAccountCommand":{ + "ConcludeOrder":"tordr1mslcn8z774t3ug9zcxa6mqr9yc29r60fg8fkhajnngc98ryh5m3sqz6jvz" + } + }, + { + "Utxo":{ + "id":{ + "Transaction":"0000000000000000000000000000000000000000000000000000000000000000" + }, + "index":1 + } + }, + { + "Utxo":{ + "id":{ + "Transaction":"0000000000000000000000000000000000000000000000000000000000000000" + }, + "index":2 + } + } + ], + "outputs":[ + { + "LockThenTransfer":[ + { + "Coin":{ + "atoms":"100" + } + }, + "tmt1qyuf7yschhzdhumusrl2r4vydhqp5l0vtsff2aw9", + { + "type":"UntilHeight", + "content":123 + } + ] + } + ] + } + }, + "witnesses":[ + null, + null, + null, + { + "Standard":{ + "sighash_type":1, + // Note: the leading 4 bytes of transfer_input_sig are: + // the index of the InputWitness::Standard variant, sighash_type and 2 bytes + // for the length of the raw_signature vec. + "raw_signature": Array.from(transfer_input_sig).slice(4) + } + }, + null + ], + "input_utxos":[ + { + "ProduceBlockFromStake":[ + "tmt1q9dn5m4svn8sds3fcy09kpxrefnu75xekgr5wa3n", + "tpool1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqza035u" + ] + }, + null, + null, + { + "Transfer":[ + { + "Coin":{ + "atoms":"100" + } + }, + "tmt1q9dn5m4svn8sds3fcy09kpxrefnu75xekgr5wa3n" + ] + }, + { + "Htlc":[ + { + "Coin":{ + "atoms":"40000" + } + }, + { + "secret_hash":"b5a48c7780e597de8012346fb30761965248e3f2", + "spend_key":"tmt1qyfvlt0tc8z8gaqyu8sjlm2yte5jr8mlnutmxwn2", + "refund_timelock":{ + "type":"UntilHeight", + "content":100 + }, + "refund_key":"tmtc1qszl7xx5rcy5s7azhee88qadccfnhj7l6vgzxlym" + } + ] + } + ], + "destinations":[ + "tmt1q9df8haugxrq83wky4ym6ldmthzzyecjr5qd3sr6", + null, + "tmt1qxdchxlzxj3srxdtfukdwdxy2n27wytq5yzkl4yc", + "tmt1q9dn5m4svn8sds3fcy09kpxrefnu75xekgr5wa3n", + "tmt1qyfvlt0tc8z8gaqyu8sjlm2yte5jr8mlnutmxwn2" + ], + "htlc_secrets":[ + null, + null, + null, + null, + { + "secret": HTLC_SECRET + } + ], + "additional_info":{ + "pool_info":{ + "tpool1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqza035u":{ + "staker_balance":{ + "atoms":"4000000000000000" + } + } + }, + "order_info":{ + "tordr1xxt0avjtt4flkq0tnlyphmdm4aaj9vmkx5r2m4g863nw3lgf7nzs7mlkqc":{ + "initially_asked":{ + "Coin":{ + "atoms":"3000000000000000" + } + }, + "initially_given":{ + "TokenV1":[ + "tmltk15tgfrs49rv88v8utcllqh0nvpaqtgvn26vdxhuner5m6ewg9c3msn9fxns", + { + "atoms":"3000000000000000" + } + ] + }, + "ask_balance":{ + "atoms":"3000000000000000" + }, + "give_balance":{ + "atoms":"3000000000000000" + } + }, + "tordr1mslcn8z774t3ug9zcxa6mqr9yc29r60fg8fkhajnngc98ryh5m3sqz6jvz":{ + "initially_asked":{ + "Coin":{ + "atoms":"4000000000000000" + } + }, + "initially_given":{ + "TokenV1":[ + "tmltk15tgfrs49rv88v8utcllqh0nvpaqtgvn26vdxhuner5m6ewg9c3msn9fxns", + { + "atoms":"4000000000000000" + } + ] + }, + "ask_balance":{ + "atoms":"4000000000000000" + }, + "give_balance":{ + "atoms":"4000000000000000" + } + } + } + } + }; + + assert_eq_vals( + JSON.stringify(ptx_json), + JSON.stringify(expected_ptx_json), + ); +} diff --git a/wasm-wrappers/js-bindings-test/tests/test_transaction_and_witness_encoding.ts b/wasm-wrappers/js-bindings-test/tests/test_transaction_and_witness_encoding.ts index 66ccf53ad0..ae6caa4385 100644 --- a/wasm-wrappers/js-bindings-test/tests/test_transaction_and_witness_encoding.ts +++ b/wasm-wrappers/js-bindings-test/tests/test_transaction_and_witness_encoding.ts @@ -43,11 +43,11 @@ import { } from "./test_encode_other_outputs.js"; export function test_transaction_and_witness_encoding() { - const account_pubkey = make_default_account_privkey( + const account_privkey = make_default_account_privkey( MNEMONIC, Network.Testnet, ); - const receiving_privkey = make_receiving_address(account_pubkey, 0); + const receiving_privkey = make_receiving_address(account_privkey, 0); try { const invalid_inputs = TEXT_ENCODER.encode("invalid inputs"); diff --git a/wasm-wrappers/src/encode_input.rs b/wasm-wrappers/src/encode_input.rs index 5c467a44fe..9c817235f9 100644 --- a/wasm-wrappers/src/encode_input.rs +++ b/wasm-wrappers/src/encode_input.rs @@ -190,6 +190,7 @@ pub fn encode_input_for_change_token_metadata_uri( /// instead of `encode_witness`). /// Note that in orders v0 FillOrder inputs can technically have a signature, it's just not checked. /// But in orders V1 we actually require that those inputs don't have signatures. +/// Also, in orders V1 the provided destination is always ignored. #[wasm_bindgen] pub fn encode_input_for_fill_order( order_id: &str, diff --git a/wasm-wrappers/src/error.rs b/wasm-wrappers/src/error.rs index 989b122703..3878931c6b 100644 --- a/wasm-wrappers/src/error.rs +++ b/wasm-wrappers/src/error.rs @@ -19,6 +19,9 @@ use common::{ address::AddressError, chain::{ classic_multisig::ClassicMultisigChallengeError, + partially_signed_transaction::{ + PartiallySignedTransactionError, SighashInputCommitmentCreationError, + }, signature::{ inputsig::{ arbitrary_message::SignArbitraryMessageError, @@ -34,8 +37,6 @@ use common::{ }; use consensus::EffectivePoolBalanceError; -use crate::sighash_input_commitments::SighashInputCommitmentCreationError; - #[allow(clippy::enum_variant_names)] #[derive(thiserror::Error, Debug, Clone)] pub enum Error { @@ -66,6 +67,9 @@ pub enum Error { #[error("Invalid transaction input utxo encoding: {0}")] InvalidInputUtxoEncoding(serialization::Error), + #[error("Invalid destination encoding: {0}")] + InvalidDestinationEncoding(serialization::Error), + #[error("Invalid transaction witness encoding: {0}")] InvalidWitnessEncoding(serialization::Error), @@ -168,6 +172,9 @@ pub enum Error { #[error("Transaction creation error: {0}")] TransactionCreationError(TransactionCreationError), + #[error("Partially signed transaction creation error: {0}")] + PartiallySignedTransactionCreationError(PartiallySignedTransactionError), + #[error("Sighash calculation error: {0}")] SighashCalculationError(DestinationSigError), diff --git a/wasm-wrappers/src/internal.rs b/wasm-wrappers/src/internal.rs index 72ad3c823e..692006a009 100644 --- a/wasm-wrappers/src/internal.rs +++ b/wasm-wrappers/src/internal.rs @@ -20,6 +20,7 @@ use wasm_bindgen::prelude::*; use common::{ chain::{ config::Builder, + partially_signed_transaction::make_sighash_input_commitments_at_height, signature::{ inputsig::{ authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, @@ -36,9 +37,8 @@ use utils::ensure; use crate::{ error::Error, - sighash_input_commitments::{make_sighash_input_commitments, TxInputsAdditionalInfo}, types::{Network, SignatureHashType, TxAdditionalInfo}, - utils::{decode_raw_array, extract_htlc_spend, parse_addressable}, + utils::{decode_raw_array, extract_htlc_spend, parse_addressable, to_ptx_additional_info}, }; /// Verify a witness produced by one of the `encode_witness` functions. @@ -71,13 +71,12 @@ pub fn internal_verify_witness( let input_utxos = decode_raw_array::>(input_utxos) .map_err(Error::InvalidInputUtxoEncoding)?; - let input_infos = - TxInputsAdditionalInfo::from_tx_additional_info(&chain_config, &additional_info)?; + let ptx_additional_info = to_ptx_additional_info(&chain_config, &additional_info)?; - let input_commitments = make_sighash_input_commitments( + let input_commitments = make_sighash_input_commitments_at_height( tx.inputs(), &input_utxos, - &input_infos, + &ptx_additional_info, &chain_config, BlockHeight::new(current_block_height), )?; diff --git a/wasm-wrappers/src/lib.rs b/wasm-wrappers/src/lib.rs index 4b15f5dc6d..e068964c8e 100644 --- a/wasm-wrappers/src/lib.rs +++ b/wasm-wrappers/src/lib.rs @@ -43,6 +43,10 @@ use common::{ config::{Builder, BIP44_PATH}, htlc::HtlcSecret, make_delegation_id, make_order_id, make_pool_id, make_token_id, + partially_signed_transaction::{ + make_sighash_input_commitments_at_height, PartiallySignedTransaction, + PartiallySignedTransactionConsistencyCheck, + }, signature::{ inputsig::{ arbitrary_message::{produce_message_challenge, ArbitraryMessageSignature}, @@ -78,17 +82,14 @@ use serialization::{json_encoded::JsonEncoded, Decode, DecodeAll, Encode}; use crate::{ error::Error, - sighash_input_commitments::{make_sighash_input_commitments, TxInputsAdditionalInfo}, - types::TxAdditionalInfo, - types::{Amount, Network, SignatureHashType, SourceId}, - utils::{decode_raw_array, extract_htlc_spend, parse_addressable}, + types::{Amount, Network, SignatureHashType, SourceId, TxAdditionalInfo}, + utils::{decode_raw_array, extract_htlc_spend, parse_addressable, to_ptx_additional_info}, }; mod encode_input; mod encode_output; mod error; mod internal; -mod sighash_input_commitments; #[cfg(test)] mod tests; mod types; @@ -756,13 +757,12 @@ pub fn encode_witness( let input_utxos = decode_raw_array::>(input_utxos) .map_err(Error::InvalidInputUtxoEncoding)?; - let input_infos = - TxInputsAdditionalInfo::from_tx_additional_info(&chain_config, &additional_info)?; + let ptx_additional_info = to_ptx_additional_info(&chain_config, &additional_info)?; - let input_commitments = make_sighash_input_commitments( + let input_commitments = make_sighash_input_commitments_at_height( tx.inputs(), &input_utxos, - &input_infos, + &ptx_additional_info, &chain_config, BlockHeight::new(current_block_height), )?; @@ -813,13 +813,12 @@ pub fn encode_witness_htlc_secret( let input_utxos = decode_raw_array::>(input_utxos) .map_err(Error::InvalidInputUtxoEncoding)?; - let input_infos = - TxInputsAdditionalInfo::from_tx_additional_info(&chain_config, &additional_info)?; + let ptx_additional_info = to_ptx_additional_info(&chain_config, &additional_info)?; - let input_commitments = make_sighash_input_commitments( + let input_commitments = make_sighash_input_commitments_at_height( tx.inputs(), &input_utxos, - &input_infos, + &ptx_additional_info, &chain_config, BlockHeight::new(current_block_height), )?; @@ -918,13 +917,12 @@ pub fn encode_witness_htlc_multisig( let input_utxos = decode_raw_array::>(input_utxos) .map_err(Error::InvalidInputUtxoEncoding)?; - let input_infos = - TxInputsAdditionalInfo::from_tx_additional_info(&chain_config, &additional_info)?; + let ptx_additional_info = to_ptx_additional_info(&chain_config, &additional_info)?; - let input_commitments = make_sighash_input_commitments( + let input_commitments = make_sighash_input_commitments_at_height( tx.inputs(), &input_utxos, - &input_infos, + &ptx_additional_info, &chain_config, BlockHeight::new(current_block_height), )?; @@ -988,6 +986,99 @@ pub fn encode_signed_transaction(transaction: &[u8], signatures: &[u8]) -> Resul Ok(tx.encode()) } +/// Return a PartiallySignedTransaction object as bytes. +/// +/// `transaction` is an encoded `Transaction` (which can be produced via `encode_transaction`). +/// +/// `signatures`, `input_utxos`, `input_destinations` and `htlc_secrets` are encoded lists of +/// optional objects of the corresponding type. To produce such a list, iterate over your +/// original list of optional objects and then: +/// 1) emit byte 0 if the current object is null; +/// 2) otherwise emit byte 1 followed by the object in its encoded form. +/// +/// Each individual object in each of the lists corresponds to the transaction input with the same +/// index and its meaning is as follows: +/// 1) `signatures` - the signature for the input; +/// 2) `input_utxos`- the utxo for the input (if it's utxo-based); +/// 3) `input_destinations` - the destination (address) corresponding to the input; this determines +/// the key(s) with which the input has to be signed. Note that for utxo-based inputs the +/// corresponding destination can usually be extracted from the utxo itself (the exception +/// being the `ProduceBlockFromStake` utxo, which doesn't contain the pool's decommission key). +/// However, PartiallySignedTransaction requires that *all* input destinations are provided +/// explicitly anyway. +/// 4) `htlc_secrets` - if the input is an HTLC one and if the transaction is spending the HTLC, +/// this should be the HTLC secret. Otherwise it should be null. +/// +/// The number of items in each list must be equal to the number of transaction inputs. +/// +/// `additional_info` has the same meaning as in `encode_witness`. +#[wasm_bindgen] +pub fn encode_partially_signed_transaction( + transaction: &[u8], + signatures: &[u8], + input_utxos: &[u8], + input_destinations: &[u8], + htlc_secrets: &[u8], + additional_info: TxAdditionalInfo, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + + let signatures = decode_raw_array::>(signatures) + .map_err(Error::InvalidWitnessEncoding)?; + + let tx = Transaction::decode_all(&mut &transaction[..]) + .map_err(Error::InvalidTransactionEncoding)?; + + let input_utxos = decode_raw_array::>(input_utxos) + .map_err(Error::InvalidInputUtxoEncoding)?; + + let input_destinations = decode_raw_array::>(input_destinations) + .map_err(Error::InvalidDestinationEncoding)?; + + let htlc_secrets = decode_raw_array::>(htlc_secrets) + .map_err(Error::InvalidHtlcSecretEncoding)?; + + let ptx_additional_info = to_ptx_additional_info(&chain_config, &additional_info)?; + + let tx = PartiallySignedTransaction::new( + tx, + signatures, + input_utxos, + input_destinations, + Some(htlc_secrets), + ptx_additional_info, + PartiallySignedTransactionConsistencyCheck::WithAdditionalInfo, + ) + .map_err(Error::PartiallySignedTransactionCreationError)?; + Ok(tx.encode()) +} + +/// Decodes a partially signed transaction from its binary encoding into a JavaScript object. +#[wasm_bindgen] +pub fn decode_partially_signed_transaction_to_js( + transaction: &[u8], + network: Network, +) -> Result { + let chain_config = Builder::new(network.into()).build(); + let ptx = PartiallySignedTransaction::decode_all(&mut &transaction[..]) + .map_err(Error::InvalidTransactionEncoding)?; + + let str = JsonEncoded::new(&ptx).to_string(); + let str = dehexify_all_addresses(&chain_config, &str); + + js_sys::JSON::parse(&str).map_err(Error::JsonParseError) +} + +/// Convert the specified string address into a Destination object, encoded as bytes. +#[wasm_bindgen] +pub fn encode_destination(address: &str, network: Network) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let destination = parse_addressable::(&chain_config, address)?; + + Ok(destination.encode()) +} + /// Given a `Transaction` encoded in bytes (not a signed transaction, but a signed transaction is tolerated by ignoring the extra bytes, by choice) /// this function will return the transaction id. /// diff --git a/wasm-wrappers/src/sighash_input_commitments.rs b/wasm-wrappers/src/sighash_input_commitments.rs deleted file mode 100644 index 8ac471fcc4..0000000000 --- a/wasm-wrappers/src/sighash_input_commitments.rs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) 2025 RBB S.r.l -// opensource@mintlayer.org -// SPDX-License-Identifier: MIT -// Licensed under the MIT License; -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::BTreeMap; - -use common::{ - chain::{ - signature::sighash::{ - self, - input_commitments::{ - make_sighash_input_commitments_for_transaction_inputs_at_height, OrderInfoProvider, - PoolInfoProvider, SighashInputCommitment, - }, - }, - ChainConfig, OrderId, PoolId, TxInput, TxOutput, - }, - primitives::BlockHeight, -}; - -use crate::{ - error::Error, - utils::{ - internal_amount_from_simple_amount, output_value_from_simple_currency_amount, - parse_addressable, - }, -}; - -pub fn make_sighash_input_commitments<'a>( - tx_inputs: &[TxInput], - input_utxos: &'a [Option], - inputs_info: &TxInputsAdditionalInfo, - chain_config: &ChainConfig, - block_height: BlockHeight, -) -> Result>, Error> { - Ok( - make_sighash_input_commitments_for_transaction_inputs_at_height( - tx_inputs, - &sighash::input_commitments::TrivialUtxoProvider(input_utxos), - inputs_info, - inputs_info, - chain_config, - block_height, - )?, - ) -} - -pub type SighashInputCommitmentCreationError = - sighash::input_commitments::SighashInputCommitmentCreationError< - std::convert::Infallible, - std::convert::Infallible, - std::convert::Infallible, - >; - -pub struct TxInputsAdditionalInfo { - pub pool_info: BTreeMap, - pub order_info: BTreeMap, -} - -impl TxInputsAdditionalInfo { - pub fn from_tx_additional_info( - chain_config: &ChainConfig, - info: &crate::types::TxAdditionalInfo, - ) -> Result { - let pool_info = info - .pool_info - .iter() - .map(|(pool_id, pool_info)| { - let pool_id = parse_addressable::(chain_config, pool_id)?; - let pool_info = convert_pool_info(pool_info)?; - Ok((pool_id, pool_info)) - }) - .collect::, Error>>()?; - - let order_info = info - .order_info - .iter() - .map(|(order_id, order_info)| { - let order_id = parse_addressable::(chain_config, order_id)?; - let order_info = convert_order_info(chain_config, order_info)?; - Ok((order_id, order_info)) - }) - .collect::, Error>>()?; - - Ok(Self { - pool_info, - order_info, - }) - } -} - -impl PoolInfoProvider for TxInputsAdditionalInfo { - type Error = std::convert::Infallible; - - fn get_pool_info( - &self, - pool_id: &PoolId, - ) -> Result, Self::Error> { - Ok(self.pool_info.get(pool_id).cloned()) - } -} - -impl OrderInfoProvider for TxInputsAdditionalInfo { - type Error = std::convert::Infallible; - - fn get_order_info( - &self, - order_id: &OrderId, - ) -> Result, Self::Error> { - Ok(self.order_info.get(order_id).cloned()) - } -} - -fn convert_pool_info( - info: &crate::types::PoolAdditionalInfo, -) -> Result { - let staker_balance = internal_amount_from_simple_amount(&info.staker_balance)?; - - Ok(sighash::input_commitments::PoolInfo { staker_balance }) -} - -fn convert_order_info( - chain_config: &ChainConfig, - info: &crate::types::OrderAdditionalInfo, -) -> Result { - let initially_asked = - output_value_from_simple_currency_amount(chain_config, &info.initially_asked)?; - let initially_given = - output_value_from_simple_currency_amount(chain_config, &info.initially_given)?; - - let ask_balance = internal_amount_from_simple_amount(&info.ask_balance)?; - let give_balance = internal_amount_from_simple_amount(&info.give_balance)?; - - Ok(sighash::input_commitments::OrderInfo { - initially_asked, - initially_given, - ask_balance, - give_balance, - }) -} diff --git a/wasm-wrappers/src/utils.rs b/wasm-wrappers/src/utils.rs index da0e704d61..e1ccce6eb9 100644 --- a/wasm-wrappers/src/utils.rs +++ b/wasm-wrappers/src/utils.rs @@ -19,6 +19,10 @@ use common::{ address::{traits::Addressable, Address}, chain::{ output_value::OutputValue, + partially_signed_transaction::{ + OrderAdditionalInfo as PtxOrderAdditionalInfo, + PoolAdditionalInfo as PtxPoolAdditionalInfo, TxAdditionalInfo as PtxAdditionalInfo, + }, signature::{ inputsig::{ authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, @@ -27,15 +31,15 @@ use common::{ sighash::sighashtype::SigHashType, }, tokens::TokenId, - ChainConfig, + ChainConfig, OrderId, PoolId, }, - primitives, + primitives::{self}, }; use serialization::Decode; use crate::{ error::Error, - types::{SimpleAmount, SimpleCurrencyAmount}, + types::{SimpleAmount, SimpleCurrencyAmount, TxAdditionalInfo}, }; pub fn decode_raw_array(mut array: &[u8]) -> Result, serialization::Error> { @@ -108,3 +112,52 @@ pub fn extract_htlc_spend( )), } } + +pub fn to_ptx_additional_info( + chain_config: &ChainConfig, + info: &TxAdditionalInfo, +) -> Result { + let mut ptx_info = PtxAdditionalInfo::new(); + + for (pool_id_str, pool_info) in &info.pool_info { + let pool_id = parse_addressable::(chain_config, pool_id_str)?; + let ptx_pool_info = convert_pool_info(pool_info)?; + ptx_info.add_pool_info(pool_id, ptx_pool_info); + } + + for (order_id_str, order_info) in &info.order_info { + let order_id = parse_addressable::(chain_config, order_id_str)?; + let ptx_order_info = convert_order_info(chain_config, order_info)?; + ptx_info.add_order_info(order_id, ptx_order_info); + } + + Ok(ptx_info) +} + +fn convert_pool_info( + info: &crate::types::PoolAdditionalInfo, +) -> Result { + let staker_balance = internal_amount_from_simple_amount(&info.staker_balance)?; + + Ok(PtxPoolAdditionalInfo { staker_balance }) +} + +fn convert_order_info( + chain_config: &ChainConfig, + info: &crate::types::OrderAdditionalInfo, +) -> Result { + let initially_asked = + output_value_from_simple_currency_amount(chain_config, &info.initially_asked)?; + let initially_given = + output_value_from_simple_currency_amount(chain_config, &info.initially_given)?; + + let ask_balance = internal_amount_from_simple_amount(&info.ask_balance)?; + let give_balance = internal_amount_from_simple_amount(&info.give_balance)?; + + Ok(PtxOrderAdditionalInfo { + initially_asked, + initially_given, + ask_balance, + give_balance, + }) +}