From d004d8971879b1100060416a5c69d3341a3da801 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Mon, 2 Sep 2024 16:02:05 +0300 Subject: [PATCH 01/13] pending tx (WIP) --- wallet/core/src/tx/generator/pending.rs | 143 ++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/wallet/core/src/tx/generator/pending.rs b/wallet/core/src/tx/generator/pending.rs index ad7e2e40f..451572a60 100644 --- a/wallet/core/src/tx/generator/pending.rs +++ b/wallet/core/src/tx/generator/pending.rs @@ -28,12 +28,18 @@ pub(crate) struct PendingTransactionInner { pub(crate) is_submitted: AtomicBool, /// Payment value of the transaction (transaction destination amount) pub(crate) payment_value: Option, + /// The index (position) of the change output in the transaction + pub(crate) change_output_index: Option, /// Change value of the transaction (transaction change amount) pub(crate) change_output_value: u64, /// Total aggregate value of all inputs pub(crate) aggregate_input_value: u64, /// Total aggregate value of all outputs pub(crate) aggregate_output_value: u64, + /// Minimum number of signatures required for the transaction + /// (passed in during transaction creation). This value is used + /// to estimate the mass of the transaction. + pub(crate) minimum_signatures: u16, // Transaction mass pub(crate) mass: u64, /// Fees of the transaction @@ -42,6 +48,29 @@ pub(crate) struct PendingTransactionInner { pub(crate) kind: DataKind, } + +// impl Clone for PendingTransactionInner { +// fn clone(&self) -> Self { +// Self { +// generator: self.generator.clone(), +// utxo_entries: self.utxo_entries.clone(), +// id: self.id, +// signable_tx: Mutex::new(self.signable_tx.lock().unwrap().clone()), +// addresses: self.addresses.clone(), +// is_submitted: AtomicBool::new(self.is_submitted.load(Ordering::SeqCst)), +// payment_value: self.payment_value, +// change_output_index: self.change_output_index, +// change_output_value: self.change_output_value, +// aggregate_input_value: self.aggregate_input_value, +// aggregate_output_value: self.aggregate_output_value, +// minimum_signatures: self.minimum_signatures, +// mass: self.mass, +// fees: self.fees, +// kind: self.kind, +// } +// } +// } + impl std::fmt::Debug for PendingTransaction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let transaction = self.transaction(); @@ -49,8 +78,10 @@ impl std::fmt::Debug for PendingTransaction { .field("utxo_entries", &self.inner.utxo_entries) .field("addresses", &self.inner.addresses) .field("payment_value", &self.inner.payment_value) + .field("change_output_index", &self.inner.change_output_index) .field("change_output_value", &self.inner.change_output_value) .field("aggregate_input_value", &self.inner.aggregate_input_value) + .field("minimum_signatures", &self.inner.minimum_signatures) .field("mass", &self.inner.mass) .field("fees", &self.inner.fees) .field("kind", &self.inner.kind) @@ -75,9 +106,11 @@ impl PendingTransaction { utxo_entries: Vec, addresses: Vec
, payment_value: Option, + change_output_index: Option, change_output_value: u64, aggregate_input_value: u64, aggregate_output_value: u64, + minimum_signatures: u16, mass: u64, fees: u64, kind: DataKind, @@ -95,9 +128,11 @@ impl PendingTransaction { addresses, is_submitted: AtomicBool::new(false), payment_value, + change_output_index, change_output_value, aggregate_input_value, aggregate_output_value, + minimum_signatures, mass, fees, kind, @@ -135,6 +170,14 @@ impl PendingTransaction { self.inner.fees } + pub fn mass(&self) -> u64 { + self.inner.mass + } + + pub fn minimum_signatures(&self) -> u16 { + self.inner.minimum_signatures + } + pub fn aggregate_input_value(&self) -> u64 { self.inner.aggregate_input_value } @@ -147,6 +190,10 @@ impl PendingTransaction { self.inner.payment_value } + pub fn change_output_index(&self) -> Option { + self.inner.change_output_index + } + pub fn change_value(&self) -> u64 { self.inner.change_output_value } @@ -271,4 +318,100 @@ impl PendingTransaction { *self.inner.signable_tx.lock().unwrap() = signed_tx; Ok(()) } + + pub fn increase_fees(&self, fee_increase: u64) -> Result { + if self.is_batch() { + + Err(Error::NotImplemented) + } else { + + let PendingTransactionInner { + generator, + utxo_entries, + id, + signable_tx, + addresses, + is_submitted, + payment_value, + change_output_index, + change_output_value, + aggregate_input_value, + aggregate_output_value, + minimum_signatures, + mass, + fees, + kind, + } = &*self.inner; + + let generator = generator.clone(); + let utxo_entries = utxo_entries.clone(); + let id = *id; + let signable_tx = Mutex::new(signable_tx.lock()?.clone()); + let addresses = addresses.clone(); + let is_submitted = AtomicBool::new(is_submitted.load(Ordering::SeqCst)); + let payment_value = *payment_value; + let change_output_index = *change_output_index; + let change_output_value = *change_output_value; + let aggregate_input_value = *aggregate_input_value; + let aggregate_output_value = *aggregate_output_value; + let minimum_signatures = *minimum_signatures; + let mass = *mass; + let fees = *fees; + let kind = *kind; + + if change_output_value > fees { + + // let mut inner = self.inner.deref().clone(); + // Ok(PendingTransaction(Arc::new(inner))) + + let inner = PendingTransactionInner { + generator, + utxo_entries, + id, + signable_tx, + addresses, + is_submitted, + payment_value, + change_output_index, + change_output_value, + aggregate_input_value, + aggregate_output_value, + minimum_signatures, + mass, + fees, + kind, + }; + + Ok(PendingTransaction { inner : Arc::new(inner) }) + + } else { + + let inner = PendingTransactionInner { + generator, + utxo_entries, + id, + signable_tx, + addresses, + is_submitted, + payment_value, + change_output_index, + change_output_value, + aggregate_input_value, + aggregate_output_value, + minimum_signatures, + mass, + fees, + kind, + }; + + Ok(PendingTransaction { inner : Arc::new(inner) }) + } + + + } + // let mut mutable_tx = self.inner.signable_tx.lock()?.clone(); + // mutable_tx.tx.fee += fees; + // *self.inner.signable_tx.lock().unwrap() = mutable_tx; + + } } From db21255233308b64124c7ff57d3478b6768d94e0 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Thu, 5 Sep 2024 11:41:20 +0300 Subject: [PATCH 02/13] wip --- consensus/client/src/utxo.rs | 1 + wallet/core/src/tx/generator/generator.rs | 90 +++++++-- wallet/core/src/tx/generator/pending.rs | 224 +++++++++++++--------- 3 files changed, 211 insertions(+), 104 deletions(-) diff --git a/consensus/client/src/utxo.rs b/consensus/client/src/utxo.rs index 3f519d067..5c752296e 100644 --- a/consensus/client/src/utxo.rs +++ b/consensus/client/src/utxo.rs @@ -198,6 +198,7 @@ impl UtxoEntryReference { pub fn transaction_id_as_ref(&self) -> &TransactionId { self.utxo.outpoint.transaction_id_as_ref() } + } impl std::hash::Hash for UtxoEntryReference { diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 52dafa0f7..b569450be 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -103,6 +103,10 @@ struct Context { number_of_transactions: usize, /// current tree stage stage: Option>, + /// stage during the final transaction generation + /// preserved in case we need to increase priority fees + /// after the final transaction has been generated. + final_stage: Option>, /// Rejected or "stashed" UTXO entries that are consumed before polling /// the iterator. This store is used in edge cases when UTXO entry from the /// iterator has been consumed but was rejected due to mass constraints or @@ -434,6 +438,7 @@ impl Generator { aggregated_utxos: 0, aggregate_fees: 0, stage: Some(Box::default()), + final_stage: None, utxo_stash: VecDeque::default(), final_transaction_id: None, is_done: false, @@ -467,61 +472,84 @@ impl Generator { } /// Returns the current [`NetworkType`] + #[inline(always)] pub fn network_type(&self) -> NetworkType { self.inner.network_id.into() } - + /// Returns the current [`NetworkId`] + #[inline(always)] pub fn network_id(&self) -> NetworkId { self.inner.network_id } - + /// Returns current [`NetworkParams`] + #[inline(always)] pub fn network_params(&self) -> &NetworkParams { self.inner.network_params } - + + /// Returns owned mass calculator instance (bound to [`NetworkParams`] of the [`Generator`]) + #[inline(always)] + pub fn mass_calculator(&self) -> &MassCalculator { + &self.inner.mass_calculator + } + + #[inline(always)] + pub fn sig_op_count(&self) -> u8 { + &self.inner.sig_op_count + } + /// The underlying [`UtxoContext`] (if available). + #[inline(always)] pub fn source_utxo_context(&self) -> &Option { &self.inner.source_utxo_context } - + /// Signifies that the transaction is a transfer between accounts + #[inline(always)] pub fn destination_utxo_context(&self) -> &Option { &self.inner.destination_utxo_context } - + /// Core [`Multiplexer`] (if available) + #[inline(always)] pub fn multiplexer(&self) -> &Option>> { &self.inner.multiplexer } - + /// Mutable context used by the generator to track state + #[inline(always)] fn context(&self) -> MutexGuard { self.inner.context.lock().unwrap() } - + /// Returns the underlying instance of the [Signer](SignerT) + #[inline(always)] pub(crate) fn signer(&self) -> &Option> { &self.inner.signer } - + /// The total amount of fees in SOMPI consumed during the transaction generation process. + #[inline(always)] pub fn aggregate_fees(&self) -> u64 { self.context().aggregate_fees } - + /// The total number of UTXOs consumed during the transaction generation process. + #[inline(always)] pub fn aggregate_utxos(&self) -> usize { self.context().aggregated_utxos } - + /// The final transaction amount (if available). + #[inline(always)] pub fn final_transaction_value_no_fees(&self) -> Option { self.inner.final_transaction.as_ref().map(|final_transaction| final_transaction.value_no_fees) } - + /// Returns the final transaction id if the generator has finished successfully. + #[inline(always)] pub fn final_transaction_id(&self) -> Option { self.context().final_transaction_id } @@ -529,17 +557,19 @@ impl Generator { /// Returns an async Stream causes the [Generator] to produce /// transaction for each stream item request. NOTE: transactions /// are generated only when each stream item is polled. + #[inline(always)] pub fn stream(&self) -> impl Stream> { Box::pin(PendingTransactionStream::new(self)) } - + /// Returns an iterator that causes the [Generator] to produce /// transaction for each iterator poll request. NOTE: transactions /// are generated only when the returned iterator is iterated. + #[inline(always)] pub fn iter(&self) -> impl Iterator> { PendingTransactionIterator::new(self) } - + /// Get next UTXO entry. This function obtains UTXO in the following order: /// 1. From the UTXO stash (used to store UTxOs that were consumed during previous transaction generation but were rejected due to various conditions, such as mass overflow) /// 2. From the current stage @@ -566,12 +596,46 @@ impl Generator { }) } + // pub(crate) fn get_utxo_entry_for_rbf(&self) -> Result> { + // let mut context = &mut self.context(); + // let utxo_entry = if let Some(mut stage) = context.stage.take() { + // let utxo_entry = self.get_utxo_entry(&mut context, &mut stage); + // context.stage.replace(stage); + // utxo_entry + // } else if let Some(mut stage) = context.final_stage.take() { + // let utxo_entry = self.get_utxo_entry(&mut context, &mut stage); + // context.final_stage.replace(stage); + // utxo_entry + // } else { + // return Err(Error::GeneratorNoStage); + // }; + + // Ok(utxo_entry) + // } + + /// Adds a [`UtxoEntryReference`] to the UTXO stash. UTXO stash + /// is the first source of UTXO entries. + pub fn stash(&self, into_iter: impl IntoIterator) { + // let iter = iter.into_iterator(); + // let mut context = self.context(); + // context.utxo_stash.extend(iter); + self.context().utxo_stash.extend(into_iter.into_iter()); + } + + // /// Adds multiple [`UtxoEntryReference`] structs to the UTXO stash. UTXO stash + // /// is the first source of UTXO entries. + // pub fn stash_multiple(&self, utxo_entries: Vec) { + // self.context().utxo_stash.extend(utxo_entries); + // } + /// Calculate relay transaction mass for the current transaction `data` + #[inline(always)] fn calc_relay_transaction_mass(&self, data: &Data) -> u64 { data.aggregate_mass + self.inner.standard_change_output_compute_mass } /// Calculate relay transaction fees for the current transaction `data` + #[inline(always)] fn calc_relay_transaction_compute_fees(&self, data: &Data) -> u64 { self.inner.mass_calculator.calc_minimum_transaction_fee_from_mass(self.calc_relay_transaction_mass(data)) } diff --git a/wallet/core/src/tx/generator/pending.rs b/wallet/core/src/tx/generator/pending.rs index 451572a60..2d2591ab4 100644 --- a/wallet/core/src/tx/generator/pending.rs +++ b/wallet/core/src/tx/generator/pending.rs @@ -6,11 +6,11 @@ use crate::imports::*; use crate::result::Result; use crate::rpc::DynRpcApi; -use crate::tx::{DataKind, Generator}; -use crate::utxo::{UtxoContext, UtxoEntryId, UtxoEntryReference}; +use crate::tx::{DataKind, Generator, MAXIMUM_STANDARD_TRANSACTION_MASS}; +use crate::utxo::{UtxoContext, UtxoEntryId, UtxoEntryReference, UtxoIterator}; use kaspa_consensus_core::hashing::sighash_type::SigHashType; use kaspa_consensus_core::sign::{sign_input, sign_with_multiple_v2, Signed}; -use kaspa_consensus_core::tx::{SignableTransaction, Transaction, TransactionId}; +use kaspa_consensus_core::tx::{SignableTransaction, Transaction, TransactionId, TransactionInput, TransactionOutput}; use kaspa_rpc_core::{RpcTransaction, RpcTransactionId}; pub(crate) struct PendingTransactionInner { @@ -48,7 +48,6 @@ pub(crate) struct PendingTransactionInner { pub(crate) kind: DataKind, } - // impl Clone for PendingTransactionInner { // fn clone(&self) -> Self { // Self { @@ -319,99 +318,142 @@ impl PendingTransaction { Ok(()) } - pub fn increase_fees(&self, fee_increase: u64) -> Result { - if self.is_batch() { - - Err(Error::NotImplemented) - } else { + pub fn increase_fees_for_rbf(&self, additional_fees: u64) -> Result { + let PendingTransactionInner { + generator, + utxo_entries, + id, + signable_tx, + addresses, + is_submitted, + payment_value, + change_output_index, + change_output_value, + aggregate_input_value, + aggregate_output_value, + minimum_signatures, + mass, + fees, + kind, + } = &*self.inner; + + let generator = generator.clone(); + let utxo_entries = utxo_entries.clone(); + let id = *id; + // let signable_tx = Mutex::new(signable_tx.lock()?.clone()); + let mut signable_tx = signable_tx.lock()?.clone(); + let addresses = addresses.clone(); + let is_submitted = AtomicBool::new(false); + let payment_value = *payment_value; + let mut change_output_index = *change_output_index; + let mut change_output_value = *change_output_value; + let mut aggregate_input_value = *aggregate_input_value; + let mut aggregate_output_value = *aggregate_output_value; + let minimum_signatures = *minimum_signatures; + let mass = *mass; + let fees = *fees; + let kind = *kind; + + match kind { + DataKind::Final => { + // change output has sufficient amount to cover fee increase + // if change_output_value > fee_increase && change_output_index.is_some() { + if let (Some(index), true) = (change_output_index,change_output_value >= additional_fees) { + change_output_value -= additional_fees; + if generator.mass_calculator().is_dust(change_output_value) { + aggregate_output_value -= change_output_value; + signable_tx.tx.outputs.remove(index); + change_output_index = None; + change_output_value = 0; + } else { + signable_tx.tx.outputs[index].value = change_output_value; + } + } else { + // we need more utxos... + let mut utxo_entries_rbf = vec![]; + let mut available = change_output_value; + + let utxo_context = generator.source_utxo_context().as_ref().ok_or(Error::custom("No utxo context"))?; + let mut context_utxo_entries = UtxoIterator::new(utxo_context); + while available < additional_fees { + // let utxo_entry = utxo_entries.next().ok_or(Error::InsufficientFunds { additional_needed: additional_fees - available, origin: "increase_fees_for_rbf" })?; + // let utxo_entry = generator.get_utxo_entry_for_rbf()?; + if let Some(utxo_entry) = context_utxo_entries.next() { + // let utxo = utxo_entry.utxo.as_ref(); + let value = utxo_entry.amount(); + available += value; + // aggregate_input_value += value; + + + utxo_entries_rbf.push(utxo_entry); + // signable_tx.lock().unwrap().tx.inputs.push(utxo.as_input()); + } else { + // generator.stash(utxo_entries_rbf); + // utxo_entries_rbf.into_iter().for_each(|utxo_entry|generator.stash(utxo_entry)); + return Err(Error::InsufficientFunds { additional_needed : additional_fees - available, origin : "increase_fees_for_rbf" }); + } + } + + let utxo_entries_vec = utxo_entries + .iter() + .map(|(_,utxo_entry)| utxo_entry.as_ref().clone()) + .chain(utxo_entries_rbf.iter().map(|utxo_entry|utxo_entry.as_ref().clone())) + .collect::>(); + + let inputs = utxo_entries_rbf.into_iter().map(|utxo| { + TransactionInput::new(utxo.outpoint().clone().into(), vec![], 0, generator.sig_op_count()) + }); + + signable_tx.tx.inputs.extend(inputs); + + let transaction_mass = generator.mass_calculator().calc_overall_mass_for_unsigned_consensus_transaction( + &signable_tx.tx, + &utxo_entries_vec, + self.inner.minimum_signatures, + )?; + if transaction_mass > MAXIMUM_STANDARD_TRANSACTION_MASS { + // this should never occur as we should not produce transactions higher than the mass limit + return Err(Error::MassCalculationError); + } + signable_tx.tx.set_mass(transaction_mass); + + // utxo + + // let input = ; + + + } + + } + _ => { - let PendingTransactionInner { - generator, - utxo_entries, - id, - signable_tx, - addresses, - is_submitted, - payment_value, - change_output_index, - change_output_value, - aggregate_input_value, - aggregate_output_value, - minimum_signatures, - mass, - fees, - kind, - } = &*self.inner; - - let generator = generator.clone(); - let utxo_entries = utxo_entries.clone(); - let id = *id; - let signable_tx = Mutex::new(signable_tx.lock()?.clone()); - let addresses = addresses.clone(); - let is_submitted = AtomicBool::new(is_submitted.load(Ordering::SeqCst)); - let payment_value = *payment_value; - let change_output_index = *change_output_index; - let change_output_value = *change_output_value; - let aggregate_input_value = *aggregate_input_value; - let aggregate_output_value = *aggregate_output_value; - let minimum_signatures = *minimum_signatures; - let mass = *mass; - let fees = *fees; - let kind = *kind; - - if change_output_value > fees { - - // let mut inner = self.inner.deref().clone(); - // Ok(PendingTransaction(Arc::new(inner))) - - let inner = PendingTransactionInner { - generator, - utxo_entries, - id, - signable_tx, - addresses, - is_submitted, - payment_value, - change_output_index, - change_output_value, - aggregate_input_value, - aggregate_output_value, - minimum_signatures, - mass, - fees, - kind, - }; - - Ok(PendingTransaction { inner : Arc::new(inner) }) - - } else { - - let inner = PendingTransactionInner { - generator, - utxo_entries, - id, - signable_tx, - addresses, - is_submitted, - payment_value, - change_output_index, - change_output_value, - aggregate_input_value, - aggregate_output_value, - minimum_signatures, - mass, - fees, - kind, - }; - - Ok(PendingTransaction { inner : Arc::new(inner) }) } + } + + let inner = PendingTransactionInner { + generator, + utxo_entries, + id, + signable_tx : Mutex::new(signable_tx), + addresses, + is_submitted, + payment_value, + change_output_index, + change_output_value, + aggregate_input_value, + aggregate_output_value, + minimum_signatures, + mass, + fees, + kind, + }; + + Ok(PendingTransaction { inner: Arc::new(inner) }) + - } // let mut mutable_tx = self.inner.signable_tx.lock()?.clone(); // mutable_tx.tx.fee += fees; // *self.inner.signable_tx.lock().unwrap() = mutable_tx; - } } From 8d06a34f2cc45a3f54f021072cd402abdc5977c3 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Mon, 9 Sep 2024 21:24:04 +0300 Subject: [PATCH 03/13] WIP fee_rate --- wallet/core/src/tx/generator/generator.rs | 21 ++++++++++++++++++--- wallet/core/src/tx/generator/settings.rs | 8 ++++++++ wallet/core/src/tx/generator/test.rs | 2 ++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index b569450be..6bb54ba08 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -289,6 +289,8 @@ struct Inner { standard_change_output_compute_mass: u64, // signature mass per input signature_mass_per_input: u64, + // fee rate + fee_rate : Option, // final transaction amount and fees // `None` is used for sweep transactions final_transaction: Option, @@ -322,6 +324,7 @@ impl std::fmt::Debug for Inner { .field("standard_change_output_compute_mass", &self.standard_change_output_compute_mass) .field("signature_mass_per_input", &self.signature_mass_per_input) // .field("final_transaction", &self.final_transaction) + .field("fee_rate", &self.fee_rate) .field("final_transaction_priority_fee", &self.final_transaction_priority_fee) .field("final_transaction_outputs", &self.final_transaction_outputs) .field("final_transaction_outputs_harmonic", &self.final_transaction_outputs_harmonic) @@ -353,6 +356,7 @@ impl Generator { sig_op_count, minimum_signatures, change_address, + fee_rate, final_transaction_priority_fee, final_transaction_destination, final_transaction_payload, @@ -458,6 +462,7 @@ impl Generator { change_address, standard_change_output_compute_mass: standard_change_output_mass, signature_mass_per_input, + fee_rate, final_transaction, final_transaction_priority_fee, final_transaction_outputs, @@ -637,7 +642,12 @@ impl Generator { /// Calculate relay transaction fees for the current transaction `data` #[inline(always)] fn calc_relay_transaction_compute_fees(&self, data: &Data) -> u64 { - self.inner.mass_calculator.calc_minimum_transaction_fee_from_mass(self.calc_relay_transaction_mass(data)) + let mass = self.calc_relay_transaction_mass(data); + self.inner.mass_calculator.calc_minimum_transaction_fee_from_mass(mass) + self.calc_fee_rate(mass) + } + + fn calc_fees_from_mass(&self, mass : u64) -> u64 { + self.inner.mass_calculator.calc_minimum_transaction_fee_from_mass(mass) + self.calc_fee_rate(mass) } /// Main UTXO entry processing loop. This function sources UTXOs from [`Generator::get_utxo_entry()`] and @@ -794,6 +804,10 @@ impl Generator { calc.calc_storage_mass(output_harmonics, data.aggregate_input_value, data.inputs.len() as u64) } + fn calc_fee_rate(&self, mass : u64) -> u64 { + self.inner.fee_rate.map(|fee_rate| (fee_rate * mass as f64) as u64).unwrap_or(0) + } + /// Check if the current state has sufficient funds for the final transaction, /// initiate new stage if necessary, or finish stage processing creating the /// final transaction. @@ -962,7 +976,7 @@ impl Generator { Err(Error::StorageMassExceedsMaximumTransactionMass { storage_mass }) } else { let transaction_mass = calc.combine_mass(compute_mass_with_change, storage_mass); - let transaction_fees = calc.calc_minimum_transaction_fee_from_mass(transaction_mass); + let transaction_fees = self.calc_fees_from_mass(transaction_mass);//calc.calc_minimum_transaction_fee_from_mass(transaction_mass) + self.calc_fee_rate(transaction_mass); Ok(MassDisposition { transaction_mass, transaction_fees, storage_mass, absorb_change_to_fees }) } @@ -976,7 +990,8 @@ impl Generator { let compute_mass = data.aggregate_mass + self.inner.standard_change_output_compute_mass + self.inner.network_params.additional_compound_transaction_mass(); - let compute_fees = calc.calc_minimum_transaction_fee_from_mass(compute_mass); + let compute_fees = calc.calc_minimum_transaction_fee_from_mass(compute_mass) + self.calc_fee_rate(compute_mass); + // let compute_fees = self.calc_fees_from_mass(compute_mass); // TODO - consider removing this as calculated storage mass should produce `0` value let edge_output_harmonic = diff --git a/wallet/core/src/tx/generator/settings.rs b/wallet/core/src/tx/generator/settings.rs index 34fd1bb6e..b8a6396f6 100644 --- a/wallet/core/src/tx/generator/settings.rs +++ b/wallet/core/src/tx/generator/settings.rs @@ -28,6 +28,8 @@ pub struct GeneratorSettings { pub minimum_signatures: u16, // change address pub change_address: Address, + // fee rate + pub fee_rate: Option, // applies only to the final transaction pub final_transaction_priority_fee: Fees, // final transaction outputs @@ -60,6 +62,7 @@ impl GeneratorSettings { pub fn try_new_with_account( account: Arc, final_transaction_destination: PaymentDestination, + fee_rate: Option, final_priority_fee: Fees, final_transaction_payload: Option>, ) -> Result { @@ -81,6 +84,7 @@ impl GeneratorSettings { source_utxo_context: Some(account.utxo_context().clone()), priority_utxo_entries: None, + fee_rate, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, @@ -97,6 +101,7 @@ impl GeneratorSettings { sig_op_count: u8, minimum_signatures: u16, final_transaction_destination: PaymentDestination, + fee_rate: Option, final_priority_fee: Fees, final_transaction_payload: Option>, multiplexer: Option>>, @@ -114,6 +119,7 @@ impl GeneratorSettings { source_utxo_context: Some(utxo_context), priority_utxo_entries, + fee_rate, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, @@ -130,6 +136,7 @@ impl GeneratorSettings { change_address: Address, sig_op_count: u8, minimum_signatures: u16, + fee_rate : Option, final_transaction_destination: PaymentDestination, final_priority_fee: Fees, final_transaction_payload: Option>, @@ -145,6 +152,7 @@ impl GeneratorSettings { source_utxo_context: None, priority_utxo_entries, + fee_rate, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index 769e13bdf..322620d1b 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -403,6 +403,8 @@ where source_utxo_context, priority_utxo_entries, destination_utxo_context, + // TODO + fee_rate : None, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, From 0ccad51d2af585606b802df8753b27aa653351a5 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Mon, 9 Sep 2024 22:16:55 +0300 Subject: [PATCH 04/13] WIP --- consensus/client/src/utxo.rs | 1 - wallet/core/src/tx/generator/generator.rs | 49 ++++++++++++----------- wallet/core/src/tx/generator/pending.rs | 31 ++++++-------- wallet/core/src/tx/generator/settings.rs | 2 +- wallet/core/src/tx/generator/test.rs | 2 +- 5 files changed, 40 insertions(+), 45 deletions(-) diff --git a/consensus/client/src/utxo.rs b/consensus/client/src/utxo.rs index 5c752296e..3f519d067 100644 --- a/consensus/client/src/utxo.rs +++ b/consensus/client/src/utxo.rs @@ -198,7 +198,6 @@ impl UtxoEntryReference { pub fn transaction_id_as_ref(&self) -> &TransactionId { self.utxo.outpoint.transaction_id_as_ref() } - } impl std::hash::Hash for UtxoEntryReference { diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 6bb54ba08..30e5d6d7e 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -290,7 +290,7 @@ struct Inner { // signature mass per input signature_mass_per_input: u64, // fee rate - fee_rate : Option, + fee_rate: Option, // final transaction amount and fees // `None` is used for sweep transactions final_transaction: Option, @@ -481,78 +481,78 @@ impl Generator { pub fn network_type(&self) -> NetworkType { self.inner.network_id.into() } - + /// Returns the current [`NetworkId`] #[inline(always)] pub fn network_id(&self) -> NetworkId { self.inner.network_id } - + /// Returns current [`NetworkParams`] #[inline(always)] pub fn network_params(&self) -> &NetworkParams { self.inner.network_params } - + /// Returns owned mass calculator instance (bound to [`NetworkParams`] of the [`Generator`]) #[inline(always)] pub fn mass_calculator(&self) -> &MassCalculator { &self.inner.mass_calculator } - + #[inline(always)] pub fn sig_op_count(&self) -> u8 { &self.inner.sig_op_count } - + /// The underlying [`UtxoContext`] (if available). #[inline(always)] pub fn source_utxo_context(&self) -> &Option { &self.inner.source_utxo_context } - + /// Signifies that the transaction is a transfer between accounts #[inline(always)] pub fn destination_utxo_context(&self) -> &Option { &self.inner.destination_utxo_context } - + /// Core [`Multiplexer`] (if available) #[inline(always)] pub fn multiplexer(&self) -> &Option>> { &self.inner.multiplexer } - + /// Mutable context used by the generator to track state #[inline(always)] fn context(&self) -> MutexGuard { self.inner.context.lock().unwrap() } - + /// Returns the underlying instance of the [Signer](SignerT) #[inline(always)] pub(crate) fn signer(&self) -> &Option> { &self.inner.signer } - + /// The total amount of fees in SOMPI consumed during the transaction generation process. #[inline(always)] pub fn aggregate_fees(&self) -> u64 { self.context().aggregate_fees } - + /// The total number of UTXOs consumed during the transaction generation process. #[inline(always)] pub fn aggregate_utxos(&self) -> usize { self.context().aggregated_utxos } - + /// The final transaction amount (if available). #[inline(always)] pub fn final_transaction_value_no_fees(&self) -> Option { self.inner.final_transaction.as_ref().map(|final_transaction| final_transaction.value_no_fees) } - + /// Returns the final transaction id if the generator has finished successfully. #[inline(always)] pub fn final_transaction_id(&self) -> Option { @@ -566,7 +566,7 @@ impl Generator { pub fn stream(&self) -> impl Stream> { Box::pin(PendingTransactionStream::new(self)) } - + /// Returns an iterator that causes the [Generator] to produce /// transaction for each iterator poll request. NOTE: transactions /// are generated only when the returned iterator is iterated. @@ -574,7 +574,7 @@ impl Generator { pub fn iter(&self) -> impl Iterator> { PendingTransactionIterator::new(self) } - + /// Get next UTXO entry. This function obtains UTXO in the following order: /// 1. From the UTXO stash (used to store UTxOs that were consumed during previous transaction generation but were rejected due to various conditions, such as mass overflow) /// 2. From the current stage @@ -618,7 +618,7 @@ impl Generator { // Ok(utxo_entry) // } - /// Adds a [`UtxoEntryReference`] to the UTXO stash. UTXO stash + /// Adds a [`UtxoEntryReference`] to the UTXO stash. UTXO stash /// is the first source of UTXO entries. pub fn stash(&self, into_iter: impl IntoIterator) { // let iter = iter.into_iterator(); @@ -627,7 +627,7 @@ impl Generator { self.context().utxo_stash.extend(into_iter.into_iter()); } - // /// Adds multiple [`UtxoEntryReference`] structs to the UTXO stash. UTXO stash + // /// Adds multiple [`UtxoEntryReference`] structs to the UTXO stash. UTXO stash // /// is the first source of UTXO entries. // pub fn stash_multiple(&self, utxo_entries: Vec) { // self.context().utxo_stash.extend(utxo_entries); @@ -646,7 +646,7 @@ impl Generator { self.inner.mass_calculator.calc_minimum_transaction_fee_from_mass(mass) + self.calc_fee_rate(mass) } - fn calc_fees_from_mass(&self, mass : u64) -> u64 { + fn calc_fees_from_mass(&self, mass: u64) -> u64 { self.inner.mass_calculator.calc_minimum_transaction_fee_from_mass(mass) + self.calc_fee_rate(mass) } @@ -804,7 +804,7 @@ impl Generator { calc.calc_storage_mass(output_harmonics, data.aggregate_input_value, data.inputs.len() as u64) } - fn calc_fee_rate(&self, mass : u64) -> u64 { + fn calc_fee_rate(&self, mass: u64) -> u64 { self.inner.fee_rate.map(|fee_rate| (fee_rate * mass as f64) as u64).unwrap_or(0) } @@ -976,7 +976,7 @@ impl Generator { Err(Error::StorageMassExceedsMaximumTransactionMass { storage_mass }) } else { let transaction_mass = calc.combine_mass(compute_mass_with_change, storage_mass); - let transaction_fees = self.calc_fees_from_mass(transaction_mass);//calc.calc_minimum_transaction_fee_from_mass(transaction_mass) + self.calc_fee_rate(transaction_mass); + let transaction_fees = self.calc_fees_from_mass(transaction_mass); //calc.calc_minimum_transaction_fee_from_mass(transaction_mass) + self.calc_fee_rate(transaction_mass); Ok(MassDisposition { transaction_mass, transaction_fees, storage_mass, absorb_change_to_fees }) } @@ -990,8 +990,8 @@ impl Generator { let compute_mass = data.aggregate_mass + self.inner.standard_change_output_compute_mass + self.inner.network_params.additional_compound_transaction_mass(); - let compute_fees = calc.calc_minimum_transaction_fee_from_mass(compute_mass) + self.calc_fee_rate(compute_mass); - // let compute_fees = self.calc_fees_from_mass(compute_mass); + // let compute_fees = calc.calc_minimum_transaction_fee_from_mass(compute_mass) + self.calc_fee_rate(compute_mass); + let compute_fees = self.calc_fees_from_mass(compute_mass); // TODO - consider removing this as calculated storage mass should produce `0` value let edge_output_harmonic = @@ -1010,7 +1010,8 @@ impl Generator { } } else { data.aggregate_mass = transaction_mass; - data.transaction_fees = calc.calc_minimum_transaction_fee_from_mass(transaction_mass); + data.transaction_fees = + calc.calc_minimum_transaction_fee_from_mass(transaction_mass) + self.calc_fee_rate(transaction_mass); stage.aggregate_fees += data.transaction_fees; context.aggregate_fees += data.transaction_fees; Ok(Some(DataKind::Edge)) diff --git a/wallet/core/src/tx/generator/pending.rs b/wallet/core/src/tx/generator/pending.rs index 2d2591ab4..02c97c113 100644 --- a/wallet/core/src/tx/generator/pending.rs +++ b/wallet/core/src/tx/generator/pending.rs @@ -358,7 +358,7 @@ impl PendingTransaction { DataKind::Final => { // change output has sufficient amount to cover fee increase // if change_output_value > fee_increase && change_output_index.is_some() { - if let (Some(index), true) = (change_output_index,change_output_value >= additional_fees) { + if let (Some(index), true) = (change_output_index, change_output_value >= additional_fees) { change_output_value -= additional_fees; if generator.mass_calculator().is_dust(change_output_value) { aggregate_output_value -= change_output_value; @@ -384,25 +384,27 @@ impl PendingTransaction { available += value; // aggregate_input_value += value; - utxo_entries_rbf.push(utxo_entry); // signable_tx.lock().unwrap().tx.inputs.push(utxo.as_input()); } else { // generator.stash(utxo_entries_rbf); // utxo_entries_rbf.into_iter().for_each(|utxo_entry|generator.stash(utxo_entry)); - return Err(Error::InsufficientFunds { additional_needed : additional_fees - available, origin : "increase_fees_for_rbf" }); + return Err(Error::InsufficientFunds { + additional_needed: additional_fees - available, + origin: "increase_fees_for_rbf", + }); } } let utxo_entries_vec = utxo_entries .iter() - .map(|(_,utxo_entry)| utxo_entry.as_ref().clone()) - .chain(utxo_entries_rbf.iter().map(|utxo_entry|utxo_entry.as_ref().clone())) + .map(|(_, utxo_entry)| utxo_entry.as_ref().clone()) + .chain(utxo_entries_rbf.iter().map(|utxo_entry| utxo_entry.as_ref().clone())) .collect::>(); - let inputs = utxo_entries_rbf.into_iter().map(|utxo| { - TransactionInput::new(utxo.outpoint().clone().into(), vec![], 0, generator.sig_op_count()) - }); + let inputs = utxo_entries_rbf + .into_iter() + .map(|utxo| TransactionInput::new(utxo.outpoint().clone().into(), vec![], 0, generator.sig_op_count())); signable_tx.tx.inputs.extend(inputs); @@ -416,25 +418,20 @@ impl PendingTransaction { return Err(Error::MassCalculationError); } signable_tx.tx.set_mass(transaction_mass); - + // utxo // let input = ; - - } - - } - _ => { - } + _ => {} } let inner = PendingTransactionInner { generator, utxo_entries, id, - signable_tx : Mutex::new(signable_tx), + signable_tx: Mutex::new(signable_tx), addresses, is_submitted, payment_value, @@ -450,8 +447,6 @@ impl PendingTransaction { Ok(PendingTransaction { inner: Arc::new(inner) }) - - // let mut mutable_tx = self.inner.signable_tx.lock()?.clone(); // mutable_tx.tx.fee += fees; // *self.inner.signable_tx.lock().unwrap() = mutable_tx; diff --git a/wallet/core/src/tx/generator/settings.rs b/wallet/core/src/tx/generator/settings.rs index b8a6396f6..846a04080 100644 --- a/wallet/core/src/tx/generator/settings.rs +++ b/wallet/core/src/tx/generator/settings.rs @@ -136,7 +136,7 @@ impl GeneratorSettings { change_address: Address, sig_op_count: u8, minimum_signatures: u16, - fee_rate : Option, + fee_rate: Option, final_transaction_destination: PaymentDestination, final_priority_fee: Fees, final_transaction_payload: Option>, diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index 322620d1b..0240fd91f 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -404,7 +404,7 @@ where priority_utxo_entries, destination_utxo_context, // TODO - fee_rate : None, + fee_rate: None, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, From bc254da38f76ba609c84197dc58996ac56484b2d Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Mon, 9 Sep 2024 23:14:01 +0300 Subject: [PATCH 05/13] fee rate propagation --- cli/src/modules/account.rs | 7 ++++-- cli/src/modules/estimate.rs | 5 +++- cli/src/modules/pskb.rs | 5 ++++ cli/src/modules/send.rs | 3 +++ cli/src/modules/sweep.rs | 3 +++ cli/src/modules/transfer.rs | 3 +++ wallet/core/src/account/mod.rs | 25 +++++++++++++++---- wallet/core/src/account/pskb.rs | 1 + wallet/core/src/api/message.rs | 3 +++ wallet/core/src/tx/generator/generator.rs | 9 ++----- wallet/core/src/tx/generator/pending.rs | 25 +++++++++++-------- wallet/core/src/tx/generator/settings.rs | 3 ++- wallet/core/src/wallet/api.rs | 11 +++++--- wallet/core/src/wasm/api/message.rs | 14 +++++++++-- .../core/src/wasm/tx/generator/generator.rs | 15 +++++++++++ 15 files changed, 100 insertions(+), 32 deletions(-) diff --git a/cli/src/modules/account.rs b/cli/src/modules/account.rs index 5848d43fb..9d423e46d 100644 --- a/cli/src/modules/account.rs +++ b/cli/src/modules/account.rs @@ -234,8 +234,9 @@ impl Account { count = count.max(1); let sweep = action.eq("sweep"); - - self.derivation_scan(&ctx, start, count, window, sweep).await?; + // TODO fee_rate + let fee_rate = None; + self.derivation_scan(&ctx, start, count, window, sweep, fee_rate).await?; } v => { tprintln!(ctx, "unknown command: '{v}'\r\n"); @@ -276,6 +277,7 @@ impl Account { count: usize, window: usize, sweep: bool, + fee_rate: Option, ) -> Result<()> { let account = ctx.account().await?; let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(Some(&account)).await?; @@ -293,6 +295,7 @@ impl Account { start + count, window, sweep, + fee_rate, &abortable, Some(Arc::new(move |processed: usize, _, balance, txid| { if let Some(txid) = txid { diff --git a/cli/src/modules/estimate.rs b/cli/src/modules/estimate.rs index a37a8a47c..9ab717d54 100644 --- a/cli/src/modules/estimate.rs +++ b/cli/src/modules/estimate.rs @@ -17,13 +17,16 @@ impl Estimate { } let amount_sompi = try_parse_required_nonzero_kaspa_as_sompi_u64(argv.first())?; + // TODO fee_rate + let fee_rate = None; let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.get(1))?.unwrap_or(0); let abortable = Abortable::default(); // just use any address for an estimate (change address) let change_address = account.change_address()?; let destination = PaymentDestination::PaymentOutputs(PaymentOutputs::from((change_address.clone(), amount_sompi))); - let estimate = account.estimate(destination, priority_fee_sompi.into(), None, &abortable).await?; + // TODO fee_rate + let estimate = account.estimate(destination, fee_rate, priority_fee_sompi.into(), None, &abortable).await?; tprintln!(ctx, "Estimate - {estimate}"); diff --git a/cli/src/modules/pskb.rs b/cli/src/modules/pskb.rs index fd33087c2..3757f939a 100644 --- a/cli/src/modules/pskb.rs +++ b/cli/src/modules/pskb.rs @@ -45,6 +45,8 @@ impl Pskb { let signer = account .pskb_from_send_generator( outputs.into(), + // fee_rate + None, priority_fee_sompi.into(), None, wallet_secret.clone(), @@ -89,12 +91,15 @@ impl Pskb { "lock" => { let amount_sompi = try_parse_required_nonzero_kaspa_as_sompi_u64(argv.first())?; let outputs = PaymentOutputs::from((script_p2sh, amount_sompi)); + // TODO fee_rate + let fee_rate = None; let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.get(1))?.unwrap_or(0); let abortable = Abortable::default(); let signer = account .pskb_from_send_generator( outputs.into(), + fee_rate, priority_fee_sompi.into(), None, wallet_secret.clone(), diff --git a/cli/src/modules/send.rs b/cli/src/modules/send.rs index 773861dd4..8c28679a9 100644 --- a/cli/src/modules/send.rs +++ b/cli/src/modules/send.rs @@ -18,6 +18,8 @@ impl Send { let address = Address::try_from(argv.first().unwrap().as_str())?; let amount_sompi = try_parse_required_nonzero_kaspa_as_sompi_u64(argv.get(1))?; + // TODO fee_rate + let fee_rate = None; let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.get(2))?.unwrap_or(0); let outputs = PaymentOutputs::from((address.clone(), amount_sompi)); let abortable = Abortable::default(); @@ -27,6 +29,7 @@ impl Send { let (summary, _ids) = account .send( outputs.into(), + fee_rate, priority_fee_sompi.into(), None, wallet_secret, diff --git a/cli/src/modules/sweep.rs b/cli/src/modules/sweep.rs index aeca2baa3..6e68b3945 100644 --- a/cli/src/modules/sweep.rs +++ b/cli/src/modules/sweep.rs @@ -10,12 +10,15 @@ impl Sweep { let account = ctx.wallet().account()?; let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(Some(&account)).await?; + // TODO fee_rate + let fee_rate = None; let abortable = Abortable::default(); // let ctx_ = ctx.clone(); let (summary, _ids) = account .sweep( wallet_secret, payment_secret, + fee_rate, &abortable, Some(Arc::new(move |_ptx| { // tprintln!(ctx_, "Sending transaction: {}", ptx.id()); diff --git a/cli/src/modules/transfer.rs b/cli/src/modules/transfer.rs index 3dea69299..0caf0e493 100644 --- a/cli/src/modules/transfer.rs +++ b/cli/src/modules/transfer.rs @@ -21,6 +21,8 @@ impl Transfer { return Err("Cannot transfer to the same account".into()); } let amount_sompi = try_parse_required_nonzero_kaspa_as_sompi_u64(argv.get(1))?; + // TODO fee_rate + let fee_rate = None; let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.get(2))?.unwrap_or(0); let target_address = target_account.receive_address()?; let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(Some(&account)).await?; @@ -32,6 +34,7 @@ impl Transfer { let (summary, _ids) = account .send( outputs.into(), + fee_rate, priority_fee_sompi.into(), None, wallet_secret, diff --git a/wallet/core/src/account/mod.rs b/wallet/core/src/account/mod.rs index 31c7fea9d..3314978e6 100644 --- a/wallet/core/src/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -305,13 +305,19 @@ pub trait Account: AnySync + Send + Sync + 'static { self: Arc, wallet_secret: Secret, payment_secret: Option, + fee_rate: Option, abortable: &Abortable, notifier: Option, ) -> Result<(GeneratorSummary, Vec)> { let keydata = self.prv_key_data(wallet_secret).await?; let signer = Arc::new(Signer::new(self.clone().as_dyn_arc(), keydata, payment_secret)); - let settings = - GeneratorSettings::try_new_with_account(self.clone().as_dyn_arc(), PaymentDestination::Change, Fees::None, None)?; + let settings = GeneratorSettings::try_new_with_account( + self.clone().as_dyn_arc(), + PaymentDestination::Change, + fee_rate, + Fees::None, + None, + )?; let generator = Generator::try_new(settings, Some(signer), Some(abortable))?; let mut stream = generator.stream(); @@ -334,6 +340,7 @@ pub trait Account: AnySync + Send + Sync + 'static { async fn send( self: Arc, destination: PaymentDestination, + fee_rate: Option, priority_fee_sompi: Fees, payload: Option>, wallet_secret: Secret, @@ -344,7 +351,8 @@ pub trait Account: AnySync + Send + Sync + 'static { let keydata = self.prv_key_data(wallet_secret).await?; let signer = Arc::new(Signer::new(self.clone().as_dyn_arc(), keydata, payment_secret)); - let settings = GeneratorSettings::try_new_with_account(self.clone().as_dyn_arc(), destination, priority_fee_sompi, payload)?; + let settings = + GeneratorSettings::try_new_with_account(self.clone().as_dyn_arc(), destination, fee_rate, priority_fee_sompi, payload)?; let generator = Generator::try_new(settings, Some(signer), Some(abortable))?; @@ -366,13 +374,15 @@ pub trait Account: AnySync + Send + Sync + 'static { async fn pskb_from_send_generator( self: Arc, destination: PaymentDestination, + fee_rate: Option, priority_fee_sompi: Fees, payload: Option>, wallet_secret: Secret, payment_secret: Option, abortable: &Abortable, ) -> Result { - let settings = GeneratorSettings::try_new_with_account(self.clone().as_dyn_arc(), destination, priority_fee_sompi, payload)?; + let settings = + GeneratorSettings::try_new_with_account(self.clone().as_dyn_arc(), destination, fee_rate, priority_fee_sompi, payload)?; let keydata = self.prv_key_data(wallet_secret).await?; let signer = Arc::new(PSKBSigner::new(self.clone().as_dyn_arc(), keydata, payment_secret)); let generator = Generator::try_new(settings, None, Some(abortable))?; @@ -428,6 +438,7 @@ pub trait Account: AnySync + Send + Sync + 'static { self: Arc, destination_account_id: AccountId, transfer_amount_sompi: u64, + fee_rate: Option, priority_fee_sompi: Fees, wallet_secret: Secret, payment_secret: Option, @@ -451,6 +462,7 @@ pub trait Account: AnySync + Send + Sync + 'static { let settings = GeneratorSettings::try_new_with_account( self.clone().as_dyn_arc(), final_transaction_destination, + fee_rate, priority_fee_sompi, final_transaction_payload, )? @@ -476,11 +488,12 @@ pub trait Account: AnySync + Send + Sync + 'static { async fn estimate( self: Arc, destination: PaymentDestination, + fee_rate: Option, priority_fee_sompi: Fees, payload: Option>, abortable: &Abortable, ) -> Result { - let settings = GeneratorSettings::try_new_with_account(self.as_dyn_arc(), destination, priority_fee_sompi, payload)?; + let settings = GeneratorSettings::try_new_with_account(self.as_dyn_arc(), destination, fee_rate, priority_fee_sompi, payload)?; let generator = Generator::try_new(settings, None, Some(abortable))?; @@ -531,6 +544,7 @@ pub trait DerivationCapableAccount: Account { extent: usize, window: usize, sweep: bool, + fee_rate: Option, abortable: &Abortable, notifier: Option, ) -> Result<()> { @@ -605,6 +619,7 @@ pub trait DerivationCapableAccount: Account { 1, 1, PaymentDestination::Change, + fee_rate, Fees::None, None, None, diff --git a/wallet/core/src/account/pskb.rs b/wallet/core/src/account/pskb.rs index 8fc46088b..7aa817e90 100644 --- a/wallet/core/src/account/pskb.rs +++ b/wallet/core/src/account/pskb.rs @@ -333,6 +333,7 @@ pub fn pskt_to_pending_transaction( priority_utxo_entries: None, source_utxo_context: None, destination_utxo_context: None, + fee_rate: None, final_transaction_priority_fee: fee_u.into(), final_transaction_destination, final_transaction_payload: None, diff --git a/wallet/core/src/api/message.rs b/wallet/core/src/api/message.rs index 3b96abd1a..dba09b951 100644 --- a/wallet/core/src/api/message.rs +++ b/wallet/core/src/api/message.rs @@ -490,6 +490,7 @@ pub struct AccountsSendRequest { pub wallet_secret: Secret, pub payment_secret: Option, pub destination: PaymentDestination, + pub fee_rate: Option, pub priority_fee_sompi: Fees, pub payload: Option>, } @@ -509,6 +510,7 @@ pub struct AccountsTransferRequest { pub wallet_secret: Secret, pub payment_secret: Option, pub transfer_amount_sompi: u64, + pub fee_rate: Option, pub priority_fee_sompi: Option, // pub priority_fee_sompi: Fees, } @@ -527,6 +529,7 @@ pub struct AccountsTransferResponse { pub struct AccountsEstimateRequest { pub account_id: AccountId, pub destination: PaymentDestination, + pub fee_rate: Option, pub priority_fee_sompi: Fees, pub payload: Option>, } diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index fc49902e6..8a7d9be1b 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -103,10 +103,6 @@ struct Context { number_of_transactions: usize, /// current tree stage stage: Option>, - /// stage during the final transaction generation - /// preserved in case we need to increase priority fees - /// after the final transaction has been generated. - final_stage: Option>, /// Rejected or "stashed" UTXO entries that are consumed before polling /// the iterator. This store is used in edge cases when UTXO entry from the /// iterator has been consumed but was rejected due to mass constraints or @@ -442,7 +438,6 @@ impl Generator { aggregated_utxos: 0, aggregate_fees: 0, stage: Some(Box::default()), - final_stage: None, utxo_stash: VecDeque::default(), final_transaction_id: None, is_done: false, @@ -502,7 +497,7 @@ impl Generator { #[inline(always)] pub fn sig_op_count(&self) -> u8 { - &self.inner.sig_op_count + self.inner.sig_op_count } /// The underlying [`UtxoContext`] (if available). @@ -624,7 +619,7 @@ impl Generator { // let iter = iter.into_iterator(); // let mut context = self.context(); // context.utxo_stash.extend(iter); - self.context().utxo_stash.extend(into_iter.into_iter()); + self.context().utxo_stash.extend(into_iter); } // /// Adds multiple [`UtxoEntryReference`] structs to the UTXO stash. UTXO stash diff --git a/wallet/core/src/tx/generator/pending.rs b/wallet/core/src/tx/generator/pending.rs index 02c97c113..9a3bbdd48 100644 --- a/wallet/core/src/tx/generator/pending.rs +++ b/wallet/core/src/tx/generator/pending.rs @@ -2,6 +2,7 @@ //! Pending transaction encapsulating a //! transaction generated by the [`Generator`]. //! +#![allow(unused_imports)] use crate::imports::*; use crate::result::Result; @@ -319,6 +320,9 @@ impl PendingTransaction { } pub fn increase_fees_for_rbf(&self, additional_fees: u64) -> Result { + #![allow(unused_mut)] + #![allow(unused_variables)] + let PendingTransactionInner { generator, utxo_entries, @@ -354,6 +358,7 @@ impl PendingTransaction { let fees = *fees; let kind = *kind; + #[allow(clippy::single_match)] match kind { DataKind::Final => { // change output has sufficient amount to cover fee increase @@ -408,16 +413,16 @@ impl PendingTransaction { signable_tx.tx.inputs.extend(inputs); - let transaction_mass = generator.mass_calculator().calc_overall_mass_for_unsigned_consensus_transaction( - &signable_tx.tx, - &utxo_entries_vec, - self.inner.minimum_signatures, - )?; - if transaction_mass > MAXIMUM_STANDARD_TRANSACTION_MASS { - // this should never occur as we should not produce transactions higher than the mass limit - return Err(Error::MassCalculationError); - } - signable_tx.tx.set_mass(transaction_mass); + // let transaction_mass = generator.mass_calculator().calc_overall_mass_for_unsigned_consensus_transaction( + // &signable_tx.tx, + // &utxo_entries_vec, + // self.inner.minimum_signatures, + // )?; + // if transaction_mass > MAXIMUM_STANDARD_TRANSACTION_MASS { + // // this should never occur as we should not produce transactions higher than the mass limit + // return Err(Error::MassCalculationError); + // } + // signable_tx.tx.set_mass(transaction_mass); // utxo diff --git a/wallet/core/src/tx/generator/settings.rs b/wallet/core/src/tx/generator/settings.rs index 846a04080..a1fcf2acf 100644 --- a/wallet/core/src/tx/generator/settings.rs +++ b/wallet/core/src/tx/generator/settings.rs @@ -129,6 +129,7 @@ impl GeneratorSettings { Ok(settings) } + #[allow(clippy::too_many_arguments)] pub fn try_new_with_iterator( network_id: NetworkId, utxo_iterator: Box + Send + Sync + 'static>, @@ -136,8 +137,8 @@ impl GeneratorSettings { change_address: Address, sig_op_count: u8, minimum_signatures: u16, - fee_rate: Option, final_transaction_destination: PaymentDestination, + fee_rate: Option, final_priority_fee: Fees, final_transaction_payload: Option>, multiplexer: Option>>, diff --git a/wallet/core/src/wallet/api.rs b/wallet/core/src/wallet/api.rs index adeb00075..c4b6fa151 100644 --- a/wallet/core/src/wallet/api.rs +++ b/wallet/core/src/wallet/api.rs @@ -377,7 +377,8 @@ impl WalletApi for super::Wallet { } async fn accounts_send_call(self: Arc, request: AccountsSendRequest) -> Result { - let AccountsSendRequest { account_id, wallet_secret, payment_secret, destination, priority_fee_sompi, payload } = request; + let AccountsSendRequest { account_id, wallet_secret, payment_secret, destination, fee_rate, priority_fee_sompi, payload } = + request; let guard = self.guard(); let guard = guard.lock().await; @@ -385,7 +386,7 @@ impl WalletApi for super::Wallet { let abortable = Abortable::new(); let (generator_summary, transaction_ids) = - account.send(destination, priority_fee_sompi, payload, wallet_secret, payment_secret, &abortable, None).await?; + account.send(destination, fee_rate, priority_fee_sompi, payload, wallet_secret, payment_secret, &abortable, None).await?; Ok(AccountsSendResponse { generator_summary, transaction_ids }) } @@ -396,6 +397,7 @@ impl WalletApi for super::Wallet { destination_account_id, wallet_secret, payment_secret, + fee_rate, priority_fee_sompi, transfer_amount_sompi, } = request; @@ -411,6 +413,7 @@ impl WalletApi for super::Wallet { .transfer( destination_account_id, transfer_amount_sompi, + fee_rate, priority_fee_sompi.unwrap_or(Fees::SenderPays(0)), wallet_secret, payment_secret, @@ -424,7 +427,7 @@ impl WalletApi for super::Wallet { } async fn accounts_estimate_call(self: Arc, request: AccountsEstimateRequest) -> Result { - let AccountsEstimateRequest { account_id, destination, priority_fee_sompi, payload } = request; + let AccountsEstimateRequest { account_id, destination, fee_rate, priority_fee_sompi, payload } = request; let guard = self.guard(); let guard = guard.lock().await; @@ -443,7 +446,7 @@ impl WalletApi for super::Wallet { let abortable = Abortable::new(); self.inner.estimation_abortables.lock().unwrap().insert(account_id, abortable.clone()); - let result = account.estimate(destination, priority_fee_sompi, payload, &abortable).await; + let result = account.estimate(destination, fee_rate, priority_fee_sompi, payload, &abortable).await; self.inner.estimation_abortables.lock().unwrap().remove(&account_id); Ok(AccountsEstimateResponse { generator_summary: result? }) diff --git a/wallet/core/src/wasm/api/message.rs b/wallet/core/src/wasm/api/message.rs index 8a023267b..b7000de2d 100644 --- a/wallet/core/src/wasm/api/message.rs +++ b/wallet/core/src/wasm/api/message.rs @@ -1372,6 +1372,10 @@ declare! { * Optional key encryption secret or BIP39 passphrase. */ paymentSecret? : string; + /** + * Fee rate in sompi per 1 gram of mass. + */ + feeRate? : number; /** * Priority fee. */ @@ -1392,6 +1396,7 @@ try_from! ( args: IAccountsSendRequest, AccountsSendRequest, { let account_id = args.get_account_id("accountId")?; let wallet_secret = args.get_secret("walletSecret")?; let payment_secret = args.try_get_secret("paymentSecret")?; + let fee_rate = args.get_f64("feeRate").ok(); let priority_fee_sompi = args.get::("priorityFeeSompi")?.try_into()?; let payload = args.try_get_value("payload")?.map(|v| v.try_as_vec_u8()).transpose()?; @@ -1399,7 +1404,7 @@ try_from! ( args: IAccountsSendRequest, AccountsSendRequest, { let destination: PaymentDestination = if outputs.is_undefined() { PaymentDestination::Change } else { PaymentOutputs::try_owned_from(outputs)?.into() }; - Ok(AccountsSendRequest { account_id, wallet_secret, payment_secret, priority_fee_sompi, destination, payload }) + Ok(AccountsSendRequest { account_id, wallet_secret, payment_secret, fee_rate, priority_fee_sompi, destination, payload }) }); declare! { @@ -1446,6 +1451,7 @@ declare! { destinationAccountId : HexString; walletSecret : string; paymentSecret? : string; + feeRate? : number; priorityFeeSompi? : IFees | bigint; transferAmountSompi : bigint; } @@ -1457,6 +1463,7 @@ try_from! ( args: IAccountsTransferRequest, AccountsTransferRequest, { let destination_account_id = args.get_account_id("destinationAccountId")?; let wallet_secret = args.get_secret("walletSecret")?; let payment_secret = args.try_get_secret("paymentSecret")?; + let fee_rate = args.get_f64("feeRate").ok(); let priority_fee_sompi = args.try_get::("priorityFeeSompi")?.map(Fees::try_from).transpose()?; let transfer_amount_sompi = args.get_u64("transferAmountSompi")?; @@ -1465,6 +1472,7 @@ try_from! ( args: IAccountsTransferRequest, AccountsTransferRequest, { destination_account_id, wallet_secret, payment_secret, + fee_rate, priority_fee_sompi, transfer_amount_sompi, }) @@ -1505,6 +1513,7 @@ declare! { export interface IAccountsEstimateRequest { accountId : HexString; destination : IPaymentOutput[]; + feeRate? : number; priorityFeeSompi : IFees | bigint; payload? : Uint8Array | string; } @@ -1513,6 +1522,7 @@ declare! { try_from! ( args: IAccountsEstimateRequest, AccountsEstimateRequest, { let account_id = args.get_account_id("accountId")?; + let fee_rate = args.get_f64("feeRate").ok(); let priority_fee_sompi = args.get::("priorityFeeSompi")?.try_into()?; let payload = args.try_get_value("payload")?.map(|v| v.try_as_vec_u8()).transpose()?; @@ -1520,7 +1530,7 @@ try_from! ( args: IAccountsEstimateRequest, AccountsEstimateRequest, { let destination: PaymentDestination = if outputs.is_undefined() { PaymentDestination::Change } else { PaymentOutputs::try_owned_from(outputs)?.into() }; - Ok(AccountsEstimateRequest { account_id, priority_fee_sompi, destination, payload }) + Ok(AccountsEstimateRequest { account_id, fee_rate, priority_fee_sompi, destination, payload }) }); declare! { diff --git a/wallet/core/src/wasm/tx/generator/generator.rs b/wallet/core/src/wasm/tx/generator/generator.rs index 5724b8481..8cc236238 100644 --- a/wallet/core/src/wasm/tx/generator/generator.rs +++ b/wallet/core/src/wasm/tx/generator/generator.rs @@ -42,6 +42,14 @@ interface IGeneratorSettingsObject { * Address to be used for change, if any. */ changeAddress: Address | string; + /** + * Fee rate in SOMPI per 1 gram of mass. + * + * Fee rate is applied to all transactions generated by the {@link Generator}. + * This includes batch and final transactions. If not set, the fee rate is + * not applied. + */ + feeRate?: number; /** * Priority fee in SOMPI. * @@ -160,6 +168,7 @@ impl Generator { multiplexer, final_transaction_destination, change_address, + fee_rate, final_priority_fee, sig_op_count, minimum_signatures, @@ -182,6 +191,7 @@ impl Generator { sig_op_count, minimum_signatures, final_transaction_destination, + fee_rate, final_priority_fee, payload, multiplexer, @@ -198,6 +208,7 @@ impl Generator { sig_op_count, minimum_signatures, final_transaction_destination, + fee_rate, final_priority_fee, payload, multiplexer, @@ -260,6 +271,7 @@ struct GeneratorSettings { pub multiplexer: Option>>, pub final_transaction_destination: PaymentDestination, pub change_address: Option
, + pub fee_rate: Option, pub final_priority_fee: Fees, pub sig_op_count: u8, pub minimum_signatures: u16, @@ -278,6 +290,8 @@ impl TryFrom for GeneratorSettings { let change_address = args.try_cast_into::
("changeAddress")?; + let fee_rate = args.get_f64("feeRate").ok().and_then(|v| (v.is_finite() && !v.is_nan() && v >= 1e-8).then_some(v)); + let final_priority_fee = args.get::("priorityFee")?.try_into()?; let generator_source = if let Ok(Some(context)) = args.try_cast_into::("entries") { @@ -310,6 +324,7 @@ impl TryFrom for GeneratorSettings { multiplexer: None, final_transaction_destination, change_address, + fee_rate, final_priority_fee, sig_op_count, minimum_signatures, From e8cbe86bf3f3ecf215f882c6dc1a8634ae25cb79 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Mon, 9 Sep 2024 23:20:22 +0300 Subject: [PATCH 06/13] propagate fee_rate in generator tests --- wallet/core/src/tx/generator/test.rs | 70 ++++++++++++++++++++-------- wallet/core/src/utxo/test.rs | 3 +- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index f4e439d7d..cb88f7eff 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -376,7 +376,14 @@ impl Harness { } } -pub(crate) fn generator(network_id: NetworkId, head: &[f64], tail: &[f64], fees: Fees, outputs: &[(F, T)]) -> Result +pub(crate) fn generator( + network_id: NetworkId, + head: &[f64], + tail: &[f64], + fee_rate: Option, + fees: Fees, + outputs: &[(F, T)], +) -> Result where T: Into + Clone, F: FnOnce(NetworkType) -> Address + Clone, @@ -388,13 +395,14 @@ where (address.clone()(network_id.into()), sompi.0) }) .collect::>(); - make_generator(network_id, head, tail, fees, change_address, PaymentOutputs::from(outputs.as_slice()).into()) + make_generator(network_id, head, tail, fee_rate, fees, change_address, PaymentOutputs::from(outputs.as_slice()).into()) } pub(crate) fn make_generator( network_id: NetworkId, head: &[f64], tail: &[f64], + fee_rate: Option, fees: Fees, change_address: F, final_transaction_destination: PaymentDestination, @@ -427,8 +435,7 @@ where source_utxo_context, priority_utxo_entries, destination_utxo_context, - // TODO - fee_rate: None, + fee_rate, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, @@ -455,7 +462,7 @@ pub(crate) fn output_address(network_type: NetworkType) -> Address { #[test] fn test_generator_empty_utxo_noop() -> Result<()> { - let generator = make_generator(test_network_id(), &[], &[], Fees::None, change_address, PaymentDestination::Change).unwrap(); + let generator = make_generator(test_network_id(), &[], &[], None, Fees::None, change_address, PaymentDestination::Change).unwrap(); let tx = generator.generate_transaction().unwrap(); assert!(tx.is_none()); Ok(()) @@ -463,7 +470,7 @@ fn test_generator_empty_utxo_noop() -> Result<()> { #[test] fn test_generator_sweep_single_utxo_noop() -> Result<()> { - let generator = make_generator(test_network_id(), &[10.0], &[], Fees::None, change_address, PaymentDestination::Change) + let generator = make_generator(test_network_id(), &[10.0], &[], None, Fees::None, change_address, PaymentDestination::Change) .expect("single UTXO input: generator"); let tx = generator.generate_transaction().unwrap(); assert!(tx.is_none()); @@ -472,7 +479,7 @@ fn test_generator_sweep_single_utxo_noop() -> Result<()> { #[test] fn test_generator_sweep_two_utxos() -> Result<()> { - make_generator(test_network_id(), &[10.0, 10.0], &[], Fees::None, change_address, PaymentDestination::Change) + make_generator(test_network_id(), &[10.0, 10.0], &[], None, Fees::None, change_address, PaymentDestination::Change) .expect("merge 2 UTXOs without fees: generator") .harness() .fetch(&Expected { @@ -488,8 +495,15 @@ fn test_generator_sweep_two_utxos() -> Result<()> { #[test] fn test_generator_sweep_two_utxos_with_priority_fees_rejection() -> Result<()> { - let generator = - make_generator(test_network_id(), &[10.0, 10.0], &[], Fees::sender(Kaspa(5.0)), change_address, PaymentDestination::Change); + let generator = make_generator( + test_network_id(), + &[10.0, 10.0], + &[], + None, + Fees::sender(Kaspa(5.0)), + change_address, + PaymentDestination::Change, + ); match generator { Err(Error::GeneratorFeesInSweepTransaction) => {} _ => panic!("merge 2 UTXOs with fees must fail generator creation"), @@ -499,11 +513,18 @@ fn test_generator_sweep_two_utxos_with_priority_fees_rejection() -> Result<()> { #[test] fn test_generator_compound_200k_10kas_transactions() -> Result<()> { - generator(test_network_id(), &[10.0; 200_000], &[], Fees::sender(Kaspa(5.0)), [(output_address, Kaspa(190_000.0))].as_slice()) - .unwrap() - .harness() - .validate() - .finalize(); + generator( + test_network_id(), + &[10.0; 200_000], + &[], + None, + Fees::sender(Kaspa(5.0)), + [(output_address, Kaspa(190_000.0))].as_slice(), + ) + .unwrap() + .harness() + .validate() + .finalize(); Ok(()) } @@ -514,7 +535,11 @@ fn test_generator_compound_100k_random_transactions() -> Result<()> { let inputs: Vec = (0..100_000).map(|_| rng.gen_range(0.001..10.0)).collect(); let total = inputs.iter().sum::(); let outputs = [(output_address, Kaspa(total - 10.0))]; - generator(test_network_id(), &inputs, &[], Fees::sender(Kaspa(5.0)), outputs.as_slice()).unwrap().harness().validate().finalize(); + generator(test_network_id(), &inputs, &[], None, Fees::sender(Kaspa(5.0)), outputs.as_slice()) + .unwrap() + .harness() + .validate() + .finalize(); Ok(()) } @@ -526,7 +551,7 @@ fn test_generator_random_outputs() -> Result<()> { let total = outputs.iter().sum::(); let outputs: Vec<_> = outputs.into_iter().map(|v| (output_address, Kaspa(v))).collect(); - generator(test_network_id(), &[total + 100.0], &[], Fees::sender(Kaspa(5.0)), outputs.as_slice()) + generator(test_network_id(), &[total + 100.0], &[], None, Fees::sender(Kaspa(5.0)), outputs.as_slice()) .unwrap() .harness() .validate() @@ -541,6 +566,7 @@ fn test_generator_dust_1_1() -> Result<()> { test_network_id(), &[10.0; 20], &[], + None, Fees::sender(Kaspa(5.0)), [(output_address, Kaspa(1.0)), (output_address, Kaspa(1.0))].as_slice(), ) @@ -564,6 +590,7 @@ fn test_generator_inputs_2_outputs_2_fees_exclude() -> Result<()> { test_network_id(), &[10.0; 2], &[], + None, Fees::sender(Kaspa(5.0)), [(output_address, Kaspa(10.0)), (output_address, Kaspa(1.0))].as_slice(), ) @@ -584,7 +611,7 @@ fn test_generator_inputs_2_outputs_2_fees_exclude() -> Result<()> { #[test] fn test_generator_inputs_100_outputs_1_fees_exclude_success() -> Result<()> { // generator(test_network_id(), &[10.0; 100], &[], Fees::sender(Kaspa(5.0)), [(output_address, Kaspa(990.0))].as_slice()) - generator(test_network_id(), &[10.0; 100], &[], Fees::sender(Kaspa(0.0)), [(output_address, Kaspa(990.0))].as_slice()) + generator(test_network_id(), &[10.0; 100], &[], None, Fees::sender(Kaspa(0.0)), [(output_address, Kaspa(990.0))].as_slice()) .unwrap() .harness() .fetch(&Expected { @@ -620,6 +647,7 @@ fn test_generator_inputs_100_outputs_1_fees_include_success() -> Result<()> { test_network_id(), &[1.0; 100], &[], + None, Fees::receiver(Kaspa(5.0)), // [(output_address, Kaspa(100.0))].as_slice(), [(output_address, Kaspa(100.0))].as_slice(), @@ -654,7 +682,7 @@ fn test_generator_inputs_100_outputs_1_fees_include_success() -> Result<()> { #[test] fn test_generator_inputs_100_outputs_1_fees_exclude_insufficient_funds() -> Result<()> { - generator(test_network_id(), &[10.0; 100], &[], Fees::sender(Kaspa(5.0)), [(output_address, Kaspa(1000.0))].as_slice()) + generator(test_network_id(), &[10.0; 100], &[], None, Fees::sender(Kaspa(5.0)), [(output_address, Kaspa(1000.0))].as_slice()) .unwrap() .harness() .fetch(&Expected { @@ -671,7 +699,7 @@ fn test_generator_inputs_100_outputs_1_fees_exclude_insufficient_funds() -> Resu #[test] fn test_generator_inputs_1k_outputs_2_fees_exclude() -> Result<()> { - generator(test_network_id(), &[10.0; 1_000], &[], Fees::sender(Kaspa(5.0)), [(output_address, Kaspa(9_000.0))].as_slice()) + generator(test_network_id(), &[10.0; 1_000], &[], None, Fees::sender(Kaspa(5.0)), [(output_address, Kaspa(9_000.0))].as_slice()) .unwrap() .harness() .drain( @@ -710,6 +738,7 @@ fn test_generator_inputs_32k_outputs_2_fees_exclude() -> Result<()> { test_network_id(), &[f; 32_747], &[], + None, Fees::sender(Kaspa(10_000.0)), [(output_address, Kaspa(f * 32_747.0 - 10_001.0))].as_slice(), ) @@ -723,7 +752,8 @@ fn test_generator_inputs_32k_outputs_2_fees_exclude() -> Result<()> { #[test] fn test_generator_inputs_250k_outputs_2_sweep() -> Result<()> { let f = 130.0; - let generator = make_generator(test_network_id(), &[f; 250_000], &[], Fees::None, change_address, PaymentDestination::Change); + let generator = + make_generator(test_network_id(), &[f; 250_000], &[], None, Fees::None, change_address, PaymentDestination::Change); generator.unwrap().harness().accumulate(2875).finalize(); Ok(()) } diff --git a/wallet/core/src/utxo/test.rs b/wallet/core/src/utxo/test.rs index a1b41f998..6932bc651 100644 --- a/wallet/core/src/utxo/test.rs +++ b/wallet/core/src/utxo/test.rs @@ -26,7 +26,8 @@ fn test_utxo_generator_empty_utxo_noop() -> Result<()> { let output_address = output_address(network_id.into()); let payment_output = PaymentOutput::new(output_address, kaspa_to_sompi(2.0)); - let generator = make_generator(network_id, &[10.0], &[], Fees::SenderPays(0), change_address, payment_output.into()).unwrap(); + let generator = + make_generator(network_id, &[10.0], &[], None, Fees::SenderPays(0), change_address, payment_output.into()).unwrap(); let _tx = generator.generate_transaction().unwrap(); // println!("tx: {:?}", tx); // assert!(tx.is_none()); From c9e9247a8a57337a5781b5daeb271c073a8244cd Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Mon, 9 Sep 2024 23:47:54 +0300 Subject: [PATCH 07/13] WIP --- wallet/core/src/error.rs | 3 +++ wallet/core/src/tx/generator/generator.rs | 6 +++++- wallet/core/src/tx/generator/test.rs | 20 +++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/wallet/core/src/error.rs b/wallet/core/src/error.rs index 8992a8a92..531218252 100644 --- a/wallet/core/src/error.rs +++ b/wallet/core/src/error.rs @@ -310,6 +310,9 @@ pub enum Error { #[error("Mass calculation error")] MassCalculationError, + #[error("Transaction fees are too high")] + TransactionFeesAreTooHigh, + #[error("Invalid argument: {0}")] InvalidArgument(String), diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 8a7d9be1b..d009d0896 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -1140,7 +1140,11 @@ impl Generator { assert_eq!(change_output_value, None); - let output_value = aggregate_input_value - transaction_fees; + if aggregate_input_value <= transaction_fees { + return Err(Error::TransactionFeesAreTooHigh); + } + + let output_value = aggregate_input_value.saturating_sub(transaction_fees); let script_public_key = pay_to_address_script(&self.inner.change_address); let output = TransactionOutput::new(output_value, script_public_key.clone()); let tx = Transaction::new(0, inputs, vec![output], 0, SUBNETWORK_ID_NATIVE, 0, vec![]); diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index cb88f7eff..b2bd11c55 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -16,7 +16,7 @@ use workflow_log::style; use super::*; -const DISPLAY_LOGS: bool = false; +const DISPLAY_LOGS: bool = true; const DISPLAY_EXPECTED: bool = true; #[derive(Clone, Copy, Debug)] @@ -529,6 +529,24 @@ fn test_generator_compound_200k_10kas_transactions() -> Result<()> { Ok(()) } +#[test] +fn test_generator_fee_rate_compound_200k_10kas_transactions() -> Result<()> { + generator( + test_network_id(), + &[10.0; 200_000], + &[], + Some(100.0), + Fees::sender(Sompi(0)), + [(output_address, Kaspa(190_000.0))].as_slice(), + ) + .unwrap() + .harness() + .validate() + .finalize(); + + Ok(()) +} + #[test] fn test_generator_compound_100k_random_transactions() -> Result<()> { let mut rng = StdRng::seed_from_u64(0); From 0fff1ef6d9a1c369f49c90d695c7a91b2445359e Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Tue, 10 Sep 2024 01:41:26 +0300 Subject: [PATCH 08/13] change fee_rate combining to use max() --- wallet/core/src/tx/generator/generator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index d009d0896..cb127a046 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -638,11 +638,11 @@ impl Generator { #[inline(always)] fn calc_relay_transaction_compute_fees(&self, data: &Data) -> u64 { let mass = self.calc_relay_transaction_mass(data); - self.inner.mass_calculator.calc_minimum_transaction_fee_from_mass(mass) + self.calc_fee_rate(mass) + self.inner.mass_calculator.calc_minimum_transaction_fee_from_mass(mass).max(self.calc_fee_rate(mass)) } fn calc_fees_from_mass(&self, mass: u64) -> u64 { - self.inner.mass_calculator.calc_minimum_transaction_fee_from_mass(mass) + self.calc_fee_rate(mass) + self.inner.mass_calculator.calc_minimum_transaction_fee_from_mass(mass).max(self.calc_fee_rate(mass)) } /// Main UTXO entry processing loop. This function sources UTXOs from [`Generator::get_utxo_entry()`] and From be330812030bfb48eccf5da5ade57ed765cb1b77 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Tue, 10 Sep 2024 01:57:24 +0300 Subject: [PATCH 09/13] update max() handling --- wallet/core/src/tx/generator/generator.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index cb127a046..5c960fee0 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -915,7 +915,7 @@ impl Generator { // calculate for edge transaction boundaries // we know that stage.number_of_transactions > 0 will trigger stage generation let edge_compute_mass = data.aggregate_mass + self.inner.standard_change_output_compute_mass; //self.inner.final_transaction_outputs_compute_mass + self.inner.final_transaction_payload_mass; - let edge_fees = calc.calc_minimum_transaction_fee_from_mass(edge_compute_mass); + let edge_fees = self.calc_fees_from_mass(edge_compute_mass); let edge_output_value = data.aggregate_input_value.saturating_sub(edge_fees); if edge_output_value != 0 { let edge_output_harmonic = calc.calc_storage_mass_output_harmonic_single(edge_output_value); @@ -1004,8 +1004,7 @@ impl Generator { } } else { data.aggregate_mass = transaction_mass; - data.transaction_fees = - calc.calc_minimum_transaction_fee_from_mass(transaction_mass) + self.calc_fee_rate(transaction_mass); + data.transaction_fees = self.calc_fees_from_mass(transaction_mass); stage.aggregate_fees += data.transaction_fees; context.aggregate_fees += data.transaction_fees; Ok(Some(DataKind::Edge)) From 4cf3d5251f8c01eaed0af6020c71b87c30da3b8f Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Wed, 11 Sep 2024 02:01:33 +0300 Subject: [PATCH 10/13] Generator summary aggregate_mass --- Cargo.lock | 2 +- wallet/core/src/tx/generator/generator.rs | 11 ++++++++++- wallet/core/src/tx/generator/summary.rs | 17 +++++++++++------ wallet/core/src/tx/generator/test.rs | 2 +- wallet/core/src/wasm/tx/generator/summary.rs | 9 +++++++-- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c61450bd9..557802dde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3687,7 +3687,7 @@ dependencies = [ [[package]] name = "kaspa-wrpc-simple-client-example" -version = "0.14.5" +version = "0.14.7" dependencies = [ "futures", "kaspa-rpc-core", diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 5c960fee0..cd46f1b85 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -99,6 +99,9 @@ struct Context { /// total fees of all transactions issued by /// the single generator instance aggregate_fees: u64, + /// total mass of all transactions issued by + /// the single generator instance + aggregate_mass: u64, /// number of generated transactions number_of_transactions: usize, /// current tree stage @@ -437,6 +440,7 @@ impl Generator { number_of_transactions: 0, aggregated_utxos: 0, aggregate_fees: 0, + aggregate_mass: 0, stage: Some(Box::default()), utxo_stash: VecDeque::default(), final_transaction_id: None, @@ -751,6 +755,7 @@ impl Generator { data.transaction_fees = self.calc_relay_transaction_compute_fees(data); stage.aggregate_fees += data.transaction_fees; context.aggregate_fees += data.transaction_fees; + // context.aggregate_mass += data.aggregate_mass; Some(DataKind::Node) } else { context.aggregated_utxos += 1; @@ -774,6 +779,7 @@ impl Generator { Ok((DataKind::NoOp, data)) } else if stage.number_of_transactions > 0 { data.aggregate_mass += self.inner.standard_change_output_compute_mass; + // context.aggregate_mass += data.aggregate_mass; Ok((DataKind::Edge, data)) } else if data.aggregate_input_value < data.transaction_fees { Err(Error::InsufficientFunds { additional_needed: data.transaction_fees - data.aggregate_input_value, origin: "relay" }) @@ -1106,6 +1112,7 @@ impl Generator { } tx.set_mass(transaction_mass); + context.aggregate_mass += transaction_mass; context.final_transaction_id = Some(tx.id()); context.number_of_transactions += 1; @@ -1160,6 +1167,7 @@ impl Generator { } tx.set_mass(transaction_mass); + context.aggregate_mass += transaction_mass; context.number_of_transactions += 1; let previous_batch_utxo_entry_reference = @@ -1227,7 +1235,8 @@ impl Generator { GeneratorSummary { network_id: self.inner.network_id, aggregated_utxos: context.aggregated_utxos, - aggregated_fees: context.aggregate_fees, + aggregate_fees: context.aggregate_fees, + aggregate_mass: context.aggregate_mass, final_transaction_amount: self.final_transaction_value_no_fees(), final_transaction_id: context.final_transaction_id, number_of_generated_transactions: context.number_of_transactions, diff --git a/wallet/core/src/tx/generator/summary.rs b/wallet/core/src/tx/generator/summary.rs index 76ed6d964..6cc496477 100644 --- a/wallet/core/src/tx/generator/summary.rs +++ b/wallet/core/src/tx/generator/summary.rs @@ -16,7 +16,8 @@ use std::fmt; pub struct GeneratorSummary { pub network_id: NetworkId, pub aggregated_utxos: usize, - pub aggregated_fees: u64, + pub aggregate_fees: u64, + pub aggregate_mass: u64, pub number_of_generated_transactions: usize, pub final_transaction_amount: Option, pub final_transaction_id: Option, @@ -35,8 +36,12 @@ impl GeneratorSummary { self.aggregated_utxos } - pub fn aggregated_fees(&self) -> u64 { - self.aggregated_fees + pub fn aggregate_mass(&self) -> u64 { + self.aggregate_mass + } + + pub fn aggregate_fees(&self) -> u64 { + self.aggregate_fees } pub fn number_of_generated_transactions(&self) -> usize { @@ -61,12 +66,12 @@ impl fmt::Display for GeneratorSummary { }; if let Some(final_transaction_amount) = self.final_transaction_amount { - let total = final_transaction_amount + self.aggregated_fees; + let total = final_transaction_amount + self.aggregate_fees; write!( f, "Amount: {} Fees: {} Total: {} UTXOs: {} {}", sompi_to_kaspa_string_with_suffix(final_transaction_amount, &self.network_id), - sompi_to_kaspa_string_with_suffix(self.aggregated_fees, &self.network_id), + sompi_to_kaspa_string_with_suffix(self.aggregate_fees, &self.network_id), sompi_to_kaspa_string_with_suffix(total, &self.network_id), self.aggregated_utxos, transactions @@ -75,7 +80,7 @@ impl fmt::Display for GeneratorSummary { write!( f, "Fees: {} UTXOs: {} {}", - sompi_to_kaspa_string_with_suffix(self.aggregated_fees, &self.network_id), + sompi_to_kaspa_string_with_suffix(self.aggregate_fees, &self.network_id), self.aggregated_utxos, transactions )?; diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index b2bd11c55..156f7ec8d 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -107,7 +107,7 @@ impl GeneratorSummaryExtension for GeneratorSummary { "number of utxo entries" ); let aggregated_fees = accumulator.list.iter().map(|pt| pt.fees()).sum::(); - assert_eq!(self.aggregated_fees, aggregated_fees, "aggregated fees"); + assert_eq!(self.aggregate_fees, aggregated_fees, "aggregated fees"); self } } diff --git a/wallet/core/src/wasm/tx/generator/summary.rs b/wallet/core/src/wasm/tx/generator/summary.rs index 8d572ec1e..ad87430ff 100644 --- a/wallet/core/src/wasm/tx/generator/summary.rs +++ b/wallet/core/src/wasm/tx/generator/summary.rs @@ -28,8 +28,13 @@ impl GeneratorSummary { } #[wasm_bindgen(getter, js_name = fees)] - pub fn aggregated_fees(&self) -> BigInt { - BigInt::from(self.inner.aggregated_fees()) + pub fn aggregate_fees(&self) -> BigInt { + BigInt::from(self.inner.aggregate_fees()) + } + + #[wasm_bindgen(getter, js_name = mass)] + pub fn aggregate_mass(&self) -> BigInt { + BigInt::from(self.inner.aggregate_mass()) } #[wasm_bindgen(getter, js_name = transactions)] From b910457945407a95962c458d866edac824afbc60 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Wed, 11 Sep 2024 04:11:19 +0300 Subject: [PATCH 11/13] generator summary number_of_stages --- wallet/core/src/tx/generator/generator.rs | 10 ++++++++++ wallet/core/src/tx/generator/summary.rs | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index cd46f1b85..cbbbbc34d 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -104,6 +104,12 @@ struct Context { aggregate_mass: u64, /// number of generated transactions number_of_transactions: usize, + /// Number of generated stages. Stage represents multiple transactions + /// executed in parallel. Each stage is a tree level in the transaction + /// tree. When calculating time for submission of transactions, the estimated + /// time per transaction (either as DAA score or a fee-rate based estimate) + /// should be multiplied by the number of stages. + number_of_stages: usize, /// current tree stage stage: Option>, /// Rejected or "stashed" UTXO entries that are consumed before polling @@ -437,6 +443,7 @@ impl Generator { utxo_source_iterator: utxo_iterator, priority_utxo_entries, priority_utxo_entry_filter, + number_of_stages: 0, number_of_transactions: 0, aggregated_utxos: 0, aggregate_fees: 0, @@ -1114,6 +1121,7 @@ impl Generator { context.aggregate_mass += transaction_mass; context.final_transaction_id = Some(tx.id()); + context.number_of_stages += 1; context.number_of_transactions += 1; Ok(Some(PendingTransaction::try_new( @@ -1185,6 +1193,7 @@ impl Generator { let mut stage = context.stage.take().unwrap(); stage.utxo_accumulator.push(previous_batch_utxo_entry_reference); stage.number_of_transactions += 1; + context.number_of_stages += 1; context.stage.replace(Box::new(Stage::new(*stage))); } _ => unreachable!(), @@ -1240,6 +1249,7 @@ impl Generator { final_transaction_amount: self.final_transaction_value_no_fees(), final_transaction_id: context.final_transaction_id, number_of_generated_transactions: context.number_of_transactions, + number_of_generated_stages: context.number_of_stages, } } } diff --git a/wallet/core/src/tx/generator/summary.rs b/wallet/core/src/tx/generator/summary.rs index 6cc496477..37ce6555f 100644 --- a/wallet/core/src/tx/generator/summary.rs +++ b/wallet/core/src/tx/generator/summary.rs @@ -19,6 +19,7 @@ pub struct GeneratorSummary { pub aggregate_fees: u64, pub aggregate_mass: u64, pub number_of_generated_transactions: usize, + pub number_of_generated_stages: usize, pub final_transaction_amount: Option, pub final_transaction_id: Option, } @@ -48,6 +49,10 @@ impl GeneratorSummary { self.number_of_generated_transactions } + pub fn number_of_generated_stages(&self) -> usize { + self.number_of_generated_stages + } + pub fn final_transaction_amount(&self) -> Option { self.final_transaction_amount } From 458354b4350b021f9bd5c3259c2e94bb66ebdd9d Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Thu, 12 Sep 2024 21:03:17 +0300 Subject: [PATCH 12/13] WIP --- wallet/core/src/tx/generator/summary.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/wallet/core/src/tx/generator/summary.rs b/wallet/core/src/tx/generator/summary.rs index 37ce6555f..2ce309410 100644 --- a/wallet/core/src/tx/generator/summary.rs +++ b/wallet/core/src/tx/generator/summary.rs @@ -25,6 +25,19 @@ pub struct GeneratorSummary { } impl GeneratorSummary { + pub fn new(network_id: NetworkId) -> Self { + Self { + network_id, + aggregated_utxos: 0, + aggregate_fees: 0, + aggregate_mass: 0, + number_of_generated_transactions: 0, + number_of_generated_stages: 0, + final_transaction_amount: None, + final_transaction_id: None, + } + } + pub fn network_type(&self) -> NetworkType { self.network_id.into() } From ec41727f2930c92ba07a80d900d7b36c4c524c04 Mon Sep 17 00:00:00 2001 From: KaffinPX Date: Wed, 25 Sep 2024 02:47:35 +0300 Subject: [PATCH 13/13] Output shuffling on Generator (missing tests and mod.rs considerations) --- wallet/core/src/account/mod.rs | 26 +++++++++++++---- wallet/core/src/account/pskb.rs | 1 + wallet/core/src/tx/generator/generator.rs | 29 +++++++++++++++---- wallet/core/src/tx/generator/settings.rs | 12 ++++++-- wallet/core/src/tx/generator/test.rs | 1 + .../core/src/wasm/tx/generator/generator.rs | 7 +++++ 6 files changed, 63 insertions(+), 13 deletions(-) diff --git a/wallet/core/src/account/mod.rs b/wallet/core/src/account/mod.rs index 3314978e6..ee75d49bb 100644 --- a/wallet/core/src/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -315,6 +315,7 @@ pub trait Account: AnySync + Send + Sync + 'static { self.clone().as_dyn_arc(), PaymentDestination::Change, fee_rate, + None, Fees::None, None, )?; @@ -351,8 +352,14 @@ pub trait Account: AnySync + Send + Sync + 'static { let keydata = self.prv_key_data(wallet_secret).await?; let signer = Arc::new(Signer::new(self.clone().as_dyn_arc(), keydata, payment_secret)); - let settings = - GeneratorSettings::try_new_with_account(self.clone().as_dyn_arc(), destination, fee_rate, priority_fee_sompi, payload)?; + let settings = GeneratorSettings::try_new_with_account( + self.clone().as_dyn_arc(), + destination, + fee_rate, + None, + priority_fee_sompi, + payload, + )?; let generator = Generator::try_new(settings, Some(signer), Some(abortable))?; @@ -381,8 +388,14 @@ pub trait Account: AnySync + Send + Sync + 'static { payment_secret: Option, abortable: &Abortable, ) -> Result { - let settings = - GeneratorSettings::try_new_with_account(self.clone().as_dyn_arc(), destination, fee_rate, priority_fee_sompi, payload)?; + let settings = GeneratorSettings::try_new_with_account( + self.clone().as_dyn_arc(), + destination, + fee_rate, + None, + priority_fee_sompi, + payload, + )?; let keydata = self.prv_key_data(wallet_secret).await?; let signer = Arc::new(PSKBSigner::new(self.clone().as_dyn_arc(), keydata, payment_secret)); let generator = Generator::try_new(settings, None, Some(abortable))?; @@ -463,6 +476,7 @@ pub trait Account: AnySync + Send + Sync + 'static { self.clone().as_dyn_arc(), final_transaction_destination, fee_rate, + None, priority_fee_sompi, final_transaction_payload, )? @@ -493,7 +507,8 @@ pub trait Account: AnySync + Send + Sync + 'static { payload: Option>, abortable: &Abortable, ) -> Result { - let settings = GeneratorSettings::try_new_with_account(self.as_dyn_arc(), destination, fee_rate, priority_fee_sompi, payload)?; + let settings = + GeneratorSettings::try_new_with_account(self.as_dyn_arc(), destination, fee_rate, None, priority_fee_sompi, payload)?; let generator = Generator::try_new(settings, None, Some(abortable))?; @@ -620,6 +635,7 @@ pub trait DerivationCapableAccount: Account { 1, PaymentDestination::Change, fee_rate, + None, Fees::None, None, None, diff --git a/wallet/core/src/account/pskb.rs b/wallet/core/src/account/pskb.rs index 7aa817e90..a5d8f61a6 100644 --- a/wallet/core/src/account/pskb.rs +++ b/wallet/core/src/account/pskb.rs @@ -334,6 +334,7 @@ pub fn pskt_to_pending_transaction( source_utxo_context: None, destination_utxo_context: None, fee_rate: None, + shuffle_outputs: None, final_transaction_priority_fee: fee_u.into(), final_transaction_destination, final_transaction_payload: None, diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index cbbbbc34d..f17a979b0 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -70,6 +70,8 @@ use kaspa_consensus_core::mass::Kip9Version; use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; use kaspa_consensus_core::tx::{Transaction, TransactionInput, TransactionOutpoint, TransactionOutput}; use kaspa_txscript::pay_to_address_script; +use rand::seq::SliceRandom; +use rand::thread_rng; use std::collections::VecDeque; use super::SignerT; @@ -296,6 +298,8 @@ struct Inner { signature_mass_per_input: u64, // fee rate fee_rate: Option, + // shuffle outputs of final transaction + shuffle_outputs: Option, // final transaction amount and fees // `None` is used for sweep transactions final_transaction: Option, @@ -362,6 +366,7 @@ impl Generator { minimum_signatures, change_address, fee_rate, + shuffle_outputs, final_transaction_priority_fee, final_transaction_destination, final_transaction_payload, @@ -469,6 +474,7 @@ impl Generator { standard_change_output_compute_mass: standard_change_output_mass, signature_mass_per_input, fee_rate, + shuffle_outputs, final_transaction, final_transaction_priority_fee, final_transaction_outputs, @@ -1069,7 +1075,7 @@ impl Generator { let change_output_value = change_output_value.unwrap_or(0); - let mut final_outputs = self.inner.final_transaction_outputs.clone(); + let mut final_outputs: Vec = self.inner.final_transaction_outputs.clone(); if self.inner.final_transaction_priority_fee.receiver_pays() { let output = final_outputs.get_mut(0).expect("include fees requires one output"); @@ -1080,10 +1086,23 @@ impl Generator { } } - let change_output_index = if change_output_value > 0 { - let change_output_index = Some(final_outputs.len()); - final_outputs.push(TransactionOutput::new(change_output_value, pay_to_address_script(&self.inner.change_address))); - change_output_index + // Cache the change output (if any) before shuffling so we can find its index. + let change_output = if change_output_value > 0 { + let change_output = TransactionOutput::new(change_output_value, pay_to_address_script(&self.inner.change_address)); + final_outputs.push(change_output.clone()); + Some(change_output) + } else { + None + }; + + // Shuffle the outputs if required for extra privacy. + if self.inner.shuffle_outputs.unwrap_or(true) { + final_outputs.shuffle(&mut thread_rng()); + } + + // Find the new change_output_index after shuffling if there was a change output. + let change_output_index = if let Some(change_output) = change_output { + final_outputs.iter().position(|output| output == &change_output) } else { None }; diff --git a/wallet/core/src/tx/generator/settings.rs b/wallet/core/src/tx/generator/settings.rs index a1fcf2acf..b7b3b0c73 100644 --- a/wallet/core/src/tx/generator/settings.rs +++ b/wallet/core/src/tx/generator/settings.rs @@ -30,6 +30,8 @@ pub struct GeneratorSettings { pub change_address: Address, // fee rate pub fee_rate: Option, + // Whether to shuffle the outputs of the final transaction for privacy reasons. + pub shuffle_outputs: Option, // applies only to the final transaction pub final_transaction_priority_fee: Fees, // final transaction outputs @@ -63,6 +65,7 @@ impl GeneratorSettings { account: Arc, final_transaction_destination: PaymentDestination, fee_rate: Option, + shuffle_outputs: Option, final_priority_fee: Fees, final_transaction_payload: Option>, ) -> Result { @@ -83,8 +86,8 @@ impl GeneratorSettings { utxo_iterator: Box::new(utxo_iterator), source_utxo_context: Some(account.utxo_context().clone()), priority_utxo_entries: None, - fee_rate, + shuffle_outputs, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, @@ -94,6 +97,7 @@ impl GeneratorSettings { Ok(settings) } + #[allow(clippy::too_many_arguments)] pub fn try_new_with_context( utxo_context: UtxoContext, priority_utxo_entries: Option>, @@ -102,6 +106,7 @@ impl GeneratorSettings { minimum_signatures: u16, final_transaction_destination: PaymentDestination, fee_rate: Option, + shuffle_outputs: Option, final_priority_fee: Fees, final_transaction_payload: Option>, multiplexer: Option>>, @@ -118,8 +123,8 @@ impl GeneratorSettings { utxo_iterator: Box::new(utxo_iterator), source_utxo_context: Some(utxo_context), priority_utxo_entries, - fee_rate, + shuffle_outputs, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, @@ -139,6 +144,7 @@ impl GeneratorSettings { minimum_signatures: u16, final_transaction_destination: PaymentDestination, fee_rate: Option, + shuffle_outputs: Option, final_priority_fee: Fees, final_transaction_payload: Option>, multiplexer: Option>>, @@ -152,8 +158,8 @@ impl GeneratorSettings { utxo_iterator: Box::new(utxo_iterator), source_utxo_context: None, priority_utxo_entries, - fee_rate, + shuffle_outputs, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index 156f7ec8d..46a644bc5 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -436,6 +436,7 @@ where priority_utxo_entries, destination_utxo_context, fee_rate, + shuffle_outputs: None, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, diff --git a/wallet/core/src/wasm/tx/generator/generator.rs b/wallet/core/src/wasm/tx/generator/generator.rs index 8cc236238..b1ba6dd14 100644 --- a/wallet/core/src/wasm/tx/generator/generator.rs +++ b/wallet/core/src/wasm/tx/generator/generator.rs @@ -169,6 +169,7 @@ impl Generator { final_transaction_destination, change_address, fee_rate, + shuffle_outputs, final_priority_fee, sig_op_count, minimum_signatures, @@ -192,6 +193,7 @@ impl Generator { minimum_signatures, final_transaction_destination, fee_rate, + shuffle_outputs, final_priority_fee, payload, multiplexer, @@ -209,6 +211,7 @@ impl Generator { minimum_signatures, final_transaction_destination, fee_rate, + shuffle_outputs, final_priority_fee, payload, multiplexer, @@ -272,6 +275,7 @@ struct GeneratorSettings { pub final_transaction_destination: PaymentDestination, pub change_address: Option
, pub fee_rate: Option, + pub shuffle_outputs: Option, pub final_priority_fee: Fees, pub sig_op_count: u8, pub minimum_signatures: u16, @@ -292,6 +296,8 @@ impl TryFrom for GeneratorSettings { let fee_rate = args.get_f64("feeRate").ok().and_then(|v| (v.is_finite() && !v.is_nan() && v >= 1e-8).then_some(v)); + let shuffle_outputs = args.get_bool("shuffleOutputs").ok(); + let final_priority_fee = args.get::("priorityFee")?.try_into()?; let generator_source = if let Ok(Some(context)) = args.try_cast_into::("entries") { @@ -325,6 +331,7 @@ impl TryFrom for GeneratorSettings { final_transaction_destination, change_address, fee_rate, + shuffle_outputs, final_priority_fee, sig_op_count, minimum_signatures,