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..ee75d49bb 100644 --- a/wallet/core/src/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -305,13 +305,20 @@ 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, + None, + Fees::None, + None, + )?; let generator = Generator::try_new(settings, Some(signer), Some(abortable))?; let mut stream = generator.stream(); @@ -334,6 +341,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 +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, 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))?; @@ -366,13 +381,21 @@ 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, + 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))?; @@ -428,6 +451,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 +475,8 @@ pub trait Account: AnySync + Send + Sync + 'static { let settings = GeneratorSettings::try_new_with_account( self.clone().as_dyn_arc(), final_transaction_destination, + fee_rate, + None, priority_fee_sompi, final_transaction_payload, )? @@ -476,11 +502,13 @@ 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, None, priority_fee_sompi, payload)?; let generator = Generator::try_new(settings, None, Some(abortable))?; @@ -531,6 +559,7 @@ pub trait DerivationCapableAccount: Account { extent: usize, window: usize, sweep: bool, + fee_rate: Option, abortable: &Abortable, notifier: Option, ) -> Result<()> { @@ -605,6 +634,8 @@ pub trait DerivationCapableAccount: Account { 1, 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 8fc46088b..a5d8f61a6 100644 --- a/wallet/core/src/account/pskb.rs +++ b/wallet/core/src/account/pskb.rs @@ -333,6 +333,8 @@ pub fn pskt_to_pending_transaction( priority_utxo_entries: None, 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/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/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 398ba1b4d..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; @@ -99,8 +101,17 @@ 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, + /// 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 @@ -285,6 +296,10 @@ struct Inner { standard_change_output_compute_mass: u64, // signature mass per input 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, @@ -318,6 +333,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) @@ -349,6 +365,8 @@ impl Generator { sig_op_count, minimum_signatures, change_address, + fee_rate, + shuffle_outputs, final_transaction_priority_fee, final_transaction_destination, final_transaction_payload, @@ -430,9 +448,11 @@ 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, + aggregate_mass: 0, stage: Some(Box::default()), utxo_stash: VecDeque::default(), final_transaction_id: None, @@ -453,6 +473,8 @@ impl Generator { change_address, 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, @@ -467,61 +489,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,6 +574,7 @@ 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)) } @@ -536,6 +582,7 @@ impl Generator { /// 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) } @@ -566,14 +613,53 @@ 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); + } + + // /// 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)) + let mass = self.calc_relay_transaction_mass(data); + 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).max(self.calc_fee_rate(mass)) } /// Main UTXO entry processing loop. This function sources UTXOs from [`Generator::get_utxo_entry()`] and @@ -682,6 +768,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; @@ -705,6 +792,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" }) @@ -729,6 +817,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. @@ -842,7 +934,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); @@ -897,7 +989,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 }) } @@ -911,7 +1003,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 = @@ -930,7 +1023,7 @@ impl Generator { } } else { data.aggregate_mass = transaction_mass; - data.transaction_fees = calc.calc_minimum_transaction_fee_from_mass(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)) @@ -982,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"); @@ -993,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 }; @@ -1032,7 +1138,9 @@ impl Generator { } tx.set_mass(transaction_mass); + 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( @@ -1065,7 +1173,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![]); @@ -1082,6 +1194,7 @@ impl Generator { } tx.set_mass(transaction_mass); + context.aggregate_mass += transaction_mass; context.number_of_transactions += 1; let previous_batch_utxo_entry_reference = @@ -1099,6 +1212,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!(), @@ -1149,10 +1263,12 @@ 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, + number_of_generated_stages: context.number_of_stages, } } } diff --git a/wallet/core/src/tx/generator/pending.rs b/wallet/core/src/tx/generator/pending.rs index 8b4beddf2..9a3bbdd48 100644 --- a/wallet/core/src/tx/generator/pending.rs +++ b/wallet/core/src/tx/generator/pending.rs @@ -2,15 +2,16 @@ //! Pending transaction encapsulating a //! transaction generated by the [`Generator`]. //! +#![allow(unused_imports)] 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,6 +49,28 @@ 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(); @@ -295,4 +318,142 @@ impl PendingTransaction { *self.inner.signable_tx.lock().unwrap() = signed_tx; Ok(()) } + + pub fn increase_fees_for_rbf(&self, additional_fees: u64) -> Result { + #![allow(unused_mut)] + #![allow(unused_variables)] + + 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; + + #[allow(clippy::single_match)] + 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 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; + } } diff --git a/wallet/core/src/tx/generator/settings.rs b/wallet/core/src/tx/generator/settings.rs index 34fd1bb6e..b7b3b0c73 100644 --- a/wallet/core/src/tx/generator/settings.rs +++ b/wallet/core/src/tx/generator/settings.rs @@ -28,6 +28,10 @@ pub struct GeneratorSettings { pub minimum_signatures: u16, // change address 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 @@ -60,6 +64,8 @@ impl GeneratorSettings { pub fn try_new_with_account( account: Arc, final_transaction_destination: PaymentDestination, + fee_rate: Option, + shuffle_outputs: Option, final_priority_fee: Fees, final_transaction_payload: Option>, ) -> Result { @@ -80,7 +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, @@ -90,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>, @@ -97,6 +105,8 @@ impl GeneratorSettings { sig_op_count: u8, minimum_signatures: u16, final_transaction_destination: PaymentDestination, + fee_rate: Option, + shuffle_outputs: Option, final_priority_fee: Fees, final_transaction_payload: Option>, multiplexer: Option>>, @@ -113,7 +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, @@ -123,6 +134,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>, @@ -131,6 +143,8 @@ impl GeneratorSettings { sig_op_count: u8, minimum_signatures: u16, final_transaction_destination: PaymentDestination, + fee_rate: Option, + shuffle_outputs: Option, final_priority_fee: Fees, final_transaction_payload: Option>, multiplexer: Option>>, @@ -144,7 +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/summary.rs b/wallet/core/src/tx/generator/summary.rs index 76ed6d964..2ce309410 100644 --- a/wallet/core/src/tx/generator/summary.rs +++ b/wallet/core/src/tx/generator/summary.rs @@ -16,13 +16,28 @@ 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 number_of_generated_stages: usize, pub final_transaction_amount: Option, pub final_transaction_id: Option, } 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() } @@ -35,14 +50,22 @@ 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 { 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 } @@ -61,12 +84,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 +98,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 990698b72..46a644bc5 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)] @@ -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 } } @@ -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,6 +435,8 @@ where source_utxo_context, priority_utxo_entries, destination_utxo_context, + fee_rate, + shuffle_outputs: None, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, @@ -453,7 +463,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(()) @@ -461,7 +471,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()); @@ -470,7 +480,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 { @@ -486,8 +496,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"), @@ -497,11 +514,36 @@ 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(()) +} + +#[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(()) } @@ -512,7 +554,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(()) } @@ -524,7 +570,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() @@ -539,6 +585,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(), ) @@ -562,6 +609,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(), ) @@ -582,7 +630,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 { @@ -618,6 +666,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(), @@ -652,7 +701,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 { @@ -669,7 +718,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( @@ -708,6 +757,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(), ) @@ -721,7 +771,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()); 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..b1ba6dd14 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,8 @@ impl Generator { multiplexer, final_transaction_destination, change_address, + fee_rate, + shuffle_outputs, final_priority_fee, sig_op_count, minimum_signatures, @@ -182,6 +192,8 @@ impl Generator { sig_op_count, minimum_signatures, final_transaction_destination, + fee_rate, + shuffle_outputs, final_priority_fee, payload, multiplexer, @@ -198,6 +210,8 @@ impl Generator { sig_op_count, minimum_signatures, final_transaction_destination, + fee_rate, + shuffle_outputs, final_priority_fee, payload, multiplexer, @@ -260,6 +274,8 @@ struct GeneratorSettings { pub multiplexer: Option>>, 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, @@ -278,6 +294,10 @@ 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 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") { @@ -310,6 +330,8 @@ impl TryFrom for GeneratorSettings { multiplexer: None, final_transaction_destination, change_address, + fee_rate, + shuffle_outputs, final_priority_fee, sig_op_count, minimum_signatures, 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)]