diff --git a/Cargo.lock b/Cargo.lock index 5023bf4187..440f2f256f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3436,6 +3436,7 @@ dependencies = [ "kaspa-txscript-errors", "kaspa-utils", "secp256k1", + "separator", "serde", "serde-value", "serde-wasm-bindgen", @@ -3443,6 +3444,8 @@ dependencies = [ "serde_repr", "thiserror", "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", "workflow-wasm", ] diff --git a/cli/src/cli.rs b/cli/src/cli.rs index a32956740a..43a62ec5c5 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -285,12 +285,14 @@ impl KaspaCli { if let Ok(msg) = msg { match *msg { + Events::WalletList { .. } => {}, Events::WalletPing => { // log_info!("Kaspa NG - received wallet ping"); }, Events::Metrics { network_id : _, metrics : _ } => { // log_info!("Kaspa NG - received metrics event {metrics:?}") } + Events::FeeRate { .. } => {}, Events::Error { message } => { terrorln!(this,"{message}"); }, Events::UtxoProcStart => {}, Events::UtxoProcStop => {}, diff --git a/cli/src/modules/account.rs b/cli/src/modules/account.rs index 5848d43fba..49823eefb2 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 a37a8a47c2..9ab717d54b 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 fd33087c22..960578e7f6 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(), @@ -209,13 +214,12 @@ impl Pskb { for (pskt_index, bundle_inner) in pskb.0.iter().enumerate() { tprintln!(ctx, "PSKT #{:03} finalized check:", pskt_index + 1); let pskt: PSKT = PSKT::::from(bundle_inner.to_owned()); - + let params = ctx.wallet().network_id()?.into(); let finalizer = pskt.finalizer(); - if let Ok(pskt_finalizer) = finalize_pskt_one_or_more_sig_and_redeem_script(finalizer) { // Verify if extraction is possible. match pskt_finalizer.extractor() { - Ok(ex) => match ex.extract_tx() { + Ok(ex) => match ex.extract_tx(¶ms) { Ok(_) => tprintln!( ctx, " Transaction extracted successfully: PSKT is finalized with a valid script signature." diff --git a/cli/src/modules/send.rs b/cli/src/modules/send.rs index 773861dd4a..8c28679a99 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 aeca2baa35..6e68b39456 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 3dea692993..0caf0e4930 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/cli/src/wizards/account.rs b/cli/src/wizards/account.rs index 9d3d4d5916..62f68a651d 100644 --- a/cli/src/wizards/account.rs +++ b/cli/src/wizards/account.rs @@ -3,6 +3,7 @@ use crate::imports::*; use crate::result::Result; use kaspa_bip32::{Language, Mnemonic, WordCount}; use kaspa_wallet_core::account::MULTISIG_ACCOUNT_KIND; +use kaspa_wallet_core::storage::keydata::PrvKeyDataVariantKind; // use kaspa_wallet_core::runtime::wallet::AccountCreateArgsBip32; // use kaspa_wallet_core::runtime::{PrvKeyDataArgs, PrvKeyDataCreateArgs}; // use kaspa_wallet_core::storage::AccountKind; @@ -66,7 +67,7 @@ async fn create_multisig(ctx: &Arc, account_name: Option, mnem for _ in 0..prv_keys_len { let bip39_mnemonic = Secret::from(Mnemonic::random(mnemonic_phrase_word_count, Language::default())?.phrase()); - let prv_key_data_create_args = PrvKeyDataCreateArgs::new(None, None, bip39_mnemonic); // can be optimized with Rc + let prv_key_data_create_args = PrvKeyDataCreateArgs::new(None, None, bip39_mnemonic, PrvKeyDataVariantKind::Mnemonic); // can be optimized with Rc let prv_key_data_id = wallet.create_prv_key_data(&wallet_secret, prv_key_data_create_args).await?; prv_key_data_args.push(PrvKeyDataArgs::new(prv_key_data_id, None)); diff --git a/cli/src/wizards/wallet.rs b/cli/src/wizards/wallet.rs index 0fae267a36..4c0ae98a15 100644 --- a/cli/src/wizards/wallet.rs +++ b/cli/src/wizards/wallet.rs @@ -2,6 +2,7 @@ use crate::cli::KaspaCli; use crate::imports::*; use crate::result::Result; use kaspa_bip32::{Language, Mnemonic, WordCount}; +use kaspa_wallet_core::storage::keydata::PrvKeyDataVariantKind; use kaspa_wallet_core::{ storage::{make_filename, Hint}, wallet::WalletGuard, @@ -123,16 +124,17 @@ pub(crate) async fn create( let prv_key_data_args = if import_with_mnemonic { let words = crate::wizards::import::prompt_for_mnemonic(&term).await?; - PrvKeyDataCreateArgs::new(None, payment_secret.clone(), Secret::from(words.join(" "))) + PrvKeyDataCreateArgs::new(None, payment_secret.clone(), Secret::from(words.join(" ")), PrvKeyDataVariantKind::Mnemonic) } else { PrvKeyDataCreateArgs::new( None, payment_secret.clone(), Secret::from(Mnemonic::random(word_count, Language::default())?.phrase()), + PrvKeyDataVariantKind::Mnemonic, ) }; - let mnemonic_phrase = prv_key_data_args.mnemonic.clone(); + let mnemonic_phrase = prv_key_data_args.secret.clone(); let notifier = ctx.notifier().show(Notification::Processing).await; diff --git a/crypto/addresses/src/lib.rs b/crypto/addresses/src/lib.rs index aa0d6d8d3f..b923febfee 100644 --- a/crypto/addresses/src/lib.rs +++ b/crypto/addresses/src/lib.rs @@ -512,7 +512,7 @@ impl TryCastFromJs for Address { { Self::resolve(value, || { if let Some(string) = value.as_ref().as_string() { - Address::try_from(string) + Address::try_from(string.trim()) } else if let Some(object) = js_sys::Object::try_from(value.as_ref()) { let prefix: Prefix = object.get_string("prefix")?.as_str().try_into()?; let payload = object.get_string("payload")?; //.as_str(); diff --git a/crypto/txscript/src/opcodes/macros.rs b/crypto/txscript/src/opcodes/macros.rs index a4b3bfbbf4..84cf484806 100644 --- a/crypto/txscript/src/opcodes/macros.rs +++ b/crypto/txscript/src/opcodes/macros.rs @@ -139,7 +139,7 @@ macro_rules! opcode_list { } } else if let Some(Ok(value)) = token.strip_prefix("0x").and_then(|trimmed| Some(hex::decode(trimmed))) { - builder.extend(&value); + builder.script_mut().extend(&value); } else if token.len() >= 2 && token.chars().nth(0) == Some('\'') && token.chars().last() == Some('\'') { builder.add_data(token[1..token.len()-1].as_bytes())?; diff --git a/crypto/txscript/src/script_builder.rs b/crypto/txscript/src/script_builder.rs index 7a5b28ca5a..8636e226a6 100644 --- a/crypto/txscript/src/script_builder.rs +++ b/crypto/txscript/src/script_builder.rs @@ -74,11 +74,15 @@ impl ScriptBuilder { &self.script } - #[cfg(any(test, target_arch = "wasm32"))] - pub fn extend(&mut self, data: &[u8]) { - self.script.extend(data); + pub fn script_mut(&mut self) -> &mut Vec { + &mut self.script } + // #[cfg(any(test, target_arch = "wasm32"))] + // pub fn extend(&mut self, data: &[u8]) { + // self.script.extend(data); + // } + pub fn drain(&mut self) -> Vec { // Note that the internal script, when taken, is replaced by // vector with no predefined capacity because the script diff --git a/crypto/txscript/src/wasm/builder.rs b/crypto/txscript/src/wasm/builder.rs index 57c6b8b4f4..edf9e169c4 100644 --- a/crypto/txscript/src/wasm/builder.rs +++ b/crypto/txscript/src/wasm/builder.rs @@ -53,7 +53,7 @@ impl ScriptBuilder { pub fn from_script(script: BinaryT) -> Result { let builder = ScriptBuilder::default(); let script = script.try_as_vec_u8()?; - builder.inner_mut().extend(&script); + builder.inner_mut().script_mut().extend(&script); Ok(builder) } diff --git a/rpc/wrpc/client/Cargo.toml b/rpc/wrpc/client/Cargo.toml index 1cc0a21917..911813de90 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 ccd9cb284b..63382fcca2 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/utils/src/lib.rs b/utils/src/lib.rs index c6cc077c4a..de40d4a7bd 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -73,6 +73,34 @@ pub mod as_slice; /// assert_eq!(test_struct, from_json); /// ``` pub mod serde_bytes; + +/// # Examples +/// ## Implement serde::Serialize/serde::Deserialize for the Vec field of the struct +/// ``` +/// #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +/// struct MyStructVec { +/// #[serde(with = "kaspa_utils::serde_bytes_optional")] +/// v: Option>, +/// } +/// let v = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; +/// let len = v.len(); +/// let test_struct = MyStructVec { v: Some(v.clone()) }; +/// +/// +/// // Serialize using bincode +/// let encoded = bincode::serialize(&test_struct).unwrap(); +/// assert_eq!(encoded, std::iter::once(1).chain(len.to_le_bytes().into_iter()).chain(v.into_iter()).collect::>()); +/// // Deserialize using bincode +/// let decoded: MyStructVec = bincode::deserialize(&encoded).unwrap(); +/// assert_eq!(test_struct, decoded); +/// +/// let expected_str = r#"{"v":"000102030405060708090a0b0c0d0e0f10111213"}"#; +/// // Serialize using serde_json +/// let json = serde_json::to_string(&test_struct).unwrap(); +/// assert_eq!(expected_str, json); +/// // Deserialize using serde_json +/// let from_json: MyStructVec = serde_json::from_str(&json).unwrap(); +/// assert_eq!(test_struct, from_json); pub mod serde_bytes_optional; /// # Examples diff --git a/wallet/bip32/src/lib.rs b/wallet/bip32/src/lib.rs index 1926728c43..74486e9a75 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/descriptor.rs b/wallet/core/src/account/descriptor.rs index c3bf97cc1b..1ca725b851 100644 --- a/wallet/core/src/account/descriptor.rs +++ b/wallet/core/src/account/descriptor.rs @@ -26,6 +26,7 @@ pub struct AccountDescriptor { pub prv_key_data_ids: AssocPrvKeyDataIds, pub receive_address: Option
, pub change_address: Option
, + pub addresses: Option>, pub properties: BTreeMap, } @@ -39,6 +40,7 @@ impl AccountDescriptor { prv_key_data_ids: AssocPrvKeyDataIds, receive_address: Option
, change_address: Option
, + addresses: Option>, ) -> Self { Self { kind, @@ -48,6 +50,7 @@ impl AccountDescriptor { prv_key_data_ids, receive_address, change_address, + addresses, properties: BTreeMap::default(), } } @@ -241,6 +244,7 @@ declare! { accountName? : string, receiveAddress? : Address, changeAddress? : Address, + addresses? : Address[], prvKeyDataIds : HexString[], // balance? : Balance, [key: string]: any @@ -259,6 +263,9 @@ impl TryFrom for IAccountDescriptor { object.set("receiveAddress", &descriptor.receive_address.into())?; object.set("changeAddress", &descriptor.change_address.into())?; + let addresses = js_sys::Array::from_iter(descriptor.addresses.into_iter().map(JsValue::from)); + object.set("addresses", &addresses)?; + let prv_key_data_ids = js_sys::Array::from_iter(descriptor.prv_key_data_ids.into_iter().map(JsValue::from)); object.set("prvKeyDataIds", &prv_key_data_ids)?; diff --git a/wallet/core/src/account/mod.rs b/wallet/core/src/account/mod.rs index 31c7fea9d5..70cca0ad12 100644 --- a/wallet/core/src/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -11,8 +11,8 @@ use kaspa_hashes::Hash; use kaspa_wallet_pskt::bundle::Bundle; pub use kind::*; use pskb::{ - bundle_from_pskt_generator, bundle_to_finalizer_stream, pskb_signer_for_address, pskt_to_pending_transaction, PSKBSigner, - PSKTGenerator, + bundle_from_pskt_generator, bundle_to_finalizer_stream, commit_reveal_batch_bundle, pskb_signer_for_address, + pskt_to_pending_transaction, PSKBSigner, PSKTGenerator, }; pub use variants::*; @@ -27,6 +27,7 @@ use crate::tx::{Fees, Generator, GeneratorSettings, GeneratorSummary, PaymentDes use crate::utxo::balance::{AtomicBalance, BalanceStrings}; use crate::utxo::UtxoContextBinding; use kaspa_bip32::{ChildNumber, ExtendedPrivateKey, PrivateKey}; +use kaspa_consensus_client::UtxoEntry; use kaspa_consensus_client::UtxoEntryReference; use kaspa_wallet_keys::derivation::gen0::WalletDerivationManagerV0; use workflow_core::abortable::Abortable; @@ -265,6 +266,16 @@ pub trait Account: AnySync + Send + Sync + 'static { fn minimum_signatures(&self) -> u16; + // default account address (receive[0]) + fn default_address(&self) -> Result
{ + Err(Error::NotImplemented) + } + + // all addresses in the account (receive + change up to and including the last used index) + fn account_addresses(&self) -> Result> { + Err(Error::NotImplemented) + } + fn receive_address(&self) -> Result
; fn change_address(&self) -> Result
; @@ -305,13 +316,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 +351,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 +362,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))?; @@ -363,16 +382,70 @@ pub trait Account: AnySync + Send + Sync + 'static { Ok((generator.summary(), ids)) } + async fn commit_reveal_manual( + self: Arc, + start_destination: PaymentDestination, + end_destination: PaymentDestination, + script_sig: Vec, + wallet_secret: Secret, + payment_secret: Option, + fee_rate: Option, + reveal_fee_sompi: u64, + payload: Option>, + abortable: &Abortable, + ) -> Result { + commit_reveal_batch_bundle( + pskb::CommitRevealBatchKind::Manual { hop_payment: start_destination, destination_payment: end_destination }, + reveal_fee_sompi, + script_sig, + payload, + fee_rate, + self.clone().as_dyn_arc(), + wallet_secret, + payment_secret, + abortable, + ) + .await + } + + async fn commit_reveal( + self: Arc, + address: Address, + script_sig: Vec, + wallet_secret: Secret, + payment_secret: Option, + commit_amount_sompi: u64, + fee_rate: Option, + reveal_fee_sompi: u64, + payload: Option>, + abortable: &Abortable, + ) -> Result { + commit_reveal_batch_bundle( + pskb::CommitRevealBatchKind::Parameterized { address, commit_amount_sompi }, + reveal_fee_sompi, + script_sig, + payload, + fee_rate, + self.clone().as_dyn_arc(), + wallet_secret, + payment_secret, + abortable, + ) + .await + } + 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))?; @@ -391,12 +464,19 @@ pub trait Account: AnySync + Send + Sync + 'static { let signer = Arc::new(PSKBSigner::new(self.clone().as_dyn_arc(), keydata.clone(), payment_secret.clone())); let network_id = self.wallet().clone().network_id()?; - let derivation = self.as_derivation_capable()?; + let (derivation_path, key_fingerprint) = if self.account_kind() == KEYPAIR_ACCOUNT_KIND { + // let secret_key = keydata.as_secret_key(payment_secret.as_ref())?.ok_or(Error::Custom(format!("Private key not found for account")))?; + // (None, secp256k1::PublicKey::from_secret_key_global(&secret_key).fingerprint()) + (None, None) + } else { + let derivation = self.as_derivation_capable()?; - let (derivation_path, _) = - build_derivate_paths(&derivation.account_kind(), derivation.account_index(), derivation.cosigner_index())?; + let (derivation_path, _) = + build_derivate_paths(&derivation.account_kind(), derivation.account_index(), derivation.cosigner_index())?; - let key_fingerprint = keydata.get_xprv(payment_secret.clone().as_ref())?.public_key().fingerprint(); + let key_fingerprint = keydata.get_xprv(payment_secret.clone().as_ref())?.public_key().fingerprint(); + (Some(derivation_path), Some(key_fingerprint)) + }; match pskb_signer_for_address(bundle, signer, network_id, sign_for_address, derivation_path, key_fingerprint).await { Ok(signer) => Ok(signer), @@ -411,23 +491,32 @@ pub trait Account: AnySync + Send + Sync + 'static { while let Some(result) = stream.next().await { match result { Ok(pskt) => { - let change = self.wallet().account()?.change_address()?; - let transaction = pskt_to_pending_transaction(pskt, self.wallet().network_id()?, change)?; + let change = self.change_address()?; + let transaction = + pskt_to_pending_transaction(pskt, self.wallet().network_id()?, change, self.utxo_context().clone().into())?; + log_info!("Submitting to rpc"); ids.push(transaction.try_submit(&self.wallet().rpc_api()).await?); + log_info!("Submitted to rpc"); } Err(e) => { - eprintln!("Error processing a PSKT from bundle: {:?}", e); + log_info!("Error processing a PSKT from bundle: {:?}", e); } } } Ok(ids) } + async fn get_utxos(self: Arc, addresses: Option>, min_amount_sompi: Option) -> Result> { + let utxos = self.utxo_context().get_utxos(addresses, min_amount_sompi).await?; + Ok(utxos) + } + /// Execute a transfer to another wallet account. async fn transfer( 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 +540,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 +566,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))?; @@ -499,6 +590,18 @@ pub trait Account: AnySync + Send + Sync + 'static { fn as_legacy_account(self: Arc) -> Result> { Err(Error::InvalidAccountKind) } + + fn create_address_private_keys<'l>( + self: Arc, + key_data: &PrvKeyData, + payment_secret: &Option, + addresses: &[&'l Address], + ) -> Result> { + let account = self.clone().as_derivation_capable().expect("expecting derivation capable account"); + let (receive, change) = account.derivation().addresses_indexes(addresses)?; + let private_keys = account.create_private_keys(key_data, payment_secret, &receive, &change)?; + Ok(private_keys) + } } downcast_sync!(dyn Account); @@ -517,6 +620,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 +635,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 +664,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 +675,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 +689,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 +731,7 @@ pub trait DerivationCapableAccount: Account { 1, 1, PaymentDestination::Change, + fee_rate, Fees::None, None, None, @@ -646,6 +773,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?; } @@ -694,6 +833,18 @@ pub trait DerivationCapableAccount: Account { let xkey = payload.get_xprv(payment_secret.as_ref())?; create_private_keys(&self.account_kind(), self.cosigner_index(), self.account_index(), &xkey, receive, change) } + + // Retrieve receive address by index. + async fn receive_address_at_index(self: Arc, index: u32) -> Result
{ + let address = self.derivation().receive_address_manager().get_range(index..index + 1)?.first().unwrap().clone(); + Ok(address) + } + + // Retrieve change address by index. + async fn change_address_at_index(self: Arc, index: u32) -> Result
{ + let address = self.derivation().change_address_manager().get_range(index..index + 1)?.first().unwrap().clone(); + Ok(address) + } } downcast_sync!(dyn DerivationCapableAccount); diff --git a/wallet/core/src/account/pskb.rs b/wallet/core/src/account/pskb.rs index fad6bdb4ab..9632a592b3 100644 --- a/wallet/core/src/account/pskb.rs +++ b/wallet/core/src/account/pskb.rs @@ -5,6 +5,7 @@ pub use crate::error::Error; use crate::imports::*; +use crate::tx::PaymentOutput; use crate::tx::PaymentOutputs; use futures::stream; use kaspa_bip32::{DerivationPath, KeyFingerprint, PrivateKey}; @@ -17,6 +18,8 @@ use kaspa_txscript::opcodes::codes::OpData65; use kaspa_txscript::script_builder::ScriptBuilder; use kaspa_wallet_core::tx::{Generator, GeneratorSettings, PaymentDestination, PendingTransaction}; pub use kaspa_wallet_pskt::bundle::Bundle; +use kaspa_wallet_pskt::bundle::{script_sig_to_address, unlock_utxo_outputs_as_batch_transaction_pskb}; +use kaspa_wallet_pskt::prelude::lock_script_sig_templating_bytes; use kaspa_wallet_pskt::prelude::KeySource; use kaspa_wallet_pskt::prelude::{Finalizer, Inner, SignInputOk, Signature, Signer}; pub use kaspa_wallet_pskt::pskt::{Creator, PSKT}; @@ -46,9 +49,14 @@ impl PSKBSigner { // Skip addresses that are already present in the key map. let addresses = addresses.iter().filter(|a| !keys.contains_key(a)).collect::>(); if !addresses.is_empty() { - let account = self.inner.account.clone().as_derivation_capable().expect("expecting derivation capable account"); - let (receive, change) = account.derivation().addresses_indexes(&addresses)?; - let private_keys = account.create_private_keys(&self.inner.keydata, &self.inner.payment_secret, &receive, &change)?; + // let account = self.inner.account.clone().as_derivation_capable().expect("expecting derivation capable account"); + // let (receive, change) = account.derivation().addresses_indexes(&addresses)?; + // let private_keys = account.create_private_keys(&self.inner.keydata, &self.inner.payment_secret, &receive, &change)?; + let private_keys = self.inner.account.clone().create_address_private_keys( + &self.inner.keydata, + &self.inner.payment_secret, + addresses.as_slice(), + )?; for (address, private_key) in private_keys { keys.insert(address.clone(), private_key.to_bytes()); } @@ -150,74 +158,92 @@ pub async fn bundle_from_pskt_generator(generator: PSKTGenerator) -> Result, network_id: NetworkId, sign_for_address: Option<&Address>, - derivation_path: DerivationPath, - key_fingerprint: KeyFingerprint, + derivation_path: Option, + key_fingerprint: Option, ) -> Result { let mut signed_bundle = Bundle::new(); - let reused_values = SigHashReusedValuesUnsync::new(); - // If set, sign-for address is used for signing. - // Else, all addresses from inputs are. - let addresses: Vec
= match sign_for_address { - Some(signer) => vec![signer.clone()], - None => bundle + // If sign_for_address is provided, we'll use it for all signatures + // Otherwise, collect addresses per PSKT + let addresses_per_pskt: Vec> = if sign_for_address.is_some() { + // Create a vec of single-address vecs + bundle.iter().map(|_| vec![sign_for_address.unwrap().clone()]).collect() + } else { + // Collect addresses for each PSKT separately + bundle .iter() - .flat_map(|inner| { - inner.inputs + .map(|inner| { + inner + .inputs .iter() - .filter_map(|input| input.utxo_entry.as_ref()) // Filter out None and get a reference to UtxoEntry if it exists + .filter_map(|input| input.utxo_entry.as_ref()) .filter_map(|utxo_entry| { extract_script_pub_key_address(&utxo_entry.script_public_key.clone(), network_id.into()).ok() }) - .collect::>() + .collect() }) - .collect(), + .collect() }; - // Prepare the signer. - signer.ingest(addresses.as_ref())?; + // Prepare the signer with all unique addresses + let all_addresses: Vec
= addresses_per_pskt.iter().flat_map(|addresses| addresses.iter().cloned()).collect(); + signer.ingest(all_addresses.as_slice())?; - for pskt_inner in bundle.iter().cloned() { + // in case of keypair account, we don't have a derivation path, + // so we need to skip the key source + let mut key_source = None; + if let Some(key_fingerprint) = key_fingerprint { + if let Some(derivation_path) = derivation_path { + key_source = Some(KeySource { key_fingerprint, derivation_path: derivation_path.clone() }); + } + } + + // Process each PSKT in the bundle + for (pskt_idx, pskt_inner) in bundle.iter().cloned().enumerate() { let pskt: PSKT = PSKT::from(pskt_inner); + let current_addresses = &addresses_per_pskt[pskt_idx]; + + // Create new reused values for each PSKT + let reused_values = SigHashReusedValuesUnsync::new(); - let sign = |signer_pskt: PSKT| { + let sign = |signer_pskt: PSKT| -> Result, Error> { signer_pskt .pass_signature_sync(|tx, sighash| -> Result, String> { tx.tx .inputs .iter() .enumerate() - .map(|(idx, _input)| { - let hash = calc_schnorr_signature_hash(&tx.as_verifiable(), idx, sighash[idx], &reused_values); - let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice()).unwrap(); - - // When address represents a locked UTXO, no private key is available. - // Instead, use the account receive address' private key. - let address: &Address = match sign_for_address { - Some(address) => address, - None => addresses.get(idx).expect("Input indexed address"), + .map(|(input_idx, _input)| { + let hash = calc_schnorr_signature_hash(&tx.as_verifiable(), input_idx, sighash[input_idx], &reused_values); + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice()).map_err(|e| e.to_string())?; + + // Get the appropriate address for this input + let address = if let Some(sign_addr) = sign_for_address { + sign_addr + } else { + current_addresses.get(input_idx).ok_or_else(|| format!("No address found for input {}", input_idx))? }; - let public_key = signer.public_key(address).expect("Public key for input indexed address"); + let pub_key = signer.public_key(address).map_err(|e| format!("Failed to get public key: {}", e))?; + + let signature = signer.sign_schnorr(address, msg).map_err(|e| format!("Failed to sign: {}", e))?; - Ok(SignInputOk { - signature: Signature::Schnorr(signer.sign_schnorr(address, msg).unwrap()), - pub_key: public_key, - key_source: Some(KeySource { key_fingerprint, derivation_path: derivation_path.clone() }), - }) + Ok(SignInputOk { signature: Signature::Schnorr(signature), pub_key, key_source: key_source.clone() }) }) .collect() }) - .unwrap() + .map_err(Error::from) }; - signed_bundle.add_pskt(sign(pskt.clone())); + + let signed_pskt = sign(pskt)?; + signed_bundle.add_pskt(signed_pskt); } + Ok(signed_bundle) } @@ -287,57 +313,64 @@ pub fn pskt_to_pending_transaction( finalized_pskt: PSKT, network_id: NetworkId, change_address: Address, + source_utxo_context: Option, ) -> Result { - let mass = 10; - let (signed_tx, _) = match finalized_pskt.clone().extractor() { - Ok(extractor) => match extractor.extract_tx() { - Ok(once_mass) => once_mass(mass), - Err(e) => return Err(Error::PendingTransactionFromPSKTError(e.to_string())), - }, - Err(e) => return Err(Error::PendingTransactionFromPSKTError(e.to_string())), - }; - - let inner_pskt = finalized_pskt.deref().clone(); - - let utxo_entries_ref: Vec = inner_pskt + let inner_pskt = finalized_pskt.deref(); + let (utxo_entries_ref, aggregate_input_value): (Vec, u64) = inner_pskt .inputs .iter() .filter_map(|input| { - if let Some(ue) = input.clone().utxo_entry { - return Some(UtxoEntryReference { - utxo: Arc::new(ClientUTXO { - address: Some(extract_script_pub_key_address(&ue.script_public_key, network_id.into()).unwrap()), - amount: ue.amount, - outpoint: input.previous_outpoint.into(), - script_public_key: ue.script_public_key, - block_daa_score: ue.block_daa_score, - is_coinbase: ue.is_coinbase, - }), - }); - } - None + input.utxo_entry.as_ref().map(|ue| { + ( + UtxoEntryReference { + utxo: Arc::new(ClientUTXO { + address: Some(extract_script_pub_key_address(&ue.script_public_key, network_id.into()).unwrap()), + amount: ue.amount, + outpoint: input.previous_outpoint.into(), + script_public_key: ue.script_public_key.clone(), + block_daa_score: ue.block_daa_score, + is_coinbase: ue.is_coinbase, + }), + }, + ue.amount, + ) + }) }) - .collect(); - - let output: Vec = signed_tx.outputs.clone(); + .fold((Vec::new(), 0), |(mut vec, sum), (entry, amount)| { + vec.push(entry); + (vec, sum + amount) + }); + let signed_tx = match finalized_pskt.extractor() { + Ok(extractor) => match extractor.extract_tx(&network_id.into()) { + Ok(tx) => tx.tx, + Err(e) => return Err(Error::PendingTransactionFromPSKTError(e.to_string())), + }, + Err(e) => return Err(Error::PendingTransactionFromPSKTError(e.to_string())), + }; + let output: &Vec = &signed_tx.outputs; + if output.is_empty() { + return Err(Error::Custom("0 outputs pskt is not supported".to_string())); + // todo support 0 outputs + } let recipient = extract_script_pub_key_address(&output[0].script_public_key, network_id.into())?; let fee_u: u64 = 0; let utxo_iterator: Box + Send + Sync + 'static> = Box::new(utxo_entries_ref.clone().into_iter()); - let final_transaction_destination = PaymentDestination::PaymentOutputs(PaymentOutputs::from((recipient.clone(), output[0].value))); + let final_transaction_destination = PaymentDestination::PaymentOutputs(PaymentOutputs::from((recipient, output[0].value))); let settings = GeneratorSettings { network_id, multiplexer: None, sig_op_count: 1, minimum_signatures: 1, - change_address, + change_address: change_address.clone(), utxo_iterator, priority_utxo_entries: None, - source_utxo_context: None, + source_utxo_context, destination_utxo_context: None, + fee_rate: None, final_transaction_priority_fee: fee_u.into(), final_transaction_destination, final_transaction_payload: None, @@ -346,17 +379,37 @@ pub fn pskt_to_pending_transaction( // Create the Generator let generator = Generator::try_new(settings, None, None)?; + let aggregate_output_value = output.iter().map(|output| output.value).sum::(); + + let (change_output_index, change_output_value) = output + .iter() + .enumerate() + .find_map(|(idx, output)| { + if let Ok(address) = extract_script_pub_key_address(&output.script_public_key, change_address.prefix) { + if address == change_address { + Some((Some(idx), output.value)) + } else { + None + } + } else { + None + } + }) + .unwrap_or((None, 0)); + // Create PendingTransaction (WIP) + let addresses = utxo_entries_ref.iter().filter_map(|a| a.address()).collect(); + // todo where the source of mass and fees. why does it equal to zero? let pending_tx = PendingTransaction::try_new( &generator, - signed_tx.clone(), - utxo_entries_ref.clone(), - vec![], - None, - None, - 0, - 0, - 0, + signed_tx, + utxo_entries_ref, + addresses, + Some(aggregate_output_value), + change_output_index, + change_output_value, + aggregate_input_value, + aggregate_output_value, 1, 0, 0, @@ -365,3 +418,151 @@ pub fn pskt_to_pending_transaction( Ok(pending_tx) } + +// Allow creation of atomic commit reveal operation with two +// different parameters sets. +pub enum CommitRevealBatchKind { + Manual { hop_payment: PaymentDestination, destination_payment: PaymentDestination }, + Parameterized { address: Address, commit_amount_sompi: u64 }, +} + +struct BundleCommitRevealConfig { + pub address_commit: Address, + pub addresses_reveal: Vec
, + pub commit_destination: PaymentDestination, + pub redeem_script: Vec, + pub payment_outputs: PaymentOutputs, +} + +// Create signed atomic commit reveal PSKB. +pub async fn commit_reveal_batch_bundle( + batch_config: CommitRevealBatchKind, + reveal_fee_sompi: u64, + script_sig: Vec, + payload: Option>, + fee_rate: Option, + account: Arc, + wallet_secret: Secret, + payment_secret: Option, + abortable: &Abortable, +) -> Result { + let network_id = account.wallet().clone().network_id()?; + + // Configure atomic batch of commit reveal transactions + let conf: BundleCommitRevealConfig = match batch_config { + CommitRevealBatchKind::Manual { hop_payment, destination_payment } => { + let addr_commit = match hop_payment.clone() { + PaymentDestination::Change => Err(Error::CommitRevealInvalidPaymentDestination), + PaymentDestination::PaymentOutputs(payment_outputs) => { + payment_outputs.outputs.first().map(|out| out.address.clone()).ok_or(Error::CommitRevealEmptyPaymentOutputs) + } + }?; + + let (addresses, payment_outputs) = match destination_payment { + PaymentDestination::Change => Err(Error::CommitRevealInvalidPaymentDestination), + PaymentDestination::PaymentOutputs(payment_outputs) => { + Ok((payment_outputs.outputs.iter().map(|out| out.address.clone()).collect(), payment_outputs)) + } + }?; + + BundleCommitRevealConfig { + address_commit: addr_commit, + addresses_reveal: addresses, + commit_destination: hop_payment, + redeem_script: script_sig, + payment_outputs, + } + } + CommitRevealBatchKind::Parameterized { address, commit_amount_sompi } => { + let redeem_script = lock_script_sig_templating_bytes(script_sig.to_vec(), Some(&address.payload)) + .map_err(|_| Error::RevealRedeemScriptTemplateError)?; + + let lock_address = script_sig_to_address(&redeem_script, network_id.into())?; + + let amt_reveal: u64 = commit_amount_sompi - reveal_fee_sompi; + + BundleCommitRevealConfig { + address_commit: lock_address.clone(), + addresses_reveal: vec![address.clone()], + commit_destination: PaymentDestination::from(PaymentOutput::new(lock_address, commit_amount_sompi)), + redeem_script, + payment_outputs: PaymentOutputs { outputs: vec![PaymentOutput::new(address.clone(), amt_reveal)] }, + } + } + }; + + // Generate commit transaction + let settings = GeneratorSettings::try_new_with_account( + account.clone().as_dyn_arc(), + conf.commit_destination.clone(), + fee_rate.or(Some(1.0)), + 0u64.into(), + payload, + ) + .map_err(|e| Error::PSKTGenerationError(e.to_string()))?; + + let signer = Arc::new(PSKBSigner::new( + account.clone().as_dyn_arc(), + account.prv_key_data(wallet_secret.clone()).await?, + payment_secret.clone(), + )); + + let generator = Generator::try_new(settings, None, Some(abortable)).map_err(|e| Error::PSKTGenerationError(e.to_string()))?; + + let pskt_generator = PSKTGenerator::new(generator, signer, account.wallet().address_prefix()?); + + let bundle_commit = bundle_from_pskt_generator(pskt_generator).await.map_err(|e| Error::PSKTGenerationError(e.to_string()))?; + + // Generate reveal transaction + let bundle_unlock = unlock_utxo_outputs_as_batch_transaction_pskb( + conf.commit_destination.amount().unwrap(), + &conf.address_commit, + &conf.redeem_script, + conf.payment_outputs.outputs.into_iter().map(|i| (i.address.clone(), i.amount)).collect(), + ) + .map_err(|e| Error::PSKTGenerationError(e.to_string()))?; + + // Sign and finalize commit transaction + let (mut merge_bundle, commit_transaction_id) = { + let signed_pskb = account + .clone() + .pskb_sign(&bundle_commit, wallet_secret.clone(), payment_secret.clone(), None) + .await + .map_err(|_| Error::CommitTransactionSigningError)?; + + let merge_bundle = Bundle::deserialize(&signed_pskb.serialize()?).map_err(|_| Error::CommitRevealBundleMergeError)?; + + let pskt: PSKT = PSKT::::from(signed_pskb.as_ref()[0].to_owned()); + let finalizer = pskt.finalizer(); + + let pskt_finalizer = finalize_pskt_one_or_more_sig_and_redeem_script(finalizer).map_err(|_| Error::PSKTFinalizationError)?; + + let transaction_id = pskt_to_pending_transaction( + pskt_finalizer.clone(), + network_id, + account.change_address()?, + account.utxo_context().clone().into(), + ) + .map_err(|_| Error::CommitTransactionIdExtractionError)? + .id(); + (merge_bundle, transaction_id) + }; + + // Set commit transaction ID in reveal batch transaction input + let reveal_pskt: PSKT = PSKT::::from(bundle_unlock.as_ref()[0].to_owned()); + let unorphaned_bundle_unlock = Bundle::from(reveal_pskt.set_input_prev_transaction_id(commit_transaction_id)); + + // Try signing with each reveal address + for reveal_address in &conf.addresses_reveal { + if let Ok(signed_pskb) = account + .clone() + .pskb_sign(&unorphaned_bundle_unlock, wallet_secret.clone(), payment_secret.clone(), Some(reveal_address)) + .await + { + merge_bundle.merge(signed_pskb); + return Ok(merge_bundle); + } + } + + Err(Error::NoQualifiedRevealSignerFound) +} diff --git a/wallet/core/src/account/variants/bip32.rs b/wallet/core/src/account/variants/bip32.rs index 1c120df4b4..d5c5d99083 100644 --- a/wallet/core/src/account/variants/bip32.rs +++ b/wallet/core/src/account/variants/bip32.rs @@ -192,10 +192,29 @@ impl Account for Bip32 { fn receive_address(&self) -> Result
{ self.derivation.receive_address_manager().current_address() } + fn change_address(&self) -> Result
{ self.derivation.change_address_manager().current_address() } + // default account address (receive[0]) + fn default_address(&self) -> Result
{ + // TODO @surinder + let addresses = self.derivation.receive_address_manager().get_range_with_args(0..1, false)?; + addresses.first().cloned().ok_or(Error::AddressNotFound) + } + + // all addresses in the account (receive + change up to and including the last used index) + fn account_addresses(&self) -> Result> { + let meta = self.derivation.address_derivation_meta(); + let receive = meta.receive(); + let change = meta.change(); + let mut addresses = self.derivation.receive_address_manager().get_range_with_args(0..receive, false)?; + let change_addresses = self.derivation.change_address_manager().get_range_with_args(0..change, false)?; + addresses.extend(change_addresses); + Ok(addresses) + } + fn to_storage(&self) -> Result { let settings = self.context().settings.clone(); let storable = Payload::new(self.account_index, self.xpub_keys.clone(), self.ecdsa); @@ -225,6 +244,7 @@ impl Account for Bip32 { self.prv_key_data_id.into(), self.receive_address().ok(), self.change_address().ok(), + self.account_addresses().ok(), ) .with_property(AccountDescriptorProperty::AccountIndex, self.account_index.into()) .with_property(AccountDescriptorProperty::XpubKeys, self.xpub_keys.clone().into()) diff --git a/wallet/core/src/account/variants/bip32watch.rs b/wallet/core/src/account/variants/bip32watch.rs index cfadb745df..7c1c22a1f3 100644 --- a/wallet/core/src/account/variants/bip32watch.rs +++ b/wallet/core/src/account/variants/bip32watch.rs @@ -177,6 +177,24 @@ impl Account for Bip32Watch { self.derivation.change_address_manager().current_address() } + // default account address (receive[0]) + fn default_address(&self) -> Result
{ + // TODO @surinder + let addresses = self.derivation.receive_address_manager().get_range_with_args(0..1, false)?; + addresses.first().cloned().ok_or(Error::AddressNotFound) + } + + // all addresses in the account (receive + change up to and including the last used index) + fn account_addresses(&self) -> Result> { + let meta = self.derivation.address_derivation_meta(); + let receive = meta.receive(); + let change = meta.change(); + let mut addresses = self.derivation.receive_address_manager().get_range_with_args(0..receive, false)?; + let change_addresses = self.derivation.change_address_manager().get_range_with_args(0..change, false)?; + addresses.extend(change_addresses); + Ok(addresses) + } + fn to_storage(&self) -> Result { let settings = self.context().settings.clone(); let storable = Payload::new(self.xpub_keys.clone(), self.ecdsa); @@ -207,6 +225,7 @@ impl Account for Bip32Watch { AssocPrvKeyDataIds::None, self.receive_address().ok(), self.change_address().ok(), + self.account_addresses().ok(), ) .with_property(AccountDescriptorProperty::XpubKeys, self.xpub_keys.clone().into()) .with_property(AccountDescriptorProperty::Ecdsa, self.ecdsa.into()) diff --git a/wallet/core/src/account/variants/keypair.rs b/wallet/core/src/account/variants/keypair.rs index 6381ca046b..d01a6c637d 100644 --- a/wallet/core/src/account/variants/keypair.rs +++ b/wallet/core/src/account/variants/keypair.rs @@ -70,14 +70,11 @@ impl BorshSerialize for Payload { impl BorshDeserialize for Payload { fn deserialize_reader(reader: &mut R) -> IoResult { - use secp256k1::constants::PUBLIC_KEY_SIZE; - let StorageHeader { version: _, .. } = StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; - let mut public_key_bytes: [u8; PUBLIC_KEY_SIZE] = [0; PUBLIC_KEY_SIZE]; - reader.read_exact(&mut public_key_bytes)?; - let public_key = secp256k1::PublicKey::from_slice(&public_key_bytes) + let public_key_bytes: Vec = BorshDeserialize::deserialize_reader(reader)?; + let public_key = secp256k1::PublicKey::from_slice(public_key_bytes.as_slice()) .map_err(|_| IoError::new(IoErrorKind::Other, "Unable to deserialize keypair account (invalid public key)"))?; let ecdsa = BorshDeserialize::deserialize_reader(reader)?; @@ -175,6 +172,8 @@ impl Account for Keypair { } fn descriptor(&self) -> Result { + let addresses = self.receive_address().ok().map(|address| vec![address]); + let descriptor = AccountDescriptor::new( KEYPAIR_ACCOUNT_KIND.into(), *self.id(), @@ -183,9 +182,28 @@ impl Account for Keypair { self.prv_key_data_id.into(), self.receive_address().ok(), self.change_address().ok(), + addresses, ) .with_property(AccountDescriptorProperty::Ecdsa, self.ecdsa.into()); Ok(descriptor) } + + fn create_address_private_keys<'l>( + self: Arc, + key_data: &PrvKeyData, + payment_secret: &Option, + addresses: &[&'l Address], + ) -> Result> { + let private_key = + key_data.as_secret_key(payment_secret.as_ref())?.ok_or(Error::Custom("Unable to derive private key".to_string()))?; + let mut private_keys = vec![]; + let wallet_address = self.receive_address()?; + for address in addresses.iter() { + if **address == wallet_address { + private_keys.push((*address, private_key)); + } + } + Ok(private_keys) + } } diff --git a/wallet/core/src/account/variants/legacy.rs b/wallet/core/src/account/variants/legacy.rs index cf05acf681..d74c8d892a 100644 --- a/wallet/core/src/account/variants/legacy.rs +++ b/wallet/core/src/account/variants/legacy.rs @@ -167,6 +167,24 @@ impl Account for Legacy { self.derivation.change_address_manager().current_address() } + // default account address (receive[0]) + fn default_address(&self) -> Result
{ + // TODO @surinder + let addresses = self.derivation.receive_address_manager().get_range_with_args(0..1, false)?; + addresses.first().cloned().ok_or(Error::AddressNotFound) + } + + // all addresses in the account (receive + change up to and including the last used index) + fn account_addresses(&self) -> Result> { + let meta = self.derivation.address_derivation_meta(); + let receive = meta.receive(); + let change = meta.change(); + let mut addresses = self.derivation.receive_address_manager().get_range_with_args(0..receive, false)?; + let change_addresses = self.derivation.change_address_manager().get_range_with_args(0..change, false)?; + addresses.extend(change_addresses); + Ok(addresses) + } + fn to_storage(&self) -> Result { let settings = self.context().settings.clone(); let storable = Payload; @@ -196,6 +214,7 @@ impl Account for Legacy { self.prv_key_data_id.into(), self.receive_address().ok(), self.change_address().ok(), + self.account_addresses().ok(), ) .with_property(AccountDescriptorProperty::DerivationMeta, self.derivation.address_derivation_meta().into()); diff --git a/wallet/core/src/account/variants/multisig.rs b/wallet/core/src/account/variants/multisig.rs index 1128f5eef3..9a2de8f0c4 100644 --- a/wallet/core/src/account/variants/multisig.rs +++ b/wallet/core/src/account/variants/multisig.rs @@ -236,6 +236,7 @@ impl Account for MultiSig { self.prv_key_data_ids.clone().try_into()?, self.receive_address().ok(), self.change_address().ok(), + None, ) .with_property(AccountDescriptorProperty::XpubKeys, self.xpub_keys.clone().into()) .with_property(AccountDescriptorProperty::Ecdsa, self.ecdsa.into()) diff --git a/wallet/core/src/account/variants/resident.rs b/wallet/core/src/account/variants/resident.rs index c7e56d3d6c..800554e1f2 100644 --- a/wallet/core/src/account/variants/resident.rs +++ b/wallet/core/src/account/variants/resident.rs @@ -81,6 +81,7 @@ impl Account for Resident { AssocPrvKeyDataIds::None, self.receive_address().ok(), self.change_address().ok(), + None, ); Ok(descriptor) diff --git a/wallet/core/src/account/variants/watchonly.rs b/wallet/core/src/account/variants/watchonly.rs index 7212ffdfce..318ab5c7a0 100644 --- a/wallet/core/src/account/variants/watchonly.rs +++ b/wallet/core/src/account/variants/watchonly.rs @@ -253,6 +253,7 @@ impl Account for WatchOnly { AssocPrvKeyDataIds::None, self.receive_address().ok(), self.change_address().ok(), + None, ) .with_property(AccountDescriptorProperty::XpubKeys, self.xpub_keys.clone().into()) .with_property(AccountDescriptorProperty::Ecdsa, self.ecdsa.into()) diff --git a/wallet/core/src/api/message.rs b/wallet/core/src/api/message.rs index e27cb2b29c..95ddfcbdc3 100644 --- a/wallet/core/src/api/message.rs +++ b/wallet/core/src/api/message.rs @@ -8,6 +8,8 @@ use crate::imports::*; use crate::tx::{Fees, GeneratorSummary, PaymentDestination}; use kaspa_addresses::Address; +use kaspa_consensus_client::{TransactionOutpoint, UtxoEntry}; +use kaspa_rpc_core::RpcFeerateBucket; #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] @@ -401,11 +403,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 +500,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>, } @@ -504,6 +512,154 @@ pub struct AccountsSendResponse { pub transaction_ids: Vec, } +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsPskbSignRequest { + pub account_id: AccountId, + pub pskb: String, + pub wallet_secret: Secret, + pub payment_secret: Option, + pub sign_for_address: Option
, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsPskbSignResponse { + pub pskb: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsPskbBroadcastRequest { + pub account_id: AccountId, + pub pskb: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PskbBroadcastRequest { + pub pskb: String, + pub network_id: NetworkId, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsPskbBroadcastResponse { + pub transaction_ids: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PskbBroadcastResponse { + pub transaction_ids: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsPskbSendRequest { + pub account_id: AccountId, + pub pskb: String, + pub wallet_secret: Secret, + pub payment_secret: Option, + pub sign_for_address: Option
, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsPskbSendResponse { + pub transaction_ids: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsGetUtxosRequest { + pub account_id: AccountId, + pub addresses: Option>, + pub min_amount_sompi: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsGetUtxosResponse { + pub utxos: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct UtxoEntryWrapper { + pub address: Option
, + pub outpoint: TransactionOutpointWrapper, + pub amount: u64, + pub script_public_key: ScriptPublicKey, + pub block_daa_score: u64, + pub is_coinbase: bool, +} +impl UtxoEntryWrapper { + pub fn to_js_object(&self) -> Result { + let obj = js_sys::Object::new(); + if let Some(address) = &self.address { + obj.set("address", &address.to_string().into())?; + } + + let outpoint = js_sys::Object::new(); + outpoint.set("transactionId", &self.outpoint.transaction_id.to_string().into())?; + outpoint.set("index", &self.outpoint.index.into())?; + + obj.set("amount", &self.amount.to_string().into())?; + obj.set("outpoint", &outpoint.into())?; + obj.set("scriptPublicKey", &workflow_wasm::serde::to_value(&self.script_public_key)?)?; + obj.set("blockDaaScore", &self.block_daa_score.to_string().into())?; + obj.set("isCoinbase", &self.is_coinbase.into())?; + + Ok(obj) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionOutpointWrapper { + pub transaction_id: TransactionId, + pub index: TransactionIndexType, +} + +impl From for TransactionOutpointWrapper { + fn from(outpoint: TransactionOutpoint) -> Self { + Self { transaction_id: outpoint.transaction_id(), index: outpoint.index() } + } +} + +impl From for TransactionOutpoint { + fn from(outpoint: TransactionOutpointWrapper) -> Self { + Self::new(outpoint.transaction_id, outpoint.index) + } +} + +impl From for UtxoEntry { + fn from(entry: UtxoEntryWrapper) -> Self { + Self { + address: entry.address, + outpoint: entry.outpoint.into(), + amount: entry.amount, + script_public_key: entry.script_public_key, + block_daa_score: entry.block_daa_score, + is_coinbase: entry.is_coinbase, + } + } +} + +impl From for UtxoEntryWrapper { + fn from(entry: UtxoEntry) -> Self { + Self { + address: entry.address, + outpoint: entry.outpoint.into(), + amount: entry.amount, + script_public_key: entry.script_public_key, + block_daa_score: entry.block_daa_score, + is_coinbase: entry.is_coinbase, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct AccountsTransferRequest { @@ -512,6 +668,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 +687,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>, } @@ -540,6 +698,55 @@ pub struct AccountsEstimateResponse { pub generator_summary: GeneratorSummary, } +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeRateEstimateBucket { + feerate: f64, + seconds: f64, +} + +impl From for FeeRateEstimateBucket { + fn from(bucket: RpcFeerateBucket) -> Self { + Self { feerate: bucket.feerate, seconds: bucket.estimated_seconds } + } +} + +impl From<&RpcFeerateBucket> for FeeRateEstimateBucket { + fn from(bucket: &RpcFeerateBucket) -> Self { + Self { feerate: bucket.feerate, seconds: bucket.estimated_seconds } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeRateEstimateRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeRateEstimateResponse { + pub priority: FeeRateEstimateBucket, + pub normal: FeeRateEstimateBucket, + pub low: FeeRateEstimateBucket, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeRatePollerEnableRequest { + pub interval_seconds: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeRatePollerEnableResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeRatePollerDisableRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeRatePollerDisableResponse {} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionsDataGetRequest { @@ -610,3 +817,69 @@ pub struct AddressBookEnumerateResponse {} #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct WalletNotification {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsCommitRevealManualRequest { + pub account_id: AccountId, + pub script_sig: Vec, + pub start_destination: PaymentDestination, + pub end_destination: PaymentDestination, + pub wallet_secret: Secret, + pub payment_secret: Option, + pub fee_rate: Option, + pub reveal_fee_sompi: u64, + pub payload: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsCommitRevealManualResponse { + pub transaction_ids: Vec, +} + +/// Specifies the type of an account address to be used in +/// commit reveal redeem script and also to spend reveal +/// operation to. +/// +/// @category Wallet API +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, CastFromJs)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "wasm32-sdk", wasm_bindgen)] +pub enum CommitRevealAddressKind { + Receive, + Change, +} + +impl FromStr for CommitRevealAddressKind { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "receive" => Ok(CommitRevealAddressKind::Receive), + "change" => Ok(CommitRevealAddressKind::Change), + _ => Err(Error::custom(format!("Invalid address kind: {s}"))), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsCommitRevealRequest { + pub account_id: AccountId, + pub address_type: CommitRevealAddressKind, + pub address_index: u32, + pub script_sig: Vec, + pub wallet_secret: Secret, + pub commit_amount_sompi: u64, + pub payment_secret: Option, + pub fee_rate: Option, + pub reveal_fee_sompi: u64, + pub payload: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsCommitRevealResponse { + pub transaction_ids: Vec, +} diff --git a/wallet/core/src/api/traits.rs b/wallet/core/src/api/traits.rs index 357665e77b..3ab3ebdb1f 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. @@ -407,6 +415,49 @@ pub trait WalletApi: Send + Sync + AnySync { /// well `transaction_ids` containing a list of submitted transaction ids. async fn accounts_send_call(self: Arc, request: AccountsSendRequest) -> Result; + /// Wrapper around [`accounts_pskb_sign()`](Self::accounts_pskb_sign_call) + async fn accounts_pskb_sign(self: Arc, request: AccountsPskbSignRequest) -> Result { + self.accounts_pskb_sign_call(request).await + } + + /// Sign a PSKB. + async fn accounts_pskb_sign_call(self: Arc, request: AccountsPskbSignRequest) -> Result; + + /// Wrapper around [`accounts_pskb_broadcast()`](Self::accounts_pskb_broadcast_call) + async fn accounts_pskb_broadcast(self: Arc, request: AccountsPskbBroadcastRequest) -> Result { + self.accounts_pskb_broadcast_call(request).await + } + + /// Wrapper around [`pskb_broadcast_call()`](Self::pskb_broadcast_call) + async fn pskb_broadcast(self: Arc, request: PskbBroadcastRequest) -> Result { + self.pskb_broadcast_call(request).await + } + + /// Broadcast a PSKB. + async fn pskb_broadcast_call(self: Arc, request: PskbBroadcastRequest) -> Result; + + /// Broadcast a PSKB. + async fn accounts_pskb_broadcast_call( + self: Arc, + request: AccountsPskbBroadcastRequest, + ) -> Result; + + /// Wrapper around [`accounts_pskb_send_call()`](Self::accounts_pskb_send_call) + async fn accounts_pskb_send(self: Arc, request: AccountsPskbSendRequest) -> Result { + self.accounts_pskb_send_call(request).await + } + + /// Sign and broadcast a PSKB. + async fn accounts_pskb_send_call(self: Arc, request: AccountsPskbSendRequest) -> Result; + + /// Wrapper around [`accounts_get_utxos_call()`](Self::accounts_get_utxos_call) + async fn accounts_get_utxos(self: Arc, request: AccountsGetUtxosRequest) -> Result { + self.accounts_get_utxos_call(request).await + } + + /// Get UTXOs for an account. + async fn accounts_get_utxos_call(self: Arc, request: AccountsGetUtxosRequest) -> Result; + /// Transfer funds to another account. Returns an [`AccountsTransferResponse`] /// struct that contains a [`GeneratorSummary`] as well `transaction_ids` /// containing a list of submitted transaction ids. Unlike funds sent to an @@ -414,6 +465,26 @@ pub trait WalletApi: Send + Sync + AnySync { /// available immediately upon transaction acceptance. async fn accounts_transfer_call(self: Arc, request: AccountsTransferRequest) -> Result; + /// Commit-reveal funds using provided [`PaymentDestination`] in + /// [`AccountsCommitRevealManualRequest`]. + /// Returns an [`AccountsCommitRevealManualResponse`] struct that + /// contains transaction ids. + async fn accounts_commit_reveal_manual_call( + self: Arc, + request: AccountsCommitRevealManualRequest, + ) -> Result; + + /// Commit-reveal funds to P2SH of given script signature present in + /// [`AccountsCommitRevealRequest`] that provides a pubkey placeholder + /// used by given address type and address index looked up in derivation + /// manager. + /// Returns an [`AccountsCommitRevealResponse`] struct that contains + /// transaction ids. + async fn accounts_commit_reveal_call( + self: Arc, + request: AccountsCommitRevealRequest, + ) -> Result; + /// Performs a transaction estimate, returning [`AccountsEstimateResponse`] /// that contains [`GeneratorSummary`]. This call will estimate the total /// amount of fees that will be required by the transaction as well as @@ -423,6 +494,39 @@ pub trait WalletApi: Send + Sync + AnySync { /// an error. async fn accounts_estimate_call(self: Arc, request: AccountsEstimateRequest) -> Result; + /// Wrapper around [`fee_rate_estimate_call()`](Self::fee_rate_estimate_call) + async fn fee_rate_estimate(self: Arc) -> Result { + Ok(self.fee_rate_estimate_call(FeeRateEstimateRequest {}).await?) + } + + /// Estimate current network fee rate. Returns a [`FeeRateEstimateResponse`] + async fn fee_rate_estimate_call(self: Arc, request: FeeRateEstimateRequest) -> Result; + + /// Wrapper around [`fee_rate_poller_enable_call()`](Self::fee_rate_poller_enable_call). + async fn fee_rate_poller_enable(self: Arc, interval_seconds: u64) -> Result<()> { + self.fee_rate_poller_enable_call(FeeRatePollerEnableRequest { interval_seconds }).await?; + Ok(()) + } + + /// Enable the fee rate poller. The fee rate poller is a background task that + /// periodically polls the network for the current fee rate. The fee rate is + /// used to estimate the transaction fee. The poller is disabled by default. + /// This function stops the previously enabled poller and starts a new one + /// with the specified `interval`. + async fn fee_rate_poller_enable_call(self: Arc, request: FeeRatePollerEnableRequest) -> Result; + + /// Wrapper around [`fee_rate_poller_disable_call()`](Self::fee_rate_poller_disable_call). + async fn fee_rate_poller_disable(self: Arc) -> Result<()> { + self.fee_rate_poller_disable_call(FeeRatePollerDisableRequest {}).await?; + Ok(()) + } + + /// Disable the fee rate poller. + async fn fee_rate_poller_disable_call( + self: Arc, + request: FeeRatePollerDisableRequest, + ) -> Result; + /// Get a range of transaction records for a specific account id. /// Wrapper around [`transactions_data_get_call()`](Self::transactions_data_get_call). async fn transactions_data_get_range( diff --git a/wallet/core/src/api/transport.rs b/wallet/core/src/api/transport.rs index c9e5f6de63..398420f777 100644 --- a/wallet/core/src/api/transport.rs +++ b/wallet/core/src/api/transport.rs @@ -99,12 +99,22 @@ impl WalletApi for WalletClient { AccountsGet, AccountsCreateNewAddress, AccountsSend, + AccountsPskbSign, + AccountsPskbBroadcast, + PskbBroadcast, + AccountsPskbSend, + AccountsGetUtxos, AccountsTransfer, AccountsEstimate, TransactionsDataGet, TransactionsReplaceNote, TransactionsReplaceMetadata, AddressBookEnumerate, + FeeRateEstimate, + FeeRatePollerEnable, + FeeRatePollerDisable, + AccountsCommitReveal, + AccountsCommitRevealManual, ]} } @@ -176,12 +186,22 @@ impl WalletServer { AccountsGet, AccountsCreateNewAddress, AccountsSend, + AccountsPskbSign, + AccountsPskbBroadcast, + PskbBroadcast, + AccountsPskbSend, + AccountsGetUtxos, AccountsTransfer, AccountsEstimate, TransactionsDataGet, TransactionsReplaceNote, TransactionsReplaceMetadata, AddressBookEnumerate, + FeeRateEstimate, + FeeRatePollerEnable, + FeeRatePollerDisable, + AccountsCommitReveal, + AccountsCommitRevealManual, ]} } diff --git a/wallet/core/src/derivation.rs b/wallet/core/src/derivation.rs index 2e598334e8..660f54d25d 100644 --- a/wallet/core/src/derivation.rs +++ b/wallet/core/src/derivation.rs @@ -89,6 +89,19 @@ impl AddressManager { self.current_address() } + pub fn default_address(&self) -> Result
{ + // TODO @surinder + todo!() + // let list = self.pubkey_managers.iter().map(|m| m.current_pubkey()); + + // let keys = list.into_iter().collect::>>()?; + // let address = self.create_address(keys)?; + + // self.update_address_to_index_map(self.index(), &[address.clone()])?; + + // Ok(address) + } + pub fn current_address(&self) -> Result
{ let list = self.pubkey_managers.iter().map(|m| m.current_pubkey()); diff --git a/wallet/core/src/error.rs b/wallet/core/src/error.rs index 8992a8a924..d0c6c1d0a4 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), @@ -341,6 +344,36 @@ pub enum Error { #[error("Error generating pending transaction from PSKT: {0}")] PendingTransactionFromPSKTError(String), + + #[error("Address not found")] + AddressNotFound, + + #[error("Invalid payment destination: expected PaymentOutputs, found Change")] + CommitRevealInvalidPaymentDestination, + + #[error("No payment outputs found in destination")] + CommitRevealEmptyPaymentOutputs, + + #[error("Failed to generate redeem script")] + RevealRedeemScriptTemplateError, + + #[error("Failed to generate PSKT: {0}")] + PSKTGenerationError(String), + + #[error("Failed to sign commit transaction")] + CommitTransactionSigningError, + + #[error("Failed to finalize PSKT")] + PSKTFinalizationError, + + #[error("Failed to extract transaction ID from PSKT")] + CommitTransactionIdExtractionError, + + #[error("No valid reveal address found for signing")] + NoQualifiedRevealSignerFound, + + #[error("Failed to merge bundles")] + CommitRevealBundleMergeError, } impl From for Error { diff --git a/wallet/core/src/events.rs b/wallet/core/src/events.rs index 37816d8b20..7da489da38 100644 --- a/wallet/core/src/events.rs +++ b/wallet/core/src/events.rs @@ -4,6 +4,7 @@ //! produced by the client RPC and the Kaspa node monitoring subsystems. //! +use crate::api::message::FeeRateEstimateBucket; use crate::imports::*; use crate::storage::{Hint, PrvKeyDataInfo, StorageDescriptor, TransactionRecord, WalletDescriptor}; use crate::utxo::context::UtxoContextId; @@ -55,16 +56,16 @@ impl SyncState { pub enum Events { WalletPing, /// Successful RPC connection + #[serde(rename_all = "camelCase")] Connect { - #[serde(rename = "networkId")] network_id: NetworkId, /// Node RPC url on which connection /// has been established url: Option, }, /// RPC disconnection + #[serde(rename_all = "camelCase")] Disconnect { - #[serde(rename = "networkId")] network_id: NetworkId, url: Option, }, @@ -77,25 +78,33 @@ pub enum Events { }, /// [`SyncState`] notification posted /// when the node sync state changes + #[serde(rename_all = "camelCase")] SyncState { - #[serde(rename = "syncState")] sync_state: SyncState, }, + /// Emitted on wallet enumerate response + #[serde(rename_all = "camelCase")] + WalletList { + wallet_descriptors: Vec, + }, /// Emitted after the wallet has loaded and /// contains anti-phishing 'hint' set by the user. WalletHint { hint: Option, }, /// Wallet has opened + #[serde(rename_all = "camelCase")] WalletOpen { wallet_descriptor: Option, account_descriptors: Option>, }, + #[serde(rename_all = "camelCase")] WalletCreate { wallet_descriptor: WalletDescriptor, storage_descriptor: StorageDescriptor, }, /// Wallet reload initiated (development only) + #[serde(rename_all = "camelCase")] WalletReload { wallet_descriptor: Option, account_descriptors: Option>, @@ -106,8 +115,8 @@ pub enum Events { }, /// Wallet has been closed WalletClose, + #[serde(rename_all = "camelCase")] PrvKeyDataCreate { - #[serde(rename = "prvKeyDataInfo")] prv_key_data_info: PrvKeyDataInfo, }, /// Accounts have been activated @@ -123,22 +132,22 @@ pub enum Events { id: Option, }, /// Account has been created + #[serde(rename_all = "camelCase")] AccountCreate { account_descriptor: AccountDescriptor, }, /// Account has been changed /// (emitted on new address generation) + #[serde(rename_all = "camelCase")] AccountUpdate { account_descriptor: AccountDescriptor, }, /// Emitted after successful RPC connection /// after the initial state negotiation. + #[serde(rename_all = "camelCase")] ServerStatus { - #[serde(rename = "networkId")] network_id: NetworkId, - #[serde(rename = "serverVersion")] server_version: String, - #[serde(rename = "isSynced")] is_synced: bool, /// Node RPC url on which connection /// has been established @@ -160,8 +169,8 @@ pub enum Events { message: String, }, /// DAA score change + #[serde(rename_all = "camelCase")] DaaScoreChange { - #[serde(rename = "currentDaaScore")] current_daa_score: u64, }, /// New incoming pending UTXO/transaction @@ -214,13 +223,18 @@ pub enum Events { id: UtxoContextId, }, /// Periodic metrics updates (on-request) + #[serde(rename_all = "camelCase")] Metrics { - #[serde(rename = "networkId")] network_id: NetworkId, // #[serde(rename = "metricsData")] // metrics_data: MetricsData, metrics: MetricsUpdate, }, + FeeRate { + priority: FeeRateEstimateBucket, + normal: FeeRateEstimateBucket, + low: FeeRateEstimateBucket, + }, /// A general wallet framework error, emitted when an unexpected /// error occurs within the wallet framework. Error { @@ -259,6 +273,7 @@ pub enum EventKind { Disconnect, UtxoIndexNotEnabled, SyncState, + WalletList, WalletStart, WalletHint, WalletOpen, @@ -284,6 +299,7 @@ pub enum EventKind { Discovery, Balance, Metrics, + FeeRate, Error, } @@ -296,6 +312,7 @@ impl From<&Events> for EventKind { Events::Disconnect { .. } => EventKind::Disconnect, Events::UtxoIndexNotEnabled { .. } => EventKind::UtxoIndexNotEnabled, Events::SyncState { .. } => EventKind::SyncState, + Events::WalletList { .. } => EventKind::WalletList, Events::WalletHint { .. } => EventKind::WalletHint, Events::WalletOpen { .. } => EventKind::WalletOpen, Events::WalletCreate { .. } => EventKind::WalletCreate, @@ -320,6 +337,7 @@ impl From<&Events> for EventKind { Events::Discovery { .. } => EventKind::Discovery, Events::Balance { .. } => EventKind::Balance, Events::Metrics { .. } => EventKind::Metrics, + Events::FeeRate { .. } => EventKind::FeeRate, Events::Error { .. } => EventKind::Error, } } @@ -334,6 +352,7 @@ impl FromStr for EventKind { "disconnect" => Ok(EventKind::Disconnect), "utxo-index-not-enabled" => Ok(EventKind::UtxoIndexNotEnabled), "sync-state" => Ok(EventKind::SyncState), + "wallet-list" => Ok(EventKind::WalletList), "wallet-start" => Ok(EventKind::WalletStart), "wallet-hint" => Ok(EventKind::WalletHint), "wallet-open" => Ok(EventKind::WalletOpen), @@ -359,6 +378,7 @@ impl FromStr for EventKind { "discovery" => Ok(EventKind::Discovery), "balance" => Ok(EventKind::Balance), "metrics" => Ok(EventKind::Metrics), + "fee-rate" => Ok(EventKind::FeeRate), "error" => Ok(EventKind::Error), _ => Err(Error::custom("Invalid event kind")), } @@ -382,6 +402,7 @@ impl std::fmt::Display for EventKind { EventKind::Disconnect => "disconnect", EventKind::UtxoIndexNotEnabled => "utxo-index-not-enabled", EventKind::SyncState => "sync-state", + EventKind::WalletList => "wallet-list", EventKind::WalletHint => "wallet-hint", EventKind::WalletOpen => "wallet-open", EventKind::WalletCreate => "wallet-create", @@ -406,6 +427,7 @@ impl std::fmt::Display for EventKind { EventKind::Discovery => "discovery", EventKind::Balance => "balance", EventKind::Metrics => "metrics", + EventKind::FeeRate => "fee-rate", EventKind::Error => "error", }; diff --git a/wallet/core/src/storage/binding.rs b/wallet/core/src/storage/binding.rs index 18f988e2d2..b3a2df3951 100644 --- a/wallet/core/src/storage/binding.rs +++ b/wallet/core/src/storage/binding.rs @@ -33,7 +33,7 @@ export enum BindingType { */ export interface IBinding { type : BindingType; - data : HexString; + id : HexString; } "#; diff --git a/wallet/core/src/storage/keydata/data.rs b/wallet/core/src/storage/keydata/data.rs index 5b480d937c..ae3dfb1768 100644 --- a/wallet/core/src/storage/keydata/data.rs +++ b/wallet/core/src/storage/keydata/data.rs @@ -223,12 +223,40 @@ impl PrvKeyData { Ok(payload.as_variant()) } - pub fn try_from_mnemonic(mnemonic: Mnemonic, payment_secret: Option<&Secret>, encryption_kind: EncryptionKind) -> Result { + pub fn try_from_mnemonic( + mnemonic: Mnemonic, + payment_secret: Option<&Secret>, + encryption_kind: EncryptionKind, + name: Option, + ) -> Result { let key_data_payload = PrvKeyDataPayload::try_new_with_mnemonic(mnemonic)?; let key_data_payload_id = key_data_payload.id(); let key_data_payload = Encryptable::Plain(key_data_payload); - let mut prv_key_data = PrvKeyData::new(key_data_payload_id, None, key_data_payload); + let mut prv_key_data = PrvKeyData::new(key_data_payload_id, name, key_data_payload); + if let Some(payment_secret) = payment_secret { + prv_key_data.encrypt(payment_secret, encryption_kind)?; + } + + Ok(prv_key_data) + } + + pub fn as_secret_key(&self, payment_secret: Option<&Secret>) -> Result> { + let payload = self.payload.decrypt(payment_secret)?; + payload.as_secret_key() + } + + pub fn try_from_secret_key( + secret_key: SecretKey, + payment_secret: Option<&Secret>, + encryption_kind: EncryptionKind, + name: Option, + ) -> Result { + let key_data_payload = PrvKeyDataPayload::try_new_with_secret_key(secret_key)?; + let key_data_payload_id = key_data_payload.id(); + let key_data_payload = Encryptable::Plain(key_data_payload); + + let mut prv_key_data = PrvKeyData::new(key_data_payload_id, name, key_data_payload); if let Some(payment_secret) = payment_secret { prv_key_data.encrypt(payment_secret, encryption_kind)?; } diff --git a/wallet/core/src/storage/transaction/record.rs b/wallet/core/src/storage/transaction/record.rs index 05be3b69f2..05deb09b74 100644 --- a/wallet/core/src/storage/transaction/record.rs +++ b/wallet/core/src/storage/transaction/record.rs @@ -341,6 +341,17 @@ pub struct TransactionRecord { pub metadata: Option, } +#[wasm_bindgen] +impl TransactionRecord { + #[wasm_bindgen(js_name = maturityProgress)] + #[allow(non_snake_case)] + pub fn maturity_progress_js(&self, currentDaaScore: BigInt) -> String { + self.maturity_progress(currentDaaScore.try_as_u64().unwrap_or_default()) + .map(|progress| format!("{}", (progress * 100.) as usize)) + .unwrap_or_default() + } +} + impl TransactionRecord { const STORAGE_MAGIC: u32 = 0x5854414b; const STORAGE_VERSION: u32 = 0; diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 67b4759839..80fa7057bb 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 8b4beddf2c..9a3bbdd489 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 34fd1bb6ef..a1fcf2acfa 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/signer.rs b/wallet/core/src/tx/generator/signer.rs index e5d745bbdb..e24ef653d5 100644 --- a/wallet/core/src/tx/generator/signer.rs +++ b/wallet/core/src/tx/generator/signer.rs @@ -31,9 +31,14 @@ impl Signer { // skip address that are already present in the key map let addresses = addresses.iter().filter(|a| !keys.contains_key(a)).collect::>(); if !addresses.is_empty() { - let account = self.inner.account.clone().as_derivation_capable().expect("expecting derivation capable account"); - let (receive, change) = account.derivation().addresses_indexes(&addresses)?; - let private_keys = account.create_private_keys(&self.inner.keydata, &self.inner.payment_secret, &receive, &change)?; + // let account = self.inner.account.clone().as_derivation_capable().expect("expecting derivation capable account"); + // let (receive, change) = account.derivation().addresses_indexes(&addresses)?; + // let private_keys = account.create_private_keys(&self.inner.keydata, &self.inner.payment_secret, &receive, &change)?; + let private_keys = self.inner.account.clone().create_address_private_keys( + &self.inner.keydata, + &self.inner.payment_secret, + addresses.as_slice(), + )?; for (address, private_key) in private_keys { keys.insert(address.clone(), private_key.to_bytes()); } diff --git a/wallet/core/src/tx/generator/summary.rs b/wallet/core/src/tx/generator/summary.rs index 76ed6d964f..2ce309410d 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 e1db97c446..1818fe414f 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/context.rs b/wallet/core/src/utxo/context.rs index 39575a64f6..14e2980df2 100644 --- a/wallet/core/src/utxo/context.rs +++ b/wallet/core/src/utxo/context.rs @@ -14,6 +14,7 @@ use crate::utxo::{ Maturity, NetworkParams, OutgoingTransaction, PendingUtxoEntryReference, UtxoContextBinding, UtxoEntryId, UtxoEntryReference, UtxoEntryReferenceExtension, UtxoProcessor, }; +use kaspa_consensus_client::UtxoEntry; use kaspa_hashes::Hash; use sorted_insert::SortedInsertBinaryByKey; @@ -702,6 +703,57 @@ impl UtxoContext { self.update_balance().await?; Ok(()) } + + pub async fn get_utxos(&self, addresses: Option>, min_amount_sompi: Option) -> Result> { + let utxos = &self.context().mature; + let mut amount = 0; + if let Some(addresses) = &addresses { + if let Some(min_amount_sompi) = min_amount_sompi { + let mut amount = 0; + let filtered_utxos = utxos + .iter() + .filter_map(|utxo| { + if let Some(address) = utxo.address() { + if addresses.contains(&address) && amount < min_amount_sompi { + amount += utxo.amount(); + return Some(utxo.entry().clone()); + } + } + + None + }) + .collect(); + return Ok(filtered_utxos); + } else { + let filtered_utxos = utxos + .iter() + .filter_map(|utxo| { + if let Some(address) = utxo.address() { + if addresses.contains(&address) { + return Some(utxo.entry().clone()); + } + } + None + }) + .collect(); + return Ok(filtered_utxos); + } + } + if let Some(min_amount_sompi) = min_amount_sompi { + let filtered_utxos = utxos + .iter() + .filter_map(|utxo| { + if amount < min_amount_sompi { + amount += utxo.amount(); + return Some(utxo.entry().clone()); + } + None + }) + .collect(); + return Ok(filtered_utxos); + } + Ok(utxos.iter().map(|utxo| utxo.entry().clone()).collect()) + } } impl Eq for UtxoContext {} diff --git a/wallet/core/src/utxo/processor.rs b/wallet/core/src/utxo/processor.rs index f6480f333e..3c70479173 100644 --- a/wallet/core/src/utxo/processor.rs +++ b/wallet/core/src/utxo/processor.rs @@ -17,7 +17,7 @@ use kaspa_rpc_core::{ ops::{RPC_API_REVISION, RPC_API_VERSION}, }, message::UtxosChangedNotification, - GetServerInfoResponse, + GetServerInfoResponse, RpcFeeEstimate, }; use kaspa_wrpc_client::KaspaRpcClient; use workflow_core::channel::{Channel, DuplexChannel, Sender}; @@ -61,6 +61,8 @@ pub struct Inner { metrics: Arc, metrics_kinds: Mutex>, connection_signaler: Mutex>>>, + fee_rate_task_ctl: DuplexChannel, + fee_rate_task_is_running: AtomicBool, } impl Inner { @@ -91,6 +93,8 @@ impl Inner { metrics: Arc::new(Metrics::default()), metrics_kinds: Mutex::new(vec![]), connection_signaler: Mutex::new(None), + fee_rate_task_ctl: DuplexChannel::oneshot(), + fee_rate_task_is_running: AtomicBool::new(false), } } } @@ -728,6 +732,48 @@ impl UtxoProcessor { pub fn enable_metrics_kinds(&self, metrics_kinds: &[MetricsUpdateKind]) { *self.inner.metrics_kinds.lock().unwrap() = metrics_kinds.to_vec(); } + + pub async fn start_fee_rate_poller(&self, poller_interval: Duration) -> Result<()> { + self.stop_fee_rate_poller().await.ok(); + + let this = self.clone(); + this.inner.fee_rate_task_is_running.store(true, Ordering::SeqCst); + let fee_rate_task_ctl_receiver = self.inner.fee_rate_task_ctl.request.receiver.clone(); + let fee_rate_task_ctl_sender = self.inner.fee_rate_task_ctl.response.sender.clone(); + + let mut interval = workflow_core::task::interval(poller_interval); + + spawn(async move { + loop { + select_biased! { + _ = interval.next().fuse() => { + if let Ok(fee_rate) = this.rpc_api().get_fee_estimate().await { + let RpcFeeEstimate { priority_bucket, normal_buckets, low_buckets } = fee_rate; + this.notify(Events::FeeRate { + priority : priority_bucket.into(), + normal : normal_buckets.first().expect("missing normal feerate bucket").into(), + low : low_buckets.first().expect("missing normal feerate bucket").into() + }).await.ok(); + } + }, + _ = fee_rate_task_ctl_receiver.recv().fuse() => { + break; + }, + } + } + + fee_rate_task_ctl_sender.send(()).await.unwrap(); + }); + + Ok(()) + } + + pub async fn stop_fee_rate_poller(&self) -> Result<()> { + if self.inner.fee_rate_task_is_running.load(Ordering::SeqCst) { + self.inner.fee_rate_task_ctl.signal(()).await.expect("UtxoProcessor::stop_task() `signal` error"); + } + Ok(()) + } } #[cfg(test)] diff --git a/wallet/core/src/utxo/test.rs b/wallet/core/src/utxo/test.rs index a1b41f9987..6932bc6518 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 93becef420..28a6ae673d 100644 --- a/wallet/core/src/wallet/api.rs +++ b/wallet/core/src/wallet/api.rs @@ -2,14 +2,18 @@ //! [`WalletApi`] trait implementation for the [`Wallet`] struct. //! +use crate::account::pskb::bundle_to_finalizer_stream; use crate::api::{message::*, traits::WalletApi}; +use crate::events::Events; use crate::imports::*; use crate::result::Result; use crate::storage::interface::TransactionRangeResult; use crate::storage::Binding; use crate::tx::Fees; -use workflow_core::channel::Receiver; +use kaspa_rpc_core::RpcFeeEstimate; +use kaspa_wallet_pskt::bundle::Bundle; +use workflow_core::channel::Receiver; #[async_trait] impl WalletApi for super::Wallet { async fn register_notifications(self: Arc, _channel: Receiver) -> Result { @@ -151,6 +155,7 @@ impl WalletApi for super::Wallet { async fn wallet_enumerate_call(self: Arc, _request: WalletEnumerateRequest) -> Result { let wallet_descriptors = self.store().wallet_list().await?; + self.notify(Events::WalletList { wallet_descriptors: wallet_descriptors.clone() }).await.ok(); Ok(WalletEnumerateResponse { wallet_descriptors }) } @@ -343,9 +348,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 +394,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,17 +403,95 @@ 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 }) } + async fn accounts_pskb_sign_call(self: Arc, request: AccountsPskbSignRequest) -> Result { + let AccountsPskbSignRequest { account_id, pskb, wallet_secret, payment_secret, sign_for_address } = request; + let pskb = Bundle::deserialize(&pskb)?; + let guard = self.guard(); + let guard = guard.lock().await; + + let account = self.get_account_by_id(&account_id, &guard).await?.ok_or(Error::AccountNotFound(account_id))?; + let pskb = account.pskb_sign(&pskb, wallet_secret, payment_secret, sign_for_address.as_ref()).await?; + + Ok(AccountsPskbSignResponse { pskb: pskb.serialize()? }) + } + + async fn accounts_pskb_broadcast_call( + self: Arc, + request: AccountsPskbBroadcastRequest, + ) -> Result { + let AccountsPskbBroadcastRequest { account_id, pskb } = request; + let pskb = Bundle::deserialize(&pskb)?; + let guard = self.guard(); + let guard = guard.lock().await; + + let account = self.get_account_by_id(&account_id, &guard).await?.ok_or(Error::AccountNotFound(account_id))?; + let transaction_ids = account.pskb_broadcast(&pskb).await?; + Ok(AccountsPskbBroadcastResponse { transaction_ids }) + } + + async fn pskb_broadcast_call(self: Arc, request: PskbBroadcastRequest) -> Result { + let PskbBroadcastRequest { pskb, network_id } = request; + let pskb = Bundle::deserialize(&pskb)?; + + let mut transaction_ids = Vec::new(); + let mut stream = bundle_to_finalizer_stream(&pskb); + let rpc = self.rpc_api(); + while let Some(result) = stream.next().await { + match result { + Ok(finalized_pskt) => { + let signed_tx = match finalized_pskt.extractor() { + Ok(extractor) => match extractor.extract_tx(&network_id.into()) { + Ok(tx) => tx.tx, + Err(e) => return Err(Error::PendingTransactionFromPSKTError(e.to_string())), + }, + Err(e) => return Err(Error::PendingTransactionFromPSKTError(e.to_string())), + }; + log_info!("Submitting to rpc"); + transaction_ids.push(rpc.submit_transaction((&signed_tx).into(), false).await?); + log_info!("Submitted to rpc"); + } + Err(e) => { + log_info!("Error processing a PSKT from bundle: {:?}", e); + } + } + } + + Ok(PskbBroadcastResponse { transaction_ids }) + } + + async fn accounts_get_utxos_call(self: Arc, request: AccountsGetUtxosRequest) -> Result { + let AccountsGetUtxosRequest { account_id, addresses, min_amount_sompi } = request; + let guard = self.guard(); + let guard = guard.lock().await; + let account = self.get_account_by_id(&account_id, &guard).await?.ok_or(Error::AccountNotFound(account_id))?; + let utxos = account.get_utxos(addresses, min_amount_sompi).await?; + Ok(AccountsGetUtxosResponse { utxos: utxos.into_iter().map(|entry| entry.into()).collect::>() }) + } + + async fn accounts_pskb_send_call(self: Arc, request: AccountsPskbSendRequest) -> Result { + let AccountsPskbSendRequest { account_id, pskb, wallet_secret, payment_secret, sign_for_address } = request; + let pskb = Bundle::deserialize(&pskb)?; + let guard = self.guard(); + let guard = guard.lock().await; + + let account = self.get_account_by_id(&account_id, &guard).await?.ok_or(Error::AccountNotFound(account_id))?; + let pskb = account.clone().pskb_sign(&pskb, wallet_secret, payment_secret, sign_for_address.as_ref()).await?; + let transaction_ids = account.pskb_broadcast(&pskb).await?; + Ok(AccountsPskbSendResponse { transaction_ids }) + } + async fn accounts_transfer_call(self: Arc, request: AccountsTransferRequest) -> Result { let AccountsTransferRequest { source_account_id, destination_account_id, wallet_secret, payment_secret, + fee_rate, priority_fee_sompi, transfer_amount_sompi, } = request; @@ -413,6 +507,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, @@ -425,8 +520,110 @@ impl WalletApi for super::Wallet { Ok(AccountsTransferResponse { generator_summary, transaction_ids }) } + async fn accounts_commit_reveal_manual_call( + self: Arc, + request: AccountsCommitRevealManualRequest, + ) -> Result { + let AccountsCommitRevealManualRequest { + account_id, + script_sig, + start_destination, + end_destination, + wallet_secret, + payment_secret, + fee_rate, + reveal_fee_sompi, + payload, + } = request; + + let guard = self.guard(); + let guard = guard.lock().await; + + let account = self.get_account_by_id(&account_id, &guard).await?.ok_or(Error::AccountNotFound(account_id))?; + + let abortable = Abortable::new(); + + let bundle = account + .clone() + .commit_reveal_manual( + start_destination, + end_destination, + script_sig, + wallet_secret, + payment_secret, + fee_rate, + reveal_fee_sompi, + payload, + &abortable, + ) + .await?; + + let transaction_ids = account.pskb_broadcast(&bundle).await?; + Ok(AccountsCommitRevealManualResponse { transaction_ids }) + } + + async fn accounts_commit_reveal_call( + self: Arc, + request: AccountsCommitRevealRequest, + ) -> Result { + let AccountsCommitRevealRequest { + account_id, + address_type, + address_index, + script_sig, + commit_amount_sompi, + wallet_secret, + payment_secret, + fee_rate, + reveal_fee_sompi, + payload, + } = request; + + let guard = self.guard(); + let guard = guard.lock().await; + + let account = self.get_account_by_id(&account_id, &guard).await?.ok_or(Error::AccountNotFound(account_id))?; + + let address = match address_type { + CommitRevealAddressKind::Receive => { + if account.account_kind() == KEYPAIR_ACCOUNT_KIND { + account.receive_address()? + } else { + account.clone().as_derivation_capable()?.receive_address_at_index(address_index).await? + } + } + CommitRevealAddressKind::Change => { + if account.account_kind() == KEYPAIR_ACCOUNT_KIND { + account.change_address()? + } else { + account.clone().as_derivation_capable()?.change_address_at_index(address_index).await? + } + } + }; + + let abortable = Abortable::new(); + + let bundle = account + .clone() + .commit_reveal( + address, + script_sig, + wallet_secret, + payment_secret, + commit_amount_sompi, + fee_rate, + reveal_fee_sompi, + payload, + &abortable, + ) + .await?; + + let transaction_ids = account.pskb_broadcast(&bundle).await?; + Ok(AccountsCommitRevealResponse { transaction_ids }) + } + 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 +642,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? }) @@ -500,4 +697,28 @@ impl WalletApi for super::Wallet { ) -> Result { return Err(Error::NotImplemented); } + + async fn fee_rate_estimate_call(self: Arc, _request: FeeRateEstimateRequest) -> Result { + let RpcFeeEstimate { priority_bucket, normal_buckets, low_buckets } = self.rpc_api().get_fee_estimate().await?; + + Ok(FeeRateEstimateResponse { + priority: priority_bucket.into(), + normal: normal_buckets.first().ok_or(Error::custom("missing normal feerate bucket"))?.into(), + low: low_buckets.first().ok_or(Error::custom("missing normal feerate bucket"))?.into(), + }) + } + + async fn fee_rate_poller_enable_call(self: Arc, request: FeeRatePollerEnableRequest) -> Result { + let FeeRatePollerEnableRequest { interval_seconds } = request; + self.utxo_processor().start_fee_rate_poller(Duration::from_secs(interval_seconds)).await?; + Ok(FeeRatePollerEnableResponse {}) + } + + async fn fee_rate_poller_disable_call( + self: Arc, + _request: FeeRatePollerDisableRequest, + ) -> Result { + self.utxo_processor().stop_fee_rate_poller().await?; + Ok(FeeRatePollerDisableResponse {}) + } } diff --git a/wallet/core/src/wallet/args.rs b/wallet/core/src/wallet/args.rs index f0168f7406..c2cd0e7d81 100644 --- a/wallet/core/src/wallet/args.rs +++ b/wallet/core/src/wallet/args.rs @@ -5,6 +5,7 @@ use crate::imports::*; use crate::storage::interface::CreateArgs; use crate::storage::{Hint, PrvKeyDataId}; +use crate::wallet::keydata::PrvKeyDataVariantKind; use borsh::{BorshDeserialize, BorshSerialize}; use zeroize::Zeroize; @@ -62,18 +63,20 @@ impl WalletOpenArgs { pub struct PrvKeyDataCreateArgs { pub name: Option, pub payment_secret: Option, - pub mnemonic: Secret, + pub secret: Secret, + pub kind: PrvKeyDataVariantKind, } impl PrvKeyDataCreateArgs { - pub fn new(name: Option, payment_secret: Option, mnemonic: Secret) -> Self { - Self { name, payment_secret, mnemonic } + pub fn new(name: Option, payment_secret: Option, secret: Secret, kind: PrvKeyDataVariantKind) -> Self { + Self { name, payment_secret, secret, kind } } } impl Zeroize for PrvKeyDataCreateArgs { fn zeroize(&mut self) { - self.mnemonic.zeroize(); + self.secret.zeroize(); + self.payment_secret.zeroize(); } } @@ -156,6 +159,11 @@ pub enum AccountCreateArgs { Bip32Watch { account_args: AccountCreateArgsBip32Watch, }, + Keypair { + prv_key_data_id: PrvKeyDataId, + account_name: Option, + ecdsa: bool, + }, } impl AccountCreateArgs { @@ -174,6 +182,10 @@ impl AccountCreateArgs { AccountCreateArgs::Legacy { prv_key_data_id, account_name } } + pub fn new_keypair_key(prv_key_data_id: PrvKeyDataId, account_name: Option, ecdsa: bool) -> Self { + AccountCreateArgs::Keypair { prv_key_data_id, account_name, ecdsa } + } + pub fn new_multisig( prv_key_data_args: Vec, additional_xpub_keys: Vec, diff --git a/wallet/core/src/wallet/mod.rs b/wallet/core/src/wallet/mod.rs index e9316a13b1..4899bfe956 100644 --- a/wallet/core/src/wallet/mod.rs +++ b/wallet/core/src/wallet/mod.rs @@ -24,6 +24,7 @@ use crate::settings::{SettingsStore, WalletSettings}; use crate::storage::interface::{OpenArgs, StorageDescriptor}; use crate::storage::local::interface::LocalStore; use crate::storage::local::Storage; +use crate::wallet::keydata::PrvKeyDataVariantKind; use crate::wallet::maps::ActiveAccountMap; use kaspa_bip32::{ExtendedKey, Language, Mnemonic, Prefix as KeyPrefix, WordCount}; use kaspa_notify::{ @@ -442,7 +443,7 @@ impl Wallet { .as_legacy_account()?; legacy_account.clone().start().await?; legacy_account.clear_private_context().await?; - } else { + } else if self.active_accounts().get(account_storage.id()).is_none() { let account = try_load_account(self, account_storage, meta).await?; account.clone().start().await?; } @@ -689,6 +690,9 @@ impl Wallet { self.create_account_multisig(wallet_secret, prv_key_data_args, additional_xpub_keys, name, minimum_signatures).await? } AccountCreateArgs::Bip32Watch { account_args } => self.create_account_bip32_watch(wallet_secret, account_args).await?, + AccountCreateArgs::Keypair { prv_key_data_id, account_name, ecdsa } => { + self.create_account_keypair(wallet_secret, None, prv_key_data_id, account_name, ecdsa).await? + } }; if notify { @@ -886,6 +890,45 @@ impl Wallet { Ok(account) } + pub async fn create_account_keypair( + self: &Arc, + wallet_secret: &Secret, + payment_secret: Option<&Secret>, + prv_key_data_id: PrvKeyDataId, + account_name: Option, + ecdsa: bool, + ) -> Result> { + let account_store = self.inner.store.clone().as_account_store()?; + + let prv_key_data = self + .inner + .store + .as_prv_key_data_store()? + .load_key_data(wallet_secret, &prv_key_data_id) + .await? + .ok_or_else(|| Error::PrivateKeyNotFound(prv_key_data_id))?; + + let secret_key = prv_key_data + .as_secret_key(payment_secret) + .map_err(|_| Error::custom("Invalid private key"))? + .ok_or(Error::custom("Sectet key is required"))?; + + let secp = secp256k1::Secp256k1::new(); + let public_key = secret_key.public_key(&secp); + let prv_key_data_id = prv_key_data.id; + let account: Arc = + Arc::new(keypair::Keypair::try_new(self, account_name, public_key, prv_key_data_id, ecdsa).await?); + + if account_store.load_single(account.id()).await?.is_some() { + return Err(Error::AccountAlreadyExists(*account.id())); + } + + self.inner.store.clone().as_account_store()?.store_single(&account.to_storage()?, None).await?; + self.inner.store.commit(wallet_secret).await?; + + Ok(account) + } + pub async fn create_wallet( self: &Arc, wallet_secret: &Secret, @@ -911,12 +954,24 @@ impl Wallet { wallet_secret: &Secret, prv_key_data_create_args: PrvKeyDataCreateArgs, ) -> Result { - let mnemonic = Mnemonic::new(prv_key_data_create_args.mnemonic.as_str()?, Language::default())?; - let prv_key_data = PrvKeyData::try_from_mnemonic( - mnemonic.clone(), - prv_key_data_create_args.payment_secret.as_ref(), - self.store().encryption_kind()?, - )?; + let PrvKeyDataCreateArgs { secret, payment_secret, kind, name } = prv_key_data_create_args; + let prv_key_data = match kind { + PrvKeyDataVariantKind::Mnemonic => { + let mnemonic = Mnemonic::new(secret.as_str()?, Language::default())?; + PrvKeyData::try_from_mnemonic(mnemonic.clone(), payment_secret.as_ref(), self.store().encryption_kind()?, name)? + } + PrvKeyDataVariantKind::SecretKey => { + //let secp = secp256k1::Secp256k1::new(); + let secret_key = secp256k1::SecretKey::from_slice(secret.as_ref())?; + //let public_key = secret_key.public_key(&secp); + //log_info!("public_key: {}", public_key.to_string()); + PrvKeyData::try_from_secret_key(secret_key, payment_secret.as_ref(), self.store().encryption_kind()?, name)? + } + _ => { + return Err(Error::Custom("Invalid prv key data kind, supported types are Mnemonic and SecretKey".to_string())); + } + }; + let prv_key_data_info = PrvKeyDataInfo::from(prv_key_data.as_ref()); let prv_key_data_id = prv_key_data.id; let prv_key_data_store = self.inner.store.as_prv_key_data_store()?; @@ -944,7 +999,7 @@ impl Wallet { let storage_descriptor = self.inner.store.location()?; let mnemonic = Mnemonic::random(mnemonic_phrase_word_count, Default::default())?; let account_index = 0; - let prv_key_data = PrvKeyData::try_from_mnemonic(mnemonic.clone(), payment_secret.as_ref(), encryption_kind)?; + let prv_key_data = PrvKeyData::try_from_mnemonic(mnemonic.clone(), payment_secret.as_ref(), encryption_kind, None)?; let xpub_key = prv_key_data .create_xpub(payment_secret.as_ref(), account_kind.unwrap_or(BIP32_ACCOUNT_KIND.into()), account_index) .await?; @@ -1474,7 +1529,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 +1537,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) @@ -1701,7 +1748,8 @@ impl Wallet { Secret::from(mnemonic.phrase_string()) }; - let prv_key_data_args = PrvKeyDataCreateArgs::new(None, payment_secret.cloned(), mnemonic_phrase_string); + let prv_key_data_args = + PrvKeyDataCreateArgs::new(None, payment_secret.cloned(), mnemonic_phrase_string, PrvKeyDataVariantKind::Mnemonic); self.store().batch().await?; let prv_key_data_id = self.clone().create_prv_key_data(wallet_secret, prv_key_data_args).await?; diff --git a/wallet/core/src/wasm/api/extensions.rs b/wallet/core/src/wasm/api/extensions.rs index 8057d74017..e5474defc7 100644 --- a/wallet/core/src/wasm/api/extensions.rs +++ b/wallet/core/src/wasm/api/extensions.rs @@ -11,6 +11,7 @@ pub trait WalletApiObjectExtension { fn get_account_id(&self, key: &str) -> Result; fn try_get_account_id_list(&self, key: &str) -> Result>>; fn get_transaction_id(&self, key: &str) -> Result; + fn try_get_addresses(&self, key: &str) -> Result>>; } impl WalletApiObjectExtension for Object { @@ -69,4 +70,16 @@ impl WalletApiObjectExtension for Object { Ok(None) } } + + fn try_get_addresses(&self, key: &str) -> Result>> { + if let Ok(array) = self.get_vec(key) { + let mut addresses = Vec::new(); + for address in array.into_iter() { + addresses.push(Address::try_cast_from(&address)?.into_owned()); + } + Ok(Some(addresses)) + } else { + Ok(None) + } + } } diff --git a/wallet/core/src/wasm/api/message.rs b/wallet/core/src/wasm/api/message.rs index 8a023267b8..eed5b29b34 100644 --- a/wallet/core/src/wasm/api/message.rs +++ b/wallet/core/src/wasm/api/message.rs @@ -5,14 +5,14 @@ use crate::account::descriptor::IAccountDescriptor; use crate::api::message::*; use crate::imports::*; use crate::tx::{Fees, PaymentDestination, PaymentOutputs}; +use crate::wasm::api::keydata::PrvKeyDataVariantKind; use crate::wasm::tx::fees::IFees; use crate::wasm::tx::GeneratorSummary; use js_sys::Array; +use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; use serde_wasm_bindgen::from_value; use workflow_wasm::serde::to_value; -use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; - macro_rules! try_from { ($name:ident : $from_type:ty, $to_type:ty, $body:block) => { impl TryFrom<$from_type> for $to_type { @@ -749,8 +749,12 @@ declare! { * to be provided. */ paymentSecret? : string; - /** BIP39 mnemonic phrase (12 or 24 words)*/ - mnemonic : string; + /** BIP39 mnemonic phrase (12 or 24 words) if kind is mnemonic */ + mnemonic? : string; + /** Secret key if kind is secretKey */ + secretKey? : string; + /** Kind of the private key data */ + kind : "mnemonic" | "secretKey"; } "#, } @@ -759,12 +763,27 @@ try_from! ( args: IPrvKeyDataCreateRequest, PrvKeyDataCreateRequest, { let wallet_secret = args.get_secret("walletSecret")?; let name = args.try_get_string("name")?; let payment_secret = args.try_get_secret("paymentSecret")?; - let mnemonic = args.get_secret("mnemonic")?; + let kind = args.get_string("kind")?; + let (secret, kind) = match kind.as_str() { + "mnemonic" => (args.get_secret("mnemonic")?, PrvKeyDataVariantKind::Mnemonic), + "secretKey" => { + let mut hex_key = args.get_string("secretKey")?; + let mut secret = [0u8; 32]; + faster_hex::hex_decode(hex_key.as_bytes(), &mut secret).map_err(|err|Error::custom(format!("secretKey: {err}")))?; + hex_key.zeroize(); + let secret = Secret::new(secret.to_vec()); + (secret, PrvKeyDataVariantKind::SecretKey) + }, + _ => return Err(Error::custom("Invalid kind, supported: mnemonic, secretKey".to_string())), + }; + + //log_info!("secret: {:?}", secret); let prv_key_data_args = PrvKeyDataCreateArgs { name, payment_secret, - mnemonic, + kind, + secret, }; Ok(PrvKeyDataCreateRequest { wallet_secret, prv_key_data_args }) @@ -1033,16 +1052,14 @@ declare! { accountIndex?:number; prvKeyDataId:string; paymentSecret?:string; + } | { + walletSecret: string; + type: "kaspa-keypair-standard"; + accountName:string; + prvKeyDataId:string; + paymentSecret?:string; + ecdsa?:boolean; }; - // |{ - // walletSecret: string; - // type: "multisig"; - // accountName:string; - // accountIndex?:number; - // prvKeyDataId:string; - // pubkeys:HexString[]; - // paymentSecret?:string; - // } // |{ // walletSecret: string; @@ -1060,21 +1077,32 @@ try_from! (args: IAccountsCreateRequest, AccountsCreateRequest, { let kind = AccountKind::try_from(args.try_get_value("type")?.ok_or(Error::custom("type is required"))?)?; - if kind != crate::account::BIP32_ACCOUNT_KIND { - return Err(Error::custom("only BIP32 accounts are currently supported")); - } + let account_create_args = match kind.as_str() { + crate::account::BIP32_ACCOUNT_KIND => { + let prv_key_data_args = PrvKeyDataArgs { + prv_key_data_id: args.try_get_prv_key_data_id("prvKeyDataId")?.ok_or(Error::custom("prvKeyDataId is required"))?, + payment_secret: args.try_get_secret("paymentSecret")?, + }; - let prv_key_data_args = PrvKeyDataArgs { - prv_key_data_id: args.try_get_prv_key_data_id("prvKeyDataId")?.ok_or(Error::custom("prvKeyDataId is required"))?, - payment_secret: args.try_get_secret("paymentSecret")?, - }; + let account_args = AccountCreateArgsBip32 { + account_name: args.try_get_string("accountName")?, + account_index: args.get_u64("accountIndex").ok(), + }; - let account_args = AccountCreateArgsBip32 { - account_name: args.try_get_string("accountName")?, - account_index: args.get_u64("accountIndex").ok(), - }; + AccountCreateArgs::Bip32 { prv_key_data_args, account_args } - let account_create_args = AccountCreateArgs::Bip32 { prv_key_data_args, account_args }; + } + crate::account::KEYPAIR_ACCOUNT_KIND => { + AccountCreateArgs::Keypair { + prv_key_data_id: args.try_get_prv_key_data_id("prvKeyDataId")?.ok_or(Error::custom("prvKeyDataId is required"))?, + account_name: args.try_get_string("accountName")?, + ecdsa: args.get_bool("ecdsa").unwrap_or(false), + } + } + _ => { + return Err(Error::custom("only BIP32/kaspa-keypair-standard accounts are currently supported")); + } + }; Ok(AccountsCreateRequest { wallet_secret, account_create_args }) }); @@ -1372,6 +1400,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 +1424,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 +1432,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! { @@ -1433,6 +1466,277 @@ try_from!(args: AccountsSendResponse, IAccountsSendResponse, { // --- +declare! { + IAccountsPskbSignRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsPskbSignRequest { + /** + * Hex identifier of the account. + */ + accountId : HexString; + /** + * Wallet encryption secret. + */ + walletSecret : string; + /** + * Optional key encryption secret or BIP39 passphrase. + */ + paymentSecret? : string; + + /** + * PSKB to sign. + */ + pskb : string; + + /** + * Address to sign for. + */ + signForAddress? : Address | string; + } + "#, +} + +try_from! ( args: IAccountsPskbSignRequest, AccountsPskbSignRequest, { + let account_id = args.get_account_id("accountId")?; + let wallet_secret = args.get_secret("walletSecret")?; + let payment_secret = args.try_get_secret("paymentSecret")?; + let pskb = args.get_string("pskb")?; + let sign_for_address = match args.try_get_value("signForAddress")? { + Some(v) => Some(Address::try_cast_from(&v)?.into_owned()), + None => None, + }; + Ok(AccountsPskbSignRequest { account_id, wallet_secret, payment_secret, pskb, sign_for_address }) +}); + +declare! { + IAccountsPskbSignResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsPskbSignResponse { + /** + * signed PSKB. + */ + pskb: string; + } + "#, +} + +try_from!(args: AccountsPskbSignResponse, IAccountsPskbSignResponse, { + + let response = IAccountsPskbSignResponse::default(); + response.set("pskb", &args.pskb.into())?; + Ok(response) +}); + +// --- + +declare! { + IAccountsPskbBroadcastRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsPskbBroadcastRequest { + accountId : HexString; + pskb : string; + } + "#, +} + +try_from! ( args: IAccountsPskbBroadcastRequest, AccountsPskbBroadcastRequest, { + let account_id = args.get_account_id("accountId")?; + let pskb = args.get_string("pskb")?; + Ok(AccountsPskbBroadcastRequest { account_id, pskb }) +}); + +declare! { + IAccountsPskbBroadcastResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsPskbBroadcastResponse { + transactionIds : HexString[]; + } + "#, +} + +try_from! ( args: AccountsPskbBroadcastResponse, IAccountsPskbBroadcastResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IPskbBroadcastRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IPskbBroadcastRequest { + pskb : string; + networkId : NetworkId | string; + } + "#, +} + +try_from! ( args: IPskbBroadcastRequest, PskbBroadcastRequest, { + let pskb = args.get_string("pskb")?; + let network_id = args.get_network_id("networkId")?; + Ok(PskbBroadcastRequest { pskb, network_id }) +}); + +declare! { + IPskbBroadcastResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IPskbBroadcastResponse { + transactionIds : HexString[]; + } + "#, +} + +try_from! ( args: PskbBroadcastResponse, IPskbBroadcastResponse, { + Ok(to_value(&args)?.into()) +}); + +declare! { + IAccountsPskbSendRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsPskbSendRequest { + /** + * Hex identifier of the account. + */ + accountId : HexString; + /** + * Wallet encryption secret. + */ + walletSecret : string; + /** + * Optional key encryption secret or BIP39 passphrase. + */ + paymentSecret? : string; + + /** + * PSKB to sign. + */ + pskb : string; + + /** + * Address to sign for. + */ + signForAddress? : Address | string; + } + "#, +} + +try_from! ( args: IAccountsPskbSendRequest, AccountsPskbSendRequest, { + let account_id = args.get_account_id("accountId")?; + let wallet_secret = args.get_secret("walletSecret")?; + let payment_secret = args.try_get_secret("paymentSecret")?; + let pskb = args.get_string("pskb")?; + let sign_for_address = match args.try_get_value("signForAddress")? { + Some(v) => Some(Address::try_cast_from(&v)?.into_owned()), + None => None, + }; + Ok(AccountsPskbSendRequest { account_id, wallet_secret, payment_secret, pskb, sign_for_address }) +}); + +// --- + +declare! { + IAccountsPskbSendResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsPskbSendResponse { + transactionIds : HexString[]; + } + "#, +} + +try_from! ( args: AccountsPskbSendResponse, IAccountsPskbSendResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IAccountsGetUtxosRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsGetUtxosRequest { + accountId : HexString; + addresses : Address[] | string[]; + minAmountSompi? : bigint; + } + "#, +} + +try_from! ( args: IAccountsGetUtxosRequest, AccountsGetUtxosRequest, { + let account_id = args.get_account_id("accountId")?; + let addresses = args.try_get_addresses("addresses")?; + let min_amount_sompi = args.get_u64("minAmountSompi").ok(); + Ok(AccountsGetUtxosRequest { account_id, addresses, min_amount_sompi }) +}); + +declare! { + IAccountsGetUtxosResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsGetUtxosResponse { + utxos : UtxoEntry[]; + } + "#, +} + +try_from! ( args: AccountsGetUtxosResponse, IAccountsGetUtxosResponse, { + let response = IAccountsGetUtxosResponse::default(); + + + let utxos = args.utxos.into_iter().map(|entry| entry.to_js_object()).collect::>>()?; + let utxos = js_sys::Array::from_iter(utxos.into_iter()); + response.set("utxos", &utxos)?; + Ok(response) +}); + +// --- + declare! { IAccountsTransferRequest, r#" @@ -1446,6 +1750,7 @@ declare! { destinationAccountId : HexString; walletSecret : string; paymentSecret? : string; + feeRate? : number; priorityFeeSompi? : IFees | bigint; transferAmountSompi : bigint; } @@ -1457,6 +1762,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 +1771,7 @@ try_from! ( args: IAccountsTransferRequest, AccountsTransferRequest, { destination_account_id, wallet_secret, payment_secret, + fee_rate, priority_fee_sompi, transfer_amount_sompi, }) @@ -1505,6 +1812,7 @@ declare! { export interface IAccountsEstimateRequest { accountId : HexString; destination : IPaymentOutput[]; + feeRate? : number; priorityFeeSompi : IFees | bigint; payload? : Uint8Array | string; } @@ -1513,6 +1821,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 +1829,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! { @@ -1545,6 +1854,91 @@ try_from! ( args: AccountsEstimateResponse, IAccountsEstimateResponse, { // --- +declare! { + IFeeRateEstimateBucket, + r#" + export interface IFeeRateEstimateBucket { + feeRate : number; + seconds : number; + } + "#, +} + +declare! { + IFeeRateEstimateRequest, + r#" + export interface IFeeRateEstimateRequest { } + "#, +} + +try_from! ( _args: IFeeRateEstimateRequest, FeeRateEstimateRequest, { + Ok(FeeRateEstimateRequest { }) +}); + +declare! { + IFeeRateEstimateResponse, + r#" + export interface IFeeRateEstimateResponse { + priority : IFeeRateEstimateBucket, + normal : IFeeRateEstimateBucket, + low : IFeeRateEstimateBucket, + } + "#, +} + +try_from! ( args: FeeRateEstimateResponse, IFeeRateEstimateResponse, { + Ok(to_value(&args)?.into()) +}); + +declare! { + IFeeRatePollerEnableRequest, + r#" + export interface IFeeRatePollerEnableRequest { + intervalSeconds : number; + } + "#, +} + +try_from! ( args: IFeeRatePollerEnableRequest, FeeRatePollerEnableRequest, { + let interval_seconds = args.get_u64("intervalSeconds")?; + Ok(FeeRatePollerEnableRequest { interval_seconds }) +}); + +declare! { + IFeeRatePollerEnableResponse, + r#" + export interface IFeeRatePollerEnableResponse { } + "#, +} + +try_from! ( _args: FeeRatePollerEnableResponse, IFeeRatePollerEnableResponse, { + Ok(IFeeRatePollerEnableResponse::default()) +}); + +declare! { + IFeeRatePollerDisableRequest, + r#" + export interface IFeeRatePollerDisableRequest { } + "#, +} + +try_from! ( _args: IFeeRatePollerDisableRequest, FeeRatePollerDisableRequest, { + Ok(FeeRatePollerDisableRequest { }) +}); + +declare! { + IFeeRatePollerDisableResponse, + r#" + export interface IFeeRatePollerDisableResponse { } + "#, +} + +try_from! ( _args: FeeRatePollerDisableResponse, IFeeRatePollerDisableResponse, { + Ok(IFeeRatePollerDisableResponse::default()) +}); + +// --- + declare! { ITransactionsDataGetRequest, r#" @@ -1605,6 +1999,34 @@ try_from! ( args: TransactionsDataGetResponse, ITransactionsDataGetResponse, { // --- +declare! { + INetworkParams, + r#" + /** + * + * + * @category Wallet API + */ + export interface INetworkParams { + coinbaseTransactionMaturityPeriodDaa : number; + coinbaseTransactionStasisPeriodDaa : number; + userTransactionMaturityPeriodDaa : number; + additionalCompoundTransactionMass : number; + } + "#, +} + +try_from! ( args: &NetworkParams, INetworkParams, { + let response = INetworkParams::default(); + response.set("coinbaseTransactionMaturityPeriodDaa", &to_value(&args.coinbase_transaction_maturity_period_daa)?)?; + response.set("coinbaseTransactionStasisPeriodDaa", &to_value(&args.coinbase_transaction_stasis_period_daa)?)?; + response.set("userTransactionMaturityPeriodDaa", &to_value(&args.user_transaction_maturity_period_daa)?)?; + response.set("additionalCompoundTransactionMass", &to_value(&args.additional_compound_transaction_mass)?)?; + Ok(response) +}); + +// --- + declare! { ITransactionsReplaceNoteRequest, r#" @@ -1773,3 +2195,184 @@ try_from! ( _args: AddressBookEnumerateResponse, IAddressBookEnumerateResponse, }); // --- +// --- + +declare! { + IAccountsCommitRevealRequest, + r#" + /** + * + * Atomic commit reveal operation using parameterized account address to + * dynamically generate the commit P2SH address. + * + * The account address is selected through addressType and addressIndex + * and will be used to complete the script signature. + * + * A placeholder of format {{pubkey}} is to be provided inside ScriptSig + * in order to be superseded by the selected address' payload. + * + * The selected address will also be used to spend reveal transaction to. + * + * The default revealFeeSompi is 100_000 sompi. + * + * @category Wallet API + */ + export interface IAccountsCommitRevealRequest { + accountId : HexString; + addressType : CommitRevealAddressKind; + addressIndex : number; + scriptSig : Uint8Array | HexString; + walletSecret : string; + commitAmountSompi : bigint; + paymentSecret? : string; + feeRate? : number; + revealFeeSompi : bigint; + payload? : Uint8Array | HexString; + } + "#, +} + +try_from! ( args: IAccountsCommitRevealRequest, AccountsCommitRevealRequest, { + let account_id = args.get_account_id("accountId")?; + let address_type = args.get_value("addressType")?; + + let address_type = if let Some(address_type) = address_type.as_string() { + address_type.parse()? + } else { + CommitRevealAddressKind::try_enum_from(&address_type)? + }; + + let address_index = args.get_u32("addressIndex")?; + let script_sig = args.get_vec_u8("scriptSig")?; + let wallet_secret = args.get_secret("walletSecret")?; + let payment_secret = args.try_get_secret("paymentSecret")?; + let commit_amount_sompi = args.get_u64("commitAmountSompi")?; + let fee_rate = args.get_f64("feeRate").ok(); + + let reveal_fee_sompi = args.get_u64("revealFeeSompi")?; + + let payload = args.try_get_value("payload")?.map(|v| v.try_as_vec_u8()).transpose()?; + + Ok(AccountsCommitRevealRequest { + account_id, + address_type, + address_index, + script_sig, + wallet_secret, + payment_secret, + commit_amount_sompi, + fee_rate, + reveal_fee_sompi, + payload, + }) +}); + +declare! { + IAccountsCommitRevealResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsCommitRevealResponse { + transactionIds : HexString[]; + } + "#, +} + +try_from! ( args: AccountsCommitRevealResponse, IAccountsCommitRevealResponse, { + let response = IAccountsCommitRevealResponse::default(); + response.set("transactionIds", &to_value(&args.transaction_ids)?)?; + Ok(response) +}); + +// --- + +declare! { + IAccountsCommitRevealManualRequest, + r#" + /** + * + * Atomic commit reveal operation using given payment outputs. + * + * The startDestination stands for the commit transaction and the endDestination + * for the reveal transaction. + * + * The scriptSig will be used to spend the UTXO of the first transaction and + * must therefore match the startDestination output P2SH. + * + * Set revealFeeSompi or reflect the reveal fee transaction on endDestination + * output amount. + * + * The default revealFeeSompi is 100_000 sompi. + * + * @category Wallet API + */ + export interface IAccountsCommitRevealManualRequest { + accountId : HexString; + scriptSig : Uint8Array | HexString; + startDestination: IPaymentOutput; + endDestination: IPaymentOutput; + walletSecret : string; + paymentSecret? : string; + feeRate? : number; + revealFeeSompi : bigint; + payload? : Uint8Array | HexString; + } + "#, +} + +try_from! ( args: IAccountsCommitRevealManualRequest, AccountsCommitRevealManualRequest, { + let account_id = args.get_account_id("accountId")?; + let script_sig = args.get_vec_u8("scriptSig")?; + let wallet_secret = args.get_secret("walletSecret")?; + let payment_secret = args.try_get_secret("paymentSecret")?; + + let commit_output = args.get_value("startDestination")?; + let start_destination: PaymentDestination = + if commit_output.is_undefined() { PaymentDestination::Change } else { PaymentOutputs::try_owned_from(commit_output)?.into() }; + + let reveal_output = args.get_value("endDestination")?; + let end_destination: PaymentDestination = + if reveal_output.is_undefined() { PaymentDestination::Change } else { PaymentOutputs::try_owned_from(reveal_output)?.into() }; + + let fee_rate = args.get_f64("feeRate").ok(); + let reveal_fee_sompi = args.get_u64("revealFeeSompi")?; + + let payload = args.try_get_value("payload")?.map(|v| v.try_as_vec_u8()).transpose()?; + + Ok(AccountsCommitRevealManualRequest { + account_id, + script_sig, + wallet_secret, + payment_secret, + start_destination, + end_destination, + fee_rate, + reveal_fee_sompi, + payload, + }) +}); + +declare! { + IAccountsCommitRevealManualResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsCommitRevealManualResponse { + transactionIds : HexString[]; + } + "#, +} + +try_from! ( args: AccountsCommitRevealManualResponse, IAccountsCommitRevealManualResponse, { + let response = IAccountsCommitRevealManualResponse::default(); + response.set("transactionIds", &to_value(&args.transaction_ids)?)?; + Ok(response) +}); + +// --- diff --git a/wallet/core/src/wasm/api/mod.rs b/wallet/core/src/wasm/api/mod.rs index 9d06e017d3..4d9d852f15 100644 --- a/wallet/core/src/wasm/api/mod.rs +++ b/wallet/core/src/wasm/api/mod.rs @@ -46,10 +46,20 @@ declare_wasm_handlers!([ AccountsGet, AccountsCreateNewAddress, AccountsSend, + AccountsPskbSign, + AccountsPskbBroadcast, + PskbBroadcast, + AccountsPskbSend, + AccountsGetUtxos, AccountsTransfer, AccountsEstimate, TransactionsDataGet, TransactionsReplaceNote, TransactionsReplaceMetadata, AddressBookEnumerate, + FeeRateEstimate, + FeeRatePollerEnable, + FeeRatePollerDisable, + AccountsCommitReveal, + AccountsCommitRevealManual, ]); diff --git a/wallet/core/src/wasm/notify.rs b/wallet/core/src/wasm/notify.rs index 3383e1353e..5f167176d1 100644 --- a/wallet/core/src/wasm/notify.rs +++ b/wallet/core/src/wasm/notify.rs @@ -131,6 +131,7 @@ cfg_if! { Discovery = "discovery", Balance = "balance", Error = "error", + FeeRate = "fee-rate", } /** @@ -167,6 +168,7 @@ cfg_if! { "discovery": IDiscoveryEvent, "balance": IBalanceEvent, "error": IErrorEvent, + "fee-rate": IFeeRateEvent, } /** @@ -304,6 +306,32 @@ declare! { "#, } +#[cfg(feature = "wasm32-sdk")] +declare! { + IFeeRateEvent, + r#" + /** + * Emitted by {@link Wallet} when the fee rate changes. + * + * @category Wallet Events + */ + export interface IFeeRateEvent { + priority: { + feerate: bigint, + seconds: bigint, + }, + normal: { + feerate: bigint, + seconds: bigint, + }, + low: { + feerate: bigint, + seconds: bigint, + }, + } + "#, +} + #[cfg(feature = "wasm32-sdk")] declare! { IWalletCreateEvent, diff --git a/wallet/core/src/wasm/tx/generator/generator.rs b/wallet/core/src/wasm/tx/generator/generator.rs index 5724b84811..8cc2362382 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 8d572ec1ee..ad87430ffe 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 dedff04fb8..2414bc9087 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/utils.rs b/wallet/core/src/wasm/utils.rs index a06c6136a3..196f3d1565 100644 --- a/wallet/core/src/wasm/utils.rs +++ b/wallet/core/src/wasm/utils.rs @@ -1,6 +1,8 @@ +use crate::imports::NetworkParams; use crate::result::Result; +use crate::wasm::api::message::INetworkParams; use js_sys::BigInt; -use kaspa_consensus_core::network::{NetworkType, NetworkTypeT}; +use kaspa_consensus_core::network::{NetworkIdT, NetworkType, NetworkTypeT}; use wasm_bindgen::prelude::*; use workflow_wasm::prelude::*; @@ -44,3 +46,33 @@ pub fn sompi_to_kaspa_string_with_suffix(sompi: ISompiToKaspa, network: &Network let network_type = NetworkType::try_from(network)?; Ok(crate::utils::sompi_to_kaspa_string_with_suffix(sompi, &network_type)) } + +#[wasm_bindgen(js_name = "getNetworkParams")] +#[allow(non_snake_case)] +pub fn get_network_params(networkId: NetworkIdT) -> Result { + let params = NetworkParams::from(*networkId.try_into_cast()?); + params.try_into() +} + +#[wasm_bindgen(js_name = "getTransactionMaturityProgress")] +#[allow(non_snake_case)] +pub fn get_transaction_maturity_progress( + blockDaaScore: BigInt, + currentDaaScore: BigInt, + networkId: NetworkIdT, + isCoinbase: bool, +) -> Result { + let network_id = *networkId.try_into_cast()?; + let params = NetworkParams::from(network_id); + let block_daa_score = blockDaaScore.try_as_u64()?; + let current_daa_score = currentDaaScore.try_as_u64()?; + let maturity = + if isCoinbase { params.coinbase_transaction_maturity_period_daa() } else { params.user_transaction_maturity_period_daa() }; + + if current_daa_score < block_daa_score + maturity { + let progress = (current_daa_score - block_daa_score) as f64 / maturity as f64; + Ok(format!("{}", (progress * 100.) as usize)) + } else { + Ok("".to_string()) + } +} diff --git a/wallet/core/src/wasm/wallet/wallet.rs b/wallet/core/src/wasm/wallet/wallet.rs index bd91bedf22..57f5a817f5 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/wallet/pskt/Cargo.toml b/wallet/pskt/Cargo.toml index b3fff1bfaf..193fe4403a 100644 --- a/wallet/pskt/Cargo.toml +++ b/wallet/pskt/Cargo.toml @@ -41,6 +41,10 @@ wasm-bindgen.workspace = true serde_json.workspace = true serde-wasm-bindgen.workspace = true workflow-wasm.workspace = true +separator.workspace = true [dev-dependencies] serde_json.workspace = true +wasm-bindgen-test.workspace = true +js-sys.workspace = true +web-sys.workspace = true diff --git a/wallet/pskt/examples/multisig.rs b/wallet/pskt/examples/multisig.rs index 7a9ca190e5..486b72b0a7 100644 --- a/wallet/pskt/examples/multisig.rs +++ b/wallet/pskt/examples/multisig.rs @@ -1,3 +1,4 @@ +use kaspa_consensus_core::config::params::TESTNET11_PARAMS; use kaspa_consensus_core::{ hashing::sighash::{calc_schnorr_signature_hash, SigHashReusedValuesUnsync}, tx::{TransactionId, TransactionOutpoint, UtxoEntry}, @@ -115,7 +116,8 @@ fn main() { println!("Finalized: {}", ser_finalized); let extractor_pskt: PSKT = serde_json::from_str(&ser_finalized).expect("Failed to deserialize"); - let tx = extractor_pskt.extract_tx().unwrap()(10).0; + let params = TESTNET11_PARAMS; + let tx = extractor_pskt.extract_tx(¶ms).unwrap().tx; let ser_tx = serde_json::to_string_pretty(&tx).unwrap(); println!("Tx: {}", ser_tx); } diff --git a/wallet/pskt/src/bundle.rs b/wallet/pskt/src/bundle.rs index e08474d7a4..7d8ae601b0 100644 --- a/wallet/pskt/src/bundle.rs +++ b/wallet/pskt/src/bundle.rs @@ -9,6 +9,7 @@ use kaspa_consensus_core::network::{NetworkId, NetworkType}; use kaspa_consensus_core::tx::{ScriptPublicKey, TransactionOutpoint, UtxoEntry}; use hex; +use kaspa_consensus_core::constants::UNACCEPTED_DAA_SCORE; use kaspa_txscript::{extract_script_pub_key_address, pay_to_address_script, pay_to_script_hash_script}; use serde::{Deserialize, Serialize}; use std::ops::Deref; @@ -148,8 +149,14 @@ impl Default for Bundle { } } +// Replaces pubkey placeholder in payload string when pubkey_bytes is given. pub fn lock_script_sig_templating(payload: String, pubkey_bytes: Option<&[u8]>) -> Result, Error> { - let mut payload_bytes: Vec = hex::decode(payload)?; + let payload_bytes: Vec = hex::decode(payload)?; + lock_script_sig_templating_bytes(payload_bytes.to_vec(), pubkey_bytes) +} + +pub fn lock_script_sig_templating_bytes(payload: Vec, pubkey_bytes: Option<&[u8]>) -> Result, Error> { + let mut payload_bytes = payload; if let Some(pubkey) = pubkey_bytes { let placeholder = b"{{pubkey}}"; @@ -236,6 +243,33 @@ pub fn unlock_utxo( Ok(pskt.into()) } +// Build UTXO spending PSKB with custom input and multiple outputs +// to be used in atomic transaction batch. +pub fn unlock_utxo_outputs_as_batch_transaction_pskb( + amount: u64, + start_address: &Address, + script_sig: &[u8], + destination_outputs: Vec<(Address, u64)>, +) -> Result { + let origin_spk = pay_to_address_script(start_address); + + let utxo_entry = UtxoEntry { amount, script_public_key: origin_spk, block_daa_score: UNACCEPTED_DAA_SCORE, is_coinbase: false }; + + let input = + InputBuilder::default().utxo_entry(utxo_entry.to_owned()).sig_op_count(1).redeem_script(script_sig.to_vec()).build()?; + + let outputs: Vec = destination_outputs + .iter() + .filter_map(|(address, amount)| { + OutputBuilder::default().amount(*amount).script_public_key(pay_to_address_script(address)).build().ok() + }) + .collect(); + + let pskt: PSKT = + outputs.into_iter().fold(PSKT::::default().constructor().input(input), |pskt, output| pskt.output(output)); + Ok(pskt.into()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/wallet/pskt/src/pskt.rs b/wallet/pskt/src/pskt.rs index abae18f2dc..8a78a2e909 100644 --- a/wallet/pskt/src/pskt.rs +++ b/wallet/pskt/src/pskt.rs @@ -3,7 +3,7 @@ //! use kaspa_bip32::{secp256k1, DerivationPath, KeyFingerprint}; -use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; +use kaspa_consensus_core::{hashing::sighash::SigHashReusedValuesUnsync, Hash}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{collections::BTreeMap, fmt::Display, fmt::Formatter, future::Future, marker::PhantomData, ops::Deref}; @@ -13,7 +13,8 @@ pub use crate::global::{Global, GlobalBuilder}; pub use crate::input::{Input, InputBuilder}; pub use crate::output::{Output, OutputBuilder}; pub use crate::role::{Combiner, Constructor, Creator, Extractor, Finalizer, Signer, Updater}; -use kaspa_consensus_core::tx::UtxoEntry; +use kaspa_consensus_core::config::params::Params; +use kaspa_consensus_core::mass::MassCalculator; use kaspa_consensus_core::{ hashing::sighash_type::SigHashType, subnets::SUBNETWORK_ID_NATIVE, @@ -313,6 +314,20 @@ impl PSKT { pub fn combiner(self) -> PSKT { PSKT { inner_pskt: self.inner_pskt, role: Default::default() } } + + // Unorphan batch transaction UTXO. + pub fn set_input_prev_transaction_id(self, transaction_id: Hash) -> PSKT { + let mut new_inputs = self.inner_pskt.inputs.clone(); + + new_inputs.iter_mut().for_each(|input| { + input.previous_outpoint.transaction_id = transaction_id; + }); + + let mut updated_inner = self.inner_pskt.clone(); + updated_inner.inputs = new_inputs; + + PSKT { inner_pskt: updated_inner, role: Default::default() } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -411,7 +426,7 @@ impl PSKT { } impl PSKT { - pub fn extract_tx_unchecked(self) -> Result (Transaction, Vec>), TxNotFinalized> { + pub fn extract_tx_unchecked(self, params: &Params) -> Result, TxNotFinalized> { let tx = self.unsigned_tx(); let entries = tx.entries; let mut tx = tx.tx; @@ -419,16 +434,15 @@ impl PSKT { dest.signature_script = src.final_script_sig.ok_or(TxNotFinalized {})?; Ok(()) })?; - Ok(move |mass| { - tx.set_mass(mass); - (tx, entries) - }) + let tx = MutableTransaction { tx, entries, calculated_fee: None, calculated_compute_mass: None }; + let calculator = MassCalculator::new_with_consensus_params(params); + let mass = calculator.calc_tx_overall_mass(&tx.as_verifiable(), None).unwrap_or_default(); + tx.tx.set_mass(mass); + Ok(tx) } - pub fn extract_tx(self) -> Result (Transaction, Vec>), ExtractError> { - let (tx, entries) = self.extract_tx_unchecked()?(0); - - let tx = MutableTransaction::with_entries(tx, entries.into_iter().flatten().collect()); + pub fn extract_tx(self, params: &Params) -> Result, ExtractError> { + let tx = self.extract_tx_unchecked(params)?; use kaspa_consensus_core::tx::VerifiableTransaction; { let tx = tx.as_verifiable(); @@ -440,13 +454,7 @@ impl PSKT { >::Ok(()) })?; } - let entries = tx.entries; - let tx = tx.tx; - let closure = move |mass| { - tx.set_mass(mass); - (tx, entries) - }; - Ok(closure) + Ok(tx) } } diff --git a/wallet/pskt/src/wasm/bundle.rs b/wallet/pskt/src/wasm/bundle.rs index 8b13789179..bafd1db2b7 100644 --- a/wallet/pskt/src/wasm/bundle.rs +++ b/wallet/pskt/src/wasm/bundle.rs @@ -1 +1,249 @@ +use std::ops::Deref; +use super::error::*; +use super::result::*; +use crate::bundle::Bundle as Inner; +// use crate::pskt::Combiner; +// use crate::pskt::Constructor; +// use crate::pskt::Creator; +// use crate::pskt::Extractor; +// use crate::pskt::Finalizer; +use crate::pskt::Inner as PSKTInner; +// use crate::pskt::Signer; +// use crate::pskt::Updater; +// use crate::pskt::PSKT as Native; +use crate::wasm::pskt::*; +use crate::wasm::utils::sompi_to_kaspa_string_with_suffix; +use kaspa_consensus_core::network::{NetworkId, NetworkIdT}; +use wasm_bindgen::prelude::*; +use workflow_wasm::convert::TryCastFromJs; + +#[wasm_bindgen(getter_with_clone)] +#[derive(Debug)] +pub struct PSKB(Inner); + +impl Clone for PSKB { + fn clone(&self) -> Self { + PSKB(Inner(self.0 .0.clone())) + } +} + +#[wasm_bindgen] +impl PSKB { + #[wasm_bindgen(constructor)] + pub fn new() -> Result { + let bundle = Inner::new(); + Ok(PSKB(bundle)) + } + + #[wasm_bindgen] + pub fn serialize(&self) -> Result { + self.0.serialize().map_err(Error::from) + } + + #[wasm_bindgen(js_name = "displayFormat")] + pub fn display_format(&self, network_id: &NetworkIdT) -> Result { + let network_id = NetworkId::try_cast_from(network_id).map_err(|err| Error::Custom(err.to_string()))?.into_owned(); + Ok(self.0.display_format(network_id, sompi_to_kaspa_string_with_suffix)) + } + + #[wasm_bindgen] + pub fn deserialize(hex_data: &str) -> Result { + let bundle = Inner::deserialize(hex_data).map_err(Error::from)?; + Ok(PSKB(bundle)) + } + + #[wasm_bindgen(getter, js_name = "length")] + pub fn length(&self) -> usize { + self.0 .0.len() + } + + pub fn add(&mut self, pskt: &PSKT) -> Result<()> { + let payload = pskt.payload_getter(); + + let payload_str: String = payload.as_string().unwrap_or_else(|| "Unable to convert to string".to_string()); + println!("{}", payload_str); + + // Try multiple deserialization methods + if let Ok(inner) = serde_wasm_bindgen::from_value::(payload.clone()) { + self.0.add_inner(inner); + Ok(()) + } else if let Ok(state) = serde_wasm_bindgen::from_value::(payload) { + match state { + State::Creator(native_pskt) => { + self.0.add_inner(native_pskt.deref().clone()); + Ok(()) + } + State::Combiner(native_pskt) => { + self.0.add_inner(native_pskt.deref().clone()); + Ok(()) + } + State::Constructor(native_pskt) => { + self.0.add_inner(native_pskt.deref().clone()); + Ok(()) + } + State::Signer(native_pskt) => { + self.0.add_inner(native_pskt.deref().clone()); + Ok(()) + } + State::Extractor(native_pskt) => { + self.0.add_inner(native_pskt.deref().clone()); + Ok(()) + } + State::Finalizer(native_pskt) => { + self.0.add_inner(native_pskt.deref().clone()); + Ok(()) + } + _ => Err(Error::Custom("Unsupported PSKT state".into())), + } + } else { + Err(Error::Custom("Failed to deserialize PSKT payload".into())) + } + } + + #[wasm_bindgen] + pub fn merge(&mut self, other: &PSKB) { + self.0.merge(other.clone().0); + } +} + +impl From for PSKB { + fn from(bundle: Inner) -> Self { + PSKB(bundle) + } +} + +impl From for Inner { + fn from(bundle: PSKB) -> Self { + bundle.0 + } +} + +impl AsRef for PSKB { + fn as_ref(&self) -> &Inner { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::pskt::Finalizer; + use crate::pskt::PSKT as Native; + use kaspa_consensus_core::tx::ScriptPublicKey; + use serde_json::json; + use std::str::FromStr; + use wasm_bindgen_test::wasm_bindgen_test; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn _test_pskt_bundle_creation() { + let bundle = PSKB::new().expect("Failed to create PSKB"); + assert_eq!(bundle.length(), 0, "New bundle should have zero PSKTs"); + } + + #[wasm_bindgen_test] + fn _test_pskt_bundle_serialize_deserialize() { + // Create a bundle + let mut bundle = PSKB::new().expect("Failed to create PsktBundle"); + + let pskts: Vec = + (0..3).map(|_| PSKT::new(CtorT::from(JsValue::UNDEFINED)).expect("creator").constructor().expect("constructor")).collect(); + pskts.iter().for_each(|pskt| bundle.add(pskt).expect("added to bundle")); + + let serialized = bundle.serialize().expect("Failed to serialize bundle"); + + // Deserialize the bundle + let deserialized_bundle = PSKB::deserialize(&serialized).expect("Failed to deserialize bundle"); + + // Check that the deserialized bundle has the same number of PSKTs + assert_eq!(deserialized_bundle.length(), 3, "Deserialized bundle should have length of 3"); + assert_eq!(bundle.length(), deserialized_bundle.length(), "Deserialized bundle should have same PSKT count"); + } + + #[wasm_bindgen_test] + fn _test_bundle_add() { + let pskt_json = json!({ + "global": { + "version": 0, + "txVersion": 0, + "fallbackLockTime": null, + "inputsModifiable": false, + "outputsModifiable": false, + "inputCount": 0, + "outputCount": 0, + "xpubs": {}, + "id": null, + "proprietaries": {} + }, + "inputs": [ + { + "utxoEntry": { + "amount": 468928887, + "scriptPublicKey": "0000202d8a1414e62e081fb6bcf644e648c18061c2855575cac722f86324cad91dd0faac", + "blockDaaScore": 84981186, + "isCoinbase": false + }, + "previousOutpoint": { + "transactionId": "69155d0e3380e8816dffe2671294ad104f0b3776f35bce1a22f0c21b1f908500", + "index": 0 + }, + "sequence": null, + "minTime": null, + "partialSigs": {}, + "sighashType": 1, + "redeemScript": null, + "sigOpCount": 1, + "bip32Derivations": {}, + "finalScriptSig": null, + "proprietaries": {} + } + ], + "outputs": [ + { + "amount": 1500000000, + "scriptPublicKey": "0000", + "redeemScript": null, + "bip32Derivations": {}, + "proprietaries": {} + } + ] + }); + let json_str = pskt_json.to_string(); + let inner: PSKTInner = serde_json::from_str(&json_str).expect("Failed to parse JSON"); + + let native = Native::::from(inner); + + let wasm_pskt = PSKT::from(State::Finalizer(native)); + + // Create a bundle + let mut bundle = PSKB::new().expect("Failed to create PsktBundle"); + bundle.add(&wasm_pskt).expect("add pskt"); + + // Serialize the bundle + let serialized = bundle.serialize().expect("Failed to serialize bundle"); + console_log!("{}", serialized); + + // Deserialize the bundle + let deserialized_bundle = PSKB::deserialize(&serialized).expect("Failed to deserialize bundle"); + + // Check that the deserialized bundle has the same number of PSKTs + assert_eq!(bundle.length(), deserialized_bundle.length(), "Deserialized bundle should have same PSKT count"); + } + + #[wasm_bindgen_test] + fn _test_deser() { + let pskb = "PSKB5b7b22676c6f62616c223a7b2276657273696f6e223a302c22747856657273696f6e223a302c2266616c6c6261636b4c6f636b54696d65223a6e756c6c2c22696e707574734d6f6469666961626c65223a66616c73652c226f7574707574734d6f6469666961626c65223a66616c73652c22696e707574436f756e74223a302c226f7574707574436f756e74223a302c227870756273223a7b7d2c226964223a6e756c6c2c2270726f70726965746172696573223a7b7d7d2c22696e70757473223a5b7b227574786f456e747279223a7b22616d6f756e74223a3436383932383838372c227363726970745075626c69634b6579223a22303030303230326438613134313465363265303831666236626366363434653634386331383036316332383535353735636163373232663836333234636164393164643066616163222c22626c6f636b44616153636f7265223a38343938313138362c226973436f696e62617365223a66616c73657d2c2270726576696f75734f7574706f696e74223a7b227472616e73616374696f6e4964223a2236393135356430653333383065383831366466666532363731323934616431303466306233373736663335626365316132326630633231623166393038353030222c22696e646578223a307d2c2273657175656e6365223a6e756c6c2c226d696e54696d65223a6e756c6c2c227061727469616c53696773223a7b7d2c227369676861736854797065223a312c2272656465656d536372697074223a6e756c6c2c227369674f70436f756e74223a312c22626970333244657269766174696f6e73223a7b7d2c2266696e616c536372697074536967223a6e756c6c2c2270726f70726965746172696573223a7b7d7d5d2c226f757470757473223a5b7b22616d6f756e74223a313530303030303030302c227363726970745075626c69634b6579223a2230303030222c2272656465656d536372697074223a6e756c6c2c22626970333244657269766174696f6e73223a7b7d2c2270726f70726965746172696573223a7b7d7d5d7d5d"; + // Deserialize the bundle + let deserialized_bundle = PSKB::deserialize(pskb).expect("Failed to deserialize bundle"); + assert_eq!(deserialized_bundle.length(), 1, "Should be length 1"); + let inner = deserialized_bundle.0 .0.first().expect("pskt after deserialize"); + assert_eq!(inner.inputs.len(), 1); + let input_01 = inner.inputs.first().expect("first input"); + assert_eq!(input_01.clone().utxo_entry.expect("utxo entry").amount, 468928887); + assert_eq!( + inner.outputs.first().expect("output").script_public_key, + ScriptPublicKey::from_str("0000").expect("convert valid spk") + ); + } +} diff --git a/wallet/pskt/src/wasm/error.rs b/wallet/pskt/src/wasm/error.rs index 77fb0d8b16..fed61144f6 100644 --- a/wallet/pskt/src/wasm/error.rs +++ b/wallet/pskt/src/wasm/error.rs @@ -10,6 +10,9 @@ pub enum Error { #[error("Unexpected state: {0}")] State(String), + #[error("Expected state: {0}")] + ExpectedState(String), + #[error("Constructor argument must be a valid payload, another PSKT instance, Transaction or undefined")] Ctor(String), @@ -43,6 +46,9 @@ impl Error { pub fn state(state: impl AsRef) -> Self { Error::State(state.as_ref().display().to_string()) } + pub fn expected_state(state: impl Into) -> Self { + Error::ExpectedState(state.into()) + } } impl From<&str> for Error { diff --git a/wallet/pskt/src/wasm/mod.rs b/wallet/pskt/src/wasm/mod.rs index f5e9bea3fb..bf92489cfa 100644 --- a/wallet/pskt/src/wasm/mod.rs +++ b/wallet/pskt/src/wasm/mod.rs @@ -4,3 +4,4 @@ pub mod input; pub mod output; pub mod pskt; pub mod result; +pub mod utils; diff --git a/wallet/pskt/src/wasm/pskt.rs b/wallet/pskt/src/wasm/pskt.rs index 53c8a1cc3c..f2148df77d 100644 --- a/wallet/pskt/src/wasm/pskt.rs +++ b/wallet/pskt/src/wasm/pskt.rs @@ -1,11 +1,13 @@ -use crate::pskt::PSKT as Native; +use crate::pskt::{Input, PSKT as Native}; use crate::role::*; +use kaspa_consensus_core::network::NetworkType; use kaspa_consensus_core::tx::TransactionId; use wasm_bindgen::prelude::*; // use js_sys::Object; use crate::pskt::Inner; use kaspa_consensus_client::{Transaction, TransactionInput, TransactionInputT, TransactionOutput, TransactionOutputT}; use serde::{Deserialize, Serialize}; +use std::str::FromStr; use std::sync::MutexGuard; use std::sync::{Arc, Mutex}; use workflow_wasm::{ @@ -95,7 +97,9 @@ impl TryCastFromJs for PSKT { R: AsRef + 'a, { Self::resolve(value, || { - if let Some(data) = value.as_ref().as_string() { + if JsValue::is_undefined(value.as_ref()) { + Ok(PSKT::from(State::Creator(Native::::default()))) + } else if let Some(data) = value.as_ref().as_string() { let pskt_inner: Inner = serde_json::from_str(&data).map_err(|_| Error::InvalidPayload)?; Ok(PSKT::from(State::NoOp(Some(pskt_inner)))) } else if let Ok(transaction) = Transaction::try_owned_from(value) { @@ -123,7 +127,12 @@ impl PSKT { #[wasm_bindgen(getter, js_name = "payload")] pub fn payload_getter(&self) -> JsValue { let state = self.state(); - serde_wasm_bindgen::to_value(state.as_ref().unwrap()).unwrap() + workflow_wasm::serde::to_value(state.as_ref().unwrap()).unwrap() + } + + pub fn serialize(&self) -> String { + let state = self.state(); + serde_json::to_string(state.as_ref().unwrap()).unwrap() } fn state(&self) -> MutexGuard> { @@ -233,7 +242,7 @@ impl PSKT { pub fn fallback_lock_time(&self, lock_time: u64) -> Result { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.fallback_lock_time(lock_time)), - state => Err(Error::state(state))?, + _ => Err(Error::expected_state("Creator"))?, }; self.replace(state) @@ -243,7 +252,7 @@ impl PSKT { pub fn inputs_modifiable(&self) -> Result { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.inputs_modifiable()), - state => Err(Error::state(state))?, + _ => Err(Error::expected_state("Creator"))?, }; self.replace(state) @@ -253,7 +262,7 @@ impl PSKT { pub fn outputs_modifiable(&self) -> Result { let state = match self.take() { State::Creator(pskt) => State::Creator(pskt.outputs_modifiable()), - state => Err(Error::state(state))?, + _ => Err(Error::expected_state("Creator"))?, }; self.replace(state) @@ -263,7 +272,7 @@ impl PSKT { pub fn no_more_inputs(&self) -> Result { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_inputs()), - state => Err(Error::state(state))?, + _ => Err(Error::expected_state("Constructor"))?, }; self.replace(state) @@ -273,7 +282,27 @@ impl PSKT { pub fn no_more_outputs(&self) -> Result { let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.no_more_outputs()), - state => Err(Error::state(state))?, + _ => Err(Error::expected_state("Constructor"))?, + }; + + self.replace(state) + } + + #[wasm_bindgen(js_name = inputAndRedeemScript)] + pub fn input_with_redeem(&self, input: &TransactionInputT, data: &JsValue) -> Result { + let obj = js_sys::Object::from(data.clone()); + + let input = TransactionInput::try_owned_from(input)?; + let mut input: Input = input.try_into()?; + let redeem_script = js_sys::Reflect::get(&obj, &"redeemScript".into()) + .expect("Missing redeemscript field") + .as_string() + .expect("redeemscript must be a string"); + input.redeem_script = + Some(hex::decode(redeem_script).map_err(|e| Error::custom(format!("Redeem script is not a hex string: {}", e)))?); + let state = match self.take() { + State::Constructor(pskt) => State::Constructor(pskt.input(input)), + _ => Err(Error::expected_state("Constructor"))?, }; self.replace(state) @@ -283,7 +312,7 @@ impl PSKT { let input = TransactionInput::try_owned_from(input)?; let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.input(input.try_into()?)), - state => Err(Error::state(state))?, + _ => Err(Error::expected_state("Constructor"))?, }; self.replace(state) @@ -293,7 +322,7 @@ impl PSKT { let output = TransactionOutput::try_owned_from(output)?; let state = match self.take() { State::Constructor(pskt) => State::Constructor(pskt.output(output.try_into()?)), - state => Err(Error::state(state))?, + _ => Err(Error::expected_state("Constructor"))?, }; self.replace(state) @@ -303,7 +332,7 @@ impl PSKT { pub fn set_sequence(&self, n: u64, input_index: usize) -> Result { let state = match self.take() { State::Updater(pskt) => State::Updater(pskt.set_sequence(n, input_index)?), - state => Err(Error::state(state))?, + _ => Err(Error::expected_state("Updater"))?, }; self.replace(state) @@ -314,7 +343,45 @@ impl PSKT { let state = self.state(); match state.as_ref().unwrap() { State::Signer(pskt) => Ok(pskt.calculate_id()), - state => Err(Error::state(state))?, + _ => Err(Error::expected_state("Signer"))?, } } + + #[wasm_bindgen(js_name = calculateMass)] + pub fn calculate_mass(&self, data: &JsValue) -> Result { + let obj = js_sys::Object::from(data.clone()); + let network_id = js_sys::Reflect::get(&obj, &"networkId".into()) + .map_err(|_| Error::custom("networkId is missing"))? + .as_string() + .ok_or_else(|| Error::custom("networkId must be a string"))?; + + let network_id = NetworkType::from_str(&network_id).map_err(|e| Error::custom(format!("Invalid networkId: {}", e)))?; + + let cloned_pskt = self.clone(); + + let extractor = { + let finalizer = cloned_pskt.finalizer()?; + + let finalizer_state = finalizer.state().clone().unwrap(); + + match finalizer_state { + State::Finalizer(pskt) => { + for input in pskt.inputs.iter() { + if input.redeem_script.is_some() { + return Err(Error::custom("Mass calculation is not supported for inputs with redeem scripts")); + } + } + let pskt = pskt + .finalize_sync(|inner: &Inner| -> Result>> { Ok(vec![vec![0u8, 65]; inner.inputs.len()]) }) + .map_err(|e| Error::custom(format!("Failed to finalize PSKT: {e}")))?; + pskt.extractor()? + } + _ => panic!("Finalizer state is not valid"), + } + }; + let tx = extractor + .extract_tx_unchecked(&network_id.into()) + .map_err(|e| Error::custom(format!("Failed to extract transaction: {e}")))?; + Ok(tx.tx.mass()) + } } diff --git a/wallet/pskt/src/wasm/utils.rs b/wallet/pskt/src/wasm/utils.rs new file mode 100644 index 0000000000..93d9dfc32a --- /dev/null +++ b/wallet/pskt/src/wasm/utils.rs @@ -0,0 +1,39 @@ +use kaspa_consensus_core::constants::*; +use kaspa_consensus_core::network::NetworkType; +use separator::{separated_float, separated_int, separated_uint_with_output, Separatable}; + +#[inline] +pub fn sompi_to_kaspa(sompi: u64) -> f64 { + sompi as f64 / SOMPI_PER_KASPA as f64 +} + +#[inline] +pub fn kaspa_to_sompi(kaspa: f64) -> u64 { + (kaspa * SOMPI_PER_KASPA as f64) as u64 +} + +#[inline] +pub fn sompi_to_kaspa_string(sompi: u64) -> String { + sompi_to_kaspa(sompi).separated_string() +} + +#[inline] +pub fn sompi_to_kaspa_string_with_trailing_zeroes(sompi: u64) -> String { + separated_float!(format!("{:.8}", sompi_to_kaspa(sompi))) +} + +pub fn kaspa_suffix(network_type: &NetworkType) -> &'static str { + match network_type { + NetworkType::Mainnet => "KAS", + NetworkType::Testnet => "TKAS", + NetworkType::Simnet => "SKAS", + NetworkType::Devnet => "DKAS", + } +} + +#[inline] +pub fn sompi_to_kaspa_string_with_suffix(sompi: u64, network_type: &NetworkType) -> String { + let kas = sompi_to_kaspa_string(sompi); + let suffix = kaspa_suffix(network_type); + format!("{kas} {suffix}") +} diff --git a/wasm/core/src/types.rs b/wasm/core/src/types.rs index 7e8e29335e..ee5d066bbc 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/examples/nodejs/javascript/wallet/wallet-from-secretkey.js b/wasm/examples/nodejs/javascript/wallet/wallet-from-secretkey.js new file mode 100644 index 0000000000..728144e1af --- /dev/null +++ b/wasm/examples/nodejs/javascript/wallet/wallet-from-secretkey.js @@ -0,0 +1,294 @@ +// @ts-ignore +globalThis.WebSocket = require('websocket').w3cwebsocket; // W3C WebSocket module shim + + +const path = require('path'); +const fs = require('fs'); +const kaspa = require('../../../../nodejs/kaspa-dev'); +const { + Wallet, setDefaultStorageFolder, + AccountKind, Mnemonic, Resolver, + kaspaToSompi, + sompiToKaspaString, + Address +} = kaspa; + +let storageFolder = path.join(__dirname, '../../../data/wallets').normalize(); +if (!fs.existsSync(storageFolder)) { + fs.mkdirSync(storageFolder); +} + +setDefaultStorageFolder(storageFolder); + +(async()=>{ + //const filename = "wallet-394"; + const filename = "wallet-396-9"; + // const FILE_TX = path.join(storageFolder, filename+"-transactions.json"); + + // let txs = {}; + // if (fs.existsSync(FILE_TX)){ + // txs = JSON.parse(fs.readFileSync(FILE_TX)+"") + // } + + const balance = {}; + //const transactions = {}; + let wallet; + + const chalk = new ((await import('chalk')).Chalk)(); + + function log_title(title){ + console.log(chalk.bold(chalk.green(`\n\n${title}`))) + } + // function saveTransactions(){ + // Object.keys(transactions).forEach(id=>{ + // txs[id] = [...transactions[id].entries()]; + // }) + // fs.writeFileSync(FILE_TX, JSON.stringify(txs)) + // } + + async function log_transactions(accountId){ + + // saveTransactions(); + function value(tx){ + if (tx.data.type == "change"){ + return tx.data.data.changeValue + } + if (["external", "incoming"].includes(tx.data.type)){ + return tx.data.data.value + } + if (["transfer-outgoing", "transfer-incoming", "outgoing", "batch"].includes(tx.data.type)){ + return tx.data.data.paymentValue?? tx.data.data.changeValue + } + + } + let transactionsResult = await wallet.transactionsDataGet({ + accountId, + networkId: "testnet-11", + start:0, + end:20 + }) + //console.log("transactions", transactionsResult.transactions) + let list = []; + transactionsResult.transactions.forEach((tx)=>{ + // console.log("ID:", id); + // console.log("type:", tx.data.type, ", value:", value(tx)); + // console.log(chalk.dim("----------------------------")) + // let addresses = tx.data.data.utxoEntries.map(utxo=>{ + // return utxo.address.substring(0, 5)+"..." + // }); + list.push({ + Id: tx.id, + Type: tx.data.type, + Value: sompiToKaspaString(value(tx)||0) + }); + //console.log("tx.data", tx.id, tx.data) + }); + + log_title("Transactions") + console.table(list) + console.log(""); + + } + + try { + + const walletSecret = "abc"; + wallet = new Wallet({resident: false, networkId: "testnet-11", resolver: new Resolver()}); + //console.log("wallet", wallet) + // Ensure wallet file + if (!await wallet.exists(filename)){ + let response = await wallet.walletCreate({ + walletSecret, + filename, + title: "W-1" + }); + console.log("walletCreate : response", response) + } + + wallet.addEventListener(({type, data})=>{ + + switch (type){ + case "maturity": + case "pending": + case "discovery": + //console.log("transactions[data.binding.id]", data.binding.id, transactions[data.binding.id], transactions) + // console.log("record.hasAddress :receive:", data.hasAddress(firstAccount.receiveAddress)); + // console.log("record.hasAddress :change:", data.hasAddress(firstAccount.changeAddress)); + console.log("record.data", type, data) + // console.log("record.blockDaaScore", data.blockDaaScore) + if (data.type != "change"){ + //transactions[data.binding.id].set(data.id+"", data.serialize()); + log_transactions(data.binding.id) + } + break; + case "reorg": + //transactions[data.binding.id].delete(data.id+""); + console.log("reorg data", data) + //log_transactions(data.binding.id) + break; + case "balance": + balance[data.id] = data.balance; + + let list = []; + Object.keys(balance).map(id=>{ + let b = balance[id]; + list.push({ + Account: id.substring(0, 5)+"...", + Mature: sompiToKaspaString(b.mature), + Pending: sompiToKaspaString(b.pending), + Outgoing: sompiToKaspaString(b.outgoing), + MatureUtxo: b.matureUtxoCount, + PendingUtxo: b.pendingUtxoCount, + StasisUtxo: b.stasisUtxoCount + }) + }) + log_title("Balance"); + console.table(list) + console.log(""); + break; + case "daa-score-change": + if (data.currentDaaScore%1000 == 0){ + console.log(`[EVENT] ${type}:`, data.currentDaaScore) + } + break; + case "server-status": + case "utxo-proc-start": + case "sync-state": + case "account-activation": + case "utxo-proc-stop": + case "connect": + case "stasis": + // + break; + default: + console.log(`[EVENT] ${type}:`, data) + + } + }) + + // Open wallet + await wallet.walletOpen({ + walletSecret, + filename, + accountDescriptors: false + }); + + let prvKeyData = await wallet.prvKeyDataCreate({ + walletSecret, + kind: "secretKey", + secretKey: "34f8cca12da8f6367ce4e1af3a998af3c67a50d91bafb68079f48785f1eef87f" + }); + + //console.log("prvKeyData", prvKeyData) + + let account = await wallet.accountsCreate({ + walletSecret, + type:"kaspa-keypair-standard", + accountName:"Account-B", + prvKeyDataId: prvKeyData.prvKeyDataId, + }); + + // console.log("account", account) + // return; + + // // Ensure default account + // await wallet.accountsEnsureDefault({ + // walletSecret, + // type: new AccountKind("bip32") // "bip32" + // }); + + // // Create a new account + // // create private key + // let prvKeyData = await wallet.prvKeyDataCreate({ + // walletSecret, + // mnemonic: Mnemonic.random(24).phrase + // }); + + // //console.log("prvKeyData", prvKeyData); + + // let account = await wallet.accountsCreate({ + // walletSecret, + // type:"bip32", + // accountName:"Account-B", + // prvKeyDataId: prvKeyData.prvKeyDataId + // }); + + // console.log("new account:", account); + + // Connect to rpc + await wallet.connect(); + + // Start wallet processing + await wallet.start(); + let accounts; + let firstAccount = {}; + async function listAccount(){ + // List accounts + accounts = await wallet.accountsEnumerate({}); + firstAccount = accounts.accountDescriptors[0]; + + //console.log("firstAccount:", firstAccount); + + // Activate Account + await wallet.accountsActivate({ + accountIds:[firstAccount.accountId] + }); + + log_title("Accounts"); + accounts.accountDescriptors.forEach(a=>{ + console.log(`Account: ${a.accountId}`); + console.log(` Account type: ${a.kind.toString()}`); + console.log(` Account Name: ${a.accountName}`); + console.log(` Receive Address: ${a.receiveAddress}`); + console.log(` Change Address: ${a.changeAddress}`); + console.log("") + }); + } + + await listAccount(); + + // // Account sweep/compound transactions + // let sweepResult = await wallet.accountsSend({ + // walletSecret, + // accountId: firstAccount.accountId + // }); + // console.log("sweepResult", sweepResult) + + // Send kaspa to address + let sendResult = await wallet.accountsSend({ + walletSecret, + // @ts-ignore + accountId: firstAccount.accountId, + priorityFeeSompi: kaspaToSompi("0.001"), + destination:[{ + // @ts-ignore + address: firstAccount.changeAddress, + amount: kaspaToSompi("1.567") + }] + }); + console.log("sendResult", sendResult); + + // @ts-ignore + log_transactions(firstAccount.accountId) + + // Transfer kaspa between accounts + let transferResult = await wallet.accountsTransfer({ + walletSecret, + sourceAccountId: firstAccount.accountId, + destinationAccountId: firstAccount.accountId, + transferAmountSompi: kaspaToSompi("2.456"), + }); + console.log("transferResult", transferResult); + + + // await wallet.disconnect() + + // wallet.setNetworkId("mainnet") + // await wallet.connect() + // await listAccount(); + + + } catch(ex) { + console.error("Error:", ex); + } +})(); \ No newline at end of file diff --git a/wasm/examples/nodejs/javascript/wallet/wallet.js b/wasm/examples/nodejs/javascript/wallet/wallet.js index 49dfdbf9cc..64e97b2651 100644 --- a/wasm/examples/nodejs/javascript/wallet/wallet.js +++ b/wasm/examples/nodejs/javascript/wallet/wallet.js @@ -114,7 +114,7 @@ setDefaultStorageFolder(storageFolder); //console.log("transactions[data.binding.id]", data.binding.id, transactions[data.binding.id], transactions) // console.log("record.hasAddress :receive:", data.hasAddress(firstAccount.receiveAddress)); // console.log("record.hasAddress :change:", data.hasAddress(firstAccount.changeAddress)); - // console.log("record.data", data.data) + console.log("record.data", type, data) // console.log("record.blockDaaScore", data.blockDaaScore) if (data.type != "change"){ //transactions[data.binding.id].set(data.id+"", data.serialize()); @@ -123,23 +123,26 @@ setDefaultStorageFolder(storageFolder); break; case "reorg": //transactions[data.binding.id].delete(data.id+""); - log_transactions(data.binding.id) + console.log("reorg data", data) + //log_transactions(data.binding.id) break; case "balance": balance[data.id] = data.balance; - log_title("Balance"); + let list = []; Object.keys(balance).map(id=>{ + let b = balance[id]; list.push({ Account: id.substring(0, 5)+"...", - Mature: sompiToKaspaString(data.balance.mature), - Pending: sompiToKaspaString(data.balance.pending), - Outgoing: sompiToKaspaString(data.balance.outgoing), - MatureUtxo: data.balance.matureUtxoCount, - PendingUtxo: data.balance.pendingUtxoCount, - StasisUtxo: data.balance.stasisUtxoCount + Mature: sompiToKaspaString(b.mature), + Pending: sompiToKaspaString(b.pending), + Outgoing: sompiToKaspaString(b.outgoing), + MatureUtxo: b.matureUtxoCount, + PendingUtxo: b.pendingUtxoCount, + StasisUtxo: b.stasisUtxoCount }) }) + log_title("Balance"); console.table(list) console.log(""); break; @@ -199,27 +202,32 @@ setDefaultStorageFolder(storageFolder); // Start wallet processing await wallet.start(); + let accounts; + let firstAccount = {}; + async function listAccount(){ + // List accounts + accounts = await wallet.accountsEnumerate({}); + firstAccount = accounts.accountDescriptors[0]; - // List accounts - let accounts = await wallet.accountsEnumerate({}); - let firstAccount = accounts.accountDescriptors[0]; + //console.log("firstAccount:", firstAccount); - //console.log("firstAccount:", firstAccount); + // Activate Account + await wallet.accountsActivate({ + accountIds:[firstAccount.accountId] + }); - // Activate Account - await wallet.accountsActivate({ - accountIds:[firstAccount.accountId] - }); + log_title("Accounts"); + accounts.accountDescriptors.forEach(a=>{ + console.log(`Account: ${a.accountId}`); + console.log(` Account type: ${a.kind.toString()}`); + console.log(` Account Name: ${a.accountName}`); + console.log(` Receive Address: ${a.receiveAddress}`); + console.log(` Change Address: ${a.changeAddress}`); + console.log("") + }); + } - log_title("Accounts"); - accounts.accountDescriptors.forEach(a=>{ - console.log(`Account: ${a.accountId}`); - console.log(` Account type: ${a.kind.toString()}`); - console.log(` Account Name: ${a.accountName}`); - console.log(` Receive Address: ${a.receiveAddress}`); - console.log(` Change Address: ${a.changeAddress}`); - console.log("") - }); + await listAccount(); // // Account sweep/compound transactions // let sweepResult = await wallet.accountsSend({ @@ -231,24 +239,36 @@ setDefaultStorageFolder(storageFolder); // Send kaspa to address let sendResult = await wallet.accountsSend({ walletSecret, + // @ts-ignore accountId: firstAccount.accountId, priorityFeeSompi: kaspaToSompi("0.001"), destination:[{ + // @ts-ignore address: firstAccount.changeAddress, - amount: kaspaToSompi("1.5") + amount: kaspaToSompi("1.567") }] }); console.log("sendResult", sendResult); + // @ts-ignore + log_transactions(firstAccount.accountId) + // Transfer kaspa between accounts let transferResult = await wallet.accountsTransfer({ walletSecret, sourceAccountId: firstAccount.accountId, destinationAccountId: firstAccount.accountId, - transferAmountSompi: kaspaToSompi("2.4"), + transferAmountSompi: kaspaToSompi("2.456"), }); console.log("transferResult", transferResult); + + // await wallet.disconnect() + + // wallet.setNetworkId("mainnet") + // await wallet.connect() + // await listAccount(); + } catch(ex) { console.error("Error:", ex); diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index d8b0f06a95..1bdede15a6 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::*; } }