diff --git a/cli/src/modules/account.rs b/cli/src/modules/account.rs index 5848d43fb..49823eefb 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,7 +295,9 @@ impl Account { start + count, window, sweep, + fee_rate, &abortable, + true, Some(Arc::new(move |processed: usize, _, balance, txid| { if let Some(txid) = txid { tprintln!( 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/rpc/wrpc/client/Cargo.toml b/rpc/wrpc/client/Cargo.toml index 1cc0a2191..911813de9 100644 --- a/rpc/wrpc/client/Cargo.toml +++ b/rpc/wrpc/client/Cargo.toml @@ -5,9 +5,16 @@ rust-version.workspace = true version.workspace = true edition.workspace = true authors.workspace = true -include.workspace = true license.workspace = true repository.workspace = true +include = [ + "src/**/*.rs", + "benches/**/*.rs", + "build.rs", + "Cargo.toml", + "Cargo.lock", + "Resolvers.toml", +] [features] wasm32-sdk = ["kaspa-consensus-wasm/wasm32-sdk","kaspa-rpc-core/wasm32-sdk","workflow-rpc/wasm32-sdk"] diff --git a/rpc/wrpc/wasm/src/client.rs b/rpc/wrpc/wasm/src/client.rs index ccd9cb284..63382fcca 100644 --- a/rpc/wrpc/wasm/src/client.rs +++ b/rpc/wrpc/wasm/src/client.rs @@ -351,8 +351,8 @@ impl RpcClient { /// Set the network id for the RPC client. /// This setting will take effect on the next connection. #[wasm_bindgen(js_name = setNetworkId)] - pub fn set_network_id(&self, network_id: &NetworkId) -> Result<()> { - self.inner.client.set_network_id(network_id)?; + pub fn set_network_id(&self, network_id: &NetworkIdT) -> Result<()> { + self.inner.client.set_network_id(&network_id.try_into_owned()?)?; Ok(()) } diff --git a/wallet/bip32/src/lib.rs b/wallet/bip32/src/lib.rs index 1926728c4..74486e9a7 100644 --- a/wallet/bip32/src/lib.rs +++ b/wallet/bip32/src/lib.rs @@ -19,6 +19,11 @@ mod prefix; mod result; pub mod types; +pub mod wasm { + //! WASM bindings for the `bip32` module. + pub use crate::mnemonic::{Language, Mnemonic, WordCount}; +} + pub use address_type::AddressType; pub use attrs::ExtendedKeyAttrs; pub use child_number::ChildNumber; diff --git a/wallet/core/src/account/mod.rs b/wallet/core/src/account/mod.rs index 31c7fea9d..2b022beb1 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))?; @@ -517,6 +530,7 @@ pub trait AsLegacyAccount: Account { } /// Account trait used by derivation capable account types (BIP32, MultiSig, etc.) +#[allow(clippy::too_many_arguments)] #[async_trait] pub trait DerivationCapableAccount: Account { fn derivation(&self) -> Arc; @@ -531,7 +545,9 @@ pub trait DerivationCapableAccount: Account { extent: usize, window: usize, sweep: bool, + fee_rate: Option, abortable: &Abortable, + update_address_indexes: bool, notifier: Option, ) -> Result<()> { if let Ok(legacy_account) = self.clone().as_legacy_account() { @@ -558,6 +574,8 @@ pub trait DerivationCapableAccount: Account { let mut last_notification = 0; let mut aggregate_balance = 0; let mut aggregate_utxo_count = 0; + let mut last_change_address_index = change_address_index; + let mut last_receive_address_index = receive_address_manager.index(); let change_address = change_address_keypair[0].0.clone(); @@ -567,8 +585,8 @@ pub trait DerivationCapableAccount: Account { index = last as usize; let (mut keys, addresses) = if sweep { - let mut keypairs = derivation.get_range_with_keys(false, first..last, false, &xkey).await?; - let change_keypairs = derivation.get_range_with_keys(true, first..last, false, &xkey).await?; + let mut keypairs = derivation.get_range_with_keys(false, first..last, true, &xkey).await?; + let change_keypairs = derivation.get_range_with_keys(true, first..last, true, &xkey).await?; keypairs.extend(change_keypairs); let mut keys = vec![]; let addresses = keypairs @@ -581,22 +599,40 @@ pub trait DerivationCapableAccount: Account { keys.push(change_address_keypair[0].1.to_bytes()); (keys, addresses) } else { - let mut addresses = receive_address_manager.get_range_with_args(first..last, false)?; - let change_addresses = change_address_manager.get_range_with_args(first..last, false)?; + let mut addresses = receive_address_manager.get_range_with_args(first..last, true)?; + let change_addresses = change_address_manager.get_range_with_args(first..last, true)?; addresses.extend(change_addresses); (vec![], addresses) }; let utxos = rpc.get_utxos_by_addresses(addresses.clone()).await?; - let balance = utxos.iter().map(|utxo| utxo.utxo_entry.amount).sum::(); + let mut balance = 0; + let utxos = utxos + .iter() + .map(|utxo| { + let utxo_ref = UtxoEntryReference::from(utxo); + if let Some(address) = utxo_ref.utxo.address.as_ref() { + if let Some(address_index) = receive_address_manager.inner().address_to_index_map.get(address) { + if last_receive_address_index < *address_index { + last_receive_address_index = *address_index; + } + } else if let Some(address_index) = change_address_manager.inner().address_to_index_map.get(address) { + if last_change_address_index < *address_index { + last_change_address_index = *address_index; + } + } else { + panic!("Account::derivation_scan() has received an unknown address: `{address}`"); + } + } + balance += utxo_ref.utxo.amount; + utxo_ref + }) + .collect::>(); aggregate_utxo_count += utxos.len(); if balance > 0 { aggregate_balance += balance; - if sweep { - let utxos = utxos.into_iter().map(UtxoEntryReference::from).collect::>(); - let settings = GeneratorSettings::try_new_with_iterator( self.wallet().network_id()?, Box::new(utxos.into_iter()), @@ -605,6 +641,7 @@ pub trait DerivationCapableAccount: Account { 1, 1, PaymentDestination::Change, + fee_rate, Fees::None, None, None, @@ -646,6 +683,18 @@ pub trait DerivationCapableAccount: Account { } } + // update address manager with the last used index + if update_address_indexes { + receive_address_manager.set_index(last_receive_address_index)?; + change_address_manager.set_index(last_change_address_index)?; + + let metadata = self.metadata()?.expect("derivation accounts must provide metadata"); + let store = self.wallet().store().as_account_store()?; + store.update_metadata(vec![metadata]).await?; + self.clone().scan(None, None).await?; + self.wallet().notify(Events::AccountUpdate { account_descriptor: self.descriptor()? }).await?; + } + if let Ok(legacy_account) = self.as_legacy_account() { legacy_account.clear_private_context().await?; } diff --git a/wallet/core/src/account/pskb.rs b/wallet/core/src/account/pskb.rs index fad6bdb4a..fbc138d44 100644 --- a/wallet/core/src/account/pskb.rs +++ b/wallet/core/src/account/pskb.rs @@ -338,6 +338,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 e27cb2b29..0fe5ce951 100644 --- a/wallet/core/src/api/message.rs +++ b/wallet/core/src/api/message.rs @@ -401,11 +401,16 @@ pub struct AccountsEnsureDefaultResponse { // TODO #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] -pub struct AccountsImportRequest {} +pub struct AccountsImportRequest { + pub wallet_secret: Secret, + pub account_create_args: AccountCreateArgs, +} #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] -pub struct AccountsImportResponse {} +pub struct AccountsImportResponse { + pub account_descriptor: AccountDescriptor, +} #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] @@ -493,6 +498,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>, } @@ -512,6 +518,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, } @@ -530,6 +537,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/api/traits.rs b/wallet/core/src/api/traits.rs index 357665e77..74b11906c 100644 --- a/wallet/core/src/api/traits.rs +++ b/wallet/core/src/api/traits.rs @@ -375,7 +375,15 @@ pub trait WalletApi: Send + Sync + AnySync { request: AccountsEnsureDefaultRequest, ) -> Result; - // TODO + /// Wrapper around [`accounts_import_call()`](Self::accounts_import_call) + async fn accounts_import( + self: Arc, + wallet_secret: Secret, + account_create_args: AccountCreateArgs, + ) -> Result { + Ok(self.accounts_import_call(AccountsImportRequest { wallet_secret, account_create_args }).await?.account_descriptor) + } + async fn accounts_import_call(self: Arc, request: AccountsImportRequest) -> Result; /// Get an [`AccountDescriptor`] for a specific account id. 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 67b475983..80fa7057b 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -98,8 +98,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 @@ -284,6 +293,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, @@ -317,6 +328,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) @@ -348,6 +360,7 @@ impl Generator { sig_op_count, minimum_signatures, change_address, + fee_rate, final_transaction_priority_fee, final_transaction_destination, final_transaction_payload, @@ -429,9 +442,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, @@ -452,6 +467,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, @@ -466,61 +482,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 } @@ -528,6 +567,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)) } @@ -535,6 +575,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) } @@ -565,14 +606,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 @@ -680,6 +760,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; @@ -703,6 +784,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" }) @@ -727,6 +809,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. @@ -840,7 +926,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); @@ -892,7 +978,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 }) } @@ -906,7 +992,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 = @@ -925,7 +1012,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)) @@ -1027,7 +1114,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( @@ -1060,7 +1149,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![]); @@ -1077,6 +1170,7 @@ impl Generator { } tx.set_mass(transaction_mass); + context.aggregate_mass += transaction_mass; context.number_of_transactions += 1; let previous_batch_utxo_entry_reference = @@ -1094,6 +1188,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!(), @@ -1144,10 +1239,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..a1fcf2acf 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, @@ -123,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>, @@ -131,6 +138,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>>, @@ -145,6 +153,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/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 e1db97c44..1818fe414 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -6,6 +6,7 @@ use crate::tx::{Fees, MassCalculator, PaymentDestination}; use crate::utxo::UtxoEntryReference; use crate::{tx::PaymentOutputs, utils::kaspa_to_sompi}; use kaspa_addresses::Address; +use kaspa_consensus_core::config::params::Params; use kaspa_consensus_core::network::{NetworkId, NetworkType}; use kaspa_consensus_core::tx::Transaction; use rand::prelude::*; @@ -16,7 +17,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 +108,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 +377,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 +396,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 +436,7 @@ where source_utxo_context, priority_utxo_entries, destination_utxo_context, + fee_rate, 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,48 @@ 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(()) } + +#[test] +fn test_generator_fan_out_1() -> Result<()> { + use kaspa_consensus_core::mass::calc_storage_mass; + + let network_id = test_network_id(); + let consensus_params = Params::from(network_id); + + let storage_mass = calc_storage_mass( + false, + [100000000, 8723579967].into_iter(), + [20000000, 25000000, 31000000].into_iter(), + consensus_params.storage_mass_parameter, + ); + + println!("storage_mass: {:?}", storage_mass); + + // generator(test_network_id(), &[ + // 1.00000000, + // 87.23579967, + // ], &[], None, Fees::sender(Kaspa(1.0)), [ + // (output_address, Kaspa(0.20000000)), + // (output_address, Kaspa(0.25000000)), + // (output_address, Kaspa(0.21000000)), + // ].as_slice()) + // .unwrap() + // .harness() + // // .accumulate(1) + // .fetch(&Expected { + // is_final: true, + // input_count: 2, + // aggregate_input_value: Kaspa(1.00000000 + 87.23579967), + // output_count: 4, + // priority_fees: FeesExpected::receiver(Kaspa(1.0)), + // // priority_fees: FeesExpected::None, + // }) + // .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 93becef42..acb5869e0 100644 --- a/wallet/core/src/wallet/api.rs +++ b/wallet/core/src/wallet/api.rs @@ -343,9 +343,19 @@ impl WalletApi for super::Wallet { Ok(AccountsEnsureDefaultResponse { account_descriptor }) } - async fn accounts_import_call(self: Arc, _request: AccountsImportRequest) -> Result { - // TODO handle account imports - return Err(Error::NotImplemented); + async fn accounts_import_call(self: Arc, request: AccountsImportRequest) -> Result { + let AccountsImportRequest { wallet_secret, account_create_args } = request; + + let guard = self.guard(); + let guard = guard.lock().await; + + let account = self.create_account(&wallet_secret, account_create_args, true, &guard).await?; + account.clone().scan(Some(100), Some(5000)).await?; + let account_descriptor = account.descriptor()?; + self.store().as_account_store()?.store_single(&account.to_storage()?, account.metadata()?.as_ref()).await?; + self.store().commit(&wallet_secret).await?; + + Ok(AccountsImportResponse { account_descriptor }) } async fn accounts_get_call(self: Arc, request: AccountsGetRequest) -> Result { @@ -379,7 +389,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; @@ -387,7 +398,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 }) } @@ -398,6 +409,7 @@ impl WalletApi for super::Wallet { destination_account_id, wallet_secret, payment_secret, + fee_rate, priority_fee_sompi, transfer_amount_sompi, } = request; @@ -413,6 +425,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, @@ -426,7 +439,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; @@ -445,7 +458,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/wallet/mod.rs b/wallet/core/src/wallet/mod.rs index e9316a13b..ffec7d65c 100644 --- a/wallet/core/src/wallet/mod.rs +++ b/wallet/core/src/wallet/mod.rs @@ -1474,7 +1474,6 @@ impl Wallet { let legacy_account = account.clone().as_legacy_account()?; legacy_account.create_private_context(wallet_secret, payment_secret, None).await?; - // account.clone().initialize_private_data(wallet_secret, payment_secret, None).await?; if self.is_connected() { if let Some(notifier) = notifier { @@ -1483,13 +1482,6 @@ impl Wallet { account.clone().scan(Some(100), Some(5000)).await?; } - // let derivation = account.clone().as_derivation_capable()?.derivation(); - // let m = derivation.receive_address_manager(); - // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; - // let m = derivation.change_address_manager(); - // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; - // account.clone().clear_private_data().await?; - legacy_account.clear_private_context().await?; Ok(account) 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, 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)] diff --git a/wallet/core/src/wasm/tx/mass.rs b/wallet/core/src/wasm/tx/mass.rs index dedff04fb..2414bc908 100644 --- a/wallet/core/src/wasm/tx/mass.rs +++ b/wallet/core/src/wasm/tx/mass.rs @@ -1,8 +1,11 @@ use crate::result::Result; use crate::tx::{mass, MAXIMUM_STANDARD_TRANSACTION_MASS}; +use js_sys::Array; use kaspa_consensus_client::*; use kaspa_consensus_core::config::params::Params; +use kaspa_consensus_core::mass::calc_storage_mass; use kaspa_consensus_core::network::{NetworkId, NetworkIdT}; +use kaspa_wasm_core::types::NumberArray; use wasm_bindgen::prelude::*; use workflow_wasm::convert::*; @@ -92,3 +95,28 @@ pub fn calculate_unsigned_transaction_fee( Ok(Some(mc.calc_fee_for_mass(mass))) } } + +/// `calculateStorageMass()` is a helper function to compute the storage mass of inputs and outputs. +/// This function can be use to calculate the storage mass of transaction inputs and outputs. +/// Note that the storage mass is only a component of the total transaction mass. You are not +/// meant to use this function by itself and should use `calculateTransactionMass()` instead. +/// This function purely exists for diagnostic purposes and to help with complex algorithms that +/// may require a manual UTXO selection for identifying UTXOs and outputs needed for low storage mass. +/// +/// @category Wallet SDK +/// @see {@link maximumStandardTransactionMass} +/// @see {@link calculateTransactionMass} +/// +#[wasm_bindgen(js_name = calculateStorageMass)] +pub fn calculate_storage_mass(network_id: NetworkIdT, input_values: &NumberArray, output_values: &NumberArray) -> Result> { + let network_id = NetworkId::try_owned_from(network_id)?; + let consensus_params = Params::from(network_id); + + let input_values = Array::from(input_values).to_vec().iter().map(|v| v.as_f64().unwrap() as u64).collect::>(); + let output_values = Array::from(output_values).to_vec().iter().map(|v| v.as_f64().unwrap() as u64).collect::>(); + + let storage_mass = + calc_storage_mass(false, input_values.into_iter(), output_values.into_iter(), consensus_params.storage_mass_parameter); + + Ok(storage_mass) +} diff --git a/wallet/core/src/wasm/wallet/wallet.rs b/wallet/core/src/wasm/wallet/wallet.rs index bd91bedf2..57f5a817f 100644 --- a/wallet/core/src/wasm/wallet/wallet.rs +++ b/wallet/core/src/wasm/wallet/wallet.rs @@ -3,6 +3,7 @@ use crate::storage::local::interface::LocalStore; use crate::storage::WalletDescriptor; use crate::wallet as native; use crate::wasm::notify::{WalletEventTarget, WalletNotificationCallback, WalletNotificationTypeOrCallback}; +use kaspa_consensus_core::network::NetworkIdT; use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; use kaspa_wasm_core::events::{get_event_targets, Sink}; use kaspa_wrpc_wasm::{IConnectOptions, Resolver, RpcClient, RpcConfig, WrpcEncoding}; @@ -264,6 +265,12 @@ impl Wallet { } Ok(()) } + + #[wasm_bindgen(js_name = "setNetworkId")] + pub fn set_network_id(&self, network_id: NetworkIdT) -> Result<()> { + self.inner.wallet.set_network_id(&network_id.try_into_owned()?)?; + Ok(()) + } } impl Wallet { diff --git a/wasm/core/src/types.rs b/wasm/core/src/types.rs index 7e8e29335..ee5d066bb 100644 --- a/wasm/core/src/types.rs +++ b/wasm/core/src/types.rs @@ -50,6 +50,12 @@ extern "C" { pub type StringArray; } +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Array")] + pub type NumberArray; +} + #[wasm_bindgen] extern "C" { #[wasm_bindgen(typescript_type = "HexString | Uint8Array")] diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index d8b0f06a9..1bdede15a 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -179,8 +179,9 @@ cfg_if::cfg_if! { } pub use kaspa_consensus_wasm::*; - pub use kaspa_wallet_keys::prelude::*; pub use kaspa_wallet_core::wasm::*; + pub use kaspa_wallet_keys::prelude::*; + pub use kaspa_bip32::wasm::*; } else if #[cfg(feature = "wasm32-core")] { @@ -208,6 +209,7 @@ cfg_if::cfg_if! { pub use kaspa_consensus_wasm::*; pub use kaspa_wallet_keys::prelude::*; pub use kaspa_wallet_core::wasm::*; + pub use kaspa_bip32::wasm::*; } else if #[cfg(feature = "wasm32-rpc")] { @@ -223,8 +225,8 @@ cfg_if::cfg_if! { pub use kaspa_addresses::{Address, Version as AddressVersion}; pub use kaspa_wallet_keys::prelude::*; - pub use kaspa_bip32::*; pub use kaspa_wasm_core::types::*; + pub use kaspa_bip32::wasm::*; } }