From 63e4863eb2329f253d7a796dec499a0d8789b644 Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Mon, 26 Aug 2024 13:58:00 +0300 Subject: [PATCH 1/4] A few optimizations related to multi-level relations (#527) * parents_builder: optimize the common case for high levels * delete level relations for all levels below the affiliated proof level for this block * keep all multi-level parents of the pruning-point-and-anticone roots set * drop prune guard where possible * minor * practically impossible to reach this level (requires a pow hash which is all zeros), but for the sake of good order it should be this way * comments * fix `get_roots_of_set` for the ascending chain case + test * avoid quadratic roots search + rely on header cache * rollback `get_roots_of_set` --- .../pipeline/pruning_processor/processor.rs | 87 +++++++---- consensus/src/processes/parents_builder.rs | 139 ++++++++++-------- .../src/processes/reachability/tests/mod.rs | 6 + utils/src/sync/semaphore.rs | 2 +- 4 files changed, 141 insertions(+), 93 deletions(-) diff --git a/consensus/src/pipeline/pruning_processor/processor.rs b/consensus/src/pipeline/pruning_processor/processor.rs index 4e86857611..35dc211d51 100644 --- a/consensus/src/pipeline/pruning_processor/processor.rs +++ b/consensus/src/pipeline/pruning_processor/processor.rs @@ -2,7 +2,7 @@ use crate::{ consensus::{ - services::{ConsensusServices, DbGhostdagManager, DbPruningPointManager}, + services::{ConsensusServices, DbGhostdagManager, DbParentsManager, DbPruningPointManager}, storage::ConsensusStorage, }, model::{ @@ -31,7 +31,7 @@ use kaspa_consensus_core::{ muhash::MuHashExtensions, pruning::{PruningPointProof, PruningPointTrustedData}, trusted::ExternalGhostdagData, - BlockHashSet, + BlockHashMap, BlockHashSet, BlockLevel, }; use kaspa_consensusmanager::SessionLock; use kaspa_core::{debug, info, warn}; @@ -42,7 +42,7 @@ use kaspa_utils::iter::IterExtensions; use parking_lot::RwLockUpgradableReadGuard; use rocksdb::WriteBatch; use std::{ - collections::VecDeque, + collections::{hash_map::Entry::Vacant, VecDeque}, ops::Deref, sync::{ atomic::{AtomicBool, Ordering}, @@ -72,6 +72,7 @@ pub struct PruningProcessor { ghostdag_managers: Arc>, pruning_point_manager: DbPruningPointManager, pruning_proof_manager: Arc, + parents_manager: DbParentsManager, // Pruning lock pruning_lock: SessionLock, @@ -109,6 +110,7 @@ impl PruningProcessor { ghostdag_managers: services.ghostdag_managers.clone(), pruning_point_manager: services.pruning_point_manager.clone(), pruning_proof_manager: services.pruning_proof_manager.clone(), + parents_manager: services.parents_manager.clone(), pruning_lock, config, is_consensus_exiting, @@ -262,41 +264,35 @@ impl PruningProcessor { // We keep full data for pruning point and its anticone, relations for DAA/GD // windows and pruning proof, and only headers for past pruning points let keep_blocks: BlockHashSet = data.anticone.iter().copied().collect(); - let keep_relations: BlockHashSet = std::iter::empty() - .chain(data.anticone.iter().copied()) - .chain(data.daa_window_blocks.iter().map(|th| th.header.hash)) - .chain(data.ghostdag_blocks.iter().map(|gd| gd.hash)) - .chain(proof.iter().flatten().map(|h| h.hash)) - .collect(); - let keep_level_zero_relations: BlockHashSet = std::iter::empty() + let mut keep_relations: BlockHashMap = std::iter::empty() .chain(data.anticone.iter().copied()) .chain(data.daa_window_blocks.iter().map(|th| th.header.hash)) .chain(data.ghostdag_blocks.iter().map(|gd| gd.hash)) .chain(proof[0].iter().map(|h| h.hash)) + .map(|h| (h, 0)) // Mark block level 0 for all the above. Note that below we add the remaining levels .collect(); let keep_headers: BlockHashSet = self.past_pruning_points(); info!("Header and Block pruning: waiting for consensus write permissions..."); let mut prune_guard = self.pruning_lock.blocking_write(); - let mut lock_acquire_time = Instant::now(); - let mut reachability_read = self.reachability_store.upgradable_read(); info!("Starting Header and Block pruning..."); { let mut counter = 0; let mut batch = WriteBatch::default(); - for kept in keep_level_zero_relations.iter().copied() { + // At this point keep_relations only holds level-0 relations which is the correct filtering criteria for primary GHOSTDAG + for kept in keep_relations.keys().copied() { let Some(ghostdag) = self.ghostdag_primary_store.get_data(kept).unwrap_option() else { continue; }; - if ghostdag.unordered_mergeset().any(|h| !keep_level_zero_relations.contains(&h)) { + if ghostdag.unordered_mergeset().any(|h| !keep_relations.contains_key(&h)) { let mut mutable_ghostdag: ExternalGhostdagData = ghostdag.as_ref().into(); - mutable_ghostdag.mergeset_blues.retain(|h| keep_level_zero_relations.contains(h)); - mutable_ghostdag.mergeset_reds.retain(|h| keep_level_zero_relations.contains(h)); - mutable_ghostdag.blues_anticone_sizes.retain(|k, _| keep_level_zero_relations.contains(k)); - if !keep_level_zero_relations.contains(&mutable_ghostdag.selected_parent) { + mutable_ghostdag.mergeset_blues.retain(|h| keep_relations.contains_key(h)); + mutable_ghostdag.mergeset_reds.retain(|h| keep_relations.contains_key(h)); + mutable_ghostdag.blues_anticone_sizes.retain(|k, _| keep_relations.contains_key(k)); + if !keep_relations.contains_key(&mutable_ghostdag.selected_parent) { mutable_ghostdag.selected_parent = ORIGIN; } counter += 1; @@ -307,6 +303,45 @@ impl PruningProcessor { info!("Header and Block pruning: updated ghostdag data for {} blocks", counter); } + // No need to hold the prune guard while we continue populating keep_relations + drop(prune_guard); + + // Add additional levels only after filtering GHOSTDAG data via level 0 + for (level, level_proof) in proof.iter().enumerate().skip(1) { + let level = level as BlockLevel; + // We obtain the headers of the pruning point anticone (including the pruning point) + // in order to mark all parents of anticone roots at level as not-to-be-deleted. + // This optimizes multi-level parent validation (see ParentsManager) + // by avoiding the deletion of high-level parents which might still be needed for future + // header validation (avoiding the need for reference blocks; see therein). + // + // Notes: + // + // 1. Normally, such blocks would be part of the proof for this level, but here we address the rare case + // where there are a few such parallel blocks (since the proof only contains the past of the pruning point's + // selected-tip-at-level) + // 2. We refer to the pp anticone as roots even though technically it might contain blocks which are not a pure + // antichain (i.e., some of them are in the past of others). These blocks only add redundant info which would + // be included anyway. + let roots_parents_at_level = data + .anticone + .iter() + .copied() + .map(|hash| self.headers_store.get_header_with_block_level(hash).expect("pruning point anticone is not pruned")) + .filter(|root| level > root.block_level) // If the root itself is at level, there's no need for its level-parents + .flat_map(|root| self.parents_manager.parents_at_level(&root.header, level).iter().copied().collect_vec()); + for hash in level_proof.iter().map(|header| header.hash).chain(roots_parents_at_level) { + if let Vacant(e) = keep_relations.entry(hash) { + // This hash was not added by any lower level -- mark it as affiliated with proof level `level` + e.insert(level); + } + } + } + + prune_guard = self.pruning_lock.blocking_write(); + let mut lock_acquire_time = Instant::now(); + let mut reachability_read = self.reachability_store.upgradable_read(); + { // Start with a batch for pruning body tips and selected chain stores let mut batch = WriteBatch::default(); @@ -394,7 +429,7 @@ impl PruningProcessor { self.acceptance_data_store.delete_batch(&mut batch, current).unwrap(); self.block_transactions_store.delete_batch(&mut batch, current).unwrap(); - if keep_relations.contains(¤t) { + if let Some(&affiliated_proof_level) = keep_relations.get(¤t) { if statuses_write.get(current).unwrap_option().is_some_and(|s| s.is_valid()) { // We set the status to header-only only if it was previously set to a valid // status. This is important since some proof headers might not have their status set @@ -403,17 +438,13 @@ impl PruningProcessor { statuses_write.set_batch(&mut batch, current, StatusHeaderOnly).unwrap(); } - // Delete level-0 relations for blocks which only belong to higher proof levels. - // Note: it is also possible to delete level relations for level x > 0 for any block that only belongs - // to proof levels higher than x, but this requires maintaining such per level usage mapping. - // Since the main motivation of this deletion step is to reduce the - // number of origin's children in level 0, and this is not a bottleneck in any other - // level, we currently chose to only delete level-0 redundant relations. - if !keep_level_zero_relations.contains(¤t) { - let mut staging_level_relations = StagingRelationsStore::new(&mut level_relations_write[0]); + // Delete level-x relations for blocks which only belong to higher-than-x proof levels. + // This preserves the semantic that for each level, relations represent a contiguous DAG area in that level + for lower_level in 0..affiliated_proof_level as usize { + let mut staging_level_relations = StagingRelationsStore::new(&mut level_relations_write[lower_level]); relations::delete_level_relations(MemoryWriter, &mut staging_level_relations, current).unwrap_option(); staging_level_relations.commit(&mut batch).unwrap(); - self.ghostdag_stores[0].delete_batch(&mut batch, current).unwrap_option(); + self.ghostdag_stores[lower_level].delete_batch(&mut batch, current).unwrap_option(); } } else { // Count only blocks which get fully pruned including DAG relations diff --git a/consensus/src/processes/parents_builder.rs b/consensus/src/processes/parents_builder.rs index 474942525f..14df3fcecb 100644 --- a/consensus/src/processes/parents_builder.rs +++ b/consensus/src/processes/parents_builder.rs @@ -53,7 +53,7 @@ impl let mut origin_children_headers = None; let mut parents = Vec::with_capacity(self.max_block_level as usize); - for block_level in 0..self.max_block_level { + for block_level in 0..=self.max_block_level { // Direct parents are guaranteed to be in one another's anticones so add them all to // all the block levels they occupy. let mut level_candidates_to_reference_blocks = direct_parent_headers @@ -91,78 +91,89 @@ impl .collect::>() }; - for (i, parent) in grandparents.into_iter().enumerate() { - let has_reachability_data = self.reachability_service.has_reachability_data(parent); - - // Reference blocks are the blocks that are used in reachability queries to check if - // a candidate is in the future of another candidate. In most cases this is just the - // block itself, but in the case where a block doesn't have reachability data we need - // to use some blocks in its future as reference instead. - // If we make sure to add a parent in the future of the pruning point first, we can - // know that any pruned candidate that is in the past of some blocks in the pruning - // point anticone should be a parent (in the relevant level) of one of - // the origin children in the pruning point anticone. So we can check which - // origin children have this block as parent and use those block as - // reference blocks. - let reference_blocks = if has_reachability_data { - smallvec![parent] - } else { - // Here we explicitly declare the type because otherwise Rust would make it mutable. - let origin_children_headers: &Vec<_> = origin_children_headers.get_or_insert_with(|| { - self.relations_service - .get_children(ORIGIN) - .unwrap() - .read() - .iter() - .copied() - .map(|parent| self.headers_store.get_header(parent).unwrap()) - .collect_vec() - }); - let mut reference_blocks = SmallVec::with_capacity(origin_children_headers.len()); - for child_header in origin_children_headers.iter() { - if self.parents_at_level(child_header, block_level).contains(&parent) { - reference_blocks.push(child_header.hash); + let parents_at_level = if level_candidates_to_reference_blocks.is_empty() && first_parent_marker == grandparents.len() { + // Optimization: this is a common case for high levels where none of the direct parents is on the level + // and all direct parents have the same level parents. The condition captures this case because all grandparents + // will be below the first parent marker and there will be no additional grandparents. Bcs all grandparents come + // from a single, already validated parent, there's no need to run any additional antichain checks and we can return + // this set. + grandparents.into_iter().collect() + } else { + // + // Iterate through grandparents in order to find an antichain + for (i, parent) in grandparents.into_iter().enumerate() { + let has_reachability_data = self.reachability_service.has_reachability_data(parent); + + // Reference blocks are the blocks that are used in reachability queries to check if + // a candidate is in the future of another candidate. In most cases this is just the + // block itself, but in the case where a block doesn't have reachability data we need + // to use some blocks in its future as reference instead. + // If we make sure to add a parent in the future of the pruning point first, we can + // know that any pruned candidate that is in the past of some blocks in the pruning + // point anticone should be a parent (in the relevant level) of one of + // the origin children in the pruning point anticone. So we can check which + // origin children have this block as parent and use those block as + // reference blocks. + let reference_blocks = if has_reachability_data { + smallvec![parent] + } else { + // Here we explicitly declare the type because otherwise Rust would make it mutable. + let origin_children_headers: &Vec<_> = origin_children_headers.get_or_insert_with(|| { + self.relations_service + .get_children(ORIGIN) + .unwrap() + .read() + .iter() + .copied() + .map(|parent| self.headers_store.get_header(parent).unwrap()) + .collect_vec() + }); + let mut reference_blocks = SmallVec::with_capacity(origin_children_headers.len()); + for child_header in origin_children_headers.iter() { + if self.parents_at_level(child_header, block_level).contains(&parent) { + reference_blocks.push(child_header.hash); + } } + reference_blocks + }; + + // Make sure we process and insert all first parent's parents. See comments above. + // Note that as parents of an already validated block, they all form an antichain, + // hence no need for reachability queries yet. + if i < first_parent_marker { + level_candidates_to_reference_blocks.insert(parent, reference_blocks); + continue; } - reference_blocks - }; - - // Make sure we process and insert all first parent's parents. See comments above. - // Note that as parents of an already validated block, they all form an antichain, - // hence no need for reachability queries yet. - if i < first_parent_marker { - level_candidates_to_reference_blocks.insert(parent, reference_blocks); - continue; - } - if !has_reachability_data { - continue; - } + if !has_reachability_data { + continue; + } - let len_before_retain = level_candidates_to_reference_blocks.len(); - level_candidates_to_reference_blocks - .retain(|_, refs| !self.reachability_service.is_any_dag_ancestor(&mut refs.iter().copied(), parent)); - let is_any_candidate_ancestor_of = level_candidates_to_reference_blocks.len() < len_before_retain; - - // We should add the block as a candidate if it's in the future of another candidate - // or in the anticone of all candidates. - if is_any_candidate_ancestor_of - || !level_candidates_to_reference_blocks.iter().any(|(_, candidate_references)| { - self.reachability_service.is_dag_ancestor_of_any(parent, &mut candidate_references.iter().copied()) - }) - { - level_candidates_to_reference_blocks.insert(parent, reference_blocks); + let len_before_retain = level_candidates_to_reference_blocks.len(); + level_candidates_to_reference_blocks + .retain(|_, refs| !self.reachability_service.is_any_dag_ancestor(&mut refs.iter().copied(), parent)); + let is_any_candidate_ancestor_of = level_candidates_to_reference_blocks.len() < len_before_retain; + + // We should add the block as a candidate if it's in the future of another candidate + // or in the anticone of all candidates. + if is_any_candidate_ancestor_of + || !level_candidates_to_reference_blocks.iter().any(|(_, candidate_references)| { + self.reachability_service.is_dag_ancestor_of_any(parent, &mut candidate_references.iter().copied()) + }) + { + level_candidates_to_reference_blocks.insert(parent, reference_blocks); + } } - } - if block_level > 0 - && level_candidates_to_reference_blocks.len() == 1 - && level_candidates_to_reference_blocks.contains_key(&self.genesis_hash) - { + // After processing all grandparents, collect the successful level candidates + level_candidates_to_reference_blocks.keys().copied().collect_vec() + }; + + if block_level > 0 && parents_at_level.as_slice() == std::slice::from_ref(&self.genesis_hash) { break; } - parents.push(level_candidates_to_reference_blocks.keys().copied().collect_vec()); + parents.push(parents_at_level); } parents diff --git a/consensus/src/processes/reachability/tests/mod.rs b/consensus/src/processes/reachability/tests/mod.rs index 67131f7f93..c315a250bd 100644 --- a/consensus/src/processes/reachability/tests/mod.rs +++ b/consensus/src/processes/reachability/tests/mod.rs @@ -105,6 +105,12 @@ impl DagBlock { } } +impl From<(u64, &[u64])> for DagBlock { + fn from(value: (u64, &[u64])) -> Self { + Self::new(value.0.into(), value.1.iter().map(|&i| i.into()).collect()) + } +} + /// A struct with fluent API to streamline DAG building pub struct DagBuilder<'a, T: ReachabilityStore + ?Sized, S: RelationsStore + ChildrenStore + ?Sized> { reachability: &'a mut T, diff --git a/utils/src/sync/semaphore.rs b/utils/src/sync/semaphore.rs index 2ea6dcc031..c0ffec8e20 100644 --- a/utils/src/sync/semaphore.rs +++ b/utils/src/sync/semaphore.rs @@ -41,7 +41,7 @@ mod trace { if log_time + (Duration::from_secs(10).as_micros() as u64) < now { let log_value = self.log_value.load(Ordering::Relaxed); debug!( - "Semaphore: log interval: {:?}, readers time: {:?}, fraction: {:.2}", + "Semaphore: log interval: {:?}, readers time: {:?}, fraction: {:.4}", Duration::from_micros(now - log_time), Duration::from_micros(readers_time - log_value), (readers_time - log_value) as f64 / (now - log_time) as f64 From b0f07eff448f4e34193f3b7ace1a14d4b16b48df Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Mon, 26 Aug 2024 18:05:54 +0300 Subject: [PATCH 2/4] Query all DNS seeders if missing many connections (#530) * refactor into `dns_seed_single` (no logical change yet) * impl dns seed many * rename --- components/connectionmanager/src/lib.rs | 79 +++++++++++++++++-------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/components/connectionmanager/src/lib.rs b/components/connectionmanager/src/lib.rs index 1509df6bfe..2146ec62d1 100644 --- a/components/connectionmanager/src/lib.rs +++ b/components/connectionmanager/src/lib.rs @@ -7,7 +7,7 @@ use std::{ }; use duration_string::DurationString; -use futures_util::future::join_all; +use futures_util::future::{join_all, try_join_all}; use itertools::Itertools; use kaspa_addressmanager::{AddressManager, NetAddress}; use kaspa_core::{debug, info, warn}; @@ -227,12 +227,14 @@ impl ConnectionManager { } if missing_connections > 0 && !self.dns_seeders.is_empty() { - let cmgr = self.clone(); - // DNS lookup is a blocking i/o operation, so we spawn it as a blocking task - let _ = tokio::task::spawn_blocking(move || { - cmgr.dns_seed(missing_connections); //TODO: Consider putting a number higher than `missing_connections`. - }) - .await; + if missing_connections > self.outbound_target / 2 { + // If we are missing more than half of our target, query all in parallel. + // This will always be the case on new node start-up and is the most resilient strategy in such a case. + self.dns_seed_many(self.dns_seeders.len()).await; + } else { + // Try to obtain at least twice the number of missing connections + self.dns_seed_with_address_target(2 * missing_connections).await; + } } } @@ -251,26 +253,17 @@ impl ConnectionManager { join_all(futures).await; } - fn dns_seed(self: &Arc, mut min_addresses_to_fetch: usize) { + /// Queries DNS seeders in random order, one after the other, until obtaining `min_addresses_to_fetch` addresses + async fn dns_seed_with_address_target(self: &Arc, min_addresses_to_fetch: usize) { + let cmgr = self.clone(); + tokio::task::spawn_blocking(move || cmgr.dns_seed_with_address_target_blocking(min_addresses_to_fetch)).await.unwrap(); + } + + fn dns_seed_with_address_target_blocking(self: &Arc, mut min_addresses_to_fetch: usize) { let shuffled_dns_seeders = self.dns_seeders.choose_multiple(&mut thread_rng(), self.dns_seeders.len()); for &seeder in shuffled_dns_seeders { - info!("Querying DNS seeder {}", seeder); - // Since the DNS lookup protocol doesn't come with a port, we must assume that the default port is used. - let addrs = match (seeder, self.default_port).to_socket_addrs() { - Ok(addrs) => addrs, - Err(e) => { - warn!("Error connecting to DNS seeder {}: {}", seeder, e); - continue; - } - }; - - let addrs_len = addrs.len(); - info!("Retrieved {} addresses from DNS seeder {}", addrs_len, seeder); - let mut amgr_lock = self.address_manager.lock(); - for addr in addrs { - amgr_lock.add_address(NetAddress::new(addr.ip().into(), addr.port())); - } - + // Query seeders sequentially until reaching the desired number of addresses + let addrs_len = self.dns_seed_single(seeder); if addrs_len >= min_addresses_to_fetch { break; } else { @@ -279,6 +272,42 @@ impl ConnectionManager { } } + /// Queries `num_seeders_to_query` random DNS seeders in parallel + async fn dns_seed_many(self: &Arc, num_seeders_to_query: usize) -> usize { + info!("Querying {} DNS seeders", num_seeders_to_query); + let shuffled_dns_seeders = self.dns_seeders.choose_multiple(&mut thread_rng(), num_seeders_to_query); + let jobs = shuffled_dns_seeders.map(|seeder| { + let cmgr = self.clone(); + tokio::task::spawn_blocking(move || cmgr.dns_seed_single(seeder)) + }); + try_join_all(jobs).await.unwrap().into_iter().sum() + } + + /// Query a single DNS seeder and add the obtained addresses to the address manager. + /// + /// DNS lookup is a blocking i/o operation so this function is assumed to be called + /// from a blocking execution context. + fn dns_seed_single(self: &Arc, seeder: &str) -> usize { + info!("Querying DNS seeder {}", seeder); + // Since the DNS lookup protocol doesn't come with a port, we must assume that the default port is used. + let addrs = match (seeder, self.default_port).to_socket_addrs() { + Ok(addrs) => addrs, + Err(e) => { + warn!("Error connecting to DNS seeder {}: {}", seeder, e); + return 0; + } + }; + + let addrs_len = addrs.len(); + info!("Retrieved {} addresses from DNS seeder {}", addrs_len, seeder); + let mut amgr_lock = self.address_manager.lock(); + for addr in addrs { + amgr_lock.add_address(NetAddress::new(addr.ip().into(), addr.port())); + } + + addrs_len + } + /// Bans the given IP and disconnects from all the peers with that IP. /// /// _GO-KASPAD: BanByIP_ From 1c1a6927d20042953f0389b1089720e515fe74e5 Mon Sep 17 00:00:00 2001 From: aspect Date: Mon, 26 Aug 2024 22:52:31 +0300 Subject: [PATCH 3/4] Cumulative PR - omega branch (wRPC v2, integrations, typescript-v2, resolver v2) (#506) * support numeric interface (ip) arg without port in --rpclisten-borsh or --rpclisten-json * isActive for UtxoProcessor and UtxoContext * Script utility functions + WASM changelog update * versioned serialization for wRPC * spelling * Refactorize State into PoW (#43) * Add fromRaw with optional target_bits * Upload builds as GitHub Artifact * try moving calculateTarget into PoW class as static funct * Use FromHex trait * Make PoW constructor accept IHeader & refactorize some parts * Lint * TransactionDataT and BindingT (#44) * borsh update to 1.5.1 * fix returning receive addr from bindings change addr fn. * lints * update WASM changelog * fix WASM module reference in examples * migrate lints.clippy to workspace and inherit this in the relevant crates * Add provisional fields for storage metrics (db size) * fix file creation timestamp issue on some ext4 file systems (updated via workflow-store::fs) * bigint values in TransactionRecord (#48) * change address decoding panics to errors * update error messaging * Update for bind/listen compatibility with WRS * range support added for transactions pagination (#49) * range support added for transactions pagination * cargo fmt/clippy * lints * WRS 0.13.0 * wallet ergonomics helper functions * WRS 0.13.1 * WRS 0.13.1 lock * WRS 0.13.2 * kaspa-wallet (cli) updates (using latest resolver APIs + guide cleanup) * range support for indexdb store load_range function (#52) * add balance info to account descriptors, additional wallet connectivity utilities and connect api method changes, export wallet utils in wallet prelude, * Improve ConnectRequest builder interface * Allow UtxoEntryReference to be re-integrated from a flat object structure. * indexdb data maintenance: timestamp fix (#53) * range support for indexdb store load_range function * indexdb data maintenance: timestamp fix * removing unused * Fix incorrect balance when sending to self * fix child_number typo * allow setting custom transaction maturity periods * fix missing renaming for record.value * fix typedoc references * pre-borsh-1-5-1 compat * testing typedoc * testing typedoc * lock typedoc to 0.25.8 * disable typedoc treatWarningsAsErrors * deps * WIP SPK WASM * Cargo.lock * SPK raw object cast + fix return of ISerializableTransaction * fix le to be when parsing SPK hex version * remove string version parsing from SPK * update WASM SDK changelog * incorrect balance issue (#55) * TransactionInput signature_script as Option, add associated TS types * restructure PSKT + WASM scaffolding (WIP) * wallet guard implementation to block concurrent async access to account operations * tx serialization example * change struct serialization version fields from u32 to u16 * WIP - decoupling RPC type aliases (#45) * Provisional RpcConnection propagation via RpcApi methods (#46) * provisional RpcConnection propagation via RpcApi methods * lints * change api version handling affecting get_server_info_call * Wallet watch-only accounts (#59) * Prints extended public keys on export mnemonic command (feature of go kaspawallet). * Watch-only account implementation for bip32 and multisig kind of accounts with new command account import watchonly. * Refactor code for less variables. * Patch import type for command. * CLI Support for watch only accounts in select account with args function. * Function sig_op_count equals implemented. * Helper function in wallet to format XPUB according to network. Converter NetworkId for Prefix. BIP32-Watch Account renamed from WatchOnly, multisig feature removed. Multisig account import as watch-only command added. * cli watch-only header in list and select. * Resolve merge. * update resolver to use v2 API * change default resolver subdomains * resolver v2 updates * Refactorize some addresses and Script related parts (#61) * Refactorize some addresses and Script related parts * A ScriptBuilder example on TypeScript * addOps Opcodes are now BinaryT * Move txscript bindings to relevant folders * Sort lines of deps and imports * Lint * fix wasm subscribe_daa_score * expose native RpcClient multiplexer in KaspaRpcClient * WIP (local wRPC) * breakdown wRPC serialization trait into two ser/de; improve future compatibility patterns; * get_connections_call() RPC method + provisional metrics dictionary * change get_connections() to return the actual value instead of Response struct. * priorityEntries implementation for tx generator * fix transaction WASM interface types affecting some function returns * input and output types on transactions (WASM) * rpc caps * update client resolver resolution * GetSystemInfo RPC call * make priorityEntries optional in the TS interface definition * merge cli-resolver * remove resolver crate from workspace (move to https://github.com/aspectron/kaspa-resolver) * WRS 0.14.0 * fix merge mishap * refactor systeminfo + update resolver target generation * Custom input signatureScript ability (#69) * Refactorize some addresses and Script related parts * A ScriptBuilder example on TypeScript * addOps Opcodes are now BinaryT * Move txscript bindings to relevant folders * Sort lines of deps and imports * Lint * Prototype of custom sighash operations * Experimental * Add SighashType enum and option on SignInput * Format and a small fix * Clippy * hex view for ScriptBuilder (#67) * add git_hash to system_id * add git_hash to system_id * wip * wip * refactor git head fetch to use build.rs * comment * split utils/sysinfo into utils/git, refactor utils/build.rs to run git to obtain hashes (in addition to file check) * using WalletGuard, account guard (#73) 1) private context issue on legacy accounts 2) optional url option handling on rpc client connect method 3) using `WalletGuard` type instead of `AsyncMutexGuard` * add short hash to sysinfo, return short hash in GetSystemInfo * add contributor DNS seeders (gerri and H@H) * Update phrase.rs (#74) * Base implementation for PSKB and usage in core account with generator wrapper (#64) * Base implementation for PSKB and usage in core account with generator wrapper stream handling. * prune test file * prune test file * Converters for transanction and populated transaction. * Optional signature import in PSKT conversion. * PSKB wallet cli commands. * More PSKB wallet cli commands for custom script locks. * Serialization test case * Reviews patches, cli script debug command added. * Doc about fee per transaction for script unlocking UTXOS * Parameter changed to priority_fee_sompi_per_transaction, revert function renaming. * Error handling * fix missing RPC refs * Update resolver config (WIP) * add version to GetSystemInfoResponse * fix git version handling * update client-side resolver properties to match current structs * update resolvers * fix kaspa-utils/build.rs to always produce git related env vars. * add git commit hash to WASM32 SDK artifacts during CI build * fix WASM32 CI build (testing) * fix the default url handling in wRPC client * Key attributes (make XPrv and XPub inspectable) (#77) * getters for XPrv and XPub attributes * fmt * post merge fixes * Merge RBF (#80) * Replace by fee on mempool (#499) * Replace by fee on mempool with tests * Add a custom RemovalReason -- ReplacedByFee * Let `MinerManager` handle replace by fee (RBF) for RPC and P2P * Refines success conditions and process of RBF policies * Add an RPC `submit_transaction_replacement` method * fix fmt * Fix CLI Build WASM32 error * Avoid breaking wRPC * Extend some tests coverage to all priority, orphan & RBF policy combinations * Let RBF fail early or at least before checking transaction scripts in consensus * Cleaning * More cleaning * Use contextual instead of compute mass in RBF eviction rule * Avoid collision with another PR * Avoid collision with another PR (2) * Extended test coverage of RBF * Extended test coverage of RBF (2) * Rename `TransactionBatchValidationArgs` to `TransactionValidationBatchArgs` * Add comments * Assert instead of condition * Add an `RbfPolicy` parameter to mining manager tx validate_and_insert_... fns * Infer RBF policy from unorphaned transaction * Apply the RBF policy to all orphan-related cases * In Rbf allowed mode, check feerate threshold vs all double spends (i.e., compare to the max) * Minor hashset optimization * Rename: fee_per_mass -> feerate * Use rbf policy arg for post processing step as well * Renames and comments * Relaxation: fail gracefully if rbf replaced tx is missing (also fixes an edge case where mempool duplication resulted in this scenario) * Tx id is appended by the caller anyway --------- Co-authored-by: Tiram <18632023+tiram88@users.noreply.github.com> Co-authored-by: Michael Sutton * post merge fixes --------- Co-authored-by: KaffinPX <73744616+KaffinPX@users.noreply.github.com> Co-authored-by: Tiram <18632023+tiram88@users.noreply.github.com> Co-authored-by: Michael Sutton * createInputSignature() utility function (#79) * ``signTransactionInput`` and move sign_input to its proper location * Fix typedoc warnings left from old PR * createInputSignature * Update docs for ConsensusParams (WASM mass calc) * fmt * bump wRPC * wrs 0.15.0 * replace Uuid.as_ref() to as_bytes() * assign RpcApiOps variants numerical values * cleanup * Remove WASM32 mass calculator + change createTransaction() signature (#81) * Kip9 updates to WASM/wallet framework mass calc (#66) * WIP * update kip9 processing in WASM mass calculator * XPrv.toPrivateKey support * replace lazy_static with OnceLock * remove NetworkParams Inner * make signatureScript optional on ITransactionInput (WASM32) * WIP mass calc (WASM32) * remove WASM32 mass calc, replace with dedicated functions * use OnceCell for NetworkParams (wallet-core) * Update changelog * fmt --------- Co-authored-by: Surinder Singh Matoo * change OnceCell to LazyLock in wallet-core utxo settings * WASM: update signTransaction() signature * fix TS types and method names * lints * split GetConnections counter into separate clients and peers variables * fix missing version in GetConnections method * Adding type conversion. (#76) * fmt * refactor kaspa-metrics to expose some internal methods (needed for external processing). * Word count (#83) * Update phrase.rs * private context issue for importing legacy wallet * account filter updated for calculating account_index * gen1 decrypt_mnemonic updated for error unwraping * adding resolver tls option * cleanup * Improve input signature capability (#85) * ``signTransactionInput`` and move sign_input to its proper location * Fix typedoc warnings left from old PR * createInputSignature * Fix createInputSignature and improve PendingTransaction Inputs DX * Format * A small Omega change applied to existing code * Pass reference of transaction in createInputSignature * fix WASM32 PSKT function names * refactor PSKB as a type wrapper + update serialization (#86) * Cleanup of unused JSON test file for PSKB and comments (#87) * Remove PSKB json test file. * Remove/change old PSKB comments and commented out inclusions. * PSKB+PSKT merge with omega branch (#82) * TransactionInput signature_script as Option, add associated TS types * restructure PSKT + WASM scaffolding (WIP) * Base implementation for PSKB and usage in core account with generator wrapper (#64) * Base implementation for PSKB and usage in core account with generator wrapper stream handling. * prune test file * prune test file * Converters for transanction and populated transaction. * Optional signature import in PSKT conversion. * PSKB wallet cli commands. * More PSKB wallet cli commands for custom script locks. * Serialization test case * Reviews patches, cli script debug command added. * Doc about fee per transaction for script unlocking UTXOS * Parameter changed to priority_fee_sompi_per_transaction, revert function renaming. * Error handling * Adding type conversion. (#76) * fmt * fix WASM32 PSKT function names * refactor PSKB as a type wrapper + update serialization (#86) * Cleanup of unused JSON test file for PSKB and comments (#87) * Remove PSKB json test file. * Remove/change old PSKB comments and commented out inclusions. --------- Co-authored-by: 1bananagirl <168954040+1bananagirl@users.noreply.github.com> * extra new line char removed (#89) * lock issue on wallet creation (#91) * wasm cast refs * resolver updates * CLI review: import cli watch-only changed to watch, PSKB parse view added (#92) * CLI - Import commands for watch-only accounts changed to: account watch bip32 and account watch multisig. * CLI - PSKB parse view added next to debug view showing input/output addresses and amounts. PSKT finalized check moved from debug view to parse view. Selected account requirement in commands only if needed. * WASM RBF (RPC) * WASM FeeEstimate (RPC) * hex encoding for kaspa_utils::SystemInfo * Some symmetry and type fixes (#93) * UtxoEntry typing fix and isometry w UtxoEntryReference * Fix type of IUtxoProcessorEvent * Fix interface typings of UtxoProcessor * Remove UtxoProcessorEventData and improve UtxoProcessor event * Remove unneeded overwrite * Clippy and a small mistake fix * Note: Clippy can cause fmt issues :nerd: * update WASM GeneratorSettings::priorityEntries? to accept UtxoEntryReference[] * WASM32 - update resolver casting * cleanup * WASM32: remove no longer used WAPI account module * cleanup * introduce new rpc header/block types for BBT and SB (#95) * introduce new rpc header/block types for BBT and SB * remove unneeded clone * WASM - Update types for Mnemonic::random() * WASM update deprecated methods in web-sys * Add bip32 Mnemonic class to kaspa-wasm-sdk-keys build package * misc dependency updates * Introduce profile data to GetConnections RPC method * WASM update TS declarations for wallet events * fix WASM sdk submitTransaction API (#96) * Add custom Debug to GetSystemInfoResponse * Add HexString type to ITransactionOutput::scriptPublicKey * fix camelCase on RpcTransactionOutpoint --------- Co-authored-by: KaffinPX <73744616+KaffinPX@users.noreply.github.com> Co-authored-by: surinder singh Co-authored-by: 1bananagirl <168954040+1bananagirl@users.noreply.github.com> Co-authored-by: Tiram <18632023+tiram88@users.noreply.github.com> Co-authored-by: Michael Sutton Co-authored-by: IgorKhomenko <70977170+IgorKhomenko@users.noreply.github.com> --- .github/workflows/ci.yaml | 23 +- Cargo.lock | 1988 +++++++------- Cargo.toml | 79 +- README.md | 2 - cli/Cargo.toml | 6 +- cli/src/cli.rs | 73 +- cli/src/error.rs | 12 + cli/src/extensions/transaction.rs | 29 +- cli/src/imports.rs | 2 +- cli/src/modules/account.rs | 39 +- cli/src/modules/connect.rs | 5 +- cli/src/modules/details.rs | 12 + cli/src/modules/export.rs | 43 +- cli/src/modules/guide.txt | 46 +- cli/src/modules/history.rs | 14 +- cli/src/modules/mod.rs | 3 +- cli/src/modules/pskb.rs | 266 ++ cli/src/modules/reload.rs | 6 +- cli/src/modules/rpc.rs | 78 +- cli/src/modules/send.rs | 2 +- cli/src/modules/settings.rs | 7 +- cli/src/modules/wallet.rs | 9 +- cli/src/wizards/account.rs | 59 + cli/src/wizards/wallet.rs | 25 +- consensus/client/Cargo.toml | 4 +- consensus/client/src/header.rs | 11 +- consensus/client/src/input.rs | 44 +- consensus/client/src/lib.rs | 20 +- consensus/client/src/outpoint.rs | 9 + consensus/client/src/output.rs | 49 +- consensus/client/src/serializable/mod.rs | 8 +- consensus/client/src/serializable/numeric.rs | 19 +- consensus/client/src/serializable/string.rs | 6 +- consensus/client/src/sign.rs | 14 +- consensus/client/src/transaction.rs | 107 +- consensus/client/src/utils.rs | 81 + consensus/client/src/utxo.rs | 70 +- consensus/client/src/vtx.rs | 35 - consensus/core/Cargo.toml | 5 +- consensus/core/src/api/stats.rs | 24 +- consensus/core/src/config/params.rs | 12 + consensus/core/src/hashing/mod.rs | 2 + consensus/core/src/hashing/wasm.rs | 27 + consensus/core/src/header.rs | 6 + consensus/core/src/mass/mod.rs | 11 + consensus/core/src/network.rs | 7 +- consensus/core/src/sign.rs | 19 +- consensus/core/src/tx.rs | 13 +- consensus/core/src/tx/script_public_key.rs | 37 +- consensus/pow/Cargo.toml | 5 +- consensus/pow/src/wasm.rs | 76 +- consensus/src/processes/mass.rs | 12 +- consensus/wasm/Cargo.toml | 4 +- crypto/addresses/Cargo.toml | 4 +- crypto/addresses/src/bech32.rs | 7 +- crypto/addresses/src/lib.rs | 26 +- crypto/hashes/src/lib.rs | 7 +- crypto/txscript/Cargo.toml | 11 + crypto/txscript/src/error.rs | 89 + crypto/txscript/src/lib.rs | 4 + crypto/txscript/src/result.rs | 1 + crypto/txscript/src/script_builder.rs | 13 +- crypto/txscript/src/script_class.rs | 1 + crypto/txscript/src/wasm/builder.rs | 179 ++ crypto/txscript/src/wasm/mod.rs | 15 + .../txscript/src/wasm/opcodes.rs | 241 +- kaspad/src/daemon.rs | 7 +- metrics/core/src/data.rs | 223 +- metrics/core/src/error.rs | 3 + metrics/core/src/lib.rs | 181 +- notify/Cargo.toml | 1 + notify/src/scope.rs | 146 + notify/src/subscription/mod.rs | 1 + protocol/p2p/src/echo.rs | 2 +- rothschild/src/main.rs | 22 +- rpc/core/Cargo.toml | 6 +- rpc/core/src/api/connection.rs | 7 + rpc/core/src/api/mod.rs | 1 + rpc/core/src/api/notifications.rs | 98 +- rpc/core/src/api/ops.rs | 167 +- rpc/core/src/api/rpc.rs | 272 +- rpc/core/src/convert/block.rs | 47 +- rpc/core/src/convert/tx.rs | 22 +- rpc/core/src/convert/utxo.rs | 6 +- rpc/core/src/model/address.rs | 42 +- rpc/core/src/model/block.rs | 105 +- rpc/core/src/model/feerate_estimate.rs | 58 +- rpc/core/src/model/header.rs | 332 ++- rpc/core/src/model/mempool.rs | 40 +- rpc/core/src/model/message.rs | 2415 ++++++++++++++++- rpc/core/src/model/mod.rs | 1 + rpc/core/src/model/tests.rs | 1325 +++++++++ rpc/core/src/model/tx.rs | 275 +- rpc/core/src/wasm/convert.rs | 5 +- rpc/core/src/wasm/message.rs | 311 ++- rpc/grpc/client/src/lib.rs | 2 + rpc/grpc/client/src/route.rs | 4 +- rpc/grpc/core/proto/messages.proto | 8 +- rpc/grpc/core/proto/rpc.proto | 49 +- rpc/grpc/core/src/convert/block.rs | 19 + rpc/grpc/core/src/convert/header.rs | 85 +- rpc/grpc/core/src/convert/kaspad.rs | 4 + rpc/grpc/core/src/convert/message.rs | 75 +- rpc/grpc/core/src/convert/metrics.rs | 24 + rpc/grpc/core/src/ops.rs | 2 + .../server/src/request_handler/factory.rs | 2 + rpc/grpc/server/src/tests/rpc_core_mock.rs | 168 +- rpc/macros/src/grpc/server.rs | 5 +- rpc/macros/src/lib.rs | 6 + rpc/macros/src/wrpc/client.rs | 11 +- rpc/macros/src/wrpc/mod.rs | 1 + rpc/macros/src/wrpc/server.rs | 10 +- rpc/macros/src/wrpc/test.rs | 60 + rpc/macros/src/wrpc/wasm.rs | 4 +- rpc/service/Cargo.toml | 2 +- rpc/service/src/converter/consensus.rs | 2 +- rpc/service/src/service.rs | 217 +- rpc/wrpc/client/Cargo.toml | 3 +- rpc/wrpc/client/Resolvers.toml | 46 +- rpc/wrpc/client/src/client.rs | 53 +- rpc/wrpc/client/src/node.rs | 8 +- rpc/wrpc/client/src/resolver.rs | 166 +- rpc/wrpc/proxy/src/main.rs | 4 +- rpc/wrpc/resolver/Cargo.toml | 41 - rpc/wrpc/resolver/src/args.rs | 54 - rpc/wrpc/resolver/src/connection.rs | 262 -- rpc/wrpc/resolver/src/error.rs | 53 - rpc/wrpc/resolver/src/imports.rs | 28 - rpc/wrpc/resolver/src/log.rs | 44 - rpc/wrpc/resolver/src/main.rs | 41 - rpc/wrpc/resolver/src/monitor.rs | 241 -- rpc/wrpc/resolver/src/node.rs | 75 - rpc/wrpc/resolver/src/panic.rs | 10 - rpc/wrpc/resolver/src/params.rs | 146 - rpc/wrpc/resolver/src/result.rs | 1 - rpc/wrpc/resolver/src/server.rs | 149 - rpc/wrpc/resolver/src/transport.rs | 8 - rpc/wrpc/server/Cargo.toml | 1 + rpc/wrpc/server/src/address.rs | 1 - rpc/wrpc/server/src/connection.rs | 3 +- rpc/wrpc/server/src/router.rs | 17 +- rpc/wrpc/server/src/service.rs | 14 +- rpc/wrpc/wasm/Cargo.toml | 4 +- rpc/wrpc/wasm/src/client.rs | 27 +- rpc/wrpc/wasm/src/resolver.rs | 35 +- testing/integration/src/common/utils.rs | 10 +- .../src/daemon_integration_tests.rs | 7 +- testing/integration/src/rpc_tests.rs | 250 +- testing/integration/src/tasks/block/miner.rs | 10 +- .../integration/src/tasks/block/submitter.rs | 10 +- utils/Cargo.toml | 7 + utils/build.rs | 82 + utils/src/git.rs | 53 + utils/src/lib.rs | 5 + utils/src/networking.rs | 32 +- utils/src/sysinfo.rs | 81 + wallet/bip32/Cargo.toml | 3 +- wallet/bip32/src/mnemonic/phrase.rs | 6 +- wallet/bip32/src/prefix.rs | 13 + wallet/bip32/src/xpublic_key.rs | 12 +- wallet/core/Cargo.toml | 5 +- wallet/core/src/account/descriptor.rs | 21 +- wallet/core/src/account/kind.rs | 28 +- wallet/core/src/account/mod.rs | 81 +- wallet/core/src/account/pskb.rs | 360 +++ wallet/core/src/account/variants/bip32.rs | 15 +- .../core/src/account/variants/bip32watch.rs | 252 ++ wallet/core/src/account/variants/keypair.rs | 13 +- wallet/core/src/account/variants/legacy.rs | 5 +- wallet/core/src/account/variants/mod.rs | 2 + wallet/core/src/account/variants/multisig.rs | 31 +- wallet/core/src/account/variants/resident.rs | 1 + wallet/core/src/account/variants/watchonly.rs | 300 ++ wallet/core/src/api/message.rs | 41 + wallet/core/src/api/traits.rs | 8 +- wallet/core/src/api/transport.rs | 2 +- wallet/core/src/compat/gen1.rs | 8 +- wallet/core/src/derivation.rs | 18 +- wallet/core/src/deterministic.rs | 35 +- wallet/core/src/encryption.rs | 2 +- wallet/core/src/error.rs | 33 +- wallet/core/src/factory.rs | 1 + wallet/core/src/imports.rs | 2 +- wallet/core/src/lib.rs | 5 + wallet/core/src/prelude.rs | 5 + wallet/core/src/serializer.rs | 4 +- wallet/core/src/storage/account.rs | 28 +- wallet/core/src/storage/binding.rs | 39 + wallet/core/src/storage/keydata/data.rs | 9 +- wallet/core/src/storage/local/interface.rs | 2 +- wallet/core/src/storage/local/payload.rs | 12 +- .../src/storage/local/transaction/fsio.rs | 7 +- .../src/storage/local/transaction/indexdb.rs | 171 +- wallet/core/src/storage/local/wallet.rs | 18 +- wallet/core/src/storage/metadata.rs | 8 +- wallet/core/src/storage/mod.rs | 2 +- wallet/core/src/storage/transaction/data.rs | 100 +- wallet/core/src/storage/transaction/record.rs | 61 +- wallet/core/src/tests/rpc_core_mock.rs | 168 +- wallet/core/src/tests/storage.rs | 4 +- wallet/core/src/tx/generator/generator.rs | 62 +- wallet/core/src/tx/generator/pending.rs | 48 +- wallet/core/src/tx/generator/settings.rs | 7 + wallet/core/src/tx/generator/test.rs | 14 +- wallet/core/src/tx/mass.rs | 123 +- wallet/core/src/tx/payment.rs | 16 +- wallet/core/src/utxo/balance.rs | 1 + wallet/core/src/utxo/context.rs | 59 +- wallet/core/src/utxo/processor.rs | 66 +- wallet/core/src/utxo/reference.rs | 6 +- wallet/core/src/utxo/settings.rs | 133 +- wallet/core/src/wallet/api.rs | 125 +- wallet/core/src/wallet/args.rs | 15 + wallet/core/src/wallet/mod.rs | 173 +- wallet/core/src/wasm/api/message.rs | 19 +- wallet/core/src/wasm/cryptobox.rs | 16 +- wallet/core/src/wasm/message.rs | 8 +- wallet/core/src/wasm/notify.rs | 141 +- wallet/core/src/wasm/signer.rs | 39 +- wallet/core/src/wasm/tx/consensus.rs | 36 - wallet/core/src/wasm/tx/fees.rs | 2 +- .../core/src/wasm/tx/generator/generator.rs | 24 +- wallet/core/src/wasm/tx/generator/pending.rs | 50 +- wallet/core/src/wasm/tx/mass.rs | 170 +- wallet/core/src/wasm/tx/mod.rs | 2 - wallet/core/src/wasm/tx/utils.rs | 38 +- wallet/core/src/wasm/utxo/context.rs | 14 +- wallet/core/src/wasm/utxo/processor.rs | 53 +- wallet/core/src/wasm/wallet/account.rs | 155 -- wallet/core/src/wasm/wallet/mod.rs | 1 - wallet/keys/Cargo.toml | 4 +- wallet/keys/src/derivation/gen0/hd.rs | 2 +- wallet/keys/src/derivation_path.rs | 7 +- wallet/keys/src/imports.rs | 2 +- wallet/keys/src/keypair.rs | 5 +- wallet/keys/src/privatekey.rs | 7 +- wallet/keys/src/pubkeygen.rs | 4 +- wallet/keys/src/publickey.rs | 17 +- wallet/keys/src/xprv.rs | 71 +- wallet/keys/src/xpub.rs | 79 +- wallet/macros/src/wallet/client.rs | 2 +- wallet/macros/src/wallet/server.rs | 2 +- wallet/pskt/Cargo.toml | 11 +- wallet/pskt/examples/multisig.rs | 4 +- wallet/pskt/src/bundle.rs | 353 +++ wallet/pskt/src/convert.rs | 109 + wallet/pskt/src/error.rs | 55 +- wallet/pskt/src/global.rs | 5 +- wallet/pskt/src/input.rs | 9 +- wallet/pskt/src/lib.rs | 484 +--- wallet/pskt/src/output.rs | 3 +- wallet/pskt/src/pskt.rs | 472 ++++ wallet/pskt/src/wasm/bundle.rs | 1 + wallet/pskt/src/wasm/error.rs | 64 + wallet/pskt/src/wasm/input.rs | 1 + wallet/pskt/src/wasm/mod.rs | 6 + wallet/pskt/src/wasm/output.rs | 1 + wallet/pskt/src/wasm/pskt.rs | 320 +++ wallet/pskt/src/wasm/result.rs | 1 + wasm/CHANGELOG.md | 29 + wasm/Cargo.toml | 4 + wasm/build-node-dev | 3 +- wasm/core/Cargo.toml | 6 +- wasm/core/src/hex.rs | 152 ++ wasm/core/src/lib.rs | 3 +- wasm/core/src/types.rs | 2 +- .../nodejs/javascript/general/derivation.js | 17 +- .../{mining-state.js => mining-pow.js} | 8 +- .../javascript/transactions/serialize.js | 48 + .../transactions/simple-transaction.js | 2 +- .../transactions/single-transaction-demo.js | 8 +- .../nodejs/typescript/src/scriptBuilder.ts | 13 + wasm/src/lib.rs | 3 + 273 files changed, 14059 insertions(+), 5023 deletions(-) create mode 100644 cli/src/modules/pskb.rs create mode 100644 consensus/client/src/utils.rs delete mode 100644 consensus/client/src/vtx.rs create mode 100644 consensus/core/src/hashing/wasm.rs create mode 100644 crypto/txscript/src/error.rs create mode 100644 crypto/txscript/src/result.rs create mode 100644 crypto/txscript/src/wasm/builder.rs create mode 100644 crypto/txscript/src/wasm/mod.rs rename consensus/client/src/script.rs => crypto/txscript/src/wasm/opcodes.rs (51%) create mode 100644 rpc/core/src/api/connection.rs create mode 100644 rpc/core/src/model/tests.rs create mode 100644 rpc/macros/src/wrpc/test.rs delete mode 100644 rpc/wrpc/resolver/Cargo.toml delete mode 100644 rpc/wrpc/resolver/src/args.rs delete mode 100644 rpc/wrpc/resolver/src/connection.rs delete mode 100644 rpc/wrpc/resolver/src/error.rs delete mode 100644 rpc/wrpc/resolver/src/imports.rs delete mode 100644 rpc/wrpc/resolver/src/log.rs delete mode 100644 rpc/wrpc/resolver/src/main.rs delete mode 100644 rpc/wrpc/resolver/src/monitor.rs delete mode 100644 rpc/wrpc/resolver/src/node.rs delete mode 100644 rpc/wrpc/resolver/src/panic.rs delete mode 100644 rpc/wrpc/resolver/src/params.rs delete mode 100644 rpc/wrpc/resolver/src/result.rs delete mode 100644 rpc/wrpc/resolver/src/server.rs delete mode 100644 rpc/wrpc/resolver/src/transport.rs create mode 100644 utils/build.rs create mode 100644 utils/src/git.rs create mode 100644 utils/src/sysinfo.rs create mode 100644 wallet/core/src/account/pskb.rs create mode 100644 wallet/core/src/account/variants/bip32watch.rs create mode 100644 wallet/core/src/account/variants/watchonly.rs delete mode 100644 wallet/core/src/wasm/tx/consensus.rs delete mode 100644 wallet/core/src/wasm/wallet/account.rs create mode 100644 wallet/pskt/src/bundle.rs create mode 100644 wallet/pskt/src/convert.rs create mode 100644 wallet/pskt/src/pskt.rs create mode 100644 wallet/pskt/src/wasm/bundle.rs create mode 100644 wallet/pskt/src/wasm/error.rs create mode 100644 wallet/pskt/src/wasm/input.rs create mode 100644 wallet/pskt/src/wasm/mod.rs create mode 100644 wallet/pskt/src/wasm/output.rs create mode 100644 wallet/pskt/src/wasm/pskt.rs create mode 100644 wallet/pskt/src/wasm/result.rs create mode 100644 wasm/core/src/hex.rs rename wasm/examples/nodejs/javascript/general/{mining-state.js => mining-pow.js} (90%) create mode 100644 wasm/examples/nodejs/javascript/transactions/serialize.js create mode 100644 wasm/examples/nodejs/typescript/src/scriptBuilder.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 43e21e237b..27fe1376a0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -211,7 +211,7 @@ jobs: check-wasm32: - name: Check Wasm32 + name: Check WASM32 runs-on: ubuntu-latest steps: - name: Checkout sources @@ -274,12 +274,16 @@ jobs: run: cargo clippy -p kaspa-wasm --target wasm32-unknown-unknown build-wasm32: - name: Build Wasm32 + name: Build WASM32 SDK runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v4 + - name: Setup Environment + shell: bash + run: echo "SHORT_SHA=`git rev-parse --short HEAD`" >> $GITHUB_ENV + - name: Install Protoc uses: arduino/setup-protoc@v3 with: @@ -337,8 +341,19 @@ jobs: key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Build wasm release - run: cd wasm && bash build-release - + run: | + pushd . + cd wasm + bash build-release + popd + mv wasm/release/kaspa-wasm32-sdk.zip wasm/release/kaspa-wasm32-sdk-${{ env.SHORT_SHA }}.zip + + - name: Upload WASM build to GitHub + uses: actions/upload-artifact@v4 + with: + name: kaspa-wasm32-sdk-${{ env.SHORT_SHA }}.zip + path: wasm/release/kaspa-wasm32-sdk-${{ env.SHORT_SHA }}.zip + build-release: name: Build Ubuntu Release runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index bf353479fe..f72fdc655f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,21 +4,21 @@ version = 3 [[package]] name = "accessory" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "850bb534b9dc04744fbbb71d30ad6d25a7e4cf6dc33e223c81ef3a92ebab4e0b" +checksum = "87537f9ae7cfa78d5b8ebd1a1db25959f5e737126be4d8eb44a5452fc4b63cde" dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -29,6 +29,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.5.2" @@ -50,17 +56,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.14", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.11" @@ -68,7 +63,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if 1.0.0", - "getrandom 0.2.14", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -126,9 +121,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -141,33 +136,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -175,9 +170,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "approx" @@ -208,15 +203,15 @@ dependencies = [ [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-attributes" @@ -241,22 +236,21 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.2.1" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener 5.3.0", - "event-listener-strategy 0.5.2", + "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" dependencies = [ "async-task", "concurrent-queue", @@ -271,10 +265,10 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-executor", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.4", + "async-lock 3.4.0", "blocking", "futures-lite 2.3.0", "once_cell", @@ -302,21 +296,21 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.2" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" dependencies = [ - "async-lock 3.3.0", + "async-lock 3.4.0", "cfg-if 1.0.0", "concurrent-queue", "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.7.0", + "polling 3.7.3", "rustix 0.38.34", "slab", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -330,12 +324,12 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", + "event-listener 5.3.1", + "event-listener-strategy", "pin-project-lite", ] @@ -385,7 +379,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -396,22 +390,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", -] - -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", + "syn 2.0.75", ] [[package]] @@ -455,13 +440,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core 0.3.4", + "axum-core", "bitflags 1.3.2", "bytes", "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.30", "itoa", "matchit", "memchr", @@ -476,40 +461,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "axum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" -dependencies = [ - "async-trait", - "axum-core 0.4.3", - "bytes", - "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "http-body-util", - "hyper 1.3.1", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper 1.0.1", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "axum-core" version = "0.3.4" @@ -527,38 +478,17 @@ dependencies = [ "tower-service", ] -[[package]] -name = "axum-core" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 0.1.2", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -615,23 +545,22 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.65.1" +version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "cexpr", "clang-sys", + "itertools 0.12.1", "lazy_static", "lazycell", - "peeking_take_while", - "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -642,9 +571,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "blake2" @@ -677,12 +606,11 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ - "async-channel 2.2.1", - "async-lock 3.3.0", + "async-channel 2.3.1", "async-task", "futures-io", "futures-lite 2.3.0", @@ -691,47 +619,26 @@ dependencies = [ [[package]] name = "borsh" -version = "0.9.3" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" dependencies = [ "borsh-derive", - "hashbrown 0.11.2", + "cfg_aliases 0.2.1", ] [[package]] name = "borsh-derive" -version = "0.9.3" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", + "once_cell", "proc-macro-crate", - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "borsh-derive-internal" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" -dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.75", + "syn_derive", ] [[package]] @@ -758,9 +665,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "bzip2-sys" @@ -775,9 +682,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.6" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] @@ -813,13 +720,13 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.96" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -858,6 +765,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -906,7 +819,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -949,9 +862,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -975,9 +888,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", "clap_derive", @@ -985,9 +898,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", @@ -997,27 +910,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "concurrent-queue" @@ -1090,24 +1003,24 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if 1.0.0", ] @@ -1121,7 +1034,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.4", + "clap 4.5.16", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1146,17 +1059,11 @@ dependencies = [ "itertools 0.10.5", ] -[[package]] -name = "critical-section" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" - [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -1182,9 +1089,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -1192,10 +1099,10 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", @@ -1261,25 +1168,24 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.4" +version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" dependencies = [ - "nix", - "windows-sys 0.52.0", + "nix 0.29.0", + "windows-sys 0.59.0", ] [[package]] name = "curve25519-dalek" -version = "4.1.2" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if 1.0.0", "cpufeatures", "curve25519-dalek-derive", "fiat-crypto", - "platforms", "rustc_version", "subtle", "zeroize", @@ -1293,14 +1199,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "darling" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -1308,36 +1214,37 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.60", + "strsim 0.11.1", + "syn 2.0.75", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "dashmap" -version = "5.5.3" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" dependencies = [ "cfg-if 1.0.0", + "crossbeam-utils", "hashbrown 0.14.5", "lock_api", "once_cell", @@ -1359,7 +1266,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -1401,7 +1308,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -1411,20 +1318,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", - "syn 1.0.109", + "syn 2.0.75", ] [[package]] @@ -1435,9 +1342,9 @@ checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" [[package]] name = "deunicode" -version = "1.4.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" [[package]] name = "dhat" @@ -1499,17 +1406,29 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "duct" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c" +dependencies = [ + "libc", + "once_cell", + "os_pipe", + "shared_child", +] + [[package]] name = "duration-string" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcc1d9ae294a15ed05aeae8e11ee5f2b3fe971c077d45a42fb20825fba6ee13" +checksum = "2334658684d7c213e18602aa72ce37e94d1c9b535882ef6e30bc444b7514a1ee" [[package]] name = "either" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" @@ -1528,13 +1447,13 @@ dependencies = [ [[package]] name = "enum-primitive-derive" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c375b9c5eadb68d0a6efee2999fef292f45854c3444c86f09d8ab086ba942b0e" +checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c" dependencies = [ "num-traits", "quote", - "syn 1.0.109", + "syn 2.0.75", ] [[package]] @@ -1558,9 +1477,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1574,43 +1493,22 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] -[[package]] -name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - [[package]] name = "event-listener-strategy" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "event-listener 5.3.0", + "event-listener 5.3.1", "pin-project-lite", ] @@ -1625,22 +1523,16 @@ dependencies = [ [[package]] name = "fancy_constructor" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71f317e4af73b2f8f608fac190c52eac4b1879d2145df1db2fe48881ca69435" +checksum = "07b19d0e43eae2bfbafe4931b5e79c73fb1a849ca15cd41a761a7b8587f9a1a2" dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] -[[package]] -name = "faster-hex" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e2ce894d53b295cf97b05685aa077950ff3e8541af83217fc720a6437169f8" - [[package]] name = "faster-hex" version = "0.9.0" @@ -1667,20 +1559,20 @@ checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fiat-crypto" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.4.1", - "windows-sys 0.52.0", + "libredox", + "windows-sys 0.59.0", ] [[package]] @@ -1691,21 +1583,21 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fixedstr" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4e4dfef7b590ab7d11e531d602fdfb6a3413b09924db1428902bbc4410a9a8" +checksum = "54e049f021908beff8f8c430a99f5c136d3be69f1667346e581f446b173bc012" dependencies = [ "serde", ] [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -1822,7 +1714,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -1888,9 +1780,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -1901,9 +1793,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -1935,7 +1827,26 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.4.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.4.0", "slab", "tokio", "tokio-util", @@ -1954,22 +1865,13 @@ dependencies = [ [[package]] name = "hash32" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ "byteorder", ] -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash 0.7.8", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -1982,19 +1884,16 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash 0.8.11", + "ahash", ] [[package]] name = "heapless" -version = "0.7.17" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ - "atomic-polyfill", "hash32", - "rustc_version", - "spin", "stable_deref_trait", ] @@ -2019,6 +1918,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -2097,9 +2002,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -2107,14 +2012,14 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -2126,9 +2031,9 @@ checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -2144,15 +2049,15 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2168,21 +2073,39 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.6", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "httparse", - "httpdate", "itoa", "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.4.1", + "hyper-util", + "rustls 0.23.12", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", ] [[package]] @@ -2191,7 +2114,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.28", + "hyper 0.14.30", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2199,31 +2122,38 @@ dependencies = [ [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper 0.14.28", + "http-body-util", + "hyper 1.4.1", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" dependencies = [ "bytes", + "futures-channel", "futures-util", "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", + "http-body 1.0.1", + "hyper 1.4.1", "pin-project-lite", "socket2 0.5.7", "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -2237,7 +2167,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -2276,7 +2206,7 @@ dependencies = [ "bytes", "futures", "http 0.2.12", - "hyper 0.14.28", + "hyper 0.14.30", "log", "rand 0.8.5", "tokio", @@ -2286,16 +2216,16 @@ dependencies = [ [[package]] name = "indexed_db_futures" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cc2083760572ee02385ab8b7c02c20925d2dd1f97a1a25a8737a238608f1152" +checksum = "43315957678a70eb21fb0d2384fe86dde0d6c859a01e24ce127eb65a0143d28c" dependencies = [ "accessory", "cfg-if 1.0.0", "delegate-display", "fancy_constructor", "js-sys", - "uuid 1.6.1", + "uuid 1.10.0", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2314,9 +2244,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -2334,9 +2264,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -2386,20 +2316,20 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" @@ -2429,25 +2359,34 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.11" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -2475,7 +2414,7 @@ version = "0.14.1" dependencies = [ "borsh", "igd-next", - "itertools 0.11.0", + "itertools 0.13.0", "kaspa-consensus-core", "kaspa-core", "kaspa-database", @@ -2505,10 +2444,11 @@ version = "0.14.1" dependencies = [ "borsh", "bs58", - "faster-hex 0.6.1", - "getrandom 0.2.14", + "faster-hex", + "getrandom 0.2.15", "hmac", "js-sys", + "kaspa-consensus-core", "kaspa-utils", "once_cell", "pbkdf2", @@ -2535,8 +2475,9 @@ dependencies = [ "convert_case 0.6.0", "dashmap", "downcast", - "faster-hex 0.6.1", + "faster-hex", "futures", + "hex", "js-sys", "kaspa-addresses", "kaspa-bip32", @@ -2548,6 +2489,7 @@ dependencies = [ "kaspa-utils", "kaspa-wallet-core", "kaspa-wallet-keys", + "kaspa-wallet-pskt", "kaspa-wrpc-client", "nw-sys", "pad", @@ -2576,7 +2518,7 @@ version = "0.14.1" dependencies = [ "duration-string", "futures-util", - "itertools 0.11.0", + "itertools 0.13.0", "kaspa-addressmanager", "kaspa-core", "kaspa-p2p-lib", @@ -2592,15 +2534,15 @@ name = "kaspa-consensus" version = "0.14.1" dependencies = [ "arc-swap", - "async-channel 2.2.1", + "async-channel 2.3.1", "bincode", "criterion", "crossbeam-channel", - "faster-hex 0.6.1", + "faster-hex", "flate2", "futures-util", - "indexmap 2.2.6", - "itertools 0.11.0", + "indexmap 2.4.0", + "itertools 0.13.0", "kaspa-consensus-core", "kaspa-consensus-notify", "kaspa-consensusmanager", @@ -2634,11 +2576,11 @@ dependencies = [ name = "kaspa-consensus-client" version = "0.14.1" dependencies = [ - "ahash 0.8.11", + "ahash", "cfg-if 1.0.0", - "faster-hex 0.6.1", + "faster-hex", "hex", - "itertools 0.11.0", + "itertools 0.13.0", "js-sys", "kaspa-addresses", "kaspa-consensus-core", @@ -2667,10 +2609,10 @@ dependencies = [ "borsh", "cfg-if 1.0.0", "criterion", - "faster-hex 0.6.1", + "faster-hex", "futures-util", - "getrandom 0.2.14", - "itertools 0.11.0", + "getrandom 0.2.15", + "itertools 0.13.0", "js-sys", "kaspa-addresses", "kaspa-core", @@ -2692,6 +2634,7 @@ dependencies = [ "web-sys", "workflow-core", "workflow-log", + "workflow-serializer", "workflow-wasm", ] @@ -2699,7 +2642,7 @@ dependencies = [ name = "kaspa-consensus-notify" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "cfg-if 1.0.0", "derive_more", "futures", @@ -2719,7 +2662,7 @@ name = "kaspa-consensus-wasm" version = "0.14.1" dependencies = [ "cfg-if 1.0.0", - "faster-hex 0.6.1", + "faster-hex", "js-sys", "kaspa-addresses", "kaspa-consensus-client", @@ -2745,7 +2688,7 @@ dependencies = [ "duration-string", "futures", "futures-util", - "itertools 0.11.0", + "itertools 0.13.0", "kaspa-consensus-core", "kaspa-consensus-notify", "kaspa-core", @@ -2802,9 +2745,9 @@ version = "0.14.1" dependencies = [ "bincode", "enum-primitive-derive", - "faster-hex 0.6.1", - "indexmap 2.2.6", - "itertools 0.11.0", + "faster-hex", + "indexmap 2.4.0", + "itertools 0.13.0", "kaspa-hashes", "kaspa-utils", "num-traits", @@ -2822,14 +2765,14 @@ dependencies = [ name = "kaspa-grpc-client" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-stream", "async-trait", - "faster-hex 0.6.1", + "faster-hex", "futures", "futures-util", - "h2", - "itertools 0.11.0", + "h2 0.4.6", + "itertools 0.13.0", "kaspa-core", "kaspa-grpc-core", "kaspa-notify", @@ -2853,12 +2796,12 @@ dependencies = [ name = "kaspa-grpc-core" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-stream", "async-trait", - "faster-hex 0.6.1", + "faster-hex", "futures", - "h2", + "h2 0.4.6", "kaspa-consensus-core", "kaspa-core", "kaspa-notify", @@ -2882,13 +2825,13 @@ dependencies = [ name = "kaspa-grpc-server" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-stream", "async-trait", - "faster-hex 0.6.1", + "faster-hex", "futures", - "h2", - "itertools 0.11.0", + "h2 0.4.6", + "itertools 0.13.0", "kaspa-consensus-core", "kaspa-core", "kaspa-grpc-client", @@ -2910,7 +2853,7 @@ dependencies = [ "tokio-stream", "tonic", "triggered", - "uuid 1.6.1", + "uuid 1.10.0", ] [[package]] @@ -2921,7 +2864,7 @@ dependencies = [ "borsh", "cc", "criterion", - "faster-hex 0.6.1", + "faster-hex", "js-sys", "kaspa-utils", "keccak", @@ -2938,7 +2881,7 @@ dependencies = [ name = "kaspa-index-core" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-trait", "derive_more", "futures", @@ -2957,7 +2900,7 @@ dependencies = [ name = "kaspa-index-processor" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-trait", "derive_more", "futures", @@ -2987,7 +2930,7 @@ version = "0.14.1" dependencies = [ "borsh", "criterion", - "faster-hex 0.6.1", + "faster-hex", "js-sys", "kaspa-utils", "malachite-base", @@ -3031,7 +2974,7 @@ version = "0.14.1" dependencies = [ "criterion", "futures-util", - "itertools 0.11.0", + "itertools 0.13.0", "kaspa-addresses", "kaspa-consensus-core", "kaspa-consensusmanager", @@ -3077,15 +3020,15 @@ dependencies = [ name = "kaspa-notify" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-trait", "borsh", "criterion", "derive_more", "futures", "futures-util", - "indexmap 2.2.6", - "itertools 0.11.0", + "indexmap 2.4.0", + "itertools 0.13.0", "kaspa-addresses", "kaspa-alloc", "kaspa-consensus-core", @@ -3106,6 +3049,7 @@ dependencies = [ "workflow-core", "workflow-log", "workflow-perf-monitor", + "workflow-serializer", ] [[package]] @@ -3115,8 +3059,8 @@ dependencies = [ "async-trait", "chrono", "futures", - "indexmap 2.2.6", - "itertools 0.11.0", + "indexmap 2.4.0", + "itertools 0.13.0", "kaspa-addressmanager", "kaspa-connectionmanager", "kaspa-consensus-core", @@ -3136,7 +3080,7 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", - "uuid 1.6.1", + "uuid 1.10.0", ] [[package]] @@ -3146,9 +3090,9 @@ dependencies = [ "borsh", "ctrlc", "futures", - "h2", + "h2 0.4.6", "hex", - "itertools 0.11.0", + "itertools 0.13.0", "kaspa-consensus-core", "kaspa-core", "kaspa-hashes", @@ -3167,7 +3111,7 @@ dependencies = [ "tokio-stream", "tonic", "tonic-build", - "uuid 1.6.1", + "uuid 1.10.0", ] [[package]] @@ -3198,47 +3142,17 @@ dependencies = [ "workflow-wasm", ] -[[package]] -name = "kaspa-resolver" -version = "0.14.1" -dependencies = [ - "ahash 0.8.11", - "axum 0.7.5", - "cfg-if 1.0.0", - "clap 4.5.4", - "console", - "convert_case 0.6.0", - "futures", - "kaspa-consensus-core", - "kaspa-rpc-core", - "kaspa-utils", - "kaspa-wrpc-client", - "mime", - "serde", - "serde_json", - "thiserror", - "tokio", - "toml 0.8.12", - "tower", - "tower-http 0.5.2", - "tracing-subscriber", - "workflow-core", - "workflow-http", - "workflow-log", - "xxhash-rust", -] - [[package]] name = "kaspa-rpc-core" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-trait", "borsh", "cfg-if 1.0.0", "derive_more", "downcast", - "faster-hex 0.6.1", + "faster-hex", "hex", "js-sys", "kaspa-addresses", @@ -3257,14 +3171,16 @@ dependencies = [ "kaspa-utils", "log", "paste", + "rand 0.8.5", "serde", "serde-wasm-bindgen", "serde_json", "smallvec", "thiserror", - "uuid 1.6.1", + "uuid 1.10.0", "wasm-bindgen", "workflow-core", + "workflow-serializer", "workflow-wasm", ] @@ -3313,19 +3229,19 @@ dependencies = [ name = "kaspa-testing-integration" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-trait", "bincode", "chrono", - "clap 4.5.4", + "clap 4.5.16", "criterion", "crossbeam-channel", "dhat", - "faster-hex 0.6.1", + "faster-hex", "flate2", "futures-util", - "indexmap 2.2.6", - "itertools 0.11.0", + "indexmap 2.4.0", + "itertools 0.13.0", "kaspa-addresses", "kaspa-alloc", "kaspa-bip32", @@ -3375,24 +3291,30 @@ version = "0.14.1" dependencies = [ "blake2b_simd", "borsh", + "cfg-if 1.0.0", "criterion", "hex", - "indexmap 2.2.6", - "itertools 0.11.0", + "hexplay", + "indexmap 2.4.0", + "itertools 0.13.0", "kaspa-addresses", "kaspa-consensus-core", "kaspa-hashes", "kaspa-txscript-errors", + "kaspa-utils", + "kaspa-wasm-core", "log", "parking_lot", "rand 0.8.5", "secp256k1", "serde", + "serde-wasm-bindgen", "serde_json", "sha2", "smallvec", "thiserror", "wasm-bindgen", + "workflow-wasm", ] [[package]] @@ -3408,29 +3330,34 @@ name = "kaspa-utils" version = "0.14.1" dependencies = [ "arc-swap", - "async-channel 2.2.1", + "async-channel 2.3.1", "async-trait", "bincode", "borsh", "cfg-if 1.0.0", "criterion", + "duct", "event-listener 2.5.3", - "faster-hex 0.6.1", + "faster-hex", "futures-util", "ipnet", - "itertools 0.11.0", + "itertools 0.13.0", "log", + "mac_address", + "num_cpus", "once_cell", "parking_lot", "rand 0.8.5", "rlimit", "serde", "serde_json", + "sha2", "smallvec", + "sysinfo", "thiserror", "tokio", "triggered", - "uuid 1.6.1", + "uuid 1.10.0", "wasm-bindgen", ] @@ -3440,12 +3367,12 @@ version = "0.14.1" dependencies = [ "cfg-if 1.0.0", "futures", - "hyper 0.14.28", + "hyper 0.14.30", "log", "pin-project-lite", "tokio", "tower", - "tower-http 0.4.4", + "tower-http", ] [[package]] @@ -3500,12 +3427,12 @@ name = "kaspa-wallet-core" version = "0.14.1" dependencies = [ "aes", - "ahash 0.8.11", + "ahash", "argon2", - "async-channel 2.2.1", + "async-channel 2.3.1", "async-std", "async-trait", - "base64 0.21.7", + "base64 0.22.1", "borsh", "cfb-mode", "cfg-if 1.0.0", @@ -3516,7 +3443,7 @@ dependencies = [ "derivative", "downcast", "evpkdf", - "faster-hex 0.6.1", + "faster-hex", "fixedstr", "futures", "heapless", @@ -3524,7 +3451,7 @@ dependencies = [ "hmac", "home", "indexed_db_futures", - "itertools 0.11.0", + "itertools 0.13.0", "js-sys", "kaspa-addresses", "kaspa-bip32", @@ -3541,6 +3468,7 @@ dependencies = [ "kaspa-utils", "kaspa-wallet-keys", "kaspa-wallet-macros", + "kaspa-wallet-pskt", "kaspa-wasm-core", "kaspa-wrpc-client", "kaspa-wrpc-wasm", @@ -3582,7 +3510,7 @@ dependencies = [ "async-trait", "borsh", "downcast", - "faster-hex 0.6.1", + "faster-hex", "hmac", "js-sys", "kaspa-addresses", @@ -3625,7 +3553,12 @@ dependencies = [ name = "kaspa-wallet-pskt" version = "0.14.1" dependencies = [ + "bincode", "derive_builder", + "futures", + "hex", + "js-sys", + "kaspa-addresses", "kaspa-bip32", "kaspa-consensus-client", "kaspa-consensus-core", @@ -3635,9 +3568,12 @@ dependencies = [ "secp256k1", "serde", "serde-value", + "serde-wasm-bindgen", "serde_json", "serde_repr", "thiserror", + "wasm-bindgen", + "workflow-wasm", ] [[package]] @@ -3647,12 +3583,14 @@ dependencies = [ "cfg-if 1.0.0", "js-sys", "kaspa-addresses", + "kaspa-bip32", "kaspa-consensus-core", "kaspa-consensus-wasm", "kaspa-core", "kaspa-math", "kaspa-pow", "kaspa-rpc-core", + "kaspa-txscript", "kaspa-utils", "kaspa-wallet-core", "kaspa-wallet-keys", @@ -3670,9 +3608,11 @@ dependencies = [ name = "kaspa-wasm-core" version = "0.14.1" dependencies = [ - "faster-hex 0.6.1", + "faster-hex", + "hexplay", "js-sys", "wasm-bindgen", + "workflow-wasm", ] [[package]] @@ -3698,7 +3638,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "thiserror", - "toml 0.8.12", + "toml", "wasm-bindgen", "wasm-bindgen-futures", "workflow-core", @@ -3706,6 +3646,7 @@ dependencies = [ "workflow-http", "workflow-log", "workflow-rpc", + "workflow-serializer", "workflow-wasm", ] @@ -3729,7 +3670,7 @@ name = "kaspa-wrpc-proxy" version = "0.14.1" dependencies = [ "async-trait", - "clap 4.5.4", + "clap 4.5.16", "kaspa-consensus-core", "kaspa-grpc-client", "kaspa-rpc-core", @@ -3768,13 +3709,14 @@ dependencies = [ "workflow-core", "workflow-log", "workflow-rpc", + "workflow-serializer", ] [[package]] name = "kaspa-wrpc-wasm" version = "0.14.1" dependencies = [ - "ahash 0.8.11", + "ahash", "async-std", "cfg-if 1.0.0", "futures", @@ -3803,9 +3745,9 @@ dependencies = [ name = "kaspad" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "cfg-if 1.0.0", - "clap 4.5.4", + "clap 4.5.16", "dhat", "dirs", "futures-util", @@ -3841,7 +3783,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", - "toml 0.8.12", + "toml", "workflow-log", ] @@ -3865,9 +3807,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" @@ -3877,18 +3819,18 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.154" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if 1.0.0", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -3913,17 +3855,18 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", + "redox_syscall", ] [[package]] name = "librocksdb-sys" -version = "0.11.0+8.1.1" +version = "0.16.0+8.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +checksum = "ce3d60bc059831dc1c83903fb45c103f75db65c5a7bf22272764d9cc683e348c" dependencies = [ - "bindgen 0.65.1", + "bindgen 0.69.4", "bzip2-sys", "cc", "glob", @@ -3935,9 +3878,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "fdc53a7799a7496ebc9fd29f31f7df80e83c9bda5299768af5f9e59eeea74647" dependencies = [ "cc", "pkg-config", @@ -3972,15 +3915,15 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "local-ip-address" -version = "0.5.7" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612ed4ea9ce5acfb5d26339302528a5e1e59dfed95e9e11af3c083236ff1d15d" +checksum = "136ef34e18462b17bf39a7826f8f3bbc223341f8e83822beb8b77db9a3d49696" dependencies = [ "libc", "neli", @@ -4000,9 +3943,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" dependencies = [ "serde", "value-bag", @@ -4045,14 +3988,24 @@ dependencies = [ [[package]] name = "lz4-sys" -version = "1.9.4" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +checksum = "109de74d5d2353660401699a4174a4ff23fcc649caf553df71933c7fb45ad868" dependencies = [ "cc", "libc", ] +[[package]] +name = "mac_address" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8836fae9d0d4be2c8b4efcdd79e828a2faa058a90d005abf42f91cac5493a08e" +dependencies = [ + "nix 0.28.0", + "winapi", +] + [[package]] name = "mach" version = "0.3.2" @@ -4082,7 +4035,7 @@ dependencies = [ "cfg-if 1.0.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -4093,7 +4046,7 @@ checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -4106,14 +4059,14 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "malachite-base" -version = "0.4.7" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d073a3d1e4e037975af5ef176a2632672e25e8ddbe8e1811745c2e0726b6ad94" +checksum = "e5f8d7930df6fcb9c86761ca0999ba484d7b6469c81cee4a7d38da5386440f96" dependencies = [ "hashbrown 0.14.5", "itertools 0.11.0", @@ -4123,9 +4076,9 @@ dependencies = [ [[package]] name = "malachite-nz" -version = "0.4.7" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2546fc6ae29728079e87a2a0f011509e6060713b65e62ee46ba5d413b495ebc7" +checksum = "fa263ca62420c1f65cf6758f55c979a49ad83169f332e602b1890f1e1277a429" dependencies = [ "itertools 0.11.0", "libm", @@ -4168,9 +4121,18 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] [[package]] name = "mimalloc" @@ -4187,6 +4149,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minicov" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4195,13 +4167,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mintex" version = "0.1.3" @@ -4220,6 +4201,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + [[package]] name = "multimap" version = "0.10.0" @@ -4255,11 +4248,10 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -4315,9 +4307,22 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if 1.0.0", - "cfg_aliases", + "cfg_aliases 0.1.1", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if 1.0.0", + "cfg_aliases 0.2.1", "libc", ] @@ -4345,36 +4350,34 @@ dependencies = [ ] [[package]] -name = "nu-ansi-term" -version = "0.46.0" +name = "ntapi" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ - "overload", "winapi", ] [[package]] name = "num" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", - "num-complex 0.4.5", + "num-complex 0.4.6", "num-integer", "num-iter", - "num-rational 0.4.1", + "num-rational 0.4.2", "num-traits", ] [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -4391,9 +4394,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] @@ -4437,11 +4440,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -4497,9 +4499,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] @@ -4512,9 +4514,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "opaque-debug" @@ -4524,11 +4526,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if 1.0.0", "foreign-types", "libc", @@ -4545,7 +4547,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -4556,18 +4558,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.3+3.2.1" +version = "300.3.1+3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -4592,10 +4594,14 @@ dependencies = [ ] [[package]] -name = "overload" -version = "0.1.1" +name = "os_pipe" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] [[package]] name = "pad" @@ -4614,9 +4620,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -4630,16 +4636,16 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.5.1", + "redox_syscall", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] name = "parse-variants" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f048110646aae15ec0e4299c37a012739d28d92c82b7ad945c0578c188cbe3" +checksum = "99088a2b0695df5940d7b5a3b4c4460b765053cf5ecd6d46da43812d3fad7f13" dependencies = [ "parse-variants-derive", ] @@ -4652,7 +4658,7 @@ checksum = "70df726c43c645ef1dde24c7ae14692036ebe5457c92c5f0ec4cfceb99634ff6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -4668,9 +4674,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pbkdf2" @@ -4696,12 +4702,12 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap 2.4.0", ] [[package]] @@ -4721,7 +4727,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -4738,9 +4744,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", "fastrand 2.1.0", @@ -4753,12 +4759,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "platforms" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" - [[package]] name = "polling" version = "2.8.0" @@ -4777,17 +4777,17 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.0" +version = "3.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if 1.0.0", "concurrent-queue", - "hermit-abi 0.3.9", + "hermit-abi 0.4.0", "pin-project-lite", "rustix 0.38.34", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4803,9 +4803,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] name = "powerfmt" @@ -4815,27 +4815,30 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "prettyplease" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "proc-macro-crate" -version = "0.1.5" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml 0.5.11", + "toml_edit 0.21.1", ] [[package]] @@ -4863,18 +4866,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", "prost-derive", @@ -4882,9 +4885,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80b776a1b2dc779f5ee0641f8ade0125bc1298dd41a9a0c16d8bd57b42d222b1" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", "heck", @@ -4897,28 +4900,28 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.60", + "syn 2.0.75", "tempfile", ] [[package]] name = "prost-derive" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "prost-types" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ "prost", ] @@ -4991,7 +4994,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", ] [[package]] @@ -5050,38 +5053,29 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -5091,9 +5085,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -5102,26 +5096,29 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-rustls", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -5130,11 +5127,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pemfile 2.1.3", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "system-configuration", "tokio", "tokio-native-tls", @@ -5143,7 +5140,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "windows-registry", ] [[package]] @@ -5154,7 +5151,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if 1.0.0", - "getrandom 0.2.14", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -5181,9 +5178,9 @@ dependencies = [ [[package]] name = "rocksdb" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +checksum = "6bd13e55d6d7b8cd0ea569161127567cd587676c99f4472f779a0279aa60a7a7" dependencies = [ "libc", "librocksdb-sys", @@ -5193,11 +5190,11 @@ dependencies = [ name = "rothschild" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", - "clap 4.5.4", + "async-channel 2.3.1", + "clap 4.5.16", "criterion", - "faster-hex 0.6.1", - "itertools 0.11.0", + "faster-hex", + "itertools 0.13.0", "kaspa-addresses", "kaspa-consensus-core", "kaspa-core", @@ -5215,9 +5212,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -5254,10 +5251,10 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] @@ -5269,10 +5266,23 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.6", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -5282,6 +5292,22 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -5292,17 +5318,28 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.102.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "salsa20" @@ -5355,9 +5392,9 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.28.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3" dependencies = [ "rand 0.8.5", "secp256k1-sys", @@ -5366,20 +5403,20 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.9.2" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +checksum = "1433bd67156263443f14d603720b082dd3121779323fce20cba2aa07b874bc1b" dependencies = [ "cc", ] [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -5388,9 +5425,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -5398,9 +5435,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] @@ -5422,9 +5459,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.200" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] @@ -5452,36 +5489,27 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - [[package]] name = "serde_repr" version = "0.1.19" @@ -5490,14 +5518,14 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -5516,15 +5544,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.4.0", "serde", "serde_derive", "serde_json", @@ -5534,14 +5562,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -5550,7 +5578,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "itoa", "ryu", "serde", @@ -5590,12 +5618,13 @@ dependencies = [ ] [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "shared_child" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c" dependencies = [ - "lazy_static", + "libc", + "windows-sys 0.59.0", ] [[package]] @@ -5616,12 +5645,12 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] @@ -5638,14 +5667,14 @@ dependencies = [ name = "simpa" version = "0.14.1" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "cfg-if 1.0.0", - "clap 4.5.4", + "clap 4.5.16", "dhat", "futures", "futures-util", - "indexmap 2.2.6", - "itertools 0.11.0", + "indexmap 2.4.0", + "itertools 0.13.0", "kaspa-alloc", "kaspa-consensus", "kaspa-consensus-core", @@ -5729,9 +5758,6 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] [[package]] name = "stable_deref_trait" @@ -5766,12 +5792,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -5780,9 +5800,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sweep-bptree" @@ -5803,15 +5823,27 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.75", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -5823,23 +5855,40 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "sysinfo" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4115055da5f572fff541dd0c4e61b0262977f453cc9fe04be83aba25a89bdab" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -5847,14 +5896,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if 1.0.0", "fastrand 2.1.0", + "once_cell", "rustix 0.38.34", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5888,22 +5938,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -5914,24 +5964,14 @@ checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" [[package]] name = "thread-id" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b" +checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" dependencies = [ "libc", "winapi", ] -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if 1.0.0", - "once_cell", -] - [[package]] name = "time" version = "0.3.36" @@ -5977,9 +6017,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -5992,20 +6032,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.2", "pin-project-lite", "signal-hook-registry", "socket2 0.5.7", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -6020,13 +6059,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -6045,7 +6084,18 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.12", + "rustls-pki-types", "tokio", ] @@ -6062,9 +6112,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.21.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" dependencies = [ "futures-util", "log", @@ -6089,45 +6139,47 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.11" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.20", ] [[package]] -name = "toml" -version = "0.8.12" +name = "toml_datetime" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", ] [[package]] -name = "toml_datetime" -version = "0.6.5" +name = "toml_edit" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "serde", + "indexmap 2.4.0", + "toml_datetime", + "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.18", ] [[package]] @@ -6138,22 +6190,22 @@ checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ "async-stream", "async-trait", - "axum 0.6.20", + "axum", "base64 0.21.7", "bytes", "flate2", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.30", "hyper-timeout", "percent-encoding", "pin-project", "prost", - "rustls", - "rustls-pemfile", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-stream", "tower", "tower-layer", @@ -6171,7 +6223,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -6200,7 +6252,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bytes", "futures-core", "futures-util", @@ -6212,33 +6264,17 @@ dependencies = [ "tower-service", ] -[[package]] -name = "tower-http" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" -dependencies = [ - "bitflags 2.5.0", - "bytes", - "http 1.1.0", - "http-body 1.0.0", - "http-body-util", - "pin-project-lite", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -6260,7 +6296,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] @@ -6270,32 +6306,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", ] [[package]] @@ -6312,9 +6322,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" dependencies = [ "byteorder", "bytes", @@ -6326,7 +6336,6 @@ dependencies = [ "rand 0.8.5", "sha1", "thiserror", - "url", "utf-8", ] @@ -6380,9 +6389,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "universal-hash" @@ -6417,9 +6426,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -6434,9 +6443,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" @@ -6444,27 +6453,21 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", ] [[package]] name = "uuid" -version = "1.6.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", "rand 0.8.5", "serde", "wasm-bindgen", ] -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "value-bag" version = "1.9.0" @@ -6485,9 +6488,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "vergen" -version = "8.3.1" +version = "8.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" +checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566" dependencies = [ "anyhow", "cargo_metadata", @@ -6500,15 +6503,15 @@ dependencies = [ [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "waker-fn" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "walkdir" @@ -6543,11 +6546,12 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if 1.0.0", + "once_cell", "serde", "serde_json", "wasm-bindgen-macro", @@ -6555,24 +6559,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -6582,9 +6586,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6592,31 +6596,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wasm-bindgen-test" -version = "0.3.42" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" +checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9" dependencies = [ "console_error_panic_hook", "js-sys", + "minicov", "scoped-tls", "wasm-bindgen", "wasm-bindgen-futures", @@ -6625,20 +6630,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.42" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" +checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -6674,11 +6679,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6687,13 +6692,96 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", ] [[package]] @@ -6711,7 +6799,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -6731,18 +6828,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -6753,9 +6850,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -6765,9 +6862,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -6777,15 +6874,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -6795,9 +6892,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -6807,9 +6904,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -6819,9 +6916,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -6831,34 +6928,33 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.7" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] [[package]] -name = "winreg" -version = "0.50.0" +name = "winnow" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ - "cfg-if 1.0.0", - "windows-sys 0.48.0", + "memchr", ] [[package]] name = "workflow-chrome" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109b6289f65b3e1cdfa6f2d9e8eb454453d5763c5061350e2300473c48d91b99" +checksum = "97134771e915c188b83d7a704b7ab626ef7a5c403deb5f09e89387536a1f3156" dependencies = [ "cfg-if 1.0.0", "chrome-sys", @@ -6871,23 +6967,24 @@ dependencies = [ [[package]] name = "workflow-core" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcea01cb6122ac3f20dc14f8e4104e2c0cd9c718c17ddb3fc115f9b2ed99f9ae" +checksum = "ff42951d05b777c35c0650fa1d16bef24f37e4aae539d06463bcedb2d8125e90" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-std", "borsh", "bs58", "cfg-if 1.0.0", "chrono", "dirs", - "faster-hex 0.9.0", + "faster-hex", "futures", - "getrandom 0.2.14", + "getrandom 0.2.15", "instant", "js-sys", "rand 0.8.5", + "rlimit", "serde", "serde-wasm-bindgen", "thiserror", @@ -6898,13 +6995,14 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "workflow-core-macros", + "workflow-log", ] [[package]] name = "workflow-core-macros" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe24820a62e2b544c75c000cff72781383495a0e05157ec3e29b2abafe1ca2cb" +checksum = "137266453ee5696254d8cc0cf1a535ad931be18860b657749f1ea79431c95129" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -6919,9 +7017,9 @@ dependencies = [ [[package]] name = "workflow-dom" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91264d4e789f23c6730c2f3adede04a24b6a9eb9797f9d4ab23de370ba04c27f" +checksum = "8f632b72a510fbd06711ac00edd41800f5ff789af66ef295736a0b74fa288fde" dependencies = [ "futures", "js-sys", @@ -6937,9 +7035,9 @@ dependencies = [ [[package]] name = "workflow-http" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b191def1625c3aa5e7d62d1ebbbb3e639113a4a2f122418e4cf8d3379374f8" +checksum = "ac8974f653aa090c4627738082a3e532c5b70ef75bc5d8bdc00e9df93a84c6a8" dependencies = [ "cfg-if 1.0.0", "reqwest", @@ -6953,9 +7051,9 @@ dependencies = [ [[package]] name = "workflow-log" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "077a8f720aa45c8cd867de1ccc73e068c4084d9fea46d11be7697a108e6a00ba" +checksum = "e8a0f71b5bd0fc4fe6e256cce3c9ba3b888f154533a9c91bec647bac715e9c37" dependencies = [ "cfg-if 1.0.0", "console", @@ -6969,9 +7067,9 @@ dependencies = [ [[package]] name = "workflow-macro-tools" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a8af8b8951fa0cf94df4057b8cf583e067a525d3d997370db7797f33ba201f" +checksum = "058b63fd7d255d04a5da8a4e2aab57bd8c9693dfd09a31c82ef47a65d807ca75" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -6982,9 +7080,9 @@ dependencies = [ [[package]] name = "workflow-node" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7748eb6c76779993ed7f4457356d6b57f48f97f9e264c64c3405098330bcb8c7" +checksum = "45c6227ecff04a63c6cd42f8d179526e49a33708b6216abcd803e9a3b3ae94ac" dependencies = [ "borsh", "futures", @@ -7003,11 +7101,11 @@ dependencies = [ [[package]] name = "workflow-nw" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010fff3468303b39fb0d5d267847a3d293ed083afbf83f4184fb1a749be56010" +checksum = "9014be650ede0fdcc10e5cdeece72f30db7e35846fd4c1e9e7a0dbb2e4a11815" dependencies = [ - "ahash 0.8.11", + "ahash", "async-trait", "borsh", "futures", @@ -7027,9 +7125,9 @@ dependencies = [ [[package]] name = "workflow-panic-hook" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1ed51290daf255e5fd83dfe6bd754b108e371b971afbb5c5fd1ea8fe148af" +checksum = "05c8909242a7d2f15d15d4f20f6cdc05932b394c6f34bd122de0a94eeddde493" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen", @@ -7052,18 +7150,18 @@ dependencies = [ [[package]] name = "workflow-rpc" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14784fbad27d0403fc752d835c4c4683cfc6af970a484ea83f40ce7ad6dc7745" +checksum = "6e26bdfb04aee86d80206160d817132966a4cca636b3565d597171412c3a570a" dependencies = [ - "ahash 0.8.11", + "ahash", "async-std", "async-trait", "borsh", "downcast-rs", "futures", "futures-util", - "getrandom 0.2.14", + "getrandom 0.2.15", "manual_future", "rand 0.8.5", "serde", @@ -7082,9 +7180,9 @@ dependencies = [ [[package]] name = "workflow-rpc-macros" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c372e99d1336a137b907274a3c50fc195e30141c87fc6da4dba54e7d4b09b8ec" +checksum = "1d8bd0c19bab1fc8606fd8a7b89b76bbc1c70b5389a4895c7ab5c81199b31578" dependencies = [ "parse-variants", "proc-macro-error", @@ -7093,17 +7191,28 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "workflow-serializer" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a7e3a8965373eb92d4dde26078a08677c4aebb422096d96f36ad89c33ddadb" +dependencies = [ + "ahash", + "borsh", + "serde", +] + [[package]] name = "workflow-store" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "762861614298160b9205302bec4f2b7eb45853413d10a90ad8edca44bafc324b" +checksum = "782c42bf9ea4eff8a518c6c65b000f4d10179dedfa19348a26d34f4637571437" dependencies = [ "async-std", - "base64 0.21.7", + "base64 0.22.1", "cfg-if 1.0.0", "chrome-sys", - "faster-hex 0.9.0", + "faster-hex", "filetime", "home", "js-sys", @@ -7123,9 +7232,9 @@ dependencies = [ [[package]] name = "workflow-task" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4023e2598734e04aa4e968a4dd1cd2b5d0c344edc38b40970926d5742f5afa0" +checksum = "2dbc4593b9c3a635b11952e35e33b273db513ce304af368bfb2e59eca47e77f8" dependencies = [ "futures", "thiserror", @@ -7135,9 +7244,9 @@ dependencies = [ [[package]] name = "workflow-task-macros" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057801365ce04c520a2a694bc5bfdf1784f1a33fff97af4cd735f94eb12947b1" +checksum = "3ee242b444bc37cf4d338c3c2994a3302b82ca2fa6a33537037bfed09e1583a0" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -7151,9 +7260,9 @@ dependencies = [ [[package]] name = "workflow-terminal" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895c236dd5cf493e01fc31733c4687b3e67032f610d594ce3b8e5cafd14eaf33" +checksum = "5f7b181672f3debb02b0c1db672164a8ae827d0ae064a77c008c7beb126341b6" dependencies = [ "async-std", "async-trait", @@ -7180,9 +7289,9 @@ dependencies = [ [[package]] name = "workflow-terminal-macros" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1fe67beb12d31f2e69715898aa32abd2349ffc8fe0555617f0d77500cebc56" +checksum = "5ac116e1d603e4462496ba2ec4133c43af29e7729493ff0ed8ea91319221cf06" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -7196,12 +7305,12 @@ dependencies = [ [[package]] name = "workflow-wasm" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ffbd1de665304ba6040a1ab4e0867fd9174446491d257bc6a1474ae25d4a6c" +checksum = "a878ee79b2a117277ae24d87525686f50006b87f43cf2052401e12c330fddbbc" dependencies = [ "cfg-if 1.0.0", - "faster-hex 0.9.0", + "faster-hex", "futures", "js-sys", "serde", @@ -7217,9 +7326,9 @@ dependencies = [ [[package]] name = "workflow-wasm-macros" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "082644f52215ecc86b4b8a20a763e482adee52c338208ade268f47fe25eb07ca" +checksum = "9268b990788e1bf1a307e1bc9d9f498e9ee2cb6d4ba3292d1c7b0b5d3d0d9e46" dependencies = [ "js-sys", "proc-macro-error", @@ -7231,12 +7340,12 @@ dependencies = [ [[package]] name = "workflow-websocket" -version = "0.12.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6967baf2bd85deb2a014a32d34c1664ded9333e10d11d43ffc179fa09cc55db8" +checksum = "15d03ec3c5ad42ba1a2bfba7e752be890ecacabf73c6da300ad78a2ffa1e3c5a" dependencies = [ - "ahash 0.8.11", - "async-channel 2.2.1", + "ahash", + "async-channel 2.3.1", "async-std", "async-trait", "cfg-if 1.0.0", @@ -7260,9 +7369,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" [[package]] name = "xmltree" @@ -7275,41 +7384,42 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.10" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" +checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" [[package]] name = "zerocopy" -version = "0.7.33" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.33" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.75", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" +version = "2.0.13+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 2789c15c1a..9be64f39e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ members = [ "rpc/grpc/core", "rpc/grpc/client", "rpc/grpc/server", - "rpc/wrpc/resolver", "rpc/wrpc/server", "rpc/wrpc/client", "rpc/wrpc/proxy", @@ -62,7 +61,7 @@ members = [ ] [workspace.package] -rust-version = "1.78.0" +rust-version = "1.80.0" version = "0.14.1" authors = ["Kaspa developers"] license = "ISC" @@ -84,7 +83,6 @@ include = [ kaspa-addresses = { version = "0.14.1", path = "crypto/addresses" } kaspa-addressmanager = { version = "0.14.1", path = "components/addressmanager" } kaspa-bip32 = { version = "0.14.1", path = "wallet/bip32" } -kaspa-resolver = { version = "0.14.1", path = "rpr/wrpc/resolver" } kaspa-cli = { version = "0.14.1", path = "cli" } kaspa-connectionmanager = { version = "0.14.1", path = "components/connectionmanager" } kaspa-consensus = { version = "0.14.1", path = "consensus" } @@ -130,7 +128,6 @@ kaspa-wallet-macros = { version = "0.14.1", path = "wallet/macros" } kaspa-wasm = { version = "0.14.1", path = "wasm" } kaspa-wasm-core = { version = "0.14.1", path = "wasm/core" } kaspa-wrpc-client = { version = "0.14.1", path = "rpc/wrpc/client" } -kaspa-wrpc-core = { version = "0.14.1", path = "rpc/wrpc/core" } kaspa-wrpc-proxy = { version = "0.14.1", path = "rpc/wrpc/proxy" } kaspa-wrpc-server = { version = "0.14.1", path = "rpc/wrpc/server" } kaspa-wrpc-wasm = { version = "0.14.1", path = "rpc/wrpc/wasm" } @@ -147,10 +144,10 @@ async-channel = "2.0.0" async-std = { version = "1.12.0", features = ['attributes'] } async-stream = "0.3.5" async-trait = "0.1.74" -base64 = "0.21.5" +base64 = "0.22.1" bincode = { version = "1.3.3", default-features = false } blake2b_simd = "1.0.2" -borsh = { version = "0.9.1", features = ["rc"] } # please keep this fixed +borsh = { version = "1.5.1", features = ["derive", "rc"] } bs58 = { version = "0.5.0", features = ["check"], default-features = false } cc = "1.0.83" cfb-mode = "0.8.2" @@ -162,42 +159,45 @@ criterion = { version = "0.5.1", default-features = false } crossbeam-channel = "0.5.8" ctrlc = "3.4.1" crypto_box = { version = "0.9.1", features = ["chacha20"] } -dashmap = "5.5.3" +dashmap = "6.0.1" derivative = "2.2.0" derive_builder = "0.20.0" derive_more = "0.99.17" +# derive_more = { version = "1.0.0", features = ["full"] } dhat = "0.3.2" dirs = "5.0.1" downcast = "0.11.0" downcast-rs = "1.2.0" -duration-string = "0.3.0" -enum-primitive-derive = "0.2.2" +duration-string = "0.4.0" +enum-primitive-derive = "0.3.0" event-listener = "2.5.3" # TODO "3.0.1" evpkdf = "0.2.0" -faster-hex = "0.6.1" # TODO "0.8.1" - fails unit tests +faster-hex = "0.9.0" fixedstr = { version = "0.5.4", features = ["serde"] } flate2 = "1.0.28" futures = { version = "0.3.29" } -futures-util = { version = "0.3.29", default-features = false, features = [ - "alloc", -] } +futures-util = { version = "0.3.29", default-features = false, features = ["alloc"] } getrandom = { version = "0.2.10", features = ["js"] } -h2 = "0.3.21" -heapless = "0.7.16" +h2 = "0.4.6" +# h2 = "0.3.21" +heapless = "0.8.0" +# heapless = "0.7.16" hex = { version = "0.4.3", features = ["serde"] } hex-literal = "0.4.1" +hexplay = "0.3.0" hmac = { version = "0.12.1", default-features = false } home = "0.5.5" igd-next = { version = "0.14.2", features = ["aio_tokio"] } indexmap = "2.1.0" intertrait = "0.2.2" ipnet = "2.9.0" -itertools = "0.11.0" -js-sys = "0.3.67" +itertools = "0.13.0" +js-sys = "0.3.70" keccak = "0.1.4" -local-ip-address = "0.5.6" +local-ip-address = "0.6.1" log = "0.4.20" log4rs = "1.2.0" +mac_address = "1.1.7" malachite-base = "0.4.4" malachite-nz = "0.4.4" md-5 = "0.10.6" @@ -211,6 +211,7 @@ paste = "1.0.14" pbkdf2 = "0.12.2" portable-atomic = { version = "1.5.1", features = ["float"] } prost = "0.12.1" +# prost = "0.13.1" rand = "0.8.5" rand_chacha = "0.3.1" rand_core = { version = "0.6.4", features = ["std"] } @@ -219,8 +220,8 @@ rayon = "1.8.0" regex = "1.10.2" ripemd = { version = "0.1.3", default-features = false } rlimit = "0.10.1" -rocksdb = "0.21.0" -secp256k1 = { version = "0.28.2", features = [ +rocksdb = "0.22.0" +secp256k1 = { version = "0.29.0", features = [ "global-context", "rand-std", "serde", @@ -242,6 +243,7 @@ sorted-insert = "0.2.3" statest = "0.2.2" statrs = "0.13.0" # TODO "0.16.0" subtle = { version = "2.5.0", default-features = false } +sysinfo = "0.31.2" tempfile = "3.8.1" textwrap = "0.16.0" thiserror = "1.0.50" @@ -252,10 +254,10 @@ tonic = { version = "0.10.2", features = ["tls", "gzip", "transport"] } tonic-build = { version = "0.10.2", features = ["prost"] } triggered = "0.1.2" uuid = { version = "1.5.0", features = ["v4", "fast-rng", "serde"] } -wasm-bindgen = { version = "0.2.92", features = ["serde-serialize"] } -wasm-bindgen-futures = "0.4.40" -wasm-bindgen-test = "0.3.37" -web-sys = "0.3.67" +wasm-bindgen = { version = "0.2.93", features = ["serde-serialize"] } +wasm-bindgen-futures = "0.4.43" +wasm-bindgen-test = "0.3.43" +web-sys = "0.3.70" xxhash-rust = { version = "0.8.7", features = ["xxh3"] } zeroize = { version = "1.6.0", default-features = false, features = ["alloc"] } pin-project-lite = "0.2.13" @@ -266,7 +268,7 @@ tower-http = { version = "0.4.4", features = [ tower = "0.4.7" hyper = "0.14.27" chrono = "0.4.31" -indexed_db_futures = "0.4.1" +indexed_db_futures = "0.5.0" # workflow dependencies that are not a part of core libraries # workflow-perf-monitor = { path = "../../../workflow-perf-monitor-rs" } @@ -274,17 +276,18 @@ workflow-perf-monitor = "0.0.2" nw-sys = "0.1.6" # workflow dependencies -workflow-core = { version = "0.12.1" } -workflow-d3 = { version = "0.12.1" } -workflow-dom = { version = "0.12.1" } -workflow-http = { version = "0.12.1" } -workflow-log = { version = "0.12.1" } -workflow-node = { version = "0.12.1" } -workflow-nw = { version = "0.12.1" } -workflow-rpc = { version = "0.12.1" } -workflow-store = { version = "0.12.1" } -workflow-terminal = { version = "0.12.1" } -workflow-wasm = { version = "0.12.1" } +workflow-core = { version = "0.17.0" } +workflow-d3 = { version = "0.17.0" } +workflow-dom = { version = "0.17.0" } +workflow-http = { version = "0.17.0" } +workflow-log = { version = "0.17.0" } +workflow-node = { version = "0.17.0" } +workflow-nw = { version = "0.17.0" } +workflow-rpc = { version = "0.17.0" } +workflow-serializer = { version = "0.17.0" } +workflow-store = { version = "0.17.0" } +workflow-terminal = { version = "0.17.0" } +workflow-wasm = { version = "0.17.0" } # if below is enabled, this means that there is an ongoing work # on the workflow-rs crate. This requires that you clone workflow-rs @@ -297,6 +300,7 @@ workflow-wasm = { version = "0.12.1" } # workflow-node = { path = "../workflow-rs/node" } # workflow-nw = { path = "../workflow-rs/nw" } # workflow-rpc = { path = "../workflow-rs/rpc" } +# workflow-serializer = { path = "../workflow-rs/serializer" } # workflow-store = { path = "../workflow-rs/store" } # workflow-terminal = { path = "../workflow-rs/terminal" } # workflow-wasm = { path = "../workflow-rs/wasm" } @@ -310,6 +314,7 @@ workflow-wasm = { version = "0.12.1" } # workflow-node = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } # workflow-nw = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } # workflow-rpc = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } +# workflow-serializer = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } # workflow-store = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } # workflow-terminal = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } # workflow-wasm = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } @@ -326,3 +331,5 @@ inherits = "release" debug = true strip = false +[workspace.lints.clippy] +empty_docs = "allow" diff --git a/README.md b/README.md index 5644e06dea..22b4ef7ca6 100644 --- a/README.md +++ b/README.md @@ -414,5 +414,3 @@ Logging in `kaspad` and `simpa` can be [filtered](https://docs.rs/env_logger/0.1 - - diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f2c80a5fa3..60a43002a0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -33,6 +33,7 @@ downcast.workspace = true faster-hex.workspace = true futures.workspace = true js-sys.workspace = true +hex.workspace = true kaspa-addresses.workspace = true kaspa-bip32.workspace = true kaspa-consensus-core.workspace = true @@ -43,6 +44,7 @@ kaspa-rpc-core.workspace = true kaspa-utils.workspace = true kaspa-wallet-core.workspace = true kaspa-wallet-keys.workspace = true +kaspa-wallet-pskt.workspace = true kaspa-wrpc-client.workspace = true nw-sys.workspace = true pad.workspace = true @@ -80,5 +82,5 @@ features = [ [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio.workspace = true -[lints.clippy] -empty_docs = "allow" +[lints] +workspace = true diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 4f562e7494..5ca1997ea3 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -6,9 +6,10 @@ use crate::modules::node::Node; use crate::notifier::{Notification, Notifier}; use crate::result::Result; use kaspa_daemon::{DaemonEvent, DaemonKind, Daemons}; +use kaspa_wallet_core::account::Account; use kaspa_wallet_core::rpc::DynRpcApi; use kaspa_wallet_core::storage::{IdT, PrvKeyDataInfo}; -use kaspa_wrpc_client::KaspaRpcClient; +use kaspa_wrpc_client::{KaspaRpcClient, Resolver}; use workflow_core::channel::*; use workflow_core::time::Instant; use workflow_log::*; @@ -102,7 +103,7 @@ impl KaspaCli { } pub async fn try_new_arc(options: Options) -> Result> { - let wallet = Arc::new(Wallet::try_new(Wallet::local_store()?, None, None)?); + let wallet = Arc::new(Wallet::try_new(Wallet::local_store()?, Some(Resolver::default()), None)?); let kaspa_cli = Arc::new(KaspaCli { term: Arc::new(Mutex::new(None)), @@ -311,7 +312,9 @@ impl KaspaCli { Events::SyncState { sync_state } => { if sync_state.is_synced() && this.wallet().is_open() { - if let Err(error) = this.wallet().reload(false).await { + let guard = this.wallet().guard(); + let guard = guard.lock().await; + if let Err(error) = this.wallet().reload(false, &guard).await { terrorln!(this, "Unable to reload wallet: {error}"); } } @@ -383,8 +386,11 @@ impl KaspaCli { record } => { if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Pending)) { + let guard = this.wallet.guard(); + let guard = guard.lock().await; + let include_utxos = this.flags.get(Track::Utxo); - let tx = record.format_transaction_with_state(&this.wallet,Some("reorg"),include_utxos).await; + let tx = record.format_transaction_with_state(&this.wallet,Some("reorg"),include_utxos, &guard).await; tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); } }, @@ -393,8 +399,11 @@ impl KaspaCli { } => { // Pending and coinbase stasis fall under the same `Track` category if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Pending)) { + let guard = this.wallet.guard(); + let guard = guard.lock().await; + let include_utxos = this.flags.get(Track::Utxo); - let tx = record.format_transaction_with_state(&this.wallet,Some("stasis"),include_utxos).await; + let tx = record.format_transaction_with_state(&this.wallet,Some("stasis"),include_utxos, &guard).await; tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); } }, @@ -411,8 +420,11 @@ impl KaspaCli { record } => { if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Pending)) { + let guard = this.wallet.guard(); + let guard = guard.lock().await; + let include_utxos = this.flags.get(Track::Utxo); - let tx = record.format_transaction_with_state(&this.wallet,Some("pending"),include_utxos).await; + let tx = record.format_transaction_with_state(&this.wallet,Some("pending"),include_utxos, &guard).await; tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); } }, @@ -420,8 +432,11 @@ impl KaspaCli { record } => { if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) { + let guard = this.wallet.guard(); + let guard = guard.lock().await; + let include_utxos = this.flags.get(Track::Utxo); - let tx = record.format_transaction_with_state(&this.wallet,Some("confirmed"),include_utxos).await; + let tx = record.format_transaction_with_state(&this.wallet,Some("confirmed"),include_utxos, &guard).await; tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); } }, @@ -532,6 +547,9 @@ impl KaspaCli { } async fn select_account_with_args(&self, autoselect: bool) -> Result> { + let guard = self.wallet.guard(); + let guard = guard.lock().await; + let mut selection = None; let mut list_by_key = Vec::<(Arc, Vec<(usize, Arc)>)>::new(); @@ -540,7 +558,7 @@ impl KaspaCli { let mut keys = self.wallet.keys().await?; while let Some(key) = keys.try_next().await? { let mut prv_key_accounts = Vec::new(); - let mut accounts = self.wallet.accounts(Some(key.id)).await?; + let mut accounts = self.wallet.accounts(Some(key.id), &guard).await?; while let Some(account) = accounts.next().await { let account = account?; prv_key_accounts.push((flat_list.len(), account.clone())); @@ -550,6 +568,16 @@ impl KaspaCli { list_by_key.push((key.clone(), prv_key_accounts)); } + let mut watch_accounts = Vec::<(usize, Arc)>::new(); + let mut unfiltered_accounts = self.wallet.accounts(None, &guard).await?; + + while let Some(account) = unfiltered_accounts.try_next().await? { + if account.feature().is_some() { + watch_accounts.push((flat_list.len(), account.clone())); + flat_list.push(account.clone()); + } + } + if flat_list.is_empty() { return Err(Error::NoAccounts); } else if autoselect && flat_list.len() == 1 { @@ -569,6 +597,16 @@ impl KaspaCli { }) }); + if !watch_accounts.is_empty() { + tprintln!(self, "• watch-only"); + } + + watch_accounts.iter().for_each(|(seq, account)| { + let seq = style(seq.to_string()).cyan(); + let ls_string = account.get_list_string().unwrap_or_else(|err| panic!("{err}")); + tprintln!(self, " {seq}: {ls_string}"); + }); + tprintln!(self); let range = if flat_list.len() > 1 { format!("[{}..{}] ", 0, flat_list.len() - 1) } else { "".to_string() }; @@ -643,18 +681,35 @@ impl KaspaCli { } pub async fn list(&self) -> Result<()> { + let guard = self.wallet.guard(); + let guard = guard.lock().await; + let mut keys = self.wallet.keys().await?; tprintln!(self); while let Some(key) = keys.try_next().await? { tprintln!(self, "• {}", style(&key).dim()); - let mut accounts = self.wallet.accounts(Some(key.id)).await?; + + let mut accounts = self.wallet.accounts(Some(key.id), &guard).await?; while let Some(account) = accounts.try_next().await? { let receive_address = account.receive_address()?; tprintln!(self, " • {}", account.get_list_string()?); tprintln!(self, " {}", style(receive_address.to_string()).blue()); } } + + let mut unfiltered_accounts = self.wallet.accounts(None, &guard).await?; + let mut feature_header_printed = false; + while let Some(account) = unfiltered_accounts.try_next().await? { + if let Some(feature) = account.feature() { + if !feature_header_printed { + tprintln!(self, "{}", style("• watch-only").dim()); + feature_header_printed = true; + } + tprintln!(self, " • {}", account.get_list_string().unwrap()); + tprintln!(self, " • {}", style(feature).cyan()); + } + } tprintln!(self); Ok(()) diff --git a/cli/src/error.rs b/cli/src/error.rs index a1701be355..23bb261243 100644 --- a/cli/src/error.rs +++ b/cli/src/error.rs @@ -72,6 +72,9 @@ pub enum Error { #[error("wallet secret is required")] WalletSecretRequired, + #[error("watch-only wallet kpub is required")] + WalletBip32WatchXpubRequired, + #[error("wallet secrets do not match")] WalletSecretMatch, @@ -84,6 +87,9 @@ pub enum Error { #[error("key data not found")] KeyDataNotFound, + #[error("no key data to export for watch-only account")] + WatchOnlyAccountNoKeyData, + #[error("no accounts found, please create an account to continue")] NoAccounts, @@ -122,6 +128,12 @@ pub enum Error { #[error(transparent)] KaspaWalletKeys(#[from] kaspa_wallet_keys::error::Error), + + #[error(transparent)] + PskbLockScriptSigError(#[from] kaspa_wallet_pskt::error::Error), + + #[error("To hex serialization error")] + PskbSerializeToHexError, } impl Error { diff --git a/cli/src/extensions/transaction.rs b/cli/src/extensions/transaction.rs index 70d73615ac..415aa7a34b 100644 --- a/cli/src/extensions/transaction.rs +++ b/cli/src/extensions/transaction.rs @@ -2,6 +2,7 @@ use crate::imports::*; use kaspa_consensus_core::tx::{TransactionInput, TransactionOutpoint}; use kaspa_wallet_core::storage::Binding; use kaspa_wallet_core::storage::{TransactionData, TransactionKind, TransactionRecord}; +use kaspa_wallet_core::wallet::WalletGuard; use workflow_log::style; pub trait TransactionTypeExtension { @@ -48,8 +49,14 @@ impl TransactionTypeExtension for TransactionKind { #[async_trait] pub trait TransactionExtension { - async fn format_transaction(&self, wallet: &Arc, include_utxos: bool) -> Vec; - async fn format_transaction_with_state(&self, wallet: &Arc, state: Option<&str>, include_utxos: bool) -> Vec; + async fn format_transaction(&self, wallet: &Arc, include_utxos: bool, guard: &WalletGuard) -> Vec; + async fn format_transaction_with_state( + &self, + wallet: &Arc, + state: Option<&str>, + include_utxos: bool, + guard: &WalletGuard, + ) -> Vec; async fn format_transaction_with_args( &self, wallet: &Arc, @@ -58,17 +65,24 @@ pub trait TransactionExtension { include_utxos: bool, history: bool, account: Option>, + guard: &WalletGuard, ) -> Vec; } #[async_trait] impl TransactionExtension for TransactionRecord { - async fn format_transaction(&self, wallet: &Arc, include_utxos: bool) -> Vec { - self.format_transaction_with_args(wallet, None, None, include_utxos, false, None).await + async fn format_transaction(&self, wallet: &Arc, include_utxos: bool, guard: &WalletGuard) -> Vec { + self.format_transaction_with_args(wallet, None, None, include_utxos, false, None, guard).await } - async fn format_transaction_with_state(&self, wallet: &Arc, state: Option<&str>, include_utxos: bool) -> Vec { - self.format_transaction_with_args(wallet, state, None, include_utxos, false, None).await + async fn format_transaction_with_state( + &self, + wallet: &Arc, + state: Option<&str>, + include_utxos: bool, + guard: &WalletGuard, + ) -> Vec { + self.format_transaction_with_args(wallet, state, None, include_utxos, false, None, guard).await } async fn format_transaction_with_args( @@ -79,6 +93,7 @@ impl TransactionExtension for TransactionRecord { include_utxos: bool, history: bool, account: Option>, + guard: &WalletGuard, ) -> Vec { let TransactionRecord { id, binding, block_daa_score, transaction_data, .. } = self; @@ -88,7 +103,7 @@ impl TransactionExtension for TransactionRecord { let account = if let Some(account) = account { Some(account) } else { - wallet.get_account_by_id(account_id).await.ok().flatten() + wallet.get_account_by_id(account_id, guard).await.ok().flatten() }; if let Some(account) = account { diff --git a/cli/src/imports.rs b/cli/src/imports.rs index a158128046..24de4b0ddd 100644 --- a/cli/src/imports.rs +++ b/cli/src/imports.rs @@ -14,7 +14,7 @@ pub use kaspa_utils::hex::*; pub use kaspa_wallet_core::compat::*; pub use kaspa_wallet_core::prelude::*; pub use kaspa_wallet_core::settings::{DefaultSettings, SettingsStore, WalletSettings}; -pub use kaspa_wallet_core::utils::*; +pub use kaspa_wrpc_client::prelude::*; pub use pad::PadStr; pub use regex::Regex; pub use separator::Separatable; diff --git a/cli/src/modules/account.rs b/cli/src/modules/account.rs index be627047e0..5848d43fba 100644 --- a/cli/src/modules/account.rs +++ b/cli/src/modules/account.rs @@ -177,7 +177,44 @@ impl Account { } _ => { tprintln!(ctx, "unknown account import type: '{import_kind}'"); - tprintln!(ctx, "supported import types are: 'mnemonic' or 'legacy-data'\r\n"); + tprintln!(ctx, "supported import types are: 'mnemonic', 'legacy-data' or 'multisig-watch'\r\n"); + return Ok(()); + } + } + } + "watch" => { + if argv.is_empty() { + tprintln!(ctx, "usage: 'account watch [account name]'"); + tprintln!(ctx, ""); + tprintln!(ctx, "examples:"); + tprintln!(ctx, ""); + ctx.term().help( + &[ + ("account watch bip32", "Import a extended public key for a watch-only bip32 account"), + ("account watch multisig", "Import extended public keys for a watch-only multisig account"), + ], + None, + )?; + + return Ok(()); + } + + let watch_kind = argv.remove(0); + + let account_name = argv.first().map(|name| name.trim()).filter(|name| !name.is_empty()).map(|name| name.to_string()); + + let account_name = account_name.as_deref(); + + match watch_kind.as_ref() { + "bip32" => { + wizards::account::bip32_watch(&ctx, account_name).await?; + } + "multisig" => { + wizards::account::multisig_watch(&ctx, account_name).await?; + } + _ => { + tprintln!(ctx, "unknown account watch type: '{watch_kind}'"); + tprintln!(ctx, "supported watch types are: 'bip32' or 'multisig'\r\n"); return Ok(()); } } diff --git a/cli/src/modules/connect.rs b/cli/src/modules/connect.rs index 26173256b4..92a3c676ba 100644 --- a/cli/src/modules/connect.rs +++ b/cli/src/modules/connect.rs @@ -1,5 +1,4 @@ use crate::imports::*; -use kaspa_wrpc_client::Resolver; #[derive(Default, Handler)] #[help("Connect to a Kaspa network")] @@ -29,8 +28,8 @@ impl Connect { if is_public { tpara!( ctx, - "Please note that default public nodes are community-operated and \ - accessing them may expose your IP address to different node providers. \ + "Please note that public node infrastructure is community-operated and \ + accessing it may expose your IP address to different node providers. \ Consider running your own node for better privacy. \ ", ); diff --git a/cli/src/modules/details.rs b/cli/src/modules/details.rs index ed44a9c825..896ecd3e87 100644 --- a/cli/src/modules/details.rs +++ b/cli/src/modules/details.rs @@ -27,6 +27,18 @@ impl Details { tprintln!(ctx.term(), "{:>4}{}", "", style(address.to_string()).blue()); }); + if let Some(xpub_keys) = account.xpub_keys() { + if account.feature().is_some() { + if let Some(feature) = account.feature() { + tprintln!(ctx.term(), "Feature: {}", style(feature).cyan()); + } + tprintln!(ctx.term(), "Extended public keys:"); + xpub_keys.iter().for_each(|xpub| { + tprintln!(ctx.term(), "{:>4}{}", "", style(ctx.wallet().network_format_xpub(xpub)).dim()); + }); + } + } + Ok(()) } } diff --git a/cli/src/modules/export.rs b/cli/src/modules/export.rs index 8a6b26e577..006cd7d36a 100644 --- a/cli/src/modules/export.rs +++ b/cli/src/modules/export.rs @@ -1,5 +1,5 @@ use crate::imports::*; -use kaspa_wallet_core::account::{multisig::MultiSig, Account, MULTISIG_ACCOUNT_KIND}; +use kaspa_wallet_core::account::{multisig::MultiSig, Account, BIP32_ACCOUNT_KIND, MULTISIG_ACCOUNT_KIND}; #[derive(Default, Handler)] #[help("Export transactions, a wallet or a private key")] @@ -32,8 +32,8 @@ impl Export { async fn export_multisig_account(ctx: Arc, account: Arc) -> Result<()> { match &account.prv_key_data_ids() { - None => Err(Error::KeyDataNotFound), - Some(v) if v.is_empty() => Err(Error::KeyDataNotFound), + None => Err(Error::WatchOnlyAccountNoKeyData), + Some(v) if v.is_empty() => Err(Error::WatchOnlyAccountNoKeyData), Some(prv_key_data_ids) => { let wallet_secret = Secret::new(ctx.term().ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec()); if wallet_secret.as_ref().is_empty() { @@ -45,26 +45,38 @@ async fn export_multisig_account(ctx: Arc, account: Arc) -> let prv_key_data_store = ctx.store().as_prv_key_data_store()?; let mut generated_xpub_keys = Vec::with_capacity(prv_key_data_ids.len()); + for (id, prv_key_data_id) in prv_key_data_ids.iter().enumerate() { let prv_key_data = prv_key_data_store.load_key_data(&wallet_secret, prv_key_data_id).await?.unwrap(); let mnemonic = prv_key_data.as_mnemonic(None).unwrap().unwrap(); + let xpub_key: kaspa_bip32::ExtendedPublicKey = + prv_key_data.create_xpub(None, MULTISIG_ACCOUNT_KIND.into(), 0).await?; // todo it can be done concurrently + + tprintln!(ctx, ""); + tprintln!(ctx, "extended public key {}:", id + 1); + tprintln!(ctx, ""); + tprintln!(ctx, "{}", ctx.wallet().network_format_xpub(&xpub_key)); + tprintln!(ctx, ""); + tprintln!(ctx, "mnemonic {}:", id + 1); tprintln!(ctx, ""); tprintln!(ctx, "{}", mnemonic.phrase()); tprintln!(ctx, ""); - let xpub_key = prv_key_data.create_xpub(None, MULTISIG_ACCOUNT_KIND.into(), 0).await?; // todo it can be done concurrently generated_xpub_keys.push(xpub_key); } - - let additional = account.xpub_keys().iter().filter(|xpub| !generated_xpub_keys.contains(xpub)); - additional.enumerate().for_each(|(idx, xpub)| { - if idx == 0 { - tprintln!(ctx, "additional xpubs: "); - } - tprintln!(ctx, "{xpub}"); - }); + let test = account.xpub_keys(); + + if let Some(keys) = test { + let additional = keys.iter().filter(|item| !generated_xpub_keys.contains(item)); + additional.enumerate().for_each(|(idx, xpub)| { + if idx == 0 { + tprintln!(ctx, "additional xpubs: "); + } + tprintln!(ctx, "{}", ctx.wallet().network_format_xpub(xpub)); + }); + } Ok(()) } } @@ -94,6 +106,13 @@ async fn export_single_key_account(ctx: Arc, account: Arc let prv_key_data = keydata.payload.decrypt(payment_secret.as_ref())?; let mnemonic = prv_key_data.as_ref().as_mnemonic()?; + let xpub_key = keydata.create_xpub(None, BIP32_ACCOUNT_KIND.into(), 0).await?; // todo it can be done concurrently + + tprintln!(ctx, "extended public key:"); + tprintln!(ctx, ""); + tprintln!(ctx, "{}", ctx.wallet().network_format_xpub(&xpub_key)); + tprintln!(ctx, ""); + match mnemonic { None => { tprintln!(ctx, "mnemonic is not available for this private key"); diff --git a/cli/src/modules/guide.txt b/cli/src/modules/guide.txt index ecc0f8d968..b993622ba8 100644 --- a/cli/src/modules/guide.txt +++ b/cli/src/modules/guide.txt @@ -1,49 +1,23 @@ -Please note - this is an alpha version of the softeware, not all features are currently functional. - -If using a dekstop or a web version of this software, you can use Ctrl+'+' or Ctrl+'-' (Command on MacOS) to -change the terminal font size. - -If using a desktop version, you can use Ctrl+M (Command on MacOS) to bring up metrics. - -Type `help` to see the complete list of commands. `exit` to exit this application. -On Windows you can use `Alt+F4` and on MacOS `Command+Q` to exit. - ---- - Before you start, you must configure the default network setting. There are currently -3 networks available. `mainnet`, `testnet-10` and `testnet-11`. While this software -is in alpha stage, you should not use it on the mainnet. If you wish to experiment, -you should select `testnet-10` by entering `network testnet-10` +3 networks available. `mainnet`, `testnet-10` and `testnet-11`. If you wish to experiment, +you should select `testnet-11` by entering `network testnet-11` The `server` command configures the target server. You can connect to any Rusty Kaspa -node that has User RPC enabled with `--rpclisten-borsh=public`. If you are running the node -from within KOS, it is locked to listen to a local IP address. +node that has wRPC enabled with `--rpclisten-borsh=0.0.0.0`. If the server setting +is set to 'public' the node will connect to the public node infrastructure. Both network and server values are stored in the application settings and are used when running a local node or connecting to a remote node. --- -You can use `node start` to start the node. Type `node` to see an overview of commands. -`node mute` toggles node log output (you can also use `node logs`). `node select` allows -you to choose between locally installed flavors (if running in the development environment). -You can also specify an absolute path by typing `node select `. - -For developers: `node select` scans 'target' folder for the debug and release builds -so you can switch between builds at runtime using the `node select` command. - -Once you node is running, you can connect to it using the `connect` command. - -When starting the node and the `server` setting is configured to your local host, -the `connect` action will occure automatically. - -`wallet create []` Use theis command to create a local wallet. The argument +`wallet create []` Use this command to create a local wallet. The argument is optional (the default wallet name is "kaspa") and allows you to create multiple named wallets. Only one wallet can be opened at a time. Keep in mind that a wallet can have multiple accounts, as such you only need one wallet, unless, for example, you want to separate wallets for personal and business needs (but you can also create isolated accounts within a wallet). -Make sure to record your mnemonic, even if working with a testnet, not to loose your +Make sure to record your mnemonic, even if working with a testnet, not to lose your testnet KAS. `open ` - opens the wallet (the wallet is open automatically after creation). @@ -56,9 +30,6 @@ testnet KAS. `address` - shows your selected account address -Note - you can click on the address to copy it to the clipboard. (When on mainnet, Ctrl+Click on addresses, transactions and -block hashes will open a new browser window with an explorer.) - Before you transact: `mute` option (enabled by default) toggles mute on/off. Mute enables terminal output of internal framework events. Rust and JavaScript/TypeScript applications integrating with this platform are meant to update their state by monitoring event notifications. Mute allows you to see these events in @@ -78,11 +49,6 @@ the selected account to an account named 'pete' (starts with a 'p' letter) `history details` - Show previous account transactions with extended information. -Once your node is synced, you can start the CPU miner. - -`miner start` - Starts the miner. The miner will mine to your currently selected account. (So you need to have a wallet open and an -account selected to start the miner) - `monitor` - A test screen environment that periodically updates account balances. `rpc` - Allows you to execute RPC methods against the node (not all methods are currently available) diff --git a/cli/src/modules/history.rs b/cli/src/modules/history.rs index 299701c51f..8fdf31f4db 100644 --- a/cli/src/modules/history.rs +++ b/cli/src/modules/history.rs @@ -10,6 +10,9 @@ impl History { async fn main(self: Arc, ctx: &Arc, mut argv: Vec, _cmd: &str) -> Result<()> { let ctx = ctx.clone().downcast_arc::()?; + let guard = ctx.wallet().guard(); + let guard = guard.lock().await; + if argv.is_empty() { self.display_help(ctx, argv).await?; return Ok(()); @@ -34,7 +37,15 @@ impl History { match store.load_single(&binding, &network_id, &txid).await { Ok(tx) => { let lines = tx - .format_transaction_with_args(&ctx.wallet(), None, current_daa_score, true, true, Some(account.clone())) + .format_transaction_with_args( + &ctx.wallet(), + None, + current_daa_score, + true, + true, + Some(account.clone()), + &guard, + ) .await; lines.iter().for_each(|line| tprintln!(ctx, "{line}")); } @@ -116,6 +127,7 @@ impl History { include_utxo, true, Some(account.clone()), + &guard, ) .await; lines.iter().for_each(|line| tprintln!(ctx, "{line}")); diff --git a/cli/src/modules/mod.rs b/cli/src/modules/mod.rs index a6371814f2..7990a9d6e1 100644 --- a/cli/src/modules/mod.rs +++ b/cli/src/modules/mod.rs @@ -26,6 +26,7 @@ pub mod network; pub mod node; pub mod open; pub mod ping; +pub mod pskb; pub mod reload; pub mod rpc; pub mod select; @@ -57,7 +58,7 @@ pub fn register_handlers(cli: &Arc) -> Result<()> { cli.handlers(), [ account, address, close, connect, details, disconnect, estimate, exit, export, guide, help, history, rpc, list, miner, - message, monitor, mute, network, node, open, ping, reload, select, send, server, settings, sweep, track, transfer, + message, monitor, mute, network, node, open, ping, pskb, reload, select, send, server, settings, sweep, track, transfer, wallet, // halt, // theme, start, stop diff --git a/cli/src/modules/pskb.rs b/cli/src/modules/pskb.rs new file mode 100644 index 0000000000..fd33087c22 --- /dev/null +++ b/cli/src/modules/pskb.rs @@ -0,0 +1,266 @@ +#![allow(unused_imports)] + +use crate::imports::*; +use kaspa_addresses::Prefix; +use kaspa_consensus_core::tx::{TransactionOutpoint, UtxoEntry}; +use kaspa_wallet_core::account::pskb::finalize_pskt_one_or_more_sig_and_redeem_script; +use kaspa_wallet_pskt::{ + prelude::{lock_script_sig_templating, script_sig_to_address, unlock_utxos_as_pskb, Bundle, Signer, PSKT}, + pskt::Inner, +}; + +#[derive(Default, Handler)] +#[help("Send a Kaspa transaction to a public address")] +pub struct Pskb; + +impl Pskb { + async fn main(self: Arc, ctx: &Arc, mut argv: Vec, _cmd: &str) -> Result<()> { + let ctx = ctx.clone().downcast_arc::()?; + + if !ctx.wallet().is_open() { + return Err(Error::WalletIsNotOpen); + } + + if argv.is_empty() { + return self.display_help(ctx, argv).await; + } + + let action = argv.remove(0); + + match action.as_str() { + "create" => { + if argv.len() < 2 || argv.len() > 3 { + return self.display_help(ctx, argv).await; + } + let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(None).await?; + let _ = ctx.notifier().show(Notification::Processing).await; + + let address = Address::try_from(argv.first().unwrap().as_str())?; + let amount_sompi = try_parse_required_nonzero_kaspa_as_sompi_u64(argv.get(1))?; + let outputs = PaymentOutputs::from((address, amount_sompi)); + let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.get(2))?.unwrap_or(0); + let abortable = Abortable::default(); + + let account: Arc = ctx.wallet().account()?; + let signer = account + .pskb_from_send_generator( + outputs.into(), + priority_fee_sompi.into(), + None, + wallet_secret.clone(), + payment_secret.clone(), + &abortable, + ) + .await?; + + match signer.serialize() { + Ok(encoded) => tprintln!(ctx, "{encoded}"), + Err(e) => return Err(e.into()), + } + } + "script" => { + if argv.len() < 2 || argv.len() > 4 { + return self.display_help(ctx, argv).await; + } + let subcommand = argv.remove(0); + let payload = argv.remove(0); + let account = ctx.wallet().account()?; + let receive_address = account.receive_address()?; + let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(None).await?; + let _ = ctx.notifier().show(Notification::Processing).await; + + let script_sig = match lock_script_sig_templating(payload.clone(), Some(&receive_address.payload)) { + Ok(value) => value, + Err(e) => { + terrorln!(ctx, "{}", e.to_string()); + return Err(e.into()); + } + }; + + let script_p2sh = match script_sig_to_address(&script_sig, ctx.wallet().address_prefix()?) { + Ok(p2sh) => p2sh, + Err(e) => { + terrorln!(ctx, "Error generating script address: {}", e.to_string()); + return Err(e.into()); + } + }; + + match subcommand.as_str() { + "lock" => { + let amount_sompi = try_parse_required_nonzero_kaspa_as_sompi_u64(argv.first())?; + let outputs = PaymentOutputs::from((script_p2sh, amount_sompi)); + 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(), + priority_fee_sompi.into(), + None, + wallet_secret.clone(), + payment_secret.clone(), + &abortable, + ) + .await?; + + match signer.serialize() { + Ok(encoded) => tprintln!(ctx, "{encoded}"), + Err(e) => return Err(e.into()), + } + } + "unlock" => { + if argv.len() != 1 { + return self.display_help(ctx, argv).await; + } + + // Get locked UTXO set. + let spend_utxos: Vec = + ctx.wallet().rpc_api().get_utxos_by_addresses(vec![script_p2sh.clone()]).await?; + let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.first())?.unwrap_or(0) as u64; + + if spend_utxos.is_empty() { + twarnln!(ctx, "No locked UTXO set found."); + return Ok(()); + } + + let references: Vec<(UtxoEntry, TransactionOutpoint)> = + spend_utxos.iter().map(|entry| (entry.utxo_entry.clone().into(), entry.outpoint.into())).collect(); + + let total_locked_sompi: u64 = spend_utxos.iter().map(|entry| entry.utxo_entry.amount).sum(); + + tprintln!( + ctx, + "{} locked UTXO{} found with total amount of {} KAS", + spend_utxos.len(), + if spend_utxos.len() == 1 { "" } else { "s" }, + sompi_to_kaspa(total_locked_sompi) + ); + + // Sweep UTXO set. + match unlock_utxos_as_pskb(references, &receive_address, script_sig, priority_fee_sompi as u64) { + Ok(pskb) => { + let pskb_hex = pskb.serialize()?; + tprintln!(ctx, "{pskb_hex}"); + } + Err(e) => tprintln!(ctx, "Error generating unlock PSKB: {}", e.to_string()), + } + } + "sign" => { + let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?; + + // Sign PSKB using the account's receiver address. + match account.pskb_sign(&pskb, wallet_secret.clone(), payment_secret.clone(), Some(&receive_address)).await { + Ok(signed_pskb) => { + let pskb_pack = String::try_from(signed_pskb)?; + tprintln!(ctx, "{pskb_pack}"); + } + Err(e) => terrorln!(ctx, "{}", e.to_string()), + } + } + "address" => { + tprintln!(ctx, "\r\nP2SH address: {}", script_p2sh); + } + v => { + terrorln!(ctx, "unknown command: '{v}'\r\n"); + return self.display_help(ctx, argv).await; + } + } + } + "sign" => { + if argv.len() != 1 { + return self.display_help(ctx, argv).await; + } + let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(None).await?; + let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?; + let account = ctx.wallet().account()?; + match account.pskb_sign(&pskb, wallet_secret.clone(), payment_secret.clone(), None).await { + Ok(signed_pskb) => { + let pskb_pack = String::try_from(signed_pskb)?; + tprintln!(ctx, "{pskb_pack}"); + } + Err(e) => terrorln!(ctx, "{}", e.to_string()), + } + } + "send" => { + if argv.len() != 1 { + return self.display_help(ctx, argv).await; + } + let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?; + let account = ctx.wallet().account()?; + match account.pskb_broadcast(&pskb).await { + Ok(sent) => tprintln!(ctx, "Sent transactions {:?}", sent), + Err(e) => terrorln!(ctx, "Send error {:?}", e), + } + } + "debug" => { + if argv.len() != 1 { + return self.display_help(ctx, argv).await; + } + let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?; + tprintln!(ctx, "{:?}", pskb); + } + "parse" => { + if argv.len() != 1 { + return self.display_help(ctx, argv).await; + } + let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?; + tprintln!(ctx, "{}", pskb.display_format(ctx.wallet().network_id()?, sompi_to_kaspa_string_with_suffix)); + + 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 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(_) => tprintln!( + ctx, + " Transaction extracted successfully: PSKT is finalized with a valid script signature." + ), + Err(e) => terrorln!(ctx, " PSKT transaction extraction error: {}", e.to_string()), + }, + Err(_) => twarnln!(ctx, " PSKT not finalized"), + } + } else { + twarnln!(ctx, " PSKT not signed"); + } + } + } + v => { + tprintln!(ctx, "unknown command: '{v}'\r\n"); + return self.display_help(ctx, argv).await; + } + } + Ok(()) + } + + fn parse_input_pskb(input: &str) -> Result { + match Bundle::try_from(input) { + Ok(bundle) => Ok(bundle), + Err(e) => Err(Error::custom(format!("Error while parsing input PSKB {}", e))), + } + } + + async fn display_help(self: Arc, ctx: Arc, _argv: Vec) -> Result<()> { + ctx.term().help( + &[ + ("pskb create
", "Create a PSKB from single send transaction"), + ("pskb sign ", "Sign given PSKB"), + ("pskb send ", "Broadcast bundled transactions"), + ("pskb debug ", "Print PSKB debug view"), + ("pskb parse ", "Print PSKB formatted view"), + ("pskb script lock [priority fee]", "Generate a PSKB with one send transaction to given P2SH payload. Optional public key placeholder in payload: {{pubkey}}"), + ("pskb script unlock ", "Generate a PSKB to unlock UTXOS one by one from given P2SH payload. Fee amount will be applied to every spent UTXO, meaning every transaction. Optional public key placeholder in payload: {{pubkey}}"), + ("pskb script sign ", "Sign all PSKB's P2SH locked inputs"), + ("pskb script sign ", "Sign all PSKB's P2SH locked inputs"), + ("pskb script address ", "Prints P2SH address"), + ], + None, + )?; + + Ok(()) + } +} diff --git a/cli/src/modules/reload.rs b/cli/src/modules/reload.rs index bc1eb717eb..b4c1ed7a52 100644 --- a/cli/src/modules/reload.rs +++ b/cli/src/modules/reload.rs @@ -10,8 +10,12 @@ impl Reload { // workflow_dom::utils::window().location().reload().ok(); let ctx = ctx.clone().downcast_arc::()?; + + let guard = ctx.wallet().guard(); + let guard = guard.lock().await; + tprintln!(ctx, "{}", style("reloading wallet ...").magenta()); - ctx.wallet().reload(true).await?; + ctx.wallet().reload(true, &guard).await?; Ok(()) } diff --git a/cli/src/modules/rpc.rs b/cli/src/modules/rpc.rs index 53478c1742..db4fd3383b 100644 --- a/cli/src/modules/rpc.rs +++ b/cli/src/modules/rpc.rs @@ -1,6 +1,6 @@ use crate::imports::*; use convert_case::{Case, Casing}; -use kaspa_rpc_core::{api::ops::RpcApiOps, *}; +use kaspa_rpc_core::api::ops::RpcApiOps; #[derive(Default, Handler)] #[help("Execute RPC commands against the connected Kaspa node")] @@ -38,19 +38,27 @@ impl Rpc { tprintln!(ctx, "ok"); } RpcApiOps::GetMetrics => { - let result = rpc.get_metrics(true, true, true, true).await?; + let result = rpc.get_metrics(true, true, true, true, true, true).await?; + self.println(&ctx, result); + } + RpcApiOps::GetSystemInfo => { + let result = rpc.get_system_info().await?; + self.println(&ctx, result); + } + RpcApiOps::GetConnections => { + let result = rpc.get_connections(true).await?; self.println(&ctx, result); } RpcApiOps::GetServerInfo => { - let result = rpc.get_server_info_call(GetServerInfoRequest {}).await?; + let result = rpc.get_server_info_call(None, GetServerInfoRequest {}).await?; self.println(&ctx, result); } RpcApiOps::GetSyncStatus => { - let result = rpc.get_sync_status_call(GetSyncStatusRequest {}).await?; + let result = rpc.get_sync_status_call(None, GetSyncStatusRequest {}).await?; self.println(&ctx, result); } RpcApiOps::GetCurrentNetwork => { - let result = rpc.get_current_network_call(GetCurrentNetworkRequest {}).await?; + let result = rpc.get_current_network_call(None, GetCurrentNetworkRequest {}).await?; self.println(&ctx, result); } // RpcApiOps::SubmitBlock => { @@ -62,11 +70,11 @@ impl Rpc { // self.println(&ctx, result); // } RpcApiOps::GetPeerAddresses => { - let result = rpc.get_peer_addresses_call(GetPeerAddressesRequest {}).await?; + let result = rpc.get_peer_addresses_call(None, GetPeerAddressesRequest {}).await?; self.println(&ctx, result); } RpcApiOps::GetSink => { - let result = rpc.get_sink_call(GetSinkRequest {}).await?; + let result = rpc.get_sink_call(None, GetSinkRequest {}).await?; self.println(&ctx, result); } // RpcApiOps::GetMempoolEntry => { @@ -76,12 +84,15 @@ impl Rpc { RpcApiOps::GetMempoolEntries => { // TODO let result = rpc - .get_mempool_entries_call(GetMempoolEntriesRequest { include_orphan_pool: true, filter_transaction_pool: true }) + .get_mempool_entries_call( + None, + GetMempoolEntriesRequest { include_orphan_pool: true, filter_transaction_pool: true }, + ) .await?; self.println(&ctx, result); } RpcApiOps::GetConnectedPeerInfo => { - let result = rpc.get_connected_peer_info_call(GetConnectedPeerInfoRequest {}).await?; + let result = rpc.get_connected_peer_info_call(None, GetConnectedPeerInfoRequest {}).await?; self.println(&ctx, result); } RpcApiOps::AddPeer => { @@ -90,7 +101,7 @@ impl Rpc { } let peer_address = argv.remove(0).parse::()?; let is_permanent = argv.remove(0).parse::().unwrap_or(false); - let result = rpc.add_peer_call(AddPeerRequest { peer_address, is_permanent }).await?; + let result = rpc.add_peer_call(None, AddPeerRequest { peer_address, is_permanent }).await?; self.println(&ctx, result); } // RpcApiOps::SubmitTransaction => { @@ -103,7 +114,7 @@ impl Rpc { } let hash = argv.remove(0); let hash = RpcHash::from_hex(hash.as_str())?; - let result = rpc.get_block_call(GetBlockRequest { hash, include_transactions: true }).await?; + let result = rpc.get_block_call(None, GetBlockRequest { hash, include_transactions: true }).await?; self.println(&ctx, result); } // RpcApiOps::GetSubnetwork => { @@ -119,11 +130,11 @@ impl Rpc { // self.println(&ctx, result); // } RpcApiOps::GetBlockCount => { - let result = rpc.get_block_count_call(GetBlockCountRequest {}).await?; + let result = rpc.get_block_count_call(None, GetBlockCountRequest {}).await?; self.println(&ctx, result); } RpcApiOps::GetBlockDagInfo => { - let result = rpc.get_block_dag_info_call(GetBlockDagInfoRequest {}).await?; + let result = rpc.get_block_dag_info_call(None, GetBlockDagInfoRequest {}).await?; self.println(&ctx, result); } // RpcApiOps::ResolveFinalityConflict => { @@ -131,7 +142,7 @@ impl Rpc { // self.println(&ctx, result); // } RpcApiOps::Shutdown => { - let result = rpc.shutdown_call(ShutdownRequest {}).await?; + let result = rpc.shutdown_call(None, ShutdownRequest {}).await?; self.println(&ctx, result); } // RpcApiOps::GetHeaders => { @@ -143,7 +154,7 @@ impl Rpc { return Err(Error::custom("Please specify at least one address")); } let addresses = argv.iter().map(|s| Address::try_from(s.as_str())).collect::, _>>()?; - let result = rpc.get_utxos_by_addresses_call(GetUtxosByAddressesRequest { addresses }).await?; + let result = rpc.get_utxos_by_addresses_call(None, GetUtxosByAddressesRequest { addresses }).await?; self.println(&ctx, result); } RpcApiOps::GetBalanceByAddress => { @@ -152,7 +163,7 @@ impl Rpc { } let addresses = argv.iter().map(|s| Address::try_from(s.as_str())).collect::, _>>()?; for address in addresses { - let result = rpc.get_balance_by_address_call(GetBalanceByAddressRequest { address }).await?; + let result = rpc.get_balance_by_address_call(None, GetBalanceByAddressRequest { address }).await?; self.println(&ctx, sompi_to_kaspa(result.balance)); } } @@ -161,11 +172,11 @@ impl Rpc { return Err(Error::custom("Please specify at least one address")); } let addresses = argv.iter().map(|s| Address::try_from(s.as_str())).collect::, _>>()?; - let result = rpc.get_balances_by_addresses_call(GetBalancesByAddressesRequest { addresses }).await?; + let result = rpc.get_balances_by_addresses_call(None, GetBalancesByAddressesRequest { addresses }).await?; self.println(&ctx, result); } RpcApiOps::GetSinkBlueScore => { - let result = rpc.get_sink_blue_score_call(GetSinkBlueScoreRequest {}).await?; + let result = rpc.get_sink_blue_score_call(None, GetSinkBlueScoreRequest {}).await?; self.println(&ctx, result); } RpcApiOps::Ban => { @@ -173,7 +184,7 @@ impl Rpc { return Err(Error::custom("Please specify peer IP address")); } let ip: RpcIpAddress = argv.remove(0).parse()?; - let result = rpc.ban_call(BanRequest { ip }).await?; + let result = rpc.ban_call(None, BanRequest { ip }).await?; self.println(&ctx, result); } RpcApiOps::Unban => { @@ -181,11 +192,11 @@ impl Rpc { return Err(Error::custom("Please specify peer IP address")); } let ip: RpcIpAddress = argv.remove(0).parse()?; - let result = rpc.unban_call(UnbanRequest { ip }).await?; + let result = rpc.unban_call(None, UnbanRequest { ip }).await?; self.println(&ctx, result); } RpcApiOps::GetInfo => { - let result = rpc.get_info_call(GetInfoRequest {}).await?; + let result = rpc.get_info_call(None, GetInfoRequest {}).await?; self.println(&ctx, result); } // RpcApiOps::EstimateNetworkHashesPerSecond => { @@ -200,16 +211,15 @@ impl Rpc { let include_orphan_pool = true; let filter_transaction_pool = true; let result = rpc - .get_mempool_entries_by_addresses_call(GetMempoolEntriesByAddressesRequest { - addresses, - include_orphan_pool, - filter_transaction_pool, - }) + .get_mempool_entries_by_addresses_call( + None, + GetMempoolEntriesByAddressesRequest { addresses, include_orphan_pool, filter_transaction_pool }, + ) .await?; self.println(&ctx, result); } RpcApiOps::GetCoinSupply => { - let result = rpc.get_coin_supply_call(GetCoinSupplyRequest {}).await?; + let result = rpc.get_coin_supply_call(None, GetCoinSupplyRequest {}).await?; self.println(&ctx, result); } RpcApiOps::GetDaaScoreTimestampEstimate => { @@ -220,8 +230,9 @@ impl Rpc { match daa_score_result { Ok(daa_scores) => { - let result = - rpc.get_daa_score_timestamp_estimate_call(GetDaaScoreTimestampEstimateRequest { daa_scores }).await?; + let result = rpc + .get_daa_score_timestamp_estimate_call(None, GetDaaScoreTimestampEstimateRequest { daa_scores }) + .await?; self.println(&ctx, result); } Err(_err) => { @@ -230,12 +241,12 @@ impl Rpc { } } RpcApiOps::GetFeeEstimate => { - let result = rpc.get_fee_estimate_call(GetFeeEstimateRequest {}).await?; + let result = rpc.get_fee_estimate_call(None, GetFeeEstimateRequest {}).await?; self.println(&ctx, result); } RpcApiOps::GetFeeEstimateExperimental => { let verbose = if argv.is_empty() { false } else { argv.remove(0).parse().unwrap_or(false) }; - let result = rpc.get_fee_estimate_experimental_call(GetFeeEstimateExperimentalRequest { verbose }).await?; + let result = rpc.get_fee_estimate_experimental_call(None, GetFeeEstimateExperimentalRequest { verbose }).await?; self.println(&ctx, result); } _ => { @@ -252,9 +263,8 @@ impl Rpc { async fn display_help(self: Arc, ctx: Arc, _argv: Vec) -> Result<()> { // RpcApiOps that do not contain docs are not displayed - let help = RpcApiOps::list() - .iter() - .filter_map(|op| op.doc().is_not_empty().then_some((op.as_str().to_case(Case::Kebab).to_string(), op.doc()))) + let help = RpcApiOps::into_iter() + .filter_map(|op| op.rustdoc().is_not_empty().then_some((op.as_str().to_case(Case::Kebab).to_string(), op.rustdoc()))) .collect::>(); ctx.term().help(&help, None)?; diff --git a/cli/src/modules/send.rs b/cli/src/modules/send.rs index d9f35d994d..773861dd4a 100644 --- a/cli/src/modules/send.rs +++ b/cli/src/modules/send.rs @@ -39,7 +39,7 @@ impl Send { .await?; tprintln!(ctx, "Send - {summary}"); - // tprintln!(ctx, "\nSending {} KAS to {address}, tx ids:", sompi_to_kaspa_string(amount_sompi)); + tprintln!(ctx, "\nSending {} KAS to {address}, tx ids:", sompi_to_kaspa_string(amount_sompi)); // tprintln!(ctx, "{}\n", ids.into_iter().map(|a| a.to_string()).collect::>().join("\n")); Ok(()) diff --git a/cli/src/modules/settings.rs b/cli/src/modules/settings.rs index e7214418a3..b144c4cc3b 100644 --- a/cli/src/modules/settings.rs +++ b/cli/src/modules/settings.rs @@ -9,12 +9,11 @@ impl Settings { let ctx = ctx.clone().downcast_arc::()?; tprintln!(ctx, "\nSettings:\n"); - let list = WalletSettings::list(); - let list = list - .iter() + // let list = WalletSettings::list(); + let list = WalletSettings::into_iter() .map(|setting| { let value: String = ctx.wallet().settings().get(setting.clone()).unwrap_or_else(|| "-".to_string()); - let descr = setting.descr(); + let descr = setting.describe(); (setting.as_str().to_lowercase(), value, descr) }) .collect::>(); diff --git a/cli/src/modules/wallet.rs b/cli/src/modules/wallet.rs index 6019b19086..70180e78d1 100644 --- a/cli/src/modules/wallet.rs +++ b/cli/src/modules/wallet.rs @@ -9,6 +9,9 @@ impl Wallet { async fn main(self: Arc, ctx: &Arc, mut argv: Vec, cmd: &str) -> Result<()> { let ctx = ctx.clone().downcast_arc::()?; + let guard = ctx.wallet().guard(); + let guard = guard.lock().await; + if argv.is_empty() { return self.display_help(ctx, argv).await; } @@ -48,7 +51,7 @@ impl Wallet { let wallet_name = wallet_name.as_deref(); let import_with_mnemonic = op.as_str() == "import"; - wizards::wallet::create(&ctx, wallet_name, import_with_mnemonic).await?; + wizards::wallet::create(&ctx, guard.into(), wallet_name, import_with_mnemonic).await?; } "open" => { let name = if let Some(name) = argv.first().cloned() { @@ -67,8 +70,8 @@ impl Wallet { let (wallet_secret, _) = ctx.ask_wallet_secret(None).await?; let _ = ctx.notifier().show(Notification::Processing).await; let args = WalletOpenArgs::default_with_legacy_accounts(); - ctx.wallet().open(&wallet_secret, name, args).await?; - ctx.wallet().activate_accounts(None).await?; + ctx.wallet().open(&wallet_secret, name, args, &guard).await?; + ctx.wallet().activate_accounts(None, &guard).await?; } "close" => { ctx.wallet().close().await?; diff --git a/cli/src/wizards/account.rs b/cli/src/wizards/account.rs index 7a3afb73b1..9d3d4d5916 100644 --- a/cli/src/wizards/account.rs +++ b/cli/src/wizards/account.rs @@ -85,3 +85,62 @@ async fn create_multisig(ctx: &Arc, account_name: Option, mnem wallet.select(Some(&account)).await?; Ok(()) } + +pub(crate) async fn bip32_watch(ctx: &Arc, name: Option<&str>) -> Result<()> { + let term = ctx.term(); + let wallet = ctx.wallet(); + + let name = if let Some(name) = name { + Some(name.to_string()) + } else { + Some(term.ask(false, "Please enter account name (optional, press to skip): ").await?.trim().to_string()) + }; + + let mut xpub_keys = Vec::with_capacity(1); + let xpub_key = term.ask(false, "Enter extended public key: ").await?; + xpub_keys.push(xpub_key.trim().to_owned()); + + let wallet_secret = Secret::new(term.ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec()); + if wallet_secret.as_ref().is_empty() { + return Err(Error::WalletSecretRequired); + } + + let account_create_args_bip32_watch = AccountCreateArgsBip32Watch::new(name, xpub_keys); + let account = wallet.create_account_bip32_watch(&wallet_secret, account_create_args_bip32_watch).await?; + + tprintln!(ctx, "\naccount created: {}\n", account.get_list_string()?); + wallet.select(Some(&account)).await?; + Ok(()) +} + +pub(crate) async fn multisig_watch(ctx: &Arc, name: Option<&str>) -> Result<()> { + let term = ctx.term(); + + let account_name = if let Some(name) = name { + Some(name.to_string()) + } else { + Some(term.ask(false, "Please enter account name (optional, press to skip): ").await?.trim().to_string()) + }; + + let term = ctx.term(); + let wallet = ctx.wallet(); + let (wallet_secret, _) = ctx.ask_wallet_secret(None).await?; + let minimum_signatures: u16 = term.ask(false, "Enter the minimum number of signatures required: ").await?.parse()?; + + let prv_key_data_args = Vec::with_capacity(0); + + let answer = term.ask(false, "Enter the number of extended public keys: ").await?.trim().to_string(); //.parse()?; + let xpub_keys_len: usize = if answer.is_empty() { 0 } else { answer.parse()? }; + + let mut xpub_keys = Vec::with_capacity(xpub_keys_len); + for i in 1..=xpub_keys_len { + let xpub_key = term.ask(false, &format!("Enter extended public {i} key: ")).await?; + xpub_keys.push(xpub_key.trim().to_owned()); + } + let account = + wallet.create_account_multisig(&wallet_secret, prv_key_data_args, xpub_keys, account_name, minimum_signatures).await?; + + tprintln!(ctx, "\naccount created: {}\n", account.get_list_string()?); + wallet.select(Some(&account)).await?; + Ok(()) +} diff --git a/cli/src/wizards/wallet.rs b/cli/src/wizards/wallet.rs index 8563a8619b..0fae267a36 100644 --- a/cli/src/wizards/wallet.rs +++ b/cli/src/wizards/wallet.rs @@ -2,12 +2,25 @@ use crate::cli::KaspaCli; use crate::imports::*; use crate::result::Result; use kaspa_bip32::{Language, Mnemonic, WordCount}; -use kaspa_wallet_core::storage::{make_filename, Hint}; - -pub(crate) async fn create(ctx: &Arc, name: Option<&str>, import_with_mnemonic: bool) -> Result<()> { +use kaspa_wallet_core::{ + storage::{make_filename, Hint}, + wallet::WalletGuard, +}; + +pub(crate) async fn create( + ctx: &Arc, + wallet_guard: Option>, + name: Option<&str>, + import_with_mnemonic: bool, +) -> Result<()> { let term = ctx.term(); let wallet = ctx.wallet(); + let local_guard = ctx.wallet().guard(); + let guard = match wallet_guard { + Some(locked_guard) => locked_guard, + None => local_guard.lock().await, + }; // TODO @aspect let word_count = WordCount::Words12; @@ -86,7 +99,7 @@ pub(crate) async fn create(ctx: &Arc, name: Option<&str>, import_with_ "\ PLEASE NOTE: The optional bip39 mnemonic passphrase, if provided, will be required to \ issue transactions. This passphrase will also be required when recovering your wallet \ - in addition to your private key or mnemonic. If you loose this passphrase, you will not \ + in addition to your private key or mnemonic. If you lose this passphrase, you will not \ be able to use or recover your wallet! \ \ If you do not want to use bip39 recovery passphrase, press ENTER.\ @@ -173,8 +186,8 @@ pub(crate) async fn create(ctx: &Arc, name: Option<&str>, import_with_ term.writeln(style(receive_address).blue().to_string()); term.writeln(""); - wallet.open(&wallet_secret, name.map(String::from), WalletOpenArgs::default_with_legacy_accounts()).await?; - wallet.activate_accounts(None).await?; + wallet.open(&wallet_secret, name.map(String::from), WalletOpenArgs::default_with_legacy_accounts(), &guard).await?; + wallet.activate_accounts(None, &guard).await?; Ok(()) } diff --git a/consensus/client/Cargo.toml b/consensus/client/Cargo.toml index 38cbed9a33..698348508f 100644 --- a/consensus/client/Cargo.toml +++ b/consensus/client/Cargo.toml @@ -38,5 +38,5 @@ itertools.workspace = true workflow-wasm.workspace = true workflow-log.workspace = true -[lints.clippy] -empty_docs = "allow" +[lints] +workspace = true diff --git a/consensus/client/src/header.rs b/consensus/client/src/header.rs index 6294d21326..927e172fa6 100644 --- a/consensus/client/src/header.rs +++ b/consensus/client/src/header.rs @@ -37,7 +37,7 @@ export interface IHeader { #[wasm_bindgen] extern "C" { #[wasm_bindgen(typescript_type = "IHeader | Header")] - pub type IHeader; + pub type HeaderT; } /// @category Consensus @@ -64,7 +64,7 @@ impl Header { #[wasm_bindgen] impl Header { #[wasm_bindgen(constructor)] - pub fn constructor(js_value: IHeader) -> std::result::Result { + pub fn constructor(js_value: HeaderT) -> std::result::Result { Ok(js_value.try_into_owned()?) } @@ -232,8 +232,11 @@ impl Header { impl TryCastFromJs for Header { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { if let Some(object) = Object::try_from(value.as_ref()) { let parents_by_level = object .get_vec("parentsByLevel")? diff --git a/consensus/client/src/input.rs b/consensus/client/src/input.rs index 8b48c2d942..736696bfae 100644 --- a/consensus/client/src/input.rs +++ b/consensus/client/src/input.rs @@ -13,7 +13,7 @@ const TS_TRANSACTION: &'static str = r#" */ export interface ITransactionInput { previousOutpoint: ITransactionOutpoint; - signatureScript: HexString; + signatureScript?: HexString; sequence: bigint; sigOpCount: number; utxo?: UtxoEntryReference; @@ -33,15 +33,19 @@ export interface ITransactionInputVerboseData { } #[wasm_bindgen] extern "C" { - #[wasm_bindgen(typescript_type = "ITransactionInput")] - pub type ITransactionInput; + #[wasm_bindgen(typescript_type = "ITransactionInput | TransactionInput")] + pub type TransactionInputT; + #[wasm_bindgen(typescript_type = "(ITransactionInput | TransactionInput)[]")] + pub type TransactionInputArrayAsArgT; + #[wasm_bindgen(typescript_type = "TransactionInput[]")] + pub type TransactionInputArrayAsResultT; } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionInputInner { pub previous_outpoint: TransactionOutpoint, - pub signature_script: Vec, + pub signature_script: Option>, pub sequence: u64, pub sig_op_count: u8, pub utxo: Option, @@ -50,7 +54,7 @@ pub struct TransactionInputInner { impl TransactionInputInner { pub fn new( previous_outpoint: TransactionOutpoint, - signature_script: Vec, + signature_script: Option>, sequence: u64, sig_op_count: u8, utxo: Option, @@ -70,7 +74,7 @@ pub struct TransactionInput { impl TransactionInput { pub fn new( previous_outpoint: TransactionOutpoint, - signature_script: Vec, + signature_script: Option>, sequence: u64, sig_op_count: u8, utxo: Option, @@ -91,6 +95,10 @@ impl TransactionInput { self.inner().sig_op_count } + pub fn signature_script_length(&self) -> usize { + self.inner().signature_script.as_ref().map(|signature_script| signature_script.len()).unwrap_or_default() + } + pub fn utxo(&self) -> Option { self.inner().utxo.clone() } @@ -99,7 +107,7 @@ impl TransactionInput { #[wasm_bindgen] impl TransactionInput { #[wasm_bindgen(constructor)] - pub fn constructor(value: &ITransactionInput) -> Result { + pub fn constructor(value: &TransactionInputT) -> Result { Self::try_owned_from(value) } @@ -120,8 +128,8 @@ impl TransactionInput { } #[wasm_bindgen(getter = signatureScript)] - pub fn get_signature_script_as_hex(&self) -> String { - self.inner().signature_script.to_hex() + pub fn get_signature_script_as_hex(&self) -> Option { + self.inner().signature_script.as_ref().map(|script| script.to_hex()) } #[wasm_bindgen(setter = signatureScript)] @@ -163,7 +171,7 @@ impl TransactionInput { impl TransactionInput { pub fn set_signature_script(&self, signature_script: Vec) { - self.inner().signature_script = signature_script; + self.inner().signature_script.replace(signature_script); } pub fn script_public_key(&self) -> Option { @@ -179,14 +187,17 @@ impl AsRef for TransactionInput { impl TryCastFromJs for TransactionInput { type Error = Error; - fn try_cast_from(value: impl AsRef) -> std::result::Result, Self::Error> { - Self::resolve_cast(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve_cast(value, || { if let Some(object) = Object::try_from(value.as_ref()) { let previous_outpoint: TransactionOutpoint = object.get_value("previousOutpoint")?.as_ref().try_into()?; - let signature_script = object.get_vec_u8("signatureScript")?; + let signature_script = object.get_vec_u8("signatureScript").ok(); let sequence = object.get_u64("sequence")?; let sig_op_count = object.get_u8("sigOpCount")?; - let utxo = object.try_get_cast::("utxo")?.map(Cast::into_owned); + let utxo = object.try_cast_into::("utxo")?; Ok(TransactionInput::new(previous_outpoint, signature_script, sequence, sig_op_count, utxo).into()) } else { Err("TransactionInput must be an object".into()) @@ -199,7 +210,7 @@ impl From for TransactionInput { fn from(tx_input: cctx::TransactionInput) -> Self { TransactionInput::new( tx_input.previous_outpoint.into(), - tx_input.signature_script, + Some(tx_input.signature_script), tx_input.sequence, tx_input.sig_op_count, None, @@ -212,7 +223,8 @@ impl From<&TransactionInput> for cctx::TransactionInput { let inner = tx_input.inner(); cctx::TransactionInput::new( inner.previous_outpoint.clone().into(), - inner.signature_script.clone(), + // TODO - discuss: should this unwrap_or_default or return an error? + inner.signature_script.clone().unwrap_or_default(), inner.sequence, inner.sig_op_count, ) diff --git a/consensus/client/src/lib.rs b/consensus/client/src/lib.rs index 4935b16f76..eb482eab16 100644 --- a/consensus/client/src/lib.rs +++ b/consensus/client/src/lib.rs @@ -1,33 +1,29 @@ pub mod error; mod imports; +mod input; mod outpoint; mod output; pub mod result; +mod serializable; +mod transaction; mod utxo; +pub use input::*; pub use outpoint::*; pub use output::*; +pub use serializable::*; +pub use transaction::*; pub use utxo::*; cfg_if::cfg_if! { if #[cfg(feature = "wasm32-sdk")] { mod header; - mod input; - mod transaction; - mod vtx; + mod utils; mod hash; mod sign; - mod script; - mod serializable; - pub use header::*; - pub use input::*; - pub use transaction::*; - pub use serializable::*; - pub use vtx::*; + pub use utils::*; pub use hash::*; - // pub use signing::*; - pub use script::*; pub use sign::sign_with_multiple_v3; } } diff --git a/consensus/client/src/outpoint.rs b/consensus/client/src/outpoint.rs index 77e17d542e..06be53f6aa 100644 --- a/consensus/client/src/outpoint.rs +++ b/consensus/client/src/outpoint.rs @@ -165,6 +165,15 @@ impl From for cctx::TransactionOutpoint { } } +impl From<&TransactionOutpoint> for cctx::TransactionOutpoint { + fn from(outpoint: &TransactionOutpoint) -> Self { + let inner = outpoint.inner(); + let transaction_id = inner.transaction_id; + let index = inner.index; + cctx::TransactionOutpoint::new(transaction_id, index) + } +} + impl TransactionOutpoint { pub fn simulated() -> Self { Self::new(TransactionId::from_slice(&rand::random::<[u8; kaspa_hashes::HASH_SIZE]>()), 0) diff --git a/consensus/client/src/output.rs b/consensus/client/src/output.rs index 99fe38ec3a..8f335c47d7 100644 --- a/consensus/client/src/output.rs +++ b/consensus/client/src/output.rs @@ -9,7 +9,7 @@ const TS_TRANSACTION_OUTPUT: &'static str = r#" */ export interface ITransactionOutput { value: bigint; - scriptPublicKey: IScriptPublicKey; + scriptPublicKey: IScriptPublicKey | HexString; /** Optional verbose data provided by RPC */ verboseData?: ITransactionOutputVerboseData; @@ -26,6 +26,16 @@ export interface ITransactionOutputVerboseData { } "#; +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ITransactionOutput | TransactionOutput")] + pub type TransactionOutputT; + #[wasm_bindgen(typescript_type = "(ITransactionOutput | TransactionOutput)[]")] + pub type TransactionOutputArrayAsArgT; + #[wasm_bindgen(typescript_type = "TransactionOutput[]")] + pub type TransactionOutputArrayAsResultT; +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionOutputInner { @@ -55,7 +65,7 @@ impl TransactionOutput { self.inner.lock().unwrap() } - pub fn script_length(&self) -> usize { + pub fn script_public_key_length(&self) -> usize { self.inner().script_public_key.script().len() } } @@ -69,7 +79,7 @@ impl TransactionOutput { } #[wasm_bindgen(getter, js_name = value)] - pub fn get_value(&self) -> u64 { + pub fn value(&self) -> u64 { self.inner().value } @@ -114,25 +124,20 @@ impl From<&TransactionOutput> for cctx::TransactionOutput { } } -impl TryFrom<&JsValue> for TransactionOutput { - type Error = Error; - fn try_from(js_value: &JsValue) -> Result { - // workflow_log::log_trace!("js_value->TransactionOutput: {js_value:?}"); - if let Some(object) = Object::try_from(js_value) { - let has_address = Object::has_own(object, &JsValue::from("address")); - workflow_log::log_trace!("js_value->TransactionOutput: has_address:{has_address:?}"); - let value = object.get_u64("value")?; - let script_public_key = ScriptPublicKey::try_cast_from(object.get_value("scriptPublicKey")?)?; - Ok(TransactionOutput::new(value, script_public_key.into_owned())) - } else { - Err("TransactionInput must be an object".into()) - } - } -} - -impl TryFrom for TransactionOutput { +impl TryCastFromJs for TransactionOutput { type Error = Error; - fn try_from(js_value: JsValue) -> Result { - Self::try_from(&js_value) + fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve_cast(value, || { + if let Some(object) = Object::try_from(value.as_ref()) { + let value = object.get_u64("value")?; + let script_public_key = ScriptPublicKey::try_owned_from(object.get_value("scriptPublicKey")?)?; + Ok(TransactionOutput::new(value, script_public_key).into()) + } else { + Err("TransactionInput must be an object".into()) + } + }) } } diff --git a/consensus/client/src/serializable/mod.rs b/consensus/client/src/serializable/mod.rs index 5855e26dfb..a590ab2862 100644 --- a/consensus/client/src/serializable/mod.rs +++ b/consensus/client/src/serializable/mod.rs @@ -30,7 +30,7 @@ export interface ISerializableTransactionInput { index: number; sequence: bigint; sigOpCount: number; - signatureScript: HexString; + signatureScript?: HexString; utxo: ISerializableUtxoEntry; } @@ -77,3 +77,9 @@ export interface ISerializableTransaction { } "#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Array, typescript_type = "ISerializableTransaction")] + pub type SerializableTransactionT; +} diff --git a/consensus/client/src/serializable/numeric.rs b/consensus/client/src/serializable/numeric.rs index 0c413fdcfe..d4c145a1bc 100644 --- a/consensus/client/src/serializable/numeric.rs +++ b/consensus/client/src/serializable/numeric.rs @@ -80,6 +80,7 @@ pub struct SerializableTransactionInput { pub sequence: u64, pub sig_op_count: u8, #[serde(with = "hex::serde")] + // TODO - convert to Option> and use hex serialization over Option pub signature_script: Vec, pub utxo: SerializableUtxoEntry, } @@ -91,6 +92,8 @@ impl SerializableTransactionInput { Self { transaction_id: input.previous_outpoint.transaction_id, index: input.previous_outpoint.index, + // TODO - convert signature_script to Option> + // signature_script: (!input.signature_script.is_empty()).then_some(input.signature_script.clone()), signature_script: input.signature_script.clone(), sequence: input.sequence, sig_op_count: input.sig_op_count, @@ -134,15 +137,16 @@ impl TryFrom for cctx::TransactionInput { impl TryFrom<&SerializableTransactionInput> for TransactionInput { type Error = Error; - fn try_from(signable_input: &SerializableTransactionInput) -> Result { - let utxo = UtxoEntryReference::try_from(signable_input)?; + fn try_from(serializable_input: &SerializableTransactionInput) -> Result { + let utxo = UtxoEntryReference::try_from(serializable_input)?; - let previous_outpoint = TransactionOutpoint::new(signable_input.transaction_id, signable_input.index); + let previous_outpoint = TransactionOutpoint::new(serializable_input.transaction_id, serializable_input.index); let inner = TransactionInputInner { previous_outpoint, - signature_script: signable_input.signature_script.clone(), - sequence: signable_input.sequence, - sig_op_count: signable_input.sig_op_count, + // TODO - convert to Option> and use hex serialization over Option + signature_script: (!serializable_input.signature_script.is_empty()).then_some(serializable_input.signature_script.clone()), + sequence: serializable_input.sequence, + sig_op_count: serializable_input.sig_op_count, utxo: Some(utxo), }; @@ -159,7 +163,8 @@ impl TryFrom<&TransactionInput> for SerializableTransactionInput { Ok(Self { transaction_id: inner.previous_outpoint.transaction_id(), index: inner.previous_outpoint.index(), - signature_script: inner.signature_script.clone(), + // TODO - convert to Option> and use hex serialization over Option + signature_script: inner.signature_script.clone().unwrap_or_default(), sequence: inner.sequence, sig_op_count: inner.sig_op_count, utxo, diff --git a/consensus/client/src/serializable/string.rs b/consensus/client/src/serializable/string.rs index be3981b0ea..33c054fac3 100644 --- a/consensus/client/src/serializable/string.rs +++ b/consensus/client/src/serializable/string.rs @@ -139,7 +139,8 @@ impl TryFrom<&SerializableTransactionInput> for TransactionInput { let previous_outpoint = TransactionOutpoint::new(serializable_input.transaction_id, serializable_input.index); let inner = TransactionInputInner { previous_outpoint, - signature_script: serializable_input.signature_script.clone(), + // TODO - convert to Option> and use hex serialization over Option + signature_script: (!serializable_input.signature_script.is_empty()).then_some(serializable_input.signature_script.clone()), sequence: serializable_input.sequence.parse()?, sig_op_count: serializable_input.sig_op_count, utxo: Some(utxo), @@ -158,7 +159,8 @@ impl TryFrom<&TransactionInput> for SerializableTransactionInput { Ok(Self { transaction_id: inner.previous_outpoint.transaction_id(), index: inner.previous_outpoint.index(), - signature_script: inner.signature_script.clone(), + // TODO - convert to Option> and use hex serialization over Option + signature_script: inner.signature_script.clone().unwrap_or_default(), sequence: inner.sequence.to_string(), sig_op_count: inner.sig_op_count, utxo, diff --git a/consensus/client/src/sign.rs b/consensus/client/src/sign.rs index fdab66a602..c254aee076 100644 --- a/consensus/client/src/sign.rs +++ b/consensus/client/src/sign.rs @@ -13,14 +13,14 @@ use std::collections::BTreeMap; /// A wrapper enum that represents the transaction signed state. A transaction /// contained by this enum can be either fully signed or partially signed. -pub enum Signed { - Fully(Transaction), - Partially(Transaction), +pub enum Signed<'a> { + Fully(&'a Transaction), + Partially(&'a Transaction), } -impl Signed { +impl<'a> Signed<'a> { /// Returns the transaction regardless of whether it is fully or partially signed - pub fn unwrap(self) -> Transaction { + pub fn unwrap(self) -> &'a Transaction { match self { Signed::Fully(tx) => tx, Signed::Partially(tx) => tx, @@ -31,7 +31,7 @@ impl Signed { /// TODO (aspect) - merge this with `v1` fn above or refactor wallet core to use the script engine. /// Sign a transaction using schnorr #[allow(clippy::result_large_err)] -pub fn sign_with_multiple_v3(tx: Transaction, privkeys: &[[u8; 32]]) -> crate::result::Result { +pub fn sign_with_multiple_v3<'a>(tx: &'a Transaction, privkeys: &[[u8; 32]]) -> crate::result::Result> { let mut map = BTreeMap::new(); for privkey in privkeys { let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey).unwrap(); @@ -44,7 +44,7 @@ pub fn sign_with_multiple_v3(tx: Transaction, privkeys: &[[u8; 32]]) -> crate::r let mut additional_signatures_required = false; { let input_len = tx.inner().inputs.len(); - let (cctx, utxos) = tx.tx_and_utxos(); + let (cctx, utxos) = tx.tx_and_utxos()?; let populated_transaction = PopulatedTransaction::new(&cctx, utxos); for i in 0..input_len { let script_pub_key = match tx.inner().inputs[i].script_public_key() { diff --git a/consensus/client/src/transaction.rs b/consensus/client/src/transaction.rs index 3293497149..6c09bae4eb 100644 --- a/consensus/client/src/transaction.rs +++ b/consensus/client/src/transaction.rs @@ -1,11 +1,11 @@ #![allow(non_snake_case)] use crate::imports::*; -use crate::input::TransactionInput; +use crate::input::{TransactionInput, TransactionInputArrayAsArgT, TransactionInputArrayAsResultT}; use crate::outpoint::TransactionOutpoint; -use crate::output::TransactionOutput; +use crate::output::{TransactionOutput, TransactionOutputArrayAsArgT, TransactionOutputArrayAsResultT}; use crate::result::Result; -use crate::serializable::{numeric, string}; +use crate::serializable::{numeric, string, SerializableTransactionT}; use crate::utxo::{UtxoEntryId, UtxoEntryReference}; use ahash::AHashMap; use kaspa_consensus_core::network::NetworkType; @@ -51,8 +51,8 @@ export interface ITransactionVerboseData { #[wasm_bindgen] extern "C" { - #[wasm_bindgen(typescript_type = "ITransaction")] - pub type ITransaction; + #[wasm_bindgen(typescript_type = "ITransaction | Transaction")] + pub type TransactionT; } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -149,14 +149,14 @@ impl Transaction { } #[wasm_bindgen(constructor)] - pub fn constructor(js_value: &ITransaction) -> std::result::Result { + pub fn constructor(js_value: &TransactionT) -> std::result::Result { Ok(js_value.try_into_owned()?) } #[wasm_bindgen(getter = inputs)] - pub fn get_inputs_as_js_array(&self) -> Array { + pub fn get_inputs_as_js_array(&self) -> TransactionInputArrayAsResultT { let inputs = self.inner.lock().unwrap().inputs.clone().into_iter().map(JsValue::from); - Array::from_iter(inputs) + Array::from_iter(inputs).unchecked_into() } /// Returns a list of unique addresses used by transaction inputs. @@ -179,7 +179,7 @@ impl Transaction { } #[wasm_bindgen(setter = inputs)] - pub fn set_inputs_from_js_array(&mut self, js_value: &JsValue) { + pub fn set_inputs_from_js_array(&mut self, js_value: &TransactionInputArrayAsArgT) { let inputs = Array::from(js_value) .iter() .map(|js_value| { @@ -190,16 +190,16 @@ impl Transaction { } #[wasm_bindgen(getter = outputs)] - pub fn get_outputs_as_js_array(&self) -> Array { + pub fn get_outputs_as_js_array(&self) -> TransactionOutputArrayAsResultT { let outputs = self.inner.lock().unwrap().outputs.clone().into_iter().map(JsValue::from); - Array::from_iter(outputs) + Array::from_iter(outputs).unchecked_into() } #[wasm_bindgen(setter = outputs)] - pub fn set_outputs_from_js_array(&mut self, js_value: &JsValue) { + pub fn set_outputs_from_js_array(&mut self, js_value: &TransactionOutputArrayAsArgT) { let outputs = Array::from(js_value) .iter() - .map(|js_value| TransactionOutput::try_from(&js_value).unwrap_or_else(|err| panic!("invalid transaction output: {err}"))) + .map(|js_value| TryCastFromJs::try_owned_from(&js_value).unwrap_or_else(|err| panic!("invalid transaction output: {err}"))) .collect::>(); self.inner().outputs = outputs; } @@ -214,12 +214,12 @@ impl Transaction { self.inner().version = v; } - #[wasm_bindgen(getter, js_name = lock_time)] + #[wasm_bindgen(getter, js_name = lockTime)] pub fn get_lock_time(&self) -> u64 { self.inner().lock_time } - #[wasm_bindgen(setter, js_name = lock_time)] + #[wasm_bindgen(setter, js_name = lockTime)] pub fn set_lock_time(&self, v: u64) { self.inner().lock_time = v; } @@ -258,13 +258,18 @@ impl Transaction { impl TryCastFromJs for Transaction { type Error = Error; - fn try_cast_from(value: impl AsRef) -> std::result::Result, Self::Error> { - Self::resolve_cast(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve_cast(value, || { if let Some(object) = Object::try_from(value.as_ref()) { if let Some(tx) = object.try_get_value("tx")? { - Transaction::try_cast_from(&tx) + // TODO - optimize to use ref anchor + Transaction::try_captured_cast_from(tx) + // Ok(Cast::value(Transaction::try_owned_from(tx)?)) } else { - let id = object.try_get_cast::("id")?.map(|id| id.into_owned()); + let id = object.try_cast_into::("id")?; let version = object.get_u16("version")?; let lock_time = object.get_u64("lockTime")?; let gas = object.get_u64("gas")?; @@ -285,7 +290,7 @@ impl TryCastFromJs for Transaction { let outputs: Vec = object .get_vec("outputs")? .iter() - .map(|jsv| jsv.try_into()) + .map(TryCastFromJs::try_owned_from) .collect::, Error>>()?; Transaction::new(id, version, inputs, outputs, lock_time, subnetwork_id, gas, payload).map(Into::into) } @@ -342,7 +347,13 @@ impl Transaction { .map(|input| { let previous_outpoint: TransactionOutpoint = input.previous_outpoint.into(); let utxo = utxos.get(previous_outpoint.id()).cloned(); - TransactionInput::new(previous_outpoint, input.signature_script.clone(), input.sequence, input.sig_op_count, utxo) + TransactionInput::new( + previous_outpoint, + Some(input.signature_script.clone()), + input.sequence, + input.sig_op_count, + utxo, + ) }) .collect::>(); let outputs: Vec = tx.outputs.iter().map(|output| output.into()).collect::>(); @@ -359,18 +370,18 @@ impl Transaction { }) } - pub fn tx_and_utxos(&self) -> (cctx::Transaction, Vec) { - let mut utxos = vec![]; + pub fn tx_and_utxos(&self) -> Result<(cctx::Transaction, Vec)> { + let mut inputs = vec![]; let inner = self.inner(); - let inputs: Vec = inner + let utxos: Vec = inner .inputs .clone() .into_iter() .map(|input| { - utxos.push((&input.get_utxo().unwrap().entry()).into()); - input.as_ref().into() + inputs.push(input.as_ref().into()); + Ok(input.get_utxo().ok_or(Error::MissingUtxoEntry)?.entry().as_ref().into()) }) - .collect::>(); + .collect::>>()?; let outputs: Vec = inner.outputs.clone().into_iter().map(|output| output.as_ref().into()).collect::>(); let tx = cctx::Transaction::new( @@ -383,7 +394,37 @@ impl Transaction { inner.payload.clone(), ); - (tx, utxos) + Ok((tx, utxos)) + } + + pub fn utxo_entry_references(&self) -> Result> { + let inner = self.inner(); + let utxo_entry_references = inner + .inputs + .clone() + .into_iter() + .map(|input| input.get_utxo().ok_or(Error::MissingUtxoEntry)) + .collect::>>()?; + Ok(utxo_entry_references) + } + + pub fn outputs(&self) -> Vec { + let inner = self.inner(); + let outputs = inner.outputs.iter().map(|output| output.into()).collect::>(); + outputs + } + + pub fn inputs(&self) -> Vec { + let inner = self.inner(); + let inputs = inner.inputs.iter().map(Into::into).collect::>(); + inputs + } + + pub fn inputs_outputs(&self) -> (Vec, Vec) { + let inner = self.inner(); + let inputs = inner.inputs.iter().map(Into::into).collect::>(); + let outputs = inner.outputs.iter().map(Into::into).collect::>(); + (inputs, outputs) } pub fn set_signature_script(&self, input_index: usize, signature_script: Vec) -> Result<()> { @@ -393,6 +434,14 @@ impl Transaction { self.inner().inputs[input_index].set_signature_script(signature_script); Ok(()) } + + pub fn payload(&self) -> Vec { + self.inner().payload.clone() + } + + pub fn payload_len(&self) -> usize { + self.inner().payload.len() + } } #[wasm_bindgen] @@ -401,7 +450,7 @@ impl Transaction { /// The schema of the JavaScript object is defined by {@link ISerializableTransaction}. /// @see {@link ISerializableTransaction} #[wasm_bindgen(js_name = "serializeToObject")] - pub fn serialize_to_object(&self) -> Result { + pub fn serialize_to_object(&self) -> Result { Ok(numeric::SerializableTransaction::from_client_transaction(self)?.serialize_to_object()?.into()) } diff --git a/consensus/client/src/utils.rs b/consensus/client/src/utils.rs new file mode 100644 index 0000000000..4f543d45bc --- /dev/null +++ b/consensus/client/src/utils.rs @@ -0,0 +1,81 @@ +use crate::imports::*; +use crate::result::Result; +use kaspa_addresses::*; +use kaspa_consensus_core::{ + network::{NetworkType, NetworkTypeT}, + tx::ScriptPublicKeyT, +}; +use kaspa_txscript::{script_class::ScriptClass, standard}; +use kaspa_utils::hex::ToHex; +use kaspa_wasm_core::types::{BinaryT, HexString}; + +/// Creates a new script to pay a transaction output to the specified address. +/// @category Wallet SDK +#[wasm_bindgen(js_name = payToAddressScript)] +pub fn pay_to_address_script(address: &AddressT) -> Result { + let address = Address::try_cast_from(address)?; + Ok(standard::pay_to_address_script(address.as_ref())) +} + +/// Takes a script and returns an equivalent pay-to-script-hash script. +/// @param redeem_script - The redeem script ({@link HexString} or Uint8Array). +/// @category Wallet SDK +#[wasm_bindgen(js_name = payToScriptHashScript)] +pub fn pay_to_script_hash_script(redeem_script: BinaryT) -> Result { + let redeem_script = redeem_script.try_as_vec_u8()?; + Ok(standard::pay_to_script_hash_script(redeem_script.as_slice())) +} + +/// Generates a signature script that fits a pay-to-script-hash script. +/// @param redeem_script - The redeem script ({@link HexString} or Uint8Array). +/// @param signature - The signature ({@link HexString} or Uint8Array). +/// @category Wallet SDK +#[wasm_bindgen(js_name = payToScriptHashSignatureScript)] +pub fn pay_to_script_hash_signature_script(redeem_script: BinaryT, signature: BinaryT) -> Result { + let redeem_script = redeem_script.try_as_vec_u8()?; + let signature = signature.try_as_vec_u8()?; + let script = standard::pay_to_script_hash_signature_script(redeem_script, signature)?; + Ok(script.to_hex().into()) +} + +/// Returns the address encoded in a script public key. +/// @param script_public_key - The script public key ({@link ScriptPublicKey}). +/// @param network - The network type. +/// @category Wallet SDK +#[wasm_bindgen(js_name = addressFromScriptPublicKey)] +pub fn address_from_script_public_key(script_public_key: &ScriptPublicKeyT, network: &NetworkTypeT) -> Result { + let script_public_key = ScriptPublicKey::try_cast_from(script_public_key)?; + let network_type = NetworkType::try_from(network)?; + + match standard::extract_script_pub_key_address(script_public_key.as_ref(), network_type.into()) { + Ok(address) => Ok(AddressOrUndefinedT::from(JsValue::from(address))), + Err(_) => Ok(AddressOrUndefinedT::from(JsValue::UNDEFINED)), + } +} + +/// Returns true if the script passed is a pay-to-pubkey. +/// @param script - The script ({@link HexString} or Uint8Array). +/// @category Wallet SDK +#[wasm_bindgen(js_name = isScriptPayToPubkey)] +pub fn is_script_pay_to_pubkey(script: BinaryT) -> Result { + let script = script.try_as_vec_u8()?; + Ok(ScriptClass::is_pay_to_pubkey(script.as_slice())) +} + +/// Returns returns true if the script passed is an ECDSA pay-to-pubkey. +/// @param script - The script ({@link HexString} or Uint8Array). +/// @category Wallet SDK +#[wasm_bindgen(js_name = isScriptPayToPubkeyECDSA)] +pub fn is_script_pay_to_pubkey_ecdsa(script: BinaryT) -> Result { + let script = script.try_as_vec_u8()?; + Ok(ScriptClass::is_pay_to_pubkey_ecdsa(script.as_slice())) +} + +/// Returns true if the script passed is a pay-to-script-hash (P2SH) format, false otherwise. +/// @param script - The script ({@link HexString} or Uint8Array). +/// @category Wallet SDK +#[wasm_bindgen(js_name = isScriptPayToScriptHash)] +pub fn is_script_pay_to_script_hash(script: BinaryT) -> Result { + let script = script.try_as_vec_u8()?; + Ok(ScriptClass::is_pay_to_script_hash(script.as_slice())) +} diff --git a/consensus/client/src/utxo.rs b/consensus/client/src/utxo.rs index ffa7f7a49c..3f519d067f 100644 --- a/consensus/client/src/utxo.rs +++ b/consensus/client/src/utxo.rs @@ -101,6 +101,12 @@ impl UtxoEntry { } } +impl AsRef for UtxoEntry { + fn as_ref(&self) -> &UtxoEntry { + self + } +} + impl From<&UtxoEntry> for cctx::UtxoEntry { fn from(utxo: &UtxoEntry) -> Self { cctx::UtxoEntry { @@ -136,14 +142,14 @@ impl UtxoEntryReference { self.as_ref().clone() } - #[wasm_bindgen(js_name = "getTransactionId")] - pub fn transaction_id_as_string(&self) -> String { - self.utxo.outpoint.get_transaction_id_as_string() + #[wasm_bindgen(getter)] + pub fn outpoint(&self) -> TransactionOutpoint { + self.utxo.outpoint.clone() } - #[wasm_bindgen(js_name = "getId")] - pub fn id_string(&self) -> String { - self.utxo.outpoint.id_string() + #[wasm_bindgen(getter)] + pub fn address(&self) -> Option
{ + self.utxo.address.clone() } #[wasm_bindgen(getter)] @@ -160,6 +166,11 @@ impl UtxoEntryReference { pub fn block_daa_score(&self) -> u64 { self.utxo.block_daa_score } + + #[wasm_bindgen(getter, js_name = "scriptPublicKey")] + pub fn script_public_key(&self) -> ScriptPublicKey { + self.utxo.script_public_key.clone() + } } impl UtxoEntryReference { @@ -252,7 +263,10 @@ impl TryIntoUtxoEntryReferences for JsValue { impl TryCastFromJs for UtxoEntry { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { Ok(Self::try_ref_from_js_value_as_cast(value)?) } } @@ -372,21 +386,45 @@ impl TryFrom for UtxoEntries { impl TryCastFromJs for UtxoEntryReference { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { if let Ok(utxo_entry) = UtxoEntry::try_ref_from_js_value(&value) { Ok(Self::from(utxo_entry.clone())) } else if let Some(object) = Object::try_from(value.as_ref()) { - let address = object.get_cast::
("address")?.into_owned(); + let address = object.try_cast_into::
("address")?; let outpoint = TransactionOutpoint::try_from(object.get_value("outpoint")?.as_ref())?; let utxo_entry = Object::from(object.get_value("utxoEntry")?); - let amount = utxo_entry.get_u64("amount")?; - let script_public_key = ScriptPublicKey::try_owned_from(utxo_entry.get_value("scriptPublicKey")?)?; - let block_daa_score = utxo_entry.get_u64("blockDaaScore")?; - let is_coinbase = utxo_entry.get_bool("isCoinbase")?; - let utxo_entry = - UtxoEntry { address: Some(address), outpoint, amount, script_public_key, block_daa_score, is_coinbase }; + let utxo_entry = if !utxo_entry.is_undefined() { + let amount = utxo_entry.get_u64("amount").map_err(|_| { + Error::custom("Supplied object does not contain `utxoEntry.amount` property (or it is not a numerical value)") + })?; + let script_public_key = ScriptPublicKey::try_owned_from(utxo_entry.get_value("scriptPublicKey")?) + .map_err(|_|Error::custom("Supplied object does not contain `utxoEntry.scriptPublicKey` property (or it is not a hex string or a ScriptPublicKey class)"))?; + let block_daa_score = utxo_entry.get_u64("blockDaaScore").map_err(|_| { + Error::custom( + "Supplied object does not contain `utxoEntry.blockDaaScore` property (or it is not a numerical value)", + ) + })?; + let is_coinbase = utxo_entry.get_bool("isCoinbase")?; + + UtxoEntry { address, outpoint, amount, script_public_key, block_daa_score, is_coinbase } + } else { + let amount = object.get_u64("amount").map_err(|_| { + Error::custom("Supplied object does not contain `amount` property (or it is not a numerical value)") + })?; + let script_public_key = ScriptPublicKey::try_owned_from(object.get_value("scriptPublicKey")?) + .map_err(|_|Error::custom("Supplied object does not contain `scriptPublicKey` property (or it is not a hex string or a ScriptPublicKey class)"))?; + let block_daa_score = object.get_u64("blockDaaScore").map_err(|_| { + Error::custom("Supplied object does not contain `blockDaaScore` property (or it is not a numerical value)") + })?; + let is_coinbase = object.try_get_bool("isCoinbase")?.unwrap_or(false); + + UtxoEntry { address, outpoint, amount, script_public_key, block_daa_score, is_coinbase } + }; Ok(UtxoEntryReference::from(utxo_entry)) } else { diff --git a/consensus/client/src/vtx.rs b/consensus/client/src/vtx.rs deleted file mode 100644 index e5fdd92363..0000000000 --- a/consensus/client/src/vtx.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::imports::*; -// use crate::serializable::{numeric,string}; -use crate::result::Result; -use kaspa_addresses::Address; -use serde::de::DeserializeOwned; -// use serde::de::DeserializeOwned; - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct VirtualTransactionT -where - T: Clone + serde::Serialize, -{ - //} + Deserialize { - pub version: u32, - pub generator: Option, - pub transactions: Vec, - pub addresses: Option>, -} - -impl VirtualTransactionT -where - T: Clone + Serialize, -{ - pub fn deserialize(json: &str) -> Result - where - T: DeserializeOwned, - { - Ok(serde_json::from_str(json)?) - } - - pub fn serialize(&self) -> String { - serde_json::to_string(self).unwrap() - } -} diff --git a/consensus/core/Cargo.toml b/consensus/core/Cargo.toml index 4e4bd9ea06..44dbedd387 100644 --- a/consensus/core/Cargo.toml +++ b/consensus/core/Cargo.toml @@ -41,6 +41,7 @@ thiserror.workspace = true wasm-bindgen.workspace = true workflow-core.workspace = true workflow-log.workspace = true +workflow-serializer.workspace = true workflow-wasm.workspace = true [dev-dependencies] @@ -53,5 +54,5 @@ web-sys.workspace = true name = "serde_benchmark" harness = false -[lints.clippy] -empty_docs = "allow" +[lints] +workspace = true diff --git a/consensus/core/src/api/stats.rs b/consensus/core/src/api/stats.rs index fd59f09ae5..c2fea489cd 100644 --- a/consensus/core/src/api/stats.rs +++ b/consensus/core/src/api/stats.rs @@ -1,7 +1,7 @@ -use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Default)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct BlockCount { pub header_count: u64, @@ -14,6 +14,26 @@ impl BlockCount { } } +impl Serializer for BlockCount { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.header_count, writer)?; + store!(u64, &self.block_count, writer)?; + + Ok(()) + } +} + +impl Deserializer for BlockCount { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let header_count = load!(u64, reader)?; + let block_count = load!(u64, reader)?; + + Ok(Self { header_count, block_count }) + } +} + #[derive(Clone, Default)] pub struct VirtualStateStats { /// Number of direct parents of virtual diff --git a/consensus/core/src/config/params.rs b/consensus/core/src/config/params.rs index f512cf1e1b..f3479b4c2b 100644 --- a/consensus/core/src/config/params.rs +++ b/consensus/core/src/config/params.rs @@ -309,6 +309,10 @@ pub const MAINNET_PARAMS: Params = Params { "kaspadns.kaspacalc.net", // This DNS seeder is run by supertypo "n-mainnet.kaspa.ws", + // This DNS seeder is run by -gerri- + "dnsseeder-kaspa-mainnet.x-con.at", + // This DNS seeder is run by H@H + "ns-mainnet.kaspa-dnsseeder.net", ], net: NetworkId::new(NetworkType::Mainnet), genesis: GENESIS, @@ -368,6 +372,10 @@ pub const TESTNET_PARAMS: Params = Params { dns_seeders: &[ // This DNS seeder is run by Tiram "seeder1-testnet.kaspad.net", + // This DNS seeder is run by -gerri- + "dnsseeder-kaspa-testnet.x-con.at", + // This DNS seeder is run by H@H + "ns-testnet10.kaspa-dnsseeder.net", ], net: NetworkId::with_suffix(NetworkType::Testnet, 10), genesis: TESTNET_GENESIS, @@ -429,6 +437,10 @@ pub const TESTNET11_PARAMS: Params = Params { "seeder1-testnet-11.kaspad.net", // This DNS seeder is run by supertypo "n-testnet-11.kaspa.ws", + // This DNS seeder is run by -gerri- + "dnsseeder-kaspa-testnet11.x-con.at", + // This DNS seeder is run by H@H + "ns-testnet11.kaspa-dnsseeder.net", ], net: NetworkId::with_suffix(NetworkType::Testnet, 11), genesis: TESTNET11_GENESIS, diff --git a/consensus/core/src/hashing/mod.rs b/consensus/core/src/hashing/mod.rs index aa0ac2d9b6..edcca1d034 100644 --- a/consensus/core/src/hashing/mod.rs +++ b/consensus/core/src/hashing/mod.rs @@ -5,6 +5,8 @@ pub mod header; pub mod sighash; pub mod sighash_type; pub mod tx; +#[cfg(feature = "wasm32-sdk")] +pub mod wasm; pub trait HasherExtensions { /// Writes the len as u64 little endian bytes diff --git a/consensus/core/src/hashing/wasm.rs b/consensus/core/src/hashing/wasm.rs new file mode 100644 index 0000000000..4c9c94b223 --- /dev/null +++ b/consensus/core/src/hashing/wasm.rs @@ -0,0 +1,27 @@ +use super::sighash_type::{self, SigHashType}; +use wasm_bindgen::prelude::*; + +/// Kaspa Sighash types allowed by consensus +/// @category Consensus +#[wasm_bindgen] +pub enum SighashType { + All, + None, + Single, + AllAnyOneCanPay, + NoneAnyOneCanPay, + SingleAnyOneCanPay, +} + +impl From for SigHashType { + fn from(sighash_type: SighashType) -> SigHashType { + match sighash_type { + SighashType::All => sighash_type::SIG_HASH_ALL, + SighashType::None => sighash_type::SIG_HASH_NONE, + SighashType::Single => sighash_type::SIG_HASH_SINGLE, + SighashType::AllAnyOneCanPay => sighash_type::SIG_HASH_ANY_ONE_CAN_PAY, + SighashType::NoneAnyOneCanPay => SigHashType(sighash_type::SIG_HASH_NONE.0 | sighash_type::SIG_HASH_ANY_ONE_CAN_PAY.0), + SighashType::SingleAnyOneCanPay => SigHashType(sighash_type::SIG_HASH_SINGLE.0 | sighash_type::SIG_HASH_ANY_ONE_CAN_PAY.0), + } + } +} diff --git a/consensus/core/src/header.rs b/consensus/core/src/header.rs index b57337afc9..e53de44255 100644 --- a/consensus/core/src/header.rs +++ b/consensus/core/src/header.rs @@ -93,6 +93,12 @@ impl Header { } } +impl AsRef
for Header { + fn as_ref(&self) -> &Header { + self + } +} + impl MemSizeEstimator for Header { fn estimate_mem_bytes(&self) -> usize { size_of::() + self.parents_by_level.iter().map(|l| l.len()).sum::() * size_of::() diff --git a/consensus/core/src/mass/mod.rs b/consensus/core/src/mass/mod.rs index 6e348299c2..f58d104666 100644 --- a/consensus/core/src/mass/mod.rs +++ b/consensus/core/src/mass/mod.rs @@ -4,6 +4,17 @@ use crate::{ }; use kaspa_hashes::HASH_SIZE; +/// Temp enum for the transition phases of KIP9 +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Kip9Version { + /// Initial KIP9 mass calculation, w/o the relaxed formula and summing storage mass and compute mass + Alpha, + + /// Currently proposed KIP9 mass calculation, with the relaxed formula (for the cases `|O| = 1 OR |O| <= |I| <= 2`), + /// and using a maximum operator over storage and compute mass + Beta, +} + // transaction_estimated_serialized_size is the estimated size of a transaction in some // serialization. This has to be deterministic, but not necessarily accurate, since // it's only used as the size component in the transaction and block mass limit diff --git a/consensus/core/src/network.rs b/consensus/core/src/network.rs index ad59adfc3f..d5e9abd244 100644 --- a/consensus/core/src/network.rs +++ b/consensus/core/src/network.rs @@ -400,8 +400,11 @@ impl TryFrom for NetworkId { impl TryCastFromJs for NetworkId { type Error = NetworkIdError; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { if let Some(network_id) = value.as_ref().as_string() { Ok(NetworkId::from_str(&network_id)?) } else { diff --git a/consensus/core/src/sign.rs b/consensus/core/src/sign.rs index dee0d3844c..a40b949e35 100644 --- a/consensus/core/src/sign.rs +++ b/consensus/core/src/sign.rs @@ -1,9 +1,9 @@ use crate::{ hashing::{ sighash::{calc_schnorr_signature_hash, SigHashReusedValues}, - sighash_type::SIG_HASH_ALL, + sighash_type::{SigHashType, SIG_HASH_ALL}, }, - tx::SignableTransaction, + tx::{SignableTransaction, VerifiableTransaction}, }; use itertools::Itertools; use std::collections::BTreeMap; @@ -153,7 +153,20 @@ pub fn sign_with_multiple_v2(mut mutable_tx: SignableTransaction, privkeys: &[[u } } -pub fn verify(tx: &impl crate::tx::VerifiableTransaction) -> Result<(), Error> { +/// Sign a transaction input with a sighash_type using schnorr +pub fn sign_input(tx: &impl VerifiableTransaction, input_index: usize, private_key: &[u8; 32], hash_type: SigHashType) -> Vec { + let mut reused_values = SigHashReusedValues::new(); + + let hash = calc_schnorr_signature_hash(tx, input_index, hash_type, &mut reused_values); + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice()).unwrap(); + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, private_key).unwrap(); + let sig: [u8; 64] = *schnorr_key.sign_schnorr(msg).as_ref(); + + // This represents OP_DATA_65 (since signature length is 64 bytes and SIGHASH_TYPE is one byte) + std::iter::once(65u8).chain(sig).chain([hash_type.to_u8()]).collect() +} + +pub fn verify(tx: &impl VerifiableTransaction) -> Result<(), Error> { let mut reused_values = SigHashReusedValues::new(); for (i, (input, entry)) in tx.populated_inputs().enumerate() { if input.signature_script.is_empty() { diff --git a/consensus/core/src/tx.rs b/consensus/core/src/tx.rs index 3595dcb8b9..1d8dd83342 100644 --- a/consensus/core/src/tx.rs +++ b/consensus/core/src/tx.rs @@ -4,7 +4,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; use kaspa_utils::hex::ToHex; use kaspa_utils::mem_size::MemSizeEstimator; use kaspa_utils::{serde_bytes, serde_bytes_fixed_ref}; -pub use script_public_key::{scriptvec, ScriptPublicKey, ScriptPublicKeyVersion, ScriptPublicKeys, ScriptVec, SCRIPT_VECTOR_SIZE}; +pub use script_public_key::{ + scriptvec, ScriptPublicKey, ScriptPublicKeyT, ScriptPublicKeyVersion, ScriptPublicKeys, ScriptVec, SCRIPT_VECTOR_SIZE, +}; use serde::{Deserialize, Serialize}; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering::SeqCst; @@ -137,8 +139,8 @@ impl Clone for TransactionMass { } impl BorshDeserialize for TransactionMass { - fn deserialize(buf: &mut &[u8]) -> std::io::Result { - let mass: u64 = borsh::BorshDeserialize::deserialize(buf)?; + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let mass: u64 = borsh::BorshDeserialize::deserialize_reader(reader)?; Ok(Self(AtomicU64::new(mass))) } } @@ -163,7 +165,6 @@ pub struct Transaction { pub payload: Vec, #[serde(default)] - #[borsh_skip] // TODO: skipped for now as it is only required for consensus storage and miner grpc mass: TransactionMass, // A field that is used to cache the transaction ID. @@ -634,12 +635,12 @@ mod tests { fn test_spk_borsh() { // Tests for ScriptPublicKey Borsh ser/deser since we manually implemented them let spk = ScriptPublicKey::from_vec(12, vec![32; 20]); - let bin = spk.try_to_vec().unwrap(); + let bin = borsh::to_vec(&spk).unwrap(); let spk2: ScriptPublicKey = BorshDeserialize::try_from_slice(&bin).unwrap(); assert_eq!(spk, spk2); let spk = ScriptPublicKey::from_vec(55455, vec![11; 200]); - let bin = spk.try_to_vec().unwrap(); + let bin = borsh::to_vec(&spk).unwrap(); let spk2: ScriptPublicKey = BorshDeserialize::try_from_slice(&bin).unwrap(); assert_eq!(spk, spk2); } diff --git a/consensus/core/src/tx/script_public_key.rs b/consensus/core/src/tx/script_public_key.rs index 7f3ade6943..dfed2ab5ce 100644 --- a/consensus/core/src/tx/script_public_key.rs +++ b/consensus/core/src/tx/script_public_key.rs @@ -1,6 +1,7 @@ use alloc::borrow::Cow; use borsh::{BorshDeserialize, BorshSerialize}; use core::fmt::Formatter; +use js_sys::Object; use kaspa_utils::{ hex::{FromHex, ToHex}, serde_bytes::FromHexVisitor, @@ -41,6 +42,7 @@ const TS_SCRIPT_PUBLIC_KEY: &'static str = r#" * @category Consensus */ export interface IScriptPublicKey { + version : number; script: HexString; } "#; @@ -328,6 +330,12 @@ impl ScriptPublicKey { } } +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ScriptPublicKey | HexString")] + pub type ScriptPublicKeyT; +} + #[wasm_bindgen] impl ScriptPublicKey { #[wasm_bindgen(constructor)] @@ -357,19 +365,36 @@ impl BorshSerialize for ScriptPublicKey { } impl BorshDeserialize for ScriptPublicKey { - fn deserialize(buf: &mut &[u8]) -> std::io::Result { + fn deserialize_reader(reader: &mut R) -> std::io::Result { // Deserialize into vec first since we have no custom smallvec support - Ok(Self::from_vec(borsh::BorshDeserialize::deserialize(buf)?, borsh::BorshDeserialize::deserialize(buf)?)) + Ok(Self::from_vec(borsh::BorshDeserialize::deserialize_reader(reader)?, borsh::BorshDeserialize::deserialize_reader(reader)?)) } } type CastError = workflow_wasm::error::Error; impl TryCastFromJs for ScriptPublicKey { type Error = workflow_wasm::error::Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { if let Some(hex_str) = value.as_ref().as_string() { Ok(Self::from_str(&hex_str).map_err(CastError::custom)?) + } else if let Some(object) = Object::try_from(value.as_ref()) { + let version = object.try_get_value("version")?.ok_or(CastError::custom( + "ScriptPublicKey must be a hex string or an object with 'version' and 'script' properties", + ))?; + + let version = if let Ok(version) = version.try_as_u16() { + version + } else { + return Err(CastError::custom("Invalid version value '{version:?}'")); + }; + + let script = object.get_vec_u8("script")?; + + Ok(ScriptPublicKey::from_vec(version, script)) } else { Err(CastError::custom(format!("Unable to convert ScriptPublicKey from: {:?}", value.as_ref()))) } @@ -403,12 +428,12 @@ mod tests { fn test_spk_borsh() { // Tests for ScriptPublicKey Borsh ser/deser since we manually implemented them let spk = ScriptPublicKey::from_vec(12, vec![32; 20]); - let bin = spk.try_to_vec().unwrap(); + let bin = borsh::to_vec(&spk).unwrap(); let spk2: ScriptPublicKey = BorshDeserialize::try_from_slice(&bin).unwrap(); assert_eq!(spk, spk2); let spk = ScriptPublicKey::from_vec(55455, vec![11; 200]); - let bin = spk.try_to_vec().unwrap(); + let bin = borsh::to_vec(&spk).unwrap(); let spk2: ScriptPublicKey = BorshDeserialize::try_from_slice(&bin).unwrap(); assert_eq!(spk, spk2); } diff --git a/consensus/pow/Cargo.toml b/consensus/pow/Cargo.toml index 13dc80f378..b2dd714e74 100644 --- a/consensus/pow/Cargo.toml +++ b/consensus/pow/Cargo.toml @@ -28,4 +28,7 @@ criterion.workspace = true [[bench]] name = "bench" -harness = false \ No newline at end of file +harness = false + +[lints] +workspace = true diff --git a/consensus/pow/src/wasm.rs b/consensus/pow/src/wasm.rs index f5179e44a2..92017d6c8a 100644 --- a/consensus/pow/src/wasm.rs +++ b/consensus/pow/src/wasm.rs @@ -1,74 +1,104 @@ use crate::matrix::Matrix; use js_sys::BigInt; use kaspa_consensus_client::Header; +use kaspa_consensus_client::HeaderT; use kaspa_consensus_core::hashing; use kaspa_hashes::Hash; use kaspa_hashes::PowHash; use kaspa_math::Uint256; +use kaspa_utils::hex::FromHex; use kaspa_utils::hex::ToHex; use num::Float; use wasm_bindgen::prelude::*; +use workflow_wasm::convert::TryCastFromJs; use workflow_wasm::error::Error; -use workflow_wasm::prelude::*; use workflow_wasm::result::Result; -/// @category PoW +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Array, typescript_type = "[boolean, bigint]")] + pub type WorkT; +} + +/// Represents a Kaspa header PoW manager +/// @category Mining #[wasm_bindgen(inspectable)] -pub struct State { +pub struct PoW { inner: crate::State, pre_pow_hash: Hash, } #[wasm_bindgen] -impl State { +impl PoW { #[wasm_bindgen(constructor)] - pub fn new(header: &Header) -> Self { + pub fn new(header: &HeaderT, timestamp: Option) -> Result { // this function replicates crate::State::new() but caches // the pre_pow_hash value internally, making it available // via the `pre_pow_hash` property getter. - - // obtain locked inner + let header = Header::try_cast_from(header).map_err(Error::custom)?; + let header = header.as_ref(); let header = header.inner(); + // Get required target from header bits. let target = Uint256::from_compact_target_bits(header.bits); // Zero out the time and nonce. let pre_pow_hash = hashing::header::hash_override_nonce_time(header, 0, 0); // PRE_POW_HASH || TIME || 32 zero byte padding || NONCE - let hasher = PowHash::new(pre_pow_hash, header.timestamp); + let hasher = PowHash::new(pre_pow_hash, timestamp.unwrap_or(header.timestamp)); let matrix = Matrix::generate(pre_pow_hash); - Self { inner: crate::State { matrix, target, hasher }, pre_pow_hash } + Ok(Self { inner: crate::State { matrix, target, hasher }, pre_pow_hash }) } + /// The target based on the provided bits. #[wasm_bindgen(getter)] pub fn target(&self) -> Result { - self.inner.target.try_into().map_err(|err| Error::Custom(format!("{err:?}"))) + self.inner.target.try_into().map_err(|err| Error::custom(format!("{err:?}"))) } - #[wasm_bindgen(js_name=checkPow)] - pub fn check_pow(&self, nonce_jsv: JsValue) -> Result { - let nonce = nonce_jsv.try_as_u64()?; + /// Checks if the computed target meets or exceeds the difficulty specified in the template. + /// @returns A boolean indicating if it reached the target and a bigint representing the reached target. + #[wasm_bindgen(js_name=checkWork)] + pub fn check_work(&self, nonce: u64) -> Result { let (c, v) = self.inner.check_pow(nonce); let array = js_sys::Array::new(); array.push(&JsValue::from(c)); - array.push(&v.to_bigint().map_err(|err| Error::Custom(format!("{err:?}")))?.into()); + array.push(&v.to_bigint().map_err(|err| Error::custom(format!("{err:?}")))?.into()); - Ok(array) + Ok(array.unchecked_into()) } - #[wasm_bindgen(getter = prePowHash)] + /// Hash of the header without timestamp and nonce. + #[wasm_bindgen(getter = prePoWHash)] pub fn get_pre_pow_hash(&self) -> String { self.pre_pow_hash.to_hex() } + + /// Can be used for parsing Stratum templates. + #[wasm_bindgen(js_name=fromRaw)] + pub fn from_raw(pre_pow_hash: &str, timestamp: u64, target_bits: Option) -> Result { + // Convert the pre_pow_hash from hex string to Hash + let pre_pow_hash = Hash::from_hex(pre_pow_hash).map_err(|err| Error::custom(format!("{err:?}")))?; + + // Generate the target from compact target bits if provided + let target = Uint256::from_compact_target_bits(target_bits.unwrap_or_default()); + + // Initialize the matrix and hasher using pre_pow_hash and timestamp + let matrix = Matrix::generate(pre_pow_hash); + let hasher = PowHash::new(pre_pow_hash, timestamp); + + Ok(PoW { inner: crate::State { matrix, target, hasher }, pre_pow_hash }) + } } // https://github.com/tmrlvi/kaspa-miner/blob/bf361d02a46c580f55f46b5dfa773477634a5753/src/client/stratum.rs#L36 const DIFFICULTY_1_TARGET: (u64, i16) = (0xffffu64, 208); // 0xffff 2^208 -/// `calculate_difficulty` is based on set_difficulty function: -/// @category PoW -#[wasm_bindgen(js_name = calculateDifficulty)] -pub fn calculate_difficulty(difficulty: f32) -> std::result::Result { +/// Calculates target from difficulty, based on set_difficulty function on +/// +/// @category Mining +#[wasm_bindgen(js_name = calculateTarget)] +pub fn calculate_target(difficulty: f32) -> Result { let mut buf = [0u64, 0u64, 0u64, 0u64]; let (mantissa, exponent, _) = difficulty.recip().integer_decode(); let new_mantissa = mantissa * DIFFICULTY_1_TARGET.0; @@ -80,10 +110,8 @@ pub fn calculate_difficulty(difficulty: f32) -> std::result::Result> (64 - remainder); // top } else if new_mantissa.leading_zeros() < remainder as u32 { - return Err(JsError::new("Target is too big")); + return Err(Error::custom("Target is too big")); } - // let target_pool = Uint256(buf); - // workflow_log::log_info!("Difficulty: {:?}, Target: 0x{}", difficulty, target_pool.to_hex()); - Ok(Uint256(buf).try_into()?) + Uint256(buf).try_into().map_err(Error::custom) } diff --git a/consensus/src/processes/mass.rs b/consensus/src/processes/mass.rs index 8bb5f3339f..e6198d3462 100644 --- a/consensus/src/processes/mass.rs +++ b/consensus/src/processes/mass.rs @@ -1,19 +1,9 @@ +pub use kaspa_consensus_core::mass::Kip9Version; use kaspa_consensus_core::{ mass::transaction_estimated_serialized_size, tx::{Transaction, VerifiableTransaction}, }; -/// Temp enum for the transition phases of KIP9 -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum Kip9Version { - /// Initial KIP9 mass calculation, w/o the relaxed formula and summing storage mass and compute mass - Alpha, - - /// Currently proposed KIP9 mass calculation, with the relaxed formula (for the cases `|O| = 1 OR |O| <= |I| <= 2`), - /// and using a maximum operator over storage and compute mass - Beta, -} - // TODO (aspect) - review and potentially merge this with the new MassCalculator currently located in the wallet core // (i.e. migrate mass calculator from wallet core here or to consensus core) #[derive(Clone)] diff --git a/consensus/wasm/Cargo.toml b/consensus/wasm/Cargo.toml index ea211f3f94..747d961e24 100644 --- a/consensus/wasm/Cargo.toml +++ b/consensus/wasm/Cargo.toml @@ -35,5 +35,5 @@ wasm-bindgen.workspace = true workflow-wasm.workspace = true workflow-log.workspace = true -[lints.clippy] -empty_docs = "allow" +[lints] +workspace = true diff --git a/crypto/addresses/Cargo.toml b/crypto/addresses/Cargo.toml index dd9f8e3ac5..15fdb975ed 100644 --- a/crypto/addresses/Cargo.toml +++ b/crypto/addresses/Cargo.toml @@ -28,5 +28,5 @@ web-sys.workspace = true name = "bench" harness = false -[lints.clippy] -empty_docs = "allow" +[lints] +workspace = true diff --git a/crypto/addresses/src/bech32.rs b/crypto/addresses/src/bech32.rs index 496e2a549d..32b025343a 100644 --- a/crypto/addresses/src/bech32.rs +++ b/crypto/addresses/src/bech32.rs @@ -122,11 +122,16 @@ impl Address { }) .collect::>(); err?; + if address.len() < 8 { + return Err(AddressError::BadPayload); + } + let (payload_u5, checksum_u5) = address_u5.split_at(address.len() - 8); let fivebit_prefix = prefix.as_str().as_bytes().iter().copied().map(|c| c & 0x1fu8); // Convert to number - let checksum_ = u64::from_be_bytes([vec![0u8; 3], conv5to8(checksum_u5)].concat().try_into().expect("Is exactly 8 bytes")); + let checksum_ = + u64::from_be_bytes([vec![0u8; 3], conv5to8(checksum_u5)].concat().try_into().map_err(|_| AddressError::BadChecksumSize)?); if checksum(payload_u5, fivebit_prefix) != checksum_ { return Err(AddressError::BadChecksum); diff --git a/crypto/addresses/src/lib.rs b/crypto/addresses/src/lib.rs index fdba63ef7f..8aca863866 100644 --- a/crypto/addresses/src/lib.rs +++ b/crypto/addresses/src/lib.rs @@ -28,9 +28,15 @@ pub enum AddressError { #[error("The address contains an invalid character {0}")] DecodingError(char), + #[error("The address checksum is invalid (must be exactly 8 bytes)")] + BadChecksumSize, + #[error("The address checksum is invalid")] BadChecksum, + #[error("The address payload is invalid")] + BadPayload, + #[error("The address is invalid")] InvalidAddress, @@ -49,6 +55,7 @@ impl From for AddressError { /// Address prefix identifying the network type this address belongs to (such as `kaspa`, `kaspatest`, `kaspasim`, `kaspadev`). #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[borsh(use_discriminant = true)] pub enum Prefix { #[serde(rename = "kaspa")] Mainnet, @@ -117,6 +124,7 @@ impl TryFrom<&str> for Prefix { /// @category Address #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[repr(u8)] +#[borsh(use_discriminant = true)] #[wasm_bindgen(js_name = "AddressVersion")] pub enum Version { /// PubKey addresses always have the version byte set to 0 @@ -281,11 +289,10 @@ impl BorshSerialize for Address { } impl BorshDeserialize for Address { - fn deserialize(buf: &mut &[u8]) -> std::io::Result { - // Deserialize into vec first since we have no custom smallvec support - let prefix: Prefix = borsh::BorshDeserialize::deserialize(buf)?; - let version: Version = borsh::BorshDeserialize::deserialize(buf)?; - let payload: Vec = borsh::BorshDeserialize::deserialize(buf)?; + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let prefix: Prefix = borsh::BorshDeserialize::deserialize_reader(reader)?; + let version: Version = borsh::BorshDeserialize::deserialize_reader(reader)?; + let payload: Vec = borsh::BorshDeserialize::deserialize_reader(reader)?; Ok(Self::new(prefix, version, &payload)) } } @@ -489,8 +496,11 @@ impl<'de> Deserialize<'de> for Address { impl TryCastFromJs for Address { type Error = AddressError; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { if let Some(string) = value.as_ref().as_string() { Address::try_from(string) } else if let Some(object) = js_sys::Object::try_from(value.as_ref()) { @@ -512,6 +522,8 @@ extern "C" { pub type AddressOrStringArrayT; #[wasm_bindgen(extends = js_sys::Array, typescript_type = "Address[]")] pub type AddressArrayT; + #[wasm_bindgen(typescript_type = "Address | undefined")] + pub type AddressOrUndefinedT; } impl TryFrom for Vec
{ diff --git a/crypto/hashes/src/lib.rs b/crypto/hashes/src/lib.rs index 6384a96c5d..d9ff47997c 100644 --- a/crypto/hashes/src/lib.rs +++ b/crypto/hashes/src/lib.rs @@ -187,8 +187,11 @@ impl Hash { type TryFromError = workflow_wasm::error::Error; impl TryCastFromJs for Hash { type Error = TryFromError; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { let bytes = value.as_ref().try_as_vec_u8()?; Ok(Hash( <[u8; HASH_SIZE]>::try_from(bytes) diff --git a/crypto/txscript/Cargo.toml b/crypto/txscript/Cargo.toml index 6084df0b2a..e2f492ad38 100644 --- a/crypto/txscript/Cargo.toml +++ b/crypto/txscript/Cargo.toml @@ -9,24 +9,35 @@ include.workspace = true license.workspace = true repository.workspace = true +[features] +wasm32-core = [] +wasm32-sdk = [] + [dependencies] blake2b_simd.workspace = true borsh.workspace = true +cfg-if.workspace = true +hexplay.workspace = true indexmap.workspace = true itertools.workspace = true kaspa-addresses.workspace = true kaspa-consensus-core.workspace = true kaspa-hashes.workspace = true kaspa-txscript-errors.workspace = true +kaspa-utils.workspace = true +kaspa-wasm-core.workspace = true log.workspace = true parking_lot.workspace = true rand.workspace = true secp256k1.workspace = true +serde_json.workspace = true +serde-wasm-bindgen.workspace = true serde.workspace = true sha2.workspace = true smallvec.workspace = true thiserror.workspace = true wasm-bindgen.workspace = true +workflow-wasm.workspace = true [dev-dependencies] criterion.workspace = true diff --git a/crypto/txscript/src/error.rs b/crypto/txscript/src/error.rs new file mode 100644 index 0000000000..7d45fb05e0 --- /dev/null +++ b/crypto/txscript/src/error.rs @@ -0,0 +1,89 @@ +use crate::script_builder; +use thiserror::Error; +use wasm_bindgen::{JsError, JsValue}; +use workflow_wasm::jserror::JsErrorData; + +#[derive(Debug, Error, Clone)] +pub enum Error { + #[error("{0}")] + Custom(String), + + #[error(transparent)] + JsValue(JsErrorData), + + #[error(transparent)] + Wasm(#[from] workflow_wasm::error::Error), + + #[error(transparent)] + ScriptBuilder(#[from] script_builder::ScriptBuilderError), + + #[error("{0}")] + ParseInt(#[from] std::num::ParseIntError), + + #[error(transparent)] + SerdeWasmBindgen(JsErrorData), + + #[error(transparent)] + NetworkType(#[from] kaspa_consensus_core::network::NetworkTypeError), + + #[error("Error converting property `{0}`: {1}")] + Convert(&'static str, String), + + #[error("Error processing JSON: {0}")] + SerdeJson(String), +} + +impl Error { + pub fn custom>(msg: T) -> Self { + Error::Custom(msg.into()) + } + + pub fn convert(prop: &'static str, msg: S) -> Self { + Self::Convert(prop, msg.to_string()) + } +} + +impl From for Error { + fn from(err: String) -> Self { + Self::Custom(err) + } +} + +impl From<&str> for Error { + fn from(err: &str) -> Self { + Self::Custom(err.to_string()) + } +} + +impl From for JsValue { + fn from(value: Error) -> Self { + match value { + Error::JsValue(js_error_data) => js_error_data.into(), + _ => JsValue::from(value.to_string()), + } + } +} + +impl From for Error { + fn from(err: JsValue) -> Self { + Self::JsValue(err.into()) + } +} + +impl From for Error { + fn from(err: JsError) -> Self { + Self::JsValue(err.into()) + } +} + +impl From for Error { + fn from(err: serde_json::Error) -> Self { + Self::SerdeJson(err.to_string()) + } +} + +impl From for Error { + fn from(err: serde_wasm_bindgen::Error) -> Self { + Self::SerdeWasmBindgen(JsValue::from(err).into()) + } +} diff --git a/crypto/txscript/src/lib.rs b/crypto/txscript/src/lib.rs index 77cef45bcd..b145fb90e5 100644 --- a/crypto/txscript/src/lib.rs +++ b/crypto/txscript/src/lib.rs @@ -3,10 +3,14 @@ extern crate core; pub mod caches; mod data_stack; +pub mod error; pub mod opcodes; +pub mod result; pub mod script_builder; pub mod script_class; pub mod standard; +#[cfg(feature = "wasm32-sdk")] +pub mod wasm; use crate::caches::Cache; use crate::data_stack::{DataStack, Stack}; diff --git a/crypto/txscript/src/result.rs b/crypto/txscript/src/result.rs new file mode 100644 index 0000000000..4c8cb83f54 --- /dev/null +++ b/crypto/txscript/src/result.rs @@ -0,0 +1 @@ +pub type Result = std::result::Result; diff --git a/crypto/txscript/src/script_builder.rs b/crypto/txscript/src/script_builder.rs index c7aa05fee7..731c47680e 100644 --- a/crypto/txscript/src/script_builder.rs +++ b/crypto/txscript/src/script_builder.rs @@ -5,6 +5,7 @@ use crate::{ opcodes::{codes::*, OP_1_NEGATE_VAL, OP_DATA_MAX_VAL, OP_DATA_MIN_VAL, OP_SMALL_INT_MAX_VAL}, MAX_SCRIPTS_SIZE, MAX_SCRIPT_ELEMENT_SIZE, }; +use hexplay::{HexView, HexViewBuilder}; use thiserror::Error; /// DEFAULT_SCRIPT_ALLOC is the default size used for the backing array @@ -69,7 +70,7 @@ impl ScriptBuilder { &self.script } - #[cfg(test)] + #[cfg(any(test, target_arch = "wasm32"))] pub fn extend(&mut self, data: &[u8]) { self.script.extend(data); } @@ -248,6 +249,16 @@ impl ScriptBuilder { let trimmed = &buffer[0..trimmed_size]; self.add_data(trimmed) } + + /// Return [`HexViewBuilder`] for the script + pub fn hex_view_builder(&self) -> HexViewBuilder<'_> { + HexViewBuilder::new(&self.script) + } + + /// Return ready to use [`HexView`] for the script + pub fn hex_view(&self, offset: usize, width: usize) -> HexView<'_> { + HexViewBuilder::new(&self.script).address_offset(offset).row_width(width).finish() + } } impl Default for ScriptBuilder { diff --git a/crypto/txscript/src/script_class.rs b/crypto/txscript/src/script_class.rs index 8e7a7796c4..ad61f30d89 100644 --- a/crypto/txscript/src/script_class.rs +++ b/crypto/txscript/src/script_class.rs @@ -17,6 +17,7 @@ pub enum Error { /// Standard classes of script payment in the blockDAG #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[borsh(use_discriminant = true)] #[repr(u8)] pub enum ScriptClass { /// None of the recognized forms diff --git a/crypto/txscript/src/wasm/builder.rs b/crypto/txscript/src/wasm/builder.rs new file mode 100644 index 0000000000..57c6b8b4f4 --- /dev/null +++ b/crypto/txscript/src/wasm/builder.rs @@ -0,0 +1,179 @@ +use crate::result::Result; +use crate::{script_builder as native, standard}; +use kaspa_consensus_core::tx::ScriptPublicKey; +use kaspa_utils::hex::ToHex; +use kaspa_wasm_core::hex::{HexViewConfig, HexViewConfigT}; +use kaspa_wasm_core::types::{BinaryT, HexString}; +use std::cell::{Ref, RefCell, RefMut}; +use std::rc::Rc; +use wasm_bindgen::prelude::wasm_bindgen; +use workflow_wasm::prelude::*; + +/// ScriptBuilder provides a facility for building custom scripts. It allows +/// you to push opcodes, ints, and data while respecting canonical encoding. In +/// general it does not ensure the script will execute correctly, however any +/// data pushes which would exceed the maximum allowed script engine limits and +/// are therefore guaranteed not to execute will not be pushed and will result in +/// the Script function returning an error. +/// @category Consensus +#[derive(Clone)] +#[wasm_bindgen(inspectable)] +pub struct ScriptBuilder { + script_builder: Rc>, +} + +impl ScriptBuilder { + #[inline] + pub fn inner(&self) -> Ref<'_, native::ScriptBuilder> { + self.script_builder.borrow() + } + + #[inline] + pub fn inner_mut(&self) -> RefMut<'_, native::ScriptBuilder> { + self.script_builder.borrow_mut() + } +} + +impl Default for ScriptBuilder { + fn default() -> Self { + Self { script_builder: Rc::new(RefCell::new(native::ScriptBuilder::new())) } + } +} + +#[wasm_bindgen] +impl ScriptBuilder { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self::default() + } + + /// Creates a new ScriptBuilder over an existing script. + /// Supplied script can be represented as an `Uint8Array` or a `HexString`. + #[wasm_bindgen(js_name = "fromScript")] + pub fn from_script(script: BinaryT) -> Result { + let builder = ScriptBuilder::default(); + let script = script.try_as_vec_u8()?; + builder.inner_mut().extend(&script); + + Ok(builder) + } + + /// Pushes the passed opcode to the end of the script. The script will not + /// be modified if pushing the opcode would cause the script to exceed the + /// maximum allowed script engine size. + #[wasm_bindgen(js_name = "addOp")] + pub fn add_op(&self, op: u8) -> Result { + let mut inner = self.inner_mut(); + inner.add_op(op)?; + + Ok(self.clone()) + } + + /// Adds the passed opcodes to the end of the script. + /// Supplied opcodes can be represented as an `Uint8Array` or a `HexString`. + #[wasm_bindgen(js_name = "addOps")] + pub fn add_ops(&self, opcodes: BinaryT) -> Result { + let opcodes = opcodes.try_as_vec_u8()?; + self.inner_mut().add_ops(&opcodes)?; + + Ok(self.clone()) + } + + /// AddData pushes the passed data to the end of the script. It automatically + /// chooses canonical opcodes depending on the length of the data. + /// + /// A zero length buffer will lead to a push of empty data onto the stack (Op0 = OpFalse) + /// and any push of data greater than [`MAX_SCRIPT_ELEMENT_SIZE`](kaspa_txscript::MAX_SCRIPT_ELEMENT_SIZE) will not modify + /// the script since that is not allowed by the script engine. + /// + /// Also, the script will not be modified if pushing the data would cause the script to + /// exceed the maximum allowed script engine size [`MAX_SCRIPTS_SIZE`](kaspa_txscript::MAX_SCRIPTS_SIZE). + #[wasm_bindgen(js_name = "addData")] + pub fn add_data(&self, data: BinaryT) -> Result { + let data = data.try_as_vec_u8()?; + + let mut inner = self.inner_mut(); + inner.add_data(&data)?; + + Ok(self.clone()) + } + + #[wasm_bindgen(js_name = "addI64")] + pub fn add_i64(&self, value: i64) -> Result { + let mut inner = self.inner_mut(); + inner.add_i64(value)?; + + Ok(self.clone()) + } + + #[wasm_bindgen(js_name = "addLockTime")] + pub fn add_lock_time(&self, lock_time: u64) -> Result { + let mut inner = self.inner_mut(); + inner.add_lock_time(lock_time)?; + + Ok(self.clone()) + } + + #[wasm_bindgen(js_name = "addSequence")] + pub fn add_sequence(&self, sequence: u64) -> Result { + let mut inner = self.inner_mut(); + inner.add_sequence(sequence)?; + + Ok(self.clone()) + } + + #[wasm_bindgen(js_name = "canonicalDataSize")] + pub fn canonical_data_size(data: BinaryT) -> Result { + let data = data.try_as_vec_u8()?; + let size = native::ScriptBuilder::canonical_data_size(&data) as u32; + + Ok(size) + } + + /// Get script bytes represented by a hex string. + #[wasm_bindgen(js_name = "toString")] + pub fn to_string_js(&self) -> HexString { + let inner = self.inner(); + + HexString::from(inner.script()) + } + + /// Drains (empties) the script builder, returning the + /// script bytes represented by a hex string. + pub fn drain(&self) -> HexString { + let mut inner = self.inner_mut(); + + HexString::from(inner.drain().as_slice()) + } + + /// Creates an equivalent pay-to-script-hash script. + /// Can be used to create an P2SH address. + /// @see {@link addressFromScriptPublicKey} + #[wasm_bindgen(js_name = "createPayToScriptHashScript")] + pub fn pay_to_script_hash_script(&self) -> ScriptPublicKey { + let inner = self.inner(); + let script = inner.script(); + + standard::pay_to_script_hash_script(script) + } + + /// Generates a signature script that fits a pay-to-script-hash script. + #[wasm_bindgen(js_name = "encodePayToScriptHashSignatureScript")] + pub fn pay_to_script_hash_signature_script(&self, signature: BinaryT) -> Result { + let inner = self.inner(); + let script = inner.script(); + let signature = signature.try_as_vec_u8()?; + let generated_script = standard::pay_to_script_hash_signature_script(script.into(), signature)?; + + Ok(generated_script.to_hex().into()) + } + + #[wasm_bindgen(js_name = "hexView")] + pub fn hex_view(&self, args: Option) -> Result { + let inner = self.inner(); + let script = inner.script(); + + let config = args.map(HexViewConfig::try_from).transpose()?.unwrap_or_default(); + Ok(config.build(script).to_string()) + } +} diff --git a/crypto/txscript/src/wasm/mod.rs b/crypto/txscript/src/wasm/mod.rs new file mode 100644 index 0000000000..e88e580c7d --- /dev/null +++ b/crypto/txscript/src/wasm/mod.rs @@ -0,0 +1,15 @@ +//! +//! WASM32 bindings for the txscript framework components. +//! + +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(any(feature = "wasm32-sdk", feature = "wasm32-core"))] { + pub mod opcodes; + pub mod builder; + + pub use self::opcodes::*; + pub use self::builder::*; + } +} diff --git a/consensus/client/src/script.rs b/crypto/txscript/src/wasm/opcodes.rs similarity index 51% rename from consensus/client/src/script.rs rename to crypto/txscript/src/wasm/opcodes.rs index 7392b1d856..40492cc837 100644 --- a/consensus/client/src/script.rs +++ b/crypto/txscript/src/wasm/opcodes.rs @@ -1,20 +1,12 @@ -use std::cell::{Ref, RefCell, RefMut}; -use std::rc::Rc; +pub use wasm_bindgen::prelude::*; -use kaspa_wasm_core::types::{BinaryT, HexString}; - -use crate::imports::*; -use crate::result::Result; -use kaspa_txscript::script_builder as native; +/// Kaspa Transaction Script Opcodes +/// @see {@link ScriptBuilder} +/// @category Consensus +#[wasm_bindgen] +pub enum Opcodes { + OpFalse = 0x00, -#[wasm_bindgen(typescript_custom_section)] -const TS_SCRIPT_OPCODES: &'static str = r#" -/** - * Kaspa Transaction Script Opcodes - * @see {@link ScriptBuilder} - * @category Consensus - */ -export enum Opcode { OpData1 = 0x01, OpData2 = 0x02, OpData3 = 0x03, @@ -90,15 +82,17 @@ export enum Opcode { OpData73 = 0x49, OpData74 = 0x4a, OpData75 = 0x4b, + OpPushData1 = 0x4c, OpPushData2 = 0x4d, OpPushData4 = 0x4e, + Op1Negate = 0x4f, - /** - * Reserved - */ + OpReserved = 0x50, - Op1 = 0x51, + + OpTrue = 0x51, + Op2 = 0x52, Op3 = 0x53, Op4 = 0x54, @@ -114,27 +108,21 @@ export enum Opcode { Op14 = 0x5e, Op15 = 0x5f, Op16 = 0x60, + OpNop = 0x61, - /** - * Reserved - */ OpVer = 0x62, OpIf = 0x63, OpNotIf = 0x64, - /** - * Reserved - */ OpVerIf = 0x65, - /** - * Reserved - */ OpVerNotIf = 0x66, + OpElse = 0x67, OpEndIf = 0x68, OpVerify = 0x69, OpReturn = 0x6a, OpToAltStack = 0x6b, OpFromAltStack = 0x6c, + Op2Drop = 0x6d, Op2Dup = 0x6e, Op3Dup = 0x6f, @@ -148,88 +136,57 @@ export enum Opcode { OpNip = 0x77, OpOver = 0x78, OpPick = 0x79, + OpRoll = 0x7a, OpRot = 0x7b, OpSwap = 0x7c, OpTuck = 0x7d, - /** - * Disabled - */ + + /// Splice opcodes. OpCat = 0x7e, - /** - * Disabled - */ OpSubStr = 0x7f, - /** - * Disabled - */ OpLeft = 0x80, - /** - * Disabled - */ OpRight = 0x81, + OpSize = 0x82, - /** - * Disabled - */ + + /// Bitwise logic opcodes. OpInvert = 0x83, - /** - * Disabled - */ OpAnd = 0x84, - /** - * Disabled - */ OpOr = 0x85, - /** - * Disabled - */ OpXor = 0x86, + OpEqual = 0x87, OpEqualVerify = 0x88, + OpReserved1 = 0x89, OpReserved2 = 0x8a, + + /// Numeric related opcodes. Op1Add = 0x8b, Op1Sub = 0x8c, - /** - * Disabled - */ Op2Mul = 0x8d, - /** - * Disabled - */ Op2Div = 0x8e, OpNegate = 0x8f, OpAbs = 0x90, OpNot = 0x91, Op0NotEqual = 0x92, + OpAdd = 0x93, OpSub = 0x94, - /** - * Disabled - */ OpMul = 0x95, - /** - * Disabled - */ OpDiv = 0x96, - /** - * Disabled - */ OpMod = 0x97, - /** - * Disabled - */ OpLShift = 0x98, - /** - * Disabled - */ OpRShift = 0x99, + OpBoolAnd = 0x9a, OpBoolOr = 0x9b, + OpNumEqual = 0x9c, OpNumEqualVerify = 0x9d, OpNumNotEqual = 0x9e, + OpLessThan = 0x9f, OpGreaterThan = 0xa0, OpLessThanOrEqual = 0xa1, @@ -237,10 +194,16 @@ export enum Opcode { OpMin = 0xa3, OpMax = 0xa4, OpWithin = 0xa5, + + /// Undefined opcodes. OpUnknown166 = 0xa6, OpUnknown167 = 0xa7, - OpSha256 = 0xa8, + + /// Crypto opcodes. + OpSHA256 = 0xa8, + OpCheckMultiSigECDSA = 0xa9, + OpBlake2b = 0xaa, OpCheckSigECDSA = 0xab, OpCheckSig = 0xac, @@ -249,6 +212,8 @@ export enum Opcode { OpCheckMultiSigVerify = 0xaf, OpCheckLockTimeVerify = 0xb0, OpCheckSequenceVerify = 0xb1, + + /// Undefined opcodes. OpUnknown178 = 0xb2, OpUnknown179 = 0xb3, OpUnknown180 = 0xb4, @@ -321,6 +286,7 @@ export enum Opcode { OpUnknown247 = 0xf7, OpUnknown248 = 0xf8, OpUnknown249 = 0xf9, + OpSmallInteger = 0xfa, OpPubKeys = 0xfb, OpUnknown252 = 0xfc, @@ -328,130 +294,3 @@ export enum Opcode { OpPubKey = 0xfe, OpInvalidOpCode = 0xff, } - -"#; - -/// -/// ScriptBuilder provides a facility for building custom scripts. It allows -/// you to push opcodes, ints, and data while respecting canonical encoding. In -/// general it does not ensure the script will execute correctly, however any -/// data pushes which would exceed the maximum allowed script engine limits and -/// are therefore guaranteed not to execute will not be pushed and will result in -/// the Script function returning an error. -/// -/// @see {@link Opcode} -/// @category Consensus -#[derive(Clone)] -#[wasm_bindgen(inspectable)] -pub struct ScriptBuilder { - script_builder: Rc>, -} - -impl ScriptBuilder { - #[inline] - pub fn inner(&self) -> Ref<'_, native::ScriptBuilder> { - self.script_builder.borrow() - } - - #[inline] - pub fn inner_mut(&self) -> RefMut<'_, native::ScriptBuilder> { - self.script_builder.borrow_mut() - } -} - -impl Default for ScriptBuilder { - fn default() -> Self { - Self { script_builder: Rc::new(RefCell::new(kaspa_txscript::script_builder::ScriptBuilder::new())) } - } -} - -#[wasm_bindgen] -impl ScriptBuilder { - #[wasm_bindgen(constructor)] - pub fn new() -> Self { - Self::default() - } - - #[wasm_bindgen(getter)] - pub fn data(&self) -> HexString { - self.script() - } - - /// Get script bytes represented by a hex string. - pub fn script(&self) -> HexString { - let inner = self.inner(); - HexString::from(inner.script()) - } - - /// Drains (empties) the script builder, returning the - /// script bytes represented by a hex string. - pub fn drain(&self) -> HexString { - let mut inner = self.inner_mut(); - HexString::from(inner.drain().as_slice()) - } - - #[wasm_bindgen(js_name = canonicalDataSize)] - pub fn canonical_data_size(data: BinaryT) -> Result { - let data = data.try_as_vec_u8()?; - let size = native::ScriptBuilder::canonical_data_size(&data) as u32; - Ok(size) - } - - /// Pushes the passed opcode to the end of the script. The script will not - /// be modified if pushing the opcode would cause the script to exceed the - /// maximum allowed script engine size. - #[wasm_bindgen(js_name = addOp)] - pub fn add_op(&self, op: u8) -> Result { - let mut inner = self.inner_mut(); - inner.add_op(op)?; - Ok(self.clone()) - } - - /// Adds the passed opcodes to the end of the script. - /// Supplied opcodes can be represented as a `Uint8Array` or a `HexString`. - #[wasm_bindgen(js_name = "addOps")] - pub fn add_ops(&self, opcodes: JsValue) -> Result { - let opcodes = opcodes.try_as_vec_u8()?; - self.inner_mut().add_ops(&opcodes)?; - Ok(self.clone()) - } - - /// AddData pushes the passed data to the end of the script. It automatically - /// chooses canonical opcodes depending on the length of the data. - /// - /// A zero length buffer will lead to a push of empty data onto the stack (Op0 = OpFalse) - /// and any push of data greater than [`MAX_SCRIPT_ELEMENT_SIZE`](kaspa_txscript::MAX_SCRIPT_ELEMENT_SIZE) will not modify - /// the script since that is not allowed by the script engine. - /// - /// Also, the script will not be modified if pushing the data would cause the script to - /// exceed the maximum allowed script engine size [`MAX_SCRIPTS_SIZE`](kaspa_txscript::MAX_SCRIPTS_SIZE). - #[wasm_bindgen(js_name = addData)] - pub fn add_data(&self, data: BinaryT) -> Result { - let data = data.try_as_vec_u8()?; - - let mut inner = self.inner_mut(); - inner.add_data(&data)?; - Ok(self.clone()) - } - - #[wasm_bindgen(js_name = addI64)] - pub fn add_i64(&self, value: i64) -> Result { - let mut inner = self.inner_mut(); - inner.add_i64(value)?; - Ok(self.clone()) - } - - #[wasm_bindgen(js_name = addLockTime)] - pub fn add_lock_time(&self, lock_time: u64) -> Result { - let mut inner = self.inner_mut(); - inner.add_lock_time(lock_time)?; - Ok(self.clone()) - } - - #[wasm_bindgen(js_name = addSequence)] - pub fn add_sequence(&self, sequence: u64) -> Result { - let mut inner = self.inner_mut(); - inner.add_sequence(sequence)?; - Ok(self.clone()) - } -} diff --git a/kaspad/src/daemon.rs b/kaspad/src/daemon.rs index 9ba7a62d3b..c13ed1c38a 100644 --- a/kaspad/src/daemon.rs +++ b/kaspad/src/daemon.rs @@ -13,7 +13,9 @@ use kaspa_grpc_server::service::GrpcService; use kaspa_notify::{address::tracker::Tracker, subscription::context::SubscriptionContext}; use kaspa_rpc_service::service::RpcCoreService; use kaspa_txscript::caches::TxScriptCacheCounters; +use kaspa_utils::git; use kaspa_utils::networking::ContextualNetAddress; +use kaspa_utils::sysinfo::SystemInfo; use kaspa_utils_tower::counters::TowerConnectionCounters; use kaspa_addressmanager::AddressManager; @@ -233,7 +235,7 @@ pub fn create_core_with_runtime(runtime: &Runtime, args: &Args, fd_total_budget: let db_dir = app_dir.join(network.to_prefixed()).join(DEFAULT_DATA_DIR); // Print package name and version - info!("{} v{}", env!("CARGO_PKG_NAME"), version()); + info!("{} v{}", env!("CARGO_PKG_NAME"), git::with_short_hash(version())); assert!(!db_dir.to_str().unwrap().is_empty()); info!("Application directory: {}", app_dir.display()); @@ -408,6 +410,8 @@ do you confirm? (answer y/n or pass --yes to the Kaspad command line to confirm Arc::new(perf_monitor_builder.build()) }; + let system_info = SystemInfo::default(); + let notify_service = Arc::new(NotifyService::new(notification_root.clone(), notification_recv, subscription_context.clone())); let index_service: Option> = if args.utxoindex { // Use only a single thread for none-consensus databases @@ -472,6 +476,7 @@ do you confirm? (answer y/n or pass --yes to the Kaspad command line to confirm perf_monitor.clone(), p2p_tower_counters.clone(), grpc_tower_counters.clone(), + system_info, )); let grpc_service_broadcasters: usize = 3; // TODO: add a command line argument or derive from other arg/config/host-related fields let grpc_service = if !args.disable_grpc { diff --git a/metrics/core/src/data.rs b/metrics/core/src/data.rs index c920f0a049..d8030f1f3d 100644 --- a/metrics/core/src/data.rs +++ b/metrics/core/src/data.rs @@ -1,4 +1,7 @@ +use crate::error::Error; +use crate::result::Result; use borsh::{BorshDeserialize, BorshSerialize}; +use kaspa_rpc_core::GetMetricsResponse; use separator::{separated_float, separated_int, separated_uint_with_output, Separatable}; use serde::{Deserialize, Serialize}; use workflow_core::enums::Describe; @@ -37,10 +40,6 @@ impl MetricGroup { } impl MetricGroup { - pub fn iter() -> impl Iterator { - [MetricGroup::System, MetricGroup::Storage, MetricGroup::Connections, MetricGroup::Network].into_iter() - } - pub fn metrics(&self) -> impl Iterator { match self { MetricGroup::System => [ @@ -56,6 +55,7 @@ impl MetricGroup { Metric::NodeDiskIoReadPerSec, Metric::NodeDiskIoWriteBytes, Metric::NodeDiskIoWritePerSec, + Metric::NodeStorageSizeBytes, ] .as_slice() .iter(), @@ -127,7 +127,8 @@ impl From for MetricGroup { | Metric::NodeDiskIoReadBytes | Metric::NodeDiskIoWriteBytes | Metric::NodeDiskIoReadPerSec - | Metric::NodeDiskIoWritePerSec => MetricGroup::Storage, + | Metric::NodeDiskIoWritePerSec + | Metric::NodeStorageSizeBytes => MetricGroup::Storage, // -- Metric::NodeBorshLiveConnections | Metric::NodeBorshConnectionAttempts @@ -194,6 +195,7 @@ pub enum Metric { NodeDiskIoWriteBytes, NodeDiskIoReadPerSec, NodeDiskIoWritePerSec, + NodeStorageSizeBytes, // --- NodeActivePeers, NodeBorshLiveConnections, @@ -252,62 +254,62 @@ pub enum Metric { impl Metric { // TODO - this will be refactored at a later date // as this requires changes and testing in /kos - pub fn group(&self) -> &'static str { - match self { - Metric::NodeCpuUsage - | Metric::NodeResidentSetSizeBytes - | Metric::NodeVirtualMemorySizeBytes - | Metric::NodeFileHandlesCount - | Metric::NodeDiskIoReadBytes - | Metric::NodeDiskIoWriteBytes - | Metric::NodeDiskIoReadPerSec - | Metric::NodeDiskIoWritePerSec - | Metric::NodeBorshLiveConnections - | Metric::NodeBorshConnectionAttempts - | Metric::NodeBorshHandshakeFailures - | Metric::NodeJsonLiveConnections - | Metric::NodeJsonConnectionAttempts - | Metric::NodeJsonHandshakeFailures - | Metric::NodeBorshBytesTx - | Metric::NodeBorshBytesRx - | Metric::NodeJsonBytesTx - | Metric::NodeJsonBytesRx - | Metric::NodeP2pBytesTx - | Metric::NodeP2pBytesRx - | Metric::NodeGrpcUserBytesTx - | Metric::NodeGrpcUserBytesRx - | Metric::NodeTotalBytesTx - | Metric::NodeTotalBytesRx - | Metric::NodeBorshBytesTxPerSecond - | Metric::NodeBorshBytesRxPerSecond - | Metric::NodeJsonBytesTxPerSecond - | Metric::NodeJsonBytesRxPerSecond - | Metric::NodeP2pBytesTxPerSecond - | Metric::NodeP2pBytesRxPerSecond - | Metric::NodeGrpcUserBytesTxPerSecond - | Metric::NodeGrpcUserBytesRxPerSecond - | Metric::NodeTotalBytesTxPerSecond - | Metric::NodeTotalBytesRxPerSecond - | Metric::NodeActivePeers => "system", - // -- - Metric::NodeBlocksSubmittedCount - | Metric::NodeHeadersProcessedCount - | Metric::NodeDependenciesProcessedCount - | Metric::NodeBodiesProcessedCount - | Metric::NodeTransactionsProcessedCount - | Metric::NodeChainBlocksProcessedCount - | Metric::NodeMassProcessedCount - | Metric::NodeDatabaseBlocksCount - | Metric::NodeDatabaseHeadersCount - | Metric::NetworkMempoolSize - | Metric::NetworkTransactionsPerSecond - | Metric::NetworkTipHashesCount - | Metric::NetworkDifficulty - | Metric::NetworkPastMedianTime - | Metric::NetworkVirtualParentHashesCount - | Metric::NetworkVirtualDaaScore => "kaspa", - } - } + // pub fn group(&self) -> &'static str { + // match self { + // Metric::NodeCpuUsage + // | Metric::NodeResidentSetSizeBytes + // | Metric::NodeVirtualMemorySizeBytes + // | Metric::NodeFileHandlesCount + // | Metric::NodeDiskIoReadBytes + // | Metric::NodeDiskIoWriteBytes + // | Metric::NodeDiskIoReadPerSec + // | Metric::NodeDiskIoWritePerSec + // | Metric::NodeBorshLiveConnections + // | Metric::NodeBorshConnectionAttempts + // | Metric::NodeBorshHandshakeFailures + // | Metric::NodeJsonLiveConnections + // | Metric::NodeJsonConnectionAttempts + // | Metric::NodeJsonHandshakeFailures + // | Metric::NodeBorshBytesTx + // | Metric::NodeBorshBytesRx + // | Metric::NodeJsonBytesTx + // | Metric::NodeJsonBytesRx + // | Metric::NodeP2pBytesTx + // | Metric::NodeP2pBytesRx + // | Metric::NodeGrpcUserBytesTx + // | Metric::NodeGrpcUserBytesRx + // | Metric::NodeTotalBytesTx + // | Metric::NodeTotalBytesRx + // | Metric::NodeBorshBytesTxPerSecond + // | Metric::NodeBorshBytesRxPerSecond + // | Metric::NodeJsonBytesTxPerSecond + // | Metric::NodeJsonBytesRxPerSecond + // | Metric::NodeP2pBytesTxPerSecond + // | Metric::NodeP2pBytesRxPerSecond + // | Metric::NodeGrpcUserBytesTxPerSecond + // | Metric::NodeGrpcUserBytesRxPerSecond + // | Metric::NodeTotalBytesTxPerSecond + // | Metric::NodeTotalBytesRxPerSecond + // | Metric::NodeActivePeers => "system", + // // -- + // Metric::NodeBlocksSubmittedCount + // | Metric::NodeHeadersProcessedCount + // | Metric::NodeDependenciesProcessedCount + // | Metric::NodeBodiesProcessedCount + // | Metric::NodeTransactionsProcessedCount + // | Metric::NodeChainBlocksProcessedCount + // | Metric::NodeMassProcessedCount + // | Metric::NodeDatabaseBlocksCount + // | Metric::NodeDatabaseHeadersCount + // | Metric::NetworkMempoolSize + // | Metric::NetworkTransactionsPerSecond + // | Metric::NetworkTipHashesCount + // | Metric::NetworkDifficulty + // | Metric::NetworkPastMedianTime + // | Metric::NetworkVirtualParentHashesCount + // | Metric::NetworkVirtualDaaScore => "kaspa", + // } + // } pub fn is_key_performance_metric(&self) -> bool { matches!( @@ -362,6 +364,7 @@ impl Metric { Metric::NodeDiskIoWriteBytes => as_mb(f, si, short), Metric::NodeDiskIoReadPerSec => format!("{}/s", as_data_size(f, si)), Metric::NodeDiskIoWritePerSec => format!("{}/s", as_data_size(f, si)), + Metric::NodeStorageSizeBytes => as_gb(f, si, short), // -- Metric::NodeBorshLiveConnections => f.trunc().separated_string(), Metric::NodeBorshConnectionAttempts => f.trunc().separated_string(), @@ -425,6 +428,7 @@ impl Metric { Metric::NodeDiskIoWriteBytes => ("Storage Write", "Stor Write"), Metric::NodeDiskIoReadPerSec => ("Storage Read/s", "Stor Read"), Metric::NodeDiskIoWritePerSec => ("Storage Write/s", "Stor Write"), + Metric::NodeStorageSizeBytes => ("Storage Size", "Stor Size"), // -- Metric::NodeActivePeers => ("Active p2p Peers", "Peers"), Metric::NodeBorshLiveConnections => ("Borsh Active Connections", "Borsh Conn"), @@ -493,6 +497,7 @@ pub struct MetricsData { pub node_disk_io_write_bytes: u64, pub node_disk_io_read_per_sec: f32, pub node_disk_io_write_per_sec: f32, + pub node_storage_size_bytes: u64, // --- pub node_borsh_live_connections: u32, pub node_borsh_connection_attempts: u64, @@ -512,17 +517,6 @@ pub struct MetricsData { pub node_grpc_user_bytes_rx: u64, pub node_total_bytes_tx: u64, pub node_total_bytes_rx: u64, - - pub node_borsh_bytes_tx_per_second: u64, - pub node_borsh_bytes_rx_per_second: u64, - pub node_json_bytes_tx_per_second: u64, - pub node_json_bytes_rx_per_second: u64, - pub node_p2p_bytes_tx_per_second: u64, - pub node_p2p_bytes_rx_per_second: u64, - pub node_grpc_user_bytes_tx_per_second: u64, - pub node_grpc_user_bytes_rx_per_second: u64, - pub node_total_bytes_tx_per_second: u64, - pub node_total_bytes_rx_per_second: u64, // --- pub node_blocks_submitted_count: u64, pub node_headers_processed_count: u64, @@ -549,6 +543,87 @@ impl MetricsData { } } +impl TryFrom for MetricsData { + type Error = Error; + fn try_from(response: GetMetricsResponse) -> Result { + let GetMetricsResponse { + server_time, + consensus_metrics, + connection_metrics, + bandwidth_metrics, + process_metrics, + storage_metrics, + custom_metrics: _, + } = response; //rpc.get_metrics(true, true, true, true, true, false).await?; + + let consensus_metrics = consensus_metrics.ok_or(Error::MissingData("Consensus Metrics"))?; + let connection_metrics = connection_metrics.ok_or(Error::MissingData("Connection Metrics"))?; + let bandwidth_metrics = bandwidth_metrics.ok_or(Error::MissingData("Bandwidth Metrics"))?; + let process_metrics = process_metrics.ok_or(Error::MissingData("Process Metrics"))?; + let storage_metrics = storage_metrics.ok_or(Error::MissingData("Storage Metrics"))?; + + Ok(MetricsData { + unixtime_millis: server_time as f64, + + node_blocks_submitted_count: consensus_metrics.node_blocks_submitted_count, + node_headers_processed_count: consensus_metrics.node_headers_processed_count, + node_dependencies_processed_count: consensus_metrics.node_dependencies_processed_count, + node_bodies_processed_count: consensus_metrics.node_bodies_processed_count, + node_transactions_processed_count: consensus_metrics.node_transactions_processed_count, + node_chain_blocks_processed_count: consensus_metrics.node_chain_blocks_processed_count, + node_mass_processed_count: consensus_metrics.node_mass_processed_count, + // -- + node_database_blocks_count: consensus_metrics.node_database_blocks_count, + node_database_headers_count: consensus_metrics.node_database_headers_count, + network_mempool_size: consensus_metrics.network_mempool_size, + network_tip_hashes_count: consensus_metrics.network_tip_hashes_count, + network_difficulty: consensus_metrics.network_difficulty, + network_past_median_time: consensus_metrics.network_past_median_time, + network_virtual_parent_hashes_count: consensus_metrics.network_virtual_parent_hashes_count, + network_virtual_daa_score: consensus_metrics.network_virtual_daa_score, + + node_borsh_live_connections: connection_metrics.borsh_live_connections, + node_borsh_connection_attempts: connection_metrics.borsh_connection_attempts, + node_borsh_handshake_failures: connection_metrics.borsh_handshake_failures, + node_json_live_connections: connection_metrics.json_live_connections, + node_json_connection_attempts: connection_metrics.json_connection_attempts, + node_json_handshake_failures: connection_metrics.json_handshake_failures, + node_active_peers: connection_metrics.active_peers, + + node_borsh_bytes_tx: bandwidth_metrics.borsh_bytes_tx, + node_borsh_bytes_rx: bandwidth_metrics.borsh_bytes_rx, + node_json_bytes_tx: bandwidth_metrics.json_bytes_tx, + node_json_bytes_rx: bandwidth_metrics.json_bytes_rx, + node_p2p_bytes_tx: bandwidth_metrics.p2p_bytes_tx, + node_p2p_bytes_rx: bandwidth_metrics.p2p_bytes_rx, + node_grpc_user_bytes_tx: bandwidth_metrics.grpc_bytes_tx, + node_grpc_user_bytes_rx: bandwidth_metrics.grpc_bytes_rx, + + node_total_bytes_tx: bandwidth_metrics.borsh_bytes_tx + + bandwidth_metrics.json_bytes_tx + + bandwidth_metrics.p2p_bytes_tx + + bandwidth_metrics.grpc_bytes_tx, + + node_total_bytes_rx: bandwidth_metrics.borsh_bytes_rx + + bandwidth_metrics.json_bytes_rx + + bandwidth_metrics.p2p_bytes_rx + + bandwidth_metrics.grpc_bytes_rx, + + node_resident_set_size_bytes: process_metrics.resident_set_size, + node_virtual_memory_size_bytes: process_metrics.virtual_memory_size, + node_cpu_cores: process_metrics.core_num, + node_cpu_usage: process_metrics.cpu_usage, + node_file_handles: process_metrics.fd_num, + node_disk_io_read_bytes: process_metrics.disk_io_read_bytes, + node_disk_io_write_bytes: process_metrics.disk_io_write_bytes, + node_disk_io_read_per_sec: process_metrics.disk_io_read_per_sec, + node_disk_io_write_per_sec: process_metrics.disk_io_write_per_sec, + + node_storage_size_bytes: storage_metrics.storage_size_bytes, + }) + } +} + #[derive(Default, Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct MetricsSnapshot { pub data: MetricsData, @@ -615,6 +690,8 @@ pub struct MetricsSnapshot { pub network_past_median_time: f64, pub network_virtual_parent_hashes_count: f64, pub network_virtual_daa_score: f64, + // --- + pub node_storage_size_bytes: f64, } impl MetricsSnapshot { @@ -629,6 +706,7 @@ impl MetricsSnapshot { Metric::NodeDiskIoWriteBytes => self.node_disk_io_write_bytes, Metric::NodeDiskIoReadPerSec => self.node_disk_io_read_per_sec, Metric::NodeDiskIoWritePerSec => self.node_disk_io_write_per_sec, + Metric::NodeStorageSizeBytes => self.node_storage_size_bytes, // --- Metric::NodeActivePeers => self.node_active_peers, Metric::NodeBorshLiveConnections => self.node_borsh_active_connections, @@ -725,6 +803,7 @@ impl From<(&MetricsData, &MetricsData)> for MetricsSnapshot { node_disk_io_write_bytes: b.node_disk_io_write_bytes as f64, node_disk_io_read_per_sec: b.node_disk_io_read_per_sec as f64, node_disk_io_write_per_sec: b.node_disk_io_write_per_sec as f64, + node_storage_size_bytes: b.node_storage_size_bytes as f64, // --- node_borsh_active_connections: b.node_borsh_live_connections as f64, node_borsh_connection_attempts: b.node_borsh_connection_attempts as f64, diff --git a/metrics/core/src/error.rs b/metrics/core/src/error.rs index e31142a76b..4c8a441f8d 100644 --- a/metrics/core/src/error.rs +++ b/metrics/core/src/error.rs @@ -6,6 +6,9 @@ pub enum Error { #[error("{0}")] Custom(String), + #[error("Missing metrics data `{0}`")] + MissingData(&'static str), + #[error(transparent)] RpcError(#[from] RpcError), } diff --git a/metrics/core/src/lib.rs b/metrics/core/src/lib.rs index 4a3ca2a0fc..b88dcf0946 100644 --- a/metrics/core/src/lib.rs +++ b/metrics/core/src/lib.rs @@ -6,7 +6,7 @@ pub use data::{Metric, MetricGroup, MetricsData, MetricsSnapshot}; use crate::result::Result; use futures::{pin_mut, select, FutureExt, StreamExt}; -use kaspa_rpc_core::{api::rpc::RpcApi, GetMetricsResponse}; +use kaspa_rpc_core::api::rpc::RpcApi; use std::{ future::Future, pin::Pin, @@ -81,23 +81,31 @@ impl Metrics { }, _ = interval.next().fuse() => { - let last_metrics_data = current_metrics_data; - current_metrics_data = MetricsData::new(unixtime_as_millis_f64()); + // current_metrics_data = MetricsData::new(unixtime_as_millis_f64()); if let Some(rpc) = this.rpc() { - if let Err(err) = this.sample_metrics(rpc.clone(), &mut current_metrics_data).await { - log_trace!("Metrics::sample_metrics() error: {}", err); + // if let Err(err) = this.sample_metrics(rpc.clone(), &mut current_metrics_data).await { + match this.sample_metrics(rpc.clone()).await { + Ok(incoming_data) => { + let last_metrics_data = current_metrics_data; + current_metrics_data = incoming_data; + this.data.lock().unwrap().replace(current_metrics_data.clone()); + + if let Some(sink) = this.sink() { + let snapshot = MetricsSnapshot::from((&last_metrics_data, ¤t_metrics_data)); + if let Some(future) = sink(snapshot) { + future.await.ok(); + } + } + + } + Err(err) => { + // current_metrics_data = last_metrics_data.clone(); + log_trace!("Metrics::sample_metrics() error: {}", err); + } } } - this.data.lock().unwrap().replace(current_metrics_data.clone()); - - if let Some(sink) = this.sink() { - let snapshot = MetricsSnapshot::from((&last_metrics_data, ¤t_metrics_data)); - if let Some(future) = sink(snapshot) { - future.await.ok(); - } - } } } } @@ -114,72 +122,85 @@ impl Metrics { // --- samplers - async fn sample_metrics(self: &Arc, rpc: Arc, data: &mut MetricsData) -> Result<()> { - let GetMetricsResponse { server_time: _, consensus_metrics, connection_metrics, bandwidth_metrics, process_metrics } = - rpc.get_metrics(true, true, true, true).await?; - - if let Some(consensus_metrics) = consensus_metrics { - data.node_blocks_submitted_count = consensus_metrics.node_blocks_submitted_count; - data.node_headers_processed_count = consensus_metrics.node_headers_processed_count; - data.node_dependencies_processed_count = consensus_metrics.node_dependencies_processed_count; - data.node_bodies_processed_count = consensus_metrics.node_bodies_processed_count; - data.node_transactions_processed_count = consensus_metrics.node_transactions_processed_count; - data.node_chain_blocks_processed_count = consensus_metrics.node_chain_blocks_processed_count; - data.node_mass_processed_count = consensus_metrics.node_mass_processed_count; - // -- - data.node_database_blocks_count = consensus_metrics.node_database_blocks_count; - data.node_database_headers_count = consensus_metrics.node_database_headers_count; - data.network_mempool_size = consensus_metrics.network_mempool_size; - data.network_tip_hashes_count = consensus_metrics.network_tip_hashes_count; - data.network_difficulty = consensus_metrics.network_difficulty; - data.network_past_median_time = consensus_metrics.network_past_median_time; - data.network_virtual_parent_hashes_count = consensus_metrics.network_virtual_parent_hashes_count; - data.network_virtual_daa_score = consensus_metrics.network_virtual_daa_score; - } - - if let Some(connection_metrics) = connection_metrics { - data.node_borsh_live_connections = connection_metrics.borsh_live_connections; - data.node_borsh_connection_attempts = connection_metrics.borsh_connection_attempts; - data.node_borsh_handshake_failures = connection_metrics.borsh_handshake_failures; - data.node_json_live_connections = connection_metrics.json_live_connections; - data.node_json_connection_attempts = connection_metrics.json_connection_attempts; - data.node_json_handshake_failures = connection_metrics.json_handshake_failures; - data.node_active_peers = connection_metrics.active_peers; - } - - if let Some(bandwidth_metrics) = bandwidth_metrics { - data.node_borsh_bytes_tx = bandwidth_metrics.borsh_bytes_tx; - data.node_borsh_bytes_rx = bandwidth_metrics.borsh_bytes_rx; - data.node_json_bytes_tx = bandwidth_metrics.json_bytes_tx; - data.node_json_bytes_rx = bandwidth_metrics.json_bytes_rx; - data.node_p2p_bytes_tx = bandwidth_metrics.p2p_bytes_tx; - data.node_p2p_bytes_rx = bandwidth_metrics.p2p_bytes_rx; - data.node_grpc_user_bytes_tx = bandwidth_metrics.grpc_bytes_tx; - data.node_grpc_user_bytes_rx = bandwidth_metrics.grpc_bytes_rx; - - data.node_total_bytes_tx = bandwidth_metrics.borsh_bytes_tx - + bandwidth_metrics.json_bytes_tx - + bandwidth_metrics.p2p_bytes_tx - + bandwidth_metrics.grpc_bytes_tx; - - data.node_total_bytes_rx = bandwidth_metrics.borsh_bytes_rx - + bandwidth_metrics.json_bytes_rx - + bandwidth_metrics.p2p_bytes_rx - + bandwidth_metrics.grpc_bytes_rx; - } - - if let Some(process_metrics) = process_metrics { - data.node_resident_set_size_bytes = process_metrics.resident_set_size; - data.node_virtual_memory_size_bytes = process_metrics.virtual_memory_size; - data.node_cpu_cores = process_metrics.core_num; - data.node_cpu_usage = process_metrics.cpu_usage; - data.node_file_handles = process_metrics.fd_num; - data.node_disk_io_read_bytes = process_metrics.disk_io_read_bytes; - data.node_disk_io_write_bytes = process_metrics.disk_io_write_bytes; - data.node_disk_io_read_per_sec = process_metrics.disk_io_read_per_sec; - data.node_disk_io_write_per_sec = process_metrics.disk_io_write_per_sec; - } - - Ok(()) + async fn sample_metrics(self: &Arc, rpc: Arc) -> Result { + // let GetMetricsResponse { + // server_time: _, + // consensus_metrics, + // connection_metrics, + // bandwidth_metrics, + // process_metrics, + // storage_metrics, + // custom_metrics: _, + // } = + let response = rpc.get_metrics(true, true, true, true, true, false).await?; + + MetricsData::try_from(response) + + // if let Some(consensus_metrics) = consensus_metrics { + // data.node_blocks_submitted_count = consensus_metrics.node_blocks_submitted_count; + // data.node_headers_processed_count = consensus_metrics.node_headers_processed_count; + // data.node_dependencies_processed_count = consensus_metrics.node_dependencies_processed_count; + // data.node_bodies_processed_count = consensus_metrics.node_bodies_processed_count; + // data.node_transactions_processed_count = consensus_metrics.node_transactions_processed_count; + // data.node_chain_blocks_processed_count = consensus_metrics.node_chain_blocks_processed_count; + // data.node_mass_processed_count = consensus_metrics.node_mass_processed_count; + // // -- + // data.node_database_blocks_count = consensus_metrics.node_database_blocks_count; + // data.node_database_headers_count = consensus_metrics.node_database_headers_count; + // data.network_mempool_size = consensus_metrics.network_mempool_size; + // data.network_tip_hashes_count = consensus_metrics.network_tip_hashes_count; + // data.network_difficulty = consensus_metrics.network_difficulty; + // data.network_past_median_time = consensus_metrics.network_past_median_time; + // data.network_virtual_parent_hashes_count = consensus_metrics.network_virtual_parent_hashes_count; + // data.network_virtual_daa_score = consensus_metrics.network_virtual_daa_score; + // } + + // if let Some(connection_metrics) = connection_metrics { + // data.node_borsh_live_connections = connection_metrics.borsh_live_connections; + // data.node_borsh_connection_attempts = connection_metrics.borsh_connection_attempts; + // data.node_borsh_handshake_failures = connection_metrics.borsh_handshake_failures; + // data.node_json_live_connections = connection_metrics.json_live_connections; + // data.node_json_connection_attempts = connection_metrics.json_connection_attempts; + // data.node_json_handshake_failures = connection_metrics.json_handshake_failures; + // data.node_active_peers = connection_metrics.active_peers; + // } + + // if let Some(bandwidth_metrics) = bandwidth_metrics { + // data.node_borsh_bytes_tx = bandwidth_metrics.borsh_bytes_tx; + // data.node_borsh_bytes_rx = bandwidth_metrics.borsh_bytes_rx; + // data.node_json_bytes_tx = bandwidth_metrics.json_bytes_tx; + // data.node_json_bytes_rx = bandwidth_metrics.json_bytes_rx; + // data.node_p2p_bytes_tx = bandwidth_metrics.p2p_bytes_tx; + // data.node_p2p_bytes_rx = bandwidth_metrics.p2p_bytes_rx; + // data.node_grpc_user_bytes_tx = bandwidth_metrics.grpc_bytes_tx; + // data.node_grpc_user_bytes_rx = bandwidth_metrics.grpc_bytes_rx; + + // data.node_total_bytes_tx = bandwidth_metrics.borsh_bytes_tx + // + bandwidth_metrics.json_bytes_tx + // + bandwidth_metrics.p2p_bytes_tx + // + bandwidth_metrics.grpc_bytes_tx; + + // data.node_total_bytes_rx = bandwidth_metrics.borsh_bytes_rx + // + bandwidth_metrics.json_bytes_rx + // + bandwidth_metrics.p2p_bytes_rx + // + bandwidth_metrics.grpc_bytes_rx; + // } + + // if let Some(process_metrics) = process_metrics { + // data.node_resident_set_size_bytes = process_metrics.resident_set_size; + // data.node_virtual_memory_size_bytes = process_metrics.virtual_memory_size; + // data.node_cpu_cores = process_metrics.core_num; + // data.node_cpu_usage = process_metrics.cpu_usage; + // data.node_file_handles = process_metrics.fd_num; + // data.node_disk_io_read_bytes = process_metrics.disk_io_read_bytes; + // data.node_disk_io_write_bytes = process_metrics.disk_io_write_bytes; + // data.node_disk_io_read_per_sec = process_metrics.disk_io_read_per_sec; + // data.node_disk_io_write_per_sec = process_metrics.disk_io_write_per_sec; + // } + + // if let Some(storage_metrics) = storage_metrics { + // data.node_storage_size_bytes = storage_metrics.storage_size_bytes; + // } + // Ok(()) } } diff --git a/notify/Cargo.toml b/notify/Cargo.toml index 09d3f58651..3ef23ff86e 100644 --- a/notify/Cargo.toml +++ b/notify/Cargo.toml @@ -34,6 +34,7 @@ thiserror.workspace = true triggered.workspace = true workflow-core.workspace = true workflow-log.workspace = true +workflow-serializer.workspace = true [dev-dependencies] criterion.workspace = true diff --git a/notify/src/scope.rs b/notify/src/scope.rs index 0d9e33544e..1fe7924711 100644 --- a/notify/src/scope.rs +++ b/notify/src/scope.rs @@ -3,6 +3,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use derive_more::Display; use kaspa_addresses::Address; use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; macro_rules! scope_enum { ($(#[$meta:meta])* $vis:vis enum $name:ident { @@ -53,9 +54,38 @@ impl Scope { } } +impl Serializer for Scope { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Scope, self, writer)?; + Ok(()) + } +} + +impl Deserializer for Scope { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + load!(Scope, reader) + } +} + #[derive(Clone, Display, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct BlockAddedScope {} +impl Serializer for BlockAddedScope { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for BlockAddedScope { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct VirtualChainChangedScope { pub include_accepted_transaction_ids: bool, @@ -73,12 +103,56 @@ impl std::fmt::Display for VirtualChainChangedScope { } } +impl Serializer for VirtualChainChangedScope { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(bool, &self.include_accepted_transaction_ids, writer)?; + Ok(()) + } +} + +impl Deserializer for VirtualChainChangedScope { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let include_accepted_transaction_ids = load!(bool, reader)?; + Ok(Self { include_accepted_transaction_ids }) + } +} + #[derive(Clone, Display, Debug, PartialEq, Eq, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct FinalityConflictScope {} +impl Serializer for FinalityConflictScope { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for FinalityConflictScope { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + #[derive(Clone, Display, Debug, PartialEq, Eq, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct FinalityConflictResolvedScope {} +impl Serializer for FinalityConflictResolvedScope { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for FinalityConflictResolvedScope { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + #[derive(Clone, Debug, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct UtxosChangedScope { pub addresses: Vec
, @@ -109,14 +183,86 @@ impl UtxosChangedScope { } } +impl Serializer for UtxosChangedScope { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec
, &self.addresses, writer)?; + Ok(()) + } +} + +impl Deserializer for UtxosChangedScope { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let addresses = load!(Vec
, reader)?; + Ok(Self { addresses }) + } +} + #[derive(Clone, Display, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct SinkBlueScoreChangedScope {} +impl Serializer for SinkBlueScoreChangedScope { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for SinkBlueScoreChangedScope { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + #[derive(Clone, Display, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct VirtualDaaScoreChangedScope {} +impl Serializer for VirtualDaaScoreChangedScope { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for VirtualDaaScoreChangedScope { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + #[derive(Clone, Display, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct PruningPointUtxoSetOverrideScope {} +impl Serializer for PruningPointUtxoSetOverrideScope { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for PruningPointUtxoSetOverrideScope { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + #[derive(Clone, Display, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct NewBlockTemplateScope {} + +impl Serializer for NewBlockTemplateScope { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NewBlockTemplateScope { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} diff --git a/notify/src/subscription/mod.rs b/notify/src/subscription/mod.rs index 6cded477d8..ff468be74b 100644 --- a/notify/src/subscription/mod.rs +++ b/notify/src/subscription/mod.rs @@ -16,6 +16,7 @@ pub mod context; pub mod single; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[borsh(use_discriminant = true)] pub enum Command { Start = 0, Stop = 1, diff --git a/protocol/p2p/src/echo.rs b/protocol/p2p/src/echo.rs index ed03db2044..07a26aac6b 100644 --- a/protocol/p2p/src/echo.rs +++ b/protocol/p2p/src/echo.rs @@ -96,7 +96,7 @@ fn build_dummy_version_message() -> VersionMessage { services: 0, timestamp: unix_now() as i64, address: None, - id: Vec::from(Uuid::new_v4().as_ref()), + id: Vec::from(Uuid::new_v4().as_bytes()), user_agent: String::new(), disable_relay_tx: false, subnetwork_id: None, diff --git a/rothschild/src/main.rs b/rothschild/src/main.rs index ad212b5e4f..d303f1da05 100644 --- a/rothschild/src/main.rs +++ b/rothschild/src/main.rs @@ -13,7 +13,7 @@ use kaspa_consensus_core::{ use kaspa_core::{info, kaspad_env::version, time::unix_now, warn}; use kaspa_grpc_client::{ClientPool, GrpcClient}; use kaspa_notify::subscription::context::SubscriptionContext; -use kaspa_rpc_core::{api::rpc::RpcApi, notify::mode::NotificationMode}; +use kaspa_rpc_core::{api::rpc::RpcApi, notify::mode::NotificationMode, RpcUtxoEntry}; use kaspa_txscript::pay_to_address_script; use parking_lot::Mutex; use rayon::prelude::*; @@ -323,7 +323,7 @@ async fn populate_pending_outpoints_from_mempool( for entry in entries { for entry in entry.sending { for input in entry.transaction.inputs { - pending_outpoints.insert(input.previous_outpoint, now); + pending_outpoints.insert(input.previous_outpoint.into(), now); } } } @@ -337,20 +337,20 @@ async fn fetch_spendable_utxos( ) -> Vec<(TransactionOutpoint, UtxoEntry)> { let resp = rpc_client.get_utxos_by_addresses(vec![kaspa_addr]).await.unwrap(); let dag_info = rpc_client.get_block_dag_info().await.unwrap(); - let mut utxos = Vec::with_capacity(resp.len()); - for resp_entry in resp - .into_iter() - .filter(|resp_entry| is_utxo_spendable(&resp_entry.utxo_entry, dag_info.virtual_daa_score, coinbase_maturity)) + + let mut utxos = resp.into_iter() + .filter(|entry| { + is_utxo_spendable(&entry.utxo_entry, dag_info.virtual_daa_score, coinbase_maturity) + }) + .map(|entry| (TransactionOutpoint::from(entry.outpoint), UtxoEntry::from(entry.utxo_entry))) // Eliminates UTXOs we already tried to spend so we don't try to spend them again in this period - .filter(|utxo| !pending.contains_key(&utxo.outpoint)) - { - utxos.push((resp_entry.outpoint, resp_entry.utxo_entry)); - } + .filter(|(outpoint,_)| !pending.contains_key(outpoint)) + .collect::>(); utxos.sort_by(|a, b| b.1.amount.cmp(&a.1.amount)); utxos } -fn is_utxo_spendable(entry: &UtxoEntry, virtual_daa_score: u64, coinbase_maturity: u64) -> bool { +fn is_utxo_spendable(entry: &RpcUtxoEntry, virtual_daa_score: u64, coinbase_maturity: u64) -> bool { let needed_confs = if !entry.is_coinbase { 10 } else { diff --git a/rpc/core/Cargo.toml b/rpc/core/Cargo.toml index cfa4895f25..f2e9f72f9e 100644 --- a/rpc/core/Cargo.toml +++ b/rpc/core/Cargo.toml @@ -42,6 +42,7 @@ hex.workspace = true js-sys.workspace = true log.workspace = true paste.workspace = true +rand.workspace = true serde-wasm-bindgen.workspace = true serde.workspace = true smallvec.workspace = true @@ -49,10 +50,11 @@ thiserror.workspace = true uuid.workspace = true wasm-bindgen.workspace = true workflow-core.workspace = true +workflow-serializer.workspace = true workflow-wasm.workspace = true [dev-dependencies] serde_json.workspace = true -[lints.clippy] -empty_docs = "allow" +[lints] +workspace = true diff --git a/rpc/core/src/api/connection.rs b/rpc/core/src/api/connection.rs new file mode 100644 index 0000000000..5b4254288d --- /dev/null +++ b/rpc/core/src/api/connection.rs @@ -0,0 +1,7 @@ +use std::sync::Arc; + +pub trait RpcConnection: Send + Sync { + fn id(&self) -> u64; +} + +pub type DynRpcConnection = Arc; diff --git a/rpc/core/src/api/mod.rs b/rpc/core/src/api/mod.rs index 6bc968b46f..1373bd6e0b 100644 --- a/rpc/core/src/api/mod.rs +++ b/rpc/core/src/api/mod.rs @@ -1,3 +1,4 @@ +pub mod connection; pub mod ctl; pub mod notifications; pub mod ops; diff --git a/rpc/core/src/api/notifications.rs b/rpc/core/src/api/notifications.rs index 6449f25c02..e07a7c4d98 100644 --- a/rpc/core/src/api/notifications.rs +++ b/rpc/core/src/api/notifications.rs @@ -1,5 +1,4 @@ use crate::model::message::*; -use borsh::{BorshDeserialize, BorshSerialize}; use derive_more::Display; use kaspa_notify::{ events::EventType, @@ -13,10 +12,11 @@ use kaspa_notify::{ use serde::{Deserialize, Serialize}; use std::sync::Arc; use wasm_bindgen::JsValue; +use workflow_serializer::prelude::*; use workflow_wasm::serde::to_value; full_featured! { -#[derive(Clone, Debug, Display, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Display, Serialize, Deserialize)] pub enum Notification { #[display(fmt = "BlockAdded notification: block hash {}", "_0.block.header.hash")] BlockAdded(BlockAddedNotification), @@ -113,14 +113,92 @@ impl NotificationTrait for Notification { } } -#[cfg(test)] -mod test { - use super::*; +impl Serializer for Notification { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + match self { + Notification::BlockAdded(notification) => { + store!(u16, &0, writer)?; + serialize!(BlockAddedNotification, notification, writer)?; + } + Notification::VirtualChainChanged(notification) => { + store!(u16, &1, writer)?; + serialize!(VirtualChainChangedNotification, notification, writer)?; + } + Notification::FinalityConflict(notification) => { + store!(u16, &2, writer)?; + serialize!(FinalityConflictNotification, notification, writer)?; + } + Notification::FinalityConflictResolved(notification) => { + store!(u16, &3, writer)?; + serialize!(FinalityConflictResolvedNotification, notification, writer)?; + } + Notification::UtxosChanged(notification) => { + store!(u16, &4, writer)?; + serialize!(UtxosChangedNotification, notification, writer)?; + } + Notification::SinkBlueScoreChanged(notification) => { + store!(u16, &5, writer)?; + serialize!(SinkBlueScoreChangedNotification, notification, writer)?; + } + Notification::VirtualDaaScoreChanged(notification) => { + store!(u16, &6, writer)?; + serialize!(VirtualDaaScoreChangedNotification, notification, writer)?; + } + Notification::PruningPointUtxoSetOverride(notification) => { + store!(u16, &7, writer)?; + serialize!(PruningPointUtxoSetOverrideNotification, notification, writer)?; + } + Notification::NewBlockTemplate(notification) => { + store!(u16, &8, writer)?; + serialize!(NewBlockTemplateNotification, notification, writer)?; + } + } + Ok(()) + } +} - #[test] - fn test_notification_from_bytes() { - let bytes = &vec![6, 169, 167, 75, 2, 0, 0, 0, 0][..]; - let notification = Notification::try_from_slice(bytes); - println!("notification: {notification:?}"); +impl Deserializer for Notification { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + match load!(u16, reader)? { + 0 => { + let notification = deserialize!(BlockAddedNotification, reader)?; + Ok(Notification::BlockAdded(notification)) + } + 1 => { + let notification = deserialize!(VirtualChainChangedNotification, reader)?; + Ok(Notification::VirtualChainChanged(notification)) + } + 2 => { + let notification = deserialize!(FinalityConflictNotification, reader)?; + Ok(Notification::FinalityConflict(notification)) + } + 3 => { + let notification = deserialize!(FinalityConflictResolvedNotification, reader)?; + Ok(Notification::FinalityConflictResolved(notification)) + } + 4 => { + let notification = deserialize!(UtxosChangedNotification, reader)?; + Ok(Notification::UtxosChanged(notification)) + } + 5 => { + let notification = deserialize!(SinkBlueScoreChangedNotification, reader)?; + Ok(Notification::SinkBlueScoreChanged(notification)) + } + 6 => { + let notification = deserialize!(VirtualDaaScoreChangedNotification, reader)?; + Ok(Notification::VirtualDaaScoreChanged(notification)) + } + 7 => { + let notification = deserialize!(PruningPointUtxoSetOverrideNotification, reader)?; + Ok(Notification::PruningPointUtxoSetOverride(notification)) + } + 8 => { + let notification = deserialize!(NewBlockTemplateNotification, reader)?; + Ok(Notification::NewBlockTemplate(notification)) + } + _ => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid variant")), + } } } diff --git a/rpc/core/src/api/ops.rs b/rpc/core/src/api/ops.rs index 6d53574062..20a1d877c7 100644 --- a/rpc/core/src/api/ops.rs +++ b/rpc/core/src/api/ops.rs @@ -3,124 +3,133 @@ use kaspa_notify::events::EventType; use serde::{Deserialize, Serialize}; use workflow_core::enums::Describe; -/// Rpc Api version (4 x short values); First short is reserved. -/// The version format is as follows: `[reserved, major, minor, patch]`. -/// The difference in the major version value indicates breaking binary API changes -/// (i.e. changes in non-versioned model data structures) -/// If such change occurs, BorshRPC-client should refuse to connect to the -/// server and should request a client-side upgrade. JsonRPC-client may opt-in to -/// continue interop, but data structures should handle mutations by pre-filtering -/// or using Serde attributes. This applies only to RPC infrastructure that uses internal -/// data structures and does not affect gRPC. gRPC should issue and handle its -/// own versioning. -pub const RPC_API_VERSION: [u16; 4] = [0, 1, 0, 0]; +/// API version. Change in this value should result +/// in the client refusing to connect. +pub const RPC_API_VERSION: u16 = 1; +/// API revision. Change in this value denotes +/// backwards-compatible changes. +pub const RPC_API_REVISION: u16 = 0; #[derive(Describe, Clone, Copy, Debug, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[borsh(use_discriminant = true)] pub enum RpcApiOps { + NoOp = 0, + + // connection control (provisional) + Connect = 1, + Disconnect = 2, + + // subscription management + Subscribe = 3, + Unsubscribe = 4, + + // ~~~ + + // Subscription commands for starting/stopping notifications + NotifyBlockAdded = 10, + NotifyNewBlockTemplate = 11, + NotifyUtxosChanged = 12, + NotifyPruningPointUtxoSetOverride = 13, + NotifyFinalityConflict = 14, + NotifyFinalityConflictResolved = 15, // for uniformity purpose only since subscribing to NotifyFinalityConflict means receiving both FinalityConflict and FinalityConflictResolved + NotifyVirtualDaaScoreChanged = 16, + NotifyVirtualChainChanged = 17, + NotifySinkBlueScoreChanged = 18, + + // Notification ops required by wRPC + + // TODO: Remove these ops and use EventType as NotificationOps when workflow_rpc::server::interface::Interface + // will be generic over a MethodOps and NotificationOps instead of a single Ops param. + BlockAddedNotification = 60, + VirtualChainChangedNotification = 61, + FinalityConflictNotification = 62, + FinalityConflictResolvedNotification = 63, + UtxosChangedNotification = 64, + SinkBlueScoreChangedNotification = 65, + VirtualDaaScoreChangedNotification = 66, + PruningPointUtxoSetOverrideNotification = 67, + NewBlockTemplateNotification = 68, + + // RPC methods /// Ping the node to check if connection is alive - Ping = 0, + Ping = 110, /// Get metrics for consensus information and node performance - GetMetrics, + GetMetrics = 111, + /// Get system information (RAM available, number of cores, available file descriptors) + GetSystemInfo = 112, + /// Get current number of active TCP connections + GetConnections = 113, /// Get state information on the node - GetServerInfo, + GetServerInfo = 114, /// Get the current sync status of the node - GetSyncStatus, + GetSyncStatus = 115, /// Returns the network this Kaspad is connected to (Mainnet, Testnet) - GetCurrentNetwork, + GetCurrentNetwork = 116, /// Extracts a block out of the request message and attempts to add it to the DAG Returns an empty response or an error message - SubmitBlock, + SubmitBlock = 117, /// Returns a "template" by which a miner can mine a new block - GetBlockTemplate, + GetBlockTemplate = 118, /// Returns a list of all the addresses (IP, port) this Kaspad knows and a list of all addresses that are currently banned by this Kaspad - GetPeerAddresses, + GetPeerAddresses = 119, /// Returns the hash of the current selected tip block of the DAG - GetSink, + GetSink = 120, /// Get information about an entry in the node's mempool - GetMempoolEntry, + GetMempoolEntry = 121, /// Get a snapshot of the node's mempool - GetMempoolEntries, + GetMempoolEntries = 122, /// Returns a list of the peers currently connected to this Kaspad, along with some statistics on them - GetConnectedPeerInfo, + GetConnectedPeerInfo = 123, /// Instructs Kaspad to connect to a given IP address. - AddPeer, + AddPeer = 124, /// Extracts a transaction out of the request message and attempts to add it to the mempool Returns an empty response or an error message - SubmitTransaction, + SubmitTransaction = 125, /// Requests info on a block corresponding to a given block hash Returns block info if the block is known. - GetBlock, + GetBlock = 126, // - GetSubnetwork, + GetSubnetwork = 127, // - GetVirtualChainFromBlock, + GetVirtualChainFromBlock = 128, // - GetBlocks, + GetBlocks = 129, /// Returns the amount of blocks in the DAG - GetBlockCount, + GetBlockCount = 130, /// Returns info on the current state of the DAG - GetBlockDagInfo, + GetBlockDagInfo = 131, // - ResolveFinalityConflict, + ResolveFinalityConflict = 132, /// Instructs this node to shut down Returns an empty response or an error message - Shutdown, + Shutdown = 133, // - GetHeaders, + GetHeaders = 134, /// Get a list of available UTXOs for a given address - GetUtxosByAddresses, + GetUtxosByAddresses = 135, /// Get a balance for a given address - GetBalanceByAddress, + GetBalanceByAddress = 136, /// Get a balance for a number of addresses - GetBalancesByAddresses, + GetBalancesByAddresses = 137, // ? - GetSinkBlueScore, + GetSinkBlueScore = 138, /// Ban a specific peer by it's IP address - Ban, + Ban = 139, /// Unban a specific peer by it's IP address - Unban, + Unban = 140, /// Get generic node information - GetInfo, + GetInfo = 141, // - EstimateNetworkHashesPerSecond, + EstimateNetworkHashesPerSecond = 142, /// Get a list of mempool entries that belong to a specific address - GetMempoolEntriesByAddresses, + GetMempoolEntriesByAddresses = 143, /// Get current issuance supply - GetCoinSupply, + GetCoinSupply = 144, /// Get DAA Score timestamp estimate - GetDaaScoreTimestampEstimate, - - // Subscription commands for starting/stopping notifications - NotifyBlockAdded, - NotifyNewBlockTemplate, - NotifyUtxosChanged, - NotifyPruningPointUtxoSetOverride, - NotifyFinalityConflict, - NotifyFinalityConflictResolved, // for uniformity purpose only since subscribing to NotifyFinalityConflict means receiving both FinalityConflict and FinalityConflictResolved - NotifyVirtualDaaScoreChanged, - NotifyVirtualChainChanged, - NotifySinkBlueScoreChanged, - - // ~ - Subscribe, - Unsubscribe, - - // Notification ops required by wRPC - // TODO: Remove these ops and use EventType as NotificationOps when workflow_rpc::server::interface::Interface - // will be generic over a MethodOps and NotificationOps instead of a single Ops param. - BlockAddedNotification, - VirtualChainChangedNotification, - FinalityConflictNotification, - FinalityConflictResolvedNotification, - UtxosChangedNotification, - SinkBlueScoreChangedNotification, - VirtualDaaScoreChangedNotification, - PruningPointUtxoSetOverrideNotification, - NewBlockTemplateNotification, - + GetDaaScoreTimestampEstimate = 145, /// Extracts a transaction out of the request message and attempts to replace a matching transaction in the mempool with it, applying a mandatory Replace by Fee policy - SubmitTransactionReplacement, - - // Fee estimation related commands - GetFeeEstimate, - GetFeeEstimateExperimental, + SubmitTransactionReplacement = 146, + /// Fee estimation + GetFeeEstimate = 147, + /// Fee estimation (experimental) + GetFeeEstimateExperimental = 148, } impl RpcApiOps { diff --git a/rpc/core/src/api/rpc.rs b/rpc/core/src/api/rpc.rs index 8fd26b058f..9f92e3e5a6 100644 --- a/rpc/core/src/api/rpc.rs +++ b/rpc/core/src/api/rpc.rs @@ -4,6 +4,7 @@ //! All data provided by the RCP server can be trusted by the client //! No data submitted by the client to the server can be trusted +use crate::api::connection::DynRpcConnection; use crate::{model::*, notify::connection::ChannelConnection, RpcResult}; use async_trait::async_trait; use downcast::{downcast_sync, AnySync}; @@ -21,10 +22,32 @@ pub const MAX_SAFE_WINDOW_SIZE: u32 = 10_000; pub trait RpcApi: Sync + Send + AnySync { /// async fn ping(&self) -> RpcResult<()> { - self.ping_call(PingRequest {}).await?; + self.ping_call(None, PingRequest {}).await?; Ok(()) } - async fn ping_call(&self, request: PingRequest) -> RpcResult; + async fn ping_call(&self, connection: Option<&DynRpcConnection>, request: PingRequest) -> RpcResult; + + // --- + + async fn get_system_info(&self) -> RpcResult { + Ok(self.get_system_info_call(None, GetSystemInfoRequest {}).await?) + } + async fn get_system_info_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetSystemInfoRequest, + ) -> RpcResult; + + // --- + + async fn get_connections(&self, include_profile_data: bool) -> RpcResult { + self.get_connections_call(None, GetConnectionsRequest { include_profile_data }).await + } + async fn get_connections_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetConnectionsRequest, + ) -> RpcResult; // --- @@ -34,59 +57,100 @@ pub trait RpcApi: Sync + Send + AnySync { connection_metrics: bool, bandwidth_metrics: bool, consensus_metrics: bool, + storage_metrics: bool, + custom_metrics: bool, ) -> RpcResult { - self.get_metrics_call(GetMetricsRequest { process_metrics, connection_metrics, bandwidth_metrics, consensus_metrics }).await - } - async fn get_metrics_call(&self, request: GetMetricsRequest) -> RpcResult; + self.get_metrics_call( + None, + GetMetricsRequest { + process_metrics, + connection_metrics, + bandwidth_metrics, + consensus_metrics, + storage_metrics, + custom_metrics, + }, + ) + .await + } + async fn get_metrics_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetMetricsRequest, + ) -> RpcResult; // get_info alternative that carries only version, network_id (full), is_synced, virtual_daa_score // these are the only variables needed to negotiate a wRPC connection (besides the wRPC handshake) async fn get_server_info(&self) -> RpcResult { - self.get_server_info_call(GetServerInfoRequest {}).await + self.get_server_info_call(None, GetServerInfoRequest {}).await } - async fn get_server_info_call(&self, request: GetServerInfoRequest) -> RpcResult; + async fn get_server_info_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetServerInfoRequest, + ) -> RpcResult; // Get current sync status of the node (should be converted to a notification subscription) async fn get_sync_status(&self) -> RpcResult { - Ok(self.get_sync_status_call(GetSyncStatusRequest {}).await?.is_synced) + Ok(self.get_sync_status_call(None, GetSyncStatusRequest {}).await?.is_synced) } - async fn get_sync_status_call(&self, request: GetSyncStatusRequest) -> RpcResult; + async fn get_sync_status_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetSyncStatusRequest, + ) -> RpcResult; // --- /// Requests the network the node is currently running against. async fn get_current_network(&self) -> RpcResult { - Ok(self.get_current_network_call(GetCurrentNetworkRequest {}).await?.network) + Ok(self.get_current_network_call(None, GetCurrentNetworkRequest {}).await?.network) } - async fn get_current_network_call(&self, request: GetCurrentNetworkRequest) -> RpcResult; + async fn get_current_network_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetCurrentNetworkRequest, + ) -> RpcResult; /// Submit a block into the DAG. /// /// Blocks are generally expected to have been generated using the get_block_template call. - async fn submit_block(&self, block: RpcBlock, allow_non_daa_blocks: bool) -> RpcResult { - self.submit_block_call(SubmitBlockRequest::new(block, allow_non_daa_blocks)).await + async fn submit_block(&self, block: RpcRawBlock, allow_non_daa_blocks: bool) -> RpcResult { + self.submit_block_call(None, SubmitBlockRequest::new(block, allow_non_daa_blocks)).await } - async fn submit_block_call(&self, request: SubmitBlockRequest) -> RpcResult; + async fn submit_block_call( + &self, + connection: Option<&DynRpcConnection>, + request: SubmitBlockRequest, + ) -> RpcResult; /// Request a current block template. /// /// Callers are expected to solve the block template and submit it using the submit_block call. async fn get_block_template(&self, pay_address: RpcAddress, extra_data: RpcExtraData) -> RpcResult { - self.get_block_template_call(GetBlockTemplateRequest::new(pay_address, extra_data)).await + self.get_block_template_call(None, GetBlockTemplateRequest::new(pay_address, extra_data)).await } - async fn get_block_template_call(&self, request: GetBlockTemplateRequest) -> RpcResult; + async fn get_block_template_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetBlockTemplateRequest, + ) -> RpcResult; /// Requests the list of known kaspad addresses in the current network (mainnet, testnet, etc.) async fn get_peer_addresses(&self) -> RpcResult { - self.get_peer_addresses_call(GetPeerAddressesRequest {}).await + self.get_peer_addresses_call(None, GetPeerAddressesRequest {}).await } - async fn get_peer_addresses_call(&self, request: GetPeerAddressesRequest) -> RpcResult; + async fn get_peer_addresses_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetPeerAddressesRequest, + ) -> RpcResult; /// requests the hash of the current virtual's selected parent. async fn get_sink(&self) -> RpcResult { - self.get_sink_call(GetSinkRequest {}).await + self.get_sink_call(None, GetSinkRequest {}).await } - async fn get_sink_call(&self, request: GetSinkRequest) -> RpcResult; + async fn get_sink_call(&self, connection: Option<&DynRpcConnection>, request: GetSinkRequest) -> RpcResult; /// Requests information about a specific transaction in the mempool. async fn get_mempool_entry( @@ -96,64 +160,85 @@ pub trait RpcApi: Sync + Send + AnySync { filter_transaction_pool: bool, ) -> RpcResult { Ok(self - .get_mempool_entry_call(GetMempoolEntryRequest::new(transaction_id, include_orphan_pool, filter_transaction_pool)) + .get_mempool_entry_call(None, GetMempoolEntryRequest::new(transaction_id, include_orphan_pool, filter_transaction_pool)) .await? .mempool_entry) } - async fn get_mempool_entry_call(&self, request: GetMempoolEntryRequest) -> RpcResult; + async fn get_mempool_entry_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetMempoolEntryRequest, + ) -> RpcResult; /// Requests information about all the transactions currently in the mempool. async fn get_mempool_entries(&self, include_orphan_pool: bool, filter_transaction_pool: bool) -> RpcResult> { Ok(self - .get_mempool_entries_call(GetMempoolEntriesRequest::new(include_orphan_pool, filter_transaction_pool)) + .get_mempool_entries_call(None, GetMempoolEntriesRequest::new(include_orphan_pool, filter_transaction_pool)) .await? .mempool_entries) } - async fn get_mempool_entries_call(&self, request: GetMempoolEntriesRequest) -> RpcResult; + async fn get_mempool_entries_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetMempoolEntriesRequest, + ) -> RpcResult; /// requests information about all the p2p peers currently connected to this node. async fn get_connected_peer_info(&self) -> RpcResult { - self.get_connected_peer_info_call(GetConnectedPeerInfoRequest {}).await + self.get_connected_peer_info_call(None, GetConnectedPeerInfoRequest {}).await } - async fn get_connected_peer_info_call(&self, request: GetConnectedPeerInfoRequest) -> RpcResult; + async fn get_connected_peer_info_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetConnectedPeerInfoRequest, + ) -> RpcResult; /// Adds a peer to the node's outgoing connection list. /// /// This will, in most cases, result in the node connecting to said peer. async fn add_peer(&self, peer_address: RpcContextualPeerAddress, is_permanent: bool) -> RpcResult<()> { - self.add_peer_call(AddPeerRequest::new(peer_address, is_permanent)).await?; + self.add_peer_call(None, AddPeerRequest::new(peer_address, is_permanent)).await?; Ok(()) } - async fn add_peer_call(&self, request: AddPeerRequest) -> RpcResult; + async fn add_peer_call(&self, connection: Option<&DynRpcConnection>, request: AddPeerRequest) -> RpcResult; /// Submits a transaction to the mempool. async fn submit_transaction(&self, transaction: RpcTransaction, allow_orphan: bool) -> RpcResult { - Ok(self.submit_transaction_call(SubmitTransactionRequest { transaction, allow_orphan }).await?.transaction_id) + Ok(self.submit_transaction_call(None, SubmitTransactionRequest { transaction, allow_orphan }).await?.transaction_id) } - async fn submit_transaction_call(&self, request: SubmitTransactionRequest) -> RpcResult; + async fn submit_transaction_call( + &self, + connection: Option<&DynRpcConnection>, + request: SubmitTransactionRequest, + ) -> RpcResult; /// Submits a transaction replacement to the mempool, applying a mandatory Replace by Fee policy. /// /// Returns the ID of the inserted transaction and the transaction the submission replaced in the mempool. async fn submit_transaction_replacement(&self, transaction: RpcTransaction) -> RpcResult { - self.submit_transaction_replacement_call(SubmitTransactionReplacementRequest { transaction }).await + self.submit_transaction_replacement_call(None, SubmitTransactionReplacementRequest { transaction }).await } async fn submit_transaction_replacement_call( &self, + connection: Option<&DynRpcConnection>, request: SubmitTransactionReplacementRequest, ) -> RpcResult; /// Requests information about a specific block. async fn get_block(&self, hash: RpcHash, include_transactions: bool) -> RpcResult { - Ok(self.get_block_call(GetBlockRequest::new(hash, include_transactions)).await?.block) + Ok(self.get_block_call(None, GetBlockRequest::new(hash, include_transactions)).await?.block) } - async fn get_block_call(&self, request: GetBlockRequest) -> RpcResult; + async fn get_block_call(&self, connection: Option<&DynRpcConnection>, request: GetBlockRequest) -> RpcResult; /// Requests information about a specific subnetwork. async fn get_subnetwork(&self, subnetwork_id: RpcSubnetworkId) -> RpcResult { - self.get_subnetwork_call(GetSubnetworkRequest::new(subnetwork_id)).await + self.get_subnetwork_call(None, GetSubnetworkRequest::new(subnetwork_id)).await } - async fn get_subnetwork_call(&self, request: GetSubnetworkRequest) -> RpcResult; + async fn get_subnetwork_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetSubnetworkRequest, + ) -> RpcResult; /// Requests the virtual selected parent chain from some `start_hash` to this node's current virtual. async fn get_virtual_chain_from_block( @@ -161,11 +246,15 @@ pub trait RpcApi: Sync + Send + AnySync { start_hash: RpcHash, include_accepted_transaction_ids: bool, ) -> RpcResult { - self.get_virtual_chain_from_block_call(GetVirtualChainFromBlockRequest::new(start_hash, include_accepted_transaction_ids)) - .await + self.get_virtual_chain_from_block_call( + None, + GetVirtualChainFromBlockRequest::new(start_hash, include_accepted_transaction_ids), + ) + .await } async fn get_virtual_chain_from_block_call( &self, + connection: Option<&DynRpcConnection>, request: GetVirtualChainFromBlockRequest, ) -> RpcResult; @@ -176,61 +265,79 @@ pub trait RpcApi: Sync + Send + AnySync { include_blocks: bool, include_transactions: bool, ) -> RpcResult { - self.get_blocks_call(GetBlocksRequest::new(low_hash, include_blocks, include_transactions)).await + self.get_blocks_call(None, GetBlocksRequest::new(low_hash, include_blocks, include_transactions)).await } - async fn get_blocks_call(&self, request: GetBlocksRequest) -> RpcResult; + async fn get_blocks_call(&self, connection: Option<&DynRpcConnection>, request: GetBlocksRequest) -> RpcResult; /// Requests the current number of blocks in this node. /// /// Note that this number may decrease as pruning occurs. async fn get_block_count(&self) -> RpcResult { - self.get_block_count_call(GetBlockCountRequest {}).await + self.get_block_count_call(None, GetBlockCountRequest {}).await } - async fn get_block_count_call(&self, request: GetBlockCountRequest) -> RpcResult; + async fn get_block_count_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetBlockCountRequest, + ) -> RpcResult; /// Requests general information about the current state of this node's DAG. async fn get_block_dag_info(&self) -> RpcResult { - self.get_block_dag_info_call(GetBlockDagInfoRequest {}).await + self.get_block_dag_info_call(None, GetBlockDagInfoRequest {}).await } - async fn get_block_dag_info_call(&self, request: GetBlockDagInfoRequest) -> RpcResult; + async fn get_block_dag_info_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetBlockDagInfoRequest, + ) -> RpcResult; /// async fn resolve_finality_conflict(&self, finality_block_hash: RpcHash) -> RpcResult<()> { - self.resolve_finality_conflict_call(ResolveFinalityConflictRequest::new(finality_block_hash)).await?; + self.resolve_finality_conflict_call(None, ResolveFinalityConflictRequest::new(finality_block_hash)).await?; Ok(()) } async fn resolve_finality_conflict_call( &self, + connection: Option<&DynRpcConnection>, request: ResolveFinalityConflictRequest, ) -> RpcResult; /// Shuts down this node. async fn shutdown(&self) -> RpcResult<()> { - self.shutdown_call(ShutdownRequest {}).await?; + self.shutdown_call(None, ShutdownRequest {}).await?; Ok(()) } - async fn shutdown_call(&self, request: ShutdownRequest) -> RpcResult; + async fn shutdown_call(&self, connection: Option<&DynRpcConnection>, request: ShutdownRequest) -> RpcResult; /// Requests headers between the given `start_hash` and the current virtual, up to the given limit. async fn get_headers(&self, start_hash: RpcHash, limit: u64, is_ascending: bool) -> RpcResult> { - Ok(self.get_headers_call(GetHeadersRequest::new(start_hash, limit, is_ascending)).await?.headers) + Ok(self.get_headers_call(None, GetHeadersRequest::new(start_hash, limit, is_ascending)).await?.headers) } - async fn get_headers_call(&self, request: GetHeadersRequest) -> RpcResult; + async fn get_headers_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetHeadersRequest, + ) -> RpcResult; /// Returns the total balance in unspent transactions towards a given address. /// /// This call is only available when this node was started with `--utxoindex`. async fn get_balance_by_address(&self, address: RpcAddress) -> RpcResult { - Ok(self.get_balance_by_address_call(GetBalanceByAddressRequest::new(address)).await?.balance) + Ok(self.get_balance_by_address_call(None, GetBalanceByAddressRequest::new(address)).await?.balance) } - async fn get_balance_by_address_call(&self, request: GetBalanceByAddressRequest) -> RpcResult; + async fn get_balance_by_address_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetBalanceByAddressRequest, + ) -> RpcResult; /// async fn get_balances_by_addresses(&self, addresses: Vec) -> RpcResult> { - Ok(self.get_balances_by_addresses_call(GetBalancesByAddressesRequest::new(addresses)).await?.entries) + Ok(self.get_balances_by_addresses_call(None, GetBalancesByAddressesRequest::new(addresses)).await?.entries) } async fn get_balances_by_addresses_call( &self, + connection: Option<&DynRpcConnection>, request: GetBalancesByAddressesRequest, ) -> RpcResult; @@ -238,45 +345,54 @@ pub trait RpcApi: Sync + Send + AnySync { /// /// This call is only available when this node was started with `--utxoindex`. async fn get_utxos_by_addresses(&self, addresses: Vec) -> RpcResult> { - Ok(self.get_utxos_by_addresses_call(GetUtxosByAddressesRequest::new(addresses)).await?.entries) + Ok(self.get_utxos_by_addresses_call(None, GetUtxosByAddressesRequest::new(addresses)).await?.entries) } - async fn get_utxos_by_addresses_call(&self, request: GetUtxosByAddressesRequest) -> RpcResult; + async fn get_utxos_by_addresses_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetUtxosByAddressesRequest, + ) -> RpcResult; /// Requests the blue score of the current selected parent of the virtual block. async fn get_sink_blue_score(&self) -> RpcResult { - Ok(self.get_sink_blue_score_call(GetSinkBlueScoreRequest {}).await?.blue_score) + Ok(self.get_sink_blue_score_call(None, GetSinkBlueScoreRequest {}).await?.blue_score) } - async fn get_sink_blue_score_call(&self, request: GetSinkBlueScoreRequest) -> RpcResult; + async fn get_sink_blue_score_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetSinkBlueScoreRequest, + ) -> RpcResult; /// Bans the given ip. async fn ban(&self, ip: RpcIpAddress) -> RpcResult<()> { - self.ban_call(BanRequest::new(ip)).await?; + self.ban_call(None, BanRequest::new(ip)).await?; Ok(()) } - async fn ban_call(&self, request: BanRequest) -> RpcResult; + async fn ban_call(&self, connection: Option<&DynRpcConnection>, request: BanRequest) -> RpcResult; /// Unbans the given ip. async fn unban(&self, ip: RpcIpAddress) -> RpcResult<()> { - self.unban_call(UnbanRequest::new(ip)).await?; + self.unban_call(None, UnbanRequest::new(ip)).await?; Ok(()) } - async fn unban_call(&self, request: UnbanRequest) -> RpcResult; + async fn unban_call(&self, connection: Option<&DynRpcConnection>, request: UnbanRequest) -> RpcResult; /// Returns info about the node. - async fn get_info_call(&self, request: GetInfoRequest) -> RpcResult; async fn get_info(&self) -> RpcResult { - self.get_info_call(GetInfoRequest {}).await + self.get_info_call(None, GetInfoRequest {}).await } + async fn get_info_call(&self, connection: Option<&DynRpcConnection>, request: GetInfoRequest) -> RpcResult; /// async fn estimate_network_hashes_per_second(&self, window_size: u32, start_hash: Option) -> RpcResult { Ok(self - .estimate_network_hashes_per_second_call(EstimateNetworkHashesPerSecondRequest::new(window_size, start_hash)) + .estimate_network_hashes_per_second_call(None, EstimateNetworkHashesPerSecondRequest::new(window_size, start_hash)) .await? .network_hashes_per_second) } async fn estimate_network_hashes_per_second_call( &self, + connection: Option<&DynRpcConnection>, request: EstimateNetworkHashesPerSecondRequest, ) -> RpcResult; @@ -288,30 +404,35 @@ pub trait RpcApi: Sync + Send + AnySync { filter_transaction_pool: bool, ) -> RpcResult> { Ok(self - .get_mempool_entries_by_addresses_call(GetMempoolEntriesByAddressesRequest::new( - addresses, - include_orphan_pool, - filter_transaction_pool, - )) + .get_mempool_entries_by_addresses_call( + None, + GetMempoolEntriesByAddressesRequest::new(addresses, include_orphan_pool, filter_transaction_pool), + ) .await? .entries) } async fn get_mempool_entries_by_addresses_call( &self, + connection: Option<&DynRpcConnection>, request: GetMempoolEntriesByAddressesRequest, ) -> RpcResult; /// async fn get_coin_supply(&self) -> RpcResult { - self.get_coin_supply_call(GetCoinSupplyRequest {}).await + self.get_coin_supply_call(None, GetCoinSupplyRequest {}).await } - async fn get_coin_supply_call(&self, request: GetCoinSupplyRequest) -> RpcResult; + async fn get_coin_supply_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetCoinSupplyRequest, + ) -> RpcResult; async fn get_daa_score_timestamp_estimate(&self, daa_scores: Vec) -> RpcResult> { - Ok(self.get_daa_score_timestamp_estimate_call(GetDaaScoreTimestampEstimateRequest { daa_scores }).await?.timestamps) + Ok(self.get_daa_score_timestamp_estimate_call(None, GetDaaScoreTimestampEstimateRequest { daa_scores }).await?.timestamps) } async fn get_daa_score_timestamp_estimate_call( &self, + connection: Option<&DynRpcConnection>, request: GetDaaScoreTimestampEstimateRequest, ) -> RpcResult; @@ -319,15 +440,20 @@ pub trait RpcApi: Sync + Send + AnySync { // Fee estimation API async fn get_fee_estimate(&self) -> RpcResult { - Ok(self.get_fee_estimate_call(GetFeeEstimateRequest {}).await?.estimate) + Ok(self.get_fee_estimate_call(None, GetFeeEstimateRequest {}).await?.estimate) } - async fn get_fee_estimate_call(&self, request: GetFeeEstimateRequest) -> RpcResult; + async fn get_fee_estimate_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetFeeEstimateRequest, + ) -> RpcResult; async fn get_fee_estimate_experimental(&self, verbose: bool) -> RpcResult { - self.get_fee_estimate_experimental_call(GetFeeEstimateExperimentalRequest { verbose }).await + self.get_fee_estimate_experimental_call(None, GetFeeEstimateExperimentalRequest { verbose }).await } async fn get_fee_estimate_experimental_call( &self, + connection: Option<&DynRpcConnection>, request: GetFeeEstimateExperimentalRequest, ) -> RpcResult; diff --git a/rpc/core/src/convert/block.rs b/rpc/core/src/convert/block.rs index 3f66b617f2..8888fe2bb8 100644 --- a/rpc/core/src/convert/block.rs +++ b/rpc/core/src/convert/block.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::{RpcBlock, RpcError, RpcResult, RpcTransaction}; +use crate::{RpcBlock, RpcError, RpcRawBlock, RpcResult, RpcTransaction}; use kaspa_consensus_core::block::{Block, MutableBlock}; // ---------------------------------------------------------------------------- @@ -10,7 +10,7 @@ use kaspa_consensus_core::block::{Block, MutableBlock}; impl From<&Block> for RpcBlock { fn from(item: &Block) -> Self { Self { - header: (*item.header).clone(), + header: item.header.as_ref().into(), transactions: item.transactions.iter().map(RpcTransaction::from).collect(), // TODO: Implement a populating process inspired from kaspad\app\rpc\rpccontext\verbosedata.go verbose_data: None, @@ -18,28 +18,61 @@ impl From<&Block> for RpcBlock { } } +impl From<&Block> for RpcRawBlock { + fn from(item: &Block) -> Self { + Self { header: item.header.as_ref().into(), transactions: item.transactions.iter().map(RpcTransaction::from).collect() } + } +} + impl From<&MutableBlock> for RpcBlock { fn from(item: &MutableBlock) -> Self { Self { - header: item.header.clone(), + header: item.header.as_ref().into(), transactions: item.transactions.iter().map(RpcTransaction::from).collect(), verbose_data: None, } } } +impl From<&MutableBlock> for RpcRawBlock { + fn from(item: &MutableBlock) -> Self { + Self { header: item.header.as_ref().into(), transactions: item.transactions.iter().map(RpcTransaction::from).collect() } + } +} + +impl From for RpcRawBlock { + fn from(item: MutableBlock) -> Self { + Self { header: item.header.into(), transactions: item.transactions.iter().map(RpcTransaction::from).collect() } + } +} + // ---------------------------------------------------------------------------- // rpc_core to consensus_core // ---------------------------------------------------------------------------- -impl TryFrom<&RpcBlock> for Block { +impl TryFrom for Block { + type Error = RpcError; + fn try_from(item: RpcBlock) -> RpcResult { + Ok(Self { + header: Arc::new(item.header.into()), + transactions: Arc::new( + item.transactions + .into_iter() + .map(kaspa_consensus_core::tx::Transaction::try_from) + .collect::>>()?, + ), + }) + } +} + +impl TryFrom for Block { type Error = RpcError; - fn try_from(item: &RpcBlock) -> RpcResult { + fn try_from(item: RpcRawBlock) -> RpcResult { Ok(Self { - header: Arc::new(item.header.clone()), + header: Arc::new(item.header.into()), transactions: Arc::new( item.transactions - .iter() + .into_iter() .map(kaspa_consensus_core::tx::Transaction::try_from) .collect::>>()?, ), diff --git a/rpc/core/src/convert/tx.rs b/rpc/core/src/convert/tx.rs index 44a9389a4b..20d41d6741 100644 --- a/rpc/core/src/convert/tx.rs +++ b/rpc/core/src/convert/tx.rs @@ -36,7 +36,7 @@ impl From<&TransactionOutput> for RpcTransactionOutput { impl From<&TransactionInput> for RpcTransactionInput { fn from(item: &TransactionInput) -> Self { Self { - previous_outpoint: item.previous_outpoint, + previous_outpoint: item.previous_outpoint.into(), signature_script: item.signature_script.clone(), sequence: item.sequence, sig_op_count: item.sig_op_count, @@ -50,17 +50,17 @@ impl From<&TransactionInput> for RpcTransactionInput { // rpc_core to consensus_core // ---------------------------------------------------------------------------- -impl TryFrom<&RpcTransaction> for Transaction { +impl TryFrom for Transaction { type Error = RpcError; - fn try_from(item: &RpcTransaction) -> RpcResult { + fn try_from(item: RpcTransaction) -> RpcResult { let transaction = Transaction::new( item.version, item.inputs - .iter() + .into_iter() .map(kaspa_consensus_core::tx::TransactionInput::try_from) .collect::>>()?, item.outputs - .iter() + .into_iter() .map(kaspa_consensus_core::tx::TransactionOutput::try_from) .collect::>>()?, item.lock_time, @@ -73,16 +73,16 @@ impl TryFrom<&RpcTransaction> for Transaction { } } -impl TryFrom<&RpcTransactionOutput> for TransactionOutput { +impl TryFrom for TransactionOutput { type Error = RpcError; - fn try_from(item: &RpcTransactionOutput) -> RpcResult { - Ok(Self::new(item.value, item.script_public_key.clone())) + fn try_from(item: RpcTransactionOutput) -> RpcResult { + Ok(Self::new(item.value, item.script_public_key)) } } -impl TryFrom<&RpcTransactionInput> for TransactionInput { +impl TryFrom for TransactionInput { type Error = RpcError; - fn try_from(item: &RpcTransactionInput) -> RpcResult { - Ok(Self::new(item.previous_outpoint, item.signature_script.clone(), item.sequence, item.sig_op_count)) + fn try_from(item: RpcTransactionInput) -> RpcResult { + Ok(Self::new(item.previous_outpoint.into(), item.signature_script, item.sequence, item.sig_op_count)) } } diff --git a/rpc/core/src/convert/utxo.rs b/rpc/core/src/convert/utxo.rs index 305fb0931f..a0376580d7 100644 --- a/rpc/core/src/convert/utxo.rs +++ b/rpc/core/src/convert/utxo.rs @@ -1,6 +1,6 @@ +use crate::RpcUtxoEntry; use crate::RpcUtxosByAddressesEntry; use kaspa_addresses::Prefix; -use kaspa_consensus_core::tx::UtxoEntry; use kaspa_index_core::indexed_utxos::UtxoSetByScriptPublicKey; use kaspa_txscript::extract_script_pub_key_address; @@ -16,8 +16,8 @@ pub fn utxo_set_into_rpc(item: &UtxoSetByScriptPublicKey, prefix: Option .iter() .map(|(outpoint, entry)| RpcUtxosByAddressesEntry { address: address.clone(), - outpoint: *outpoint, - utxo_entry: UtxoEntry::new(entry.amount, script_public_key.clone(), entry.block_daa_score, entry.is_coinbase), + outpoint: (*outpoint).into(), + utxo_entry: RpcUtxoEntry::new(entry.amount, script_public_key.clone(), entry.block_daa_score, entry.is_coinbase), }) .collect::>() }) diff --git a/rpc/core/src/model/address.rs b/rpc/core/src/model/address.rs index 720cb4f86d..e2c069a50b 100644 --- a/rpc/core/src/model/address.rs +++ b/rpc/core/src/model/address.rs @@ -1,11 +1,11 @@ use crate::{RpcTransactionOutpoint, RpcUtxoEntry}; -use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; pub type RpcAddress = kaspa_addresses::Address; /// Represents a UTXO entry of an address returned by the `GetUtxosByAddresses` RPC. -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcUtxosByAddressesEntry { pub address: Option, @@ -13,8 +13,27 @@ pub struct RpcUtxosByAddressesEntry { pub utxo_entry: RpcUtxoEntry, } +impl Serializer for RpcUtxosByAddressesEntry { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; // version + store!(Option, &self.address, writer)?; + serialize!(RpcTransactionOutpoint, &self.outpoint, writer)?; + serialize!(RpcUtxoEntry, &self.utxo_entry, writer) + } +} + +impl Deserializer for RpcUtxosByAddressesEntry { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version: u8 = load!(u8, reader)?; + let address = load!(Option, reader)?; + let outpoint = deserialize!(RpcTransactionOutpoint, reader)?; + let utxo_entry = deserialize!(RpcUtxoEntry, reader)?; + Ok(Self { address, outpoint, utxo_entry }) + } +} + /// Represents a balance of an address returned by the `GetBalancesByAddresses` RPC. -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcBalancesByAddressesEntry { pub address: RpcAddress, @@ -22,3 +41,20 @@ pub struct RpcBalancesByAddressesEntry { /// Balance of `address` if available pub balance: Option, } + +impl Serializer for RpcBalancesByAddressesEntry { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; // version + store!(RpcAddress, &self.address, writer)?; + store!(Option, &self.balance, writer) + } +} + +impl Deserializer for RpcBalancesByAddressesEntry { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version: u8 = load!(u8, reader)?; + let address = load!(RpcAddress, reader)?; + let balance = load!(Option, reader)?; + Ok(Self { address, balance }) + } +} diff --git a/rpc/core/src/model/block.rs b/rpc/core/src/model/block.rs index c4c501afb9..96dbcd335e 100644 --- a/rpc/core/src/model/block.rs +++ b/rpc/core/src/model/block.rs @@ -1,8 +1,18 @@ +use super::RpcRawHeader; use crate::prelude::{RpcHash, RpcHeader, RpcTransaction}; -use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +/// Raw Rpc block type - without a cached header hash and without verbose data. +/// Used for mining APIs (get_block_template & submit_block) +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcRawBlock { + pub header: RpcRawHeader, + pub transactions: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcBlock { pub header: RpcHeader, @@ -10,7 +20,49 @@ pub struct RpcBlock { pub verbose_data: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for RpcBlock { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcHeader, &self.header, writer)?; + serialize!(Vec, &self.transactions, writer)?; + serialize!(Option, &self.verbose_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcBlock { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let header = deserialize!(RpcHeader, reader)?; + let transactions = deserialize!(Vec, reader)?; + let verbose_data = deserialize!(Option, reader)?; + + Ok(Self { header, transactions, verbose_data }) + } +} + +impl Serializer for RpcRawBlock { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcRawHeader, &self.header, writer)?; + serialize!(Vec, &self.transactions, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcRawBlock { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let header = deserialize!(RpcRawHeader, reader)?; + let transactions = deserialize!(Vec, reader)?; + + Ok(Self { header, transactions }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcBlockVerboseData { pub hash: RpcHash, @@ -25,6 +77,53 @@ pub struct RpcBlockVerboseData { pub is_chain_block: bool, } +impl Serializer for RpcBlockVerboseData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(RpcHash, &self.hash, writer)?; + store!(f64, &self.difficulty, writer)?; + store!(RpcHash, &self.selected_parent_hash, writer)?; + store!(Vec, &self.transaction_ids, writer)?; + store!(bool, &self.is_header_only, writer)?; + store!(u64, &self.blue_score, writer)?; + store!(Vec, &self.children_hashes, writer)?; + store!(Vec, &self.merge_set_blues_hashes, writer)?; + store!(Vec, &self.merge_set_reds_hashes, writer)?; + store!(bool, &self.is_chain_block, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcBlockVerboseData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let hash = load!(RpcHash, reader)?; + let difficulty = load!(f64, reader)?; + let selected_parent_hash = load!(RpcHash, reader)?; + let transaction_ids = load!(Vec, reader)?; + let is_header_only = load!(bool, reader)?; + let blue_score = load!(u64, reader)?; + let children_hashes = load!(Vec, reader)?; + let merge_set_blues_hashes = load!(Vec, reader)?; + let merge_set_reds_hashes = load!(Vec, reader)?; + let is_chain_block = load!(bool, reader)?; + + Ok(Self { + hash, + difficulty, + selected_parent_hash, + transaction_ids, + is_header_only, + blue_score, + children_hashes, + merge_set_blues_hashes, + merge_set_reds_hashes, + is_chain_block, + }) + } +} + cfg_if::cfg_if! { if #[cfg(feature = "wasm32-sdk")] { use wasm_bindgen::prelude::*; diff --git a/rpc/core/src/model/feerate_estimate.rs b/rpc/core/src/model/feerate_estimate.rs index f9de9de90d..cf97a4b36e 100644 --- a/rpc/core/src/model/feerate_estimate.rs +++ b/rpc/core/src/model/feerate_estimate.rs @@ -1,5 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; #[derive(Clone, Copy, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] @@ -11,7 +12,7 @@ pub struct RpcFeerateBucket { pub estimated_seconds: f64, } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcFeeEstimate { /// *Top-priority* feerate bucket. Provides an estimation of the feerate required for sub-second DAG inclusion. @@ -42,7 +43,27 @@ impl RpcFeeEstimate { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for RpcFeeEstimate { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcFeerateBucket, &self.priority_bucket, writer)?; + store!(Vec, &self.normal_buckets, writer)?; + store!(Vec, &self.low_buckets, writer)?; + Ok(()) + } +} + +impl Deserializer for RpcFeeEstimate { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let priority_bucket = load!(RpcFeerateBucket, reader)?; + let normal_buckets = load!(Vec, reader)?; + let low_buckets = load!(Vec, reader)?; + Ok(Self { priority_bucket, normal_buckets, low_buckets }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcFeeEstimateVerboseExperimentalData { pub mempool_ready_transactions_count: u64, @@ -53,3 +74,36 @@ pub struct RpcFeeEstimateVerboseExperimentalData { pub next_block_template_feerate_median: f64, pub next_block_template_feerate_max: f64, } + +impl Serializer for RpcFeeEstimateVerboseExperimentalData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.mempool_ready_transactions_count, writer)?; + store!(u64, &self.mempool_ready_transactions_total_mass, writer)?; + store!(u64, &self.network_mass_per_second, writer)?; + store!(f64, &self.next_block_template_feerate_min, writer)?; + store!(f64, &self.next_block_template_feerate_median, writer)?; + store!(f64, &self.next_block_template_feerate_max, writer)?; + Ok(()) + } +} + +impl Deserializer for RpcFeeEstimateVerboseExperimentalData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let mempool_ready_transactions_count = load!(u64, reader)?; + let mempool_ready_transactions_total_mass = load!(u64, reader)?; + let network_mass_per_second = load!(u64, reader)?; + let next_block_template_feerate_min = load!(f64, reader)?; + let next_block_template_feerate_median = load!(f64, reader)?; + let next_block_template_feerate_max = load!(f64, reader)?; + Ok(Self { + mempool_ready_transactions_count, + mempool_ready_transactions_total_mass, + network_mass_per_second, + next_block_template_feerate_min, + next_block_template_feerate_median, + next_block_template_feerate_max, + }) + } +} diff --git a/rpc/core/src/model/header.rs b/rpc/core/src/model/header.rs index e116f87eae..dddf767b7f 100644 --- a/rpc/core/src/model/header.rs +++ b/rpc/core/src/model/header.rs @@ -1 +1,331 @@ -pub type RpcHeader = kaspa_consensus_core::header::Header; +use borsh::{BorshDeserialize, BorshSerialize}; +use kaspa_consensus_core::{header::Header, BlueWorkType}; +use kaspa_hashes::Hash; +use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; + +/// Raw Rpc header type - without a cached header hash. +/// Used for mining APIs (get_block_template & submit_block) +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] + +pub struct RpcRawHeader { + pub version: u16, + pub parents_by_level: Vec>, + pub hash_merkle_root: Hash, + pub accepted_id_merkle_root: Hash, + pub utxo_commitment: Hash, + /// Timestamp is in milliseconds + pub timestamp: u64, + pub bits: u32, + pub nonce: u64, + pub daa_score: u64, + pub blue_work: BlueWorkType, + pub blue_score: u64, + pub pruning_point: Hash, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcHeader { + /// Cached hash + pub hash: Hash, + pub version: u16, + pub parents_by_level: Vec>, + pub hash_merkle_root: Hash, + pub accepted_id_merkle_root: Hash, + pub utxo_commitment: Hash, + /// Timestamp is in milliseconds + pub timestamp: u64, + pub bits: u32, + pub nonce: u64, + pub daa_score: u64, + pub blue_work: BlueWorkType, + pub blue_score: u64, + pub pruning_point: Hash, +} + +impl RpcHeader { + pub fn direct_parents(&self) -> &[Hash] { + if self.parents_by_level.is_empty() { + &[] + } else { + &self.parents_by_level[0] + } + } +} + +impl AsRef for RpcHeader { + fn as_ref(&self) -> &RpcHeader { + self + } +} + +impl From
for RpcHeader { + fn from(header: Header) -> Self { + Self { + hash: header.hash, + version: header.version, + parents_by_level: header.parents_by_level, + hash_merkle_root: header.hash_merkle_root, + accepted_id_merkle_root: header.accepted_id_merkle_root, + utxo_commitment: header.utxo_commitment, + timestamp: header.timestamp, + bits: header.bits, + nonce: header.nonce, + daa_score: header.daa_score, + blue_work: header.blue_work, + blue_score: header.blue_score, + pruning_point: header.pruning_point, + } + } +} + +impl From<&Header> for RpcHeader { + fn from(header: &Header) -> Self { + Self { + hash: header.hash, + version: header.version, + parents_by_level: header.parents_by_level.clone(), + hash_merkle_root: header.hash_merkle_root, + accepted_id_merkle_root: header.accepted_id_merkle_root, + utxo_commitment: header.utxo_commitment, + timestamp: header.timestamp, + bits: header.bits, + nonce: header.nonce, + daa_score: header.daa_score, + blue_work: header.blue_work, + blue_score: header.blue_score, + pruning_point: header.pruning_point, + } + } +} + +impl From for Header { + fn from(header: RpcHeader) -> Self { + Self { + hash: header.hash, + version: header.version, + parents_by_level: header.parents_by_level, + hash_merkle_root: header.hash_merkle_root, + accepted_id_merkle_root: header.accepted_id_merkle_root, + utxo_commitment: header.utxo_commitment, + timestamp: header.timestamp, + bits: header.bits, + nonce: header.nonce, + daa_score: header.daa_score, + blue_work: header.blue_work, + blue_score: header.blue_score, + pruning_point: header.pruning_point, + } + } +} + +impl From<&RpcHeader> for Header { + fn from(header: &RpcHeader) -> Self { + Self { + hash: header.hash, + version: header.version, + parents_by_level: header.parents_by_level.clone(), + hash_merkle_root: header.hash_merkle_root, + accepted_id_merkle_root: header.accepted_id_merkle_root, + utxo_commitment: header.utxo_commitment, + timestamp: header.timestamp, + bits: header.bits, + nonce: header.nonce, + daa_score: header.daa_score, + blue_work: header.blue_work, + blue_score: header.blue_score, + pruning_point: header.pruning_point, + } + } +} + +impl Serializer for RpcHeader { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + + store!(Hash, &self.hash, writer)?; + store!(u16, &self.version, writer)?; + store!(Vec>, &self.parents_by_level, writer)?; + store!(Hash, &self.hash_merkle_root, writer)?; + store!(Hash, &self.accepted_id_merkle_root, writer)?; + store!(Hash, &self.utxo_commitment, writer)?; + store!(u64, &self.timestamp, writer)?; + store!(u32, &self.bits, writer)?; + store!(u64, &self.nonce, writer)?; + store!(u64, &self.daa_score, writer)?; + store!(BlueWorkType, &self.blue_work, writer)?; + store!(u64, &self.blue_score, writer)?; + store!(Hash, &self.pruning_point, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcHeader { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + + let hash = load!(Hash, reader)?; + let version = load!(u16, reader)?; + let parents_by_level = load!(Vec>, reader)?; + let hash_merkle_root = load!(Hash, reader)?; + let accepted_id_merkle_root = load!(Hash, reader)?; + let utxo_commitment = load!(Hash, reader)?; + let timestamp = load!(u64, reader)?; + let bits = load!(u32, reader)?; + let nonce = load!(u64, reader)?; + let daa_score = load!(u64, reader)?; + let blue_work = load!(BlueWorkType, reader)?; + let blue_score = load!(u64, reader)?; + let pruning_point = load!(Hash, reader)?; + + Ok(Self { + hash, + version, + parents_by_level, + hash_merkle_root, + accepted_id_merkle_root, + utxo_commitment, + timestamp, + bits, + nonce, + daa_score, + blue_work, + blue_score, + pruning_point, + }) + } +} + +impl From for Header { + fn from(header: RpcRawHeader) -> Self { + Self::new_finalized( + header.version, + header.parents_by_level, + header.hash_merkle_root, + header.accepted_id_merkle_root, + header.utxo_commitment, + header.timestamp, + header.bits, + header.nonce, + header.daa_score, + header.blue_work, + header.blue_score, + header.pruning_point, + ) + } +} + +impl From<&RpcRawHeader> for Header { + fn from(header: &RpcRawHeader) -> Self { + Self::new_finalized( + header.version, + header.parents_by_level.clone(), + header.hash_merkle_root, + header.accepted_id_merkle_root, + header.utxo_commitment, + header.timestamp, + header.bits, + header.nonce, + header.daa_score, + header.blue_work, + header.blue_score, + header.pruning_point, + ) + } +} + +impl From<&Header> for RpcRawHeader { + fn from(header: &Header) -> Self { + Self { + version: header.version, + parents_by_level: header.parents_by_level.clone(), + hash_merkle_root: header.hash_merkle_root, + accepted_id_merkle_root: header.accepted_id_merkle_root, + utxo_commitment: header.utxo_commitment, + timestamp: header.timestamp, + bits: header.bits, + nonce: header.nonce, + daa_score: header.daa_score, + blue_work: header.blue_work, + blue_score: header.blue_score, + pruning_point: header.pruning_point, + } + } +} + +impl From
for RpcRawHeader { + fn from(header: Header) -> Self { + Self { + version: header.version, + parents_by_level: header.parents_by_level, + hash_merkle_root: header.hash_merkle_root, + accepted_id_merkle_root: header.accepted_id_merkle_root, + utxo_commitment: header.utxo_commitment, + timestamp: header.timestamp, + bits: header.bits, + nonce: header.nonce, + daa_score: header.daa_score, + blue_work: header.blue_work, + blue_score: header.blue_score, + pruning_point: header.pruning_point, + } + } +} + +impl Serializer for RpcRawHeader { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + + store!(u16, &self.version, writer)?; + store!(Vec>, &self.parents_by_level, writer)?; + store!(Hash, &self.hash_merkle_root, writer)?; + store!(Hash, &self.accepted_id_merkle_root, writer)?; + store!(Hash, &self.utxo_commitment, writer)?; + store!(u64, &self.timestamp, writer)?; + store!(u32, &self.bits, writer)?; + store!(u64, &self.nonce, writer)?; + store!(u64, &self.daa_score, writer)?; + store!(BlueWorkType, &self.blue_work, writer)?; + store!(u64, &self.blue_score, writer)?; + store!(Hash, &self.pruning_point, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcRawHeader { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + + let version = load!(u16, reader)?; + let parents_by_level = load!(Vec>, reader)?; + let hash_merkle_root = load!(Hash, reader)?; + let accepted_id_merkle_root = load!(Hash, reader)?; + let utxo_commitment = load!(Hash, reader)?; + let timestamp = load!(u64, reader)?; + let bits = load!(u32, reader)?; + let nonce = load!(u64, reader)?; + let daa_score = load!(u64, reader)?; + let blue_work = load!(BlueWorkType, reader)?; + let blue_score = load!(u64, reader)?; + let pruning_point = load!(Hash, reader)?; + + Ok(Self { + version, + parents_by_level, + hash_merkle_root, + accepted_id_merkle_root, + utxo_commitment, + timestamp, + bits, + nonce, + daa_score, + blue_work, + blue_score, + pruning_point, + }) + } +} diff --git a/rpc/core/src/model/mempool.rs b/rpc/core/src/model/mempool.rs index bd08b745a0..1a04bed756 100644 --- a/rpc/core/src/model/mempool.rs +++ b/rpc/core/src/model/mempool.rs @@ -1,9 +1,9 @@ use super::RpcAddress; use super::RpcTransaction; -use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct RpcMempoolEntry { pub fee: u64, pub transaction: RpcTransaction, @@ -16,7 +16,24 @@ impl RpcMempoolEntry { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for RpcMempoolEntry { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u64, &self.fee, writer)?; + serialize!(RpcTransaction, &self.transaction, writer)?; + store!(bool, &self.is_orphan, writer) + } +} + +impl Deserializer for RpcMempoolEntry { + fn deserialize(reader: &mut R) -> std::io::Result { + let fee = load!(u64, reader)?; + let transaction = deserialize!(RpcTransaction, reader)?; + let is_orphan = load!(bool, reader)?; + Ok(Self { fee, transaction, is_orphan }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct RpcMempoolEntryByAddress { pub address: RpcAddress, pub sending: Vec, @@ -29,6 +46,23 @@ impl RpcMempoolEntryByAddress { } } +impl Serializer for RpcMempoolEntryByAddress { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(RpcAddress, &self.address, writer)?; + serialize!(Vec, &self.sending, writer)?; + serialize!(Vec, &self.receiving, writer) + } +} + +impl Deserializer for RpcMempoolEntryByAddress { + fn deserialize(reader: &mut R) -> std::io::Result { + let address = load!(RpcAddress, reader)?; + let sending = deserialize!(Vec, reader)?; + let receiving = deserialize!(Vec, reader)?; + Ok(Self { address, sending, receiving }) + } +} + cfg_if::cfg_if! { if #[cfg(feature = "wasm32-sdk")] { use wasm_bindgen::prelude::*; diff --git a/rpc/core/src/model/message.rs b/rpc/core/src/model/message.rs index 5c003546cf..6a2253d8e8 100644 --- a/rpc/core/src/model/message.rs +++ b/rpc/core/src/model/message.rs @@ -3,11 +3,14 @@ use borsh::{BorshDeserialize, BorshSerialize}; use kaspa_consensus_core::api::stats::BlockCount; use kaspa_core::debug; use kaspa_notify::subscription::{context::SubscriptionContext, single::UtxosChangedSubscription, Command}; +use kaspa_utils::hex::ToHex; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::{ fmt::{Display, Formatter}, sync::Arc, }; +use workflow_serializer::prelude::*; pub type RpcExtraData = Vec; @@ -15,21 +18,42 @@ pub type RpcExtraData = Vec; /// Blocks are generally expected to have been generated using the getBlockTemplate call. /// /// See: [`GetBlockTemplateRequest`] -#[derive(Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SubmitBlockRequest { - pub block: RpcBlock, + pub block: RpcRawBlock, #[serde(alias = "allowNonDAABlocks")] pub allow_non_daa_blocks: bool, } impl SubmitBlockRequest { - pub fn new(block: RpcBlock, allow_non_daa_blocks: bool) -> Self { + pub fn new(block: RpcRawBlock, allow_non_daa_blocks: bool) -> Self { Self { block, allow_non_daa_blocks } } } +impl Serializer for SubmitBlockRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcRawBlock, &self.block, writer)?; + store!(bool, &self.allow_non_daa_blocks, writer)?; + + Ok(()) + } +} + +impl Deserializer for SubmitBlockRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let block = deserialize!(RpcRawBlock, reader)?; + let allow_non_daa_blocks = load!(bool, reader)?; + + Ok(Self { block, allow_non_daa_blocks }) + } +} + #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] +#[borsh(use_discriminant = true)] pub enum SubmitBlockRejectReason { BlockInvalid = 1, IsInIBD = 2, @@ -54,6 +78,7 @@ impl Display for SubmitBlockRejectReason { #[derive(Eq, PartialEq, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "lowercase")] #[serde(tag = "type", content = "reason")] +#[borsh(use_discriminant = true)] pub enum SubmitBlockReport { Success, Reject(SubmitBlockRejectReason), @@ -64,17 +89,34 @@ impl SubmitBlockReport { } } -#[derive(Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SubmitBlockResponse { pub report: SubmitBlockReport, } +impl Serializer for SubmitBlockResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(SubmitBlockReport, &self.report, writer)?; + Ok(()) + } +} + +impl Deserializer for SubmitBlockResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let report = load!(SubmitBlockReport, reader)?; + + Ok(Self { report }) + } +} + /// GetBlockTemplateRequest requests a current block template. /// Callers are expected to solve the block template and submit it using the submitBlock call /// /// See: [`SubmitBlockRequest`] -#[derive(Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBlockTemplateRequest { /// Which kaspa address should the coinbase block reward transaction pay into @@ -88,10 +130,30 @@ impl GetBlockTemplateRequest { } } -#[derive(Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetBlockTemplateRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcAddress, &self.pay_address, writer)?; + store!(RpcExtraData, &self.extra_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetBlockTemplateRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let pay_address = load!(RpcAddress, reader)?; + let extra_data = load!(RpcExtraData, reader)?; + + Ok(Self { pay_address, extra_data }) + } +} + +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBlockTemplateResponse { - pub block: RpcBlock, + pub block: RpcRawBlock, /// Whether kaspad thinks that it's synced. /// Callers are discouraged (but not forbidden) from solving blocks when kaspad is not synced. @@ -100,8 +162,28 @@ pub struct GetBlockTemplateResponse { pub is_synced: bool, } +impl Serializer for GetBlockTemplateResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcRawBlock, &self.block, writer)?; + store!(bool, &self.is_synced, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetBlockTemplateResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let block = deserialize!(RpcRawBlock, reader)?; + let is_synced = load!(bool, reader)?; + + Ok(Self { block, is_synced }) + } +} + /// GetBlockRequest requests information about a specific block -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBlockRequest { /// The hash of the requested block @@ -116,18 +198,70 @@ impl GetBlockRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetBlockRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcHash, &self.hash, writer)?; + store!(bool, &self.include_transactions, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetBlockRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let hash = load!(RpcHash, reader)?; + let include_transactions = load!(bool, reader)?; + + Ok(Self { hash, include_transactions }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBlockResponse { pub block: RpcBlock, } +impl Serializer for GetBlockResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcBlock, &self.block, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetBlockResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let block = deserialize!(RpcBlock, reader)?; + + Ok(Self { block }) + } +} + /// GetInfoRequest returns info about the node. -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetInfoRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetInfoRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetInfoRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetInfoResponse { pub p2p_id: String, @@ -139,11 +273,55 @@ pub struct GetInfoResponse { pub has_message_id: bool, } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetInfoResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(String, &self.p2p_id, writer)?; + store!(u64, &self.mempool_size, writer)?; + store!(String, &self.server_version, writer)?; + store!(bool, &self.is_utxo_indexed, writer)?; + store!(bool, &self.is_synced, writer)?; + store!(bool, &self.has_notify_command, writer)?; + store!(bool, &self.has_message_id, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetInfoResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let p2p_id = load!(String, reader)?; + let mempool_size = load!(u64, reader)?; + let server_version = load!(String, reader)?; + let is_utxo_indexed = load!(bool, reader)?; + let is_synced = load!(bool, reader)?; + let has_notify_command = load!(bool, reader)?; + let has_message_id = load!(bool, reader)?; + + Ok(Self { p2p_id, mempool_size, server_version, is_utxo_indexed, is_synced, has_notify_command, has_message_id }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetCurrentNetworkRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetCurrentNetworkRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetCurrentNetworkRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetCurrentNetworkResponse { pub network: RpcNetworkType, @@ -155,11 +333,41 @@ impl GetCurrentNetworkResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetCurrentNetworkResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcNetworkType, &self.network, writer)?; + Ok(()) + } +} + +impl Deserializer for GetCurrentNetworkResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let network = load!(RpcNetworkType, reader)?; + Ok(Self { network }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetPeerAddressesRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetPeerAddressesRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetPeerAddressesRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetPeerAddressesResponse { pub known_addresses: Vec, @@ -172,11 +380,43 @@ impl GetPeerAddressesResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetPeerAddressesResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.known_addresses, writer)?; + store!(Vec, &self.banned_addresses, writer)?; + Ok(()) + } +} + +impl Deserializer for GetPeerAddressesResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let known_addresses = load!(Vec, reader)?; + let banned_addresses = load!(Vec, reader)?; + Ok(Self { known_addresses, banned_addresses }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetSinkRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetSinkRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetSinkRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetSinkResponse { pub sink: RpcHash, @@ -188,7 +428,23 @@ impl GetSinkResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetSinkResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcHash, &self.sink, writer)?; + Ok(()) + } +} + +impl Deserializer for GetSinkResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let sink = load!(RpcHash, reader)?; + Ok(Self { sink }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetMempoolEntryRequest { pub transaction_id: RpcTransactionId, @@ -203,7 +459,29 @@ impl GetMempoolEntryRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetMempoolEntryRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcTransactionId, &self.transaction_id, writer)?; + store!(bool, &self.include_orphan_pool, writer)?; + store!(bool, &self.filter_transaction_pool, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetMempoolEntryRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let transaction_id = load!(RpcTransactionId, reader)?; + let include_orphan_pool = load!(bool, reader)?; + let filter_transaction_pool = load!(bool, reader)?; + + Ok(Self { transaction_id, include_orphan_pool, filter_transaction_pool }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetMempoolEntryResponse { pub mempool_entry: RpcMempoolEntry, @@ -215,7 +493,23 @@ impl GetMempoolEntryResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetMempoolEntryResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcMempoolEntry, &self.mempool_entry, writer)?; + Ok(()) + } +} + +impl Deserializer for GetMempoolEntryResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let mempool_entry = deserialize!(RpcMempoolEntry, reader)?; + Ok(Self { mempool_entry }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetMempoolEntriesRequest { pub include_orphan_pool: bool, @@ -229,7 +523,27 @@ impl GetMempoolEntriesRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetMempoolEntriesRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(bool, &self.include_orphan_pool, writer)?; + store!(bool, &self.filter_transaction_pool, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetMempoolEntriesRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let include_orphan_pool = load!(bool, reader)?; + let filter_transaction_pool = load!(bool, reader)?; + + Ok(Self { include_orphan_pool, filter_transaction_pool }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetMempoolEntriesResponse { pub mempool_entries: Vec, @@ -241,11 +555,41 @@ impl GetMempoolEntriesResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetMempoolEntriesResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(Vec, &self.mempool_entries, writer)?; + Ok(()) + } +} + +impl Deserializer for GetMempoolEntriesResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let mempool_entries = deserialize!(Vec, reader)?; + Ok(Self { mempool_entries }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetConnectedPeerInfoRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetConnectedPeerInfoRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetConnectedPeerInfoRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetConnectedPeerInfoResponse { pub peer_info: Vec, @@ -257,7 +601,23 @@ impl GetConnectedPeerInfoResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetConnectedPeerInfoResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.peer_info, writer)?; + Ok(()) + } +} + +impl Deserializer for GetConnectedPeerInfoResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let peer_info = load!(Vec, reader)?; + Ok(Self { peer_info }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AddPeerRequest { pub peer_address: RpcContextualPeerAddress, @@ -270,11 +630,45 @@ impl AddPeerRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for AddPeerRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcContextualPeerAddress, &self.peer_address, writer)?; + store!(bool, &self.is_permanent, writer)?; + + Ok(()) + } +} + +impl Deserializer for AddPeerRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let peer_address = load!(RpcContextualPeerAddress, reader)?; + let is_permanent = load!(bool, reader)?; + + Ok(Self { peer_address, is_permanent }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AddPeerResponse {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for AddPeerResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for AddPeerResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SubmitTransactionRequest { pub transaction: RpcTransaction, @@ -287,7 +681,27 @@ impl SubmitTransactionRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for SubmitTransactionRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcTransaction, &self.transaction, writer)?; + store!(bool, &self.allow_orphan, writer)?; + + Ok(()) + } +} + +impl Deserializer for SubmitTransactionRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let transaction = deserialize!(RpcTransaction, reader)?; + let allow_orphan = load!(bool, reader)?; + + Ok(Self { transaction, allow_orphan }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SubmitTransactionResponse { pub transaction_id: RpcTransactionId, @@ -299,7 +713,25 @@ impl SubmitTransactionResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for SubmitTransactionResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcTransactionId, &self.transaction_id, writer)?; + + Ok(()) + } +} + +impl Deserializer for SubmitTransactionResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let transaction_id = load!(RpcTransactionId, reader)?; + + Ok(Self { transaction_id }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SubmitTransactionReplacementRequest { pub transaction: RpcTransaction, @@ -311,7 +743,25 @@ impl SubmitTransactionReplacementRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for SubmitTransactionReplacementRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcTransaction, &self.transaction, writer)?; + + Ok(()) + } +} + +impl Deserializer for SubmitTransactionReplacementRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let transaction = deserialize!(RpcTransaction, reader)?; + + Ok(Self { transaction }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SubmitTransactionReplacementResponse { pub transaction_id: RpcTransactionId, @@ -324,6 +774,26 @@ impl SubmitTransactionReplacementResponse { } } +impl Serializer for SubmitTransactionReplacementResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcTransactionId, &self.transaction_id, writer)?; + serialize!(RpcTransaction, &self.replaced_transaction, writer)?; + + Ok(()) + } +} + +impl Deserializer for SubmitTransactionReplacementResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let transaction_id = load!(RpcTransactionId, reader)?; + let replaced_transaction = deserialize!(RpcTransaction, reader)?; + + Ok(Self { transaction_id, replaced_transaction }) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct GetSubnetworkRequest { @@ -336,7 +806,25 @@ impl GetSubnetworkRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetSubnetworkRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcSubnetworkId, &self.subnetwork_id, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetSubnetworkRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let subnetwork_id = load!(RpcSubnetworkId, reader)?; + + Ok(Self { subnetwork_id }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetSubnetworkResponse { pub gas_limit: u64, @@ -348,7 +836,25 @@ impl GetSubnetworkResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetSubnetworkResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.gas_limit, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetSubnetworkResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let gas_limit = load!(u64, reader)?; + + Ok(Self { gas_limit }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetVirtualChainFromBlockRequest { pub start_hash: RpcHash, @@ -361,7 +867,27 @@ impl GetVirtualChainFromBlockRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetVirtualChainFromBlockRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcHash, &self.start_hash, writer)?; + store!(bool, &self.include_accepted_transaction_ids, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetVirtualChainFromBlockRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let start_hash = load!(RpcHash, reader)?; + let include_accepted_transaction_ids = load!(bool, reader)?; + + Ok(Self { start_hash, include_accepted_transaction_ids }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetVirtualChainFromBlockResponse { pub removed_chain_block_hashes: Vec, @@ -379,7 +905,29 @@ impl GetVirtualChainFromBlockResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetVirtualChainFromBlockResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.removed_chain_block_hashes, writer)?; + store!(Vec, &self.added_chain_block_hashes, writer)?; + store!(Vec, &self.accepted_transaction_ids, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetVirtualChainFromBlockResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let removed_chain_block_hashes = load!(Vec, reader)?; + let added_chain_block_hashes = load!(Vec, reader)?; + let accepted_transaction_ids = load!(Vec, reader)?; + + Ok(Self { removed_chain_block_hashes, added_chain_block_hashes, accepted_transaction_ids }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBlocksRequest { pub low_hash: Option, @@ -393,7 +941,29 @@ impl GetBlocksRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetBlocksRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Option, &self.low_hash, writer)?; + store!(bool, &self.include_blocks, writer)?; + store!(bool, &self.include_transactions, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetBlocksRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let low_hash = load!(Option, reader)?; + let include_blocks = load!(bool, reader)?; + let include_transactions = load!(bool, reader)?; + + Ok(Self { low_hash, include_blocks, include_transactions }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBlocksResponse { pub block_hashes: Vec, @@ -406,17 +976,65 @@ impl GetBlocksResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetBlockCountRequest {} +impl Serializer for GetBlocksResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.block_hashes, writer)?; + serialize!(Vec, &self.blocks, writer)?; -pub type GetBlockCountResponse = BlockCount; + Ok(()) + } +} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Deserializer for GetBlocksResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let block_hashes = load!(Vec, reader)?; + let blocks = deserialize!(Vec, reader)?; + + Ok(Self { block_hashes, blocks }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetBlockCountRequest {} + +impl Serializer for GetBlockCountRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetBlockCountRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +pub type GetBlockCountResponse = BlockCount; + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBlockDagInfoRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetBlockDagInfoRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetBlockDagInfoRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBlockDagInfoResponse { pub network: RpcNetworkId, @@ -459,7 +1077,54 @@ impl GetBlockDagInfoResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetBlockDagInfoResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcNetworkId, &self.network, writer)?; + store!(u64, &self.block_count, writer)?; + store!(u64, &self.header_count, writer)?; + store!(Vec, &self.tip_hashes, writer)?; + store!(f64, &self.difficulty, writer)?; + store!(u64, &self.past_median_time, writer)?; + store!(Vec, &self.virtual_parent_hashes, writer)?; + store!(RpcHash, &self.pruning_point_hash, writer)?; + store!(u64, &self.virtual_daa_score, writer)?; + store!(RpcHash, &self.sink, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetBlockDagInfoResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let network = load!(RpcNetworkId, reader)?; + let block_count = load!(u64, reader)?; + let header_count = load!(u64, reader)?; + let tip_hashes = load!(Vec, reader)?; + let difficulty = load!(f64, reader)?; + let past_median_time = load!(u64, reader)?; + let virtual_parent_hashes = load!(Vec, reader)?; + let pruning_point_hash = load!(RpcHash, reader)?; + let virtual_daa_score = load!(u64, reader)?; + let sink = load!(RpcHash, reader)?; + + Ok(Self { + network, + block_count, + header_count, + tip_hashes, + difficulty, + past_median_time, + virtual_parent_hashes, + pruning_point_hash, + virtual_daa_score, + sink, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResolveFinalityConflictRequest { pub finality_block_hash: RpcHash, @@ -471,19 +1136,79 @@ impl ResolveFinalityConflictRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for ResolveFinalityConflictRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcHash, &self.finality_block_hash, writer)?; + + Ok(()) + } +} + +impl Deserializer for ResolveFinalityConflictRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let finality_block_hash = load!(RpcHash, reader)?; + + Ok(Self { finality_block_hash }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResolveFinalityConflictResponse {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for ResolveFinalityConflictResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for ResolveFinalityConflictResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ShutdownRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for ShutdownRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for ShutdownRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ShutdownResponse {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for ShutdownResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for ShutdownResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetHeadersRequest { pub start_hash: RpcHash, @@ -497,7 +1222,29 @@ impl GetHeadersRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetHeadersRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcHash, &self.start_hash, writer)?; + store!(u64, &self.limit, writer)?; + store!(bool, &self.is_ascending, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetHeadersRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let start_hash = load!(RpcHash, reader)?; + let limit = load!(u64, reader)?; + let is_ascending = load!(bool, reader)?; + + Ok(Self { start_hash, limit, is_ascending }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetHeadersResponse { pub headers: Vec, @@ -509,7 +1256,25 @@ impl GetHeadersResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetHeadersResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.headers, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetHeadersResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let headers = load!(Vec, reader)?; + + Ok(Self { headers }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBalanceByAddressRequest { pub address: RpcAddress, @@ -521,7 +1286,25 @@ impl GetBalanceByAddressRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetBalanceByAddressRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcAddress, &self.address, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetBalanceByAddressRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let address = load!(RpcAddress, reader)?; + + Ok(Self { address }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBalanceByAddressResponse { pub balance: u64, @@ -533,7 +1316,25 @@ impl GetBalanceByAddressResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetBalanceByAddressResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.balance, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetBalanceByAddressResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let balance = load!(u64, reader)?; + + Ok(Self { balance }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBalancesByAddressesRequest { pub addresses: Vec, @@ -545,7 +1346,25 @@ impl GetBalancesByAddressesRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetBalancesByAddressesRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.addresses, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetBalancesByAddressesRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let addresses = load!(Vec, reader)?; + + Ok(Self { addresses }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBalancesByAddressesResponse { pub entries: Vec, @@ -557,11 +1376,43 @@ impl GetBalancesByAddressesResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetBalancesByAddressesResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(Vec, &self.entries, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetBalancesByAddressesResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let entries = deserialize!(Vec, reader)?; + + Ok(Self { entries }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetSinkBlueScoreRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetSinkBlueScoreRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetSinkBlueScoreRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetSinkBlueScoreResponse { pub blue_score: u64, @@ -573,7 +1424,25 @@ impl GetSinkBlueScoreResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetSinkBlueScoreResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.blue_score, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetSinkBlueScoreResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let blue_score = load!(u64, reader)?; + + Ok(Self { blue_score }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetUtxosByAddressesRequest { pub addresses: Vec, @@ -585,7 +1454,25 @@ impl GetUtxosByAddressesRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetUtxosByAddressesRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.addresses, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetUtxosByAddressesRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let addresses = load!(Vec, reader)?; + + Ok(Self { addresses }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetUtxosByAddressesResponse { pub entries: Vec, @@ -597,7 +1484,25 @@ impl GetUtxosByAddressesResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetUtxosByAddressesResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(Vec, &self.entries, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetUtxosByAddressesResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let entries = deserialize!(Vec, reader)?; + + Ok(Self { entries }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BanRequest { pub ip: RpcIpAddress, @@ -609,11 +1514,43 @@ impl BanRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for BanRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcIpAddress, &self.ip, writer)?; + + Ok(()) + } +} + +impl Deserializer for BanRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let ip = load!(RpcIpAddress, reader)?; + + Ok(Self { ip }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BanResponse {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for BanResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for BanResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UnbanRequest { pub ip: RpcIpAddress, @@ -625,11 +1562,43 @@ impl UnbanRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for UnbanRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcIpAddress, &self.ip, writer)?; + + Ok(()) + } +} + +impl Deserializer for UnbanRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let ip = load!(RpcIpAddress, reader)?; + + Ok(Self { ip }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UnbanResponse {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for UnbanResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for UnbanResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EstimateNetworkHashesPerSecondRequest { pub window_size: u32, @@ -642,7 +1611,27 @@ impl EstimateNetworkHashesPerSecondRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for EstimateNetworkHashesPerSecondRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u32, &self.window_size, writer)?; + store!(Option, &self.start_hash, writer)?; + + Ok(()) + } +} + +impl Deserializer for EstimateNetworkHashesPerSecondRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let window_size = load!(u32, reader)?; + let start_hash = load!(Option, reader)?; + + Ok(Self { window_size, start_hash }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EstimateNetworkHashesPerSecondResponse { pub network_hashes_per_second: u64, @@ -654,7 +1643,25 @@ impl EstimateNetworkHashesPerSecondResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for EstimateNetworkHashesPerSecondResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.network_hashes_per_second, writer)?; + + Ok(()) + } +} + +impl Deserializer for EstimateNetworkHashesPerSecondResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let network_hashes_per_second = load!(u64, reader)?; + + Ok(Self { network_hashes_per_second }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetMempoolEntriesByAddressesRequest { pub addresses: Vec, @@ -669,7 +1676,29 @@ impl GetMempoolEntriesByAddressesRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetMempoolEntriesByAddressesRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.addresses, writer)?; + store!(bool, &self.include_orphan_pool, writer)?; + store!(bool, &self.filter_transaction_pool, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetMempoolEntriesByAddressesRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let addresses = load!(Vec, reader)?; + let include_orphan_pool = load!(bool, reader)?; + let filter_transaction_pool = load!(bool, reader)?; + + Ok(Self { addresses, include_orphan_pool, filter_transaction_pool }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetMempoolEntriesByAddressesResponse { pub entries: Vec, @@ -681,11 +1710,43 @@ impl GetMempoolEntriesByAddressesResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetMempoolEntriesByAddressesResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(Vec, &self.entries, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetMempoolEntriesByAddressesResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let entries = deserialize!(Vec, reader)?; + + Ok(Self { entries }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetCoinSupplyRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetCoinSupplyRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetCoinSupplyRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetCoinSupplyResponse { pub max_sompi: u64, @@ -698,26 +1759,229 @@ impl GetCoinSupplyResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetCoinSupplyResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.max_sompi, writer)?; + store!(u64, &self.circulating_sompi, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetCoinSupplyResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let max_sompi = load!(u64, reader)?; + let circulating_sompi = load!(u64, reader)?; + + Ok(Self { max_sompi, circulating_sompi }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PingRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for PingRequest { + fn serialize(&self, _writer: &mut W) -> std::io::Result<()> { + Ok(()) + } +} + +impl Deserializer for PingRequest { + fn deserialize(_reader: &mut R) -> std::io::Result { + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PingResponse {} -// TODO - custom wRPC commands (need review and implementation in gRPC) +impl Serializer for PingResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for PingResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + Ok(Self {}) + } +} #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] +pub struct ConnectionsProfileData { + pub cpu_usage: f32, + pub memory_usage: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetConnectionsRequest { + pub include_profile_data: bool, +} + +impl Serializer for GetConnectionsRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(bool, &self.include_profile_data, writer)?; + Ok(()) + } +} + +impl Deserializer for GetConnectionsRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let include_profile_data = load!(bool, reader)?; + Ok(Self { include_profile_data }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetConnectionsResponse { + pub clients: u32, + pub peers: u16, + pub profile_data: Option, +} + +impl Serializer for GetConnectionsResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u32, &self.clients, writer)?; + store!(u16, &self.peers, writer)?; + store!(Option, &self.profile_data, writer)?; + Ok(()) + } +} + +impl Deserializer for GetConnectionsResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let clients = load!(u32, reader)?; + let peers = load!(u16, reader)?; + let extra = load!(Option, reader)?; + Ok(Self { clients, peers, profile_data: extra }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSystemInfoRequest {} + +impl Serializer for GetSystemInfoRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetSystemInfoRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + + Ok(Self {}) + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSystemInfoResponse { + pub version: String, + pub system_id: Option>, + pub git_hash: Option>, + pub cpu_physical_cores: u16, + pub total_memory: u64, + pub fd_limit: u32, +} + +impl std::fmt::Debug for GetSystemInfoResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GetSystemInfoResponse") + .field("version", &self.version) + .field("system_id", &self.system_id.as_ref().map(|id| id.to_hex())) + .field("git_hash", &self.git_hash.as_ref().map(|hash| hash.to_hex())) + .field("cpu_physical_cores", &self.cpu_physical_cores) + .field("total_memory", &self.total_memory) + .field("fd_limit", &self.fd_limit) + .finish() + } +} + +impl Serializer for GetSystemInfoResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(String, &self.version, writer)?; + store!(Option>, &self.system_id, writer)?; + store!(Option>, &self.git_hash, writer)?; + store!(u16, &self.cpu_physical_cores, writer)?; + store!(u64, &self.total_memory, writer)?; + store!(u32, &self.fd_limit, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetSystemInfoResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let version = load!(String, reader)?; + let system_id = load!(Option>, reader)?; + let git_hash = load!(Option>, reader)?; + let cpu_physical_cores = load!(u16, reader)?; + let total_memory = load!(u64, reader)?; + let fd_limit = load!(u32, reader)?; + + Ok(Self { version, system_id, git_hash, cpu_physical_cores, total_memory, fd_limit }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct GetMetricsRequest { pub process_metrics: bool, pub connection_metrics: bool, pub bandwidth_metrics: bool, pub consensus_metrics: bool, + pub storage_metrics: bool, + pub custom_metrics: bool, +} + +impl Serializer for GetMetricsRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(bool, &self.process_metrics, writer)?; + store!(bool, &self.connection_metrics, writer)?; + store!(bool, &self.bandwidth_metrics, writer)?; + store!(bool, &self.consensus_metrics, writer)?; + store!(bool, &self.storage_metrics, writer)?; + store!(bool, &self.custom_metrics, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetMetricsRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let process_metrics = load!(bool, reader)?; + let connection_metrics = load!(bool, reader)?; + let bandwidth_metrics = load!(bool, reader)?; + let consensus_metrics = load!(bool, reader)?; + let storage_metrics = load!(bool, reader)?; + let custom_metrics = load!(bool, reader)?; + + Ok(Self { process_metrics, connection_metrics, bandwidth_metrics, consensus_metrics, storage_metrics, custom_metrics }) + } } -#[derive(Default, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Default, Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProcessMetrics { pub resident_set_size: u64, @@ -731,7 +1995,51 @@ pub struct ProcessMetrics { pub disk_io_write_per_sec: f32, } -#[derive(Default, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for ProcessMetrics { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.resident_set_size, writer)?; + store!(u64, &self.virtual_memory_size, writer)?; + store!(u32, &self.core_num, writer)?; + store!(f32, &self.cpu_usage, writer)?; + store!(u32, &self.fd_num, writer)?; + store!(u64, &self.disk_io_read_bytes, writer)?; + store!(u64, &self.disk_io_write_bytes, writer)?; + store!(f32, &self.disk_io_read_per_sec, writer)?; + store!(f32, &self.disk_io_write_per_sec, writer)?; + + Ok(()) + } +} + +impl Deserializer for ProcessMetrics { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let resident_set_size = load!(u64, reader)?; + let virtual_memory_size = load!(u64, reader)?; + let core_num = load!(u32, reader)?; + let cpu_usage = load!(f32, reader)?; + let fd_num = load!(u32, reader)?; + let disk_io_read_bytes = load!(u64, reader)?; + let disk_io_write_bytes = load!(u64, reader)?; + let disk_io_read_per_sec = load!(f32, reader)?; + let disk_io_write_per_sec = load!(f32, reader)?; + + Ok(Self { + resident_set_size, + virtual_memory_size, + core_num, + cpu_usage, + fd_num, + disk_io_read_bytes, + disk_io_write_bytes, + disk_io_read_per_sec, + disk_io_write_per_sec, + }) + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ConnectionMetrics { pub borsh_live_connections: u32, @@ -744,7 +2052,45 @@ pub struct ConnectionMetrics { pub active_peers: u32, } -#[derive(Default, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for ConnectionMetrics { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u32, &self.borsh_live_connections, writer)?; + store!(u64, &self.borsh_connection_attempts, writer)?; + store!(u64, &self.borsh_handshake_failures, writer)?; + store!(u32, &self.json_live_connections, writer)?; + store!(u64, &self.json_connection_attempts, writer)?; + store!(u64, &self.json_handshake_failures, writer)?; + store!(u32, &self.active_peers, writer)?; + + Ok(()) + } +} + +impl Deserializer for ConnectionMetrics { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let borsh_live_connections = load!(u32, reader)?; + let borsh_connection_attempts = load!(u64, reader)?; + let borsh_handshake_failures = load!(u64, reader)?; + let json_live_connections = load!(u32, reader)?; + let json_connection_attempts = load!(u64, reader)?; + let json_handshake_failures = load!(u64, reader)?; + let active_peers = load!(u32, reader)?; + + Ok(Self { + borsh_live_connections, + borsh_connection_attempts, + borsh_handshake_failures, + json_live_connections, + json_connection_attempts, + json_handshake_failures, + active_peers, + }) + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BandwidthMetrics { pub borsh_bytes_tx: u64, @@ -757,7 +2103,48 @@ pub struct BandwidthMetrics { pub grpc_bytes_rx: u64, } -#[derive(Default, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for BandwidthMetrics { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.borsh_bytes_tx, writer)?; + store!(u64, &self.borsh_bytes_rx, writer)?; + store!(u64, &self.json_bytes_tx, writer)?; + store!(u64, &self.json_bytes_rx, writer)?; + store!(u64, &self.p2p_bytes_tx, writer)?; + store!(u64, &self.p2p_bytes_rx, writer)?; + store!(u64, &self.grpc_bytes_tx, writer)?; + store!(u64, &self.grpc_bytes_rx, writer)?; + + Ok(()) + } +} + +impl Deserializer for BandwidthMetrics { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let borsh_bytes_tx = load!(u64, reader)?; + let borsh_bytes_rx = load!(u64, reader)?; + let json_bytes_tx = load!(u64, reader)?; + let json_bytes_rx = load!(u64, reader)?; + let p2p_bytes_tx = load!(u64, reader)?; + let p2p_bytes_rx = load!(u64, reader)?; + let grpc_bytes_tx = load!(u64, reader)?; + let grpc_bytes_rx = load!(u64, reader)?; + + Ok(Self { + borsh_bytes_tx, + borsh_bytes_rx, + json_bytes_tx, + json_bytes_rx, + p2p_bytes_tx, + p2p_bytes_rx, + grpc_bytes_tx, + grpc_bytes_rx, + }) + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ConsensusMetrics { pub node_blocks_submitted_count: u64, @@ -779,7 +2166,115 @@ pub struct ConsensusMetrics { pub network_virtual_daa_score: u64, } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for ConsensusMetrics { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.node_blocks_submitted_count, writer)?; + store!(u64, &self.node_headers_processed_count, writer)?; + store!(u64, &self.node_dependencies_processed_count, writer)?; + store!(u64, &self.node_bodies_processed_count, writer)?; + store!(u64, &self.node_transactions_processed_count, writer)?; + store!(u64, &self.node_chain_blocks_processed_count, writer)?; + store!(u64, &self.node_mass_processed_count, writer)?; + store!(u64, &self.node_database_blocks_count, writer)?; + store!(u64, &self.node_database_headers_count, writer)?; + store!(u64, &self.network_mempool_size, writer)?; + store!(u32, &self.network_tip_hashes_count, writer)?; + store!(f64, &self.network_difficulty, writer)?; + store!(u64, &self.network_past_median_time, writer)?; + store!(u32, &self.network_virtual_parent_hashes_count, writer)?; + store!(u64, &self.network_virtual_daa_score, writer)?; + + Ok(()) + } +} + +impl Deserializer for ConsensusMetrics { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let node_blocks_submitted_count = load!(u64, reader)?; + let node_headers_processed_count = load!(u64, reader)?; + let node_dependencies_processed_count = load!(u64, reader)?; + let node_bodies_processed_count = load!(u64, reader)?; + let node_transactions_processed_count = load!(u64, reader)?; + let node_chain_blocks_processed_count = load!(u64, reader)?; + let node_mass_processed_count = load!(u64, reader)?; + let node_database_blocks_count = load!(u64, reader)?; + let node_database_headers_count = load!(u64, reader)?; + let network_mempool_size = load!(u64, reader)?; + let network_tip_hashes_count = load!(u32, reader)?; + let network_difficulty = load!(f64, reader)?; + let network_past_median_time = load!(u64, reader)?; + let network_virtual_parent_hashes_count = load!(u32, reader)?; + let network_virtual_daa_score = load!(u64, reader)?; + + Ok(Self { + node_blocks_submitted_count, + node_headers_processed_count, + node_dependencies_processed_count, + node_bodies_processed_count, + node_transactions_processed_count, + node_chain_blocks_processed_count, + node_mass_processed_count, + node_database_blocks_count, + node_database_headers_count, + network_mempool_size, + network_tip_hashes_count, + network_difficulty, + network_past_median_time, + network_virtual_parent_hashes_count, + network_virtual_daa_score, + }) + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageMetrics { + pub storage_size_bytes: u64, +} + +impl Serializer for StorageMetrics { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.storage_size_bytes, writer)?; + + Ok(()) + } +} + +impl Deserializer for StorageMetrics { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let storage_size_bytes = load!(u64, reader)?; + + Ok(Self { storage_size_bytes }) + } +} + +// TODO: Custom metrics dictionary +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum CustomMetricValue { + Placeholder, +} + +impl Serializer for CustomMetricValue { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + + Ok(()) + } +} + +impl Deserializer for CustomMetricValue { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + + Ok(CustomMetricValue::Placeholder) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetMetricsResponse { pub server_time: u64, @@ -787,6 +2282,9 @@ pub struct GetMetricsResponse { pub connection_metrics: Option, pub bandwidth_metrics: Option, pub consensus_metrics: Option, + pub storage_metrics: Option, + // TODO: this is currently a placeholder + pub custom_metrics: Option>, } impl GetMetricsResponse { @@ -796,19 +2294,95 @@ impl GetMetricsResponse { connection_metrics: Option, bandwidth_metrics: Option, consensus_metrics: Option, + storage_metrics: Option, + custom_metrics: Option>, ) -> Self { - Self { process_metrics, connection_metrics, bandwidth_metrics, consensus_metrics, server_time } + Self { + process_metrics, + connection_metrics, + bandwidth_metrics, + consensus_metrics, + storage_metrics, + server_time, + custom_metrics, + } + } +} + +impl Serializer for GetMetricsResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.server_time, writer)?; + serialize!(Option, &self.process_metrics, writer)?; + serialize!(Option, &self.connection_metrics, writer)?; + serialize!(Option, &self.bandwidth_metrics, writer)?; + serialize!(Option, &self.consensus_metrics, writer)?; + serialize!(Option, &self.storage_metrics, writer)?; + serialize!(Option>, &self.custom_metrics, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetMetricsResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let server_time = load!(u64, reader)?; + let process_metrics = deserialize!(Option, reader)?; + let connection_metrics = deserialize!(Option, reader)?; + let bandwidth_metrics = deserialize!(Option, reader)?; + let consensus_metrics = deserialize!(Option, reader)?; + let storage_metrics = deserialize!(Option, reader)?; + let custom_metrics = deserialize!(Option>, reader)?; + + Ok(Self { + server_time, + process_metrics, + connection_metrics, + bandwidth_metrics, + consensus_metrics, + storage_metrics, + custom_metrics, + }) } } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] +#[borsh(use_discriminant = true)] +pub enum RpcCaps { + Full = 0, + Blocks, + UtxoIndex, + Mempool, + Metrics, + Visualizer, + Mining, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct GetServerInfoRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetServerInfoRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetServerInfoRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetServerInfoResponse { - pub rpc_api_version: [u16; 4], + pub rpc_api_version: u16, + pub rpc_api_revision: u16, pub server_version: String, pub network_id: RpcNetworkId, pub has_utxo_index: bool, @@ -816,17 +2390,81 @@ pub struct GetServerInfoResponse { pub virtual_daa_score: u64, } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetSyncStatusRequest {} +impl Serializer for GetServerInfoResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + + store!(u16, &self.rpc_api_version, writer)?; + store!(u16, &self.rpc_api_revision, writer)?; + + store!(String, &self.server_version, writer)?; + store!(RpcNetworkId, &self.network_id, writer)?; + store!(bool, &self.has_utxo_index, writer)?; + store!(bool, &self.is_synced, writer)?; + store!(u64, &self.virtual_daa_score, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetServerInfoResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + + let rpc_api_version = load!(u16, reader)?; + let rpc_api_revision = load!(u16, reader)?; + + let server_version = load!(String, reader)?; + let network_id = load!(RpcNetworkId, reader)?; + let has_utxo_index = load!(bool, reader)?; + let is_synced = load!(bool, reader)?; + let virtual_daa_score = load!(u64, reader)?; + + Ok(Self { rpc_api_version, rpc_api_revision, server_version, network_id, has_utxo_index, is_synced, virtual_daa_score }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSyncStatusRequest {} + +impl Serializer for GetSyncStatusRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetSyncStatusRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetSyncStatusResponse { pub is_synced: bool, } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetSyncStatusResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(bool, &self.is_synced, writer)?; + Ok(()) + } +} + +impl Deserializer for GetSyncStatusResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let is_synced = load!(bool, reader)?; + Ok(Self { is_synced }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetDaaScoreTimestampEstimateRequest { pub daa_scores: Vec, @@ -838,7 +2476,23 @@ impl GetDaaScoreTimestampEstimateRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetDaaScoreTimestampEstimateRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.daa_scores, writer)?; + Ok(()) + } +} + +impl Deserializer for GetDaaScoreTimestampEstimateRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let daa_scores = load!(Vec, reader)?; + Ok(Self { daa_scores }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetDaaScoreTimestampEstimateResponse { pub timestamps: Vec, @@ -850,26 +2504,88 @@ impl GetDaaScoreTimestampEstimateResponse { } } +impl Serializer for GetDaaScoreTimestampEstimateResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.timestamps, writer)?; + Ok(()) + } +} + +impl Deserializer for GetDaaScoreTimestampEstimateResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let timestamps = load!(Vec, reader)?; + Ok(Self { timestamps }) + } +} + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Fee rate estimations -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetFeeEstimateRequest {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetFeeEstimateRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for GetFeeEstimateRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetFeeEstimateResponse { pub estimate: RpcFeeEstimate, } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetFeeEstimateResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcFeeEstimate, &self.estimate, writer)?; + Ok(()) + } +} + +impl Deserializer for GetFeeEstimateResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let estimate = deserialize!(RpcFeeEstimate, reader)?; + Ok(Self { estimate }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetFeeEstimateExperimentalRequest { pub verbose: bool, } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for GetFeeEstimateExperimentalRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(bool, &self.verbose, writer)?; + Ok(()) + } +} + +impl Deserializer for GetFeeEstimateExperimentalRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let verbose = load!(bool, reader)?; + Ok(Self { verbose }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetFeeEstimateExperimentalResponse { /// The usual feerate estimate response @@ -879,6 +2595,24 @@ pub struct GetFeeEstimateExperimentalResponse { pub verbose: Option, } +impl Serializer for GetFeeEstimateExperimentalResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcFeeEstimate, &self.estimate, writer)?; + serialize!(Option, &self.verbose, writer)?; + Ok(()) + } +} + +impl Deserializer for GetFeeEstimateExperimentalResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let estimate = deserialize!(RpcFeeEstimate, reader)?; + let verbose = deserialize!(Option, reader)?; + Ok(Self { estimate, verbose }) + } +} + // ---------------------------------------------------------------------------- // Subscriptions & notifications // ---------------------------------------------------------------------------- @@ -889,7 +2623,7 @@ pub struct GetFeeEstimateExperimentalResponse { /// NotifyBlockAddedRequest registers this connection for blockAdded notifications. /// /// See: BlockAddedNotification -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyBlockAddedRequest { pub command: Command, @@ -900,20 +2634,66 @@ impl NotifyBlockAddedRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyBlockAddedRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Command, &self.command, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyBlockAddedRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let command = load!(Command, reader)?; + Ok(Self { command }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyBlockAddedResponse {} +impl Serializer for NotifyBlockAddedResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyBlockAddedResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + /// BlockAddedNotification is sent whenever a blocks has been added (NOT accepted) /// into the DAG. /// /// See: NotifyBlockAddedRequest -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BlockAddedNotification { pub block: Arc, } +impl Serializer for BlockAddedNotification { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcBlock, &self.block, writer)?; + Ok(()) + } +} + +impl Deserializer for BlockAddedNotification { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let block = deserialize!(RpcBlock, reader)?; + Ok(Self { block: block.into() }) + } +} + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // VirtualChainChangedNotification @@ -921,7 +2701,7 @@ pub struct BlockAddedNotification { // virtualDaaScoreChanged notifications. // // See: VirtualChainChangedNotification -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyVirtualChainChangedRequest { pub include_accepted_transaction_ids: bool, @@ -934,15 +2714,47 @@ impl NotifyVirtualChainChangedRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyVirtualChainChangedRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(bool, &self.include_accepted_transaction_ids, writer)?; + store!(Command, &self.command, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyVirtualChainChangedRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let include_accepted_transaction_ids = load!(bool, reader)?; + let command = load!(Command, reader)?; + Ok(Self { include_accepted_transaction_ids, command }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyVirtualChainChangedResponse {} +impl Serializer for NotifyVirtualChainChangedResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyVirtualChainChangedResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + // VirtualChainChangedNotification is sent whenever the DAG's selected parent // chain had changed. // // See: NotifyVirtualChainChangedRequest -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct VirtualChainChangedNotification { pub removed_chain_block_hashes: Arc>, @@ -950,10 +2762,34 @@ pub struct VirtualChainChangedNotification { pub accepted_transaction_ids: Arc>, } +impl Serializer for VirtualChainChangedNotification { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.removed_chain_block_hashes, writer)?; + store!(Vec, &self.added_chain_block_hashes, writer)?; + store!(Vec, &self.accepted_transaction_ids, writer)?; + Ok(()) + } +} + +impl Deserializer for VirtualChainChangedNotification { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let removed_chain_block_hashes = load!(Vec, reader)?; + let added_chain_block_hashes = load!(Vec, reader)?; + let accepted_transaction_ids = load!(Vec, reader)?; + Ok(Self { + removed_chain_block_hashes: removed_chain_block_hashes.into(), + added_chain_block_hashes: added_chain_block_hashes.into(), + accepted_transaction_ids: accepted_transaction_ids.into(), + }) + } +} + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // FinalityConflictNotification -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyFinalityConflictRequest { pub command: Command, @@ -965,20 +2801,66 @@ impl NotifyFinalityConflictRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyFinalityConflictRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Command, &self.command, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyFinalityConflictRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let command = load!(Command, reader)?; + Ok(Self { command }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyFinalityConflictResponse {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyFinalityConflictResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyFinalityConflictResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FinalityConflictNotification { pub violating_block_hash: RpcHash, } +impl Serializer for FinalityConflictNotification { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcHash, &self.violating_block_hash, writer)?; + Ok(()) + } +} + +impl Deserializer for FinalityConflictNotification { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let violating_block_hash = load!(RpcHash, reader)?; + Ok(Self { violating_block_hash }) + } +} + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // FinalityConflictResolvedNotification -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyFinalityConflictResolvedRequest { pub command: Command, @@ -990,16 +2872,62 @@ impl NotifyFinalityConflictResolvedRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyFinalityConflictResolvedRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Command, &self.command, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyFinalityConflictResolvedRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let command = load!(Command, reader)?; + Ok(Self { command }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyFinalityConflictResolvedResponse {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyFinalityConflictResolvedResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyFinalityConflictResolvedResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FinalityConflictResolvedNotification { pub finality_block_hash: RpcHash, } +impl Serializer for FinalityConflictResolvedNotification { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcHash, &self.finality_block_hash, writer)?; + Ok(()) + } +} + +impl Deserializer for FinalityConflictResolvedNotification { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let finality_block_hash = load!(RpcHash, reader)?; + Ok(Self { finality_block_hash }) + } +} + // ~~~~~~~~~~~~~~~~~~~~~~~~ // UtxosChangedNotification @@ -1012,7 +2940,7 @@ pub struct FinalityConflictResolvedNotification { // This call is only available when this kaspad was started with `--utxoindex` // // See: UtxosChangedNotification -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyUtxosChangedRequest { pub addresses: Vec, @@ -1025,14 +2953,46 @@ impl NotifyUtxosChangedRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyUtxosChangedRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.addresses, writer)?; + store!(Command, &self.command, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyUtxosChangedRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let addresses = load!(Vec, reader)?; + let command = load!(Command, reader)?; + Ok(Self { addresses, command }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyUtxosChangedResponse {} +impl Serializer for NotifyUtxosChangedResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyUtxosChangedResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + // UtxosChangedNotificationMessage is sent whenever the UTXO index had been updated. // // See: NotifyUtxosChangedRequest -#[derive(Clone, Debug, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UtxosChangedNotification { pub added: Arc>, @@ -1069,6 +3029,24 @@ impl UtxosChangedNotification { } } +impl Serializer for UtxosChangedNotification { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(Vec, &self.added, writer)?; + serialize!(Vec, &self.removed, writer)?; + Ok(()) + } +} + +impl Deserializer for UtxosChangedNotification { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let added = deserialize!(Vec, reader)?; + let removed = deserialize!(Vec, reader)?; + Ok(Self { added: added.into(), removed: removed.into() }) + } +} + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SinkBlueScoreChangedNotification @@ -1076,7 +3054,7 @@ impl UtxosChangedNotification { // sinkBlueScoreChanged notifications. // // See: SinkBlueScoreChangedNotification -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifySinkBlueScoreChangedRequest { pub command: Command, @@ -1088,20 +3066,66 @@ impl NotifySinkBlueScoreChangedRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifySinkBlueScoreChangedRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Command, &self.command, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifySinkBlueScoreChangedRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let command = load!(Command, reader)?; + Ok(Self { command }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifySinkBlueScoreChangedResponse {} +impl Serializer for NotifySinkBlueScoreChangedResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifySinkBlueScoreChangedResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + // SinkBlueScoreChangedNotification is sent whenever the blue score // of the virtual's selected parent changes. // /// See: NotifySinkBlueScoreChangedRequest -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SinkBlueScoreChangedNotification { pub sink_blue_score: u64, } +impl Serializer for SinkBlueScoreChangedNotification { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.sink_blue_score, writer)?; + Ok(()) + } +} + +impl Deserializer for SinkBlueScoreChangedNotification { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let sink_blue_score = load!(u64, reader)?; + Ok(Self { sink_blue_score }) + } +} + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // VirtualDaaScoreChangedNotification @@ -1109,7 +3133,7 @@ pub struct SinkBlueScoreChangedNotification { // virtualDaaScoreChanged notifications. // // See: VirtualDaaScoreChangedNotification -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyVirtualDaaScoreChangedRequest { pub command: Command, @@ -1121,24 +3145,70 @@ impl NotifyVirtualDaaScoreChangedRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyVirtualDaaScoreChangedRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Command, &self.command, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyVirtualDaaScoreChangedRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let command = load!(Command, reader)?; + Ok(Self { command }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyVirtualDaaScoreChangedResponse {} +impl Serializer for NotifyVirtualDaaScoreChangedResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyVirtualDaaScoreChangedResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + // VirtualDaaScoreChangedNotification is sent whenever the DAA score // of the virtual changes. // // See NotifyVirtualDaaScoreChangedRequest -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct VirtualDaaScoreChangedNotification { pub virtual_daa_score: u64, } +impl Serializer for VirtualDaaScoreChangedNotification { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.virtual_daa_score, writer)?; + Ok(()) + } +} + +impl Deserializer for VirtualDaaScoreChangedNotification { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let virtual_daa_score = load!(u64, reader)?; + Ok(Self { virtual_daa_score }) + } +} + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // PruningPointUtxoSetOverrideNotification -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyPruningPointUtxoSetOverrideRequest { pub command: Command, @@ -1150,21 +3220,65 @@ impl NotifyPruningPointUtxoSetOverrideRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyPruningPointUtxoSetOverrideRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Command, &self.command, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyPruningPointUtxoSetOverrideRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let command = load!(Command, reader)?; + Ok(Self { command }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyPruningPointUtxoSetOverrideResponse {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyPruningPointUtxoSetOverrideResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyPruningPointUtxoSetOverrideResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PruningPointUtxoSetOverrideNotification {} +impl Serializer for PruningPointUtxoSetOverrideNotification { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for PruningPointUtxoSetOverrideNotification { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // NewBlockTemplateNotification /// NotifyNewBlockTemplateRequest registers this connection for blockAdded notifications. /// /// See: NewBlockTemplateNotification -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyNewBlockTemplateRequest { pub command: Command, @@ -1175,22 +3289,66 @@ impl NotifyNewBlockTemplateRequest { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +impl Serializer for NotifyNewBlockTemplateRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Command, &self.command, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyNewBlockTemplateRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let command = load!(Command, reader)?; + Ok(Self { command }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyNewBlockTemplateResponse {} +impl Serializer for NotifyNewBlockTemplateResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NotifyNewBlockTemplateResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + /// NewBlockTemplateNotification is sent whenever a blocks has been added (NOT accepted) /// into the DAG. /// /// See: NotifyNewBlockTemplateRequest -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NewBlockTemplateNotification {} +impl Serializer for NewBlockTemplateNotification { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for NewBlockTemplateNotification { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + Ok(Self {}) + } +} + /// /// wRPC response for RpcApiOps::Subscribe request /// -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SubscribeResponse { id: u64, @@ -1202,9 +3360,38 @@ impl SubscribeResponse { } } +impl Serializer for SubscribeResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u64, &self.id, writer)?; + Ok(()) + } +} + +impl Deserializer for SubscribeResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let id = load!(u64, reader)?; + Ok(Self { id }) + } +} + /// /// wRPC response for RpcApiOps::Unsubscribe request /// -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UnsubscribeResponse {} + +impl Serializer for UnsubscribeResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer) + } +} + +impl Deserializer for UnsubscribeResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader); + Ok(Self {}) + } +} diff --git a/rpc/core/src/model/mod.rs b/rpc/core/src/model/mod.rs index 8950bd1cb3..beef032572 100644 --- a/rpc/core/src/model/mod.rs +++ b/rpc/core/src/model/mod.rs @@ -11,6 +11,7 @@ pub mod network; pub mod peer; pub mod script_class; pub mod subnets; +mod tests; pub mod tx; pub use address::*; diff --git a/rpc/core/src/model/tests.rs b/rpc/core/src/model/tests.rs new file mode 100644 index 0000000000..bc3369ee0f --- /dev/null +++ b/rpc/core/src/model/tests.rs @@ -0,0 +1,1325 @@ +#[cfg(test)] +mod mockery { + + use crate::{model::*, RpcScriptClass}; + use kaspa_addresses::{Prefix, Version}; + use kaspa_consensus_core::api::BlockCount; + use kaspa_consensus_core::network::NetworkType; + use kaspa_consensus_core::subnets::SubnetworkId; + use kaspa_consensus_core::tx::ScriptPublicKey; + use kaspa_hashes::Hash; + use kaspa_math::Uint192; + use kaspa_notify::subscription::Command; + use kaspa_rpc_macros::test_wrpc_serializer as test; + use kaspa_utils::networking::{ContextualNetAddress, IpAddress, NetAddress}; + use rand::Rng; + use std::net::{IpAddr, Ipv4Addr}; + use std::sync::Arc; + use uuid::Uuid; + use workflow_serializer::prelude::*; + + // this trait is used to generate random + // values for testing on various data types + trait Mock { + fn mock() -> Self; + } + + impl Mock for Option + where + T: Mock, + { + fn mock() -> Self { + Some(T::mock()) + } + } + + impl Mock for Vec + where + T: Mock, + { + fn mock() -> Self { + vec![T::mock()] + } + } + + impl Mock for Arc + where + T: Mock, + { + fn mock() -> Self { + Arc::new(T::mock()) + } + } + + fn mock() -> T + where + T: Mock, + { + Mock::mock() + } + + // this function tests serialization and deserialization of a type + // by serializing it (A), deserializing it, serializing it again (B) + // and comparing A and B buffers. + fn test(kind: &str) + where + T: Serializer + Deserializer + Mock, + { + let data = T::mock(); + + const PREFIX: u32 = 0x12345678; + const SUFFIX: u32 = 0x90abcdef; + + let mut buffer1 = Vec::new(); + let writer = &mut buffer1; + store!(u32, &PREFIX, writer).unwrap(); + serialize!(T, &data, writer).unwrap(); + store!(u32, &SUFFIX, writer).unwrap(); + + let reader = &mut buffer1.as_slice(); + let prefix: u32 = load!(u32, reader).unwrap(); + // this will never occur, but it's a good practice to check in case + // the serialization/deserialization logic changes in the future + assert_eq!(prefix, PREFIX, "misalignment when consuming serialized buffer in `{kind}`"); + let tmp = deserialize!(T, reader).unwrap(); + let suffix: u32 = load!(u32, reader).unwrap(); + assert_eq!(suffix, SUFFIX, "misalignment when consuming serialized buffer in `{kind}`"); + + let mut buffer2 = Vec::new(); + let writer = &mut buffer2; + store!(u32, &PREFIX, writer).unwrap(); + serialize!(T, &tmp, writer).unwrap(); + store!(u32, &SUFFIX, writer).unwrap(); + + assert!(buffer1 == buffer2, "serialization/deserialization failure while testing `{kind}`"); + } + + #[macro_export] + macro_rules! impl_mock { + ($($type:ty),*) => { + $(impl Mock for $type { + fn mock() -> Self { + rand::thread_rng().gen() + } + })* + }; + } + + impl_mock!(bool, u8, u16, u32, f32, u64, i64, f64); + + impl Mock for Uint192 { + fn mock() -> Self { + Uint192([mock(), mock(), mock()]) + } + } + + impl Mock for SubnetworkId { + fn mock() -> Self { + let mut bytes: [u8; 20] = [0; 20]; + rand::thread_rng().fill(&mut bytes); + SubnetworkId::from_bytes(bytes) + } + } + + impl Mock for Hash { + fn mock() -> Self { + let mut bytes: [u8; 32] = [0; 32]; + rand::thread_rng().fill(&mut bytes); + Hash::from_bytes(bytes) + } + } + + impl Mock for RpcAddress { + fn mock() -> Self { + RpcAddress::new(Prefix::Mainnet, Version::PubKey, Hash::mock().as_bytes().as_slice()) + } + } + + impl Mock for RpcHeader { + fn mock() -> Self { + RpcHeader { + version: mock(), + timestamp: mock(), + bits: mock(), + nonce: mock(), + hash_merkle_root: mock(), + accepted_id_merkle_root: mock(), + utxo_commitment: mock(), + hash: mock(), + parents_by_level: vec![mock()], + daa_score: mock(), + blue_score: mock(), + blue_work: mock(), + pruning_point: mock(), + } + } + } + + impl Mock for RpcRawHeader { + fn mock() -> Self { + RpcRawHeader { + version: mock(), + timestamp: mock(), + bits: mock(), + nonce: mock(), + hash_merkle_root: mock(), + accepted_id_merkle_root: mock(), + utxo_commitment: mock(), + parents_by_level: vec![mock()], + daa_score: mock(), + blue_score: mock(), + blue_work: mock(), + pruning_point: mock(), + } + } + } + + impl Mock for RpcBlockVerboseData { + fn mock() -> Self { + RpcBlockVerboseData { + hash: mock(), + difficulty: mock(), + selected_parent_hash: mock(), + transaction_ids: mock(), + is_header_only: mock(), + blue_score: mock(), + children_hashes: mock(), + merge_set_blues_hashes: mock(), + merge_set_reds_hashes: mock(), + is_chain_block: mock(), + } + } + } + + impl Mock for RpcBlock { + fn mock() -> Self { + RpcBlock { header: mock(), transactions: mock(), verbose_data: mock() } + } + } + + impl Mock for RpcRawBlock { + fn mock() -> Self { + RpcRawBlock { header: mock(), transactions: mock() } + } + } + + impl Mock for RpcTransactionInputVerboseData { + fn mock() -> Self { + RpcTransactionInputVerboseData {} + } + } + + impl Mock for RpcTransactionInput { + fn mock() -> Self { + RpcTransactionInput { + previous_outpoint: mock(), + signature_script: Hash::mock().as_bytes().to_vec(), + sequence: mock(), + sig_op_count: mock(), + verbose_data: mock(), + } + } + } + + impl Mock for RpcTransactionOutputVerboseData { + fn mock() -> Self { + RpcTransactionOutputVerboseData { script_public_key_type: RpcScriptClass::PubKey, script_public_key_address: mock() } + } + } + + impl Mock for RpcTransactionOutput { + fn mock() -> Self { + RpcTransactionOutput { value: mock(), script_public_key: mock(), verbose_data: mock() } + } + } + + impl Mock for RpcTransactionVerboseData { + fn mock() -> Self { + RpcTransactionVerboseData { transaction_id: mock(), hash: mock(), mass: mock(), block_hash: mock(), block_time: mock() } + } + } + + impl Mock for RpcTransaction { + fn mock() -> Self { + RpcTransaction { + version: mock(), + inputs: mock(), + outputs: mock(), + lock_time: mock(), + subnetwork_id: mock(), + gas: mock(), + payload: Hash::mock().as_bytes().to_vec(), + mass: mock(), + verbose_data: mock(), + } + } + } + + impl Mock for RpcNodeId { + fn mock() -> Self { + RpcNodeId::new(Uuid::new_v4()) + } + } + + impl Mock for IpAddr { + fn mock() -> Self { + IpAddr::V4(Ipv4Addr::new(mock(), mock(), mock(), mock())) + } + } + + impl Mock for IpAddress { + fn mock() -> Self { + IpAddress::new(mock()) + } + } + + impl Mock for NetAddress { + fn mock() -> Self { + NetAddress::new(IpAddress::new(mock()), mock()) + } + } + + impl Mock for ContextualNetAddress { + fn mock() -> Self { + ContextualNetAddress::new(mock(), mock()) + } + } + + impl Mock for RpcPeerInfo { + fn mock() -> Self { + RpcPeerInfo { + id: mock(), + address: mock(), + last_ping_duration: mock(), + is_outbound: mock(), + time_offset: mock(), + user_agent: "0.4.2".to_string(), + advertised_protocol_version: mock(), + time_connected: mock(), + is_ibd_peer: mock(), + } + } + } + + impl Mock for RpcMempoolEntry { + fn mock() -> Self { + RpcMempoolEntry { fee: mock(), transaction: mock(), is_orphan: mock() } + } + } + + impl Mock for RpcMempoolEntryByAddress { + fn mock() -> Self { + RpcMempoolEntryByAddress { address: mock(), sending: mock(), receiving: mock() } + } + } + + impl Mock for ScriptPublicKey { + fn mock() -> Self { + let mut bytes: [u8; 36] = [0; 36]; + rand::thread_rng().fill(&mut bytes[..]); + ScriptPublicKey::from_vec(0, bytes.to_vec()) + } + } + + impl Mock for RpcUtxoEntry { + fn mock() -> Self { + RpcUtxoEntry { amount: mock(), script_public_key: mock(), block_daa_score: mock(), is_coinbase: true } + } + } + + impl Mock for RpcTransactionOutpoint { + fn mock() -> Self { + RpcTransactionOutpoint { transaction_id: mock(), index: mock() } + } + } + + impl Mock for RpcUtxosByAddressesEntry { + fn mock() -> Self { + RpcUtxosByAddressesEntry { address: mock(), outpoint: mock(), utxo_entry: mock() } + } + } + + impl Mock for ProcessMetrics { + fn mock() -> Self { + ProcessMetrics { + resident_set_size: mock(), + virtual_memory_size: mock(), + core_num: mock(), + cpu_usage: mock(), + fd_num: mock(), + disk_io_read_bytes: mock(), + disk_io_write_bytes: mock(), + disk_io_read_per_sec: mock(), + disk_io_write_per_sec: mock(), + } + } + } + + impl Mock for ConnectionMetrics { + fn mock() -> Self { + ConnectionMetrics { + borsh_live_connections: mock(), + borsh_connection_attempts: mock(), + borsh_handshake_failures: mock(), + json_live_connections: mock(), + json_connection_attempts: mock(), + json_handshake_failures: mock(), + active_peers: mock(), + } + } + } + + impl Mock for BandwidthMetrics { + fn mock() -> Self { + BandwidthMetrics { + borsh_bytes_tx: mock(), + borsh_bytes_rx: mock(), + json_bytes_tx: mock(), + json_bytes_rx: mock(), + p2p_bytes_tx: mock(), + p2p_bytes_rx: mock(), + grpc_bytes_tx: mock(), + grpc_bytes_rx: mock(), + } + } + } + + impl Mock for ConsensusMetrics { + fn mock() -> Self { + ConsensusMetrics { + node_blocks_submitted_count: mock(), + node_headers_processed_count: mock(), + node_dependencies_processed_count: mock(), + node_bodies_processed_count: mock(), + node_transactions_processed_count: mock(), + node_chain_blocks_processed_count: mock(), + node_mass_processed_count: mock(), + node_database_blocks_count: mock(), + node_database_headers_count: mock(), + network_mempool_size: mock(), + network_tip_hashes_count: mock(), + network_difficulty: mock(), + network_past_median_time: mock(), + network_virtual_parent_hashes_count: mock(), + network_virtual_daa_score: mock(), + } + } + } + + impl Mock for StorageMetrics { + fn mock() -> Self { + StorageMetrics { storage_size_bytes: mock() } + } + } + + // -------------------------------------------- + // implementations for all the rpc request + // and response data structures. + + impl Mock for SubmitBlockRequest { + fn mock() -> Self { + SubmitBlockRequest { block: mock(), allow_non_daa_blocks: true } + } + } + + test!(SubmitBlockRequest); + + impl Mock for SubmitBlockResponse { + fn mock() -> Self { + SubmitBlockResponse { report: SubmitBlockReport::Success } + } + } + + test!(SubmitBlockResponse); + + impl Mock for GetBlockTemplateRequest { + fn mock() -> Self { + GetBlockTemplateRequest { pay_address: mock(), extra_data: vec![4, 2] } + } + } + + test!(GetBlockTemplateRequest); + + impl Mock for GetBlockTemplateResponse { + fn mock() -> Self { + GetBlockTemplateResponse { block: mock(), is_synced: true } + } + } + + test!(GetBlockTemplateResponse); + + impl Mock for GetBlockRequest { + fn mock() -> Self { + GetBlockRequest { hash: mock(), include_transactions: true } + } + } + + test!(GetBlockRequest); + + impl Mock for GetBlockResponse { + fn mock() -> Self { + GetBlockResponse { block: mock() } + } + } + + test!(GetBlockResponse); + + impl Mock for GetInfoRequest { + fn mock() -> Self { + GetInfoRequest {} + } + } + + test!(GetInfoRequest); + + impl Mock for GetInfoResponse { + fn mock() -> Self { + GetInfoResponse { + p2p_id: Hash::mock().to_string(), + mempool_size: mock(), + server_version: "0.4.2".to_string(), + is_utxo_indexed: true, + is_synced: false, + has_notify_command: true, + has_message_id: false, + } + } + } + + test!(GetInfoResponse); + + impl Mock for GetCurrentNetworkRequest { + fn mock() -> Self { + GetCurrentNetworkRequest {} + } + } + + test!(GetCurrentNetworkRequest); + + impl Mock for GetCurrentNetworkResponse { + fn mock() -> Self { + GetCurrentNetworkResponse { network: NetworkType::Mainnet } + } + } + + test!(GetCurrentNetworkResponse); + + impl Mock for GetPeerAddressesRequest { + fn mock() -> Self { + GetPeerAddressesRequest {} + } + } + + test!(GetPeerAddressesRequest); + + impl Mock for GetPeerAddressesResponse { + fn mock() -> Self { + GetPeerAddressesResponse { known_addresses: mock(), banned_addresses: mock() } + } + } + + test!(GetPeerAddressesResponse); + + impl Mock for GetSinkRequest { + fn mock() -> Self { + GetSinkRequest {} + } + } + + test!(GetSinkRequest); + + impl Mock for GetSinkResponse { + fn mock() -> Self { + GetSinkResponse { sink: mock() } + } + } + + test!(GetSinkResponse); + + impl Mock for GetMempoolEntryRequest { + fn mock() -> Self { + GetMempoolEntryRequest { transaction_id: mock(), include_orphan_pool: true, filter_transaction_pool: false } + } + } + + test!(GetMempoolEntryRequest); + + impl Mock for GetMempoolEntryResponse { + fn mock() -> Self { + GetMempoolEntryResponse { mempool_entry: RpcMempoolEntry { fee: mock(), transaction: mock(), is_orphan: false } } + } + } + + test!(GetMempoolEntryResponse); + + impl Mock for GetMempoolEntriesRequest { + fn mock() -> Self { + GetMempoolEntriesRequest { include_orphan_pool: true, filter_transaction_pool: false } + } + } + + test!(GetMempoolEntriesRequest); + + impl Mock for GetMempoolEntriesResponse { + fn mock() -> Self { + GetMempoolEntriesResponse { mempool_entries: mock() } + } + } + + test!(GetMempoolEntriesResponse); + + impl Mock for GetConnectedPeerInfoRequest { + fn mock() -> Self { + GetConnectedPeerInfoRequest {} + } + } + + test!(GetConnectedPeerInfoRequest); + + impl Mock for GetConnectedPeerInfoResponse { + fn mock() -> Self { + GetConnectedPeerInfoResponse { peer_info: mock() } + } + } + + test!(GetConnectedPeerInfoResponse); + + impl Mock for AddPeerRequest { + fn mock() -> Self { + AddPeerRequest { peer_address: mock(), is_permanent: mock() } + } + } + + test!(AddPeerRequest); + + impl Mock for AddPeerResponse { + fn mock() -> Self { + AddPeerResponse {} + } + } + + test!(AddPeerResponse); + + impl Mock for SubmitTransactionRequest { + fn mock() -> Self { + SubmitTransactionRequest { transaction: mock(), allow_orphan: mock() } + } + } + + test!(SubmitTransactionRequest); + + impl Mock for SubmitTransactionResponse { + fn mock() -> Self { + SubmitTransactionResponse { transaction_id: mock() } + } + } + + test!(SubmitTransactionResponse); + + impl Mock for GetSubnetworkRequest { + fn mock() -> Self { + GetSubnetworkRequest { subnetwork_id: mock() } + } + } + + test!(GetSubnetworkRequest); + + impl Mock for GetSubnetworkResponse { + fn mock() -> Self { + GetSubnetworkResponse { gas_limit: mock() } + } + } + + test!(GetSubnetworkResponse); + + impl Mock for GetVirtualChainFromBlockRequest { + fn mock() -> Self { + GetVirtualChainFromBlockRequest { start_hash: mock(), include_accepted_transaction_ids: mock() } + } + } + + test!(GetVirtualChainFromBlockRequest); + + impl Mock for RpcAcceptedTransactionIds { + fn mock() -> Self { + RpcAcceptedTransactionIds { accepting_block_hash: mock(), accepted_transaction_ids: mock() } + } + } + + impl Mock for GetVirtualChainFromBlockResponse { + fn mock() -> Self { + GetVirtualChainFromBlockResponse { + removed_chain_block_hashes: mock(), + added_chain_block_hashes: mock(), + accepted_transaction_ids: mock(), + } + } + } + + test!(GetVirtualChainFromBlockResponse); + + impl Mock for GetBlocksRequest { + fn mock() -> Self { + GetBlocksRequest { low_hash: mock(), include_blocks: mock(), include_transactions: mock() } + } + } + + test!(GetBlocksRequest); + + impl Mock for GetBlocksResponse { + fn mock() -> Self { + GetBlocksResponse { block_hashes: mock(), blocks: mock() } + } + } + + test!(GetBlocksResponse); + + impl Mock for GetBlockCountRequest { + fn mock() -> Self { + GetBlockCountRequest {} + } + } + + test!(GetBlockCountRequest); + + impl Mock for BlockCount { + fn mock() -> Self { + BlockCount { header_count: mock(), block_count: mock() } + } + } + + test!(BlockCount); + + impl Mock for GetBlockDagInfoRequest { + fn mock() -> Self { + GetBlockDagInfoRequest {} + } + } + + test!(GetBlockDagInfoRequest); + + impl Mock for GetBlockDagInfoResponse { + fn mock() -> Self { + GetBlockDagInfoResponse { + network: NetworkType::Mainnet.try_into().unwrap(), + block_count: mock(), + header_count: mock(), + tip_hashes: mock(), + difficulty: mock(), + past_median_time: mock(), + virtual_parent_hashes: mock(), + pruning_point_hash: mock(), + virtual_daa_score: mock(), + sink: mock(), + } + } + } + + test!(GetBlockDagInfoResponse); + + impl Mock for ResolveFinalityConflictRequest { + fn mock() -> Self { + ResolveFinalityConflictRequest { finality_block_hash: mock() } + } + } + + test!(ResolveFinalityConflictRequest); + + impl Mock for ResolveFinalityConflictResponse { + fn mock() -> Self { + ResolveFinalityConflictResponse {} + } + } + + test!(ResolveFinalityConflictResponse); + + impl Mock for ShutdownRequest { + fn mock() -> Self { + ShutdownRequest {} + } + } + + test!(ShutdownRequest); + + impl Mock for ShutdownResponse { + fn mock() -> Self { + ShutdownResponse {} + } + } + + test!(ShutdownResponse); + + impl Mock for GetHeadersRequest { + fn mock() -> Self { + GetHeadersRequest { start_hash: mock(), limit: mock(), is_ascending: mock() } + } + } + + test!(GetHeadersRequest); + + impl Mock for GetHeadersResponse { + fn mock() -> Self { + GetHeadersResponse { headers: mock() } + } + } + + test!(GetHeadersResponse); + + impl Mock for GetBalanceByAddressRequest { + fn mock() -> Self { + GetBalanceByAddressRequest { address: mock() } + } + } + + test!(GetBalanceByAddressRequest); + + impl Mock for GetBalanceByAddressResponse { + fn mock() -> Self { + GetBalanceByAddressResponse { balance: mock() } + } + } + + test!(GetBalanceByAddressResponse); + + impl Mock for GetBalancesByAddressesRequest { + fn mock() -> Self { + GetBalancesByAddressesRequest { addresses: mock() } + } + } + + test!(GetBalancesByAddressesRequest); + + impl Mock for RpcBalancesByAddressesEntry { + fn mock() -> Self { + RpcBalancesByAddressesEntry { address: mock(), balance: mock() } + } + } + + impl Mock for GetBalancesByAddressesResponse { + fn mock() -> Self { + GetBalancesByAddressesResponse { entries: mock() } + } + } + + test!(GetBalancesByAddressesResponse); + + impl Mock for GetSinkBlueScoreRequest { + fn mock() -> Self { + GetSinkBlueScoreRequest {} + } + } + + test!(GetSinkBlueScoreRequest); + + impl Mock for GetSinkBlueScoreResponse { + fn mock() -> Self { + GetSinkBlueScoreResponse { blue_score: mock() } + } + } + + test!(GetSinkBlueScoreResponse); + + impl Mock for GetUtxosByAddressesRequest { + fn mock() -> Self { + GetUtxosByAddressesRequest { addresses: mock() } + } + } + + test!(GetUtxosByAddressesRequest); + + impl Mock for GetUtxosByAddressesResponse { + fn mock() -> Self { + GetUtxosByAddressesResponse { entries: mock() } + } + } + + test!(GetUtxosByAddressesResponse); + + impl Mock for BanRequest { + fn mock() -> Self { + BanRequest { ip: mock() } + } + } + + test!(BanRequest); + + impl Mock for BanResponse { + fn mock() -> Self { + BanResponse {} + } + } + + test!(BanResponse); + + impl Mock for UnbanRequest { + fn mock() -> Self { + UnbanRequest { ip: mock() } + } + } + + test!(UnbanRequest); + + impl Mock for UnbanResponse { + fn mock() -> Self { + UnbanResponse {} + } + } + + test!(UnbanResponse); + + impl Mock for EstimateNetworkHashesPerSecondRequest { + fn mock() -> Self { + EstimateNetworkHashesPerSecondRequest { window_size: mock(), start_hash: mock() } + } + } + + test!(EstimateNetworkHashesPerSecondRequest); + + impl Mock for EstimateNetworkHashesPerSecondResponse { + fn mock() -> Self { + EstimateNetworkHashesPerSecondResponse { network_hashes_per_second: mock() } + } + } + + test!(EstimateNetworkHashesPerSecondResponse); + + impl Mock for GetMempoolEntriesByAddressesRequest { + fn mock() -> Self { + GetMempoolEntriesByAddressesRequest { addresses: mock(), include_orphan_pool: true, filter_transaction_pool: false } + } + } + + test!(GetMempoolEntriesByAddressesRequest); + + impl Mock for GetMempoolEntriesByAddressesResponse { + fn mock() -> Self { + GetMempoolEntriesByAddressesResponse { entries: mock() } + } + } + + test!(GetMempoolEntriesByAddressesResponse); + + impl Mock for GetCoinSupplyRequest { + fn mock() -> Self { + GetCoinSupplyRequest {} + } + } + + test!(GetCoinSupplyRequest); + + impl Mock for GetCoinSupplyResponse { + fn mock() -> Self { + GetCoinSupplyResponse { max_sompi: mock(), circulating_sompi: mock() } + } + } + + test!(GetCoinSupplyResponse); + + impl Mock for PingRequest { + fn mock() -> Self { + PingRequest {} + } + } + + test!(PingRequest); + + impl Mock for PingResponse { + fn mock() -> Self { + PingResponse {} + } + } + + test!(PingResponse); + + impl Mock for GetConnectionsRequest { + fn mock() -> Self { + GetConnectionsRequest { include_profile_data: false } + } + } + + test!(GetConnectionsRequest); + + impl Mock for GetConnectionsResponse { + fn mock() -> Self { + GetConnectionsResponse { clients: mock(), peers: mock(), profile_data: None } + } + } + + test!(GetConnectionsResponse); + + impl Mock for GetSystemInfoRequest { + fn mock() -> Self { + GetSystemInfoRequest {} + } + } + + test!(GetSystemInfoRequest); + + impl Mock for GetSystemInfoResponse { + fn mock() -> Self { + GetSystemInfoResponse { + version: "1.2.3".to_string(), + system_id: mock(), + git_hash: mock(), + cpu_physical_cores: mock(), + total_memory: mock(), + fd_limit: mock(), + } + } + } + + test!(GetSystemInfoResponse); + + impl Mock for GetMetricsRequest { + fn mock() -> Self { + GetMetricsRequest { + process_metrics: true, + connection_metrics: true, + bandwidth_metrics: true, + consensus_metrics: true, + storage_metrics: true, + custom_metrics: false, + } + } + } + + test!(GetMetricsRequest); + + impl Mock for GetMetricsResponse { + fn mock() -> Self { + GetMetricsResponse { + server_time: mock(), + process_metrics: mock(), + connection_metrics: mock(), + bandwidth_metrics: mock(), + consensus_metrics: mock(), + storage_metrics: mock(), + custom_metrics: None, + } + } + } + + test!(GetMetricsResponse); + + impl Mock for GetServerInfoRequest { + fn mock() -> Self { + GetServerInfoRequest {} + } + } + + test!(GetServerInfoRequest); + + impl Mock for GetServerInfoResponse { + fn mock() -> Self { + GetServerInfoResponse { + rpc_api_version: mock(), + rpc_api_revision: mock(), + server_version: "0.4.2".to_string(), + network_id: NetworkType::Mainnet.try_into().unwrap(), + has_utxo_index: true, + is_synced: false, + virtual_daa_score: mock(), + } + } + } + + test!(GetServerInfoResponse); + + impl Mock for GetSyncStatusRequest { + fn mock() -> Self { + GetSyncStatusRequest {} + } + } + + test!(GetSyncStatusRequest); + + impl Mock for GetSyncStatusResponse { + fn mock() -> Self { + GetSyncStatusResponse { is_synced: true } + } + } + + test!(GetSyncStatusResponse); + + impl Mock for GetDaaScoreTimestampEstimateRequest { + fn mock() -> Self { + GetDaaScoreTimestampEstimateRequest { daa_scores: mock() } + } + } + + test!(GetDaaScoreTimestampEstimateRequest); + + impl Mock for GetDaaScoreTimestampEstimateResponse { + fn mock() -> Self { + GetDaaScoreTimestampEstimateResponse { timestamps: mock() } + } + } + + test!(GetDaaScoreTimestampEstimateResponse); + + impl Mock for NotifyBlockAddedRequest { + fn mock() -> Self { + NotifyBlockAddedRequest { command: Command::Start } + } + } + + test!(NotifyBlockAddedRequest); + + impl Mock for NotifyBlockAddedResponse { + fn mock() -> Self { + NotifyBlockAddedResponse {} + } + } + + test!(NotifyBlockAddedResponse); + + impl Mock for BlockAddedNotification { + fn mock() -> Self { + BlockAddedNotification { block: mock() } + } + } + + test!(BlockAddedNotification); + + impl Mock for NotifyVirtualChainChangedRequest { + fn mock() -> Self { + NotifyVirtualChainChangedRequest { command: Command::Start, include_accepted_transaction_ids: true } + } + } + + test!(NotifyVirtualChainChangedRequest); + + impl Mock for NotifyVirtualChainChangedResponse { + fn mock() -> Self { + NotifyVirtualChainChangedResponse {} + } + } + + test!(NotifyVirtualChainChangedResponse); + + impl Mock for VirtualChainChangedNotification { + fn mock() -> Self { + VirtualChainChangedNotification { + removed_chain_block_hashes: mock(), + added_chain_block_hashes: mock(), + accepted_transaction_ids: mock(), + } + } + } + + test!(VirtualChainChangedNotification); + + impl Mock for NotifyFinalityConflictRequest { + fn mock() -> Self { + NotifyFinalityConflictRequest { command: Command::Start } + } + } + + test!(NotifyFinalityConflictRequest); + + impl Mock for NotifyFinalityConflictResponse { + fn mock() -> Self { + NotifyFinalityConflictResponse {} + } + } + + test!(NotifyFinalityConflictResponse); + + impl Mock for FinalityConflictNotification { + fn mock() -> Self { + FinalityConflictNotification { violating_block_hash: mock() } + } + } + + test!(FinalityConflictNotification); + + impl Mock for NotifyFinalityConflictResolvedRequest { + fn mock() -> Self { + NotifyFinalityConflictResolvedRequest { command: Command::Start } + } + } + + test!(NotifyFinalityConflictResolvedRequest); + + impl Mock for NotifyFinalityConflictResolvedResponse { + fn mock() -> Self { + NotifyFinalityConflictResolvedResponse {} + } + } + + test!(NotifyFinalityConflictResolvedResponse); + + impl Mock for FinalityConflictResolvedNotification { + fn mock() -> Self { + FinalityConflictResolvedNotification { finality_block_hash: mock() } + } + } + + test!(FinalityConflictResolvedNotification); + + impl Mock for NotifyUtxosChangedRequest { + fn mock() -> Self { + NotifyUtxosChangedRequest { addresses: mock(), command: Command::Start } + } + } + + test!(NotifyUtxosChangedRequest); + + impl Mock for NotifyUtxosChangedResponse { + fn mock() -> Self { + NotifyUtxosChangedResponse {} + } + } + + test!(NotifyUtxosChangedResponse); + + impl Mock for UtxosChangedNotification { + fn mock() -> Self { + UtxosChangedNotification { added: mock(), removed: mock() } + } + } + + test!(UtxosChangedNotification); + + impl Mock for NotifySinkBlueScoreChangedRequest { + fn mock() -> Self { + NotifySinkBlueScoreChangedRequest { command: Command::Start } + } + } + + test!(NotifySinkBlueScoreChangedRequest); + + impl Mock for NotifySinkBlueScoreChangedResponse { + fn mock() -> Self { + NotifySinkBlueScoreChangedResponse {} + } + } + + test!(NotifySinkBlueScoreChangedResponse); + + impl Mock for SinkBlueScoreChangedNotification { + fn mock() -> Self { + SinkBlueScoreChangedNotification { sink_blue_score: mock() } + } + } + + test!(SinkBlueScoreChangedNotification); + + impl Mock for NotifyVirtualDaaScoreChangedRequest { + fn mock() -> Self { + NotifyVirtualDaaScoreChangedRequest { command: Command::Start } + } + } + + test!(NotifyVirtualDaaScoreChangedRequest); + + impl Mock for NotifyVirtualDaaScoreChangedResponse { + fn mock() -> Self { + NotifyVirtualDaaScoreChangedResponse {} + } + } + + test!(NotifyVirtualDaaScoreChangedResponse); + + impl Mock for VirtualDaaScoreChangedNotification { + fn mock() -> Self { + VirtualDaaScoreChangedNotification { virtual_daa_score: mock() } + } + } + + test!(VirtualDaaScoreChangedNotification); + + impl Mock for NotifyPruningPointUtxoSetOverrideRequest { + fn mock() -> Self { + NotifyPruningPointUtxoSetOverrideRequest { command: Command::Start } + } + } + + test!(NotifyPruningPointUtxoSetOverrideRequest); + + impl Mock for NotifyPruningPointUtxoSetOverrideResponse { + fn mock() -> Self { + NotifyPruningPointUtxoSetOverrideResponse {} + } + } + + test!(NotifyPruningPointUtxoSetOverrideResponse); + + impl Mock for PruningPointUtxoSetOverrideNotification { + fn mock() -> Self { + PruningPointUtxoSetOverrideNotification {} + } + } + + test!(PruningPointUtxoSetOverrideNotification); + + impl Mock for NotifyNewBlockTemplateRequest { + fn mock() -> Self { + NotifyNewBlockTemplateRequest { command: Command::Start } + } + } + + test!(NotifyNewBlockTemplateRequest); + + impl Mock for NotifyNewBlockTemplateResponse { + fn mock() -> Self { + NotifyNewBlockTemplateResponse {} + } + } + + test!(NotifyNewBlockTemplateResponse); + + impl Mock for NewBlockTemplateNotification { + fn mock() -> Self { + NewBlockTemplateNotification {} + } + } + + test!(NewBlockTemplateNotification); + + impl Mock for SubscribeResponse { + fn mock() -> Self { + SubscribeResponse::new(mock()) + } + } + + test!(SubscribeResponse); + + impl Mock for UnsubscribeResponse { + fn mock() -> Self { + UnsubscribeResponse {} + } + } + + test!(UnsubscribeResponse); + + struct Misalign; + + impl Mock for Misalign { + fn mock() -> Self { + Misalign + } + } + + impl Serializer for Misalign { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u32, &1, writer)?; + store!(u32, &2, writer)?; + store!(u32, &3, writer)?; + Ok(()) + } + } + + impl Deserializer for Misalign { + fn deserialize(reader: &mut R) -> std::io::Result { + let version: u32 = load!(u32, reader)?; + assert_eq!(version, 1); + Ok(Self) + } + } + + #[test] + fn test_misalignment() { + test::("Misalign"); + } +} diff --git a/rpc/core/src/model/tx.rs b/rpc/core/src/model/tx.rs index bb13f797de..ad1fea8a31 100644 --- a/rpc/core/src/model/tx.rs +++ b/rpc/core/src/model/tx.rs @@ -1,9 +1,12 @@ use borsh::{BorshDeserialize, BorshSerialize}; use kaspa_addresses::Address; use kaspa_consensus_core::tx::{ - ScriptPublicKey, ScriptVec, TransactionId, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry, + ScriptPublicKey, ScriptVec, TransactionId, TransactionIndexType, TransactionInput, TransactionOutpoint, TransactionOutput, + UtxoEntry, }; +use kaspa_utils::serde_bytes_fixed_ref; use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; use crate::prelude::{RpcHash, RpcScriptClass, RpcSubnetworkId}; @@ -12,13 +15,123 @@ pub type RpcTransactionId = TransactionId; pub type RpcScriptVec = ScriptVec; pub type RpcScriptPublicKey = ScriptPublicKey; -pub type RpcUtxoEntry = UtxoEntry; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcUtxoEntry { + pub amount: u64, + pub script_public_key: ScriptPublicKey, + pub block_daa_score: u64, + pub is_coinbase: bool, +} + +impl RpcUtxoEntry { + pub fn new(amount: u64, script_public_key: ScriptPublicKey, block_daa_score: u64, is_coinbase: bool) -> Self { + Self { amount, script_public_key, block_daa_score, is_coinbase } + } +} + +impl From for RpcUtxoEntry { + fn from(entry: UtxoEntry) -> Self { + Self { + 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 UtxoEntry { + fn from(entry: RpcUtxoEntry) -> Self { + Self { + amount: entry.amount, + script_public_key: entry.script_public_key, + block_daa_score: entry.block_daa_score, + is_coinbase: entry.is_coinbase, + } + } +} + +impl Serializer for RpcUtxoEntry { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(u64, &self.amount, writer)?; + store!(ScriptPublicKey, &self.script_public_key, writer)?; + store!(u64, &self.block_daa_score, writer)?; + store!(bool, &self.is_coinbase, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcUtxoEntry { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let amount = load!(u64, reader)?; + let script_public_key = load!(ScriptPublicKey, reader)?; + let block_daa_score = load!(u64, reader)?; + let is_coinbase = load!(bool, reader)?; + + Ok(Self { amount, script_public_key, block_daa_score, is_coinbase }) + } +} /// Represents a Kaspa transaction outpoint -pub type RpcTransactionOutpoint = TransactionOutpoint; +#[derive(Eq, Hash, PartialEq, Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcTransactionOutpoint { + #[serde(with = "serde_bytes_fixed_ref")] + pub transaction_id: TransactionId, + pub index: TransactionIndexType, +} + +impl From for RpcTransactionOutpoint { + fn from(outpoint: TransactionOutpoint) -> Self { + Self { transaction_id: outpoint.transaction_id, index: outpoint.index } + } +} + +impl From for TransactionOutpoint { + fn from(outpoint: RpcTransactionOutpoint) -> Self { + Self { transaction_id: outpoint.transaction_id, index: outpoint.index } + } +} + +impl From for RpcTransactionOutpoint { + fn from(outpoint: kaspa_consensus_client::TransactionOutpoint) -> Self { + TransactionOutpoint::from(outpoint).into() + } +} + +impl From for kaspa_consensus_client::TransactionOutpoint { + fn from(outpoint: RpcTransactionOutpoint) -> Self { + TransactionOutpoint::from(outpoint).into() + } +} + +impl Serializer for RpcTransactionOutpoint { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(TransactionId, &self.transaction_id, writer)?; + store!(TransactionIndexType, &self.index, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionOutpoint { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let transaction_id = load!(TransactionId, reader)?; + let index = load!(TransactionIndexType, reader)?; + + Ok(Self { transaction_id, index }) + } +} /// Represents a Kaspa transaction input -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionInput { pub previous_outpoint: RpcTransactionOutpoint, @@ -32,7 +145,7 @@ pub struct RpcTransactionInput { impl From for RpcTransactionInput { fn from(input: TransactionInput) -> Self { Self { - previous_outpoint: input.previous_outpoint, + previous_outpoint: input.previous_outpoint.into(), signature_script: input.signature_script, sequence: input.sequence, sig_op_count: input.sig_op_count, @@ -47,13 +160,53 @@ impl RpcTransactionInput { } } +impl Serializer for RpcTransactionInput { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + serialize!(RpcTransactionOutpoint, &self.previous_outpoint, writer)?; + store!(Vec, &self.signature_script, writer)?; + store!(u64, &self.sequence, writer)?; + store!(u8, &self.sig_op_count, writer)?; + serialize!(Option, &self.verbose_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionInput { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let previous_outpoint = deserialize!(RpcTransactionOutpoint, reader)?; + let signature_script = load!(Vec, reader)?; + let sequence = load!(u64, reader)?; + let sig_op_count = load!(u8, reader)?; + let verbose_data = deserialize!(Option, reader)?; + + Ok(Self { previous_outpoint, signature_script, sequence, sig_op_count, verbose_data }) + } +} + /// Represent Kaspa transaction input verbose data -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionInputVerboseData {} +impl Serializer for RpcTransactionInputVerboseData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + Ok(()) + } +} + +impl Deserializer for RpcTransactionInputVerboseData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + Ok(Self {}) + } +} + /// Represents a Kaspad transaction output -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionOutput { pub value: u64, @@ -73,16 +226,58 @@ impl From for RpcTransactionOutput { } } +impl Serializer for RpcTransactionOutput { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(u64, &self.value, writer)?; + store!(RpcScriptPublicKey, &self.script_public_key, writer)?; + serialize!(Option, &self.verbose_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionOutput { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let value = load!(u64, reader)?; + let script_public_key = load!(RpcScriptPublicKey, reader)?; + let verbose_data = deserialize!(Option, reader)?; + + Ok(Self { value, script_public_key, verbose_data }) + } +} + /// Represent Kaspa transaction output verbose data -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionOutputVerboseData { pub script_public_key_type: RpcScriptClass, pub script_public_key_address: Address, } +impl Serializer for RpcTransactionOutputVerboseData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(RpcScriptClass, &self.script_public_key_type, writer)?; + store!(Address, &self.script_public_key_address, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionOutputVerboseData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let script_public_key_type = load!(RpcScriptClass, reader)?; + let script_public_key_address = load!(Address, reader)?; + + Ok(Self { script_public_key_type, script_public_key_address }) + } +} + /// Represents a Kaspa transaction -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransaction { pub version: u16, @@ -97,8 +292,42 @@ pub struct RpcTransaction { pub verbose_data: Option, } +impl Serializer for RpcTransaction { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(u16, &self.version, writer)?; + serialize!(Vec, &self.inputs, writer)?; + serialize!(Vec, &self.outputs, writer)?; + store!(u64, &self.lock_time, writer)?; + store!(RpcSubnetworkId, &self.subnetwork_id, writer)?; + store!(u64, &self.gas, writer)?; + store!(Vec, &self.payload, writer)?; + store!(u64, &self.mass, writer)?; + serialize!(Option, &self.verbose_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransaction { + fn deserialize(reader: &mut R) -> std::io::Result { + let _struct_version = load!(u16, reader)?; + let version = load!(u16, reader)?; + let inputs = deserialize!(Vec, reader)?; + let outputs = deserialize!(Vec, reader)?; + let lock_time = load!(u64, reader)?; + let subnetwork_id = load!(RpcSubnetworkId, reader)?; + let gas = load!(u64, reader)?; + let payload = load!(Vec, reader)?; + let mass = load!(u64, reader)?; + let verbose_data = deserialize!(Option, reader)?; + + Ok(Self { version, inputs, outputs, lock_time, subnetwork_id, gas, payload, mass, verbose_data }) + } +} + /// Represent Kaspa transaction verbose data -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionVerboseData { pub transaction_id: RpcTransactionId, @@ -108,6 +337,32 @@ pub struct RpcTransactionVerboseData { pub block_time: u64, } +impl Serializer for RpcTransactionVerboseData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(RpcTransactionId, &self.transaction_id, writer)?; + store!(RpcHash, &self.hash, writer)?; + store!(u64, &self.mass, writer)?; + store!(RpcHash, &self.block_hash, writer)?; + store!(u64, &self.block_time, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionVerboseData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let transaction_id = load!(RpcTransactionId, reader)?; + let hash = load!(RpcHash, reader)?; + let mass = load!(u64, reader)?; + let block_hash = load!(RpcHash, reader)?; + let block_time = load!(u64, reader)?; + + Ok(Self { transaction_id, hash, mass, block_hash, block_time }) + } +} + /// Represents accepted transaction ids #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] diff --git a/rpc/core/src/wasm/convert.rs b/rpc/core/src/wasm/convert.rs index 0c33cf0ec3..ddbb88dd72 100644 --- a/rpc/core/src/wasm/convert.rs +++ b/rpc/core/src/wasm/convert.rs @@ -1,12 +1,11 @@ use crate::model::*; use kaspa_consensus_client::*; -use kaspa_consensus_core::tx as cctx; use std::sync::Arc; impl From for UtxoEntry { fn from(entry: RpcUtxosByAddressesEntry) -> UtxoEntry { let RpcUtxosByAddressesEntry { address, outpoint, utxo_entry } = entry; - let cctx::UtxoEntry { amount, script_public_key, block_daa_score, is_coinbase } = utxo_entry; + let RpcUtxoEntry { amount, script_public_key, block_daa_score, is_coinbase } = utxo_entry; UtxoEntry { address, outpoint: outpoint.into(), amount, script_public_key, block_daa_score, is_coinbase } } } @@ -31,7 +30,7 @@ cfg_if::cfg_if! { let inner = tx_input.inner(); RpcTransactionInput { previous_outpoint: inner.previous_outpoint.clone().into(), - signature_script: inner.signature_script.clone(), + signature_script: inner.signature_script.clone().unwrap_or_default(), sequence: inner.sequence, sig_op_count: inner.sig_op_count, verbose_data: None, diff --git a/rpc/core/src/wasm/message.rs b/rpc/core/src/wasm/message.rs index 56af183dbf..639ba22aed 100644 --- a/rpc/core/src/wasm/message.rs +++ b/rpc/core/src/wasm/message.rs @@ -1,5 +1,4 @@ #![allow(non_snake_case)] - use crate::error::RpcError as Error; use crate::error::RpcResult as Result; use crate::model::*; @@ -7,6 +6,7 @@ use kaspa_addresses::Address; use kaspa_addresses::AddressOrStringArrayT; use kaspa_consensus_client::Transaction; use kaspa_consensus_client::UtxoEntryReference; +use kaspa_consensus_core::tx as cctx; use kaspa_rpc_macros::declare_typescript_wasm_interface as declare; pub use serde_wasm_bindgen::from_value; use wasm_bindgen::prelude::*; @@ -317,6 +317,38 @@ try_from! ( args: GetMetricsResponse, IGetMetricsResponse, { // --- +declare! { + IGetConnectionsRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetConnectionsRequest { } + "#, +} + +try_from! ( args: IGetConnectionsRequest, GetConnectionsRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetConnectionsResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetConnectionsResponse { + [key: string]: any + } + "#, +} + +try_from! ( args: GetConnectionsResponse, IGetConnectionsResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + declare! { IGetSinkRequest, r#" @@ -788,7 +820,7 @@ declare! { } try_from! ( args: IGetBlockTemplateRequest, GetBlockTemplateRequest, { - let pay_address = args.get_cast::
("payAddress")?.into_owned(); + let pay_address = args.cast_into::
("payAddress")?; let extra_data = if let Some(extra_data) = args.try_get_value("extraData")? { if let Some(text) = extra_data.as_string() { text.into_bytes() @@ -1129,7 +1161,7 @@ declare! { * @category Node RPC */ export interface IGetUtxosByAddressesResponse { - entries : IUtxoEntry[]; + entries : UtxoEntryReference[]; } "#, } @@ -1292,6 +1324,66 @@ try_from! ( args: SubmitBlockResponse, ISubmitBlockResponse, { // --- +declare! { + ISubmitTransactionReplacementRequest, + // "ISubmitTransactionRequest | Transaction", + r#" + /** + * Submit transaction replacement to the node. + * + * @category Node RPC + */ + export interface ISubmitTransactionReplacementRequest { + transaction : Transaction, + } + "#, +} + +try_from! ( args: ISubmitTransactionReplacementRequest, SubmitTransactionReplacementRequest, { + let transaction = if let Some(transaction) = args.try_get_value("transaction")? { + transaction + } else { + args.into() + }; + + let request = if let Ok(transaction) = Transaction::try_owned_from(&transaction) { + SubmitTransactionReplacementRequest { + transaction : transaction.into(), + } + } else { + from_value(transaction)? + }; + Ok(request) +}); + +declare! { + ISubmitTransactionReplacementResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface ISubmitTransactionReplacementResponse { + transactionId : HexString; + replacedTransaction: Transaction; + } + "#, +} + +try_from! ( args: SubmitTransactionReplacementResponse, ISubmitTransactionReplacementResponse, { + let transaction_id = args.transaction_id; + let replaced_transaction = cctx::Transaction::try_from(args.replaced_transaction)?; + let replaced_transaction = Transaction::from(replaced_transaction); + + let response = ISubmitTransactionReplacementResponse::default(); + response.set("transactionId", &transaction_id.into())?; + response.set("replacedTransaction", &replaced_transaction.into())?; + Ok(response) +}); + +// --- + declare! { ISubmitTransactionRequest, // "ISubmitTransactionRequest | Transaction", @@ -1322,7 +1414,11 @@ try_from! ( args: ISubmitTransactionRequest, SubmitTransactionRequest, { allow_orphan, } } else { - from_value(transaction)? + let tx = Transaction::try_cast_from(&transaction)?; + SubmitTransactionRequest { + transaction : tx.as_ref().into(), + allow_orphan, + } }; Ok(request) }); @@ -1383,3 +1479,210 @@ declare! { try_from! ( args: UnbanResponse, IUnbanResponse, { Ok(to_value(&args)?.into()) }); + +// --- + +declare! { + IFeerateBucket, + r#" + /** + * + * + * @category Node RPC + */ + export interface IFeerateBucket { + /** + * The fee/mass ratio estimated to be required for inclusion time <= estimated_seconds + */ + feerate : number; + /** + * The estimated inclusion time for a transaction with fee/mass = feerate + */ + estimatedSeconds : number; + } + "#, +} + +declare! { + IFeeEstimate, + r#" + /** + * + * + * @category Node RPC + */ + export interface IFeeEstimate { + /** + * *Top-priority* feerate bucket. Provides an estimation of the feerate required for sub-second DAG inclusion. + * + * Note: for all buckets, feerate values represent fee/mass of a transaction in `sompi/gram` units. + * Given a feerate value recommendation, calculate the required fee by + * taking the transaction mass and multiplying it by feerate: `fee = feerate * mass(tx)` + */ + + priorityBucket : IFeerateBucket; + /** + * A vector of *normal* priority feerate values. The first value of this vector is guaranteed to exist and + * provide an estimation for sub-*minute* DAG inclusion. All other values will have shorter estimation + * times than all `low_bucket` values. Therefor by chaining `[priority] | normal | low` and interpolating + * between them, one can compose a complete feerate function on the client side. The API makes an effort + * to sample enough "interesting" points on the feerate-to-time curve, so that the interpolation is meaningful. + */ + + normalBucket : IFeerateBucket[]; + /** + * An array of *low* priority feerate values. The first value of this vector is guaranteed to + * exist and provide an estimation for sub-*hour* DAG inclusion. + */ + lowBucket : IFeerateBucket[]; + } + "#, +} + +try_from!( estimate: RpcFeeEstimate, IFeeEstimate, { + + let priority_bucket = IFeerateBucket::default(); + priority_bucket.set("feerate", &estimate.priority_bucket.feerate.into())?; + priority_bucket.set("estimatedSeconds", &estimate.priority_bucket.estimated_seconds.into())?; + + let normal_buckets = estimate.normal_buckets.into_iter().map(|normal_bucket| { + let bucket = IFeerateBucket::default(); + bucket.set("feerate", &normal_bucket.feerate.into())?; + bucket.set("estimatedSeconds", &normal_bucket.estimated_seconds.into())?; + Ok(bucket) + }).collect::>>()?; + + let low_buckets = estimate.low_buckets.into_iter().map(|low_bucket| { + let bucket = IFeerateBucket::default(); + bucket.set("feerate", &low_bucket.feerate.into())?; + bucket.set("estimatedSeconds", &low_bucket.estimated_seconds.into())?; + Ok(bucket) + }).collect::>>()?; + + let estimate = IFeeEstimate::default(); + estimate.set("priorityBucket", &priority_bucket)?; + estimate.set("normalBuckets", &js_sys::Array::from_iter(normal_buckets))?; + estimate.set("lowBuckets", &js_sys::Array::from_iter(low_buckets))?; + + Ok(estimate) +}); + +// --- + +declare! { + IGetFeeEstimateRequest, + r#" + /** + * Get fee estimate from the node. + * + * @category Node RPC + */ + export interface IGetFeeEstimateRequest { } + "#, +} + +try_from! ( args: IGetFeeEstimateRequest, GetFeeEstimateRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetFeeEstimateResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetFeeEstimateResponse { + estimate : IFeeEstimate; + } + "#, +} + +try_from!( args: GetFeeEstimateResponse, IGetFeeEstimateResponse, { + let estimate = IFeeEstimate::try_from(args.estimate)?; + let response = IGetFeeEstimateResponse::default(); + response.set("estimate", &estimate)?; + Ok(response) +}); + +// --- + +declare! { + IFeeEstimateVerboseExperimentalData, + r#" + /** + * + * + * @category Node RPC + */ + export interface IFeeEstimateVerboseExperimentalData { + mempoolReadyTransactionsCount : bigint; + mempoolReadyTransactionsTotalMass : bigint; + networkMassPerSecond : bigint; + nextBlockTemplateFeerateMin : number; + nextBlockTemplateFeerateMedian : number; + nextBlockTemplateFeerateMax : number; + } + "#, +} + +try_from!( data: RpcFeeEstimateVerboseExperimentalData, IFeeEstimateVerboseExperimentalData, { + + let target = IFeeEstimateVerboseExperimentalData::default(); + target.set("mempoolReadyTransactionsCount", &js_sys::BigInt::from(data.mempool_ready_transactions_count).into())?; + target.set("mempoolReadyTransactionsTotalMass", &js_sys::BigInt::from(data.mempool_ready_transactions_total_mass).into())?; + target.set("networkMassPerSecond", &js_sys::BigInt::from(data.network_mass_per_second).into())?; + target.set("nextBlockTemplateFeerateMin", &data.next_block_template_feerate_min.into())?; + target.set("nextBlockTemplateFeerateMedian", &data.next_block_template_feerate_median.into())?; + target.set("nextBlockTemplateFeerateMax", &data.next_block_template_feerate_max.into())?; + + Ok(target) +}); + +declare! { + IGetFeeEstimateExperimentalRequest, + // "ISubmitTransactionRequest | Transaction", + r#" + /** + * Get fee estimate from the node. + * + * @category Node RPC + */ + export interface IGetFeeEstimateExperimentalRequest { } + "#, +} + +try_from! ( args: IGetFeeEstimateExperimentalRequest, GetFeeEstimateExperimentalRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetFeeEstimateExperimentalResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetFeeEstimateExperimentalResponse { + estimate : IFeeEstimate; + verbose? : IFeeEstimateVerboseExperimentalData + } + "#, +} + +try_from!( args: GetFeeEstimateExperimentalResponse, IGetFeeEstimateExperimentalResponse, { + let estimate = IFeeEstimate::try_from(args.estimate)?; + let response = IGetFeeEstimateExperimentalResponse::default(); + response.set("estimate", &estimate)?; + + if let Some(verbose) = args.verbose { + let verbose = IFeeEstimateVerboseExperimentalData::try_from(verbose)?; + response.set("verbose", &verbose)?; + } + + Ok(response) +}); + +// --- diff --git a/rpc/grpc/client/src/lib.rs b/rpc/grpc/client/src/lib.rs index c19a28eb50..872b3105f5 100644 --- a/rpc/grpc/client/src/lib.rs +++ b/rpc/grpc/client/src/lib.rs @@ -241,6 +241,8 @@ impl RpcApi for GrpcClient { route!(get_sync_status_call, GetSyncStatus); route!(get_server_info_call, GetServerInfo); route!(get_metrics_call, GetMetrics); + route!(get_connections_call, GetConnections); + route!(get_system_info_call, GetSystemInfo); route!(submit_block_call, SubmitBlock); route!(get_block_template_call, GetBlockTemplate); route!(get_block_call, GetBlock); diff --git a/rpc/grpc/client/src/route.rs b/rpc/grpc/client/src/route.rs index 5bb1bf3950..bb5b5ce56d 100644 --- a/rpc/grpc/client/src/route.rs +++ b/rpc/grpc/client/src/route.rs @@ -9,12 +9,14 @@ macro_rules! route { clippy::type_repetition_in_bounds, clippy::used_underscore_binding )] - fn $fn<'life0, 'async_trait>( + fn $fn<'life0, 'life1, 'async_trait>( &'life0 self, + _connection : ::core::option::Option<&'life1 Arc>, request: [<$name Request>], ) -> ::core::pin::Pin]>> + ::core::marker::Send + 'async_trait>> where 'life0: 'async_trait, + 'life1: 'async_trait, Self: 'async_trait, { Box::pin(async move { diff --git a/rpc/grpc/core/proto/messages.proto b/rpc/grpc/core/proto/messages.proto index 01075c1836..5380ba6587 100644 --- a/rpc/grpc/core/proto/messages.proto +++ b/rpc/grpc/core/proto/messages.proto @@ -59,7 +59,9 @@ message KaspadRequest { GetServerInfoRequestMessage getServerInfoRequest = 1092; GetSyncStatusRequestMessage getSyncStatusRequest = 1094; GetDaaScoreTimestampEstimateRequestMessage getDaaScoreTimestampEstimateRequest = 1096; - SubmitTransactionReplacementRequestMessage submitTransactionReplacementRequest = 2000; + SubmitTransactionReplacementRequestMessage submitTransactionReplacementRequest = 1100; + GetConnectionsRequestMessage getConnectionsRequest = 1102; + GetSystemInfoRequestMessage getSystemInfoRequest = 1104; GetFeeEstimateRequestMessage getFeeEstimateRequest = 1106; GetFeeEstimateExperimentalRequestMessage getFeeEstimateExperimentalRequest = 1108; } @@ -121,7 +123,9 @@ message KaspadResponse { GetServerInfoResponseMessage getServerInfoResponse = 1093; GetSyncStatusResponseMessage getSyncStatusResponse = 1095; GetDaaScoreTimestampEstimateResponseMessage getDaaScoreTimestampEstimateResponse = 1097; - SubmitTransactionReplacementResponseMessage submitTransactionReplacementResponse = 2001; + SubmitTransactionReplacementResponseMessage submitTransactionReplacementResponse = 1101; + GetConnectionsResponseMessage getConnectionsResponse= 1103; + GetSystemInfoResponseMessage getSystemInfoResponse= 1105; GetFeeEstimateResponseMessage getFeeEstimateResponse = 1107; GetFeeEstimateExperimentalResponseMessage getFeeEstimateExperimentalResponse = 1109; } diff --git a/rpc/grpc/core/proto/rpc.proto b/rpc/grpc/core/proto/rpc.proto index a0bed89771..d5c441927a 100644 --- a/rpc/grpc/core/proto/rpc.proto +++ b/rpc/grpc/core/proto/rpc.proto @@ -821,11 +821,46 @@ message ConsensusMetrics{ uint64 virtualDaaScore = 18; } +message StorageMetrics{ + uint64 storageSizeBytes = 1; +} + +message GetConnectionsRequestMessage{ + bool includeProfileData = 1; +} + +message ConnectionsProfileData { + double cpuUsage = 1; + uint64 memoryUsage = 2; +} + +message GetConnectionsResponseMessage{ + uint32 clients = 1; + uint32 peers = 2; + ConnectionsProfileData profileData = 3; + RPCError error = 1000; +} + +message GetSystemInfoRequestMessage{ +} + +message GetSystemInfoResponseMessage{ + string version = 1; + string systemId = 2; + string gitHash = 3; + uint32 coreNum = 4; + uint64 totalMemory = 5; + uint32 fdLimit = 6; + RPCError error = 1000; +} + message GetMetricsRequestMessage{ bool processMetrics = 1; bool connectionMetrics = 2; bool bandwidthMetrics = 3; bool consensusMetrics = 4; + bool storageMetrics = 5; + bool customMetrics = 6; } message GetMetricsResponseMessage{ @@ -834,6 +869,7 @@ message GetMetricsResponseMessage{ ConnectionMetrics connectionMetrics = 12; BandwidthMetrics bandwidthMetrics = 13; ConsensusMetrics consensusMetrics = 14; + StorageMetrics storageMetrics = 15; RPCError error = 1000; } @@ -841,12 +877,13 @@ message GetServerInfoRequestMessage{ } message GetServerInfoResponseMessage{ - repeated uint32 rpcApiVersion = 1; // Expecting exactly 4 elements - string serverVersion = 2; - string networkId = 3; - bool hasUtxoIndex = 4; - bool isSynced = 5; - uint64 virtualDaaScore = 6; + uint32 rpcApiVersion = 1; + uint32 rpcApiRevision = 2; + string serverVersion = 3; + string networkId = 4; + bool hasUtxoIndex = 5; + bool isSynced = 6; + uint64 virtualDaaScore = 7; RPCError error = 1000; } diff --git a/rpc/grpc/core/src/convert/block.rs b/rpc/grpc/core/src/convert/block.rs index 8429f3256d..6ab9e37fa0 100644 --- a/rpc/grpc/core/src/convert/block.rs +++ b/rpc/grpc/core/src/convert/block.rs @@ -15,6 +15,14 @@ from!(item: &kaspa_rpc_core::RpcBlock, protowire::RpcBlock, { } }); +from!(item: &kaspa_rpc_core::RpcRawBlock, protowire::RpcBlock, { + Self { + header: Some(protowire::RpcBlockHeader::from(&item.header)), + transactions: item.transactions.iter().map(protowire::RpcTransaction::from).collect(), + verbose_data: None, + } +}); + from!(item: &kaspa_rpc_core::RpcBlockVerboseData, protowire::RpcBlockVerboseData, { Self { hash: item.hash.to_string(), @@ -46,6 +54,17 @@ try_from!(item: &protowire::RpcBlock, kaspa_rpc_core::RpcBlock, { } }); +try_from!(item: &protowire::RpcBlock, kaspa_rpc_core::RpcRawBlock, { + Self { + header: item + .header + .as_ref() + .ok_or_else(|| RpcError::MissingRpcFieldError("RpcBlock".to_string(), "header".to_string()))? + .try_into()?, + transactions: item.transactions.iter().map(kaspa_rpc_core::RpcTransaction::try_from).collect::, _>>()?, + } +}); + try_from!(item: &protowire::RpcBlockVerboseData, kaspa_rpc_core::RpcBlockVerboseData, { Self { hash: RpcHash::from_str(&item.hash)?, diff --git a/rpc/grpc/core/src/convert/header.rs b/rpc/grpc/core/src/convert/header.rs index f4d78b7c11..5d763034a7 100644 --- a/rpc/grpc/core/src/convert/header.rs +++ b/rpc/grpc/core/src/convert/header.rs @@ -1,5 +1,6 @@ use crate::protowire; use crate::{from, try_from}; +use kaspa_consensus_core::header::Header; use kaspa_rpc_core::{FromRpcHex, RpcError, RpcHash, RpcResult, ToRpcHex}; use std::str::FromStr; @@ -24,6 +25,23 @@ from!(item: &kaspa_rpc_core::RpcHeader, protowire::RpcBlockHeader, { } }); +from!(item: &kaspa_rpc_core::RpcRawHeader, protowire::RpcBlockHeader, { + Self { + version: item.version.into(), + parents: item.parents_by_level.iter().map(protowire::RpcBlockLevelParents::from).collect(), + hash_merkle_root: item.hash_merkle_root.to_string(), + accepted_id_merkle_root: item.accepted_id_merkle_root.to_string(), + utxo_commitment: item.utxo_commitment.to_string(), + timestamp: item.timestamp.try_into().expect("timestamp is always convertible to i64"), + bits: item.bits, + nonce: item.nonce, + daa_score: item.daa_score, + blue_work: item.blue_work.to_rpc_hex(), + blue_score: item.blue_score, + pruning_point: item.pruning_point.to_string(), + } +}); + from!(item: &Vec, protowire::RpcBlockLevelParents, { Self { parent_hashes: item.iter().map(|x| x.to_string()).collect() } }); // ---------------------------------------------------------------------------- @@ -32,7 +50,7 @@ from!(item: &Vec, protowire::RpcBlockLevelParents, { Self { parent_hash try_from!(item: &protowire::RpcBlockHeader, kaspa_rpc_core::RpcHeader, { // We re-hash the block to remain as most trustless as possible - Self::new_finalized( + let header = Header::new_finalized( item.version.try_into()?, item.parents.iter().map(Vec::::try_from).collect::>>>()?, RpcHash::from_str(&item.hash_merkle_root)?, @@ -45,7 +63,26 @@ try_from!(item: &protowire::RpcBlockHeader, kaspa_rpc_core::RpcHeader, { kaspa_rpc_core::RpcBlueWorkType::from_rpc_hex(&item.blue_work)?, item.blue_score, RpcHash::from_str(&item.pruning_point)?, - ) + ); + + header.into() +}); + +try_from!(item: &protowire::RpcBlockHeader, kaspa_rpc_core::RpcRawHeader, { + Self { + version: item.version.try_into()?, + parents_by_level: item.parents.iter().map(Vec::::try_from).collect::>>>()?, + hash_merkle_root: RpcHash::from_str(&item.hash_merkle_root)?, + accepted_id_merkle_root: RpcHash::from_str(&item.accepted_id_merkle_root)?, + utxo_commitment: RpcHash::from_str(&item.utxo_commitment)?, + timestamp: item.timestamp.try_into()?, + bits: item.bits, + nonce: item.nonce, + daa_score: item.daa_score, + blue_work: kaspa_rpc_core::RpcBlueWorkType::from_rpc_hex(&item.blue_work)?, + blue_score: item.blue_score, + pruning_point: RpcHash::from_str(&item.pruning_point)?, + } }); try_from!(item: &protowire::RpcBlockLevelParents, Vec, { @@ -55,7 +92,8 @@ try_from!(item: &protowire::RpcBlockLevelParents, Vec, { #[cfg(test)] mod tests { use crate::protowire; - use kaspa_rpc_core::{RpcHash, RpcHeader}; + use kaspa_consensus_core::{block::Block, header::Header}; + use kaspa_rpc_core::{RpcBlock, RpcHash, RpcHeader}; fn new_unique() -> RpcHash { use std::sync::atomic::{AtomicU64, Ordering}; @@ -106,7 +144,7 @@ mod tests { #[test] fn test_rpc_header() { - let r = RpcHeader::new_finalized( + let r = Header::new_finalized( 0, vec![vec![new_unique(), new_unique(), new_unique()], vec![new_unique()], vec![new_unique(), new_unique()]], new_unique(), @@ -120,6 +158,7 @@ mod tests { 1928374, new_unique(), ); + let r = RpcHeader::from(r); let p: protowire::RpcBlockHeader = (&r).into(); let r2: RpcHeader = (&p).try_into().unwrap(); let p2: protowire::RpcBlockHeader = (&r2).into(); @@ -134,4 +173,42 @@ mod tests { assert_eq!(r.hash, r2.hash); assert_eq!(p, p2); } + + #[test] + fn test_rpc_block() { + let h = Header::new_finalized( + 0, + vec![vec![new_unique(), new_unique(), new_unique()], vec![new_unique()], vec![new_unique(), new_unique()]], + new_unique(), + new_unique(), + new_unique(), + 123, + 12345, + 98765, + 120055, + 459912.into(), + 1928374, + new_unique(), + ); + let b = Block::from_header(h); + let r: RpcBlock = (&b).into(); + let p: protowire::RpcBlock = (&r).into(); + let r2: RpcBlock = (&p).try_into().unwrap(); + let b2: Block = r2.clone().try_into().unwrap(); + let r3: RpcBlock = (&b2).into(); + let p2: protowire::RpcBlock = (&r3).into(); + + assert_eq!(r.header.parents_by_level, r2.header.parents_by_level); + assert_eq!(p.header.as_ref().unwrap().parents, p2.header.as_ref().unwrap().parents); + test_parents_by_level_rxr(&r.header.parents_by_level, &r2.header.parents_by_level); + test_parents_by_level_rxr(&r.header.parents_by_level, &r3.header.parents_by_level); + test_parents_by_level_rxr(&b.header.parents_by_level, &r2.header.parents_by_level); + test_parents_by_level_rxr(&b.header.parents_by_level, &b2.header.parents_by_level); + test_parents_by_level_rxp(&r.header.parents_by_level, &p.header.as_ref().unwrap().parents); + test_parents_by_level_rxp(&r.header.parents_by_level, &p2.header.as_ref().unwrap().parents); + test_parents_by_level_rxp(&r2.header.parents_by_level, &p2.header.as_ref().unwrap().parents); + + assert_eq!(b.hash(), b2.hash()); + assert_eq!(p, p2); + } } diff --git a/rpc/grpc/core/src/convert/kaspad.rs b/rpc/grpc/core/src/convert/kaspad.rs index d43442fe3b..9296a944ec 100644 --- a/rpc/grpc/core/src/convert/kaspad.rs +++ b/rpc/grpc/core/src/convert/kaspad.rs @@ -55,6 +55,8 @@ pub mod kaspad_request_convert { impl_into_kaspad_request!(GetCoinSupply); impl_into_kaspad_request!(Ping); impl_into_kaspad_request!(GetMetrics); + impl_into_kaspad_request!(GetConnections); + impl_into_kaspad_request!(GetSystemInfo); impl_into_kaspad_request!(GetServerInfo); impl_into_kaspad_request!(GetSyncStatus); impl_into_kaspad_request!(GetDaaScoreTimestampEstimate); @@ -189,6 +191,8 @@ pub mod kaspad_response_convert { impl_into_kaspad_response!(GetCoinSupply); impl_into_kaspad_response!(Ping); impl_into_kaspad_response!(GetMetrics); + impl_into_kaspad_response!(GetConnections); + impl_into_kaspad_response!(GetSystemInfo); impl_into_kaspad_response!(GetServerInfo); impl_into_kaspad_response!(GetSyncStatus); impl_into_kaspad_response!(GetDaaScoreTimestampEstimate); diff --git a/rpc/grpc/core/src/convert/message.rs b/rpc/grpc/core/src/convert/message.rs index 75606dc9ac..5a0e946103 100644 --- a/rpc/grpc/core/src/convert/message.rs +++ b/rpc/grpc/core/src/convert/message.rs @@ -26,6 +26,7 @@ use kaspa_rpc_core::{ RpcContextualPeerAddress, RpcError, RpcExtraData, RpcHash, RpcIpAddress, RpcNetworkType, RpcPeerAddress, RpcResult, SubmitBlockRejectReason, SubmitBlockReport, }; +use kaspa_utils::hex::*; use std::str::FromStr; macro_rules! from { @@ -429,6 +430,8 @@ from!(item: &kaspa_rpc_core::GetMetricsRequest, protowire::GetMetricsRequestMess connection_metrics: item.connection_metrics, bandwidth_metrics: item.bandwidth_metrics, consensus_metrics: item.consensus_metrics, + storage_metrics: item.storage_metrics, + custom_metrics: item.custom_metrics, } }); from!(item: RpcResult<&kaspa_rpc_core::GetMetricsResponse>, protowire::GetMetricsResponseMessage, { @@ -438,13 +441,45 @@ from!(item: RpcResult<&kaspa_rpc_core::GetMetricsResponse>, protowire::GetMetric connection_metrics: item.connection_metrics.as_ref().map(|x| x.into()), bandwidth_metrics: item.bandwidth_metrics.as_ref().map(|x| x.into()), consensus_metrics: item.consensus_metrics.as_ref().map(|x| x.into()), + storage_metrics: item.storage_metrics.as_ref().map(|x| x.into()), + // TODO + // custom_metrics : None, error: None, } }); + +from!(item: &kaspa_rpc_core::GetConnectionsRequest, protowire::GetConnectionsRequestMessage, { + Self { + include_profile_data : item.include_profile_data, + } +}); +from!(item: RpcResult<&kaspa_rpc_core::GetConnectionsResponse>, protowire::GetConnectionsResponseMessage, { + Self { + clients: item.clients, + peers: item.peers as u32, + profile_data: item.profile_data.as_ref().map(|x| x.into()), + error: None, + } +}); + +from!(&kaspa_rpc_core::GetSystemInfoRequest, protowire::GetSystemInfoRequestMessage); +from!(item: RpcResult<&kaspa_rpc_core::GetSystemInfoResponse>, protowire::GetSystemInfoResponseMessage, { + Self { + version : item.version.clone(), + system_id : item.system_id.as_ref().map(|system_id|system_id.to_hex()).unwrap_or_default(), + git_hash : item.git_hash.as_ref().map(|git_hash|git_hash.to_hex()).unwrap_or_default(), + total_memory : item.total_memory, + core_num : item.cpu_physical_cores as u32, + fd_limit : item.fd_limit, + error: None, + } +}); + from!(&kaspa_rpc_core::GetServerInfoRequest, protowire::GetServerInfoRequestMessage); from!(item: RpcResult<&kaspa_rpc_core::GetServerInfoResponse>, protowire::GetServerInfoResponseMessage, { Self { - rpc_api_version: item.rpc_api_version.iter().map(|x| *x as u32).collect(), + rpc_api_version: item.rpc_api_version as u32, + rpc_api_revision: item.rpc_api_revision as u32, server_version: item.server_version.clone(), network_id: item.network_id.to_string(), has_utxo_index: item.has_utxo_index, @@ -865,7 +900,14 @@ try_from!(&protowire::PingRequestMessage, kaspa_rpc_core::PingRequest); try_from!(&protowire::PingResponseMessage, RpcResult); try_from!(item: &protowire::GetMetricsRequestMessage, kaspa_rpc_core::GetMetricsRequest, { - Self { process_metrics: item.process_metrics, connection_metrics: item.connection_metrics, bandwidth_metrics:item.bandwidth_metrics, consensus_metrics: item.consensus_metrics } + Self { + process_metrics: item.process_metrics, + connection_metrics: item.connection_metrics, + bandwidth_metrics:item.bandwidth_metrics, + consensus_metrics: item.consensus_metrics, + storage_metrics: item.storage_metrics, + custom_metrics : item.custom_metrics, + } }); try_from!(item: &protowire::GetMetricsResponseMessage, RpcResult, { Self { @@ -874,13 +916,40 @@ try_from!(item: &protowire::GetMetricsResponseMessage, RpcResult, { + Self { + clients: item.clients, + peers: item.peers as u16, + profile_data: item.profile_data.as_ref().map(|x| x.try_into()).transpose()?, + } +}); + +try_from!(&protowire::GetSystemInfoRequestMessage, kaspa_rpc_core::GetSystemInfoRequest); +try_from!(item: &protowire::GetSystemInfoResponseMessage, RpcResult, { + Self { + version: item.version.clone(), + system_id: (!item.system_id.is_empty()).then(|| FromHex::from_hex(&item.system_id)).transpose()?, + git_hash: (!item.git_hash.is_empty()).then(|| FromHex::from_hex(&item.git_hash)).transpose()?, + total_memory: item.total_memory, + cpu_physical_cores: item.core_num as u16, + fd_limit: item.fd_limit, } }); try_from!(&protowire::GetServerInfoRequestMessage, kaspa_rpc_core::GetServerInfoRequest); try_from!(item: &protowire::GetServerInfoResponseMessage, RpcResult, { Self { - rpc_api_version: item.rpc_api_version.iter().map(|x| *x as u16).collect::>().as_slice().try_into().map_err(|_| RpcError::RpcApiVersionFormatError)?, + rpc_api_version: item.rpc_api_version as u16, + rpc_api_revision: item.rpc_api_revision as u16, server_version: item.server_version.clone(), network_id: NetworkId::from_str(&item.network_id)?, has_utxo_index: item.has_utxo_index, diff --git a/rpc/grpc/core/src/convert/metrics.rs b/rpc/grpc/core/src/convert/metrics.rs index 8e0e48c045..5037b370f4 100644 --- a/rpc/grpc/core/src/convert/metrics.rs +++ b/rpc/grpc/core/src/convert/metrics.rs @@ -6,6 +6,14 @@ use kaspa_rpc_core::RpcError; // rpc_core to protowire // ---------------------------------------------------------------------------- +from!(item: &kaspa_rpc_core::ConnectionsProfileData, protowire::ConnectionsProfileData, { + Self { + cpu_usage: item.cpu_usage as f64, + memory_usage: item.memory_usage, + + } +}); + from!(item: &kaspa_rpc_core::ProcessMetrics, protowire::ProcessMetrics, { Self { resident_set_size: item.resident_set_size, @@ -66,10 +74,20 @@ from!(item: &kaspa_rpc_core::ConsensusMetrics, protowire::ConsensusMetrics, { } }); +from!(item: &kaspa_rpc_core::StorageMetrics, protowire::StorageMetrics, { + Self { + storage_size_bytes: item.storage_size_bytes, + } +}); + // ---------------------------------------------------------------------------- // protowire to rpc_core // ---------------------------------------------------------------------------- +try_from!(item: &protowire::ConnectionsProfileData, kaspa_rpc_core::ConnectionsProfileData, { + Self { cpu_usage : item.cpu_usage as f32, memory_usage : item.memory_usage } +}); + try_from!(item: &protowire::ProcessMetrics, kaspa_rpc_core::ProcessMetrics, { Self { resident_set_size: item.resident_set_size, @@ -129,3 +147,9 @@ try_from!(item: &protowire::ConsensusMetrics, kaspa_rpc_core::ConsensusMetrics, network_virtual_daa_score: item.virtual_daa_score, } }); + +try_from!(item: &protowire::StorageMetrics, kaspa_rpc_core::StorageMetrics, { + Self { + storage_size_bytes: item.storage_size_bytes, + } +}); diff --git a/rpc/grpc/core/src/ops.rs b/rpc/grpc/core/src/ops.rs index 605d27efde..3ae1f0a2d0 100644 --- a/rpc/grpc/core/src/ops.rs +++ b/rpc/grpc/core/src/ops.rs @@ -79,6 +79,8 @@ pub enum KaspadPayloadOps { GetCoinSupply, Ping, GetMetrics, + GetConnections, + GetSystemInfo, GetServerInfo, GetSyncStatus, GetDaaScoreTimestampEstimate, diff --git a/rpc/grpc/server/src/request_handler/factory.rs b/rpc/grpc/server/src/request_handler/factory.rs index a70fb629fb..aa03be0b50 100644 --- a/rpc/grpc/server/src/request_handler/factory.rs +++ b/rpc/grpc/server/src/request_handler/factory.rs @@ -73,6 +73,8 @@ impl Factory { GetCoinSupply, Ping, GetMetrics, + GetConnections, + GetSystemInfo, GetServerInfo, GetSyncStatus, GetDaaScoreTimestampEstimate, diff --git a/rpc/grpc/server/src/tests/rpc_core_mock.rs b/rpc/grpc/server/src/tests/rpc_core_mock.rs index 2f4afa9c9f..12d8fc949a 100644 --- a/rpc/grpc/server/src/tests/rpc_core_mock.rs +++ b/rpc/grpc/server/src/tests/rpc_core_mock.rs @@ -6,7 +6,7 @@ use kaspa_notify::notifier::{Notifier, Notify}; use kaspa_notify::scope::Scope; use kaspa_notify::subscription::context::SubscriptionContext; use kaspa_notify::subscription::{MutationPolicies, UtxosChangedMutationPolicy}; -use kaspa_rpc_core::{api::rpc::RpcApi, *}; +use kaspa_rpc_core::{api::connection::DynRpcConnection, api::rpc::RpcApi, *}; use kaspa_rpc_core::{notify::connection::ChannelConnection, RpcResult}; use std::sync::Arc; @@ -66,7 +66,7 @@ impl RpcCoreMock { #[async_trait] impl RpcApi for RpcCoreMock { // This fn needs to succeed while the client connects - async fn get_info_call(&self, _request: GetInfoRequest) -> RpcResult { + async fn get_info_call(&self, _connection: Option<&DynRpcConnection>, _request: GetInfoRequest) -> RpcResult { Ok(GetInfoResponse { p2p_id: "p2p-mock".to_string(), mempool_size: 1234, @@ -78,140 +78,237 @@ impl RpcApi for RpcCoreMock { }) } - async fn ping_call(&self, _request: PingRequest) -> RpcResult { + async fn ping_call(&self, _connection: Option<&DynRpcConnection>, _request: PingRequest) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_metrics_call(&self, _request: GetMetricsRequest) -> RpcResult { + async fn get_metrics_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetMetricsRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_server_info_call(&self, _request: GetServerInfoRequest) -> RpcResult { + async fn get_connections_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetConnectionsRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_sync_status_call(&self, _request: GetSyncStatusRequest) -> RpcResult { + async fn get_system_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetSystemInfoRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_current_network_call(&self, _request: GetCurrentNetworkRequest) -> RpcResult { + async fn get_server_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetServerInfoRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn submit_block_call(&self, _request: SubmitBlockRequest) -> RpcResult { + async fn get_sync_status_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetSyncStatusRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_block_template_call(&self, _request: GetBlockTemplateRequest) -> RpcResult { + async fn get_current_network_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetCurrentNetworkRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_peer_addresses_call(&self, _request: GetPeerAddressesRequest) -> RpcResult { + async fn submit_block_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: SubmitBlockRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_sink_call(&self, _request: GetSinkRequest) -> RpcResult { + async fn get_block_template_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetBlockTemplateRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_mempool_entry_call(&self, _request: GetMempoolEntryRequest) -> RpcResult { + async fn get_peer_addresses_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetPeerAddressesRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_mempool_entries_call(&self, _request: GetMempoolEntriesRequest) -> RpcResult { + async fn get_sink_call(&self, _connection: Option<&DynRpcConnection>, _request: GetSinkRequest) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_connected_peer_info_call(&self, _request: GetConnectedPeerInfoRequest) -> RpcResult { + async fn get_mempool_entry_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetMempoolEntryRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn add_peer_call(&self, _request: AddPeerRequest) -> RpcResult { + async fn get_mempool_entries_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetMempoolEntriesRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn submit_transaction_call(&self, _request: SubmitTransactionRequest) -> RpcResult { + async fn get_connected_peer_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetConnectedPeerInfoRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn submit_transaction_replacement_call( &self, + _connection: Option<&DynRpcConnection>, _request: SubmitTransactionReplacementRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_block_call(&self, _request: GetBlockRequest) -> RpcResult { + async fn add_peer_call(&self, _connection: Option<&DynRpcConnection>, _request: AddPeerRequest) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_subnetwork_call(&self, _request: GetSubnetworkRequest) -> RpcResult { + async fn submit_transaction_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: SubmitTransactionRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_block_call(&self, _connection: Option<&DynRpcConnection>, _request: GetBlockRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_subnetwork_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetSubnetworkRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn get_virtual_chain_from_block_call( &self, + _connection: Option<&DynRpcConnection>, _request: GetVirtualChainFromBlockRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_blocks_call(&self, _request: GetBlocksRequest) -> RpcResult { + async fn get_blocks_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetBlocksRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_block_count_call(&self, _request: GetBlockCountRequest) -> RpcResult { + async fn get_block_count_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetBlockCountRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_block_dag_info_call(&self, _request: GetBlockDagInfoRequest) -> RpcResult { + async fn get_block_dag_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetBlockDagInfoRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn resolve_finality_conflict_call( &self, + _connection: Option<&DynRpcConnection>, _request: ResolveFinalityConflictRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn shutdown_call(&self, _request: ShutdownRequest) -> RpcResult { + async fn shutdown_call(&self, _connection: Option<&DynRpcConnection>, _request: ShutdownRequest) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_headers_call(&self, _request: GetHeadersRequest) -> RpcResult { + async fn get_headers_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetHeadersRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_balance_by_address_call(&self, _request: GetBalanceByAddressRequest) -> RpcResult { + async fn get_balance_by_address_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetBalanceByAddressRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn get_balances_by_addresses_call( &self, + _connection: Option<&DynRpcConnection>, _request: GetBalancesByAddressesRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_utxos_by_addresses_call(&self, _request: GetUtxosByAddressesRequest) -> RpcResult { + async fn get_utxos_by_addresses_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetUtxosByAddressesRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_sink_blue_score_call(&self, _request: GetSinkBlueScoreRequest) -> RpcResult { + async fn get_sink_blue_score_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetSinkBlueScoreRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn ban_call(&self, _request: BanRequest) -> RpcResult { + async fn ban_call(&self, _connection: Option<&DynRpcConnection>, _request: BanRequest) -> RpcResult { Err(RpcError::NotImplemented) } - async fn unban_call(&self, _request: UnbanRequest) -> RpcResult { + async fn unban_call(&self, _connection: Option<&DynRpcConnection>, _request: UnbanRequest) -> RpcResult { Err(RpcError::NotImplemented) } async fn estimate_network_hashes_per_second_call( &self, + _connection: Option<&DynRpcConnection>, _request: EstimateNetworkHashesPerSecondRequest, ) -> RpcResult { Err(RpcError::NotImplemented) @@ -219,28 +316,39 @@ impl RpcApi for RpcCoreMock { async fn get_mempool_entries_by_addresses_call( &self, + _connection: Option<&DynRpcConnection>, _request: GetMempoolEntriesByAddressesRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_coin_supply_call(&self, _request: GetCoinSupplyRequest) -> RpcResult { + async fn get_coin_supply_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetCoinSupplyRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn get_daa_score_timestamp_estimate_call( &self, + _connection: Option<&DynRpcConnection>, _request: GetDaaScoreTimestampEstimateRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_fee_estimate_call(&self, _request: GetFeeEstimateRequest) -> RpcResult { + async fn get_fee_estimate_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetFeeEstimateRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn get_fee_estimate_experimental_call( &self, + _connection: Option<&DynRpcConnection>, _request: GetFeeEstimateExperimentalRequest, ) -> RpcResult { Err(RpcError::NotImplemented) diff --git a/rpc/macros/src/grpc/server.rs b/rpc/macros/src/grpc/server.rs index 91dea1dd96..9378117195 100644 --- a/rpc/macros/src/grpc/server.rs +++ b/rpc/macros/src/grpc/server.rs @@ -72,7 +72,8 @@ impl ToTokens for RpcTable { Box::pin(async move { let mut response: #kaspad_response_type = match request.payload { Some(Payload::#request_type(ref request)) => match request.try_into() { - Ok(request) => server_ctx.core_service.#fn_call(request).await.into(), + // TODO: RPC-CONNECTION + Ok(request) => server_ctx.core_service.#fn_call(None,request).await.into(), Err(err) => #response_message_type::from(err).into(), }, _ => { @@ -128,7 +129,7 @@ impl ToTokens for RpcTable { { let mut interface = Interface::new(#server_ctx); - for op in #payload_ops::list() { + for op in #payload_ops::iter() { match op { #(#targets)* } diff --git a/rpc/macros/src/lib.rs b/rpc/macros/src/lib.rs index 1c205c26ef..9ca49bf54a 100644 --- a/rpc/macros/src/lib.rs +++ b/rpc/macros/src/lib.rs @@ -39,3 +39,9 @@ pub fn build_wrpc_wasm_bindgen_subscriptions(input: TokenStream) -> TokenStream pub fn build_grpc_server_interface(input: TokenStream) -> TokenStream { grpc::server::build_grpc_server_interface(input) } + +#[proc_macro] +#[proc_macro_error] +pub fn test_wrpc_serializer(input: TokenStream) -> TokenStream { + wrpc::test::build_test(input) +} diff --git a/rpc/macros/src/wrpc/client.rs b/rpc/macros/src/wrpc/client.rs index f33fe57f31..12f41687a7 100644 --- a/rpc/macros/src/wrpc/client.rs +++ b/rpc/macros/src/wrpc/client.rs @@ -52,26 +52,29 @@ impl ToTokens for RpcTable { // the async implementation of the RPC caller is inlined targets.push(quote! { - fn #fn_call<'life0, 'async_trait>( + fn #fn_call<'life0, 'life1, 'async_trait>( &'life0 self, + _connection : ::core::option::Option<&'life1 Arc>, request: #request_type, ) -> ::core::pin::Pin> + ::core::marker::Send + 'async_trait>> where 'life0: 'async_trait, + 'life1: 'async_trait, Self: 'async_trait, { + use workflow_serializer::prelude::*; Box::pin(async move { if let ::core::option::Option::Some(__ret) = ::core::option::Option::None::> { return __ret; } let __self = self; //let request = request; - let __ret: RpcResult<#response_type> = { - let resp: ClientResult<#response_type> = __self.inner.rpc_client.call(#rpc_api_ops::#handler, request).await; + let __ret: RpcResult> = { + let resp: ClientResult> = __self.inner.rpc_client.call(#rpc_api_ops::#handler, Serializable(request)).await; Ok(resp.map_err(|e| kaspa_rpc_core::error::RpcError::RpcSubsystem(e.to_string()))?) }; #[allow(unreachable_code)] - __ret + __ret.map(Serializable::into_inner) }) } diff --git a/rpc/macros/src/wrpc/mod.rs b/rpc/macros/src/wrpc/mod.rs index 1a15b06646..8c8238cdb8 100644 --- a/rpc/macros/src/wrpc/mod.rs +++ b/rpc/macros/src/wrpc/mod.rs @@ -1,3 +1,4 @@ pub mod client; pub mod server; +pub mod test; pub mod wasm; diff --git a/rpc/macros/src/wrpc/server.rs b/rpc/macros/src/wrpc/server.rs index 09a3e0c692..092b1edb32 100644 --- a/rpc/macros/src/wrpc/server.rs +++ b/rpc/macros/src/wrpc/server.rs @@ -50,13 +50,14 @@ impl ToTokens for RpcTable { targets.push(quote! { #rpc_api_ops::#handler => { - interface.method(#rpc_api_ops::#handler, method!(|server_ctx: #server_ctx_type, connection_ctx: #connection_ctx_type, request: #request_type| async move { + interface.method(#rpc_api_ops::#handler, method!(|server_ctx: #server_ctx_type, connection_ctx: #connection_ctx_type, request: Serializable<#request_type>| async move { let verbose = server_ctx.verbose(); if verbose { workflow_log::log_info!("request: {:?}",request); } - let response: #response_type = server_ctx.rpc_service(&connection_ctx).#fn_call(request).await + // TODO: RPC-CONNECT + let response: #response_type = server_ctx.rpc_service(&connection_ctx).#fn_call(None, request.into_inner()).await .map_err(|e|ServerError::Text(e.to_string()))?; if verbose { workflow_log::log_info!("response: {:?}",response); } - Ok(response) + Ok(Serializable(response)) })); } }); @@ -71,7 +72,8 @@ impl ToTokens for RpcTable { #rpc_api_ops >::new(#server_ctx); - for op in #rpc_api_ops::list() { + for op in #rpc_api_ops::iter() { + use workflow_serializer::prelude::*; match op { #(#targets)* _ => { } diff --git a/rpc/macros/src/wrpc/test.rs b/rpc/macros/src/wrpc/test.rs new file mode 100644 index 0000000000..92591b22b0 --- /dev/null +++ b/rpc/macros/src/wrpc/test.rs @@ -0,0 +1,60 @@ +use convert_case::{Case, Casing}; +use proc_macro2::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::{quote, ToTokens}; +use std::convert::Into; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, + Error, Expr, Result, Token, +}; + +#[derive(Debug)] +struct TestTable { + rpc_op: Expr, +} + +impl Parse for TestTable { + fn parse(input: ParseStream) -> Result { + let parsed = Punctuated::::parse_terminated(input).unwrap(); + if parsed.len() != 1 { + return Err(Error::new_spanned(parsed, "usage: test!(GetInfo)".to_string())); + } + + let mut iter = parsed.iter(); + let rpc_op = iter.next().unwrap().clone(); + + Ok(TestTable { rpc_op }) + } +} + +impl ToTokens for TestTable { + fn to_tokens(&self, tokens: &mut TokenStream) { + let rpc_op = &self.rpc_op; + + let (name, _docs) = match rpc_op { + syn::Expr::Path(expr_path) => (expr_path.path.to_token_stream().to_string(), expr_path.attrs.clone()), + _ => (rpc_op.to_token_stream().to_string(), vec![]), + }; + let typename = Ident::new(&name.to_string(), Span::call_site()); + let fn_test = Ident::new(&format!("test_wrpc_serializer_{}", name.to_case(Case::Snake)), Span::call_site()); + + quote! { + + #[test] + fn #fn_test() { + test::<#typename>(#name); + } + + } + .to_tokens(tokens); + } +} + +pub fn build_test(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let rpc_table = parse_macro_input!(input as TestTable); + let ts = rpc_table.to_token_stream(); + // println!("MACRO: {}", ts.to_string()); + ts.into() +} diff --git a/rpc/macros/src/wrpc/wasm.rs b/rpc/macros/src/wrpc/wasm.rs index 0118220199..30af3e74a3 100644 --- a/rpc/macros/src/wrpc/wasm.rs +++ b/rpc/macros/src/wrpc/wasm.rs @@ -59,7 +59,7 @@ impl ToTokens for RpcHandlers { pub async fn #fn_no_suffix(&self, request : Option<#ts_request_type>) -> Result<#ts_response_type> { let request: #request_type = request.unwrap_or_default().try_into()?; // log_info!("request: {:#?}",request); - let result: RpcResult<#response_type> = self.inner.client.#fn_call(request).await; + let result: RpcResult<#response_type> = self.inner.client.#fn_call(None, request).await; // log_info!("result: {:#?}",result); let response: #response_type = result.map_err(|err|wasm_bindgen::JsError::new(&err.to_string()))?; //log_info!("response: {:#?}",response); @@ -83,7 +83,7 @@ impl ToTokens for RpcHandlers { #[wasm_bindgen(js_name = #fn_camel)] pub async fn #fn_no_suffix(&self, request: #ts_request_type) -> Result<#ts_response_type> { let request: #request_type = request.try_into()?; - let result: RpcResult<#response_type> = self.inner.client.#fn_call(request).await; + let result: RpcResult<#response_type> = self.inner.client.#fn_call(None, request).await; let response: #response_type = result.map_err(|err|wasm_bindgen::JsError::new(&err.to_string()))?; Ok(response.try_into()?) } diff --git a/rpc/service/Cargo.toml b/rpc/service/Cargo.toml index d606d51533..54e9764088 100644 --- a/rpc/service/Cargo.toml +++ b/rpc/service/Cargo.toml @@ -33,4 +33,4 @@ async-trait.workspace = true log.workspace = true tokio.workspace = true triggered.workspace = true -workflow-rpc.workspace = true +workflow-rpc.workspace = true \ No newline at end of file diff --git a/rpc/service/src/converter/consensus.rs b/rpc/service/src/converter/consensus.rs index 9f3f5b661d..dda04899f5 100644 --- a/rpc/service/src/converter/consensus.rs +++ b/rpc/service/src/converter/consensus.rs @@ -81,7 +81,7 @@ impl ConsensusConverter { vec![] }; - Ok(RpcBlock { header: (*block.header).clone(), transactions, verbose_data }) + Ok(RpcBlock { header: block.header.as_ref().into(), transactions, verbose_data }) } pub fn get_mempool_entry(&self, consensus: &ConsensusProxy, transaction: &MutableTransaction) -> RpcMempoolEntry { diff --git a/rpc/service/src/service.rs b/rpc/service/src/service.rs index fc6f266fb4..972006848b 100644 --- a/rpc/service/src/service.rs +++ b/rpc/service/src/service.rs @@ -55,7 +55,8 @@ use kaspa_p2p_lib::common::ProtocolError; use kaspa_perf_monitor::{counters::CountersSnapshot, Monitor as PerfMonitor}; use kaspa_rpc_core::{ api::{ - ops::RPC_API_VERSION, + connection::DynRpcConnection, + ops::{RPC_API_REVISION, RPC_API_VERSION}, rpc::{RpcApi, MAX_SAFE_WINDOW_SIZE}, }, model::*, @@ -64,6 +65,7 @@ use kaspa_rpc_core::{ }; use kaspa_txscript::{extract_script_pub_key_address, pay_to_address_script}; use kaspa_utils::expiring_cache::ExpiringCache; +use kaspa_utils::sysinfo::SystemInfo; use kaspa_utils::{channel::Channel, triggers::SingleTrigger}; use kaspa_utils_tower::counters::TowerConnectionCounters; use kaspa_utxoindex::api::UtxoIndexProxy; @@ -113,6 +115,7 @@ pub struct RpcCoreService { perf_monitor: Arc>>, p2p_tower_counters: Arc, grpc_tower_counters: Arc, + system_info: SystemInfo, fee_estimate_cache: ExpiringCache, fee_estimate_verbose_cache: ExpiringCache>, } @@ -139,6 +142,7 @@ impl RpcCoreService { perf_monitor: Arc>>, p2p_tower_counters: Arc, grpc_tower_counters: Arc, + system_info: SystemInfo, ) -> Self { // This notifier UTXOs subscription granularity to index-processor or consensus notifier let policies = match index_notifier { @@ -214,6 +218,7 @@ impl RpcCoreService { perf_monitor, p2p_tower_counters, grpc_tower_counters, + system_info, fee_estimate_cache: ExpiringCache::new(Duration::from_millis(500), Duration::from_millis(1000)), fee_estimate_verbose_cache: ExpiringCache::new(Duration::from_millis(500), Duration::from_millis(1000)), } @@ -283,7 +288,11 @@ impl RpcCoreService { #[async_trait] impl RpcApi for RpcCoreService { - async fn submit_block_call(&self, request: SubmitBlockRequest) -> RpcResult { + async fn submit_block_call( + &self, + _connection: Option<&DynRpcConnection>, + request: SubmitBlockRequest, + ) -> RpcResult { let session = self.consensus_manager.consensus().unguarded_session(); // TODO: consider adding an error field to SubmitBlockReport to document both the report and error fields @@ -294,7 +303,7 @@ impl RpcApi for RpcCoreService { return Ok(SubmitBlockResponse { report: SubmitBlockReport::Reject(SubmitBlockRejectReason::IsInIBD) }); } - let try_block: RpcResult = (&request.block).try_into(); + let try_block: RpcResult = request.block.try_into(); if let Err(err) = &try_block { trace!("incoming SubmitBlockRequest with block conversion error: {}", err); // error = format!("Could not parse block: {0}", err) @@ -341,7 +350,11 @@ NOTE: This error usually indicates an RPC conversion error between the node and } } - async fn get_block_template_call(&self, request: GetBlockTemplateRequest) -> RpcResult { + async fn get_block_template_call( + &self, + _connection: Option<&DynRpcConnection>, + request: GetBlockTemplateRequest, + ) -> RpcResult { trace!("incoming GetBlockTemplate request"); if *self.config.net == NetworkType::Mainnet && !self.config.enable_mainnet_mining { @@ -368,12 +381,12 @@ NOTE: This error usually indicates an RPC conversion error between the node and let is_nearly_synced = self.config.is_nearly_synced(block_template.selected_parent_timestamp, block_template.selected_parent_daa_score); Ok(GetBlockTemplateResponse { - block: (&block_template.block).into(), + block: block_template.block.into(), is_synced: self.has_sufficient_peer_connectivity() && is_nearly_synced, }) } - async fn get_block_call(&self, request: GetBlockRequest) -> RpcResult { + async fn get_block_call(&self, _connection: Option<&DynRpcConnection>, request: GetBlockRequest) -> RpcResult { // TODO: test let session = self.consensus_manager.consensus().session().await; let block = session.async_get_block_even_if_header_only(request.hash).await?; @@ -385,7 +398,11 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } - async fn get_blocks_call(&self, request: GetBlocksRequest) -> RpcResult { + async fn get_blocks_call( + &self, + _connection: Option<&DynRpcConnection>, + request: GetBlocksRequest, + ) -> RpcResult { // Validate that user didn't set include_transactions without setting include_blocks if !request.include_blocks && request.include_transactions { return Err(RpcError::InvalidGetBlocksRequest); @@ -434,7 +451,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetBlocksResponse { block_hashes, blocks }) } - async fn get_info_call(&self, _request: GetInfoRequest) -> RpcResult { + async fn get_info_call(&self, _connection: Option<&DynRpcConnection>, _request: GetInfoRequest) -> RpcResult { let is_nearly_synced = self.consensus_manager.consensus().unguarded_session().async_is_nearly_synced().await; Ok(GetInfoResponse { p2p_id: self.flow_context.node_id.to_string(), @@ -447,7 +464,11 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } - async fn get_mempool_entry_call(&self, request: GetMempoolEntryRequest) -> RpcResult { + async fn get_mempool_entry_call( + &self, + _connection: Option<&DynRpcConnection>, + request: GetMempoolEntryRequest, + ) -> RpcResult { let query = self.extract_tx_query(request.filter_transaction_pool, request.include_orphan_pool)?; let Some(transaction) = self.mining_manager.clone().get_transaction(request.transaction_id, query).await else { return Err(RpcError::TransactionNotFound(request.transaction_id)); @@ -456,7 +477,11 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetMempoolEntryResponse::new(self.consensus_converter.get_mempool_entry(&session, &transaction))) } - async fn get_mempool_entries_call(&self, request: GetMempoolEntriesRequest) -> RpcResult { + async fn get_mempool_entries_call( + &self, + _connection: Option<&DynRpcConnection>, + request: GetMempoolEntriesRequest, + ) -> RpcResult { let query = self.extract_tx_query(request.filter_transaction_pool, request.include_orphan_pool)?; let session = self.consensus_manager.consensus().unguarded_session(); let (transactions, orphans) = self.mining_manager.clone().get_all_transactions(query).await; @@ -470,6 +495,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and async fn get_mempool_entries_by_addresses_call( &self, + _connection: Option<&DynRpcConnection>, request: GetMempoolEntriesByAddressesRequest, ) -> RpcResult { let query = self.extract_tx_query(request.filter_transaction_pool, request.include_orphan_pool)?; @@ -493,13 +519,17 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetMempoolEntriesByAddressesResponse::new(mempool_entries)) } - async fn submit_transaction_call(&self, request: SubmitTransactionRequest) -> RpcResult { + async fn submit_transaction_call( + &self, + _connection: Option<&DynRpcConnection>, + request: SubmitTransactionRequest, + ) -> RpcResult { let allow_orphan = self.config.unsafe_rpc && request.allow_orphan; if !self.config.unsafe_rpc && request.allow_orphan { warn!("SubmitTransaction RPC command called with AllowOrphan enabled while node in safe RPC mode -- switching to ForbidOrphan."); } - let transaction: Transaction = (&request.transaction).try_into()?; + let transaction: Transaction = request.transaction.try_into()?; let transaction_id = transaction.id(); let session = self.consensus_manager.consensus().unguarded_session(); let orphan = match allow_orphan { @@ -516,9 +546,10 @@ NOTE: This error usually indicates an RPC conversion error between the node and async fn submit_transaction_replacement_call( &self, + _connection: Option<&DynRpcConnection>, request: SubmitTransactionReplacementRequest, ) -> RpcResult { - let transaction: Transaction = (&request.transaction).try_into()?; + let transaction: Transaction = request.transaction.try_into()?; let transaction_id = transaction.id(); let session = self.consensus_manager.consensus().unguarded_session(); let replaced_transaction = @@ -530,25 +561,38 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(SubmitTransactionReplacementResponse::new(transaction_id, (&*replaced_transaction).into())) } - async fn get_current_network_call(&self, _: GetCurrentNetworkRequest) -> RpcResult { + async fn get_current_network_call( + &self, + _connection: Option<&DynRpcConnection>, + _: GetCurrentNetworkRequest, + ) -> RpcResult { Ok(GetCurrentNetworkResponse::new(*self.config.net)) } - async fn get_subnetwork_call(&self, _: GetSubnetworkRequest) -> RpcResult { + async fn get_subnetwork_call( + &self, + _connection: Option<&DynRpcConnection>, + _: GetSubnetworkRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_sink_call(&self, _: GetSinkRequest) -> RpcResult { + async fn get_sink_call(&self, _connection: Option<&DynRpcConnection>, _: GetSinkRequest) -> RpcResult { Ok(GetSinkResponse::new(self.consensus_manager.consensus().unguarded_session().async_get_sink().await)) } - async fn get_sink_blue_score_call(&self, _: GetSinkBlueScoreRequest) -> RpcResult { + async fn get_sink_blue_score_call( + &self, + _connection: Option<&DynRpcConnection>, + _: GetSinkBlueScoreRequest, + ) -> RpcResult { let session = self.consensus_manager.consensus().unguarded_session(); Ok(GetSinkBlueScoreResponse::new(session.async_get_ghostdag_data(session.async_get_sink().await).await?.blue_score)) } async fn get_virtual_chain_from_block_call( &self, + _connection: Option<&DynRpcConnection>, request: GetVirtualChainFromBlockRequest, ) -> RpcResult { let session = self.consensus_manager.consensus().session().await; @@ -561,11 +605,19 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetVirtualChainFromBlockResponse::new(virtual_chain.removed, virtual_chain.added, accepted_transaction_ids)) } - async fn get_block_count_call(&self, _: GetBlockCountRequest) -> RpcResult { + async fn get_block_count_call( + &self, + _connection: Option<&DynRpcConnection>, + _: GetBlockCountRequest, + ) -> RpcResult { Ok(self.consensus_manager.consensus().unguarded_session().async_estimate_block_count().await) } - async fn get_utxos_by_addresses_call(&self, request: GetUtxosByAddressesRequest) -> RpcResult { + async fn get_utxos_by_addresses_call( + &self, + _connection: Option<&DynRpcConnection>, + request: GetUtxosByAddressesRequest, + ) -> RpcResult { if !self.config.utxoindex { return Err(RpcError::NoUtxoIndex); } @@ -575,7 +627,11 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetUtxosByAddressesResponse::new(self.index_converter.get_utxos_by_addresses_entries(&entry_map))) } - async fn get_balance_by_address_call(&self, request: GetBalanceByAddressRequest) -> RpcResult { + async fn get_balance_by_address_call( + &self, + _connection: Option<&DynRpcConnection>, + request: GetBalanceByAddressRequest, + ) -> RpcResult { if !self.config.utxoindex { return Err(RpcError::NoUtxoIndex); } @@ -586,6 +642,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and async fn get_balances_by_addresses_call( &self, + _connection: Option<&DynRpcConnection>, request: GetBalancesByAddressesRequest, ) -> RpcResult { if !self.config.utxoindex { @@ -604,7 +661,11 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetBalancesByAddressesResponse::new(entries)) } - async fn get_coin_supply_call(&self, _: GetCoinSupplyRequest) -> RpcResult { + async fn get_coin_supply_call( + &self, + _connection: Option<&DynRpcConnection>, + _: GetCoinSupplyRequest, + ) -> RpcResult { if !self.config.utxoindex { return Err(RpcError::NoUtxoIndex); } @@ -615,6 +676,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and async fn get_daa_score_timestamp_estimate_call( &self, + _connection: Option<&DynRpcConnection>, request: GetDaaScoreTimestampEstimateRequest, ) -> RpcResult { let session = self.consensus_manager.consensus().session().await; @@ -671,7 +733,11 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetDaaScoreTimestampEstimateResponse::new(timestamps)) } - async fn get_fee_estimate_call(&self, _request: GetFeeEstimateRequest) -> RpcResult { + async fn get_fee_estimate_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetFeeEstimateRequest, + ) -> RpcResult { let mining_manager = self.mining_manager.clone(); let estimate = self.fee_estimate_cache.get(async move { mining_manager.get_realtime_feerate_estimations().await.into_rpc() }).await; @@ -680,6 +746,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and async fn get_fee_estimate_experimental_call( &self, + connection: Option<&DynRpcConnection>, request: GetFeeEstimateExperimentalRequest, ) -> RpcResult { if request.verbose { @@ -696,20 +763,28 @@ NOTE: This error usually indicates an RPC conversion error between the node and .await?; Ok(response) } else { - let estimate = self.get_fee_estimate_call(GetFeeEstimateRequest {}).await?.estimate; + let estimate = self.get_fee_estimate_call(connection, GetFeeEstimateRequest {}).await?.estimate; Ok(GetFeeEstimateExperimentalResponse { estimate, verbose: None }) } } - async fn ping_call(&self, _: PingRequest) -> RpcResult { + async fn ping_call(&self, _connection: Option<&DynRpcConnection>, _: PingRequest) -> RpcResult { Ok(PingResponse {}) } - async fn get_headers_call(&self, _request: GetHeadersRequest) -> RpcResult { + async fn get_headers_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetHeadersRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_block_dag_info_call(&self, _: GetBlockDagInfoRequest) -> RpcResult { + async fn get_block_dag_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _: GetBlockDagInfoRequest, + ) -> RpcResult { let session = self.consensus_manager.consensus().unguarded_session(); let (consensus_stats, tips, pruning_point, sink) = join!(session.async_get_stats(), session.async_get_tips(), session.async_pruning_point(), session.async_get_sink()); @@ -729,6 +804,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and async fn estimate_network_hashes_per_second_call( &self, + _connection: Option<&DynRpcConnection>, request: EstimateNetworkHashesPerSecondRequest, ) -> RpcResult { if !self.config.unsafe_rpc && request.window_size > MAX_SAFE_WINDOW_SIZE { @@ -758,7 +834,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and )) } - async fn add_peer_call(&self, request: AddPeerRequest) -> RpcResult { + async fn add_peer_call(&self, _connection: Option<&DynRpcConnection>, request: AddPeerRequest) -> RpcResult { if !self.config.unsafe_rpc { warn!("AddPeer RPC command called while node in safe RPC mode -- ignoring."); return Err(RpcError::UnavailableInSafeMode); @@ -772,12 +848,16 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(AddPeerResponse {}) } - async fn get_peer_addresses_call(&self, _: GetPeerAddressesRequest) -> RpcResult { + async fn get_peer_addresses_call( + &self, + _connection: Option<&DynRpcConnection>, + _: GetPeerAddressesRequest, + ) -> RpcResult { let address_manager = self.flow_context.address_manager.lock(); Ok(GetPeerAddressesResponse::new(address_manager.get_all_addresses(), address_manager.get_all_banned_addresses())) } - async fn ban_call(&self, request: BanRequest) -> RpcResult { + async fn ban_call(&self, _connection: Option<&DynRpcConnection>, request: BanRequest) -> RpcResult { if !self.config.unsafe_rpc { warn!("Ban RPC command called while node in safe RPC mode -- ignoring."); return Err(RpcError::UnavailableInSafeMode); @@ -794,7 +874,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(BanResponse {}) } - async fn unban_call(&self, request: UnbanRequest) -> RpcResult { + async fn unban_call(&self, _connection: Option<&DynRpcConnection>, request: UnbanRequest) -> RpcResult { if !self.config.unsafe_rpc { warn!("Unban RPC command called while node in safe RPC mode -- ignoring."); return Err(RpcError::UnavailableInSafeMode); @@ -808,13 +888,17 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(UnbanResponse {}) } - async fn get_connected_peer_info_call(&self, _: GetConnectedPeerInfoRequest) -> RpcResult { + async fn get_connected_peer_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _: GetConnectedPeerInfoRequest, + ) -> RpcResult { let peers = self.flow_context.hub().active_peers(); let peer_info = self.protocol_converter.get_peers_info(&peers); Ok(GetConnectedPeerInfoResponse::new(peer_info)) } - async fn shutdown_call(&self, _: ShutdownRequest) -> RpcResult { + async fn shutdown_call(&self, _connection: Option<&DynRpcConnection>, _: ShutdownRequest) -> RpcResult { if !self.config.unsafe_rpc { warn!("Shutdown RPC command called while node in safe RPC mode -- ignoring."); return Err(RpcError::UnavailableInSafeMode); @@ -837,6 +921,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and async fn resolve_finality_conflict_call( &self, + _connection: Option<&DynRpcConnection>, _request: ResolveFinalityConflictRequest, ) -> RpcResult { if !self.config.unsafe_rpc { @@ -846,7 +931,25 @@ NOTE: This error usually indicates an RPC conversion error between the node and Err(RpcError::NotImplemented) } - async fn get_metrics_call(&self, req: GetMetricsRequest) -> RpcResult { + async fn get_connections_call( + &self, + _connection: Option<&DynRpcConnection>, + req: GetConnectionsRequest, + ) -> RpcResult { + let clients = (self.wrpc_borsh_counters.active_connections.load(Ordering::Relaxed) + + self.wrpc_json_counters.active_connections.load(Ordering::Relaxed)) as u32; + let peers = self.flow_context.hub().active_peers_len() as u16; + + let profile_data = req.include_profile_data.then(|| { + let CountersSnapshot { resident_set_size: memory_usage, cpu_usage, .. } = self.perf_monitor.snapshot(); + + ConnectionsProfileData { cpu_usage: cpu_usage as f32, memory_usage } + }); + + Ok(GetConnectionsResponse { clients, peers, profile_data }) + } + + async fn get_metrics_call(&self, _connection: Option<&DynRpcConnection>, req: GetMetricsRequest) -> RpcResult { let CountersSnapshot { resident_set_size, virtual_memory_size, @@ -871,7 +974,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and disk_io_write_per_sec: disk_io_write_per_sec as f32, }); - let connection_metrics = req.connection_metrics.then_some(ConnectionMetrics { + let connection_metrics = req.connection_metrics.then(|| ConnectionMetrics { borsh_live_connections: self.wrpc_borsh_counters.active_connections.load(Ordering::Relaxed) as u32, borsh_connection_attempts: self.wrpc_borsh_counters.total_connections.load(Ordering::Relaxed) as u64, borsh_handshake_failures: self.wrpc_borsh_counters.handshake_failures.load(Ordering::Relaxed) as u64, @@ -882,7 +985,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and active_peers: self.flow_context.hub().active_peers_len() as u32, }); - let bandwidth_metrics = req.bandwidth_metrics.then_some(BandwidthMetrics { + let bandwidth_metrics = req.bandwidth_metrics.then(|| BandwidthMetrics { borsh_bytes_tx: self.wrpc_borsh_counters.tx_bytes.load(Ordering::Relaxed) as u64, borsh_bytes_rx: self.wrpc_borsh_counters.rx_bytes.load(Ordering::Relaxed) as u64, json_bytes_tx: self.wrpc_json_counters.tx_bytes.load(Ordering::Relaxed) as u64, @@ -920,20 +1023,54 @@ NOTE: This error usually indicates an RPC conversion error between the node and None }; + let storage_metrics = req.storage_metrics.then_some(StorageMetrics { storage_size_bytes: 0 }); + + let custom_metrics: Option> = None; + let server_time = unix_now(); - let response = GetMetricsResponse { server_time, process_metrics, connection_metrics, bandwidth_metrics, consensus_metrics }; + let response = GetMetricsResponse { + server_time, + process_metrics, + connection_metrics, + bandwidth_metrics, + consensus_metrics, + storage_metrics, + custom_metrics, + }; Ok(response) } - async fn get_server_info_call(&self, _request: GetServerInfoRequest) -> RpcResult { + async fn get_system_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetSystemInfoRequest, + ) -> RpcResult { + let response = GetSystemInfoResponse { + version: self.system_info.version.clone(), + system_id: self.system_info.system_id.clone(), + git_hash: self.system_info.git_short_hash.clone(), + cpu_physical_cores: self.system_info.cpu_physical_cores, + total_memory: self.system_info.total_memory, + fd_limit: self.system_info.fd_limit, + }; + + Ok(response) + } + + async fn get_server_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetServerInfoRequest, + ) -> RpcResult { let session = self.consensus_manager.consensus().unguarded_session(); let is_synced: bool = self.has_sufficient_peer_connectivity() && session.async_is_nearly_synced().await; let virtual_daa_score = session.get_virtual_daa_score(); Ok(GetServerInfoResponse { rpc_api_version: RPC_API_VERSION, + rpc_api_revision: RPC_API_REVISION, server_version: version().to_string(), network_id: self.config.net, has_utxo_index: self.config.utxoindex, @@ -942,7 +1079,11 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } - async fn get_sync_status_call(&self, _request: GetSyncStatusRequest) -> RpcResult { + async fn get_sync_status_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetSyncStatusRequest, + ) -> RpcResult { let session = self.consensus_manager.consensus().unguarded_session(); let is_synced: bool = self.has_sufficient_peer_connectivity() && session.async_is_nearly_synced().await; Ok(GetSyncStatusResponse { is_synced }) diff --git a/rpc/wrpc/client/Cargo.toml b/rpc/wrpc/client/Cargo.toml index 199232ff00..c9dccc65b6 100644 --- a/rpc/wrpc/client/Cargo.toml +++ b/rpc/wrpc/client/Cargo.toml @@ -44,4 +44,5 @@ workflow-dom.workspace = true workflow-http.workspace = true workflow-log.workspace = true workflow-rpc.workspace = true -workflow-wasm.workspace = true \ No newline at end of file +workflow-serializer.workspace = true +workflow-wasm.workspace = true diff --git a/rpc/wrpc/client/Resolvers.toml b/rpc/wrpc/client/Resolvers.toml index 295a352d2f..cb8b436527 100644 --- a/rpc/wrpc/client/Resolvers.toml +++ b/rpc/wrpc/client/Resolvers.toml @@ -1,11 +1,41 @@ [[resolver]] -url = "https://beacon.kaspa-ng.org" -enable = true +enable = false +address = "http://127.0.0.1:8888" -[[resolver]] -url = "https://beacon.kaspa-ng.io" -enable = true +[[group]] +template = "https://*.kaspa.stream" +nodes = ["eric","maxim","sean","troy"] -[[resolver]] -url = "http://127.0.0.1:8888" -enable = false +[[group]] +template = "https://*.kaspa.red" +nodes = ["john", "mike", "paul", "alex"] + +[[group]] +template = "https://*.kaspa.green" +nodes = ["jake", "mark", "adam", "liam"] + +[[group]] +template = "https://*.kaspa.blue" +nodes = ["noah", "ryan", "jack", "luke"] + +# [[group]] +# enable = true +# template = "https://*.kaspa-ng.org" +# nodes = ["cole", "ivan", "oscar", "zane"] + +# [[group]] +# enable = true +# template = "https://*.kaspa-ng.io" +# nodes = ["gary", "hugo", "finn", "evan"] + +# [[group]] +# enable = true +# template = "https://*.kaspa-ng.net" +# nodes = ["neil", "dave", "kyle", "toby"] + +# --- + +# [[group]] +# enable = false +# template = "https://*." +# nodes = ["rudy", "todd", "clay", "walt"] diff --git a/rpc/wrpc/client/src/client.rs b/rpc/wrpc/client/src/client.rs index e57024c4b0..a0a33444b8 100644 --- a/rpc/wrpc/client/src/client.rs +++ b/rpc/wrpc/client/src/client.rs @@ -18,7 +18,7 @@ use workflow_rpc::client::Ctl as WrpcCtl; pub use workflow_rpc::client::{ ConnectOptions, ConnectResult, ConnectStrategy, Resolver as RpcResolver, ResolverResult, WebSocketConfig, WebSocketError, }; - +use workflow_serializer::prelude::*; type RpcClientNotifier = Arc>; struct Inner { @@ -34,7 +34,14 @@ struct Inner { connect_guard: AsyncMutex<()>, disconnect_guard: AsyncMutex<()>, // --- + // The permanent url passed in the constructor + // (dominant, overrides Resolver if supplied). + ctor_url: Mutex>, + // The url passed in the connect() method + // (overrides default URL and the Resolver). default_url: Mutex>, + // The current url wRPC is connected to + // (possibly acquired via the Resolver). current_url: Mutex>, resolver: Mutex>, network_id: Mutex>, @@ -73,16 +80,16 @@ impl Inner { let notification_sender_ = notification_relay_channel.sender.clone(); interface.notification( notification_op, - workflow_rpc::client::Notification::new(move |notification: kaspa_rpc_core::Notification| { + workflow_rpc::client::Notification::new(move |notification: Serializable| { let notification_sender = notification_sender_.clone(); Box::pin(async move { // log_info!("notification receivers: {}", notification_sender.receiver_count()); // log_trace!("notification {:?}", notification); if notification_sender.receiver_count() > 1 { // log_info!("notification: posting to channel: {notification:?}"); - notification_sender.send(notification).await?; + notification_sender.send(notification.into_inner()).await?; } else { - log_warn!("WARNING: Kaspa RPC notification is not consumed by user: {:?}", notification); + log_warn!("WARNING: Kaspa RPC notification is not consumed by user: {:?}", notification.into_inner()); } Ok(()) }) @@ -104,7 +111,8 @@ impl Inner { connect_guard: async_std::sync::Mutex::new(()), disconnect_guard: async_std::sync::Mutex::new(()), // --- - default_url: Mutex::new(url.map(|s| s.to_string())), + ctor_url: Mutex::new(url.map(|s| s.to_string())), + default_url: Mutex::new(None), current_url: Mutex::new(None), resolver: Mutex::new(resolver), network_id: Mutex::new(network_id), @@ -121,17 +129,22 @@ impl Inner { /// Start sending notifications of some type to the client. async fn start_notify_to_client(&self, scope: Scope) -> RpcResult<()> { - let _response: SubscribeResponse = self.rpc_client.call(RpcApiOps::Subscribe, scope).await.map_err(|err| err.to_string())?; + let _response: Serializable = + self.rpc_client.call(RpcApiOps::Subscribe, Serializable(scope)).await.map_err(|err| err.to_string())?; Ok(()) } /// Stop sending notifications of some type to the client. async fn stop_notify_to_client(&self, scope: Scope) -> RpcResult<()> { - let _response: UnsubscribeResponse = - self.rpc_client.call(RpcApiOps::Unsubscribe, scope).await.map_err(|err| err.to_string())?; + let _response: Serializable = + self.rpc_client.call(RpcApiOps::Unsubscribe, Serializable(scope)).await.map_err(|err| err.to_string())?; Ok(()) } + fn ctor_url(&self) -> Option { + self.ctor_url.lock().unwrap().clone() + } + fn default_url(&self) -> Option { self.default_url.lock().unwrap().clone() } @@ -213,7 +226,7 @@ impl SubscriptionManager for Inner { #[async_trait] impl RpcResolver for Inner { async fn resolve_url(&self) -> ResolverResult { - let url = if let Some(url) = self.default_url() { + let url = if let Some(url) = self.default_url().or(self.ctor_url()) { url } else if let Some(resolver) = self.resolver().as_ref() { let network_id = self.network_id().expect("Resolver requires network id in RPC client configuration"); @@ -222,7 +235,7 @@ impl RpcResolver for Inner { self.node_descriptor.lock().unwrap().replace(Arc::new(node)); url } else { - panic!("RpcClient resolver configuration error (expecting Some(Resolver))") + panic!("RpcClient resolver configuration error (expecting `url` or `resolver` as `Some(Resolver))`") }; self.rpc_ctl.set_descriptor(Some(url.clone())); @@ -254,7 +267,11 @@ impl Debug for KaspaRpcClient { } impl KaspaRpcClient { - /// Create a new `KaspaRpcClient` with the given Encoding and URL + /// Create a new `KaspaRpcClient` with the given Encoding, and an optional url or a Resolver. + /// Please note that if you pass the url to the constructor, it will force the KaspaRpcClient + /// to always use this url. If you want to have the ability to switch between urls, + /// you must pass [`Option::None`] as the `url` argument and then supply your own url to the `connect()` + /// function each time you connect. pub fn new( encoding: Encoding, url: Option<&str>, @@ -371,6 +388,10 @@ impl KaspaRpcClient { &self.inner.rpc_ctl } + pub fn ctl_multiplexer(&self) -> Multiplexer { + self.inner.wrpc_ctl_multiplexer.clone() + } + /// Start background RPC services. pub async fn start(&self) -> Result<()> { if !self.inner.background_services_running.load(Ordering::SeqCst) { @@ -403,12 +424,10 @@ impl KaspaRpcClient { pub async fn connect(&self, options: Option) -> ConnectResult { let _guard = self.inner.connect_guard.lock().await; - let mut options = options.unwrap_or_default(); + let options = options.unwrap_or_default(); let strategy = options.strategy; - if let Some(url) = options.url.take() { - self.set_url(Some(&url))?; - } + self.inner.set_default_url(options.url.as_deref()); // 1Gb message and frame size limits (on native and NodeJs platforms) let ws_config = WebSocketConfig { @@ -584,6 +603,7 @@ impl RpcApi for KaspaRpcClient { build_wrpc_client_interface!( RpcApiOps, [ + Ping, AddPeer, Ban, EstimateNetworkHashesPerSecond, @@ -596,6 +616,7 @@ impl RpcApi for KaspaRpcClient { GetBlockTemplate, GetCoinSupply, GetConnectedPeerInfo, + GetConnections, GetCurrentNetwork, GetDaaScoreTimestampEstimate, GetFeeEstimate, @@ -612,9 +633,9 @@ impl RpcApi for KaspaRpcClient { GetSinkBlueScore, GetSubnetwork, GetSyncStatus, + GetSystemInfo, GetUtxosByAddresses, GetVirtualChainFromBlock, - Ping, ResolveFinalityConflict, Shutdown, SubmitBlock, diff --git a/rpc/wrpc/client/src/node.rs b/rpc/wrpc/client/src/node.rs index 4afb0f1b7a..ca7e19c879 100644 --- a/rpc/wrpc/client/src/node.rs +++ b/rpc/wrpc/client/src/node.rs @@ -11,16 +11,10 @@ use crate::imports::*; pub struct NodeDescriptor { /// The unique identifier of the node. #[wasm_bindgen(getter_with_clone)] - pub id: String, + pub uid: String, /// The URL of the node WebSocket (wRPC URL). #[wasm_bindgen(getter_with_clone)] pub url: String, - /// Optional name of the node provider. - #[wasm_bindgen(getter_with_clone)] - pub provider_name: Option, - /// Optional site URL of the node provider. - #[wasm_bindgen(getter_with_clone)] - pub provider_url: Option, } impl Eq for NodeDescriptor {} diff --git a/rpc/wrpc/client/src/resolver.rs b/rpc/wrpc/client/src/resolver.rs index 4fd159b40b..8dcb194476 100644 --- a/rpc/wrpc/client/src/resolver.rs +++ b/rpc/wrpc/client/src/resolver.rs @@ -1,40 +1,79 @@ +use std::sync::OnceLock; + use crate::error::Error; use crate::imports::*; use crate::node::NodeDescriptor; pub use futures::future::join_all; use rand::seq::SliceRandom; use rand::thread_rng; +use workflow_core::runtime; use workflow_http::get_json; -const DEFAULT_VERSION: usize = 1; +const CURRENT_VERSION: usize = 2; +const RESOLVER_CONFIG: &str = include_str!("../Resolvers.toml"); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResolverRecord { - pub url: String, + pub address: String, + pub enable: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResolverGroup { + pub template: String, + pub nodes: Vec, pub enable: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ResolverConfig { - resolver: Vec, + #[serde(rename = "group")] + groups: Vec, + #[serde(rename = "resolver")] + resolvers: Vec, } fn try_parse_resolvers(toml: &str) -> Result>> { - Ok(toml::from_str::(toml)? - .resolver + let config = toml::from_str::(toml)?; + + let mut resolvers = config + .resolvers .into_iter() - .filter_map(|resolver| resolver.enable.unwrap_or(true).then_some(Arc::new(resolver.url))) - .collect::>()) + .filter_map(|resolver| resolver.enable.unwrap_or(true).then_some(resolver.address)) + .collect::>(); + + let groups = config.groups.into_iter().filter(|group| group.enable.unwrap_or(true)).collect::>(); + + for group in groups { + let ResolverGroup { template, nodes, .. } = group; + for node in nodes { + resolvers.push(template.replace('*', &node)); + } + } + + Ok(resolvers.into_iter().map(Arc::new).collect::>()) } #[derive(Debug)] struct Inner { pub urls: Vec>, + pub tls: bool, + public: bool, } impl Inner { - pub fn new(urls: Vec>) -> Self { - Self { urls } + pub fn new(urls: Option>>, tls: bool) -> Self { + if urls.as_ref().is_some_and(|urls| urls.is_empty()) { + panic!("Resolver: Empty URL list supplied to the constructor."); + } + + let mut public = false; + let urls = urls.unwrap_or_else(|| { + public = true; + try_parse_resolvers(RESOLVER_CONFIG).expect("TOML: Unable to parse RPC Resolver list") + }); + + Self { urls, tls, public } } } @@ -48,27 +87,61 @@ pub struct Resolver { impl Default for Resolver { fn default() -> Self { - let toml = include_str!("../Resolvers.toml"); - let urls = try_parse_resolvers(toml).expect("TOML: Unable to parse RPC Resolver list"); - Self { inner: Arc::new(Inner::new(urls)) } + Self { inner: Arc::new(Inner::new(None, false)) } } } impl Resolver { - pub fn new(urls: Vec>) -> Self { - if urls.is_empty() { - panic!("Resolver: Empty URL list supplied to the constructor."); + pub fn new(urls: Option>>, tls: bool) -> Self { + Self { inner: Arc::new(Inner::new(urls, tls)) } + } + + pub fn urls(&self) -> Option>> { + if self.inner.public { + None + } else { + Some(self.inner.urls.clone()) } + } + + pub fn tls(&self) -> bool { + self.inner.tls + } - Self { inner: Arc::new(Inner::new(urls)) } + pub fn tls_as_str(&self) -> &'static str { + if self.inner.tls { + "tls" + } else { + "any" + } } - pub fn urls(&self) -> Vec> { - self.inner.urls.clone() + fn make_url(&self, url: &str, encoding: Encoding, network_id: NetworkId) -> String { + static TLS: OnceLock<&'static str> = OnceLock::new(); + + let tls = *TLS.get_or_init(|| { + if runtime::is_web() { + let tls = js_sys::Reflect::get(&js_sys::global(), &"location".into()) + .and_then(|location| js_sys::Reflect::get(&location, &"protocol".into())) + .ok() + .and_then(|protocol| protocol.as_string()) + .map(|protocol| protocol.starts_with("https")) + .unwrap_or(false); + if tls { + "tls" + } else { + self.tls_as_str() + } + } else { + self.tls_as_str() + } + }); + + format!("{url}/v{CURRENT_VERSION}/kaspa/{network_id}/{tls}/wrpc/{encoding}") } async fn fetch_node_info(&self, url: &str, encoding: Encoding, network_id: NetworkId) -> Result { - let url = format!("{}/v{}/wrpc/{}/{}", url, DEFAULT_VERSION, encoding, network_id); + let url = self.make_url(url, encoding, network_id); let node = get_json::(&url).await.map_err(|error| Error::custom(format!("Unable to connect to {url}: {error}")))?; Ok(node) @@ -88,27 +161,6 @@ impl Resolver { Err(Error::Custom(format!("Failed to connect: {:?}", errors))) } - pub async fn fetch_all(&self, encoding: Encoding, network_id: NetworkId) -> Result> { - let futures = self.inner.urls.iter().map(|url| self.fetch_node_info(url, encoding, network_id)).collect::>(); - let mut errors = Vec::default(); - let result = join_all(futures) - .await - .into_iter() - .filter_map(|result| match result { - Ok(node) => Some(node), - Err(error) => { - errors.push(format!("{:?}", error)); - None - } - }) - .collect::>(); - if result.is_empty() { - Err(Error::Custom(format!("Failed to connect: {:?}", errors))) - } else { - Ok(result) - } - } - pub async fn get_node(&self, encoding: Encoding, network_id: NetworkId) -> Result { self.fetch(encoding, network_id).await } @@ -118,3 +170,37 @@ impl Resolver { Ok(nodes.url.clone()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_resolver_config_1() { + let toml = r#" + [[group]] + enable = true + template = "https://*.example.org" + nodes = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"] + + [[group]] + enable = true + template = "https://*.example.com" + nodes = ["iota", "kappa", "lambda", "mu", "nu", "xi", "omicron", "pi"] + + [[resolver]] + enable = true + address = "http://127.0.0.1:8888" + "#; + + let urls = try_parse_resolvers(toml).expect("TOML: Unable to parse RPC Resolver list"); + // println!("{:#?}", urls); + assert_eq!(urls.len(), 17); + } + + #[test] + fn test_resolver_config_2() { + let _urls = try_parse_resolvers(RESOLVER_CONFIG).expect("TOML: Unable to parse RPC Resolver list"); + // println!("{:#?}", urls); + } +} diff --git a/rpc/wrpc/proxy/src/main.rs b/rpc/wrpc/proxy/src/main.rs index 43b3938c97..1cb9ad5c60 100644 --- a/rpc/wrpc/proxy/src/main.rs +++ b/rpc/wrpc/proxy/src/main.rs @@ -90,13 +90,15 @@ async fn main() -> Result<()> { rpc_handler.clone(), router.interface.clone(), Some(counters), + false, ); log_info!("Kaspa wRPC server is listening on {}", options.listen_address); log_info!("Using `{encoding}` protocol encoding"); let config = WebSocketConfig { max_message_size: Some(1024 * 1024 * 1024), ..Default::default() }; - server.listen(&options.listen_address, Some(config)).await?; + let listener = server.bind(&options.listen_address).await?; + server.listen(listener, Some(config)).await?; Ok(()) } diff --git a/rpc/wrpc/resolver/Cargo.toml b/rpc/wrpc/resolver/Cargo.toml deleted file mode 100644 index cb28d82bf8..0000000000 --- a/rpc/wrpc/resolver/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "kaspa-resolver" -description = "Kaspa wRPC endpoint resolver and monitor" -version.workspace = true -edition.workspace = true -authors.workspace = true -include.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] - - -ahash.workspace = true -cfg-if.workspace = true -clap.workspace = true -convert_case.workspace = true -futures.workspace = true -kaspa-consensus-core.workspace = true -kaspa-rpc-core.workspace = true -kaspa-utils.workspace = true -kaspa-wrpc-client.workspace = true -serde_json.workspace = true -serde.workspace = true -thiserror.workspace = true -tokio.workspace = true -toml.workspace = true -workflow-core.workspace = true -workflow-http.workspace = true -workflow-log.workspace = true -xxhash-rust = { workspace = true } - -# these are temporarily localized to prevent -# conflicts with other workspace dependencies -# as tower is used in gRPC-related crates. -axum = "0.7.4" -console = "0.15.8" -mime = "0.3.16" -tower = { version = "0.4.13", features = ["buffer","limit"] } -tower-http = { version = "0.5.1", features = ["cors"] } -tracing-subscriber = "0.3.18" diff --git a/rpc/wrpc/resolver/src/args.rs b/rpc/wrpc/resolver/src/args.rs deleted file mode 100644 index 7a526b99b1..0000000000 --- a/rpc/wrpc/resolver/src/args.rs +++ /dev/null @@ -1,54 +0,0 @@ -pub use clap::Parser; -use std::str::FromStr; - -#[derive(Default, Parser, Debug)] -#[command(version, about, long_about = None)] -pub struct Args { - /// HTTP server port - #[arg(long, default_value = "127.0.0.1:8888")] - pub listen: String, - - /// Optional rate limit in the form `:`, where `requests` is the number of requests allowed per specified number of `seconds` - #[arg(long = "rate-limit", value_name = "REQUESTS:SECONDS")] - pub rate_limit: Option, - - /// Verbose mode - #[arg(short, long, default_value = "false")] - pub verbose: bool, - - /// Show node data on each election - #[arg(short, long, default_value = "false")] - pub election: bool, - - /// Enable resolver status access via `/status` - #[arg(long, default_value = "false")] - pub status: bool, -} - -#[derive(Clone, Debug)] -pub struct RateLimit { - pub requests: u64, - pub period: u64, -} - -impl FromStr for RateLimit { - type Err = String; - - fn from_str(s: &str) -> Result { - let parts = s.split_once(':'); - let (requests, period) = match parts { - None | Some(("", _)) | Some((_, "")) => { - return Err("invalid rate limit, must be `:`".to_string()); - } - Some(x) => x, - }; - let requests = requests - .parse() - .map_err(|_| format!("Unable to parse number of requests, the value must be an integer, supplied: {:?}", requests))?; - let period = period.parse().map_err(|_| { - format!("Unable to parse period, the value must be an integer specifying number of seconds, supplied: {:?}", period) - })?; - - Ok(RateLimit { requests, period }) - } -} diff --git a/rpc/wrpc/resolver/src/connection.rs b/rpc/wrpc/resolver/src/connection.rs deleted file mode 100644 index 75577719f3..0000000000 --- a/rpc/wrpc/resolver/src/connection.rs +++ /dev/null @@ -1,262 +0,0 @@ -use crate::imports::*; - -const BIAS_SCALE: u64 = 1_000_000; - -#[derive(Debug, Clone)] -pub struct Descriptor { - pub connection: Arc, - pub json: String, -} - -impl From<&Arc> for Descriptor { - fn from(connection: &Arc) -> Self { - Self { connection: connection.clone(), json: serde_json::to_string(&Output::from(connection)).unwrap() } - } -} - -impl fmt::Display for Connection { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: [{:>3}] {}", self.node.id_string, self.clients(), self.node.address) - } -} - -#[derive(Debug)] -pub struct Connection { - pub node: Arc, - bias: u64, - descriptor: RwLock>, - sender: Sender, - client: KaspaRpcClient, - shutdown_ctl: DuplexChannel<()>, - is_connected: Arc, - is_synced: Arc, - is_online: Arc, - clients: Arc, - args: Arc, -} - -impl Connection { - pub fn try_new(node: Arc, sender: Sender, args: &Arc) -> Result { - let client = KaspaRpcClient::new(node.encoding, Some(&node.address), None, None, None)?; - let descriptor = RwLock::default(); - let shutdown_ctl = DuplexChannel::oneshot(); - let is_connected = Arc::new(AtomicBool::new(false)); - let is_synced = Arc::new(AtomicBool::new(true)); - let is_online = Arc::new(AtomicBool::new(false)); - let clients = Arc::new(AtomicU64::new(0)); - let bias = (node.bias.unwrap_or(1.0) * BIAS_SCALE as f64) as u64; - let args = args.clone(); - Ok(Self { node, descriptor, sender, client, shutdown_ctl, is_connected, is_synced, is_online, clients, bias, args }) - } - - pub fn verbose(&self) -> bool { - self.args.verbose - } - - pub fn score(&self) -> u64 { - self.clients.load(Ordering::Relaxed) * self.bias / BIAS_SCALE - } - - pub fn connected(&self) -> bool { - self.is_connected.load(Ordering::Relaxed) - } - - pub fn online(&self) -> bool { - self.is_online.load(Ordering::Relaxed) - } - - pub fn is_synced(&self) -> bool { - self.is_synced.load(Ordering::Relaxed) - } - - pub fn clients(&self) -> u64 { - self.clients.load(Ordering::Relaxed) - } - - pub fn status(&self) -> &'static str { - if self.connected() { - if self.is_synced() { - "online" - } else { - "syncing" - } - } else { - "offline" - } - } - - pub fn descriptor(&self) -> Option { - self.descriptor.read().unwrap().clone() - } - - async fn connect(&self) -> Result<()> { - let options = ConnectOptions { block_async_connect: false, strategy: ConnectStrategy::Retry, ..Default::default() }; - - self.client.connect(Some(options)).await?; - Ok(()) - } - - async fn task(self: Arc) -> Result<()> { - self.connect().await?; - let rpc_ctl_channel = self.client.rpc_ctl().multiplexer().channel(); - let shutdown_ctl_receiver = self.shutdown_ctl.request.receiver.clone(); - let shutdown_ctl_sender = self.shutdown_ctl.response.sender.clone(); - - let interval = workflow_core::task::interval(Duration::from_secs(5)); - pin_mut!(interval); - - loop { - select! { - _ = interval.next().fuse() => { - if self.is_connected.load(Ordering::Relaxed) { - let previous = self.is_online.load(Ordering::Relaxed); - let online = self.update_metrics().await.is_ok(); - self.is_online.store(online, Ordering::Relaxed); - if online != previous { - if self.verbose() { - log_error!("Offline","{}", self.node.address); - } - self.update(online).await?; - } - } - } - - msg = rpc_ctl_channel.receiver.recv().fuse() => { - match msg { - Ok(msg) => { - - // handle wRPC channel connection and disconnection events - match msg { - RpcState::Connected => { - log_success!("Connected","{}",self.node.address); - self.is_connected.store(true, Ordering::Relaxed); - if self.update_metrics().await.is_ok() { - self.is_online.store(true, Ordering::Relaxed); - self.update(true).await?; - } else { - self.is_online.store(false, Ordering::Relaxed); - } - }, - RpcState::Disconnected => { - self.is_connected.store(false, Ordering::Relaxed); - self.is_online.store(false, Ordering::Relaxed); - self.update(false).await?; - log_error!("Disconnected","{}",self.node.address); - } - } - } - Err(err) => { - println!("Monitor: error while receiving rpc_ctl_channel message: {err}"); - break; - } - } - } - - _ = shutdown_ctl_receiver.recv().fuse() => { - break; - }, - - } - } - - shutdown_ctl_sender.send(()).await.unwrap(); - - Ok(()) - } - - pub fn start(self: &Arc) -> Result<()> { - let this = self.clone(); - spawn(async move { - if let Err(error) = this.task().await { - println!("NodeConnection task error: {:?}", error); - } - }); - - Ok(()) - } - - pub async fn stop(self: &Arc) -> Result<()> { - self.shutdown_ctl.signal(()).await.expect("NodeConnection shutdown signal error"); - Ok(()) - } - - async fn update_metrics(self: &Arc) -> Result { - match self.client.get_sync_status().await { - Ok(is_synced) => { - let previous_sync = self.is_synced.load(Ordering::Relaxed); - self.is_synced.store(is_synced, Ordering::Relaxed); - - if is_synced { - match self.client.get_metrics(false, true, false, false).await { - Ok(metrics) => { - if let Some(connection_metrics) = metrics.connection_metrics { - // update - let previous = self.clients.load(Ordering::Relaxed); - let clients = - connection_metrics.borsh_live_connections as u64 + connection_metrics.json_live_connections as u64; - self.clients.store(clients, Ordering::Relaxed); - if clients != previous { - if self.verbose() { - log_success!("Clients", "{self}"); - } - Ok(true) - } else { - Ok(false) - } - } else { - log_error!("Metrics", "{self} - failure"); - Err(Error::ConnectionMetrics) - } - } - Err(err) => { - log_error!("Metrics", "{self}"); - log_error!("RPC", "{err}"); - Err(Error::Metrics) - } - } - } else { - if is_synced != previous_sync { - log_error!("Syncing", "{self}"); - } - Err(Error::Sync) - } - } - Err(err) => { - log_error!("RPC", "{self}"); - log_error!("RPC", "{err}"); - Err(Error::Status) - } - } - } - - pub async fn update(self: &Arc, online: bool) -> Result<()> { - *self.descriptor.write().unwrap() = online.then_some(self.into()); - self.sender.try_send(self.node.params())?; - Ok(()) - } -} - -#[derive(Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Output<'a> { - pub id: &'a str, - pub url: &'a str, - - #[serde(skip_serializing_if = "Option::is_none")] - pub provider_name: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - pub provider_url: Option<&'a str>, -} - -impl<'a> From<&'a Arc> for Output<'a> { - fn from(connection: &'a Arc) -> Self { - let id = connection.node.id_string.as_str(); - let url = connection.node.address.as_str(); - let provider_name = connection.node.provider.as_ref().map(|provider| provider.name.as_str()); - let provider_url = connection.node.provider.as_ref().map(|provider| provider.url.as_str()); - - // let provider_name = connection.node.provider.as_deref(); - // let provider_url = connection.node.link.as_deref(); - Self { id, url, provider_name, provider_url } - } -} diff --git a/rpc/wrpc/resolver/src/error.rs b/rpc/wrpc/resolver/src/error.rs deleted file mode 100644 index 4f390c3ce5..0000000000 --- a/rpc/wrpc/resolver/src/error.rs +++ /dev/null @@ -1,53 +0,0 @@ -use kaspa_wrpc_client::error::Error as RpcError; -use thiserror::Error; -use toml::de::Error as TomlError; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Custom(String), - - #[error("RPC error: {0}")] - Rpc(#[from] RpcError), - - #[error("TOML error: {0}")] - Toml(#[from] TomlError), - - #[error("IO Error: {0}")] - Io(#[from] std::io::Error), - - #[error(transparent)] - Serde(#[from] serde_json::Error), - - #[error("Connection Metrics")] - ConnectionMetrics, - #[error("Metrics")] - Metrics, - #[error("Sync")] - Sync, - #[error("Status")] - Status, - - #[error("Channel send error")] - ChannelSend, - #[error("Channel try send error")] - TryChannelSend, -} - -impl Error { - pub fn custom(msg: T) -> Self { - Error::Custom(msg.to_string()) - } -} - -impl From> for Error { - fn from(_: workflow_core::channel::SendError) -> Self { - Error::ChannelSend - } -} - -impl From> for Error { - fn from(_: workflow_core::channel::TrySendError) -> Self { - Error::TryChannelSend - } -} diff --git a/rpc/wrpc/resolver/src/imports.rs b/rpc/wrpc/resolver/src/imports.rs deleted file mode 100644 index 29c86a4813..0000000000 --- a/rpc/wrpc/resolver/src/imports.rs +++ /dev/null @@ -1,28 +0,0 @@ -pub use crate::args::Args; -pub use crate::error::Error; -pub use crate::log::*; -pub use crate::node::Node; -pub use crate::params::{PathParams, QueryParams}; -pub use crate::result::Result; -pub use crate::transport::Transport; -pub use ahash::AHashMap; -pub use cfg_if::cfg_if; -pub use futures::{pin_mut, select, FutureExt, StreamExt}; -pub use kaspa_consensus_core::network::NetworkId; -pub use kaspa_rpc_core::api::ctl::RpcState; -pub use kaspa_rpc_core::api::rpc::RpcApi; -pub use kaspa_utils::hashmap::GroupExtension; -pub use kaspa_wrpc_client::{ - client::{ConnectOptions, ConnectStrategy}, - KaspaRpcClient, WrpcEncoding, -}; -pub use serde::{de::DeserializeOwned, Deserialize, Serialize}; -pub use std::collections::HashMap; -pub use std::fmt; -pub use std::path::Path; -pub use std::sync::atomic::AtomicBool; -pub use std::sync::atomic::{AtomicU64, Ordering}; -pub use std::sync::{Arc, Mutex, OnceLock, RwLock}; -pub use std::time::Duration; -pub use workflow_core::channel::*; -pub use workflow_core::task::spawn; diff --git a/rpc/wrpc/resolver/src/log.rs b/rpc/wrpc/resolver/src/log.rs deleted file mode 100644 index 5f66416a0a..0000000000 --- a/rpc/wrpc/resolver/src/log.rs +++ /dev/null @@ -1,44 +0,0 @@ -pub mod impls { - use console::style; - use std::fmt; - - pub fn log_success(source: &str, args: &fmt::Arguments<'_>) { - println!("{:>12} {}", style(source).green().bold(), args); - } - - pub fn log_warn(source: &str, args: &fmt::Arguments<'_>) { - println!("{:>12} {}", style(source).yellow().bold(), args); - } - - pub fn log_error(source: &str, args: &fmt::Arguments<'_>) { - println!("{:>12} {}", style(source).red().bold(), args); - } -} - -#[macro_export] -macro_rules! log_success { - ($target:expr, $($t:tt)*) => ( - $crate::log::impls::log_success($target, &format_args!($($t)*)) - ) -} - -pub use log_success; - -#[macro_export] -macro_rules! log_warn { - - ($target:expr, $($t:tt)*) => ( - $crate::log::impls::log_warn($target, &format_args!($($t)*)) - ) -} - -pub use log_warn; - -#[macro_export] -macro_rules! log_error { - ($target:expr, $($t:tt)*) => ( - $crate::log::impls::log_error($target, &format_args!($($t)*)) - ) -} - -pub use log_error; diff --git a/rpc/wrpc/resolver/src/main.rs b/rpc/wrpc/resolver/src/main.rs deleted file mode 100644 index f071b12874..0000000000 --- a/rpc/wrpc/resolver/src/main.rs +++ /dev/null @@ -1,41 +0,0 @@ -mod args; -mod connection; -mod error; -pub mod imports; -mod log; -mod monitor; -mod node; -mod panic; -mod params; -mod result; -mod server; -mod transport; - -use args::*; -use result::Result; -use std::sync::Arc; - -#[tokio::main] -async fn main() { - if let Err(error) = run().await { - eprintln!("Error: {}", error); - std::process::exit(1); - } -} - -async fn run() -> Result<()> { - let args = Arc::new(Args::parse()); - - workflow_log::set_log_level(workflow_log::LevelFilter::Info); - panic::init_ungraceful_panic_handler(); - - println!(); - println!("Kaspa wRPC Resolver v{} starting...", env!("CARGO_PKG_VERSION")); - - monitor::init(&args); - let (listener, app) = server::server(&args).await?; - monitor::start().await?; - axum::serve(listener, app).await?; - monitor::stop().await?; - Ok(()) -} diff --git a/rpc/wrpc/resolver/src/monitor.rs b/rpc/wrpc/resolver/src/monitor.rs deleted file mode 100644 index 748a5148f3..0000000000 --- a/rpc/wrpc/resolver/src/monitor.rs +++ /dev/null @@ -1,241 +0,0 @@ -use crate::connection::{Connection, Descriptor}; -use crate::imports::*; - -static MONITOR: OnceLock> = OnceLock::new(); - -pub fn init(args: &Arc) { - MONITOR.set(Arc::new(Monitor::new(args))).unwrap(); -} - -pub fn monitor() -> &'static Arc { - MONITOR.get().unwrap() -} - -pub async fn start() -> Result<()> { - monitor().start().await -} - -pub async fn stop() -> Result<()> { - monitor().stop().await -} - -/// Monitor receives updates from [Connection] monitoring tasks -/// and updates the descriptors for each [Params] based on the -/// connection store (number of connections * bias). -pub struct Monitor { - args: Arc, - connections: RwLock>>>, - descriptors: RwLock>, - channel: Channel, - shutdown_ctl: DuplexChannel<()>, -} - -impl Default for Monitor { - fn default() -> Self { - Self { - args: Arc::new(Args::default()), - connections: Default::default(), - descriptors: Default::default(), - channel: Channel::unbounded(), - shutdown_ctl: DuplexChannel::oneshot(), - } - } -} - -impl fmt::Debug for Monitor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Monitor") - .field("verbose", &self.verbose()) - .field("connections", &self.connections) - .field("descriptors", &self.descriptors) - .finish() - } -} - -impl Monitor { - pub fn new(args: &Arc) -> Self { - Self { args: args.clone(), ..Default::default() } - } - - pub fn verbose(&self) -> bool { - self.args.verbose - } - - pub fn connections(&self) -> AHashMap>> { - self.connections.read().unwrap().clone() - } - - /// Process an update to `Server.toml` removing or adding node connections accordingly. - pub async fn update_nodes(&self, nodes: Vec>) -> Result<()> { - let mut connections = self.connections(); - - for params in PathParams::iter() { - let nodes = nodes.iter().filter(|node| node.params() == params).collect::>(); - - let list = connections.entry(params).or_default(); - - let create: Vec<_> = nodes.iter().filter(|node| !list.iter().any(|connection| connection.node == ***node)).collect(); - - let remove: Vec<_> = - list.iter().filter(|connection| !nodes.iter().any(|node| connection.node == **node)).cloned().collect(); - - for node in create { - let created = Arc::new(Connection::try_new((*node).clone(), self.channel.sender.clone(), &self.args)?); - created.start()?; - list.push(created); - } - - for removed in remove { - removed.stop().await?; - list.retain(|c| c.node != removed.node); - } - } - - *self.connections.write().unwrap() = connections; - - // flush all params to the update channel to refresh selected descriptors - PathParams::iter().for_each(|param| self.channel.sender.try_send(param).unwrap()); - - Ok(()) - } - - pub async fn start(self: &Arc) -> Result<()> { - let toml = std::fs::read_to_string(Path::new("Servers.toml"))?; - let nodes = crate::node::try_parse_nodes(toml.as_str())?; - - let this = self.clone(); - spawn(async move { - if let Err(error) = this.task().await { - println!("NodeConnection task error: {:?}", error); - } - }); - - self.update_nodes(nodes).await?; - - Ok(()) - } - - pub async fn stop(&self) -> Result<()> { - self.shutdown_ctl.signal(()).await.expect("Monitor shutdown signal error"); - Ok(()) - } - - async fn task(self: Arc) -> Result<()> { - let receiver = self.channel.receiver.clone(); - let shutdown_ctl_receiver = self.shutdown_ctl.request.receiver.clone(); - let shutdown_ctl_sender = self.shutdown_ctl.response.sender.clone(); - - loop { - select! { - - msg = receiver.recv().fuse() => { - match msg { - Ok(params) => { - - // run node elections - - let mut connections = self.connections() - .get(¶ms) - .expect("Monitor: expecting existing connection params") - .clone() - .into_iter() - .filter(|connection|connection.online()) - .collect::>(); - if connections.is_empty() { - self.descriptors.write().unwrap().remove(¶ms); - } else { - connections.sort_by_key(|connection| connection.score()); - - if self.args.election { - log_success!("",""); - connections.iter().for_each(|connection| { - log_warn!("Node","{}", connection); - }); - } - - if let Some(descriptor) = connections.first().unwrap().descriptor() { - let mut descriptors = self.descriptors.write().unwrap(); - - // extra debug output & monitoring - if self.args.verbose || self.args.election { - if let Some(current) = descriptors.get(¶ms) { - if current.connection.node.id != descriptor.connection.node.id { - log_success!("Election","{}", descriptor.connection); - descriptors.insert(params,descriptor); - } else { - log_success!("Keep","{}", descriptor.connection); - } - } else { - log_success!("Default","{}", descriptor.connection); - descriptors.insert(params,descriptor); - } - } else { - descriptors.insert(params,descriptor); - } - } - - if self.args.election && self.args.verbose { - log_success!("",""); - } - } - } - Err(err) => { - println!("Monitor: error while receiving update message: {err}"); - } - } - - } - _ = shutdown_ctl_receiver.recv().fuse() => { - break; - }, - - } - } - - shutdown_ctl_sender.send(()).await.unwrap(); - - Ok(()) - } - - /// Get the status of all nodes as a JSON string (available via `/status` endpoint if enabled). - pub fn get_all_json(&self) -> String { - let connections = self.connections(); - let nodes = connections.values().flatten().map(Status::from).collect::>(); - serde_json::to_string(&nodes).unwrap() - } - - /// Get JSON string representing node information (id, url, provider, link) - pub fn get_json(&self, params: &PathParams) -> Option { - self.descriptors.read().unwrap().get(params).cloned().map(|descriptor| descriptor.json) - } -} - -#[derive(Serialize)] -pub struct Status<'a> { - pub id: &'a str, - pub url: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - pub provider_name: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - pub provider_url: Option<&'a str>, - pub transport: Transport, - pub encoding: WrpcEncoding, - pub network: NetworkId, - pub online: bool, - pub status: &'static str, -} - -impl<'a> From<&'a Arc> for Status<'a> { - fn from(connection: &'a Arc) -> Self { - let url = connection.node.address.as_str(); - let provider_name = connection.node.provider.as_ref().map(|provider| provider.name.as_str()); - let provider_url = connection.node.provider.as_ref().map(|provider| provider.url.as_str()); - let id = connection.node.id_string.as_str(); - let transport = connection.node.transport; - let encoding = connection.node.encoding; - let network = connection.node.network; - let status = connection.status(); - let online = connection.online(); - Self { id, url, provider_name, provider_url, transport, encoding, network, status, online } - } -} diff --git a/rpc/wrpc/resolver/src/node.rs b/rpc/wrpc/resolver/src/node.rs deleted file mode 100644 index d0968966cf..0000000000 --- a/rpc/wrpc/resolver/src/node.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::imports::*; -use xxhash_rust::xxh3::xxh3_64; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Provider { - pub name: String, - pub url: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Node { - #[serde(skip)] - pub id: u64, - #[serde(skip)] - pub id_string: String, - - pub name: Option, - pub location: Option, - pub address: String, - pub transport: Transport, - pub encoding: WrpcEncoding, - pub network: NetworkId, - pub enable: Option, - pub bias: Option, - pub version: Option, - pub provider: Option, -} - -impl Eq for Node {} - -impl PartialEq for Node { - fn eq(&self, other: &Self) -> bool { - self.address == other.address - } -} - -impl std::fmt::Display for Node { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let title = self.name.clone().unwrap_or(self.address.to_string()); - write!(f, "{}", title) - } -} - -impl Node { - pub fn params(&self) -> PathParams { - PathParams::new(self.encoding, self.network) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct NodeConfig { - #[serde(rename = "node")] - nodes: Vec, -} - -pub fn try_parse_nodes(toml: &str) -> Result>> { - let nodes: Vec> = toml::from_str::(toml)? - .nodes - .into_iter() - .filter_map(|mut node| { - let id = xxh3_64(node.address.as_bytes()); - let id_string = format!("{id:x}"); - node.id = id; - node.id_string = id_string.chars().take(8).collect(); - node.enable.unwrap_or(true).then_some(node).map(Arc::new) - }) - .collect::>(); - Ok(nodes) -} - -impl AsRef for Node { - fn as_ref(&self) -> &Node { - self - } -} diff --git a/rpc/wrpc/resolver/src/panic.rs b/rpc/wrpc/resolver/src/panic.rs deleted file mode 100644 index 7e6d78a1ff..0000000000 --- a/rpc/wrpc/resolver/src/panic.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::panic; - -pub fn init_ungraceful_panic_handler() { - let default_hook = panic::take_hook(); - panic::set_hook(Box::new(move |panic_info| { - default_hook(panic_info); - println!("Exiting..."); - std::process::exit(1); - })); -} diff --git a/rpc/wrpc/resolver/src/params.rs b/rpc/wrpc/resolver/src/params.rs deleted file mode 100644 index 7e31b69e70..0000000000 --- a/rpc/wrpc/resolver/src/params.rs +++ /dev/null @@ -1,146 +0,0 @@ -use serde::{de, Deserializer, Serializer}; - -use crate::imports::*; -use std::{fmt, str::FromStr}; -// use convert_case::{Case, Casing}; - -#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)] -pub struct PathParams { - pub encoding: WrpcEncoding, - pub network: NetworkId, -} - -impl PathParams { - pub fn new(encoding: WrpcEncoding, network: NetworkId) -> Self { - Self { encoding, network } - } - - pub fn iter() -> impl Iterator { - NetworkId::iter().flat_map(move |network_id| WrpcEncoding::iter().map(move |encoding| PathParams::new(*encoding, network_id))) - } -} - -impl fmt::Display for PathParams { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}:{}", self.encoding.to_string().to_lowercase(), self.network) - } -} - -// --- - -#[derive(Debug, Deserialize)] -pub struct QueryParams { - // Accessible via a query string like "?access=utxo-index+tx-index+block-dag+metrics+visualizer+mining" - pub access: Option, -} - -#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)] -#[serde(rename_all = "kebab-case")] -pub enum AccessType { - Transact, // UTXO and TX index, submit transaction, single mempool entry - Mempool, // Full mempool data access - BlockDag, // Access to Blocks - Network, // Network data access (peers, ban, etc.) - Metrics, // Access to Metrics - Visualizer, // Access to Visualization data feeds - Mining, // Access to submit block, GBT, etc. -} - -impl AccessType { - pub fn iter() -> impl Iterator { - [ - AccessType::Transact, - AccessType::Mempool, - AccessType::BlockDag, - AccessType::Network, - AccessType::Metrics, - AccessType::Visualizer, - AccessType::Mining, - ] - .into_iter() - } -} - -impl fmt::Display for AccessType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - AccessType::Transact => "transact", - AccessType::Mempool => "mempool", - AccessType::BlockDag => "block-dag", - AccessType::Network => "network", - AccessType::Metrics => "metrics", - AccessType::Visualizer => "visualizer", - AccessType::Mining => "mining", - }; - write!(f, "{s}") - } -} - -impl FromStr for AccessType { - type Err = String; - fn from_str(s: &str) -> std::result::Result { - match s { - "transact" => Ok(AccessType::Transact), - "mempool" => Ok(AccessType::Mempool), - "block-dag" => Ok(AccessType::BlockDag), - "network" => Ok(AccessType::Network), - "metrics" => Ok(AccessType::Metrics), - "visualizer" => Ok(AccessType::Visualizer), - "mining" => Ok(AccessType::Mining), - _ => Err(format!("Invalid access type: {}", s)), - } - } -} - -#[derive(Debug, Clone)] -pub struct AccessList { - pub access: Vec, -} - -impl std::fmt::Display for AccessList { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.access.iter().map(|access| access.to_string()).collect::>().join(" ")) - } -} - -impl FromStr for AccessList { - type Err = String; - - fn from_str(s: &str) -> std::result::Result { - let access = s.split(' ').map(|s| s.parse::()).collect::, _>>()?; - Ok(AccessList { access }) - } -} - -impl Serialize for AccessList { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -struct AccessListVisitor; -impl<'de> de::Visitor<'de> for AccessListVisitor { - type Value = AccessList; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string containing list of permissions separated by a '+'") - } - - fn visit_str(self, value: &str) -> std::result::Result - where - E: de::Error, - { - AccessList::from_str(value).map_err(|err| de::Error::custom(err.to_string())) - } -} - -impl<'de> Deserialize<'de> for AccessList { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_str(AccessListVisitor) - } -} diff --git a/rpc/wrpc/resolver/src/result.rs b/rpc/wrpc/resolver/src/result.rs deleted file mode 100644 index 605dc25cfc..0000000000 --- a/rpc/wrpc/resolver/src/result.rs +++ /dev/null @@ -1 +0,0 @@ -pub type Result = std::result::Result; diff --git a/rpc/wrpc/resolver/src/server.rs b/rpc/wrpc/resolver/src/server.rs deleted file mode 100644 index 3717a6ebf4..0000000000 --- a/rpc/wrpc/resolver/src/server.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::imports::*; -use crate::monitor::monitor; -use axum::{ - async_trait, - extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts, Query}, - http::{header, request::Parts, HeaderValue, StatusCode}, - response::IntoResponse, - routing::get, - // Json, - Router, -}; -use tokio::net::TcpListener; - -use axum::{error_handling::HandleErrorLayer, BoxError}; -use std::time::Duration; -use tower::{buffer::BufferLayer, limit::RateLimitLayer, ServiceBuilder}; -use tower_http::cors::{Any, CorsLayer}; - -pub async fn server(args: &Args) -> Result<(TcpListener, Router)> { - // initialize tracing - tracing_subscriber::fmt::init(); - - let app = Router::new().route("/v1/wrpc/:encoding/:network", get(get_elected_node)); - - let app = if args.status { - log_warn!("Routes", "Enabling `/status` route"); - app.route("/status", get(get_status_all_nodes)) - } else { - log_success!("Routes", "Disabling `/status` route"); - app - }; - - let app = if let Some(rate_limit) = args.rate_limit.as_ref() { - log_success!("Limits", "Setting rate limit to: {} requests per {} seconds", rate_limit.requests, rate_limit.period); - app.layer( - ServiceBuilder::new() - .layer(HandleErrorLayer::new(|err: BoxError| async move { - (StatusCode::INTERNAL_SERVER_ERROR, format!("Unhandled error: {}", err)) - })) - .layer(BufferLayer::new(1024)) - .layer(RateLimitLayer::new(rate_limit.requests, Duration::from_secs(rate_limit.period))), - ) - } else { - log_warn!("Limits", "Rate limit is disabled"); - app - }; - - let app = app.layer(CorsLayer::new().allow_origin(Any)); - - log_success!("Server", "Listening on http://{}", args.listen.as_str()); - let listener = tokio::net::TcpListener::bind(args.listen.as_str()).await.unwrap(); - Ok((listener, app)) -} - -// respond with a JSON object containing the status of all nodes -async fn get_status_all_nodes() -> impl IntoResponse { - let json = monitor().get_all_json(); - (StatusCode::OK, [(header::CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()))], json).into_response() -} - -// respond with a JSON object containing the elected node -async fn get_elected_node(Query(_query): Query, Path(params): Path) -> impl IntoResponse { - // println!("params: {:?}", params); - // println!("query: {:?}", query); - - if let Some(json) = monitor().get_json(¶ms) { - ([(header::CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()))], json).into_response() - } else { - ( - StatusCode::NOT_FOUND, - [(header::CONTENT_TYPE, HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()))], - "NOT FOUND".to_string(), - ) - .into_response() - } -} - -// We define our own `Path` extractor that customizes the error from `axum::extract::Path` -struct Path(T); - -#[async_trait] -impl FromRequestParts for Path -where - // these trait bounds are copied from `impl FromRequest for axum::extract::path::Path` - T: DeserializeOwned + Send, - S: Send + Sync, -{ - type Rejection = (StatusCode, axum::Json); - - async fn from_request_parts(parts: &mut Parts, state: &S) -> std::result::Result { - match axum::extract::Path::::from_request_parts(parts, state).await { - Ok(value) => Ok(Self(value.0)), - Err(rejection) => { - let (status, body) = match rejection { - PathRejection::FailedToDeserializePathParams(inner) => { - let mut status = StatusCode::BAD_REQUEST; - - let kind = inner.into_kind(); - let body = match &kind { - ErrorKind::WrongNumberOfParameters { .. } => PathError { message: kind.to_string(), location: None }, - - ErrorKind::ParseErrorAtKey { key, .. } => { - PathError { message: kind.to_string(), location: Some(key.clone()) } - } - - ErrorKind::ParseErrorAtIndex { index, .. } => { - PathError { message: kind.to_string(), location: Some(index.to_string()) } - } - - ErrorKind::ParseError { .. } => PathError { message: kind.to_string(), location: None }, - - ErrorKind::InvalidUtf8InPathParam { key } => { - PathError { message: kind.to_string(), location: Some(key.clone()) } - } - - ErrorKind::UnsupportedType { .. } => { - // this error is caused by the programmer using an unsupported type - // (such as nested maps) so respond with `500` instead - status = StatusCode::INTERNAL_SERVER_ERROR; - PathError { message: kind.to_string(), location: None } - } - - ErrorKind::Message(msg) => PathError { message: msg.clone(), location: None }, - - _ => PathError { message: format!("Unhandled deserialization error: {kind}"), location: None }, - }; - - (status, body) - } - PathRejection::MissingPathParams(error) => { - (StatusCode::INTERNAL_SERVER_ERROR, PathError { message: error.to_string(), location: None }) - } - _ => ( - StatusCode::INTERNAL_SERVER_ERROR, - PathError { message: format!("Unhandled path rejection: {rejection}"), location: None }, - ), - }; - - Err((status, axum::Json(body))) - } - } - } -} - -#[derive(Serialize)] -struct PathError { - message: String, - location: Option, -} diff --git a/rpc/wrpc/resolver/src/transport.rs b/rpc/wrpc/resolver/src/transport.rs deleted file mode 100644 index ccfd6dee73..0000000000 --- a/rpc/wrpc/resolver/src/transport.rs +++ /dev/null @@ -1,8 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Transport { - Grpc, - Wrpc, -} diff --git a/rpc/wrpc/server/Cargo.toml b/rpc/wrpc/server/Cargo.toml index 885a01b751..f0f55aed75 100644 --- a/rpc/wrpc/server/Cargo.toml +++ b/rpc/wrpc/server/Cargo.toml @@ -33,6 +33,7 @@ tokio.workspace = true workflow-core.workspace = true workflow-log.workspace = true workflow-rpc.workspace = true +workflow-serializer.workspace = true [target.x86_64-unknown-linux-gnu.dependencies] # Adding explicitely the openssl dependency here is needed for a successful build with zigbuild diff --git a/rpc/wrpc/server/src/address.rs b/rpc/wrpc/server/src/address.rs index 81ccabe5dd..1100860e26 100644 --- a/rpc/wrpc/server/src/address.rs +++ b/rpc/wrpc/server/src/address.rs @@ -43,7 +43,6 @@ impl WrpcNetAddress { } } } - impl FromStr for WrpcNetAddress { type Err = AddrParseError; fn from_str(s: &str) -> Result { diff --git a/rpc/wrpc/server/src/connection.rs b/rpc/wrpc/server/src/connection.rs index 86345e5d58..97a4e8adce 100644 --- a/rpc/wrpc/server/src/connection.rs +++ b/rpc/wrpc/server/src/connection.rs @@ -16,6 +16,7 @@ use workflow_rpc::{ server::{prelude::*, result::Result as WrpcResult}, types::{MsgT, OpsT}, }; +use workflow_serializer::prelude::*; // // FIXME: Use workflow_rpc::encoding::Encoding directly in the ConnectionT implementation by deriving Hash, Eq and PartialEq in situ @@ -157,7 +158,7 @@ impl ConnectionT for Connection { fn into_message(notification: &Self::Notification, encoding: &Self::Encoding) -> Self::Message { let op: RpcApiOps = notification.event_type().into(); - Self::create_serialized_notification_message(encoding.clone().into(), op, notification.clone()).unwrap() + Self::create_serialized_notification_message(encoding.clone().into(), op, Serializable(notification.clone())).unwrap() } async fn send(&self, message: Self::Message) -> core::result::Result<(), Self::Error> { diff --git a/rpc/wrpc/server/src/router.rs b/rpc/wrpc/server/src/router.rs index 09330eb498..82590dca1e 100644 --- a/rpc/wrpc/server/src/router.rs +++ b/rpc/wrpc/server/src/router.rs @@ -4,6 +4,7 @@ use kaspa_rpc_core::{api::ops::RpcApiOps, prelude::*}; use kaspa_rpc_macros::build_wrpc_server_interface; use std::sync::Arc; use workflow_rpc::server::prelude::*; +use workflow_serializer::prelude::*; /// A wrapper that creates an [`Interface`] instance and initializes /// RPC methods and notifications against this interface. The interface @@ -32,6 +33,7 @@ impl Router { Connection, RpcApiOps, [ + Ping, AddPeer, Ban, EstimateNetworkHashesPerSecond, @@ -55,15 +57,16 @@ impl Router { GetMempoolEntriesByAddresses, GetMempoolEntry, GetMetrics, + GetConnections, GetPeerAddresses, GetServerInfo, GetSink, GetSinkBlueScore, GetSubnetwork, GetSyncStatus, + GetSystemInfo, GetUtxosByAddresses, GetVirtualChainFromBlock, - Ping, ResolveFinalityConflict, Shutdown, SubmitBlock, @@ -75,22 +78,22 @@ impl Router { interface.method( RpcApiOps::Subscribe, - workflow_rpc::server::Method::new(move |manager: Server, connection: Connection, scope: Scope| { + workflow_rpc::server::Method::new(move |manager: Server, connection: Connection, scope: Serializable| { Box::pin(async move { - manager.start_notify(&connection, scope).await.map_err(|err| err.to_string())?; - Ok(SubscribeResponse::new(connection.id())) + manager.start_notify(&connection, scope.into_inner()).await.map_err(|err| err.to_string())?; + Ok(Serializable(SubscribeResponse::new(connection.id()))) }) }), ); interface.method( RpcApiOps::Unsubscribe, - workflow_rpc::server::Method::new(move |manager: Server, connection: Connection, scope: Scope| { + workflow_rpc::server::Method::new(move |manager: Server, connection: Connection, scope: Serializable| { Box::pin(async move { - manager.stop_notify(&connection, scope).await.unwrap_or_else(|err| { + manager.stop_notify(&connection, scope.into_inner()).await.unwrap_or_else(|err| { workflow_log::log_trace!("wRPC server -> error calling stop_notify(): {err}"); }); - Ok(UnsubscribeResponse {}) + Ok(Serializable(UnsubscribeResponse {})) }) }), ); diff --git a/rpc/wrpc/server/src/service.rs b/rpc/wrpc/server/src/service.rs index 898ac5e295..72d09f6e67 100644 --- a/rpc/wrpc/server/src/service.rs +++ b/rpc/wrpc/server/src/service.rs @@ -123,6 +123,7 @@ impl WrpcService { rpc_handler.clone(), router.interface.clone(), Some(counters), + false, ); WrpcService { options, server, rpc_handler, shutdown: SingleTrigger::default() } @@ -146,10 +147,15 @@ impl WrpcService { info!("WRPC Server starting on: {}", listen_address); tokio::spawn(async move { let config = WebSocketConfig { max_message_size: Some(MAX_WRPC_MESSAGE_SIZE), ..Default::default() }; - let serve_result = self.server.listen(&listen_address, Some(config)).await; - match serve_result { - Ok(_) => info!("WRPC Server stopped on: {}", listen_address), - Err(err) => panic!("WRPC Server {listen_address} stopped with error: {err:?}"), + match self.server.bind(&listen_address).await { + Ok(listener) => { + let serve_result = self.server.listen(listener, Some(config)).await; + match serve_result { + Ok(_) => info!("WRPC Server stopped on: {}", listen_address), + Err(err) => panic!("WRPC Server {listen_address} stopped with error: {err:?}"), + } + } + Err(err) => panic!("WRPC Server bind error on {listen_address}: {err:?}"), } }); diff --git a/rpc/wrpc/wasm/Cargo.toml b/rpc/wrpc/wasm/Cargo.toml index 83c78d26f8..9bd639fdb1 100644 --- a/rpc/wrpc/wasm/Cargo.toml +++ b/rpc/wrpc/wasm/Cargo.toml @@ -41,5 +41,5 @@ wasm-bindgen-futures.workspace = true workflow-core.workspace = true futures.workspace = true -[lints.clippy] -empty_docs = "allow" +[lints] +workspace = true diff --git a/rpc/wrpc/wasm/src/client.rs b/rpc/wrpc/wasm/src/client.rs index 35cabd8d84..631af0454f 100644 --- a/rpc/wrpc/wasm/src/client.rs +++ b/rpc/wrpc/wasm/src/client.rs @@ -364,19 +364,7 @@ impl RpcClient { /// Optional: Resolver node id. #[wasm_bindgen(getter, js_name = "nodeId")] pub fn resolver_node_id(&self) -> Option { - self.inner.client.node_descriptor().map(|node| node.id.clone()) - } - - /// Optional: public node provider name. - #[wasm_bindgen(getter, js_name = "providerName")] - pub fn resolver_node_provider_name(&self) -> Option { - self.inner.client.node_descriptor().and_then(|node| node.provider_name.clone()) - } - - /// Optional: public node provider URL. - #[wasm_bindgen(getter, js_name = "providerUrl")] - pub fn resolver_node_provider_url(&self) -> Option { - self.inner.client.node_descriptor().and_then(|node| node.provider_url.clone()) + self.inner.client.node_descriptor().map(|node| node.uid.clone()) } /// Connect to the Kaspa RPC server. This function starts a background @@ -796,7 +784,7 @@ impl RpcClient { #[wasm_bindgen(js_name = subscribeVirtualDaaScoreChanged)] pub async fn subscribe_daa_score(&self) -> Result<()> { if let Some(listener_id) = self.listener_id() { - self.inner.client.stop_notify(listener_id, Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; + self.inner.client.start_notify(listener_id, Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; } else { log_error!("RPC unsubscribe on a closed connection"); } @@ -957,6 +945,8 @@ build_wrpc_wasm_bindgen_interface!( /// performance and status of the Kaspa node. /// Returned information: Memory usage, CPU usage, network activity. GetMetrics, + /// Retrieves current number of network connections + GetConnections, /// Retrieves the current sink block, which is the block with /// the highest cumulative difficulty in the Kaspa BlockDAG. /// Returned information: Sink block hash, sink block height. @@ -1010,6 +1000,10 @@ build_wrpc_wasm_bindgen_interface!( /// score timestamp estimate. /// Returned information: DAA score timestamp estimate. GetDaaScoreTimestampEstimate, + /// Feerate estimates + GetFeeEstimate, + /// Feerate estimates (experimental) + GetFeeEstimateExperimental, /// Retrieves the current network configuration. /// Returned information: Current network configuration. GetCurrentNetwork, @@ -1042,8 +1036,11 @@ build_wrpc_wasm_bindgen_interface!( /// Returned information: None. SubmitBlock, /// Submits a transaction to the Kaspa network. - /// Returned information: None. + /// Returned information: Submitted Transaction Id. SubmitTransaction, + /// Submits an RBF transaction to the Kaspa network. + /// Returned information: Submitted Transaction Id, Transaction that was replaced. + SubmitTransactionReplacement, /// Unbans a previously banned peer, allowing it to connect /// to the Kaspa node again. /// Returned information: None. diff --git a/rpc/wrpc/wasm/src/resolver.rs b/rpc/wrpc/wasm/src/resolver.rs index ee4b5d883e..2ffc7ea56a 100644 --- a/rpc/wrpc/wasm/src/resolver.rs +++ b/rpc/wrpc/wasm/src/resolver.rs @@ -21,6 +21,20 @@ declare! { * Optional URLs for one or multiple resolvers. */ urls?: string[]; + /** + * Use strict TLS for RPC connections. + * If not set or `false` (default), the resolver will + * provide the best available connection regardless of + * whether this connection supports TLS or not. + * If set to `true`, the resolver will only provide + * TLS-enabled connections. + * + * This setting is ignored in the browser environment + * when the browser navigator location is `https`. + * In which case the resolver will always use TLS-enabled + * connections. + */ + tls?: boolean; } "#, } @@ -130,8 +144,8 @@ impl Resolver { impl Resolver { /// List of public Kaspa Resolver URLs. #[wasm_bindgen(getter)] - pub fn urls(&self) -> ResolverArrayT { - Array::from_iter(self.resolver.urls().iter().map(|v| JsValue::from(v.as_str()))).unchecked_into() + pub fn urls(&self) -> Option { + self.resolver.urls().map(|urls| Array::from_iter(urls.iter().map(|v| JsValue::from(v.as_str()))).unchecked_into()) } /// Fetches a public Kaspa wRPC endpoint for the given encoding and network identifier. @@ -163,20 +177,27 @@ impl Resolver { impl TryFrom for NativeResolver { type Error = Error; fn try_from(config: IResolverConfig) -> Result { - let resolver = config + let tls = config.get_bool("tls").unwrap_or(false); + let urls = config .get_vec("urls") .map(|urls| urls.into_iter().map(|v| v.as_string()).collect::>>()) .or_else(|_| config.dyn_into::().map(|urls| urls.into_iter().map(|v| v.as_string()).collect::>>())) - .map_err(|_| Error::custom("Invalid or missing resolver URL"))? - .map(|urls| NativeResolver::new(urls.into_iter().map(Arc::new).collect())); + .map_err(|_| Error::custom("Invalid or missing resolver URL"))?; - Ok(resolver.unwrap_or_default()) + if let Some(urls) = urls { + Ok(NativeResolver::new(Some(urls.into_iter().map(Arc::new).collect()), tls)) + } else { + Ok(NativeResolver::new(None, tls)) + } } } impl TryCastFromJs for Resolver { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result> { + fn try_cast_from<'a, R>(value: &'a R) -> Result> + where + R: AsRef + 'a, + { Ok(Self::try_ref_from_js_value_as_cast(value)?) } } diff --git a/testing/integration/src/common/utils.rs b/testing/integration/src/common/utils.rs index cb87a98c83..10fb9cb671 100644 --- a/testing/integration/src/common/utils.rs +++ b/testing/integration/src/common/utils.rs @@ -3,6 +3,7 @@ use itertools::Itertools; use kaspa_addresses::Address; use kaspa_consensus_core::{ constants::TX_VERSION, + header::Header, sign::sign, subnets::SUBNETWORK_ID_NATIVE, tx::{ @@ -16,7 +17,7 @@ use kaspa_consensus_core::{ }; use kaspa_core::info; use kaspa_grpc_client::GrpcClient; -use kaspa_rpc_core::{api::rpc::RpcApi, BlockAddedNotification, Notification, VirtualDaaScoreChangedNotification}; +use kaspa_rpc_core::{api::rpc::RpcApi, BlockAddedNotification, Notification, RpcUtxoEntry, VirtualDaaScoreChangedNotification}; use kaspa_txscript::pay_to_address_script; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use secp256k1::Keypair; @@ -170,13 +171,13 @@ pub async fn fetch_spendable_utxos( { assert!(resp_entry.address.is_some()); assert_eq!(*resp_entry.address.as_ref().unwrap(), address); - utxos.push((resp_entry.outpoint, resp_entry.utxo_entry)); + utxos.push((TransactionOutpoint::from(resp_entry.outpoint), UtxoEntry::from(resp_entry.utxo_entry))); } utxos.sort_by(|a, b| b.1.amount.cmp(&a.1.amount)); utxos } -pub fn is_utxo_spendable(entry: &UtxoEntry, virtual_daa_score: u64, coinbase_maturity: u64) -> bool { +pub fn is_utxo_spendable(entry: &RpcUtxoEntry, virtual_daa_score: u64, coinbase_maturity: u64) -> bool { let needed_confirmations = if !entry.is_coinbase { 10 } else { coinbase_maturity }; entry.block_daa_score + needed_confirmations <= virtual_daa_score } @@ -187,7 +188,8 @@ pub async fn mine_block(pay_address: Address, submitting_client: &GrpcClient, li // Mine a block let template = submitting_client.get_block_template(pay_address.clone(), vec![]).await.unwrap(); - let block_hash = template.block.header.hash; + let header: Header = (&template.block.header).into(); + let block_hash = header.hash; submitting_client.submit_block(template.block, false).await.unwrap(); // Wait for each listening client to get notified the submitted block was added to the DAG diff --git a/testing/integration/src/daemon_integration_tests.rs b/testing/integration/src/daemon_integration_tests.rs index 29f74e75ee..a923202942 100644 --- a/testing/integration/src/daemon_integration_tests.rs +++ b/testing/integration/src/daemon_integration_tests.rs @@ -7,6 +7,7 @@ use crate::common::{ use kaspa_addresses::Address; use kaspa_alloc::init_allocator_with_default_settings; use kaspa_consensus::params::SIMNET_PARAMS; +use kaspa_consensus_core::header::Header; use kaspa_consensusmanager::ConsensusManager; use kaspa_core::{task::runtime::AsyncRuntime, trace}; use kaspa_grpc_client::GrpcClient; @@ -77,7 +78,8 @@ async fn daemon_mining_test() { .get_block_template(Address::new(kaspad1.network.into(), kaspa_addresses::Version::PubKey, &[0; 32]), vec![]) .await .unwrap(); - last_block_hash = Some(template.block.header.hash); + let header: Header = (&template.block.header).into(); + last_block_hash = Some(header.hash); rpc_client1.submit_block(template.block, false).await.unwrap(); while let Ok(notification) = match tokio::time::timeout(Duration::from_secs(1), event_receiver.recv()).await { @@ -180,7 +182,8 @@ async fn daemon_utxos_propagation_test() { let mut last_block_hash = None; for i in 0..initial_blocks { let template = rpc_client1.get_block_template(miner_address.clone(), vec![]).await.unwrap(); - last_block_hash = Some(template.block.header.hash); + let header: Header = (&template.block.header).into(); + last_block_hash = Some(header.hash); rpc_client1.submit_block(template.block, false).await.unwrap(); while let Ok(notification) = match tokio::time::timeout(Duration::from_secs(1), event_receiver1.recv()).await { diff --git a/testing/integration/src/rpc_tests.rs b/testing/integration/src/rpc_tests.rs index dc26b539f9..c4e5b4ce0f 100644 --- a/testing/integration/src/rpc_tests.rs +++ b/testing/integration/src/rpc_tests.rs @@ -4,7 +4,7 @@ use crate::common::{client_notify::ChannelNotify, daemon::Daemon}; use futures_util::future::try_join_all; use kaspa_addresses::{Address, Prefix, Version}; use kaspa_consensus::params::SIMNET_GENESIS; -use kaspa_consensus_core::{constants::MAX_SOMPI, subnets::SubnetworkId, tx::Transaction}; +use kaspa_consensus_core::{constants::MAX_SOMPI, header::Header, subnets::SubnetworkId, tx::Transaction}; use kaspa_core::info; use kaspa_grpc_core::ops::KaspadPayloadOps; use kaspa_hashes::Hash; @@ -64,7 +64,7 @@ async fn sanity_test() { // The intent of this for/match design (emphasizing the absence of an arm with fallback pattern in the match) // is to force any implementor of a new RpcApi method to add a matching arm here and to strongly incentivize // the adding of an actual sanity test of said new method. - for op in KaspadPayloadOps::list() { + for op in KaspadPayloadOps::iter() { let network_id = daemon.network; let task: JoinHandle<()> = match op { KaspadPayloadOps::SubmitBlock => { @@ -79,21 +79,24 @@ async fn sanity_test() { .unwrap(); // Before submitting a first block, the sink is the genesis, - let response = rpc_client.get_sink_call(GetSinkRequest {}).await.unwrap(); + let response = rpc_client.get_sink_call(None, GetSinkRequest {}).await.unwrap(); assert_eq!(response.sink, SIMNET_GENESIS.hash); - let response = rpc_client.get_sink_blue_score_call(GetSinkBlueScoreRequest {}).await.unwrap(); + let response = rpc_client.get_sink_blue_score_call(None, GetSinkBlueScoreRequest {}).await.unwrap(); assert_eq!(response.blue_score, 0); // the block count is 0 - let response = rpc_client.get_block_count_call(GetBlockCountRequest {}).await.unwrap(); + let response = rpc_client.get_block_count_call(None, GetBlockCountRequest {}).await.unwrap(); assert_eq!(response.block_count, 0); // and the virtual chain is the genesis only let response = rpc_client - .get_virtual_chain_from_block_call(GetVirtualChainFromBlockRequest { - start_hash: SIMNET_GENESIS.hash, - include_accepted_transaction_ids: false, - }) + .get_virtual_chain_from_block_call( + None, + GetVirtualChainFromBlockRequest { + start_hash: SIMNET_GENESIS.hash, + include_accepted_transaction_ids: false, + }, + ) .await .unwrap(); assert!(response.added_chain_block_hashes.is_empty()); @@ -101,14 +104,21 @@ async fn sanity_test() { // Get a block template let GetBlockTemplateResponse { block, is_synced } = rpc_client - .get_block_template_call(GetBlockTemplateRequest { - pay_address: Address::new(Prefix::Simnet, Version::PubKey, &[0u8; 32]), - extra_data: Vec::new(), - }) + .get_block_template_call( + None, + GetBlockTemplateRequest { + pay_address: Address::new(Prefix::Simnet, Version::PubKey, &[0u8; 32]), + extra_data: Vec::new(), + }, + ) .await .unwrap(); assert!(!is_synced); + // Compute the expected block hash for the received block + let header: Header = (&block.header).into(); + let block_hash = header.hash; + // Submit the template (no mining, in simnet PoW is skipped) let response = rpc_client.submit_block(block.clone(), false).await.unwrap(); assert_eq!(response.report, SubmitBlockReport::Success); @@ -131,22 +141,25 @@ async fn sanity_test() { } // After submitting a first block, the sink is the submitted block, - let response = rpc_client.get_sink_call(GetSinkRequest {}).await.unwrap(); - assert_eq!(response.sink, block.header.hash); + let response = rpc_client.get_sink_call(None, GetSinkRequest {}).await.unwrap(); + assert_eq!(response.sink, block_hash); // the block count is 1 - let response = rpc_client.get_block_count_call(GetBlockCountRequest {}).await.unwrap(); + let response = rpc_client.get_block_count_call(None, GetBlockCountRequest {}).await.unwrap(); assert_eq!(response.block_count, 1); // and the virtual chain from genesis contains the added block let response = rpc_client - .get_virtual_chain_from_block_call(GetVirtualChainFromBlockRequest { - start_hash: SIMNET_GENESIS.hash, - include_accepted_transaction_ids: false, - }) + .get_virtual_chain_from_block_call( + None, + GetVirtualChainFromBlockRequest { + start_hash: SIMNET_GENESIS.hash, + include_accepted_transaction_ids: false, + }, + ) .await .unwrap(); - assert!(response.added_chain_block_hashes.contains(&block.header.hash)); + assert!(response.added_chain_block_hashes.contains(&block_hash)); assert!(response.removed_chain_block_hashes.is_empty()); }) } @@ -158,7 +171,7 @@ async fn sanity_test() { KaspadPayloadOps::GetCurrentNetwork => { let rpc_client = client.clone(); tst!(op, { - let response = rpc_client.get_current_network_call(GetCurrentNetworkRequest {}).await.unwrap(); + let response = rpc_client.get_current_network_call(None, GetCurrentNetworkRequest {}).await.unwrap(); assert_eq!(response.network, network_id.network_type); }) } @@ -166,11 +179,12 @@ async fn sanity_test() { KaspadPayloadOps::GetBlock => { let rpc_client = client.clone(); tst!(op, { - let result = rpc_client.get_block_call(GetBlockRequest { hash: 0.into(), include_transactions: false }).await; + let result = + rpc_client.get_block_call(None, GetBlockRequest { hash: 0.into(), include_transactions: false }).await; assert!(result.is_err()); let response = rpc_client - .get_block_call(GetBlockRequest { hash: SIMNET_GENESIS.hash, include_transactions: false }) + .get_block_call(None, GetBlockRequest { hash: SIMNET_GENESIS.hash, include_transactions: false }) .await .unwrap(); assert_eq!(response.block.header.hash, SIMNET_GENESIS.hash); @@ -181,7 +195,7 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let response = rpc_client - .get_blocks_call(GetBlocksRequest { include_blocks: true, include_transactions: false, low_hash: None }) + .get_blocks_call(None, GetBlocksRequest { include_blocks: true, include_transactions: false, low_hash: None }) .await .unwrap(); assert_eq!(response.blocks.len(), 1, "genesis block should be returned"); @@ -193,7 +207,7 @@ async fn sanity_test() { KaspadPayloadOps::GetInfo => { let rpc_client = client.clone(); tst!(op, { - let response = rpc_client.get_info_call(GetInfoRequest {}).await.unwrap(); + let response = rpc_client.get_info_call(None, GetInfoRequest {}).await.unwrap(); assert_eq!(response.server_version, kaspa_core::kaspad_env::version().to_string()); assert_eq!(response.mempool_size, 0); assert!(response.is_utxo_indexed); @@ -220,11 +234,14 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let response_result = rpc_client - .get_mempool_entry_call(GetMempoolEntryRequest { - transaction_id: 0.into(), - include_orphan_pool: true, - filter_transaction_pool: false, - }) + .get_mempool_entry_call( + None, + GetMempoolEntryRequest { + transaction_id: 0.into(), + include_orphan_pool: true, + filter_transaction_pool: false, + }, + ) .await; // Test Get Mempool Entry: // TODO: Fix by adding actual mempool entries this can get because otherwise it errors out @@ -236,10 +253,10 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let response = rpc_client - .get_mempool_entries_call(GetMempoolEntriesRequest { - include_orphan_pool: true, - filter_transaction_pool: false, - }) + .get_mempool_entries_call( + None, + GetMempoolEntriesRequest { include_orphan_pool: true, filter_transaction_pool: false }, + ) .await .unwrap(); assert!(response.mempool_entries.is_empty()); @@ -249,7 +266,7 @@ async fn sanity_test() { KaspadPayloadOps::GetConnectedPeerInfo => { let rpc_client = client.clone(); tst!(op, { - let response = rpc_client.get_connected_peer_info_call(GetConnectedPeerInfoRequest {}).await.unwrap(); + let response = rpc_client.get_connected_peer_info_call(None, GetConnectedPeerInfoRequest {}).await.unwrap(); assert!(response.peer_info.is_empty()); }) } @@ -258,12 +275,12 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let peer_address = ContextualNetAddress::from_str("1.2.3.4").unwrap(); - let _ = rpc_client.add_peer_call(AddPeerRequest { peer_address, is_permanent: true }).await.unwrap(); + let _ = rpc_client.add_peer_call(None, AddPeerRequest { peer_address, is_permanent: true }).await.unwrap(); // Add peer only adds the IP to a connection request. It will only be added to known_addresses if it // actually can be connected to. So in this test we can't expect it to be added unless we set up an // actual peer. - let response = rpc_client.get_peer_addresses_call(GetPeerAddressesRequest {}).await.unwrap(); + let response = rpc_client.get_peer_addresses_call(None, GetPeerAddressesRequest {}).await.unwrap(); assert!(response.known_addresses.is_empty()); }) } @@ -274,14 +291,14 @@ async fn sanity_test() { let peer_address = ContextualNetAddress::from_str("5.6.7.8").unwrap(); let ip = peer_address.normalize(1).ip; - let _ = rpc_client.add_peer_call(AddPeerRequest { peer_address, is_permanent: false }).await.unwrap(); - let _ = rpc_client.ban_call(BanRequest { ip }).await.unwrap(); + let _ = rpc_client.add_peer_call(None, AddPeerRequest { peer_address, is_permanent: false }).await.unwrap(); + let _ = rpc_client.ban_call(None, BanRequest { ip }).await.unwrap(); - let response = rpc_client.get_peer_addresses_call(GetPeerAddressesRequest {}).await.unwrap(); + let response = rpc_client.get_peer_addresses_call(None, GetPeerAddressesRequest {}).await.unwrap(); assert!(response.banned_addresses.contains(&ip)); - let _ = rpc_client.unban_call(UnbanRequest { ip }).await.unwrap(); - let response = rpc_client.get_peer_addresses_call(GetPeerAddressesRequest {}).await.unwrap(); + let _ = rpc_client.unban_call(None, UnbanRequest { ip }).await.unwrap(); + let response = rpc_client.get_peer_addresses_call(None, GetPeerAddressesRequest {}).await.unwrap(); assert!(!response.banned_addresses.contains(&ip)); }) } @@ -316,7 +333,7 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let result = - rpc_client.get_subnetwork_call(GetSubnetworkRequest { subnetwork_id: SubnetworkId::from_byte(0) }).await; + rpc_client.get_subnetwork_call(None, GetSubnetworkRequest { subnetwork_id: SubnetworkId::from_byte(0) }).await; // Err because it's currently unimplemented assert!(result.is_err()); @@ -334,7 +351,7 @@ async fn sanity_test() { KaspadPayloadOps::GetBlockDagInfo => { let rpc_client = client.clone(); tst!(op, { - let response = rpc_client.get_block_dag_info_call(GetBlockDagInfoRequest {}).await.unwrap(); + let response = rpc_client.get_block_dag_info_call(None, GetBlockDagInfoRequest {}).await.unwrap(); assert_eq!(response.network, network_id); }) } @@ -343,9 +360,10 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let response_result = rpc_client - .resolve_finality_conflict_call(ResolveFinalityConflictRequest { - finality_block_hash: Hash::from_bytes([0; 32]), - }) + .resolve_finality_conflict_call( + None, + ResolveFinalityConflictRequest { finality_block_hash: Hash::from_bytes([0; 32]) }, + ) .await; // Err because it's currently unimplemented @@ -357,7 +375,7 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let response_result = rpc_client - .get_headers_call(GetHeadersRequest { start_hash: SIMNET_GENESIS.hash, limit: 1, is_ascending: true }) + .get_headers_call(None, GetHeadersRequest { start_hash: SIMNET_GENESIS.hash, limit: 1, is_ascending: true }) .await; // Err because it's currently unimplemented @@ -369,7 +387,8 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let addresses = vec![Address::new(Prefix::Simnet, Version::PubKey, &[0u8; 32])]; - let response = rpc_client.get_utxos_by_addresses_call(GetUtxosByAddressesRequest { addresses }).await.unwrap(); + let response = + rpc_client.get_utxos_by_addresses_call(None, GetUtxosByAddressesRequest { addresses }).await.unwrap(); assert!(response.entries.is_empty()); }) } @@ -378,9 +397,10 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let response = rpc_client - .get_balance_by_address_call(GetBalanceByAddressRequest { - address: Address::new(Prefix::Simnet, Version::PubKey, &[0u8; 32]), - }) + .get_balance_by_address_call( + None, + GetBalanceByAddressRequest { address: Address::new(Prefix::Simnet, Version::PubKey, &[0u8; 32]) }, + ) .await .unwrap(); assert_eq!(response.balance, 0); @@ -392,7 +412,7 @@ async fn sanity_test() { tst!(op, { let addresses = vec![Address::new(Prefix::Simnet, Version::PubKey, &[1u8; 32])]; let response = rpc_client - .get_balances_by_addresses_call(GetBalancesByAddressesRequest::new(addresses.clone())) + .get_balances_by_addresses_call(None, GetBalancesByAddressesRequest::new(addresses.clone())) .await .unwrap(); assert_eq!(response.entries.len(), 1); @@ -400,7 +420,7 @@ async fn sanity_test() { assert_eq!(response.entries[0].balance, Some(0)); let response = - rpc_client.get_balances_by_addresses_call(GetBalancesByAddressesRequest::new(vec![])).await.unwrap(); + rpc_client.get_balances_by_addresses_call(None, GetBalancesByAddressesRequest::new(vec![])).await.unwrap(); assert!(response.entries.is_empty()); }) } @@ -408,7 +428,7 @@ async fn sanity_test() { KaspadPayloadOps::GetSinkBlueScore => { let rpc_client = client.clone(); tst!(op, { - let response = rpc_client.get_sink_blue_score_call(GetSinkBlueScoreRequest {}).await.unwrap(); + let response = rpc_client.get_sink_blue_score_call(None, GetSinkBlueScoreRequest {}).await.unwrap(); // A concurrent test may have added a single block so the blue score can be either 0 or 1 assert!(response.blue_score < 2); }) @@ -418,10 +438,10 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let response_result = rpc_client - .estimate_network_hashes_per_second_call(EstimateNetworkHashesPerSecondRequest { - window_size: 1000, - start_hash: None, - }) + .estimate_network_hashes_per_second_call( + None, + EstimateNetworkHashesPerSecondRequest { window_size: 1000, start_hash: None }, + ) .await; // The current DAA window is almost empty so an error is expected assert!(response_result.is_err()); @@ -433,11 +453,10 @@ async fn sanity_test() { tst!(op, { let addresses = vec![Address::new(Prefix::Simnet, Version::PubKey, &[0u8; 32])]; let response = rpc_client - .get_mempool_entries_by_addresses_call(GetMempoolEntriesByAddressesRequest::new( - addresses.clone(), - true, - false, - )) + .get_mempool_entries_by_addresses_call( + None, + GetMempoolEntriesByAddressesRequest::new(addresses.clone(), true, false), + ) .await .unwrap(); assert_eq!(response.entries.len(), 1); @@ -450,7 +469,7 @@ async fn sanity_test() { KaspadPayloadOps::GetCoinSupply => { let rpc_client = client.clone(); tst!(op, { - let response = rpc_client.get_coin_supply_call(GetCoinSupplyRequest {}).await.unwrap(); + let response = rpc_client.get_coin_supply_call(None, GetCoinSupplyRequest {}).await.unwrap(); assert_eq!(response.circulating_sompi, 0); assert_eq!(response.max_sompi, MAX_SOMPI); }) @@ -459,7 +478,14 @@ async fn sanity_test() { KaspadPayloadOps::Ping => { let rpc_client = client.clone(); tst!(op, { - let _ = rpc_client.ping_call(PingRequest {}).await.unwrap(); + let _ = rpc_client.ping_call(None, PingRequest {}).await.unwrap(); + }) + } + + KaspadPayloadOps::GetConnections => { + let rpc_client = client.clone(); + tst!(op, { + let _ = rpc_client.get_connections_call(None, GetConnectionsRequest { include_profile_data: true }).await.unwrap(); }) } @@ -467,48 +493,68 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let get_metrics_call_response = rpc_client - .get_metrics_call(GetMetricsRequest { - consensus_metrics: true, - connection_metrics: true, - bandwidth_metrics: true, - process_metrics: true, - }) + .get_metrics_call( + None, + GetMetricsRequest { + consensus_metrics: true, + connection_metrics: true, + bandwidth_metrics: true, + process_metrics: true, + storage_metrics: true, + custom_metrics: true, + }, + ) .await .unwrap(); assert!(get_metrics_call_response.process_metrics.is_some()); assert!(get_metrics_call_response.consensus_metrics.is_some()); let get_metrics_call_response = rpc_client - .get_metrics_call(GetMetricsRequest { - consensus_metrics: false, - connection_metrics: true, - bandwidth_metrics: true, - process_metrics: true, - }) + .get_metrics_call( + None, + GetMetricsRequest { + consensus_metrics: false, + connection_metrics: true, + bandwidth_metrics: true, + process_metrics: true, + storage_metrics: true, + custom_metrics: true, + }, + ) .await .unwrap(); assert!(get_metrics_call_response.process_metrics.is_some()); assert!(get_metrics_call_response.consensus_metrics.is_none()); let get_metrics_call_response = rpc_client - .get_metrics_call(GetMetricsRequest { - consensus_metrics: true, - connection_metrics: true, - bandwidth_metrics: false, - process_metrics: false, - }) + .get_metrics_call( + None, + GetMetricsRequest { + consensus_metrics: true, + connection_metrics: true, + bandwidth_metrics: false, + process_metrics: false, + storage_metrics: false, + custom_metrics: true, + }, + ) .await .unwrap(); assert!(get_metrics_call_response.process_metrics.is_none()); assert!(get_metrics_call_response.consensus_metrics.is_some()); let get_metrics_call_response = rpc_client - .get_metrics_call(GetMetricsRequest { - consensus_metrics: false, - connection_metrics: true, - bandwidth_metrics: false, - process_metrics: false, - }) + .get_metrics_call( + None, + GetMetricsRequest { + consensus_metrics: false, + connection_metrics: true, + bandwidth_metrics: false, + process_metrics: false, + storage_metrics: false, + custom_metrics: true, + }, + ) .await .unwrap(); assert!(get_metrics_call_response.process_metrics.is_none()); @@ -516,10 +562,17 @@ async fn sanity_test() { }) } + KaspadPayloadOps::GetSystemInfo => { + let rpc_client = client.clone(); + tst!(op, { + let _response = rpc_client.get_system_info_call(None, GetSystemInfoRequest {}).await.unwrap(); + }) + } + KaspadPayloadOps::GetServerInfo => { let rpc_client = client.clone(); tst!(op, { - let response = rpc_client.get_server_info_call(GetServerInfoRequest {}).await.unwrap(); + let response = rpc_client.get_server_info_call(None, GetServerInfoRequest {}).await.unwrap(); assert!(response.has_utxo_index); // we set utxoindex above assert_eq!(response.network_id, network_id); }) @@ -528,7 +581,7 @@ async fn sanity_test() { KaspadPayloadOps::GetSyncStatus => { let rpc_client = client.clone(); tst!(op, { - let _ = rpc_client.get_sync_status_call(GetSyncStatusRequest {}).await.unwrap(); + let _ = rpc_client.get_sync_status_call(None, GetSyncStatusRequest {}).await.unwrap(); }) } @@ -536,9 +589,10 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let results = rpc_client - .get_daa_score_timestamp_estimate_call(GetDaaScoreTimestampEstimateRequest { - daa_scores: vec![0, 500, 2000, u64::MAX], - }) + .get_daa_score_timestamp_estimate_call( + None, + GetDaaScoreTimestampEstimateRequest { daa_scores: vec![0, 500, 2000, u64::MAX] }, + ) .await .unwrap(); @@ -547,7 +601,7 @@ async fn sanity_test() { } let results = rpc_client - .get_daa_score_timestamp_estimate_call(GetDaaScoreTimestampEstimateRequest { daa_scores: vec![] }) + .get_daa_score_timestamp_estimate_call(None, GetDaaScoreTimestampEstimateRequest { daa_scores: vec![] }) .await .unwrap(); @@ -670,7 +724,7 @@ async fn sanity_test() { // Shutdown should only be tested after everything let rpc_client = client.clone(); - let _ = rpc_client.shutdown_call(ShutdownRequest {}).await.unwrap(); + let _ = rpc_client.shutdown_call(None, ShutdownRequest {}).await.unwrap(); // // Fold-up diff --git a/testing/integration/src/tasks/block/miner.rs b/testing/integration/src/tasks/block/miner.rs index ae759801e5..2cf117028a 100644 --- a/testing/integration/src/tasks/block/miner.rs +++ b/testing/integration/src/tasks/block/miner.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use kaspa_addresses::Address; use kaspa_core::warn; use kaspa_grpc_client::GrpcClient; -use kaspa_rpc_core::{api::rpc::RpcApi, GetBlockTemplateResponse, RpcBlock}; +use kaspa_rpc_core::{api::rpc::RpcApi, GetBlockTemplateResponse, RpcRawBlock}; use kaspa_utils::triggers::SingleTrigger; use parking_lot::Mutex; use rand::thread_rng; @@ -25,7 +25,7 @@ pub struct BlockMinerTask { client: Arc, bps: u64, block_count: usize, - sender: Sender, + sender: Sender, template: Arc>, pay_address: Address, tx_counter: Arc, @@ -38,7 +38,7 @@ impl BlockMinerTask { client: Arc, bps: u64, block_count: usize, - sender: Sender, + sender: Sender, template: Arc>, pay_address: Address, stopper: Stopper, @@ -60,7 +60,7 @@ impl BlockMinerTask { client: Arc, bps: u64, block_count: usize, - sender: Sender, + sender: Sender, template: Arc>, pay_address: Address, stopper: Stopper, @@ -68,7 +68,7 @@ impl BlockMinerTask { Arc::new(Self::new(client, bps, block_count, sender, template, pay_address, stopper)) } - pub fn sender(&self) -> Sender { + pub fn sender(&self) -> Sender { self.sender.clone() } diff --git a/testing/integration/src/tasks/block/submitter.rs b/testing/integration/src/tasks/block/submitter.rs index b57d032696..49bf9d83e5 100644 --- a/testing/integration/src/tasks/block/submitter.rs +++ b/testing/integration/src/tasks/block/submitter.rs @@ -6,18 +6,18 @@ use async_channel::Sender; use async_trait::async_trait; use kaspa_core::warn; use kaspa_grpc_client::ClientPool; -use kaspa_rpc_core::{api::rpc::RpcApi, RpcBlock}; +use kaspa_rpc_core::{api::rpc::RpcApi, RpcRawBlock}; use kaspa_utils::triggers::SingleTrigger; use std::{sync::Arc, time::Duration}; use tokio::{task::JoinHandle, time::sleep}; pub struct BlockSubmitterTask { - pool: ClientPool, + pool: ClientPool, stopper: Stopper, } impl BlockSubmitterTask { - pub fn new(pool: ClientPool, stopper: Stopper) -> Self { + pub fn new(pool: ClientPool, stopper: Stopper) -> Self { Self { pool, stopper } } @@ -26,7 +26,7 @@ impl BlockSubmitterTask { Arc::new(Self::new(pool, stopper)) } - pub fn sender(&self) -> Sender { + pub fn sender(&self) -> Sender { self.pool.sender() } } @@ -35,7 +35,7 @@ impl BlockSubmitterTask { impl Task for BlockSubmitterTask { fn start(&self, stop_signal: SingleTrigger) -> Vec> { warn!("Block submitter task starting..."); - let mut tasks = self.pool.start(|c, block: RpcBlock| async move { + let mut tasks = self.pool.start(|c, block: RpcRawBlock| async move { loop { match c.submit_block(block.clone(), false).await { Ok(response) => { diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 641ecb61ae..6e579ef0c0 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -9,6 +9,9 @@ include.workspace = true license.workspace = true repository.workspace = true +[build-dependencies] +duct = "0.13.7" + [dependencies] arc-swap.workspace = true async-channel.workspace = true @@ -19,16 +22,20 @@ faster-hex.workspace = true ipnet.workspace = true itertools.workspace = true log.workspace = true +num_cpus.workspace = true once_cell.workspace = true parking_lot.workspace = true serde.workspace = true +sha2.workspace = true smallvec.workspace = true +sysinfo.workspace = true thiserror.workspace = true triggered.workspace = true uuid.workspace = true wasm-bindgen.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +mac_address.workspace = true rlimit.workspace = true [dev-dependencies] diff --git a/utils/build.rs b/utils/build.rs new file mode 100644 index 0000000000..705e922962 --- /dev/null +++ b/utils/build.rs @@ -0,0 +1,82 @@ +use duct::cmd; +use std::env; +use std::path::*; + +struct GitHead { + head_path: String, + head_ref_path: String, + full_hash: String, + short_hash: String, +} + +fn main() { + let success = if env::var("RUSTY_KASPA_NO_COMMIT_HASH").is_err() { + if let Some(GitHead { head_path, head_ref_path, full_hash, short_hash }) = try_git_head() { + println!("cargo::rerun-if-changed={head_path}"); + println!("cargo::rerun-if-changed={head_ref_path}"); + println!("cargo:rustc-env=RUSTY_KASPA_GIT_FULL_COMMIT_HASH={full_hash}"); + println!("cargo:rustc-env=RUSTY_KASPA_GIT_SHORT_COMMIT_HASH={short_hash}"); + true + } else { + false + } + } else { + false + }; + + if !success { + println!("cargo:rustc-env=RUSTY_KASPA_GIT_FULL_COMMIT_HASH="); + println!("cargo:rustc-env=RUSTY_KASPA_GIT_SHORT_COMMIT_HASH="); + } +} + +fn try_git_head() -> Option { + let cargo_manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let path = cargo_manifest_dir.as_path().parent()?; + + let full_hash = cmd!("git", "rev-parse", "HEAD").dir(path).read().ok().map(|full_hash| full_hash.trim().to_string()); + + let short_hash = cmd!("git", "rev-parse", "--short", "HEAD").dir(path).read().ok().map(|short_hash| short_hash.trim().to_string()); + + let git_folder = path.join(".git"); + if git_folder.is_dir() { + let head_path = git_folder.join("HEAD"); + if head_path.is_file() { + let head = std::fs::read_to_string(&head_path).ok()?; + if head.starts_with("ref: ") { + let head_ref_path = head.trim_start_matches("ref: "); + let head_ref_path = git_folder.join(head_ref_path.trim()); + if head_ref_path.is_file() { + if let (Some(full_hash), Some(short_hash)) = (full_hash, short_hash) { + return Some(GitHead { + head_path: head_path.to_str().unwrap().to_string(), + head_ref_path: head_ref_path.to_str().unwrap().to_string(), + full_hash, + short_hash, + }); + } else if let Ok(full_hash) = std::fs::read_to_string(&head_ref_path) { + let full_hash = full_hash.trim().to_string(); + let short_hash = if full_hash.len() >= 7 { + // this is not actually correct as short hash has a variable + // length based on commit short hash collisions (which is) + // why we attempt to use `git rev-parse` above. But since this + // is for reference purposes only, we can live with it. + full_hash[0..7].to_string() + } else { + full_hash.to_string() + }; + + return Some(GitHead { + head_path: head_path.to_str().unwrap().to_string(), + head_ref_path: head_ref_path.to_str().unwrap().to_string(), + full_hash, + short_hash, + }); + } + } + } + } + } + + None +} diff --git a/utils/src/git.rs b/utils/src/git.rs new file mode 100644 index 0000000000..ca62da8a7c --- /dev/null +++ b/utils/src/git.rs @@ -0,0 +1,53 @@ +use crate::hex::FromHex; +use std::fmt::Display; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +// generated by `build.rs` +const FULL_HASH: &str = env!("RUSTY_KASPA_GIT_FULL_COMMIT_HASH"); +const SHORT_HASH: &str = env!("RUSTY_KASPA_GIT_SHORT_COMMIT_HASH"); + +/// Check if the codebase is built under a Git repository +/// and return the hash of the current commit as `Vec`. +pub fn hash() -> Option> { + FromHex::from_hex(FULL_HASH).ok() +} + +pub fn short_hash() -> Option> { + FromHex::from_hex(SHORT_HASH).ok() +} + +pub fn hash_str() -> Option<&'static str> { + #[allow(clippy::const_is_empty)] + (!FULL_HASH.is_empty()).then_some(FULL_HASH) +} + +pub fn short_hash_str() -> Option<&'static str> { + #[allow(clippy::const_is_empty)] + (!SHORT_HASH.is_empty()).then_some(SHORT_HASH) +} + +pub fn version() -> String { + if let Some(short_hash) = short_hash_str() { + format!("v{VERSION}-{short_hash}") + } else { + format!("v{VERSION}") + } +} + +pub fn with_short_hash(version: V) -> impl Display +where + V: Display, +{ + if let Some(short_hash) = short_hash_str() { + format!("{version}-{short_hash}") + } else { + version.to_string() + } +} + +#[test] +fn test_git_hash() { + println!("FULL_HASH: {:?}", hash_str()); + println!("SHORT_HASH: {:?}", short_hash_str()); +} diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 79e96c44f1..4e57548e72 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -192,4 +192,9 @@ pub mod sync; pub mod triggers; pub mod vec; +pub mod git; + +#[cfg(not(target_arch = "wasm32"))] pub mod fd_budget; +#[cfg(not(target_arch = "wasm32"))] +pub mod sysinfo; diff --git a/utils/src/networking.rs b/utils/src/networking.rs index ebd72b2598..b7a3397780 100644 --- a/utils/src/networking.rs +++ b/utils/src/networking.rs @@ -179,7 +179,7 @@ impl Deref for IpAddress { // impl BorshSerialize for IpAddress { - fn serialize(&self, writer: &mut W) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + fn serialize(&self, writer: &mut W) -> ::core::result::Result<(), std::io::Error> { let variant_idx: u8 = match self.0 { IpAddr::V4(..) => 0u8, IpAddr::V6(..) => 1u8, @@ -198,20 +198,20 @@ impl BorshSerialize for IpAddress { } impl BorshDeserialize for IpAddress { - fn deserialize(buf: &mut &[u8]) -> ::core::result::Result { - let variant_idx: u8 = BorshDeserialize::deserialize(buf)?; + fn deserialize_reader(reader: &mut R) -> ::core::result::Result { + let variant_idx: u8 = BorshDeserialize::deserialize_reader(reader)?; let ip = match variant_idx { 0u8 => { - let octets: [u8; 4] = BorshDeserialize::deserialize(buf)?; + let octets: [u8; 4] = BorshDeserialize::deserialize_reader(reader)?; IpAddr::V4(Ipv4Addr::from(octets)) } 1u8 => { - let octets: [u8; 16] = BorshDeserialize::deserialize(buf)?; + let octets: [u8; 16] = BorshDeserialize::deserialize_reader(reader)?; IpAddr::V6(Ipv6Addr::from(octets)) } _ => { - let msg = borsh::maybestd::format!("Unexpected variant index: {:?}", variant_idx); - return Err(borsh::maybestd::io::Error::new(borsh::maybestd::io::ErrorKind::InvalidInput, msg)); + let msg = format!("Unexpected variant index: {:?}", variant_idx); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, msg)); } }; Ok(Self(ip)) @@ -275,6 +275,10 @@ impl ContextualNetAddress { Self { ip, port } } + pub fn has_port(&self) -> bool { + self.port.is_some() + } + pub fn normalize(&self, default_port: u16) -> NetAddress { NetAddress::new(self.ip, self.port.unwrap_or(default_port)) } @@ -389,15 +393,15 @@ impl Deref for PeerId { // impl BorshSerialize for PeerId { - fn serialize(&self, writer: &mut W) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + fn serialize(&self, writer: &mut W) -> ::core::result::Result<(), std::io::Error> { borsh::BorshSerialize::serialize(&self.0.as_bytes(), writer)?; Ok(()) } } impl BorshDeserialize for PeerId { - fn deserialize(buf: &mut &[u8]) -> ::core::result::Result { - let bytes: uuid::Bytes = BorshDeserialize::deserialize(buf)?; + fn deserialize_reader(reader: &mut R) -> ::core::result::Result { + let bytes: uuid::Bytes = BorshDeserialize::deserialize_reader(reader)?; Ok(Self::new(Uuid::from_bytes(bytes))) } } @@ -411,12 +415,12 @@ mod tests { fn test_ip_address_borsh() { // Tests for IpAddress Borsh ser/deser since we manually implemented them let ip: IpAddress = Ipv4Addr::from([44u8; 4]).into(); - let bin = ip.try_to_vec().unwrap(); + let bin = borsh::to_vec(&ip).unwrap(); let ip2: IpAddress = BorshDeserialize::try_from_slice(&bin).unwrap(); assert_eq!(ip, ip2); let ip: IpAddress = Ipv6Addr::from([66u8; 16]).into(); - let bin = ip.try_to_vec().unwrap(); + let bin = borsh::to_vec(&ip).unwrap(); let ip2: IpAddress = BorshDeserialize::try_from_slice(&bin).unwrap(); assert_eq!(ip, ip2); } @@ -425,12 +429,12 @@ mod tests { fn test_peer_id_borsh() { // Tests for PeerId Borsh ser/deser since we manually implemented them let id: PeerId = Uuid::new_v4().into(); - let bin = id.try_to_vec().unwrap(); + let bin = borsh::to_vec(&id).unwrap(); let id2: PeerId = BorshDeserialize::try_from_slice(&bin).unwrap(); assert_eq!(id, id2); let id: PeerId = Uuid::from_bytes([123u8; 16]).into(); - let bin = id.try_to_vec().unwrap(); + let bin = borsh::to_vec(&id).unwrap(); let id2: PeerId = BorshDeserialize::try_from_slice(&bin).unwrap(); assert_eq!(id, id2); } diff --git a/utils/src/sysinfo.rs b/utils/src/sysinfo.rs new file mode 100644 index 0000000000..4e009d4495 --- /dev/null +++ b/utils/src/sysinfo.rs @@ -0,0 +1,81 @@ +use crate::fd_budget; +use crate::git; +use crate::hex::ToHex; +use sha2::{Digest, Sha256}; +use std::fs::File; +use std::io::Read; +use std::sync::OnceLock; + +static SYSTEM_INFO: OnceLock = OnceLock::new(); + +#[derive(Clone)] +pub struct SystemInfo { + pub system_id: Option>, + pub git_hash: Option>, + pub git_short_hash: Option>, + pub version: String, + pub cpu_physical_cores: u16, + pub total_memory: u64, + pub fd_limit: u32, +} + +// provide hex encoding for system_id, git_hash, and git_short_hash +impl std::fmt::Debug for SystemInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SystemInfo") + .field("system_id", &self.system_id.as_ref().map(|id| id.to_hex())) + .field("git_hash", &self.git_hash.as_ref().map(|hash| hash.to_hex())) + .field("git_short_hash", &self.git_short_hash.as_ref().map(|hash| hash.to_hex())) + .field("version", &self.version) + .field("cpu_physical_cores", &self.cpu_physical_cores) + .field("total_memory", &self.total_memory) + .field("fd_limit", &self.fd_limit) + .finish() + } +} + +impl Default for SystemInfo { + fn default() -> Self { + let system_info = SYSTEM_INFO.get_or_init(|| { + let mut system = sysinfo::System::new(); + system.refresh_memory(); + let cpu_physical_cores = num_cpus::get() as u16; + let total_memory = system.total_memory(); + let fd_limit = fd_budget::limit() as u32; + let system_id = Self::try_system_id(); + let git_hash = git::hash(); + let git_short_hash = git::short_hash(); + let version = git::version(); + + SystemInfo { system_id, git_hash, git_short_hash, version, cpu_physical_cores, total_memory, fd_limit } + }); + (*system_info).clone() + } +} + +impl SystemInfo { + /// Obtain a unique system (machine) identifier. + fn try_system_id() -> Option> { + let some_id = if let Ok(mut file) = File::open("/etc/machine-id") { + // fetch the system id from /etc/machine-id + let mut machine_id = String::new(); + file.read_to_string(&mut machine_id).ok(); + machine_id.trim().to_string() + } else if let Ok(Some(mac)) = mac_address::get_mac_address() { + // fallback on the mac address + mac.to_string().trim().to_string() + } else { + // 🤷 + return None; + }; + let mut sha256 = Sha256::default(); + sha256.update(some_id.as_bytes()); + Some(sha256.finalize().to_vec()) + } +} + +impl AsRef for SystemInfo { + fn as_ref(&self) -> &SystemInfo { + self + } +} diff --git a/wallet/bip32/Cargo.toml b/wallet/bip32/Cargo.toml index b4eb982024..efc31baf48 100644 --- a/wallet/bip32/Cargo.toml +++ b/wallet/bip32/Cargo.toml @@ -31,6 +31,7 @@ thiserror.workspace = true wasm-bindgen.workspace = true workflow-wasm.workspace = true zeroize.workspace = true +kaspa-consensus-core.workspace = true [dev-dependencies] -faster-hex.workspace = true \ No newline at end of file +faster-hex.workspace = true diff --git a/wallet/bip32/src/mnemonic/phrase.rs b/wallet/bip32/src/mnemonic/phrase.rs index 76450590a6..95fd921892 100644 --- a/wallet/bip32/src/mnemonic/phrase.rs +++ b/wallet/bip32/src/mnemonic/phrase.rs @@ -23,7 +23,7 @@ pub type Entropy32 = [u8; KEY_SIZE]; pub type Entropy16 = [u8; 16]; /// Word count for a BIP39 mnemonic phrase. Identifies mnemonic as 12 or 24 word variants. -#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "kebab-case")] pub enum WordCount { #[default] @@ -85,8 +85,8 @@ impl Mnemonic { } #[wasm_bindgen(js_name = random)] - pub fn create_random_js(word_count: JsValue) -> Result { - let word_count = word_count.as_f64().unwrap_or(24.0) as usize; + pub fn create_random_js(word_count: Option) -> Result { + let word_count = word_count.unwrap_or(24) as usize; Mnemonic::random(word_count.try_into()?, Default::default()) } diff --git a/wallet/bip32/src/prefix.rs b/wallet/bip32/src/prefix.rs index 8a4bcc6273..daff8267e7 100644 --- a/wallet/bip32/src/prefix.rs +++ b/wallet/bip32/src/prefix.rs @@ -6,6 +6,7 @@ use core::{ fmt::{self, Debug, Display}, str, }; +use kaspa_consensus_core::network::{NetworkId, NetworkType}; /// BIP32 extended key prefixes a.k.a. "versions" (e.g. `xpub`, `xprv`) /// @@ -234,6 +235,18 @@ impl TryFrom<&str> for Prefix { } } +impl From for Prefix { + fn from(value: NetworkId) -> Self { + let network_type = value.network_type(); + match network_type { + NetworkType::Mainnet => Prefix::KPUB, + NetworkType::Devnet => Prefix::KTUB, + NetworkType::Simnet => Prefix::KTUB, + NetworkType::Testnet => Prefix::KTUB, + } + } +} + #[cfg(test)] mod tests { use super::Prefix; diff --git a/wallet/bip32/src/xpublic_key.rs b/wallet/bip32/src/xpublic_key.rs index a52d8f1a1c..ac4eb720c9 100644 --- a/wallet/bip32/src/xpublic_key.rs +++ b/wallet/bip32/src/xpublic_key.rs @@ -174,8 +174,8 @@ impl BorshDeserialize for ExtendedPublicKey where K: PublicKey, { - fn deserialize(buf: &mut &[u8]) -> std::io::Result { - let Header { version, magic } = Header::deserialize(buf)?; + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let Header { version, magic } = Header::deserialize_reader(reader)?; if magic != Self::STORAGE_MAGIC { return Err(std::io::Error::new(std::io::ErrorKind::Other, "Invalid extended public key magic value")); } @@ -183,13 +183,11 @@ where return Err(std::io::Error::new(std::io::ErrorKind::Other, "Invalid extended public key version")); } - let public_key_bytes: [u8; KEY_SIZE + 1] = buf[..KEY_SIZE + 1] - .try_into() - .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Invalid extended public key"))?; + let mut public_key_bytes: [u8; KEY_SIZE + 1] = [0; KEY_SIZE + 1]; + reader.read_exact(&mut public_key_bytes)?; let public_key = K::from_bytes(public_key_bytes) .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Invalid extended public key"))?; - *buf = &buf[KEY_SIZE + 1..]; - let attrs = ExtendedKeyAttrs::deserialize(buf)?; + let attrs = ExtendedKeyAttrs::deserialize_reader(reader)?; Ok(Self { public_key, attrs }) } } diff --git a/wallet/core/Cargo.toml b/wallet/core/Cargo.toml index fb31afb310..3e057b6a77 100644 --- a/wallet/core/Cargo.toml +++ b/wallet/core/Cargo.toml @@ -71,6 +71,7 @@ kaspa-txscript.workspace = true kaspa-utils.workspace = true kaspa-wallet-keys.workspace = true kaspa-wallet-macros.workspace = true +kaspa-wallet-pskt.workspace = true kaspa-wasm-core.workspace = true kaspa-wrpc-client.workspace = true kaspa-wrpc-wasm.workspace = true @@ -125,5 +126,5 @@ serde_repr.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio.workspace = true -[lints.clippy] -empty_docs = "allow" +[lints] +workspace = true diff --git a/wallet/core/src/account/descriptor.rs b/wallet/core/src/account/descriptor.rs index d433b7bf8f..c549b739d2 100644 --- a/wallet/core/src/account/descriptor.rs +++ b/wallet/core/src/account/descriptor.rs @@ -17,6 +17,7 @@ pub struct AccountDescriptor { pub kind: AccountKind, pub account_id: AccountId, pub account_name: Option, + pub balance: Option, pub prv_key_data_ids: AssocPrvKeyDataIds, pub receive_address: Option
, pub change_address: Option
, @@ -29,11 +30,21 @@ impl AccountDescriptor { kind: AccountKind, account_id: AccountId, account_name: Option, + balance: Option, prv_key_data_ids: AssocPrvKeyDataIds, receive_address: Option
, change_address: Option
, ) -> Self { - Self { kind, account_id, account_name, prv_key_data_ids, receive_address, change_address, properties: BTreeMap::default() } + Self { + kind, + account_id, + account_name, + balance, + prv_key_data_ids, + receive_address, + change_address, + properties: BTreeMap::default(), + } } pub fn with_property(mut self, property: AccountDescriptorProperty, value: AccountDescriptorValue) -> Self { @@ -111,11 +122,12 @@ impl std::fmt::Display for AccountDescriptorValue { AccountDescriptorValue::Bool(value) => write!(f, "{}", value), AccountDescriptorValue::AddressDerivationMeta(value) => write!(f, "{}", value), AccountDescriptorValue::XPubKeys(value) => { - let mut s = String::new(); + let mut s = vec![]; for xpub in value.iter() { - s.push_str(&format!("{}\n", xpub)); + //s.push(xpub.to_string(None)); + s.push(format!("{}", xpub)); } - write!(f, "{}", s) + write!(f, "{}", s.join("\n")) } AccountDescriptorValue::Json(value) => write!(f, "{}", value), } @@ -225,6 +237,7 @@ declare! { receiveAddress? : Address, changeAddress? : Address, prvKeyDataIds : HexString[], + // balance? : Balance, [key: string]: any } "#, diff --git a/wallet/core/src/account/kind.rs b/wallet/core/src/account/kind.rs index faf968f296..20e863d77c 100644 --- a/wallet/core/src/account/kind.rs +++ b/wallet/core/src/account/kind.rs @@ -66,6 +66,7 @@ impl FromStr for AccountKind { "bip32" => Ok(BIP32_ACCOUNT_KIND.into()), "multisig" => Ok(MULTISIG_ACCOUNT_KIND.into()), "keypair" => Ok(KEYPAIR_ACCOUNT_KIND.into()), + "bip32watch" => Ok(BIP32_WATCH_ACCOUNT_KIND.into()), _ => Err(Error::InvalidAccountKind), } } @@ -95,22 +96,17 @@ impl BorshSerialize for AccountKind { } impl BorshDeserialize for AccountKind { - fn deserialize(buf: &mut &[u8]) -> IoResult { - if buf.is_empty() { - Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid AccountKind length")) - } else { - let len = buf[0]; - if buf.len() < (len as usize + 1) { - Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid AccountKind length")) - } else { - let s = str64::make( - std::str::from_utf8(&buf[1..(len as usize + 1)]) - .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid UTF-8 sequence"))?, - ); - *buf = &buf[(len as usize + 1)..]; - Ok(Self(s)) - } - } + fn deserialize_reader(reader: &mut R) -> IoResult { + let len = ::deserialize_reader(reader)? as usize; + let mut buf = [0; 64]; + reader + .read_exact(&mut buf[0..len]) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Invalid AccountKind length ({err:?})")))?; + let s = str64::make( + std::str::from_utf8(&buf[..len]) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid UTF-8 sequence"))?, + ); + Ok(Self(s)) } } diff --git a/wallet/core/src/account/mod.rs b/wallet/core/src/account/mod.rs index b921bf4914..31c7fea9d5 100644 --- a/wallet/core/src/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -5,8 +5,15 @@ pub mod descriptor; pub mod kind; +pub mod pskb; pub mod variants; +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, +}; pub use variants::*; use crate::derivation::build_derivate_paths; @@ -116,6 +123,14 @@ pub trait Account: AnySync + Send + Sync + 'static { self.context().settings.name.clone() } + fn feature(&self) -> Option { + None + } + + fn xpub_keys(&self) -> Option<&ExtendedPublicKeys> { + None + } + fn name_or_id(&self) -> String { if let Some(name) = self.name() { if name.is_empty() { @@ -348,6 +363,66 @@ pub trait Account: AnySync + Send + Sync + 'static { Ok((generator.summary(), ids)) } + async fn pskb_from_send_generator( + self: Arc, + destination: PaymentDestination, + 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 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))?; + let pskt_generator = PSKTGenerator::new(generator, signer, self.wallet().address_prefix()?); + bundle_from_pskt_generator(pskt_generator).await + } + + async fn pskb_sign( + self: Arc, + bundle: &Bundle, + wallet_secret: Secret, + payment_secret: Option, + sign_for_address: Option<&Address>, + ) -> Result { + let keydata = self.prv_key_data(wallet_secret).await?; + 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, _) = + 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(); + + match pskb_signer_for_address(bundle, signer, network_id, sign_for_address, derivation_path, key_fingerprint).await { + Ok(signer) => Ok(signer), + Err(e) => Err(Error::from(e.to_string())), + } + } + + async fn pskb_broadcast(self: Arc, bundle: &Bundle) -> Result, Error> { + let mut ids = Vec::new(); + let mut stream = bundle_to_finalizer_stream(bundle); + + 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)?; + ids.push(transaction.try_submit(&self.wallet().rpc_api()).await?); + } + Err(e) => { + eprintln!("Error processing a PSKT from bundle: {:?}", e); + } + } + } + Ok(ids) + } + /// Execute a transfer to another wallet account. async fn transfer( self: Arc, @@ -358,13 +433,14 @@ pub trait Account: AnySync + Send + Sync + 'static { payment_secret: Option, abortable: &Abortable, notifier: Option, + guard: &WalletGuard, ) -> 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 destination_account = self .wallet() - .get_account_by_id(&destination_account_id) + .get_account_by_id(&destination_account_id, guard) .await? .ok_or_else(|| Error::AccountNotFound(destination_account_id))?; @@ -524,6 +600,7 @@ pub trait DerivationCapableAccount: Account { let settings = GeneratorSettings::try_new_with_iterator( self.wallet().network_id()?, Box::new(utxos.into_iter()), + None, change_address.clone(), 1, 1, @@ -537,7 +614,7 @@ pub trait DerivationCapableAccount: Account { let mut stream = generator.stream(); while let Some(transaction) = stream.try_next().await? { - transaction.try_sign_with_keys(&keys)?; + transaction.try_sign_with_keys(&keys, None)?; let id = transaction.try_submit(&rpc).await?; if let Some(notifier) = notifier { notifier(index, aggregate_utxo_count, balance, Some(id)); diff --git a/wallet/core/src/account/pskb.rs b/wallet/core/src/account/pskb.rs new file mode 100644 index 0000000000..5cf1eeea9a --- /dev/null +++ b/wallet/core/src/account/pskb.rs @@ -0,0 +1,360 @@ +pub use crate::error::Error; +use crate::imports::*; +use crate::tx::PaymentOutputs; +use futures::stream; +use kaspa_bip32::{DerivationPath, KeyFingerprint, PrivateKey}; +use kaspa_consensus_client::UtxoEntry as ClientUTXO; +use kaspa_consensus_core::hashing::sighash::{calc_schnorr_signature_hash, SigHashReusedValues}; +use kaspa_consensus_core::tx::VerifiableTransaction; +use kaspa_consensus_core::tx::{TransactionInput, UtxoEntry}; +use kaspa_txscript::extract_script_pub_key_address; +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::prelude::KeySource; +use kaspa_wallet_pskt::prelude::{Finalizer, Inner, SignInputOk, Signature, Signer}; +pub use kaspa_wallet_pskt::pskt::{Creator, PSKT}; +use secp256k1::schnorr; +use secp256k1::{Message, PublicKey}; +use std::iter; + +struct PSKBSignerInner { + keydata: PrvKeyData, + account: Arc, + payment_secret: Option, + keys: Mutex>, +} + +pub struct PSKBSigner { + inner: Arc, +} + +impl PSKBSigner { + pub fn new(account: Arc, keydata: PrvKeyData, payment_secret: Option) -> Self { + Self { inner: Arc::new(PSKBSignerInner { keydata, account, payment_secret, keys: Mutex::new(AHashMap::new()) }) } + } + + pub fn ingest(&self, addresses: &[Address]) -> Result<()> { + let mut keys = self.inner.keys.lock()?; + + // 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)?; + for (address, private_key) in private_keys { + keys.insert(address.clone(), private_key.to_bytes()); + } + } + Ok(()) + } + + fn public_key(&self, for_address: &Address) -> Result { + let keys = self.inner.keys.lock()?; + match keys.get(for_address) { + Some(private_key) => { + let kp = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, private_key)?; + Ok(kp.public_key()) + } + None => Err(Error::from("PSKBSigner address coverage error")), + } + } + + fn sign_schnorr(&self, for_address: &Address, message: Message) -> Result { + let keys = self.inner.keys.lock()?; + match keys.get(for_address) { + Some(private_key) => { + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, private_key)?; + Ok(schnorr_key.sign_schnorr(message)) + } + None => Err(Error::from("PSKBSigner address coverage error")), + } + } +} + +pub struct PSKTGenerator { + generator: Generator, + signer: Arc, + prefix: Prefix, +} + +impl PSKTGenerator { + pub fn new(generator: Generator, signer: Arc, prefix: Prefix) -> Self { + Self { generator, signer, prefix } + } + + pub fn stream(&self) -> impl Stream, Error>> { + PSKTStream::new(self.generator.clone(), self.signer.clone(), self.prefix) + } +} + +struct PSKTStream { + generator_stream: Pin> + Send>>, + signer: Arc, + prefix: Prefix, +} + +impl PSKTStream { + fn new(generator: Generator, signer: Arc, prefix: Prefix) -> Self { + let generator_stream = generator.stream().map_err(Error::from); + Self { generator_stream: Box::pin(generator_stream), signer, prefix } + } +} + +impl Stream for PSKTStream { + type Item = Result, Error>; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.as_ref(); + + let _prefix = this.prefix; + let _signer = this.signer.clone(); + + match self.get_mut().generator_stream.as_mut().poll_next(cx) { + Poll::Ready(Some(Ok(pending_tx))) => { + let pskt = convert_pending_tx_to_pskt(pending_tx); + Poll::Ready(Some(pskt)) + } + Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +fn convert_pending_tx_to_pskt(pending_tx: PendingTransaction) -> Result, Error> { + let signable_tx = pending_tx.signable_transaction(); + let verifiable_tx = signable_tx.as_verifiable(); + let populated_inputs: Vec<(&TransactionInput, &UtxoEntry)> = verifiable_tx.populated_inputs().collect(); + let pskt_inner = Inner::try_from((pending_tx.transaction(), populated_inputs.to_owned()))?; + Ok(PSKT::::from(pskt_inner)) +} + +pub async fn bundle_from_pskt_generator(generator: PSKTGenerator) -> Result { + let mut bundle: Bundle = Bundle::new(); + let mut stream = generator.stream(); + + while let Some(pskt_result) = stream.next().await { + match pskt_result { + Ok(pskt) => bundle.add_pskt(pskt), + Err(e) => return Err(e), + } + } + + Ok(bundle) +} + +pub async fn pskb_signer_for_address( + bundle: &Bundle, + signer: Arc, + network_id: NetworkId, + sign_for_address: Option<&Address>, + derivation_path: DerivationPath, + key_fingerprint: KeyFingerprint, +) -> Result { + let mut signed_bundle = Bundle::new(); + let mut reused_values = SigHashReusedValues::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 + .iter() + .flat_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(|utxo_entry| { + extract_script_pub_key_address(&utxo_entry.script_public_key.clone(), network_id.into()).ok() + }) + .collect::>() + }) + .collect(), + }; + + // Prepare the signer. + signer.ingest(addresses.as_ref())?; + + for pskt_inner in bundle.iter().cloned() { + let pskt: PSKT = PSKT::from(pskt_inner); + + let mut sign = |signer_pskt: PSKT| { + 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], &mut 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"), + }; + + let public_key = signer.public_key(address).expect("Public key for input indexed address"); + + 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() }), + }) + }) + .collect() + }) + .unwrap() + }; + signed_bundle.add_pskt(sign(pskt.clone())); + } + Ok(signed_bundle) +} + +pub fn finalize_pskt_one_or_more_sig_and_redeem_script(pskt: PSKT) -> Result, Error> { + let result = pskt.finalize_sync(|inner: &Inner| -> Result>, String> { + Ok(inner + .inputs + .iter() + .map(|input| -> Vec { + let signatures: Vec<_> = input + .partial_sigs + .clone() + .into_iter() + .flat_map(|(_, signature)| iter::once(OpData65).chain(signature.into_bytes()).chain([input.sighash_type.to_u8()])) + .collect(); + + signatures + .into_iter() + .chain( + input + .redeem_script + .as_ref() + .map(|redeem_script| ScriptBuilder::new().add_data(redeem_script.as_slice()).unwrap().drain().to_vec()) + .unwrap_or_default(), + ) + .collect() + }) + .collect()) + }); + + match result { + Ok(finalized_pskt) => Ok(finalized_pskt), + Err(e) => Err(Error::from(e.to_string())), + } +} + +pub fn finalize_pskt_no_sig_and_redeem_script(pskt: PSKT) -> Result, Error> { + let result = pskt.finalize_sync(|inner: &Inner| -> Result>, String> { + Ok(inner + .inputs + .iter() + .map(|input| -> Vec { + input + .redeem_script + .as_ref() + .map(|redeem_script| ScriptBuilder::new().add_data(redeem_script.as_slice()).unwrap().drain().to_vec()) + .unwrap_or_default() + }) + .collect()) + }); + + match result { + Ok(finalized_pskt) => Ok(finalized_pskt), + Err(e) => Err(Error::from(e.to_string())), + } +} + +pub fn bundle_to_finalizer_stream(bundle: &Bundle) -> impl Stream, Error>> + Send { + stream::iter(bundle.iter().cloned().collect::>()).map(move |pskt_inner| { + let pskt: PSKT = PSKT::from(pskt_inner); + let pskt_finalizer = pskt.constructor().updater().signer().finalizer(); + finalize_pskt_one_or_more_sig_and_redeem_script(pskt_finalizer) + }) +} + +pub fn pskt_to_pending_transaction( + finalized_pskt: PSKT, + network_id: NetworkId, + change_address: Address, +) -> 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 + .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 + }) + .collect(); + + let output: Vec = signed_tx.outputs.clone(); + 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 settings = GeneratorSettings { + network_id, + multiplexer: None, + sig_op_count: 1, + minimum_signatures: 1, + change_address, + utxo_iterator, + priority_utxo_entries: None, + source_utxo_context: None, + destination_utxo_context: None, + final_transaction_priority_fee: fee_u.into(), + final_transaction_destination, + final_transaction_payload: None, + }; + + // Create the Generator + let generator = Generator::try_new(settings, None, None)?; + + // Create PendingTransaction + let pending_tx = PendingTransaction::try_new( + &generator, + signed_tx.clone(), + utxo_entries_ref.clone(), + vec![], + None, + 0, + 0, + 0, + 0, + 0, + kaspa_wallet_core::tx::DataKind::Final, + )?; + + Ok(pending_tx) +} diff --git a/wallet/core/src/account/variants/bip32.rs b/wallet/core/src/account/variants/bip32.rs index 0b2909ad09..1c120df4b4 100644 --- a/wallet/core/src/account/variants/bip32.rs +++ b/wallet/core/src/account/variants/bip32.rs @@ -70,13 +70,13 @@ impl BorshSerialize for Payload { } impl BorshDeserialize for Payload { - fn deserialize(buf: &mut &[u8]) -> IoResult { + fn deserialize_reader(reader: &mut R) -> IoResult { let StorageHeader { version: _, .. } = - StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; - let xpub_keys = BorshDeserialize::deserialize(buf)?; - let account_index = BorshDeserialize::deserialize(buf)?; - let ecdsa = BorshDeserialize::deserialize(buf)?; + let xpub_keys = BorshDeserialize::deserialize_reader(reader)?; + let account_index = BorshDeserialize::deserialize_reader(reader)?; + let ecdsa = BorshDeserialize::deserialize_reader(reader)?; Ok(Self { xpub_keys, account_index, ecdsa }) } @@ -169,6 +169,10 @@ impl Account for Bip32 { BIP32_ACCOUNT_KIND.into() } + // fn xpub_keys(&self) -> Option<&ExtendedPublicKeys> { + // None + // } + fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { Ok(&self.prv_key_data_id) } @@ -217,6 +221,7 @@ impl Account for Bip32 { BIP32_ACCOUNT_KIND.into(), *self.id(), self.name(), + self.balance(), self.prv_key_data_id.into(), self.receive_address().ok(), self.change_address().ok(), diff --git a/wallet/core/src/account/variants/bip32watch.rs b/wallet/core/src/account/variants/bip32watch.rs new file mode 100644 index 0000000000..cfadb745df --- /dev/null +++ b/wallet/core/src/account/variants/bip32watch.rs @@ -0,0 +1,252 @@ +//! +//! bip32-watch account implementation +//! + +use crate::account::Inner; +use crate::derivation::{AddressDerivationManager, AddressDerivationManagerTrait}; +use crate::imports::*; + +pub const BIP32_WATCH_ACCOUNT_KIND: &str = "kaspa-bip32-watch-standard"; + +pub struct Ctor {} + +#[async_trait] +impl Factory for Ctor { + fn name(&self) -> String { + "bip32watch".to_string() + } + + fn description(&self) -> String { + "Kaspa Core bip32-watch Account".to_string() + } + + async fn try_load( + &self, + wallet: &Arc, + storage: &AccountStorage, + meta: Option>, + ) -> Result> { + Ok(Arc::new(bip32watch::Bip32Watch::try_load(wallet, storage, meta).await?)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub struct Payload { + pub xpub_keys: ExtendedPublicKeys, + pub ecdsa: bool, +} + +impl Payload { + pub fn new(xpub_keys: Arc>, ecdsa: bool) -> Self { + Self { xpub_keys, ecdsa } + } + + pub fn try_load(storage: &AccountStorage) -> Result { + Ok(Self::try_from_slice(storage.serialized.as_slice())?) + } +} + +impl Storable for Payload { + // a unique number used for binary + // serialization data alignment check + const STORAGE_MAGIC: u32 = 0x92014137; + // binary serialization version + const STORAGE_VERSION: u32 = 0; +} + +impl AccountStorable for Payload {} + +impl BorshSerialize for Payload { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + BorshSerialize::serialize(&self.xpub_keys, writer)?; + BorshSerialize::serialize(&self.ecdsa, writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for Payload { + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + let xpub_keys = BorshDeserialize::deserialize_reader(reader)?; + let ecdsa = BorshDeserialize::deserialize_reader(reader)?; + + Ok(Self { xpub_keys, ecdsa }) + } +} + +pub struct Bip32Watch { + inner: Arc, + xpub_keys: ExtendedPublicKeys, + ecdsa: bool, + derivation: Arc, +} + +impl Bip32Watch { + pub async fn try_new(wallet: &Arc, name: Option, xpub_keys: ExtendedPublicKeys, ecdsa: bool) -> Result { + let settings = AccountSettings { name, ..Default::default() }; + + let public_key = xpub_keys.first().ok_or_else(|| Error::Bip32WatchXpubRequired)?.public_key(); + + let (id, storage_key) = make_account_hashes(from_bip32_watch(public_key)); + + let inner = Arc::new(Inner::new(wallet, id, storage_key, settings)); + + let derivation = + AddressDerivationManager::new(wallet, BIP32_WATCH_ACCOUNT_KIND.into(), &xpub_keys, ecdsa, 0, None, 1, Default::default()) + .await?; + + Ok(Self { inner, xpub_keys, ecdsa, derivation }) + } + + pub async fn try_load(wallet: &Arc, storage: &AccountStorage, meta: Option>) -> Result { + let storable = Payload::try_load(storage)?; + let inner = Arc::new(Inner::from_storage(wallet, storage)); + let Payload { xpub_keys, ecdsa, .. } = storable; + let address_derivation_indexes = meta.and_then(|meta| meta.address_derivation_indexes()).unwrap_or_default(); + + let derivation = AddressDerivationManager::new( + wallet, + BIP32_WATCH_ACCOUNT_KIND.into(), + &xpub_keys, + ecdsa, + 0, + None, + 1, + address_derivation_indexes, + ) + .await?; + + Ok(Self { inner, xpub_keys, ecdsa, derivation }) + } + + pub fn get_address_range_for_scan(&self, range: std::ops::Range) -> Result> { + let receive_addresses = self.derivation.receive_address_manager().get_range_with_args(range.clone(), false)?; + let change_addresses = self.derivation.change_address_manager().get_range_with_args(range, false)?; + Ok(receive_addresses.into_iter().chain(change_addresses).collect::>()) + } + + // pub fn xpub_keys(&self) -> &ExtendedPublicKeys { + // &self.xpub_keys + // } +} + +#[async_trait] +impl Account for Bip32Watch { + fn inner(&self) -> &Arc { + &self.inner + } + + fn account_kind(&self) -> AccountKind { + BIP32_WATCH_ACCOUNT_KIND.into() + } + + fn feature(&self) -> Option { + let info = "bip32-watch"; + Some(info.into()) + } + + fn xpub_keys(&self) -> Option<&ExtendedPublicKeys> { + Some(&self.xpub_keys) + } + + fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { + Err(Error::Bip32WatchAccount) + } + + fn as_dyn_arc(self: Arc) -> Arc { + self + } + + fn sig_op_count(&self) -> u8 { + u8::try_from(self.xpub_keys.len()).unwrap() + } + + fn minimum_signatures(&self) -> u16 { + 1 + } + + fn receive_address(&self) -> Result
{ + self.derivation.receive_address_manager().current_address() + } + fn change_address(&self) -> Result
{ + self.derivation.change_address_manager().current_address() + } + + fn to_storage(&self) -> Result { + let settings = self.context().settings.clone(); + let storable = Payload::new(self.xpub_keys.clone(), self.ecdsa); + + let storage = AccountStorage::try_new( + BIP32_WATCH_ACCOUNT_KIND.into(), + self.id(), + self.storage_key(), + AssocPrvKeyDataIds::None, + settings, + storable, + )?; + + Ok(storage) + } + + fn metadata(&self) -> Result> { + let metadata = AccountMetadata::new(self.inner.id, self.derivation.address_derivation_meta()); + Ok(Some(metadata)) + } + + fn descriptor(&self) -> Result { + let descriptor = AccountDescriptor::new( + BIP32_WATCH_ACCOUNT_KIND.into(), + *self.id(), + self.name(), + self.balance(), + AssocPrvKeyDataIds::None, + self.receive_address().ok(), + self.change_address().ok(), + ) + .with_property(AccountDescriptorProperty::XpubKeys, self.xpub_keys.clone().into()) + .with_property(AccountDescriptorProperty::Ecdsa, self.ecdsa.into()) + .with_property(AccountDescriptorProperty::DerivationMeta, self.derivation.address_derivation_meta().into()); + + Ok(descriptor) + } + + fn as_derivation_capable(self: Arc) -> Result> { + Ok(self.clone()) + } +} + +impl DerivationCapableAccount for Bip32Watch { + fn derivation(&self) -> Arc { + self.derivation.clone() + } + + fn account_index(&self) -> u64 { + 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_storage_bip32watch() -> Result<()> { + let storable_in = Payload::new(vec![make_xpub()].into(), false); + let guard = StorageGuard::new(&storable_in); + let storable_out = guard.validate()?; + + assert_eq!(storable_in.ecdsa, storable_out.ecdsa); + assert_eq!(storable_in.xpub_keys.len(), storable_out.xpub_keys.len()); + for idx in 0..storable_in.xpub_keys.len() { + assert_eq!(storable_in.xpub_keys[idx], storable_out.xpub_keys[idx]); + } + + Ok(()) + } +} diff --git a/wallet/core/src/account/variants/keypair.rs b/wallet/core/src/account/variants/keypair.rs index b6c92907f3..6381ca046b 100644 --- a/wallet/core/src/account/variants/keypair.rs +++ b/wallet/core/src/account/variants/keypair.rs @@ -69,19 +69,17 @@ impl BorshSerialize for Payload { } impl BorshDeserialize for Payload { - fn deserialize(buf: &mut &[u8]) -> IoResult { + fn deserialize_reader(reader: &mut R) -> IoResult { use secp256k1::constants::PUBLIC_KEY_SIZE; let StorageHeader { version: _, .. } = - StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; - let public_key_bytes: [u8; PUBLIC_KEY_SIZE] = buf[..PUBLIC_KEY_SIZE] - .try_into() - .map_err(|_| IoError::new(IoErrorKind::Other, "Unable to deserialize keypair account (public_key buffer try_into)"))?; + 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) .map_err(|_| IoError::new(IoErrorKind::Other, "Unable to deserialize keypair account (invalid public key)"))?; - *buf = &buf[PUBLIC_KEY_SIZE..]; - let ecdsa = BorshDeserialize::deserialize(buf)?; + let ecdsa = BorshDeserialize::deserialize_reader(reader)?; Ok(Self { public_key, ecdsa }) } @@ -181,6 +179,7 @@ impl Account for Keypair { KEYPAIR_ACCOUNT_KIND.into(), *self.id(), self.name(), + self.balance(), self.prv_key_data_id.into(), self.receive_address().ok(), self.change_address().ok(), diff --git a/wallet/core/src/account/variants/legacy.rs b/wallet/core/src/account/variants/legacy.rs index 9967fb8614..cf05acf681 100644 --- a/wallet/core/src/account/variants/legacy.rs +++ b/wallet/core/src/account/variants/legacy.rs @@ -59,9 +59,9 @@ impl BorshSerialize for Payload { } impl BorshDeserialize for Payload { - fn deserialize(buf: &mut &[u8]) -> IoResult { + fn deserialize_reader(reader: &mut R) -> IoResult { let StorageHeader { version: _, .. } = - StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; Ok(Self {}) } @@ -192,6 +192,7 @@ impl Account for Legacy { LEGACY_ACCOUNT_KIND.into(), *self.id(), self.name(), + self.balance(), self.prv_key_data_id.into(), self.receive_address().ok(), self.change_address().ok(), diff --git a/wallet/core/src/account/variants/mod.rs b/wallet/core/src/account/variants/mod.rs index e321e95079..fa105b4e03 100644 --- a/wallet/core/src/account/variants/mod.rs +++ b/wallet/core/src/account/variants/mod.rs @@ -3,12 +3,14 @@ //! pub mod bip32; +pub mod bip32watch; pub mod keypair; pub mod legacy; pub mod multisig; pub mod resident; pub use bip32::BIP32_ACCOUNT_KIND; +pub use bip32watch::BIP32_WATCH_ACCOUNT_KIND; pub use keypair::KEYPAIR_ACCOUNT_KIND; pub use legacy::LEGACY_ACCOUNT_KIND; pub use multisig::MULTISIG_ACCOUNT_KIND; diff --git a/wallet/core/src/account/variants/multisig.rs b/wallet/core/src/account/variants/multisig.rs index 9a8044aa20..1128f5eef3 100644 --- a/wallet/core/src/account/variants/multisig.rs +++ b/wallet/core/src/account/variants/multisig.rs @@ -70,14 +70,14 @@ impl BorshSerialize for Payload { } impl BorshDeserialize for Payload { - fn deserialize(buf: &mut &[u8]) -> IoResult { + fn deserialize_reader(reader: &mut R) -> IoResult { let StorageHeader { version: _, .. } = - StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; - let xpub_keys = BorshDeserialize::deserialize(buf)?; - let cosigner_index = BorshDeserialize::deserialize(buf)?; - let minimum_signatures = BorshDeserialize::deserialize(buf)?; - let ecdsa = BorshDeserialize::deserialize(buf)?; + let xpub_keys = BorshDeserialize::deserialize_reader(reader)?; + let cosigner_index = BorshDeserialize::deserialize_reader(reader)?; + let minimum_signatures = BorshDeserialize::deserialize_reader(reader)?; + let ecdsa = BorshDeserialize::deserialize_reader(reader)?; Ok(Self { xpub_keys, cosigner_index, minimum_signatures, ecdsa }) } @@ -157,8 +157,8 @@ impl MultiSig { self.minimum_signatures } - pub fn xpub_keys(&self) -> &ExtendedPublicKeys { - &self.xpub_keys + fn watch_only(&self) -> bool { + self.prv_key_data_ids.is_none() } } @@ -172,6 +172,17 @@ impl Account for MultiSig { MULTISIG_ACCOUNT_KIND.into() } + fn feature(&self) -> Option { + match self.watch_only() { + true => Some("multisig-watch".to_string()), + false => None, + } + } + + fn xpub_keys(&self) -> Option<&ExtendedPublicKeys> { + Some(&self.xpub_keys) + } + fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { Err(Error::AccountKindFeature) } @@ -181,8 +192,7 @@ impl Account for MultiSig { } fn sig_op_count(&self) -> u8 { - // TODO @maxim - 1 + u8::try_from(self.xpub_keys.len()).unwrap() } fn minimum_signatures(&self) -> u16 { @@ -222,6 +232,7 @@ impl Account for MultiSig { MULTISIG_ACCOUNT_KIND.into(), *self.id(), self.name(), + self.balance(), self.prv_key_data_ids.clone().try_into()?, self.receive_address().ok(), self.change_address().ok(), diff --git a/wallet/core/src/account/variants/resident.rs b/wallet/core/src/account/variants/resident.rs index 74f3868965..c7e56d3d6c 100644 --- a/wallet/core/src/account/variants/resident.rs +++ b/wallet/core/src/account/variants/resident.rs @@ -77,6 +77,7 @@ impl Account for Resident { RESIDENT_ACCOUNT_KIND.into(), *self.id(), self.name(), + self.balance(), AssocPrvKeyDataIds::None, self.receive_address().ok(), self.change_address().ok(), diff --git a/wallet/core/src/account/variants/watchonly.rs b/wallet/core/src/account/variants/watchonly.rs new file mode 100644 index 0000000000..7212ffdfce --- /dev/null +++ b/wallet/core/src/account/variants/watchonly.rs @@ -0,0 +1,300 @@ +//! +//! Watch-only account implementation +//! + +use crate::account::Inner; +use crate::derivation::{AddressDerivationManager, AddressDerivationManagerTrait}; +use crate::imports::*; + +pub const WATCH_ONLY_ACCOUNT_KIND: &str = "kaspa-watch-only-standard"; + +pub struct Ctor {} + +#[async_trait] +impl Factory for Ctor { + fn name(&self) -> String { + "watchonly".to_string() + } + + fn description(&self) -> String { + "Kaspa Core watch-only Account".to_string() + } + + async fn try_load( + &self, + wallet: &Arc, + storage: &AccountStorage, + meta: Option>, + ) -> Result> { + Ok(Arc::new(watchonly::WatchOnly::try_load(wallet, storage, meta).await?)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub struct Payload { + pub xpub_keys: ExtendedPublicKeys, + pub minimum_signatures: u16, + pub ecdsa: bool, +} + +impl Payload { + pub fn new(xpub_keys: Arc>, minimum_signatures: u16, ecdsa: bool) -> Self { + Self { xpub_keys, minimum_signatures, ecdsa } + } + + pub fn try_load(storage: &AccountStorage) -> Result { + Ok(Self::try_from_slice(storage.serialized.as_slice())?) + } +} + +impl Storable for Payload { + // a unique number used for binary + // serialization data alignment check + const STORAGE_MAGIC: u32 = 0x92014137; + // binary serialization version + const STORAGE_VERSION: u32 = 0; +} + +impl AccountStorable for Payload {} + +impl BorshSerialize for Payload { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + BorshSerialize::serialize(&self.xpub_keys, writer)?; + BorshSerialize::serialize(&self.minimum_signatures, writer)?; + BorshSerialize::serialize(&self.ecdsa, writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for Payload { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + let xpub_keys = BorshDeserialize::deserialize(buf)?; + let minimum_signatures = BorshDeserialize::deserialize(buf)?; + let ecdsa = BorshDeserialize::deserialize(buf)?; + + Ok(Self { xpub_keys, minimum_signatures, ecdsa }) + } +} + +pub struct WatchOnly { + inner: Arc, + xpub_keys: ExtendedPublicKeys, + minimum_signatures: u16, + ecdsa: bool, + derivation: Arc, +} + +impl WatchOnly { + pub async fn try_new( + wallet: &Arc, + name: Option, + xpub_keys: ExtendedPublicKeys, + minimum_signatures: u16, + ecdsa: bool, + ) -> Result { + let settings = AccountSettings { name, ..Default::default() }; + + let public_key = xpub_keys.first().ok_or_else(|| Error::WatchOnlyXpubRequired)?.public_key(); + + let storable = Payload::new(xpub_keys.clone(), minimum_signatures, ecdsa); + + let (id, storage_key) = match xpub_keys.len() { + 1 => make_account_hashes(from_watch_only(&public_key)), + _ => make_account_hashes(from_watch_only_multisig(&None, &storable)), + }; + let inner = Arc::new(Inner::new(wallet, id, storage_key, settings)); + + let derivation = match xpub_keys.len() { + 1 => { + AddressDerivationManager::new( + wallet, + WATCH_ONLY_ACCOUNT_KIND.into(), + &xpub_keys, + ecdsa, + 0, + None, + 1, + Default::default(), + ) + .await? + } + _ => { + AddressDerivationManager::new( + wallet, + MULTISIG_ACCOUNT_KIND.into(), + &xpub_keys, + ecdsa, + 0, + Some(u32::MIN), + minimum_signatures, + Default::default(), + ) + .await? + } + }; + + Ok(Self { inner, xpub_keys, minimum_signatures, ecdsa, derivation }) + } + + pub async fn try_load(wallet: &Arc, storage: &AccountStorage, meta: Option>) -> Result { + let storable = Payload::try_load(storage)?; + let inner = Arc::new(Inner::from_storage(wallet, storage)); + let Payload { xpub_keys, minimum_signatures, ecdsa, .. } = storable; + let address_derivation_indexes = meta.and_then(|meta| meta.address_derivation_indexes()).unwrap_or_default(); + + let derivation = match xpub_keys.len() { + 1 => { + AddressDerivationManager::new( + wallet, + WATCH_ONLY_ACCOUNT_KIND.into(), + &xpub_keys, + ecdsa, + 0, + None, + 1, + address_derivation_indexes, + ) + .await? + } + _ => { + AddressDerivationManager::new( + wallet, + MULTISIG_ACCOUNT_KIND.into(), + &xpub_keys, + ecdsa, + 0, + Some(u32::MIN), + minimum_signatures, + address_derivation_indexes, + ) + .await? + } + }; + + Ok(Self { inner, xpub_keys, minimum_signatures, ecdsa, derivation }) + } + + pub fn get_address_range_for_scan(&self, range: std::ops::Range) -> Result> { + let receive_addresses = self.derivation.receive_address_manager().get_range_with_args(range.clone(), false)?; + let change_addresses = self.derivation.change_address_manager().get_range_with_args(range, false)?; + Ok(receive_addresses.into_iter().chain(change_addresses).collect::>()) + } + + pub fn xpub_keys(&self) -> &ExtendedPublicKeys { + &self.xpub_keys + } +} + +#[async_trait] +impl Account for WatchOnly { + fn inner(&self) -> &Arc { + &self.inner + } + + fn account_kind(&self) -> AccountKind { + WATCH_ONLY_ACCOUNT_KIND.into() + } + + fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { + Err(Error::WatchOnlyAccount) + } + + fn as_dyn_arc(self: Arc) -> Arc { + self + } + + fn sig_op_count(&self) -> u8 { + u8::try_from(self.xpub_keys.len()).unwrap() + } + + fn minimum_signatures(&self) -> u16 { + self.minimum_signatures + } + + fn receive_address(&self) -> Result
{ + self.derivation.receive_address_manager().current_address() + } + fn change_address(&self) -> Result
{ + self.derivation.change_address_manager().current_address() + } + + fn to_storage(&self) -> Result { + let settings = self.context().settings.clone(); + let storable = Payload::new(self.xpub_keys.clone(), self.minimum_signatures, self.ecdsa); + + let storage = AccountStorage::try_new( + WATCH_ONLY_ACCOUNT_KIND.into(), + self.id(), + self.storage_key(), + AssocPrvKeyDataIds::None, + settings, + storable, + )?; + + Ok(storage) + } + + fn metadata(&self) -> Result> { + let metadata = AccountMetadata::new(self.inner.id, self.derivation.address_derivation_meta()); + Ok(Some(metadata)) + } + + fn descriptor(&self) -> Result { + let descriptor = AccountDescriptor::new( + WATCH_ONLY_ACCOUNT_KIND.into(), + *self.id(), + self.name(), + AssocPrvKeyDataIds::None, + self.receive_address().ok(), + self.change_address().ok(), + ) + .with_property(AccountDescriptorProperty::XpubKeys, self.xpub_keys.clone().into()) + .with_property(AccountDescriptorProperty::Ecdsa, self.ecdsa.into()) + .with_property(AccountDescriptorProperty::DerivationMeta, self.derivation.address_derivation_meta().into()); + + Ok(descriptor) + } + + fn as_derivation_capable(self: Arc) -> Result> { + Ok(self.clone()) + } + +} + +impl DerivationCapableAccount for WatchOnly { + fn derivation(&self) -> Arc { + self.derivation.clone() + } + + fn account_index(&self) -> u64 { + 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_storage_watchonly() -> Result<()> { + let storable_in = Payload::new(vec![make_xpub()].into(), 1, false); + let guard = StorageGuard::new(&storable_in); + let storable_out = guard.validate()?; + + assert_eq!(storable_in.minimum_signatures, storable_out.minimum_signatures); + assert_eq!(storable_in.ecdsa, storable_out.ecdsa); + assert_eq!(storable_in.xpub_keys.len(), storable_out.xpub_keys.len()); + for idx in 0..storable_in.xpub_keys.len() { + assert_eq!(storable_in.xpub_keys[idx], storable_out.xpub_keys[idx]); + } + + Ok(()) + } +} diff --git a/wallet/core/src/api/message.rs b/wallet/core/src/api/message.rs index 9cd2f830cd..3b96abd1a5 100644 --- a/wallet/core/src/api/message.rs +++ b/wallet/core/src/api/message.rs @@ -44,6 +44,47 @@ pub struct FlushResponse {} pub struct ConnectRequest { pub url: Option, pub network_id: NetworkId, + // retry on error, otherwise give up + pub retry_on_error: bool, + // block async call until connected, otherwise return immediately + // and continue attempting to connect in the background + pub block_async_connect: bool, + // require node to be synced, fail otherwise + pub require_sync: bool, +} + +impl Default for ConnectRequest { + fn default() -> Self { + Self { + url: None, + network_id: NetworkId::new(NetworkType::Mainnet), + retry_on_error: true, + block_async_connect: true, + require_sync: true, + } + } +} + +impl ConnectRequest { + pub fn with_url(self, url: Option) -> Self { + ConnectRequest { url, ..self } + } + + pub fn with_network_id(self, network_id: &NetworkId) -> Self { + ConnectRequest { network_id: *network_id, ..self } + } + + pub fn with_retry_on_error(self, retry_on_error: bool) -> Self { + ConnectRequest { retry_on_error, ..self } + } + + pub fn with_block_async_connect(self, block_async_connect: bool) -> Self { + ConnectRequest { block_async_connect, ..self } + } + + pub fn with_require_sync(self, require_sync: bool) -> Self { + ConnectRequest { require_sync, ..self } + } } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] diff --git a/wallet/core/src/api/traits.rs b/wallet/core/src/api/traits.rs index 2eb12907a1..08ebd65f67 100644 --- a/wallet/core/src/api/traits.rs +++ b/wallet/core/src/api/traits.rs @@ -42,8 +42,12 @@ pub trait WalletApi: Send + Sync + AnySync { /// - `is_wrpc_client` - whether the wallet is connected to a node via wRPC async fn get_status_call(self: Arc, request: GetStatusRequest) -> Result; - async fn connect(self: Arc, url: Option, network_id: NetworkId) -> Result<()> { - self.connect_call(ConnectRequest { url, network_id }).await?; + /// Synchronous connect call (blocking, single attempt, requires sync). + async fn connect(self: Arc, url: Option, network_id: &NetworkId) -> Result<()> { + let retry_on_error = false; + let block_async_connect = true; + let require_sync = true; + self.connect_call(ConnectRequest { url, network_id: *network_id, retry_on_error, block_async_connect, require_sync }).await?; Ok(()) } diff --git a/wallet/core/src/api/transport.rs b/wallet/core/src/api/transport.rs index 9f6485ec8a..4de2d78248 100644 --- a/wallet/core/src/api/transport.rs +++ b/wallet/core/src/api/transport.rs @@ -18,7 +18,7 @@ use crate::imports::*; use crate::result::Result; use crate::wallet::Wallet; use async_trait::async_trait; -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh::BorshDeserialize; use kaspa_wallet_macros::{build_wallet_client_transport_interface, build_wallet_server_transport_interface}; use workflow_core::task::spawn; diff --git a/wallet/core/src/compat/gen1.rs b/wallet/core/src/compat/gen1.rs index cd66b10166..5bf5554f55 100644 --- a/wallet/core/src/compat/gen1.rs +++ b/wallet/core/src/compat/gen1.rs @@ -14,7 +14,7 @@ pub fn decrypt_mnemonic>( let mut aead = chacha20poly1305::XChaCha20Poly1305::new(Key::from_slice(&key)); let (nonce, ciphertext) = cipher.as_ref().split_at(24); - let decrypted = aead.decrypt(nonce.into(), ciphertext).unwrap(); + let decrypted = aead.decrypt(nonce.into(), ciphertext)?; Ok(unsafe { String::from_utf8_unchecked(decrypted) }) } @@ -36,8 +36,10 @@ mod test { ecdsa: false, }; - let decrypted = decrypt_mnemonic(8, file.encrypted_mnemonic, b"").unwrap(); - assert_eq!("dizzy uncover funny time weapon chat volume squirrel comic motion until diamond response remind hurt spider door strategy entire oyster hawk marriage soon fabric", decrypted); + let decrypted = decrypt_mnemonic(8, file.encrypted_mnemonic, b""); + log_info!("decrypted: {decrypted:?}"); + assert!(decrypted.is_ok(), "decrypt error"); + assert_eq!("dizzy uncover funny time weapon chat volume squirrel comic motion until diamond response remind hurt spider door strategy entire oyster hawk marriage soon fabric", decrypted.unwrap()); } #[tokio::test] diff --git a/wallet/core/src/derivation.rs b/wallet/core/src/derivation.rs index 15ad785035..2e598334e8 100644 --- a/wallet/core/src/derivation.rs +++ b/wallet/core/src/derivation.rs @@ -15,7 +15,7 @@ use crate::error::Error; use crate::imports::*; use crate::result::Result; use kaspa_bip32::{AddressType, DerivationPath, ExtendedPrivateKey, ExtendedPublicKey, Language, Mnemonic, SecretKeyExt}; -use kaspa_consensus_core::network::NetworkType; +use kaspa_consensus_core::network::{NetworkType, NetworkTypeT}; use kaspa_txscript::{ extract_script_pub_key_address, multisig_redeem_script, multisig_redeem_script_ecdsa, pay_to_script_hash_script, }; @@ -204,7 +204,7 @@ impl AddressDerivationManager { let derivator: Arc = match account_kind.as_ref() { LEGACY_ACCOUNT_KIND => Arc::new(WalletDerivationManagerV0::from_extended_public_key(xpub.clone(), cosigner_index)?), MULTISIG_ACCOUNT_KIND => { - let cosigner_index = cosigner_index.ok_or(Error::InvalidAccountKind)?; + let cosigner_index = cosigner_index.unwrap_or(0); Arc::new(WalletDerivationManager::from_extended_public_key(xpub.clone(), Some(cosigner_index))?) } _ => Arc::new(WalletDerivationManager::from_extended_public_key(xpub.clone(), cosigner_index)?), @@ -458,20 +458,26 @@ pub fn create_multisig_address( /// @category Wallet SDK #[wasm_bindgen(js_name=createAddress)] pub fn create_address_js( - key: PublicKeyT, - network_type: NetworkType, + key: &PublicKeyT, + network: &NetworkTypeT, ecdsa: Option, account_kind: Option, ) -> Result
{ let public_key = PublicKey::try_cast_from(key)?; - create_address(1, vec![public_key.as_ref().try_into()?], network_type.into(), ecdsa.unwrap_or(false), account_kind) + create_address( + 1, + vec![public_key.as_ref().try_into()?], + NetworkType::try_from(network)?.into(), + ecdsa.unwrap_or(false), + account_kind, + ) } /// @category Wallet SDK #[wasm_bindgen(js_name=createMultisigAddress)] pub fn create_multisig_address_js( minimum_signatures: usize, - keys: PublicKeyArrayT, + keys: &PublicKeyArrayT, network_type: NetworkType, ecdsa: Option, account_kind: Option, diff --git a/wallet/core/src/deterministic.rs b/wallet/core/src/deterministic.rs index c383da6012..648738a731 100644 --- a/wallet/core/src/deterministic.rs +++ b/wallet/core/src/deterministic.rs @@ -2,7 +2,7 @@ //! Deterministic byte sequence generation (used by Account ids). //! -pub use crate::account::{bip32, keypair, legacy, multisig}; +pub use crate::account::{bip32, bip32watch, keypair, legacy, multisig}; use crate::encryption::sha256_hash; use crate::imports::*; use crate::storage::PrvKeyDataId; @@ -101,7 +101,7 @@ where T: AsSlice + BorshSerialize, { let mut hashes: [Hash; N] = [Hash::default(); N]; - let bytes = hashable.try_to_vec().unwrap(); + let bytes = borsh::to_vec(&hashable).unwrap(); hashes[0] = Hash::from_slice(sha256_hash(&bytes).as_ref()); for i in 1..N { hashes[i] = Hash::from_slice(sha256_hash(&hashes[i - 1].as_bytes()).as_ref()); @@ -143,7 +143,23 @@ pub fn from_multisig(prv_key_data_ids: &Option( + prv_key_data_ids: &Option>>, + data: &bip32watch::Payload, +) -> [Hash; N] { + let hashable = DeterministicHashData { + account_kind: &multisig::MULTISIG_ACCOUNT_KIND.into(), + prv_key_data_ids, + ecdsa: Some(data.ecdsa), + account_index: None, + secp256k1_public_key: None, + data: Some(borsh::to_vec(&data.xpub_keys).unwrap()), }; make_hashes(hashable) } @@ -174,6 +190,19 @@ pub fn from_public_key(account_kind: &AccountKind, public_key: & make_hashes(hashable) } +/// Create deterministic hashes from bip32-watch. +pub fn from_bip32_watch(public_key: &PublicKey) -> [Hash; N] { + let hashable: DeterministicHashData<[PrvKeyDataId; 0]> = DeterministicHashData { + account_kind: &bip32watch::BIP32_WATCH_ACCOUNT_KIND.into(), + prv_key_data_ids: &None, + ecdsa: None, + account_index: Some(0), + secp256k1_public_key: Some(public_key.serialize().to_vec()), + data: None, + }; + make_hashes(hashable) +} + /// Create deterministic hashes from arbitrary data (supplied data slice must be deterministic). pub fn from_data(account_kind: &AccountKind, data: &[u8]) -> [Hash; N] { let hashable: DeterministicHashData<[PrvKeyDataId; 0]> = DeterministicHashData { diff --git a/wallet/core/src/encryption.rs b/wallet/core/src/encryption.rs index bee57a1f59..f07b49cec7 100644 --- a/wallet/core/src/encryption.rs +++ b/wallet/core/src/encryption.rs @@ -146,7 +146,7 @@ where } pub fn encrypt(&self, secret: &Secret, encryption_kind: EncryptionKind) -> Result { - let bytes = self.0.try_to_vec()?; + let bytes = borsh::to_vec(&self.0)?; let encrypted = match encryption_kind { EncryptionKind::XChaCha20Poly1305 => encrypt_xchacha20poly1305(bytes.as_slice(), secret)?, }; diff --git a/wallet/core/src/error.rs b/wallet/core/src/error.rs index a89b1dcf00..8992a8a924 100644 --- a/wallet/core/src/error.rs +++ b/wallet/core/src/error.rs @@ -13,6 +13,7 @@ use std::sync::PoisonError; use thiserror::Error; use wasm_bindgen::JsValue; use workflow_core::abortable::Aborted; +use workflow_core::channel::{RecvError, SendError, TrySendError}; use workflow_core::sendable::*; use workflow_rpc::client::error::Error as RpcError; use workflow_wasm::jserror::*; @@ -186,7 +187,7 @@ pub enum Error { #[error("{0}")] TryFromEnum(#[from] workflow_core::enums::TryFromError), - #[error("Account factory found for type: {0}")] + #[error("Account factory not found for type: {0}")] AccountFactoryNotFound(AccountKind), #[error("Account not found: {0}")] @@ -231,6 +232,12 @@ pub enum Error { #[error("Not allowed on a resident account")] ResidentAccount, + #[error("Not allowed on an bip32-watch account")] + Bip32WatchAccount, + + #[error("At least one xpub is required for a bip32-watch account")] + Bip32WatchXpubRequired, + #[error("This feature is not supported by this account type")] AccountKindFeature, @@ -326,6 +333,14 @@ pub enum Error { #[error(transparent)] Metrics(#[from] kaspa_metrics_core::error::Error), + + #[error("Connected node is not synced")] + NotSynced, + #[error(transparent)] + Pskt(#[from] kaspa_wallet_pskt::error::Error), + + #[error("Error generating pending transaction from PSKT: {0}")] + PendingTransactionFromPSKTError(String), } impl From for Error { @@ -409,8 +424,20 @@ impl From> for Error { } } -impl From> for Error { - fn from(e: workflow_core::channel::SendError) -> Self { +impl From> for Error { + fn from(e: SendError) -> Self { + Error::Custom(e.to_string()) + } +} + +impl From for Error { + fn from(e: RecvError) -> Self { + Error::Custom(e.to_string()) + } +} + +impl From> for Error { + fn from(e: TrySendError) -> Self { Error::Custom(e.to_string()) } } diff --git a/wallet/core/src/factory.rs b/wallet/core/src/factory.rs index c0472059c2..515093fdc7 100644 --- a/wallet/core/src/factory.rs +++ b/wallet/core/src/factory.rs @@ -32,6 +32,7 @@ pub fn factories() -> &'static FactoryMap { (LEGACY_ACCOUNT_KIND.into(), Arc::new(legacy::Ctor {})), (MULTISIG_ACCOUNT_KIND.into(), Arc::new(multisig::Ctor {})), (KEYPAIR_ACCOUNT_KIND.into(), Arc::new(keypair::Ctor {})), + (BIP32_WATCH_ACCOUNT_KIND.into(), Arc::new(bip32watch::Ctor {})), ]; let external = EXTERNAL.get_or_init(|| Mutex::new(AHashMap::new())).lock().unwrap().clone(); diff --git a/wallet/core/src/imports.rs b/wallet/core/src/imports.rs index 2d2ce79fda..94638bf78f 100644 --- a/wallet/core/src/imports.rs +++ b/wallet/core/src/imports.rs @@ -17,7 +17,6 @@ pub use crate::rpc::Rpc; pub use crate::rpc::{DynRpcApi, RpcCtl}; pub use crate::serializer::*; pub use crate::storage::*; -pub use crate::tx::MassCombinationStrategy; pub use crate::utxo::balance::Balance; pub use crate::utxo::scan::{Scan, ScanExtent}; pub use crate::utxo::{Maturity, NetworkParams, OutgoingTransaction, UtxoContext, UtxoEntryReference, UtxoProcessor}; @@ -49,6 +48,7 @@ pub use std::collections::{HashMap, HashSet}; pub use std::pin::Pin; pub use std::str::FromStr; pub use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; +pub use std::sync::LazyLock; pub use std::sync::{Arc, Mutex, MutexGuard, RwLock}; pub use std::task::{Context, Poll}; pub use wasm_bindgen::prelude::*; diff --git a/wallet/core/src/lib.rs b/wallet/core/src/lib.rs index 6029d6c4c4..bca89d4e7e 100644 --- a/wallet/core/src/lib.rs +++ b/wallet/core/src/lib.rs @@ -119,5 +119,10 @@ pub fn version() -> String { env!("CARGO_PKG_VERSION").to_string() } +/// Returns the version of the Wallet framework combined with short git hash. +pub fn version_with_git_hash() -> String { + kaspa_utils::git::with_short_hash(env!("CARGO_PKG_VERSION")).to_string() +} + #[cfg(test)] pub mod tests; diff --git a/wallet/core/src/prelude.rs b/wallet/core/src/prelude.rs index eb9bb2b1e9..0ca0194343 100644 --- a/wallet/core/src/prelude.rs +++ b/wallet/core/src/prelude.rs @@ -14,9 +14,14 @@ pub use crate::rpc::{ConnectOptions, ConnectStrategy, DynRpcApi}; pub use crate::settings::WalletSettings; pub use crate::storage::{IdT, Interface, PrvKeyDataId, PrvKeyDataInfo, TransactionId, TransactionRecord, WalletDescriptor}; pub use crate::tx::{Fees, PaymentDestination, PaymentOutput, PaymentOutputs}; +pub use crate::utils::{ + kaspa_suffix, kaspa_to_sompi, sompi_to_kaspa, sompi_to_kaspa_string, sompi_to_kaspa_string_with_suffix, try_kaspa_str_to_sompi, + try_kaspa_str_to_sompi_i64, +}; pub use crate::utxo::balance::{Balance, BalanceStrings}; pub use crate::wallet::args::*; pub use crate::wallet::Wallet; +pub use async_std::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; pub use kaspa_addresses::{Address, Prefix as AddressPrefix}; pub use kaspa_bip32::{Language, Mnemonic, WordCount}; pub use kaspa_wallet_keys::secret::Secret; diff --git a/wallet/core/src/serializer.rs b/wallet/core/src/serializer.rs index 391f813fa1..d7451a3c20 100644 --- a/wallet/core/src/serializer.rs +++ b/wallet/core/src/serializer.rs @@ -59,8 +59,8 @@ impl BorshSerialize for StorageHeader { } impl BorshDeserialize for StorageHeader { - fn deserialize(buf: &mut &[u8]) -> std::io::Result { - let (magic, version): (u32, u32) = BorshDeserialize::deserialize(buf)?; + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let (magic, version): (u32, u32) = BorshDeserialize::deserialize_reader(reader)?; Ok(Self { magic, version }) } } diff --git a/wallet/core/src/storage/account.rs b/wallet/core/src/storage/account.rs index da0c2df32b..a33585a255 100644 --- a/wallet/core/src/storage/account.rs +++ b/wallet/core/src/storage/account.rs @@ -26,10 +26,10 @@ impl BorshSerialize for AccountSettings { } impl BorshDeserialize for AccountSettings { - fn deserialize(buf: &mut &[u8]) -> IoResult { - let _version: u32 = BorshDeserialize::deserialize(buf)?; - let name = BorshDeserialize::deserialize(buf)?; - let meta = BorshDeserialize::deserialize(buf)?; + fn deserialize_reader(reader: &mut R) -> IoResult { + let _version: u32 = BorshDeserialize::deserialize_reader(reader)?; + let name = BorshDeserialize::deserialize_reader(reader)?; + let meta = BorshDeserialize::deserialize_reader(reader)?; Ok(Self { name, meta }) } @@ -63,7 +63,7 @@ impl AccountStorage { where A: AccountStorable, { - Ok(Self { id: *id, storage_key: *storage_key, kind, prv_key_data_ids, settings, serialized: serialized.try_to_vec()? }) + Ok(Self { id: *id, storage_key: *storage_key, kind, prv_key_data_ids, settings, serialized: borsh::to_vec(&serialized)? }) } pub fn id(&self) -> &AccountId { @@ -107,16 +107,16 @@ impl BorshSerialize for AccountStorage { } impl BorshDeserialize for AccountStorage { - fn deserialize(buf: &mut &[u8]) -> IoResult { + fn deserialize_reader(reader: &mut R) -> IoResult { let StorageHeader { version: _, .. } = - StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; - - let kind = BorshDeserialize::deserialize(buf)?; - let id = BorshDeserialize::deserialize(buf)?; - let storage_key = BorshDeserialize::deserialize(buf)?; - let prv_key_data_ids = BorshDeserialize::deserialize(buf)?; - let settings = BorshDeserialize::deserialize(buf)?; - let serialized = BorshDeserialize::deserialize(buf)?; + StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + let kind = BorshDeserialize::deserialize_reader(reader)?; + let id = BorshDeserialize::deserialize_reader(reader)?; + let storage_key = BorshDeserialize::deserialize_reader(reader)?; + let prv_key_data_ids = BorshDeserialize::deserialize_reader(reader)?; + let settings = BorshDeserialize::deserialize_reader(reader)?; + let serialized = BorshDeserialize::deserialize_reader(reader)?; Ok(Self { kind, id, storage_key, prv_key_data_ids, settings, serialized }) } diff --git a/wallet/core/src/storage/binding.rs b/wallet/core/src/storage/binding.rs index 45eac4a7e9..18f988e2d2 100644 --- a/wallet/core/src/storage/binding.rs +++ b/wallet/core/src/storage/binding.rs @@ -5,6 +5,45 @@ use crate::imports::*; use crate::utxo::{UtxoContextBinding as UtxoProcessorBinding, UtxoContextId}; +#[wasm_bindgen(typescript_custom_section)] +const ITransactionRecord: &'static str = r#" + +/** + * Type of a binding record. + * @see {@link IBinding}, {@link ITransactionDataVariant}, {@link ITransactionRecord} + * @category Wallet SDK + */ +export enum BindingType { + /** + * The data structure is associated with a user-supplied id. + * @see {@link IBinding} + */ + Custom = "custom", + /** + * The data structure is associated with a wallet account. + * @see {@link IBinding}, {@link Account} + */ + Account = "account", +} + +/** + * Internal transaction data contained within the transaction record. + * @see {@link ITransactionRecord} + * @category Wallet SDK + */ +export interface IBinding { + type : BindingType; + data : HexString; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = Object, typescript_type = "IBinding")] + #[derive(Clone, Debug, PartialEq, Eq)] + pub type BindingT; +} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "kebab-case")] #[serde(tag = "type", content = "id")] diff --git a/wallet/core/src/storage/keydata/data.rs b/wallet/core/src/storage/keydata/data.rs index 37a734854c..5b480d937c 100644 --- a/wallet/core/src/storage/keydata/data.rs +++ b/wallet/core/src/storage/keydata/data.rs @@ -44,11 +44,12 @@ impl BorshSerialize for PrvKeyDataVariant { } impl BorshDeserialize for PrvKeyDataVariant { - fn deserialize(buf: &mut &[u8]) -> IoResult { - let StorageHeader { version: _, .. } = StorageHeader::deserialize(buf)?.try_magic(Self::MAGIC)?.try_version(Self::VERSION)?; + fn deserialize_reader(reader: &mut R) -> IoResult { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize_reader(reader)?.try_magic(Self::MAGIC)?.try_version(Self::VERSION)?; - let kind: PrvKeyDataVariantKind = BorshDeserialize::deserialize(buf)?; - let string: String = BorshDeserialize::deserialize(buf)?; + let kind: PrvKeyDataVariantKind = BorshDeserialize::deserialize_reader(reader)?; + let string: String = BorshDeserialize::deserialize_reader(reader)?; match kind { PrvKeyDataVariantKind::Mnemonic => Ok(Self::Mnemonic(string)), diff --git a/wallet/core/src/storage/local/interface.rs b/wallet/core/src/storage/local/interface.rs index 1e998eb61f..c4ada71e07 100644 --- a/wallet/core/src/storage/local/interface.rs +++ b/wallet/core/src/storage/local/interface.rs @@ -131,7 +131,7 @@ impl LocalStoreInner { async fn try_export(&self, wallet_secret: &Secret, _options: WalletExportOptions) -> Result> { let wallet = self.cache.read().unwrap().to_wallet(None, wallet_secret)?; - Ok(wallet.try_to_vec()?) + Ok(borsh::to_vec(&wallet)?) } fn storage(&self) -> Arc { diff --git a/wallet/core/src/storage/local/payload.rs b/wallet/core/src/storage/local/payload.rs index 2cc5c9091e..8424dd4e19 100644 --- a/wallet/core/src/storage/local/payload.rs +++ b/wallet/core/src/storage/local/payload.rs @@ -67,13 +67,13 @@ impl BorshSerialize for Payload { } impl BorshDeserialize for Payload { - fn deserialize(buf: &mut &[u8]) -> IoResult { + fn deserialize_reader(reader: &mut R) -> IoResult { let StorageHeader { version: _, .. } = - StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; - let prv_key_data = BorshDeserialize::deserialize(buf)?; - let accounts = BorshDeserialize::deserialize(buf)?; - let address_book = BorshDeserialize::deserialize(buf)?; - let encrypt_transactions = BorshDeserialize::deserialize(buf)?; + StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + let prv_key_data = BorshDeserialize::deserialize_reader(reader)?; + let accounts = BorshDeserialize::deserialize_reader(reader)?; + let address_book = BorshDeserialize::deserialize_reader(reader)?; + let encrypt_transactions = BorshDeserialize::deserialize_reader(reader)?; Ok(Self { prv_key_data, accounts, address_book, encrypt_transactions }) } diff --git a/wallet/core/src/storage/local/transaction/fsio.rs b/wallet/core/src/storage/local/transaction/fsio.rs index b4a74334cd..a57a440112 100644 --- a/wallet/core/src/storage/local/transaction/fsio.rs +++ b/wallet/core/src/storage/local/transaction/fsio.rs @@ -65,7 +65,10 @@ impl TransactionStore { match fs::readdir(folder, true).await { Ok(mut files) => { // we reverse the order of the files so that the newest files are first - files.sort_by_key(|f| std::cmp::Reverse(f.metadata().unwrap().created())); + files.sort_by_key(|f| { + let meta = f.metadata().expect("fsio: missing file metadata"); + std::cmp::Reverse(meta.created().or_else(|| meta.modified()).unwrap_or_default()) + }); for file in files { if let Ok(id) = TransactionId::from_hex(file.file_name()) { @@ -315,6 +318,6 @@ async fn write(path: &Path, record: &TransactionRecord, secret: Option<&Secret>, } else { Encryptable::from(record.clone()) }; - fs::write(path, &data.try_to_vec()?).await?; + fs::write(path, &borsh::to_vec(&data)?).await?; Ok(()) } diff --git a/wallet/core/src/storage/local/transaction/indexdb.rs b/wallet/core/src/storage/local/transaction/indexdb.rs index e4190aa1ff..463508f611 100644 --- a/wallet/core/src/storage/local/transaction/indexdb.rs +++ b/wallet/core/src/storage/local/transaction/indexdb.rs @@ -26,35 +26,104 @@ pub struct Inner { impl Inner { async fn open_db(&self, db_name: String) -> Result { call_async_no_send!(async move { - let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&db_name, 1) + let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&db_name, 2) .map_err(|err| Error::Custom(format!("Failed to open indexdb database {:?}", err)))?; - - fn on_upgrade_needed(evt: &IdbVersionChangeEvent) -> Result<(), JsValue> { - // Check if the object store exists; create it if it doesn't - if !evt.db().object_store_names().any(|n| n == TRANSACTIONS_STORE_NAME) { + let fix_timestamp = Arc::new(Mutex::new(false)); + let fix_timestamp_clone = fix_timestamp.clone(); + let on_upgrade_needed = move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { + let old_version = evt.old_version(); + if old_version < 1.0 { let object_store = evt.db().create_object_store(TRANSACTIONS_STORE_NAME)?; + let db_index_params = IdbIndexParameters::new(); + db_index_params.set_unique(true); object_store.create_index_with_params( TRANSACTIONS_STORE_ID_INDEX, &IdbKeyPath::str(TRANSACTIONS_STORE_ID_INDEX), - IdbIndexParameters::new().unique(true), + &db_index_params, )?; object_store.create_index_with_params( TRANSACTIONS_STORE_TIMESTAMP_INDEX, &IdbKeyPath::str(TRANSACTIONS_STORE_TIMESTAMP_INDEX), - IdbIndexParameters::new().unique(false), + &db_index_params, )?; object_store.create_index_with_params( TRANSACTIONS_STORE_DATA_INDEX, &IdbKeyPath::str(TRANSACTIONS_STORE_DATA_INDEX), - IdbIndexParameters::new().unique(false), + &db_index_params, )?; + + // these changes are not required for new db + } else if old_version < 2.0 { + *fix_timestamp_clone.lock().unwrap() = true; } + // // Check if the object store exists; create it if it doesn't + // if !evt.db().object_store_names().any(|n| n == TRANSACTIONS_STORE_NAME) { + + // } Ok(()) - } + }; db_req.set_on_upgrade_needed(Some(on_upgrade_needed)); - db_req.await.map_err(|err| Error::Custom(format!("Open database request failed for indexdb database {:?}", err))) + let db = + db_req.await.map_err(|err| Error::Custom(format!("Open database request failed for indexdb database {:?}", err)))?; + + if *fix_timestamp.lock().unwrap() { + log_info!("DEBUG: fixing timestamp"); + let idb_tx = db + .transaction_on_one_with_mode(TRANSACTIONS_STORE_NAME, IdbTransactionMode::Readwrite) + .map_err(|err| Error::Custom(format!("Failed to open indexdb transaction for reading {:?}", err)))?; + let store = idb_tx + .object_store(TRANSACTIONS_STORE_NAME) + .map_err(|err| Error::Custom(format!("Failed to open indexdb object store for reading {:?}", err)))?; + let binding = store + .index(TRANSACTIONS_STORE_TIMESTAMP_INDEX) + .map_err(|err| Error::Custom(format!("Failed to open indexdb indexed store cursor {:?}", err)))?; + let cursor = binding + .open_cursor_with_range_and_direction(&JsValue::NULL, web_sys::IdbCursorDirection::Prev) + .map_err(|err| Error::Custom(format!("Failed to open indexdb store cursor for reading {:?}", err)))?; + let cursor = cursor.await.map_err(|err| Error::Custom(format!("Failed to open indexdb store cursor {:?}", err)))?; + + // let next_year_date = Date::new_0(); + // next_year_date.set_full_year(next_year_date.get_full_year() + 1); + // let next_year_ts = next_year_date.get_time(); + + if let Some(cursor) = cursor { + loop { + let js_value = cursor.value(); + if let Ok(record) = transaction_record_from_js_value(&js_value, None) { + if record.unixtime_msec.is_some() { + let new_js_value = transaction_record_to_js_value(&record, None, ENCRYPTION_KIND)?; + + //log_info!("DEBUG: new_js_value: {:?}", new_js_value); + + cursor + .update(&new_js_value) + .map_err(|err| Error::Custom(format!("Failed to update record timestamp {:?}", err)))? + .await + .map_err(|err| Error::Custom(format!("Failed to update record timestamp {:?}", err)))?; + } + } + if let Ok(b) = cursor.continue_cursor() { + match b.await { + Ok(b) => { + if !b { + break; + } + } + Err(err) => { + log_info!("DEBUG IDB: Loading transaction error, cursor.continue_cursor() {:?}", err); + break; + } + } + } else { + break; + } + } + } + } + + Ok(db) }) } } @@ -218,36 +287,73 @@ impl TransactionRecordStore for TransactionStore { binding: &Binding, network_id: &NetworkId, _filter: Option>, - _range: std::ops::Range, + range: std::ops::Range, ) -> Result { - // log_info!("DEBUG IDB: Loading transaction records for range {:?}", _range); - + log_info!("DEBUG IDB: Loading transaction records for range {:?}", range); let binding_str = binding.to_hex(); let network_id_str = network_id.to_string(); let db_name = self.make_db_name(&binding_str, &network_id_str); - let inner = self.inner().clone(); - call_async_no_send!(async move { let db = inner.open_db(db_name).await?; - let idb_tx = db .transaction_on_one_with_mode(TRANSACTIONS_STORE_NAME, IdbTransactionMode::Readonly) .map_err(|err| Error::Custom(format!("Failed to open indexdb transaction for reading {:?}", err)))?; - let store = idb_tx .object_store(TRANSACTIONS_STORE_NAME) .map_err(|err| Error::Custom(format!("Failed to open indexdb object store for reading {:?}", err)))?; - - let array = store - .get_all() - .map_err(|err| Error::Custom(format!("Failed to get transaction record from indexdb {:?}", err)))? + let total = store + .count() + .map_err(|err| Error::Custom(format!("Failed to count indexdb records {:?}", err)))? .await - .map_err(|err| Error::Custom(format!("Failed to get transaction record from indexdb {:?}", err)))?; - - let transactions = array + .map_err(|err| Error::Custom(format!("Failed to count indexdb records from future {:?}", err)))?; + + let binding = store + .index(TRANSACTIONS_STORE_TIMESTAMP_INDEX) + .map_err(|err| Error::Custom(format!("Failed to open indexdb indexed store cursor {:?}", err)))?; + let cursor = binding + .open_cursor_with_range_and_direction(&JsValue::NULL, web_sys::IdbCursorDirection::Prev) + .map_err(|err| Error::Custom(format!("Failed to open indexdb store cursor for reading {:?}", err)))?; + let mut records = vec![]; + let cursor = cursor.await.map_err(|err| Error::Custom(format!("Failed to open indexdb store cursor {:?}", err)))?; + if let Some(cursor) = cursor { + if range.start > 0 { + let res = cursor + .advance(range.start as u32) + .map_err(|err| Error::Custom(format!("Unable to advance indexdb cursor {:?}", err)))? + .await; + let _res = res.map_err(|err| Error::Custom(format!("Unable to advance indexdb cursor future {:?}", err)))?; + // if !res { + // //return Err(Error::Custom(format!("Unable to advance indexdb cursor future {:?}", err))); + // } + } + let count = range.end - range.start; + loop { + if records.len() < count { + records.push(cursor.value()); + if let Ok(b) = cursor.continue_cursor() { + match b.await { + Ok(b) => { + if !b { + break; + } + } + Err(err) => { + log_info!("DEBUG IDB: Loading transaction error, cursor.continue_cursor() {:?}", err); + break; + } + } + } else { + break; + } + } else { + break; + } + } + } + let transactions = records .iter() - .filter_map(|js_value| match transaction_record_from_js_value(&js_value, None) { + .filter_map(|js_value| match transaction_record_from_js_value(js_value, None) { Ok(transaction_record) => Some(Arc::new(transaction_record)), Err(err) => { log_error!("Failed to deserialize transaction record from indexdb {:?}", err); @@ -256,8 +362,7 @@ impl TransactionRecordStore for TransactionStore { }) .collect::>(); - let total = transactions.len() as u64; - Ok(TransactionRangeResult { transactions, total }) + Ok(TransactionRangeResult { transactions, total: total.into() }) }) } @@ -285,7 +390,7 @@ impl TransactionRecordStore for TransactionStore { let inner = inner_guard.lock().unwrap().clone(); call_async_no_send!(async move { - for (db_name, items) in &items.into_iter().group_by(|item| item.db_name.clone()) { + for (db_name, items) in &items.into_iter().chunk_by(|item| item.db_name.clone()) { let db = inner.open_db(db_name).await?; let idb_tx = db @@ -474,16 +579,14 @@ fn transaction_record_to_js_value( ) -> Result { let id = transaction_record.id.to_string(); let unixtime_msec = transaction_record.unixtime_msec; - let mut borsh_data = vec![]; - ::serialize(transaction_record, &mut borsh_data)?; let id_js_value = JsValue::from_str(&id); let timestamp_js_value = match unixtime_msec { Some(unixtime_msec) => { - let unixtime_sec = (unixtime_msec / 1000) as u32; + //let unixtime_sec = (unixtime_msec / 1000) as u32; let date = Date::new_0(); - date.set_utc_seconds(unixtime_sec); + date.set_time(unixtime_msec as f64); date.into() } None => JsValue::NULL, @@ -494,7 +597,7 @@ fn transaction_record_to_js_value( } else { Encryptable::from(transaction_record.clone()) }; - let encryped_data_vec = encryped_data.try_to_vec()?; + let encryped_data_vec = borsh::to_vec(&encryped_data)?; let borsh_data_uint8_arr = Uint8Array::from(encryped_data_vec.as_slice()); let borsh_data_js_value = borsh_data_uint8_arr.into(); @@ -519,6 +622,6 @@ fn transaction_record_from_js_value(js_value: &JsValue, secret: Option<&Secret>) Ok(transaction_record.0) } else { - Err(Error::Custom("supplied argument must be an object".to_string())) + Err(Error::Custom("supplied argument must be an object, found ({js_value:?})".to_string())) } } diff --git a/wallet/core/src/storage/local/wallet.rs b/wallet/core/src/storage/local/wallet.rs index afea36ad37..482ecd70ff 100644 --- a/wallet/core/src/storage/local/wallet.rs +++ b/wallet/core/src/storage/local/wallet.rs @@ -61,7 +61,7 @@ impl WalletStorage { cfg_if! { if #[cfg(target_arch = "wasm32")] { - let serialized = BorshSerialize::try_to_vec(self)?; + let serialized = borsh::to_vec(self)?; fs::write(store.filename(), serialized.as_slice()).await?; } else { // make this platform-specific to avoid creating @@ -101,8 +101,8 @@ impl BorshSerialize for WalletStorage { } impl BorshDeserialize for WalletStorage { - fn deserialize(buf: &mut &[u8]) -> IoResult { - let StorageHeader { magic, version, .. } = StorageHeader::deserialize(buf)?; + fn deserialize_reader(reader: &mut R) -> IoResult { + let StorageHeader { magic, version, .. } = StorageHeader::deserialize_reader(reader)?; if magic != Self::STORAGE_MAGIC { return Err(IoError::new( @@ -118,12 +118,12 @@ impl BorshDeserialize for WalletStorage { )); } - let title = BorshDeserialize::deserialize(buf)?; - let user_hint = BorshDeserialize::deserialize(buf)?; - let encryption_kind = BorshDeserialize::deserialize(buf)?; - let payload = BorshDeserialize::deserialize(buf)?; - let metadata = BorshDeserialize::deserialize(buf)?; - let transactions = BorshDeserialize::deserialize(buf)?; + let title = BorshDeserialize::deserialize_reader(reader)?; + let user_hint = BorshDeserialize::deserialize_reader(reader)?; + let encryption_kind = BorshDeserialize::deserialize_reader(reader)?; + let payload = BorshDeserialize::deserialize_reader(reader)?; + let metadata = BorshDeserialize::deserialize_reader(reader)?; + let transactions = BorshDeserialize::deserialize_reader(reader)?; Ok(Self { title, user_hint, encryption_kind, payload, metadata, transactions }) } diff --git a/wallet/core/src/storage/metadata.rs b/wallet/core/src/storage/metadata.rs index 0eacb902d0..d421a17edd 100644 --- a/wallet/core/src/storage/metadata.rs +++ b/wallet/core/src/storage/metadata.rs @@ -47,12 +47,12 @@ impl BorshSerialize for AccountMetadata { } impl BorshDeserialize for AccountMetadata { - fn deserialize(buf: &mut &[u8]) -> IoResult { + fn deserialize_reader(reader: &mut R) -> IoResult { let StorageHeader { version: _, .. } = - StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; - let id = BorshDeserialize::deserialize(buf)?; - let indexes = BorshDeserialize::deserialize(buf)?; + let id = BorshDeserialize::deserialize_reader(reader)?; + let indexes = BorshDeserialize::deserialize_reader(reader)?; Ok(Self { id, indexes }) } diff --git a/wallet/core/src/storage/mod.rs b/wallet/core/src/storage/mod.rs index 21b30186a6..2516bcd483 100644 --- a/wallet/core/src/storage/mod.rs +++ b/wallet/core/src/storage/mod.rs @@ -18,7 +18,7 @@ pub mod transaction; pub use account::{AccountSettings, AccountStorable, AccountStorage}; pub use address::AddressBookEntry; -pub use binding::Binding; +pub use binding::{Binding, BindingT}; pub use hint::Hint; pub use id::IdT; pub use interface::{ diff --git a/wallet/core/src/storage/transaction/data.rs b/wallet/core/src/storage/transaction/data.rs index 51fff3df80..e976574fd1 100644 --- a/wallet/core/src/storage/transaction/data.rs +++ b/wallet/core/src/storage/transaction/data.rs @@ -282,42 +282,42 @@ impl BorshSerialize for TransactionData { } impl BorshDeserialize for TransactionData { - fn deserialize(buf: &mut &[u8]) -> IoResult { + fn deserialize_reader(reader: &mut R) -> IoResult { let StorageHeader { version: _, .. } = - StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; - let kind: TransactionKind = BorshDeserialize::deserialize(buf)?; + let kind: TransactionKind = BorshDeserialize::deserialize_reader(reader)?; match kind { TransactionKind::Reorg => { - let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; - let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + let utxo_entries: Vec = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; Ok(TransactionData::Reorg { utxo_entries, aggregate_input_value }) } TransactionKind::Incoming => { - let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; - let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + let utxo_entries: Vec = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; Ok(TransactionData::Incoming { utxo_entries, aggregate_input_value }) } TransactionKind::Stasis => { - let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; - let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + let utxo_entries: Vec = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; Ok(TransactionData::Stasis { utxo_entries, aggregate_input_value }) } TransactionKind::External => { - let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; - let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + let utxo_entries: Vec = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; Ok(TransactionData::External { utxo_entries, aggregate_input_value }) } TransactionKind::Batch => { - let fees: u64 = BorshDeserialize::deserialize(buf)?; - let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; - let aggregate_output_value: u64 = BorshDeserialize::deserialize(buf)?; - let transaction: Transaction = BorshDeserialize::deserialize(buf)?; - let payment_value: Option = BorshDeserialize::deserialize(buf)?; - let change_value: u64 = BorshDeserialize::deserialize(buf)?; - let accepted_daa_score: Option = BorshDeserialize::deserialize(buf)?; - let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + let fees: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let transaction: Transaction = BorshDeserialize::deserialize_reader(reader)?; + let payment_value: Option = BorshDeserialize::deserialize_reader(reader)?; + let change_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize_reader(reader)?; + let utxo_entries: Vec = BorshDeserialize::deserialize_reader(reader)?; Ok(TransactionData::Batch { fees, aggregate_input_value, @@ -330,14 +330,14 @@ impl BorshDeserialize for TransactionData { }) } TransactionKind::Outgoing => { - let fees: u64 = BorshDeserialize::deserialize(buf)?; - let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; - let aggregate_output_value: u64 = BorshDeserialize::deserialize(buf)?; - let transaction: Transaction = BorshDeserialize::deserialize(buf)?; - let payment_value: Option = BorshDeserialize::deserialize(buf)?; - let change_value: u64 = BorshDeserialize::deserialize(buf)?; - let accepted_daa_score: Option = BorshDeserialize::deserialize(buf)?; - let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + let fees: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let transaction: Transaction = BorshDeserialize::deserialize_reader(reader)?; + let payment_value: Option = BorshDeserialize::deserialize_reader(reader)?; + let change_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize_reader(reader)?; + let utxo_entries: Vec = BorshDeserialize::deserialize_reader(reader)?; Ok(TransactionData::Outgoing { fees, aggregate_input_value, @@ -350,14 +350,14 @@ impl BorshDeserialize for TransactionData { }) } TransactionKind::TransferIncoming => { - let fees: u64 = BorshDeserialize::deserialize(buf)?; - let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; - let aggregate_output_value: u64 = BorshDeserialize::deserialize(buf)?; - let transaction: Transaction = BorshDeserialize::deserialize(buf)?; - let payment_value: Option = BorshDeserialize::deserialize(buf)?; - let change_value: u64 = BorshDeserialize::deserialize(buf)?; - let accepted_daa_score: Option = BorshDeserialize::deserialize(buf)?; - let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + let fees: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let transaction: Transaction = BorshDeserialize::deserialize_reader(reader)?; + let payment_value: Option = BorshDeserialize::deserialize_reader(reader)?; + let change_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize_reader(reader)?; + let utxo_entries: Vec = BorshDeserialize::deserialize_reader(reader)?; Ok(TransactionData::TransferIncoming { fees, aggregate_input_value, @@ -370,14 +370,14 @@ impl BorshDeserialize for TransactionData { }) } TransactionKind::TransferOutgoing => { - let fees: u64 = BorshDeserialize::deserialize(buf)?; - let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; - let aggregate_output_value: u64 = BorshDeserialize::deserialize(buf)?; - let transaction: Transaction = BorshDeserialize::deserialize(buf)?; - let payment_value: Option = BorshDeserialize::deserialize(buf)?; - let change_value: u64 = BorshDeserialize::deserialize(buf)?; - let accepted_daa_score: Option = BorshDeserialize::deserialize(buf)?; - let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + let fees: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let transaction: Transaction = BorshDeserialize::deserialize_reader(reader)?; + let payment_value: Option = BorshDeserialize::deserialize_reader(reader)?; + let change_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize_reader(reader)?; + let utxo_entries: Vec = BorshDeserialize::deserialize_reader(reader)?; Ok(TransactionData::TransferOutgoing { fees, aggregate_input_value, @@ -390,13 +390,13 @@ impl BorshDeserialize for TransactionData { }) } TransactionKind::Change => { - let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; - let aggregate_output_value: u64 = BorshDeserialize::deserialize(buf)?; - let transaction: Transaction = BorshDeserialize::deserialize(buf)?; - let payment_value: Option = BorshDeserialize::deserialize(buf)?; - let change_value: u64 = BorshDeserialize::deserialize(buf)?; - let accepted_daa_score: Option = BorshDeserialize::deserialize(buf)?; - let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let transaction: Transaction = BorshDeserialize::deserialize_reader(reader)?; + let payment_value: Option = BorshDeserialize::deserialize_reader(reader)?; + let change_value: u64 = BorshDeserialize::deserialize_reader(reader)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize_reader(reader)?; + let utxo_entries: Vec = BorshDeserialize::deserialize_reader(reader)?; Ok(TransactionData::Change { aggregate_input_value, aggregate_output_value, diff --git a/wallet/core/src/storage/transaction/record.rs b/wallet/core/src/storage/transaction/record.rs index 7a04571331..05be3b69f2 100644 --- a/wallet/core/src/storage/transaction/record.rs +++ b/wallet/core/src/storage/transaction/record.rs @@ -4,7 +4,7 @@ use super::*; use crate::imports::*; -use crate::storage::Binding; +use crate::storage::{Binding, BindingT}; use crate::tx::PendingTransactionInner; use workflow_core::time::{unixtime_as_millis_u64, unixtime_to_locale_string}; use workflow_wasm::utils::try_get_js_value_prop; @@ -289,7 +289,9 @@ export interface ITransactionRecord { extern "C" { #[wasm_bindgen(extends = Object, typescript_type = "ITransactionRecord")] #[derive(Clone, Debug, PartialEq, Eq)] - pub type ITransactionRecord; + pub type TransactionRecordT; + #[wasm_bindgen(extends = Object, typescript_type = "ITransactionData")] + pub type TransactionDataT; } #[wasm_bindgen(inspectable)] @@ -318,11 +320,12 @@ pub struct TransactionRecord { #[serde(rename = "unixtimeMsec")] #[wasm_bindgen(js_name = unixtimeMsec)] pub unixtime_msec: Option, + #[wasm_bindgen(skip)] pub value: u64, #[wasm_bindgen(skip)] pub binding: Binding, #[serde(rename = "blockDaaScore")] - #[wasm_bindgen(js_name = blockDaaScore)] + #[wasm_bindgen(skip)] pub block_daa_score: u64, #[serde(rename = "network")] #[wasm_bindgen(js_name = network)] @@ -378,9 +381,9 @@ impl TransactionRecord { let params = NetworkParams::from(self.network_id); let maturity = if self.is_coinbase() { - params.coinbase_transaction_maturity_period_daa + params.coinbase_transaction_maturity_period_daa() } else { - params.user_transaction_maturity_period_daa + params.user_transaction_maturity_period_daa() }; if current_daa_score < self.block_daa_score() + maturity { @@ -431,9 +434,9 @@ impl TransactionRecord { pub fn maturity_progress(&self, current_daa_score: u64) -> Option { let params = NetworkParams::from(self.network_id); let maturity = if self.is_coinbase() { - params.coinbase_transaction_maturity_period_daa + params.coinbase_transaction_maturity_period_daa() } else { - params.user_transaction_maturity_period_daa + params.user_transaction_maturity_period_daa() }; if current_daa_score < self.block_daa_score + maturity { @@ -784,14 +787,24 @@ impl TransactionRecord { #[wasm_bindgen] impl TransactionRecord { + #[wasm_bindgen(getter, js_name = "value")] + pub fn value_as_js_bigint(&self) -> BigInt { + self.value.into() + } + + #[wasm_bindgen(getter, js_name = "blockDaaScore")] + pub fn block_daa_score_as_js_bigint(&self) -> BigInt { + self.block_daa_score.into() + } + #[wasm_bindgen(getter, js_name = "binding")] - pub fn binding_as_js_value(&self) -> JsValue { - serde_wasm_bindgen::to_value(&self.binding).unwrap() + pub fn binding_as_js_value(&self) -> BindingT { + serde_wasm_bindgen::to_value(&self.binding).unwrap().unchecked_into() } #[wasm_bindgen(getter, js_name = "data")] - pub fn data_as_js_value(&self) -> JsValue { - try_get_js_value_prop(&serde_wasm_bindgen::to_value(&self.transaction_data).unwrap(), "data").unwrap() + pub fn data_as_js_value(&self) -> TransactionDataT { + try_get_js_value_prop(&serde_wasm_bindgen::to_value(&self.transaction_data).unwrap(), "data").unwrap().unchecked_into() } #[wasm_bindgen(getter, js_name = "type")] @@ -837,19 +850,19 @@ impl BorshSerialize for TransactionRecord { } impl BorshDeserialize for TransactionRecord { - fn deserialize(buf: &mut &[u8]) -> IoResult { + fn deserialize_reader(reader: &mut R) -> IoResult { let StorageHeader { version: _, .. } = - StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; - - let id = BorshDeserialize::deserialize(buf)?; - let unixtime = BorshDeserialize::deserialize(buf)?; - let value = BorshDeserialize::deserialize(buf)?; - let binding = BorshDeserialize::deserialize(buf)?; - let block_daa_score = BorshDeserialize::deserialize(buf)?; - let network_id = BorshDeserialize::deserialize(buf)?; - let transaction_data = BorshDeserialize::deserialize(buf)?; - let note = BorshDeserialize::deserialize(buf)?; - let metadata = BorshDeserialize::deserialize(buf)?; + StorageHeader::deserialize_reader(reader)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + let id = BorshDeserialize::deserialize_reader(reader)?; + let unixtime = BorshDeserialize::deserialize_reader(reader)?; + let value = BorshDeserialize::deserialize_reader(reader)?; + let binding = BorshDeserialize::deserialize_reader(reader)?; + let block_daa_score = BorshDeserialize::deserialize_reader(reader)?; + let network_id = BorshDeserialize::deserialize_reader(reader)?; + let transaction_data = BorshDeserialize::deserialize_reader(reader)?; + let note = BorshDeserialize::deserialize_reader(reader)?; + let metadata = BorshDeserialize::deserialize_reader(reader)?; Ok(Self { id, unixtime_msec: unixtime, value, binding, block_daa_score, network_id, transaction_data, note, metadata }) } @@ -861,7 +874,7 @@ impl BorshDeserialize for TransactionRecord { // } // } -impl From for ITransactionRecord { +impl From for TransactionRecordT { fn from(record: TransactionRecord) -> Self { JsValue::from(record).unchecked_into() } diff --git a/wallet/core/src/tests/rpc_core_mock.rs b/wallet/core/src/tests/rpc_core_mock.rs index 1e6d70c2bb..3e1c302dd4 100644 --- a/wallet/core/src/tests/rpc_core_mock.rs +++ b/wallet/core/src/tests/rpc_core_mock.rs @@ -9,7 +9,7 @@ use kaspa_notify::scope::Scope; use kaspa_notify::subscription::context::SubscriptionContext; use kaspa_notify::subscription::{MutationPolicies, UtxosChangedMutationPolicy}; use kaspa_rpc_core::api::ctl::RpcCtl; -use kaspa_rpc_core::{api::rpc::RpcApi, *}; +use kaspa_rpc_core::{api::connection::DynRpcConnection, api::rpc::RpcApi, *}; use kaspa_rpc_core::{notify::connection::ChannelConnection, RpcResult}; use std::sync::Arc; @@ -83,7 +83,7 @@ impl Default for RpcCoreMock { #[async_trait] impl RpcApi for RpcCoreMock { // This fn needs to succeed while the client connects - async fn get_info_call(&self, _request: GetInfoRequest) -> RpcResult { + async fn get_info_call(&self, _connection: Option<&DynRpcConnection>, _request: GetInfoRequest) -> RpcResult { Ok(GetInfoResponse { p2p_id: "wallet-mock".to_string(), mempool_size: 1234, @@ -95,140 +95,237 @@ impl RpcApi for RpcCoreMock { }) } - async fn ping_call(&self, _request: PingRequest) -> RpcResult { + async fn ping_call(&self, _connection: Option<&DynRpcConnection>, _request: PingRequest) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_metrics_call(&self, _request: GetMetricsRequest) -> RpcResult { + async fn get_metrics_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetMetricsRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_server_info_call(&self, _request: GetServerInfoRequest) -> RpcResult { + async fn get_connections_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetConnectionsRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_sync_status_call(&self, _request: GetSyncStatusRequest) -> RpcResult { + async fn get_server_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetServerInfoRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_current_network_call(&self, _request: GetCurrentNetworkRequest) -> RpcResult { + async fn get_system_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetSystemInfoRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn submit_block_call(&self, _request: SubmitBlockRequest) -> RpcResult { + async fn get_sync_status_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetSyncStatusRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_block_template_call(&self, _request: GetBlockTemplateRequest) -> RpcResult { + async fn get_current_network_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetCurrentNetworkRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_peer_addresses_call(&self, _request: GetPeerAddressesRequest) -> RpcResult { + async fn submit_block_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: SubmitBlockRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_sink_call(&self, _request: GetSinkRequest) -> RpcResult { + async fn get_block_template_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetBlockTemplateRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_mempool_entry_call(&self, _request: GetMempoolEntryRequest) -> RpcResult { + async fn get_peer_addresses_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetPeerAddressesRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_mempool_entries_call(&self, _request: GetMempoolEntriesRequest) -> RpcResult { + async fn get_sink_call(&self, _connection: Option<&DynRpcConnection>, _request: GetSinkRequest) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_connected_peer_info_call(&self, _request: GetConnectedPeerInfoRequest) -> RpcResult { + async fn get_mempool_entry_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetMempoolEntryRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn add_peer_call(&self, _request: AddPeerRequest) -> RpcResult { + async fn get_mempool_entries_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetMempoolEntriesRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn submit_transaction_call(&self, _request: SubmitTransactionRequest) -> RpcResult { + async fn get_connected_peer_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetConnectedPeerInfoRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn submit_transaction_replacement_call( &self, + _connection: Option<&DynRpcConnection>, _request: SubmitTransactionReplacementRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_block_call(&self, _request: GetBlockRequest) -> RpcResult { + async fn add_peer_call(&self, _connection: Option<&DynRpcConnection>, _request: AddPeerRequest) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_subnetwork_call(&self, _request: GetSubnetworkRequest) -> RpcResult { + async fn submit_transaction_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: SubmitTransactionRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_block_call(&self, _connection: Option<&DynRpcConnection>, _request: GetBlockRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_subnetwork_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetSubnetworkRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn get_virtual_chain_from_block_call( &self, + _connection: Option<&DynRpcConnection>, _request: GetVirtualChainFromBlockRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_blocks_call(&self, _request: GetBlocksRequest) -> RpcResult { + async fn get_blocks_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetBlocksRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_block_count_call(&self, _request: GetBlockCountRequest) -> RpcResult { + async fn get_block_count_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetBlockCountRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_block_dag_info_call(&self, _request: GetBlockDagInfoRequest) -> RpcResult { + async fn get_block_dag_info_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetBlockDagInfoRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn resolve_finality_conflict_call( &self, + _connection: Option<&DynRpcConnection>, _request: ResolveFinalityConflictRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn shutdown_call(&self, _request: ShutdownRequest) -> RpcResult { + async fn shutdown_call(&self, _connection: Option<&DynRpcConnection>, _request: ShutdownRequest) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_headers_call(&self, _request: GetHeadersRequest) -> RpcResult { + async fn get_headers_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetHeadersRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_balance_by_address_call(&self, _request: GetBalanceByAddressRequest) -> RpcResult { + async fn get_balance_by_address_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetBalanceByAddressRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn get_balances_by_addresses_call( &self, + _connection: Option<&DynRpcConnection>, _request: GetBalancesByAddressesRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_utxos_by_addresses_call(&self, _request: GetUtxosByAddressesRequest) -> RpcResult { + async fn get_utxos_by_addresses_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetUtxosByAddressesRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_sink_blue_score_call(&self, _request: GetSinkBlueScoreRequest) -> RpcResult { + async fn get_sink_blue_score_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetSinkBlueScoreRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn ban_call(&self, _request: BanRequest) -> RpcResult { + async fn ban_call(&self, _connection: Option<&DynRpcConnection>, _request: BanRequest) -> RpcResult { Err(RpcError::NotImplemented) } - async fn unban_call(&self, _request: UnbanRequest) -> RpcResult { + async fn unban_call(&self, _connection: Option<&DynRpcConnection>, _request: UnbanRequest) -> RpcResult { Err(RpcError::NotImplemented) } async fn estimate_network_hashes_per_second_call( &self, + _connection: Option<&DynRpcConnection>, _request: EstimateNetworkHashesPerSecondRequest, ) -> RpcResult { Err(RpcError::NotImplemented) @@ -236,28 +333,39 @@ impl RpcApi for RpcCoreMock { async fn get_mempool_entries_by_addresses_call( &self, + _connection: Option<&DynRpcConnection>, _request: GetMempoolEntriesByAddressesRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_coin_supply_call(&self, _request: GetCoinSupplyRequest) -> RpcResult { + async fn get_coin_supply_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetCoinSupplyRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn get_daa_score_timestamp_estimate_call( &self, + _connection: Option<&DynRpcConnection>, _request: GetDaaScoreTimestampEstimateRequest, ) -> RpcResult { Err(RpcError::NotImplemented) } - async fn get_fee_estimate_call(&self, _request: GetFeeEstimateRequest) -> RpcResult { + async fn get_fee_estimate_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetFeeEstimateRequest, + ) -> RpcResult { Err(RpcError::NotImplemented) } async fn get_fee_estimate_experimental_call( &self, + _connection: Option<&DynRpcConnection>, _request: GetFeeEstimateExperimentalRequest, ) -> RpcResult { Err(RpcError::NotImplemented) diff --git a/wallet/core/src/tests/storage.rs b/wallet/core/src/tests/storage.rs index 7257311602..5af0bcbae5 100644 --- a/wallet/core/src/tests/storage.rs +++ b/wallet/core/src/tests/storage.rs @@ -20,11 +20,11 @@ where } pub fn validate(&self) -> Result { - let bytes = self.try_to_vec()?; + let bytes = borsh::to_vec(self)?; let transform = Self::try_from_slice(bytes.as_slice())?; assert_eq!(transform.before, 0xdeadbeef); assert_eq!(transform.after, 0xbaadf00d); - let transform_bytes = transform.try_to_vec()?; + let transform_bytes = borsh::to_vec(&transform)?; assert_eq!(bytes, transform_bytes); Ok(transform.storable) } diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 7040276b1c..359ee634a4 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -66,6 +66,7 @@ use crate::tx::{ use crate::utxo::{NetworkParams, UtxoContext, UtxoEntryReference}; use kaspa_consensus_client::UtxoEntry; use kaspa_consensus_core::constants::UNACCEPTED_DAA_SCORE; +use kaspa_consensus_core::mass::Kip9Version; use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; use kaspa_consensus_core::tx::{Transaction, TransactionInput, TransactionOutpoint, TransactionOutput}; use kaspa_txscript::pay_to_address_script; @@ -88,6 +89,11 @@ const TRANSACTION_MASS_BOUNDARY_FOR_STAGE_INPUT_ACCUMULATION: u64 = MAXIMUM_STAN struct Context { /// iterator containing UTXO entries available for transaction generation utxo_source_iterator: Box + Send + Sync + 'static>, + /// List of priority UTXO entries, that are consumed before polling the iterator + priority_utxo_entries: Option>, + /// HashSet containing priority UTXO entries, used for filtering + /// for potential duplicates from the iterator + priority_utxo_entry_filter: Option>, /// total number of UTXOs consumed by the single generator instance aggregated_utxos: usize, /// total fees of all transactions issued by @@ -210,7 +216,7 @@ struct Data { impl Data { fn new(calc: &MassCalculator) -> Self { - let aggregate_mass = calc.blank_transaction_mass(); + let aggregate_mass = calc.blank_transaction_compute_mass(); Data { inputs: vec![], @@ -261,7 +267,7 @@ struct Inner { // Current network id network_id: NetworkId, // Current network params - network_params: NetworkParams, + network_params: &'static NetworkParams, // Source Utxo Context (Used for source UtxoEntry aggregation) source_utxo_context: Option, @@ -340,6 +346,7 @@ impl Generator { multiplexer, utxo_iterator, source_utxo_context: utxo_context, + priority_utxo_entries, sig_op_count, minimum_signatures, change_address, @@ -351,7 +358,7 @@ impl Generator { let network_type = NetworkType::from(network_id); let network_params = NetworkParams::from(network_id); - let mass_calculator = MassCalculator::new(&network_id.into(), &network_params); + let mass_calculator = MassCalculator::new(&network_id.into(), network_params); let (final_transaction_outputs, final_transaction_amount) = match final_transaction_destination { PaymentDestination::Change => { @@ -396,11 +403,11 @@ impl Generator { } let standard_change_output_mass = - mass_calculator.calc_mass_for_output(&TransactionOutput::new(0, pay_to_address_script(&change_address))); - let signature_mass_per_input = mass_calculator.calc_signature_mass(minimum_signatures); - let final_transaction_outputs_compute_mass = mass_calculator.calc_mass_for_outputs(&final_transaction_outputs); + mass_calculator.calc_compute_mass_for_output(&TransactionOutput::new(0, pay_to_address_script(&change_address))); + let signature_mass_per_input = mass_calculator.calc_compute_mass_for_signature(minimum_signatures); + let final_transaction_outputs_compute_mass = mass_calculator.calc_compute_mass_for_outputs(&final_transaction_outputs); let final_transaction_payload = final_transaction_payload.unwrap_or_default(); - let final_transaction_payload_mass = mass_calculator.calc_mass_for_payload(final_transaction_payload.len()); + let final_transaction_payload_mass = mass_calculator.calc_compute_mass_for_payload(final_transaction_payload.len()); let final_transaction_outputs_harmonic = mass_calculator.calc_storage_mass_output_harmonic(&final_transaction_outputs).ok_or(Error::MassCalculationError)?; @@ -415,8 +422,14 @@ impl Generator { return Err(Error::GeneratorTransactionOutputsAreTooHeavy { mass: mass_sanity_check, kind: "compute mass" }); } + let priority_utxo_entry_filter = priority_utxo_entries.as_ref().map(|entries| entries.iter().cloned().collect()); + // remap to VecDeque as this list gets drained + let priority_utxo_entries = priority_utxo_entries.map(|entries| entries.into_iter().collect::>()); + let context = Mutex::new(Context { utxo_source_iterator: utxo_iterator, + priority_utxo_entries, + priority_utxo_entry_filter, number_of_transactions: 0, aggregated_utxos: 0, aggregate_fees: 0, @@ -465,7 +478,7 @@ impl Generator { /// Returns current [`NetworkParams`] pub fn network_params(&self) -> &NetworkParams { - &self.inner.network_params + self.inner.network_params } /// The underlying [`UtxoContext`] (if available). @@ -528,15 +541,29 @@ impl Generator { } /// Get next UTXO entry. This function obtains UTXO in the following order: - /// 1. From the UTXO stash (used to store UTxOs that were not used in the previous transaction) + /// 1. From the UTXO stash (used to store UTxOs that were consumed during previous transaction generation but were rejected due to various conditions, such as mass overflow) /// 2. From the current stage - /// 3. From the UTXO source iterator + /// 3. From priority UTXO entries + /// 4. From the UTXO source iterator (while filtering against priority UTXO entries) fn get_utxo_entry(&self, context: &mut Context, stage: &mut Stage) -> Option { context .utxo_stash .pop_front() .or_else(|| stage.utxo_iterator.as_mut().and_then(|utxo_stage_iterator| utxo_stage_iterator.next())) - .or_else(|| context.utxo_source_iterator.next()) + .or_else(|| context.priority_utxo_entries.as_mut().and_then(|entries| entries.pop_front())) + .or_else(|| loop { + let utxo_entry = context.utxo_source_iterator.next()?; + + if let Some(filter) = context.priority_utxo_entry_filter.as_ref() { + if filter.contains(&utxo_entry) { + // skip the entry from the iterator intake + // if it has been supplied as a priority entry + continue; + } + } + + break Some(utxo_entry); + }) } /// Calculate relay transaction mass for the current transaction `data` @@ -636,14 +663,14 @@ impl Generator { let input = TransactionInput::new(utxo.outpoint.clone().into(), vec![], 0, self.inner.sig_op_count); let input_amount = utxo.amount(); - let input_compute_mass = calc.calc_mass_for_input(&input) + self.inner.signature_mass_per_input; + let input_compute_mass = calc.calc_compute_mass_for_input(&input) + self.inner.signature_mass_per_input; // NOTE: relay transactions have no storage mass // mass threshold reached, yield transaction if data.aggregate_mass + input_compute_mass + self.inner.standard_change_output_compute_mass - + self.inner.network_params.additional_compound_transaction_mass + + self.inner.network_params.additional_compound_transaction_mass() > MAXIMUM_STANDARD_TRANSACTION_MASS { // note, we've used input for mass boundary calc and now abandon it @@ -651,7 +678,7 @@ impl Generator { context.utxo_stash.push_back(utxo_entry_reference); data.aggregate_mass += - self.inner.standard_change_output_compute_mass + self.inner.network_params.additional_compound_transaction_mass; + self.inner.standard_change_output_compute_mass + self.inner.network_params.additional_compound_transaction_mass(); data.transaction_fees = self.calc_relay_transaction_compute_fees(data); stage.aggregate_fees += data.transaction_fees; context.aggregate_fees += data.transaction_fees; @@ -839,8 +866,11 @@ impl Generator { calc.calc_storage_mass_output_harmonic_single(change_value) + self.inner.final_transaction_outputs_harmonic; let storage_mass_with_change = self.calc_storage_mass(data, output_harmonic_with_change); + // TODO - review and potentially simplify: + // this profiles the storage mass with change and without change + // and decides which one to use based on the fees if storage_mass_with_change == 0 - || (self.inner.network_params.mass_combination_strategy == MassCombinationStrategy::Max + || (self.inner.network_params.kip9_version() == Kip9Version::Beta // max(compute vs storage) && storage_mass_with_change < compute_mass_with_change) { 0 @@ -881,7 +911,7 @@ impl Generator { let compute_mass = data.aggregate_mass + self.inner.standard_change_output_compute_mass - + self.inner.network_params.additional_compound_transaction_mass; + + self.inner.network_params.additional_compound_transaction_mass(); let compute_fees = calc.calc_minimum_transaction_fee_from_mass(compute_mass); // TODO - consider removing this as calculated storage mass should produce `0` value diff --git a/wallet/core/src/tx/generator/pending.rs b/wallet/core/src/tx/generator/pending.rs index cd757e54b6..ad7e2e40f2 100644 --- a/wallet/core/src/tx/generator/pending.rs +++ b/wallet/core/src/tx/generator/pending.rs @@ -8,7 +8,8 @@ use crate::result::Result; use crate::rpc::DynRpcApi; use crate::tx::{DataKind, Generator}; use crate::utxo::{UtxoContext, UtxoEntryId, UtxoEntryReference}; -use kaspa_consensus_core::sign::sign_with_multiple_v2; +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_rpc_core::{RpcTransaction, RpcTransactionId}; @@ -223,9 +224,50 @@ impl PendingTransaction { Ok(()) } - pub fn try_sign_with_keys(&self, privkeys: &[[u8; 32]]) -> Result<()> { + pub fn create_input_signature(&self, input_index: usize, private_key: &[u8; 32], hash_type: SigHashType) -> Result> { let mutable_tx = self.inner.signable_tx.lock()?.clone(); - let signed_tx = sign_with_multiple_v2(mutable_tx, privkeys).fully_signed()?; + let verifiable_tx = mutable_tx.as_verifiable(); + + Ok(sign_input(&verifiable_tx, input_index, private_key, hash_type)) + } + + pub fn fill_input(&self, input_index: usize, signature_script: Vec) -> Result<()> { + let mut mutable_tx = self.inner.signable_tx.lock()?.clone(); + mutable_tx.tx.inputs[input_index].signature_script = signature_script; + *self.inner.signable_tx.lock().unwrap() = mutable_tx; + + Ok(()) + } + + pub fn sign_input(&self, input_index: usize, private_key: &[u8; 32], hash_type: SigHashType) -> Result<()> { + let mut mutable_tx = self.inner.signable_tx.lock()?.clone(); + + let signature_script = { + let verifiable_tx = &mutable_tx.as_verifiable(); + sign_input(verifiable_tx, input_index, private_key, hash_type) + }; + + mutable_tx.tx.inputs[input_index].signature_script = signature_script; + *self.inner.signable_tx.lock().unwrap() = mutable_tx; + + Ok(()) + } + + pub fn try_sign_with_keys(&self, privkeys: &[[u8; 32]], check_fully_signed: Option) -> Result<()> { + let mutable_tx = self.inner.signable_tx.lock()?.clone(); + let signed = sign_with_multiple_v2(mutable_tx, privkeys); + + let signed_tx = match signed { + Signed::Fully(tx) => tx, + Signed::Partially(_) => { + if check_fully_signed.unwrap_or(true) { + signed.fully_signed()? + } else { + signed.unwrap() + } + } + }; + *self.inner.signable_tx.lock().unwrap() = signed_tx; Ok(()) } diff --git a/wallet/core/src/tx/generator/settings.rs b/wallet/core/src/tx/generator/settings.rs index 0055d8fb4f..34fd1bb6ef 100644 --- a/wallet/core/src/tx/generator/settings.rs +++ b/wallet/core/src/tx/generator/settings.rs @@ -20,6 +20,8 @@ pub struct GeneratorSettings { pub utxo_iterator: Box + Send + Sync + 'static>, // Utxo Context pub source_utxo_context: Option, + // Priority utxo entries that are consumed before others + pub priority_utxo_entries: Option>, // typically a number of keys required to sign the transaction pub sig_op_count: u8, // number of minimum signatures required to sign the transaction @@ -77,6 +79,7 @@ impl GeneratorSettings { change_address, utxo_iterator: Box::new(utxo_iterator), source_utxo_context: Some(account.utxo_context().clone()), + priority_utxo_entries: None, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, @@ -89,6 +92,7 @@ impl GeneratorSettings { pub fn try_new_with_context( utxo_context: UtxoContext, + priority_utxo_entries: Option>, change_address: Address, sig_op_count: u8, minimum_signatures: u16, @@ -108,6 +112,7 @@ impl GeneratorSettings { change_address, utxo_iterator: Box::new(utxo_iterator), source_utxo_context: Some(utxo_context), + priority_utxo_entries, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, @@ -121,6 +126,7 @@ impl GeneratorSettings { pub fn try_new_with_iterator( network_id: NetworkId, utxo_iterator: Box + Send + Sync + 'static>, + priority_utxo_entries: Option>, change_address: Address, sig_op_count: u8, minimum_signatures: u16, @@ -137,6 +143,7 @@ impl GeneratorSettings { change_address, utxo_iterator: Box::new(utxo_iterator), source_utxo_context: None, + priority_utxo_entries, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index 1368c51f2b..045128a7b6 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -169,11 +169,11 @@ fn validate(pt: &PendingTransaction) { ); let calc = MassCalculator::new(&pt.network_type().into(), network_params); - let additional_mass = if pt.is_final() { 0 } else { network_params.additional_compound_transaction_mass }; - let compute_mass = calc.calc_mass_for_signed_transaction(&tx, 1); + let additional_mass = if pt.is_final() { 0 } else { network_params.additional_compound_transaction_mass() }; + let compute_mass = calc.calc_compute_mass_for_signed_transaction(&tx, 1); let utxo_entries = pt.utxo_entries().values().cloned().collect::>(); - let storage_mass = calc.calc_storage_mass_for_transaction(false, &utxo_entries, &tx.outputs).unwrap_or_default(); + let storage_mass = calc.calc_storage_mass_for_transaction_parts(&utxo_entries, &tx.outputs).unwrap_or_default(); let calculated_mass = calc.combine_mass(compute_mass, storage_mass) + additional_mass; @@ -199,12 +199,12 @@ where let pt_fees = pt.fees(); let calc = MassCalculator::new(&pt.network_type().into(), network_params); - let additional_mass = if pt.is_final() { 0 } else { network_params.additional_compound_transaction_mass }; + let additional_mass = if pt.is_final() { 0 } else { network_params.additional_compound_transaction_mass() }; - let compute_mass = calc.calc_mass_for_signed_transaction(&tx, 1); + let compute_mass = calc.calc_compute_mass_for_signed_transaction(&tx, 1); let utxo_entries = pt.utxo_entries().values().cloned().collect::>(); - let storage_mass = calc.calc_storage_mass_for_transaction(false, &utxo_entries, &tx.outputs).unwrap_or_default(); + let storage_mass = calc.calc_storage_mass_for_transaction_parts(&utxo_entries, &tx.outputs).unwrap_or_default(); if DISPLAY_LOGS && storage_mass != 0 { println!( "calculated storage mass: {} calculated_compute_mass: {} total: {}", @@ -392,6 +392,7 @@ where let sig_op_count = 1; let minimum_signatures = 1; let utxo_iterator: Box + Send + Sync + 'static> = Box::new(utxo_entries.into_iter()); + let priority_utxo_entries = None; let source_utxo_context = None; let destination_utxo_context = None; let final_priority_fee = fees; @@ -406,6 +407,7 @@ where change_address, utxo_iterator, source_utxo_context, + priority_utxo_entries, destination_utxo_context, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, diff --git a/wallet/core/src/tx/mass.rs b/wallet/core/src/tx/mass.rs index b5583ddf34..09545bf13d 100644 --- a/wallet/core/src/tx/mass.rs +++ b/wallet/core/src/tx/mass.rs @@ -2,20 +2,15 @@ //! Transaction mass calculator. //! +use crate::result::Result; use crate::utxo::NetworkParams; +use kaspa_consensus_client as kcc; use kaspa_consensus_client::UtxoEntryReference; +use kaspa_consensus_core::mass::Kip9Version; use kaspa_consensus_core::tx::{Transaction, TransactionInput, TransactionOutput, SCRIPT_VECTOR_SIZE}; use kaspa_consensus_core::{config::params::Params, constants::*, subnets::SUBNETWORK_ID_SIZE}; use kaspa_hashes::HASH_SIZE; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MassCombinationStrategy { - /// `MassCombinator::Add` adds the storage and compute mass. - Add, - /// `MassCombinator::Max` returns the maximum of the storage and compute mass. - Max, -} - // pub const ECDSA_SIGNATURE_SIZE: u64 = 64; // pub const SCHNORR_SIGNATURE_SIZE: u64 = 64; pub const SIGNATURE_SIZE: u64 = 1 + 64 + 1; //1 byte for OP_DATA_65 + 64 (length of signature) + 1 byte for sig hash type @@ -222,7 +217,7 @@ pub struct MassCalculator { mass_per_script_pub_key_byte: u64, mass_per_sig_op: u64, storage_mass_parameter: u64, - mass_combination_strategy: MassCombinationStrategy, + kip9_version: Kip9Version, } impl MassCalculator { @@ -232,7 +227,7 @@ impl MassCalculator { mass_per_script_pub_key_byte: consensus_params.mass_per_script_pub_key_byte, mass_per_sig_op: consensus_params.mass_per_sig_op, storage_mass_parameter: consensus_params.storage_mass_parameter, - mass_combination_strategy: network_params.mass_combination_strategy, + kip9_version: network_params.kip9_version(), } } @@ -243,44 +238,45 @@ impl MassCalculator { } } - pub fn calc_mass_for_transaction(&self, tx: &Transaction) -> u64 { - self.blank_transaction_mass() - + self.calc_mass_for_payload(tx.payload.len()) - + self.calc_mass_for_outputs(&tx.outputs) - + self.calc_mass_for_inputs(&tx.inputs) + pub fn calc_transaction_compute_mass(&self, tx: &Transaction) -> u64 { + let payload_len = tx.payload.len(); + self.blank_transaction_compute_mass() + + self.calc_compute_mass_for_payload(payload_len) + + self.calc_compute_mass_for_outputs(&tx.outputs) + + self.calc_compute_mass_for_inputs(&tx.inputs) } - pub fn blank_transaction_mass(&self) -> u64 { + pub(crate) fn blank_transaction_compute_mass(&self) -> u64 { blank_transaction_serialized_byte_size() * self.mass_per_tx_byte } - pub fn calc_mass_for_payload(&self, payload_byte_size: usize) -> u64 { + pub(crate) fn calc_compute_mass_for_payload(&self, payload_byte_size: usize) -> u64 { payload_byte_size as u64 * self.mass_per_tx_byte } - pub fn calc_mass_for_outputs(&self, outputs: &[TransactionOutput]) -> u64 { - outputs.iter().map(|output| self.calc_mass_for_output(output)).sum() + pub(crate) fn calc_compute_mass_for_outputs(&self, outputs: &[TransactionOutput]) -> u64 { + outputs.iter().map(|output| self.calc_compute_mass_for_output(output)).sum() } - pub fn calc_mass_for_inputs(&self, inputs: &[TransactionInput]) -> u64 { - inputs.iter().map(|input| self.calc_mass_for_input(input)).sum::() + pub(crate) fn calc_compute_mass_for_inputs(&self, inputs: &[TransactionInput]) -> u64 { + inputs.iter().map(|input| self.calc_compute_mass_for_input(input)).sum::() } - pub fn calc_mass_for_output(&self, output: &TransactionOutput) -> u64 { + pub(crate) fn calc_compute_mass_for_output(&self, output: &TransactionOutput) -> u64 { self.mass_per_script_pub_key_byte * (2 + output.script_public_key.script().len() as u64) + transaction_output_serialized_byte_size(output) * self.mass_per_tx_byte } - pub fn calc_mass_for_input(&self, input: &TransactionInput) -> u64 { + pub(crate) fn calc_compute_mass_for_input(&self, input: &TransactionInput) -> u64 { input.sig_op_count as u64 * self.mass_per_sig_op + transaction_input_serialized_byte_size(input) * self.mass_per_tx_byte } - pub fn calc_signature_mass(&self, minimum_signatures: u16) -> u64 { + pub(crate) fn calc_compute_mass_for_signature(&self, minimum_signatures: u16) -> u64 { let minimum_signatures = std::cmp::max(1, minimum_signatures); SIGNATURE_SIZE * self.mass_per_tx_byte * minimum_signatures as u64 } - pub fn calc_signature_mass_for_inputs(&self, number_of_inputs: usize, minimum_signatures: u16) -> u64 { + pub fn calc_signature_compute_mass_for_inputs(&self, number_of_inputs: usize, minimum_signatures: u16) -> u64 { let minimum_signatures = std::cmp::max(1, minimum_signatures); SIGNATURE_SIZE * self.mass_per_tx_byte * minimum_signatures as u64 * number_of_inputs as u64 } @@ -289,48 +285,66 @@ impl MassCalculator { calc_minimum_required_transaction_relay_fee(mass) } - pub fn calc_mass_for_signed_transaction(&self, tx: &Transaction, minimum_signatures: u16) -> u64 { - self.calc_mass_for_transaction(tx) + self.calc_signature_mass_for_inputs(tx.inputs.len(), minimum_signatures) + pub fn calc_compute_mass_for_signed_transaction(&self, tx: &Transaction, minimum_signatures: u16) -> u64 { + self.calc_transaction_compute_mass(tx) + self.calc_signature_compute_mass_for_inputs(tx.inputs.len(), minimum_signatures) } - pub fn calc_minium_transaction_relay_fee(&self, tx: &Transaction, minimum_signatures: u16) -> u64 { - let mass = self.calc_mass_for_transaction(tx) + self.calc_signature_mass_for_inputs(tx.inputs.len(), minimum_signatures); - calc_minimum_required_transaction_relay_fee(mass) + pub fn calc_transaction_storage_fee(&self, inputs: &[UtxoEntryReference], outputs: &[TransactionOutput]) -> u64 { + self.calc_fee_for_storage_mass(self.calc_storage_mass_for_transaction_parts(inputs, outputs).unwrap_or(u64::MAX)) } - pub fn calc_tx_storage_fee(&self, is_coinbase: bool, inputs: &[UtxoEntryReference], outputs: &[TransactionOutput]) -> u64 { - self.calc_fee_for_storage_mass(self.calc_storage_mass_for_transaction(is_coinbase, inputs, outputs).unwrap_or(u64::MAX)) + // provisional + pub fn calc_fee_for_storage_mass(&self, mass: u64) -> u64 { + mass } - pub fn calc_fee_for_storage_mass(&self, mass: u64) -> u64 { + // provisional + pub fn calc_fee_for_mass(&self, mass: u64) -> u64 { mass } pub fn combine_mass(&self, compute_mass: u64, storage_mass: u64) -> u64 { - match self.mass_combination_strategy { - MassCombinationStrategy::Add => compute_mass + storage_mass, - MassCombinationStrategy::Max => std::cmp::max(compute_mass, storage_mass), + match self.kip9_version { + Kip9Version::Alpha => compute_mass + storage_mass, + Kip9Version::Beta => std::cmp::max(compute_mass, storage_mass), } } - pub fn calc_storage_mass_for_transaction( + /// Calculates the overall mass of this transaction, combining both compute and storage masses. + pub fn calc_tx_overall_mass(&self, tx: &kcc::Transaction) -> Result> { + let cctx = Transaction::from(tx); + let mass = match self.kip9_version { + Kip9Version::Alpha => self + .calc_storage_mass_for_transaction(tx)? + .and_then(|mass| mass.checked_add(self.calc_transaction_compute_mass(&cctx))), + Kip9Version::Beta => { + self.calc_storage_mass_for_transaction(tx)?.map(|mass| mass.max(self.calc_transaction_compute_mass(&cctx))) + } + }; + + Ok(mass) + } + + pub fn calc_storage_mass_for_transaction(&self, tx: &kcc::Transaction) -> Result> { + let utxos = tx.utxo_entry_references()?; + let outputs = tx.outputs(); + Ok(self.calc_storage_mass_for_transaction_parts(&utxos, &outputs)) + } + + pub fn calc_storage_mass_for_transaction_parts( &self, - is_coinbase: bool, inputs: &[UtxoEntryReference], outputs: &[TransactionOutput], ) -> Option { - if is_coinbase { - return Some(0); - } /* The code below computes the following formula: - max( 0 , C·( |O|/H(O) - |I|/A(I) ) ) + max( 0 , C·( |O|/H(O) - |I|/A(I) ) ) where C is the mass storage parameter, O is the set of output values, I is the set of input values, H(S) := |S|/sum_{s in S} 1 / s is the harmonic mean over the set S and A(S) := sum_{s in S} / |S| is the arithmetic mean. - See the (to date unpublished) KIP-0009 for more details + See KIP-0009 for more details */ // Since we are doing integer division, we perform the multiplication with C over the inner @@ -338,15 +352,36 @@ impl MassCalculator { // // If sum of fractions overflowed (nearly impossible, requires 10^7 outputs for C = 10^12), // we return `None` indicating mass is incomputable + // + // Note: in theory this can be tighten by subtracting input mass in the process (possibly avoiding the overflow), + // however the overflow case is so unpractical with current mass limits so we avoid the hassle let harmonic_outs = outputs .iter() .map(|out| self.storage_mass_parameter / out.value) .try_fold(0u64, |total, current| total.checked_add(current))?; // C·|O|/H(O) + let outs_len = outputs.len() as u64; + let ins_len = inputs.len() as u64; + + /* + KIP-0009 relaxed formula for the cases |O| = 1 OR |O| <= |I| <= 2: + max( 0 , C·( |O|/H(O) - |I|/H(I) ) ) + + Note: in the case |I| = 1 both formulas are equal, yet the following code (harmonic_ins) is a bit more efficient. + Hence, we transform the condition to |O| = 1 OR |I| = 1 OR |O| = |I| = 2 which is equivalent (and faster). + */ + + if self.kip9_version == Kip9Version::Beta && (outs_len == 1 || ins_len == 1 || (outs_len == 2 && ins_len == 2)) { + let harmonic_ins = inputs + .iter() + .map(|entry| self.storage_mass_parameter / entry.amount()) + .fold(0u64, |total, current| total.saturating_add(current)); // C·|I|/H(I) + return Some(harmonic_outs.saturating_sub(harmonic_ins)); // max( 0 , C·( |O|/H(O) - |I|/H(I) ) ); + } + // Total supply is bounded, so a sum of existing UTXO entries cannot overflow (nor can it be zero) let sum_ins = inputs.iter().map(|entry| entry.amount()).sum::(); // |I|·A(I) - let ins_len = inputs.len() as u64; let mean_ins = sum_ins / ins_len; // Inner fraction must be with C and over the mean value, in order to maximize precision. diff --git a/wallet/core/src/tx/payment.rs b/wallet/core/src/tx/payment.rs index 0cce1f30f3..e28c75a22f 100644 --- a/wallet/core/src/tx/payment.rs +++ b/wallet/core/src/tx/payment.rs @@ -62,8 +62,11 @@ pub struct PaymentOutput { impl TryCastFromJs for PaymentOutput { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { if let Some(array) = value.as_ref().dyn_ref::() { let length = array.length(); if length != 2 { @@ -74,7 +77,7 @@ impl TryCastFromJs for PaymentOutput { Ok(Self { address, amount }) } } else if let Some(object) = Object::try_from(value.as_ref()) { - let address = object.get_cast::
("address")?.into_owned(); + let address = object.cast_into::
("address")?; let amount = object.get_u64("amount")?; Ok(Self { address, amount }) } else { @@ -145,8 +148,11 @@ impl PaymentOutputs { impl TryCastFromJs for PaymentOutputs { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { let outputs = if let Some(output_array) = value.as_ref().dyn_ref::() { let vec = output_array.to_vec(); vec.into_iter().map(PaymentOutput::try_owned_from).collect::, _>>()? diff --git a/wallet/core/src/utxo/balance.rs b/wallet/core/src/utxo/balance.rs index ce189e124e..f16ce94ff6 100644 --- a/wallet/core/src/utxo/balance.rs +++ b/wallet/core/src/utxo/balance.rs @@ -10,6 +10,7 @@ pub enum DeltaStyle { } #[derive(Default, Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[borsh(use_discriminant = true)] pub enum Delta { #[default] NoChange = 0, diff --git a/wallet/core/src/utxo/context.rs b/wallet/core/src/utxo/context.rs index 26e19bc556..e8d1ff39be 100644 --- a/wallet/core/src/utxo/context.rs +++ b/wallet/core/src/utxo/context.rs @@ -299,7 +299,7 @@ impl UtxoContext { context.mature.sorted_insert_binary_asc_by_key(utxo_entry.clone(), |entry| entry.amount_as_ref()); } else { let params = NetworkParams::from(self.processor().network_id()?); - match utxo_entry.maturity(¶ms, current_daa_score) { + match utxo_entry.maturity(params, current_daa_score) { Maturity::Stasis => { context.stasis.insert(utxo_entry.id().clone(), utxo_entry.clone()); self.processor() @@ -346,7 +346,7 @@ impl UtxoContext { } else { remove_mature_ids.push(id); } - } else { + } else if context.outgoing.get(&utxo.transaction_id()).is_none() { log_error!("Error: UTXO not found in UtxoContext map!"); } } @@ -428,7 +428,7 @@ impl UtxoContext { for utxo_entry in utxo_entries.into_iter() { if let std::collections::hash_map::Entry::Vacant(e) = context.map.entry(utxo_entry.id()) { e.insert(utxo_entry.clone()); - match utxo_entry.maturity(¶ms, current_daa_score) { + match utxo_entry.maturity(params, current_daa_score) { Maturity::Stasis => { context.stasis.insert(utxo_entry.id().clone(), utxo_entry.clone()); self.processor() @@ -482,19 +482,25 @@ impl UtxoContext { // the final payments (not compound transactions) // and outgoing transactions that have not yet // been accepted + let mut outgoing_without_batch_tx = 0; let mut outgoing: u64 = 0; let mut consumed: u64 = 0; - for tx in context.outgoing.values() { - if !tx.is_accepted() { - if let Some(payment_value) = tx.payment_value() { + + let transactions = context.outgoing.values().filter(|tx| !tx.is_accepted()); + for tx in transactions { + if let Some(payment_value) = tx.payment_value() { + consumed += tx.aggregate_input_value(); + if tx.is_batch() { + outgoing += tx.fees() + tx.aggregate_output_value(); + } else { // final tx outgoing += tx.fees() + payment_value; - consumed += tx.aggregate_input_value(); - } else { - // compound tx has no payment value - outgoing += tx.fees() + tx.aggregate_output_value(); - consumed += tx.aggregate_input_value() + outgoing_without_batch_tx += payment_value; } + } else { + // compound tx has no payment value + outgoing += tx.fees() + tx.aggregate_output_value(); + consumed += tx.aggregate_input_value(); } } @@ -502,13 +508,12 @@ impl UtxoContext { // this condition does not occur. This is a temporary // log for a fixed bug, but we want to keep the check // just in case. - if mature + consumed < outgoing { - log_error!("Error: outgoing transaction value exceeds available balance"); + if consumed < outgoing { + log_error!("Error: outgoing transaction value exceeds available balance, mature: {mature}, consumed: {consumed}, outgoing: {outgoing}"); } let mature = (mature + consumed).saturating_sub(outgoing); - - Balance::new(mature, pending, outgoing, context.mature.len(), context.pending.len(), context.stasis.len()) + Balance::new(mature, pending, outgoing_without_batch_tx, context.mature.len(), context.pending.len(), context.stasis.len()) } pub(crate) async fn handle_utxo_added(&self, utxos: Vec, current_daa_score: u64) -> Result<()> { @@ -526,13 +531,15 @@ impl UtxoContext { let force_maturity_if_outgoing = outgoing_transaction.is_some(); let is_coinbase_stasis = - utxos.first().map(|utxo| matches!(utxo.maturity(¶ms, current_daa_score), Maturity::Stasis)).unwrap_or_default(); - - for utxo in utxos.iter() { - if let Err(err) = self.insert(utxo.clone(), current_daa_score, force_maturity_if_outgoing).await { - // TODO - remove `Result<>` from insert at a later date once - // we are confident that the insert will never result in an error. - log_error!("{}", err); + utxos.first().map(|utxo| matches!(utxo.maturity(params, current_daa_score), Maturity::Stasis)).unwrap_or_default(); + let is_batch = outgoing_transaction.as_ref().map_or_else(|| false, |tx| tx.is_batch()); + if !is_batch { + for utxo in utxos.iter() { + if let Err(err) = self.insert(utxo.clone(), current_daa_score, force_maturity_if_outgoing).await { + // TODO - remove `Result<>` from insert at a later date once + // we are confident that the insert will never result in an error. + log_error!("{}", err); + } } } @@ -564,22 +571,20 @@ impl UtxoContext { Ok(()) } - pub(crate) async fn handle_utxo_removed(&self, mut utxos: Vec, current_daa_score: u64) -> Result<()> { + pub(crate) async fn handle_utxo_removed(&self, utxos: Vec, current_daa_score: u64) -> Result<()> { // remove UTXOs from account set let outgoing_transactions = self.processor().outgoing(); #[allow(clippy::mutable_key_type)] let mut accepted_outgoing_transactions = HashSet::::new(); - utxos.retain(|utxo| { + for utxo in &utxos { for outgoing_transaction in outgoing_transactions.iter() { if outgoing_transaction.utxo_entries().contains_key(&utxo.id()) { accepted_outgoing_transactions.insert((*outgoing_transaction).clone()); - return false; } } - true - }); + } for accepted_outgoing_transaction in accepted_outgoing_transactions.into_iter() { if accepted_outgoing_transaction.is_batch() { diff --git a/wallet/core/src/utxo/processor.rs b/wallet/core/src/utxo/processor.rs index 49bebbd7a4..4195a00b5f 100644 --- a/wallet/core/src/utxo/processor.rs +++ b/wallet/core/src/utxo/processor.rs @@ -14,13 +14,13 @@ use kaspa_notify::{ use kaspa_rpc_core::{ api::{ ctl::{RpcCtl, RpcState}, - ops::RPC_API_VERSION, + ops::{RPC_API_REVISION, RPC_API_VERSION}, }, message::UtxosChangedNotification, GetServerInfoResponse, }; use kaspa_wrpc_client::KaspaRpcClient; -use workflow_core::channel::{Channel, DuplexChannel}; +use workflow_core::channel::{Channel, DuplexChannel, Sender}; use workflow_core::task::spawn; use crate::events::Events; @@ -62,6 +62,7 @@ pub struct Inner { connect_disconnect_guard: AsyncMutex<()>, metrics: Arc, metrics_kinds: Mutex>, + connection_signaler: Mutex>>>, } impl Inner { @@ -91,6 +92,7 @@ impl Inner { connect_disconnect_guard: Default::default(), metrics: Arc::new(Metrics::default()), metrics_kinds: Mutex::new(vec![]), + connection_signaler: Mutex::new(None), } } } @@ -180,8 +182,10 @@ impl UtxoProcessor { } pub fn network_params(&self) -> Result<&'static NetworkParams> { + // pub fn network_params(&self) -> Result { let network_id = (*self.inner.network_id.lock().unwrap()).ok_or(Error::MissingNetworkId)?; - Ok(network_id.into()) + Ok(NetworkParams::from(network_id)) + // Ok(network_id.into()) } pub fn pending(&self) -> &DashMap { @@ -330,7 +334,7 @@ impl UtxoProcessor { } async fn handle_outgoing(&self, current_daa_score: u64) -> Result<()> { - let longevity = self.network_params()?.user_transaction_maturity_period_daa; + let longevity = self.network_params()?.user_transaction_maturity_period_daa(); self.inner.outgoing.retain(|_, outgoing| { if outgoing.acceptance_daa_score() != 0 && (outgoing.acceptance_daa_score() + longevity) < current_daa_score { @@ -439,14 +443,21 @@ impl UtxoProcessor { pub async fn init_state_from_server(&self) -> Result { let GetServerInfoResponse { + rpc_api_version, + rpc_api_revision, server_version, network_id: server_network_id, has_utxo_index, is_synced, virtual_daa_score, - rpc_api_version, } = self.rpc_api().get_server_info().await?; + if rpc_api_version > RPC_API_VERSION { + let current = format!("{RPC_API_VERSION}.{RPC_API_REVISION}"); + let connected = format!("{rpc_api_version}.{rpc_api_revision}"); + return Err(Error::RpcApiVersion(current, connected)); + } + if !has_utxo_index { self.notify(Events::UtxoIndexNotEnabled { url: self.rpc_url() }).await?; return Err(Error::MissingUtxoIndex); @@ -457,12 +468,6 @@ impl UtxoProcessor { return Err(Error::InvalidNetworkType(network_id.to_string(), server_network_id.to_string())); } - if rpc_api_version[0] > RPC_API_VERSION[0] || rpc_api_version[1] > RPC_API_VERSION[1] { - let current = RPC_API_VERSION.iter().map(|v| v.to_string()).collect::>().join("."); - let connected = rpc_api_version.iter().map(|v| v.to_string()).collect::>().join("."); - return Err(Error::RpcApiVersion(current, connected)); - } - self.inner.current_daa_score.store(virtual_daa_score, Ordering::SeqCst); log_trace!("Connected to kaspad: '{server_version}' on '{server_network_id}'; SYNC: {is_synced} DAA: {virtual_daa_score}"); @@ -489,12 +494,30 @@ impl UtxoProcessor { Ok(()) } + /// Allows use to supply a channel Sender that will + /// receive the result of the wRPC connection attempt. + pub fn set_connection_signaler(&self, signal: Sender>) { + *self.inner.connection_signaler.lock().unwrap() = Some(signal); + } + + fn signal_connection(&self, result: std::result::Result<(), String>) -> bool { + let signal = self.inner.connection_signaler.lock().unwrap().take(); + if let Some(signal) = signal.as_ref() { + let _ = signal.try_send(result); + true + } else { + false + } + } + pub async fn handle_connect(&self) -> Result<()> { let _ = self.inner.connect_disconnect_guard.lock().await; match self.handle_connect_impl().await { Err(err) => { - log_error!("UtxoProcessor: error while connecting to node: {err}"); + if !self.signal_connection(Err(err.to_string())) { + log_error!("UtxoProcessor: error while connecting to node: {err}"); + } self.notify(Events::UtxoProcError { message: err.to_string() }).await?; if let Some(client) = self.rpc_client() { // try force disconnect the client if we have failed @@ -503,7 +526,10 @@ impl UtxoProcessor { } Err(err) } - Ok(_) => Ok(()), + Ok(_) => { + self.signal_connection(Ok(())); + Ok(()) + } } } @@ -633,15 +659,11 @@ impl UtxoProcessor { // handle RPC channel connection and disconnection events match msg { RpcState::Connected => { - if !this.is_connected() { - if let Err(err) = this.handle_connect().await { - log_error!("UtxoProcessor error: {err}"); - } else { - this.inner.multiplexer.try_broadcast(Box::new(Events::Connect { - network_id : this.network_id().expect("network id expected during connection"), - url : this.rpc_url() - })).unwrap_or_else(|err| log_error!("{err}")); - } + if !this.is_connected() && this.handle_connect().await.is_ok() { + this.inner.multiplexer.try_broadcast(Box::new(Events::Connect { + network_id : this.network_id().expect("network id expected during connection"), + url : this.rpc_url() + })).unwrap_or_else(|err| log_error!("{err}")); } }, RpcState::Disconnected => { diff --git a/wallet/core/src/utxo/reference.rs b/wallet/core/src/utxo/reference.rs index 5e35f9e8b7..7bc0ec287c 100644 --- a/wallet/core/src/utxo/reference.rs +++ b/wallet/core/src/utxo/reference.rs @@ -34,14 +34,14 @@ pub trait UtxoEntryReferenceExtension { impl UtxoEntryReferenceExtension for UtxoEntryReference { fn maturity(&self, params: &NetworkParams, current_daa_score: u64) -> Maturity { if self.is_coinbase() { - if self.block_daa_score() + params.coinbase_transaction_stasis_period_daa > current_daa_score { + if self.block_daa_score() + params.coinbase_transaction_stasis_period_daa() > current_daa_score { Maturity::Stasis - } else if self.block_daa_score() + params.coinbase_transaction_maturity_period_daa > current_daa_score { + } else if self.block_daa_score() + params.coinbase_transaction_maturity_period_daa() > current_daa_score { Maturity::Pending } else { Maturity::Confirmed } - } else if self.block_daa_score() + params.user_transaction_maturity_period_daa > current_daa_score { + } else if self.block_daa_score() + params.user_transaction_maturity_period_daa() > current_daa_score { Maturity::Pending } else { Maturity::Confirmed diff --git a/wallet/core/src/utxo/settings.rs b/wallet/core/src/utxo/settings.rs index 3890263be0..6828d73cfe 100644 --- a/wallet/core/src/utxo/settings.rs +++ b/wallet/core/src/utxo/settings.rs @@ -4,58 +4,94 @@ //! use crate::imports::*; +use kaspa_consensus_core::mass::Kip9Version; #[derive(Debug)] pub struct NetworkParams { - pub coinbase_transaction_maturity_period_daa: u64, + pub coinbase_transaction_maturity_period_daa: AtomicU64, pub coinbase_transaction_stasis_period_daa: u64, - pub user_transaction_maturity_period_daa: u64, - pub mass_combination_strategy: MassCombinationStrategy, + pub user_transaction_maturity_period_daa: AtomicU64, + pub kip9_version: Kip9Version, pub additional_compound_transaction_mass: u64, } -pub const MAINNET_NETWORK_PARAMS: NetworkParams = NetworkParams { - coinbase_transaction_maturity_period_daa: 100, +impl NetworkParams { + #[inline] + pub fn coinbase_transaction_maturity_period_daa(&self) -> u64 { + self.coinbase_transaction_maturity_period_daa.load(Ordering::Relaxed) + } + + #[inline] + pub fn coinbase_transaction_stasis_period_daa(&self) -> u64 { + self.coinbase_transaction_stasis_period_daa + } + + #[inline] + pub fn user_transaction_maturity_period_daa(&self) -> u64 { + self.user_transaction_maturity_period_daa.load(Ordering::Relaxed) + } + + #[inline] + pub fn kip9_version(&self) -> Kip9Version { + self.kip9_version + } + + #[inline] + pub fn additional_compound_transaction_mass(&self) -> u64 { + self.additional_compound_transaction_mass + } + + pub fn set_coinbase_transaction_maturity_period_daa(&self, value: u64) { + self.coinbase_transaction_maturity_period_daa.store(value, Ordering::Relaxed); + } + + pub fn set_user_transaction_maturity_period_daa(&self, value: u64) { + self.user_transaction_maturity_period_daa.store(value, Ordering::Relaxed); + } +} + +static MAINNET_NETWORK_PARAMS: LazyLock = LazyLock::new(|| NetworkParams { + coinbase_transaction_maturity_period_daa: AtomicU64::new(100), coinbase_transaction_stasis_period_daa: 50, - user_transaction_maturity_period_daa: 10, - mass_combination_strategy: MassCombinationStrategy::Add, - additional_compound_transaction_mass: 0, -}; + user_transaction_maturity_period_daa: AtomicU64::new(10), + kip9_version: Kip9Version::Beta, + additional_compound_transaction_mass: 100, +}); -pub const TESTNET10_NETWORK_PARAMS: NetworkParams = NetworkParams { - coinbase_transaction_maturity_period_daa: 100, +static TESTNET10_NETWORK_PARAMS: LazyLock = LazyLock::new(|| NetworkParams { + coinbase_transaction_maturity_period_daa: AtomicU64::new(100), coinbase_transaction_stasis_period_daa: 50, - user_transaction_maturity_period_daa: 10, - mass_combination_strategy: MassCombinationStrategy::Add, + user_transaction_maturity_period_daa: AtomicU64::new(10), + kip9_version: Kip9Version::Beta, additional_compound_transaction_mass: 100, -}; +}); -pub const TESTNET11_NETWORK_PARAMS: NetworkParams = NetworkParams { - coinbase_transaction_maturity_period_daa: 1_000, +static TESTNET11_NETWORK_PARAMS: LazyLock = LazyLock::new(|| NetworkParams { + coinbase_transaction_maturity_period_daa: AtomicU64::new(1_000), coinbase_transaction_stasis_period_daa: 500, - user_transaction_maturity_period_daa: 100, - mass_combination_strategy: MassCombinationStrategy::Add, + user_transaction_maturity_period_daa: AtomicU64::new(100), + kip9_version: Kip9Version::Alpha, additional_compound_transaction_mass: 100, -}; +}); -pub const DEVNET_NETWORK_PARAMS: NetworkParams = NetworkParams { - coinbase_transaction_maturity_period_daa: 100, +static SIMNET_NETWORK_PARAMS: LazyLock = LazyLock::new(|| NetworkParams { + coinbase_transaction_maturity_period_daa: AtomicU64::new(100), coinbase_transaction_stasis_period_daa: 50, - user_transaction_maturity_period_daa: 10, - mass_combination_strategy: MassCombinationStrategy::Add, + user_transaction_maturity_period_daa: AtomicU64::new(10), + kip9_version: Kip9Version::Alpha, additional_compound_transaction_mass: 0, -}; +}); -pub const SIMNET_NETWORK_PARAMS: NetworkParams = NetworkParams { - coinbase_transaction_maturity_period_daa: 100, +static DEVNET_NETWORK_PARAMS: LazyLock = LazyLock::new(|| NetworkParams { + coinbase_transaction_maturity_period_daa: AtomicU64::new(100), coinbase_transaction_stasis_period_daa: 50, - user_transaction_maturity_period_daa: 10, - mass_combination_strategy: MassCombinationStrategy::Add, + user_transaction_maturity_period_daa: AtomicU64::new(10), + kip9_version: Kip9Version::Beta, additional_compound_transaction_mass: 0, -}; +}); -impl From for &'static NetworkParams { - fn from(value: NetworkId) -> Self { +impl NetworkParams { + pub fn from(value: NetworkId) -> &'static NetworkParams { match value.network_type { NetworkType::Mainnet => &MAINNET_NETWORK_PARAMS, NetworkType::Testnet => match value.suffix { @@ -70,18 +106,27 @@ impl From for &'static NetworkParams { } } -impl From for NetworkParams { - fn from(value: NetworkId) -> Self { - match value.network_type { - NetworkType::Mainnet => MAINNET_NETWORK_PARAMS, - NetworkType::Testnet => match value.suffix { - Some(10) => TESTNET10_NETWORK_PARAMS, - Some(11) => TESTNET11_NETWORK_PARAMS, - Some(x) => panic!("Testnet suffix {} is not supported", x), - None => panic!("Testnet suffix not provided"), - }, - NetworkType::Devnet => DEVNET_NETWORK_PARAMS, - NetworkType::Simnet => SIMNET_NETWORK_PARAMS, - } +/// Set the coinbase transaction maturity period DAA score for a given network. +/// This controls the DAA period after which the user transactions are considered mature +/// and the wallet subsystem emits the transaction maturity event. +pub fn set_coinbase_transaction_maturity_period_daa(network_id: &NetworkId, value: u64) { + let network_params = NetworkParams::from(*network_id); + if value <= network_params.coinbase_transaction_stasis_period_daa() { + panic!( + "Coinbase transaction maturity period must be greater than the stasis period of {} DAA", + network_params.coinbase_transaction_stasis_period_daa() + ); + } + network_params.set_coinbase_transaction_maturity_period_daa(value); +} + +/// Set the user transaction maturity period DAA score for a given network. +/// This controls the DAA period after which the user transactions are considered mature +/// and the wallet subsystem emits the transaction maturity event. +pub fn set_user_transaction_maturity_period_daa(network_id: &NetworkId, value: u64) { + let network_params = NetworkParams::from(*network_id); + if value == 0 { + panic!("User transaction maturity period must be greater than 0"); } + network_params.set_user_transaction_maturity_period_daa(value); } diff --git a/wallet/core/src/wallet/api.rs b/wallet/core/src/wallet/api.rs index 313759ba72..adeb000757 100644 --- a/wallet/core/src/wallet/api.rs +++ b/wallet/core/src/wallet/api.rs @@ -20,6 +20,9 @@ impl WalletApi for super::Wallet { } async fn get_status_call(self: Arc, request: GetStatusRequest) -> Result { + let guard = self.guard(); + let guard = guard.lock().await; + let GetStatusRequest { name } = request; let context = name.and_then(|name| self.inner.retained_contexts.lock().unwrap().get(&name).cloned()); @@ -34,7 +37,7 @@ impl WalletApi for super::Wallet { let (wallet_descriptor, account_descriptors) = if self.is_open() { let wallet_descriptor = self.descriptor(); - let account_descriptors = self.account_descriptors().await.ok(); + let account_descriptors = self.account_descriptors(&guard).await.ok(); (wallet_descriptor, account_descriptors) } else { (None, None) @@ -75,22 +78,37 @@ impl WalletApi for super::Wallet { async fn connect_call(self: Arc, request: ConnectRequest) -> Result { use workflow_rpc::client::{ConnectOptions, ConnectStrategy}; - let ConnectRequest { url, network_id } = request; + let ConnectRequest { url, network_id, retry_on_error, block_async_connect, require_sync } = request; if let Some(wrpc_client) = self.try_wrpc_client().as_ref() { - // self.set_network_id(network_id)?; + let strategy = if retry_on_error { ConnectStrategy::Retry } else { ConnectStrategy::Fallback }; - // let network_type = NetworkType::from(network_id); let url = url .map(|url| wrpc_client.parse_url_with_network_type(url, network_id.into()).map_err(|e| e.to_string())) .transpose()?; - let options = ConnectOptions { block_async_connect: false, strategy: ConnectStrategy::Retry, url, ..Default::default() }; + let options = ConnectOptions { block_async_connect, strategy, url, ..Default::default() }; wrpc_client.disconnect().await?; self.set_network_id(&network_id)?; + let processor = self.utxo_processor().clone(); + let (sender, receiver) = oneshot(); + + // set connection signaler that gets triggered + // by utxo processor when connection occurs + processor.set_connection_signaler(sender); + + // connect rpc wrpc_client.connect(Some(options)).await.map_err(|e| e.to_string())?; - Ok(ConnectResponse {}) + + // wait for connection signal, cascade if error + receiver.recv().await?.map_err(Error::custom)?; + + if require_sync && !self.is_synced() { + Err(Error::NotSynced) + } else { + Ok(ConnectResponse {}) + } } else { Err(Error::NotWrpcClient) } @@ -143,9 +161,12 @@ impl WalletApi for super::Wallet { } async fn wallet_open_call(self: Arc, request: WalletOpenRequest) -> Result { + let guard = self.guard(); + let guard = guard.lock().await; + let WalletOpenRequest { wallet_secret, filename, account_descriptors, legacy_accounts } = request; let args = WalletOpenArgs { account_descriptors, legacy_accounts: legacy_accounts.unwrap_or_default() }; - let account_descriptors = self.open(&wallet_secret, filename, args).await?; + let account_descriptors = self.open(&wallet_secret, filename, args, &guard).await?; Ok(WalletOpenResponse { account_descriptors }) } @@ -159,7 +180,11 @@ impl WalletApi for super::Wallet { if !self.is_open() { return Err(Error::WalletNotOpen); } - self.reload(reactivate).await?; + + let guard = self.guard(); + let guard = guard.lock().await; + + self.reload(reactivate, &guard).await?; Ok(WalletReloadResponse {}) } @@ -222,7 +247,10 @@ impl WalletApi for super::Wallet { async fn accounts_rename_call(self: Arc, request: AccountsRenameRequest) -> Result { let AccountsRenameRequest { account_id, name, wallet_secret } = request; - let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + 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))?; account.rename(&wallet_secret, name.as_deref()).await?; Ok(AccountsRenameResponse {}) @@ -231,8 +259,11 @@ impl WalletApi for super::Wallet { async fn accounts_select_call(self: Arc, request: AccountsSelectRequest) -> Result { let AccountsSelectRequest { account_id } = request; + let guard = self.guard(); + let guard = guard.lock().await; + if let Some(account_id) = account_id { - let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + let account = self.get_account_by_id(&account_id, &guard).await?.ok_or(Error::AccountNotFound(account_id))?; self.select(Some(&account)).await?; } else { self.select(None).await?; @@ -243,34 +274,20 @@ impl WalletApi for super::Wallet { } async fn accounts_enumerate_call(self: Arc, _request: AccountsEnumerateRequest) -> Result { - // let iter = self.inner.store.as_account_store().unwrap().iter(None).await.unwrap(); - // let wallet = self.clone(); - - // let stream = iter.then(move |stored| { - // let wallet = wallet.clone(); - - // async move { - // let (stored_account, stored_metadata) = stored.unwrap(); - // if let Some(account) = wallet.legacy_accounts().get(&stored_account.id) { - // account.descriptor() - // } else if let Some(account) = wallet.active_accounts().get(&stored_account.id) { - // account.descriptor() - // } else { - // try_load_account(&wallet, stored_account, stored_metadata).await?.descriptor() - // } - // } - // }); - - // let account_descriptors = stream.try_collect::>().await?; - - let account_descriptors = self.account_descriptors().await?; + let guard = self.guard(); + let guard = guard.lock().await; + + let account_descriptors = self.account_descriptors(&guard).await?; Ok(AccountsEnumerateResponse { account_descriptors }) } async fn accounts_activate_call(self: Arc, request: AccountsActivateRequest) -> Result { let AccountsActivateRequest { account_ids } = request; - self.activate_accounts(account_ids.as_deref()).await?; + let guard = self.guard(); + let guard = guard.lock().await; + + self.activate_accounts(account_ids.as_deref(), &guard).await?; Ok(AccountsActivateResponse {}) } @@ -278,7 +295,10 @@ impl WalletApi for super::Wallet { async fn accounts_deactivate_call(self: Arc, request: AccountsDeactivateRequest) -> Result { let AccountsDeactivateRequest { account_ids } = request; - self.deactivate_accounts(account_ids.as_deref()).await?; + let guard = self.guard(); + let guard = guard.lock().await; + + self.deactivate_accounts(account_ids.as_deref(), &guard).await?; Ok(AccountsDeactivateResponse {}) } @@ -296,7 +316,10 @@ impl WalletApi for super::Wallet { async fn accounts_create_call(self: Arc, request: AccountsCreateRequest) -> Result { let AccountsCreateRequest { wallet_secret, account_create_args } = request; - let account = self.create_account(&wallet_secret, account_create_args, true).await?; + let guard = self.guard(); + let guard = guard.lock().await; + + let account = self.create_account(&wallet_secret, account_create_args, true, &guard).await?; let account_descriptor = account.descriptor()?; Ok(AccountsCreateResponse { account_descriptor }) @@ -308,8 +331,12 @@ impl WalletApi for super::Wallet { ) -> Result { let AccountsEnsureDefaultRequest { wallet_secret, payment_secret, account_kind, mnemonic_phrase } = request; - let account_descriptor = - self.ensure_default_account_impl(&wallet_secret, payment_secret.as_ref(), account_kind, mnemonic_phrase.as_ref()).await?; + let guard = self.guard(); + let guard = guard.lock().await; + + let account_descriptor = self + .ensure_default_account_impl(&wallet_secret, payment_secret.as_ref(), account_kind, mnemonic_phrase.as_ref(), &guard) + .await?; Ok(AccountsEnsureDefaultResponse { account_descriptor }) } @@ -321,7 +348,11 @@ impl WalletApi for super::Wallet { async fn accounts_get_call(self: Arc, request: AccountsGetRequest) -> Result { let AccountsGetRequest { account_id } = request; - let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + + 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 account_descriptor = account.descriptor().unwrap(); Ok(AccountsGetResponse { account_descriptor }) } @@ -332,7 +363,10 @@ impl WalletApi for super::Wallet { ) -> Result { let AccountsCreateNewAddressRequest { account_id, kind } = request; - let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + 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 kind { NewAddressKind::Receive => account.as_derivation_capable()?.new_receive_address().await?, @@ -345,7 +379,9 @@ 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 account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + 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 (generator_summary, transaction_ids) = @@ -364,7 +400,11 @@ impl WalletApi for super::Wallet { transfer_amount_sompi, } = request; - let source_account = self.get_account_by_id(&source_account_id).await?.ok_or(Error::AccountNotFound(source_account_id))?; + let guard = self.guard(); + let guard = guard.lock().await; + + let source_account = + self.get_account_by_id(&source_account_id, &guard).await?.ok_or(Error::AccountNotFound(source_account_id))?; let abortable = Abortable::new(); let (generator_summary, transaction_ids) = source_account @@ -376,6 +416,7 @@ impl WalletApi for super::Wallet { payment_secret, &abortable, None, + &guard, ) .await?; @@ -385,7 +426,9 @@ impl WalletApi for super::Wallet { async fn accounts_estimate_call(self: Arc, request: AccountsEstimateRequest) -> Result { let AccountsEstimateRequest { account_id, destination, priority_fee_sompi, payload } = request; - let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + 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))?; // Abort currently running async estimate for the same account if present. The estimate // call can be invoked continuously by the client/UI. If the estimate call is diff --git a/wallet/core/src/wallet/args.rs b/wallet/core/src/wallet/args.rs index 05d57c445e..a5fa378bb5 100644 --- a/wallet/core/src/wallet/args.rs +++ b/wallet/core/src/wallet/args.rs @@ -113,6 +113,18 @@ impl AccountCreateArgsBip32 { } } +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct AccountCreateArgsBip32Watch { + pub account_name: Option, + pub xpub_keys: Vec, +} + +impl AccountCreateArgsBip32Watch { + pub fn new(account_name: Option, xpub_keys: Vec) -> Self { + Self { account_name, xpub_keys } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct PrvKeyDataArgs { pub prv_key_data_id: PrvKeyDataId, @@ -142,6 +154,9 @@ pub enum AccountCreateArgs { name: Option, minimum_signatures: u16, }, + Bip32Watch { + account_args: AccountCreateArgsBip32Watch, + }, } impl AccountCreateArgs { diff --git a/wallet/core/src/wallet/mod.rs b/wallet/core/src/wallet/mod.rs index d20f041adb..d5f4dfadd3 100644 --- a/wallet/core/src/wallet/mod.rs +++ b/wallet/core/src/wallet/mod.rs @@ -7,6 +7,7 @@ pub mod maps; pub use args::*; use crate::account::ScanNotifier; +use crate::api::traits::WalletApi; use crate::compat::gen1::decrypt_mnemonic; use crate::error::Error::Custom; use crate::factory::try_load_account; @@ -21,9 +22,12 @@ use kaspa_notify::{ listener::ListenerId, scope::{Scope, VirtualDaaScoreChangedScope}, }; +use kaspa_wallet_keys::xpub::NetworkTaggedXpub; use kaspa_wrpc_client::{KaspaRpcClient, Resolver, WrpcEncoding}; use workflow_core::task::spawn; +pub type WalletGuard<'l> = AsyncMutexGuard<'l, ()>; + #[derive(Debug)] pub struct EncryptedMnemonic> { pub cipher: T, // raw @@ -90,6 +94,9 @@ pub struct Inner { wallet_bus: Channel, estimation_abortables: Mutex>, retained_contexts: Mutex>>>, + // Mutex used to protect concurrent access to accounts at the wallet api level + guard: Arc>, + account_guard: Arc>, } /// @@ -104,6 +111,13 @@ pub struct Wallet { inner: Arc, } +impl Default for Wallet { + fn default() -> Self { + let storage = Wallet::local_store().expect("Unable to initialize local storage"); + Wallet::try_new(storage, None, None).unwrap() + } +} + impl Wallet { pub fn local_store() -> Result> { Ok(Arc::new(LocalStore::try_new(false)?)) @@ -121,14 +135,6 @@ impl Wallet { let rpc_client = Arc::new(KaspaRpcClient::new_with_args(WrpcEncoding::Borsh, Some("wrpc://127.0.0.1:17110"), resolver, network_id, None)?); - // pub fn try_with_wrpc(store: Arc, network_id: Option) -> Result { - // let rpc_client = Arc::new(KaspaRpcClient::new_with_args( - // WrpcEncoding::Borsh, - // NotificationMode::MultiListeners, - // "wrpc://127.0.0.1:17110", - // None, - // )?); - let rpc_ctl = rpc_client.ctl().clone(); let rpc_api: Arc = rpc_client; let rpc = Rpc::new(rpc_api, rpc_ctl); @@ -155,16 +161,52 @@ impl Wallet { wallet_bus, estimation_abortables: Mutex::new(HashMap::new()), retained_contexts: Mutex::new(HashMap::new()), + guard: Arc::new(AsyncMutex::new(())), + account_guard: Arc::new(AsyncMutex::new(())), }), }; Ok(wallet) } + pub fn to_arc(self) -> Arc { + Arc::new(self) + } + + /// Helper fn for creating the wallet using a builder pattern. + pub fn with_network_id(self, network_id: NetworkId) -> Self { + self.set_network_id(&network_id).expect("Unable to set network id"); + self + } + + pub fn with_resolver(self, resolver: Resolver) -> Self { + self.wrpc_client().set_resolver(resolver).expect("Unable to set resolver"); + self + } + + pub fn with_url(self, url: Option<&str>) -> Self { + self.wrpc_client().set_url(url).expect("Unable to set url"); + self + } + pub fn inner(&self) -> &Arc { &self.inner } + // + // Mutex used to protect concurrent access to accounts + // at the wallet api level. This is a global lock that + // is required by various wallet operations. + // + // Due to the fact that Rust Wallet API is async, it is + // possible for clients to concurrently execute API calls + // that can "trip over each-other", causing incorrect + // account states. + // + pub fn guard(&self) -> Arc> { + self.inner.guard.clone() + } + pub fn is_resident(&self) -> Result { Ok(self.store().location()? == StorageDescriptor::Resident) } @@ -204,10 +246,12 @@ impl Wallet { Ok(()) } - pub async fn reload(self: &Arc, reactivate: bool) -> Result<()> { + pub async fn reload(self: &Arc, reactivate: bool, _guard: &WalletGuard<'_>) -> Result<()> { if self.is_open() { // similar to reset(), but effectively reboots the wallet + // let _guard = self.inner.guard.lock().await; + let accounts = self.active_accounts().collect(); let account_descriptors = Some(accounts.iter().map(|account| account.descriptor()).collect::>>()?); let wallet_descriptor = self.store().descriptor(); @@ -293,6 +337,8 @@ impl Wallet { filename: Option, args: WalletOpenArgs, ) -> Result>> { + // let _guard = self.inner.guard.lock().await; + let filename = filename.or_else(|| self.settings().get(WalletSettings::Wallet)); // let name = Some(make_filename(&name, &None)); @@ -329,20 +375,21 @@ impl Wallet { None }; - let account_descriptors = accounts - .as_ref() - .map(|accounts| accounts.iter().map(|account| account.descriptor()).collect::>>()) - .transpose()?; - - if let Some(accounts) = accounts { - for account in accounts.into_iter() { + if let Some(accounts) = &accounts { + for account in accounts.iter() { if let Ok(legacy_account) = account.clone().as_legacy_account() { - self.legacy_accounts().insert(account); legacy_account.create_private_context(wallet_secret, None, None).await?; + log_info!("create_private_context, open_impl: receive_address: {:?}", account.receive_address()); + self.legacy_accounts().insert(account.clone()); } } } + let account_descriptors = accounts + .as_ref() + .map(|accounts| accounts.iter().map(|account| account.descriptor()).collect::>>()) + .transpose()?; + self.notify(Events::WalletOpen { wallet_descriptor: wallet_name, account_descriptors: account_descriptors.clone() }).await?; let hint = self.store().get_user_hint().await?; @@ -357,6 +404,7 @@ impl Wallet { wallet_secret: &Secret, filename: Option, args: WalletOpenArgs, + _guard: &WalletGuard<'_>, ) -> Result>> { // This is a wrapper of open_impl() that catches errors and notifies the UI match self.open_impl(wallet_secret, filename, args).await { @@ -369,6 +417,8 @@ impl Wallet { } async fn activate_accounts_impl(self: &Arc, account_ids: Option<&[AccountId]>) -> Result> { + // let _guard = self.inner.guard.lock().await; + let stored_accounts = if let Some(ids) = account_ids { self.inner.store.as_account_store().unwrap().load_multiple(ids).await? } else { @@ -399,7 +449,7 @@ impl Wallet { } /// Activates accounts (performs account address space counts, initializes balance tracking, etc.) - pub async fn activate_accounts(self: &Arc, account_ids: Option<&[AccountId]>) -> Result<()> { + pub async fn activate_accounts(self: &Arc, account_ids: Option<&[AccountId]>, _guard: &WalletGuard<'_>) -> Result<()> { // This is a wrapper of activate_accounts_impl() that catches errors and notifies the UI if let Err(err) = self.activate_accounts_impl(account_ids).await { self.notify(Events::WalletError { message: err.to_string() }).await?; @@ -409,7 +459,9 @@ impl Wallet { } } - pub async fn deactivate_accounts(self: &Arc, ids: Option<&[AccountId]>) -> Result<()> { + pub async fn deactivate_accounts(self: &Arc, ids: Option<&[AccountId]>, _guard: &WalletGuard<'_>) -> Result<()> { + let _guard = self.inner.guard.lock().await; + let (ids, futures) = if let Some(ids) = ids { let accounts = ids.iter().map(|id| self.active_accounts().get(id).ok_or(Error::AccountNotFound(*id))).collect::>>()?; @@ -424,7 +476,9 @@ impl Wallet { Ok(()) } - pub async fn account_descriptors(self: Arc) -> Result> { + pub async fn account_descriptors(self: Arc, _guard: &WalletGuard<'_>) -> Result> { + // let _guard = self.inner.guard.lock().await; + let iter = self.inner.store.as_account_store().unwrap().iter(None).await.unwrap(); let wallet = self.clone(); @@ -462,6 +516,10 @@ impl Wallet { self.try_rpc_api().and_then(|api| api.clone().downcast_arc::().ok()) } + pub fn wrpc_client(&self) -> Arc { + self.try_rpc_api().and_then(|api| api.clone().downcast_arc::().ok()).unwrap() + } + pub fn rpc_api(&self) -> Arc { self.utxo_processor().rpc_api() } @@ -487,6 +545,14 @@ impl Wallet { Ok(()) } + pub fn as_api(self: &Arc) -> Arc { + self.clone() + } + + pub fn to_api(self) -> Arc { + Arc::new(self) + } + pub fn multiplexer(&self) -> &Multiplexer> { &self.inner.multiplexer } @@ -604,6 +670,7 @@ impl Wallet { wallet_secret: &Secret, account_create_args: AccountCreateArgs, notify: bool, + _guard: &WalletGuard<'_>, ) -> Result> { let account = match account_create_args { AccountCreateArgs::Bip32 { prv_key_data_args, account_args } => { @@ -616,6 +683,7 @@ impl Wallet { AccountCreateArgs::Multisig { prv_key_data_args, additional_xpub_keys, name, minimum_signatures } => { 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?, }; if notify { @@ -724,7 +792,13 @@ impl Wallet { let account_index = if let Some(account_index) = account_index { account_index } else { - account_store.clone().len(Some(prv_key_data_id)).await? as u64 + let accounts = account_store.clone().iter(Some(prv_key_data_id)).await?.collect::>().await; + + accounts + .into_iter() + .filter(|a| a.as_ref().ok().and_then(|(a, _)| (a.kind == BIP32_ACCOUNT_KIND).then_some(true)).unwrap_or(false)) + .collect::>() + .len() as u64 }; let xpub_key = prv_key_data.create_xpub(payment_secret, BIP32_ACCOUNT_KIND.into(), account_index).await?; @@ -743,6 +817,36 @@ impl Wallet { Ok(account) } + pub async fn create_account_bip32_watch( + self: &Arc, + wallet_secret: &Secret, + account_args: AccountCreateArgsBip32Watch, + ) -> Result> { + let account_store = self.inner.store.clone().as_account_store()?; + + let AccountCreateArgsBip32Watch { account_name, xpub_keys } = account_args; + + let xpub_keys = Arc::new( + xpub_keys + .into_iter() + .map(|xpub_key| { + ExtendedPublicKeySecp256k1::from_str(&xpub_key).map_err(|err| Error::InvalidExtendedPublicKey(xpub_key, err)) + }) + .collect::>>()?, + ); + + let account: Arc = Arc::new(bip32watch::Bip32Watch::try_new(self, account_name, xpub_keys, false).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) + } + async fn create_account_legacy( self: &Arc, wallet_secret: &Secret, @@ -760,6 +864,12 @@ impl Wallet { .ok_or_else(|| Error::PrivateKeyNotFound(prv_key_data_id))?; let account: Arc = Arc::new(legacy::Legacy::try_new(self, account_name, prv_key_data.id).await?); + if let Ok(legacy_account) = account.clone().as_legacy_account() { + legacy_account.create_private_context(wallet_secret, None, None).await?; + log_info!("create_private_context: create_account_legacy, receive_address: {:?}", account.receive_address()); + self.legacy_accounts().insert(account.clone()); + //legacy_account.clear_private_context().await?; + } if account_store.load_single(account.id()).await?.is_some() { return Err(Error::AccountAlreadyExists(*account.id())); @@ -847,7 +957,13 @@ impl Wallet { Ok((wallet_descriptor, storage_descriptor, mnemonic, account)) } - pub async fn get_account_by_id(self: &Arc, account_id: &AccountId) -> Result>> { + pub async fn get_account_by_id( + self: &Arc, + account_id: &AccountId, + _guard: &WalletGuard<'_>, + ) -> Result>> { + let _guard = self.inner.account_guard.lock().await; + if let Some(account) = self.active_accounts().get(account_id) { Ok(Some(account.clone())) } else { @@ -1036,7 +1152,11 @@ impl Wallet { Ok(matches) } - pub async fn accounts(self: &Arc, filter: Option) -> Result>>> { + pub async fn accounts( + self: &Arc, + filter: Option, + _guard: &WalletGuard<'_>, + ) -> Result>>> { let iter = self.inner.store.as_account_store().unwrap().iter(filter).await.unwrap(); let wallet = self.clone(); @@ -1557,6 +1677,7 @@ impl Wallet { payment_secret: Option<&Secret>, kind: AccountKind, mnemonic_phrase: Option<&Secret>, + guard: &WalletGuard<'_>, ) -> Result { if kind != BIP32_ACCOUNT_KIND { return Err(Error::custom("Account kind is not supported")); @@ -1582,13 +1703,17 @@ impl Wallet { let account_create_args = AccountCreateArgs::new_bip32(prv_key_data_id, payment_secret.cloned(), None, None); - let account = self.clone().create_account(wallet_secret, account_create_args, false).await?; + let account = self.clone().create_account(wallet_secret, account_create_args, false, guard).await?; self.store().flush(wallet_secret).await?; Ok(account.descriptor()?) } } + + pub fn network_format_xpub(&self, xpub_key: &ExtendedPublicKeySecp256k1) -> String { + NetworkTaggedXpub::from((xpub_key.clone(), self.network_id().unwrap())).to_string() + } } // fn decrypt_mnemonic>( diff --git a/wallet/core/src/wasm/api/message.rs b/wallet/core/src/wasm/api/message.rs index 33bf5e00dd..8a023267b8 100644 --- a/wallet/core/src/wasm/api/message.rs +++ b/wallet/core/src/wasm/api/message.rs @@ -153,8 +153,16 @@ declare! { * @category Wallet API */ export interface IConnectRequest { - url : string; + // destination wRPC node URL (if omitted, the resolver is used) + url? : string; + // network identifier networkId : NetworkId | string; + // retry on error + retryOnError? : boolean; + // block async connect (method will not return until the connection is established) + block? : boolean; + // require node to be synced (fail otherwise) + requireSync? : boolean; } "#, } @@ -162,7 +170,10 @@ declare! { try_from! ( args: IConnectRequest, ConnectRequest, { let url = args.try_get_string("url")?; let network_id = args.get_network_id("networkId")?; - Ok(ConnectRequest { url, network_id }) + let retry_on_error = args.try_get_bool("retryOnError")?.unwrap_or(true); + let block_async_connect = args.try_get_bool("block")?.unwrap_or(false); + let require_sync = args.try_get_bool("requireSync")?.unwrap_or(true); + Ok(ConnectRequest { url, network_id, retry_on_error, block_async_connect, require_sync }) }); declare! { @@ -971,7 +982,7 @@ try_from! (args: IAccountsDiscoveryRequest, AccountsDiscoveryRequest, { let discovery_kind = if let Some(discovery_kind) = discovery_kind.as_string() { discovery_kind.parse()? } else { - AccountsDiscoveryKind::try_cast_from(&discovery_kind)? + AccountsDiscoveryKind::try_enum_from(&discovery_kind)? }; let account_scan_extent = args.get_u32("accountScanExtent")?; let address_scan_extent = args.get_u32("addressScanExtent")?; @@ -1312,7 +1323,7 @@ try_from!(args: IAccountsCreateNewAddressRequest, AccountsCreateNewAddressReques let value = args.get_value("addressKind")?; let kind: NewAddressKind = if let Some(string) = value.as_string() { string.parse()? - } else if let Ok(kind) = NewAddressKind::try_cast_from(&value) { + } else if let Ok(kind) = NewAddressKind::try_enum_from(&value) { kind } else { NewAddressKind::Receive diff --git a/wallet/core/src/wasm/cryptobox.rs b/wallet/core/src/wasm/cryptobox.rs index 1892b08d22..118020fe4d 100644 --- a/wallet/core/src/wasm/cryptobox.rs +++ b/wallet/core/src/wasm/cryptobox.rs @@ -35,8 +35,11 @@ impl CryptoBoxPrivateKey { impl TryCastFromJs for CryptoBoxPrivateKey { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result> + where + R: AsRef + 'a, + { + Self::resolve(value, || { let secret_key = value.as_ref().try_as_vec_u8()?; if secret_key.len() != KEY_SIZE { return Err(Error::InvalidPrivateKeyLength); @@ -63,8 +66,11 @@ pub struct CryptoBoxPublicKey { impl TryCastFromJs for CryptoBoxPublicKey { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result> + where + R: AsRef + 'a, + { + Self::resolve(value, || { let public_key = value.as_ref().try_as_vec_u8()?; if public_key.len() != KEY_SIZE { Err(Error::InvalidPublicKeyLength) @@ -114,7 +120,7 @@ pub struct CryptoBox { impl CryptoBox { #[wasm_bindgen(constructor)] #[allow(non_snake_case)] - pub fn ctor(secretKey: CryptoBoxPrivateKeyT, peerPublicKey: CryptoBoxPublicKeyT) -> Result { + pub fn ctor(secretKey: &CryptoBoxPrivateKeyT, peerPublicKey: &CryptoBoxPublicKeyT) -> Result { let secret_key = CryptoBoxPrivateKey::try_cast_from(secretKey)?; let peer_public_key = CryptoBoxPublicKey::try_cast_from(peerPublicKey)?; Ok(Self { inner: Arc::new(NativeCryptoBox::new(&secret_key, &peer_public_key)) }) diff --git a/wallet/core/src/wasm/message.rs b/wallet/core/src/wasm/message.rs index 7df6f16720..25c7f399ad 100644 --- a/wallet/core/src/wasm/message.rs +++ b/wallet/core/src/wasm/message.rs @@ -28,10 +28,10 @@ extern "C" { #[wasm_bindgen(js_name = signMessage)] pub fn js_sign_message(value: ISignMessage) -> Result { if let Some(object) = Object::try_from(&value) { - let private_key = object.get_cast::("privateKey")?; + let private_key = object.cast_into::("privateKey")?; let raw_msg = object.get_string("message")?; let mut privkey_bytes = [0u8; 32]; - privkey_bytes.copy_from_slice(&private_key.as_ref().secret_bytes()); + privkey_bytes.copy_from_slice(&private_key.secret_bytes()); let pm = PersonalMessage(&raw_msg); let sig_vec = sign_message(&pm, &privkey_bytes)?; privkey_bytes.zeroize(); @@ -66,7 +66,7 @@ extern "C" { #[wasm_bindgen(js_name = verifyMessage, skip_jsdoc)] pub fn js_verify_message(value: IVerifyMessage) -> Result { if let Some(object) = Object::try_from(&value) { - let public_key = object.get_cast::("publicKey")?; + let public_key = object.cast_into::("publicKey")?; let raw_msg = object.get_string("message")?; let signature = object.get_string("signature")?; @@ -74,7 +74,7 @@ pub fn js_verify_message(value: IVerifyMessage) -> Result { let mut signature_bytes = [0u8; 64]; faster_hex::hex_decode(signature.as_bytes(), &mut signature_bytes)?; - Ok(verify_message(&pm, &signature_bytes.to_vec(), &public_key.as_ref().xonly_public_key).is_ok()) + Ok(verify_message(&pm, &signature_bytes.to_vec(), &public_key.xonly_public_key).is_ok()) } else { Err(Error::custom("Failed to parse input")) } diff --git a/wallet/core/src/wasm/notify.rs b/wallet/core/src/wasm/notify.rs index 61d1d16876..3383e1353e 100644 --- a/wallet/core/src/wasm/notify.rs +++ b/wallet/core/src/wasm/notify.rs @@ -32,33 +32,12 @@ cfg_if! { } /** - * {@link UtxoProcessor} notification event data. - * @category Wallet SDK - */ - export type UtxoProcessorEventData = IConnectEvent - | IDisconnectEvent - | IUtxoIndexNotEnabledEvent - | ISyncStateEvent - | IServerStatusEvent - | IUtxoProcErrorEvent - | IDaaScoreChangeEvent - | IPendingEvent - | IReorgEvent - | IStasisEvent - | IMaturityEvent - | IDiscoveryEvent - | IBalanceEvent - | IErrorEvent - | undefined - ; - - /** - * UtxoProcessor notification event data map. + * {@link UtxoProcessor} notification event data map. * * @category Wallet API */ export type UtxoProcessorEventMap = { - "connect":IConnectEvent, + "connect": IConnectEvent, "disconnect": IDisconnectEvent, "utxo-index-not-enabled": IUtxoIndexNotEnabledEvent, "sync-state": ISyncStateEvent, @@ -80,10 +59,13 @@ cfg_if! { * * @category Wallet API */ - export type IUtxoProcessorEvent = { - [K in keyof UtxoProcessorEventMap]: { event: K, data: UtxoProcessorEventMap[K] } - }[keyof UtxoProcessorEventMap]; + export type UtxoProcessorEvent = { + [K in T]: { + type: K, + data: UtxoProcessorEventMap[K] + } + }[T]; /** * {@link UtxoProcessor} notification callback type. @@ -95,7 +77,8 @@ cfg_if! { * * @category Wallet SDK */ - export type UtxoProcessorNotificationCallback = (event: IUtxoProcessorEvent) => void; + + export type UtxoProcessorNotificationCallback = (event: UtxoProcessorEvent) => void; "#; #[wasm_bindgen] @@ -150,85 +133,53 @@ cfg_if! { Error = "error", } - - /** - * {@link Wallet} notification event data payload. - * @category Wallet API - */ - export type WalletEventData = IConnectEvent - | IDisconnectEvent - | IUtxoIndexNotEnabledEvent - | ISyncStateEvent - | IWalletHintEvent - | IWalletOpenEvent - | IWalletCreateEvent - | IWalletReloadEvent - | IWalletErrorEvent - // | IWalletCloseEvent - | IPrvKeyDataCreateEvent - | IAccountActivationEvent - | IAccountDeactivationEvent - | IAccountSelectionEvent - | IAccountCreateEvent - | IAccountUpdateEvent - | IServerStatusEvent - // | IUtxoProcStartEvent - // | IUtxoProcStopEvent - | IUtxoProcErrorEvent - | IDaaScoreChangeEvent - | IPendingEvent - | IReorgEvent - | IStasisEvent - | IMaturityEvent - | IDiscoveryEvent - | IBalanceEvent - | IErrorEvent - | undefined - ; - /** * Wallet notification event data map. * @see {@link Wallet.addEventListener} * @category Wallet API */ export type WalletEventMap = { - "connect": IConnectEvent, - "disconnect": IDisconnectEvent, - "utxo-index-not-enabled": IUtxoIndexNotEnabledEvent, - "sync-state": ISyncStateEvent, - "wallet-hint": IWalletHintEvent, - "wallet-open": IWalletOpenEvent, - "wallet-create": IWalletCreateEvent, - "wallet-reload": IWalletReloadEvent, - "wallet-error": IWalletErrorEvent, - "wallet-close": undefined, - "prv-key-data-create": IPrvKeyDataCreateEvent, - "account-activation": IAccountActivationEvent, - "account-deactivation": IAccountDeactivationEvent, - "account-selection": IAccountSelectionEvent, - "account-create": IAccountCreateEvent, - "account-update": IAccountUpdateEvent, - "server-status": IServerStatusEvent, - "utxo-proc-start": undefined, - "utxo-proc-stop": undefined, - "utxo-proc-error": IUtxoProcErrorEvent, - "daa-score-change": IDaaScoreChangeEvent, - "pending": IPendingEvent, - "reorg": IReorgEvent, - "stasis": IStasisEvent, - "maturity": IMaturityEvent, - "discovery": IDiscoveryEvent, - "balance": IBalanceEvent, - "error": IErrorEvent, + "connect": IConnectEvent, + "disconnect": IDisconnectEvent, + "utxo-index-not-enabled": IUtxoIndexNotEnabledEvent, + "sync-state": ISyncStateEvent, + "wallet-hint": IWalletHintEvent, + "wallet-open": IWalletOpenEvent, + "wallet-create": IWalletCreateEvent, + "wallet-reload": IWalletReloadEvent, + "wallet-error": IWalletErrorEvent, + "wallet-close": undefined, + "prv-key-data-create": IPrvKeyDataCreateEvent, + "account-activation": IAccountActivationEvent, + "account-deactivation": IAccountDeactivationEvent, + "account-selection": IAccountSelectionEvent, + "account-create": IAccountCreateEvent, + "account-update": IAccountUpdateEvent, + "server-status": IServerStatusEvent, + "utxo-proc-start": undefined, + "utxo-proc-stop": undefined, + "utxo-proc-error": IUtxoProcErrorEvent, + "daa-score-change": IDaaScoreChangeEvent, + "pending": IPendingEvent, + "reorg": IReorgEvent, + "stasis": IStasisEvent, + "maturity": IMaturityEvent, + "discovery": IDiscoveryEvent, + "balance": IBalanceEvent, + "error": IErrorEvent, } /** * {@link Wallet} notification event interface. * @category Wallet API */ - export type IWalletEvent = { - [K in keyof WalletEventMap]: { type: K, data: WalletEventMap[K] } - }[keyof WalletEventMap]; + export type IWalletEvent = { + [K in T]: { + type: K, + data: WalletEventMap[K] + } + }[T]; + /** * Wallet notification callback type. @@ -240,7 +191,7 @@ cfg_if! { * * @category Wallet API */ - export type WalletNotificationCallback = (event: IWalletEvent) => void; + export type WalletNotificationCallback = (event: IWalletEvent) => void; "#; #[wasm_bindgen] diff --git a/wallet/core/src/wasm/signer.rs b/wallet/core/src/wasm/signer.rs index e2ff8e6fb2..e00729ef50 100644 --- a/wallet/core/src/wasm/signer.rs +++ b/wallet/core/src/wasm/signer.rs @@ -2,10 +2,13 @@ use crate::imports::*; use crate::result::Result; use js_sys::Array; use kaspa_consensus_client::{sign_with_multiple_v3, Transaction}; +use kaspa_consensus_core::hashing::wasm::SighashType; +use kaspa_consensus_core::sign::sign_input; use kaspa_consensus_core::tx::PopulatedTransaction; use kaspa_consensus_core::{hashing::sighash_type::SIG_HASH_ALL, sign::verify}; use kaspa_hashes::Hash; use kaspa_wallet_keys::privatekey::PrivateKey; +use kaspa_wasm_core::types::HexString; use serde_wasm_bindgen::from_value; #[wasm_bindgen] @@ -31,26 +34,26 @@ impl TryFrom for Vec { /// `signTransaction()` is a helper function to sign a transaction using a private key array or a signer array. /// @category Wallet SDK #[wasm_bindgen(js_name = "signTransaction")] -pub fn js_sign_transaction(tx: Transaction, signer: PrivateKeyArrayT, verify_sig: bool) -> Result { +pub fn js_sign_transaction(tx: &Transaction, signer: &PrivateKeyArrayT, verify_sig: bool) -> Result { if signer.is_array() { let mut private_keys: Vec<[u8; 32]> = vec![]; - for key in Array::from(&signer).iter() { - let key = PrivateKey::try_cast_from(key).map_err(|_| Error::Custom("Unable to cast PrivateKey".to_string()))?; + for key in Array::from(signer).iter() { + let key = PrivateKey::try_cast_from(&key).map_err(|_| Error::Custom("Unable to cast PrivateKey".to_string()))?; private_keys.push(key.as_ref().secret_bytes()); } let tx = sign_transaction(tx, &private_keys, verify_sig).map_err(|err| Error::Custom(format!("Unable to sign: {err:?}")))?; private_keys.zeroize(); - Ok(tx) + Ok(tx.clone()) } else { Err(Error::custom("signTransaction() requires an array of signatures")) } } -pub fn sign_transaction(tx: Transaction, private_keys: &[[u8; 32]], verify_sig: bool) -> Result { +pub fn sign_transaction<'a>(tx: &'a Transaction, private_keys: &[[u8; 32]], verify_sig: bool) -> Result<&'a Transaction> { let tx = sign(tx, private_keys)?; if verify_sig { - let (cctx, utxos) = tx.tx_and_utxos(); + let (cctx, utxos) = tx.tx_and_utxos()?; let populated_transaction = PopulatedTransaction::new(&cctx, utxos); verify(&populated_transaction)?; } @@ -60,10 +63,32 @@ pub fn sign_transaction(tx: Transaction, private_keys: &[[u8; 32]], verify_sig: /// Sign a transaction using schnorr, returns a new transaction with the signatures added. /// The resulting transaction may be partially signed if the supplied keys are not sufficient /// to sign all of its inputs. -pub fn sign(tx: Transaction, privkeys: &[[u8; 32]]) -> Result { +pub fn sign<'a>(tx: &'a Transaction, privkeys: &[[u8; 32]]) -> Result<&'a Transaction> { Ok(sign_with_multiple_v3(tx, privkeys)?.unwrap()) } +/// `createInputSignature()` is a helper function to sign a transaction input with a specific SigHash type using a private key. +/// @category Wallet SDK +#[wasm_bindgen(js_name = "createInputSignature")] +pub fn create_input_signature( + tx: &Transaction, + input_index: u8, + private_key: &PrivateKey, + sighash_type: Option, +) -> Result { + let (cctx, utxos) = tx.tx_and_utxos()?; + let populated_transaction = PopulatedTransaction::new(&cctx, utxos); + + let signature = sign_input( + &populated_transaction, + input_index.into(), + &private_key.secret_bytes(), + sighash_type.unwrap_or(SighashType::All).into(), + ); + + Ok(signature.to_hex().into()) +} + /// @category Wallet SDK #[wasm_bindgen(js_name=signScriptHash)] pub fn sign_script_hash(script_hash: JsValue, privkey: &PrivateKey) -> Result { diff --git a/wallet/core/src/wasm/tx/consensus.rs b/wallet/core/src/wasm/tx/consensus.rs deleted file mode 100644 index f1400d0915..0000000000 --- a/wallet/core/src/wasm/tx/consensus.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::tx::consensus as core; -use kaspa_addresses::Address; -use kaspa_consensus_core::{config::params::Params, network::NetworkType}; -use wasm_bindgen::prelude::*; - -/// @category Wallet SDK -#[wasm_bindgen] -pub struct ConsensusParams { - params: Params, -} - -impl From for ConsensusParams { - fn from(params: Params) -> Self { - Self { params } - } -} - -impl From for Params { - fn from(cp: ConsensusParams) -> Self { - cp.params - } -} - -/// find Consensus parameters for given Address -/// @category Wallet SDK -#[wasm_bindgen(js_name = getConsensusParametersByAddress)] -pub fn get_consensus_params_by_address(address: &Address) -> ConsensusParams { - core::get_consensus_params_by_address(address).into() -} - -/// find Consensus parameters for given NetworkType -/// @category Wallet SDK -#[wasm_bindgen(js_name = getConsensusParametersByNetwork)] -pub fn get_consensus_params_by_network(network: NetworkType) -> ConsensusParams { - core::get_consensus_params_by_network(&network).into() -} diff --git a/wallet/core/src/wasm/tx/fees.rs b/wallet/core/src/wasm/tx/fees.rs index ea38c6ebf6..200634cd56 100644 --- a/wallet/core/src/wasm/tx/fees.rs +++ b/wallet/core/src/wasm/tx/fees.rs @@ -39,7 +39,7 @@ impl TryFrom for Fees { } else if let Ok(object) = args.dyn_into::() { let amount = object.get_u64("amount")?; if let Some(source) = object.try_get_value("source")? { - let source = FeeSource::try_cast_from(&source)?; + let source = FeeSource::try_enum_from(&source)?; match source { FeeSource::SenderPays => Ok(Fees::SenderPays(amount)), FeeSource::ReceiverPays => Ok(Fees::ReceiverPays(amount)), diff --git a/wallet/core/src/wasm/tx/generator/generator.rs b/wallet/core/src/wasm/tx/generator/generator.rs index 5303b4d3e6..5724b84811 100644 --- a/wallet/core/src/wasm/tx/generator/generator.rs +++ b/wallet/core/src/wasm/tx/generator/generator.rs @@ -64,6 +64,15 @@ interface IGeneratorSettingsObject { * interface, or a {@link UtxoContext} instance. */ entries: IUtxoEntry[] | UtxoEntryReference[] | UtxoContext; + /** + * Optional UTXO entries that will be consumed before those available in `entries`. + * You can use this property to apply custom input selection logic. + * Please note that these inputs are consumed first, then `entries` are consumed + * to generate a desirable transaction output amount. If transaction mass + * overflows, these inputs will be consumed into a batch/sweep transaction + * where the destination if the `changeAddress`. + */ + priorityEntries?: IUtxoEntry[] | UtxoEntryReference[], /** * Optional number of signature operations in the transaction. */ @@ -147,6 +156,7 @@ impl Generator { let GeneratorSettings { network_id, source, + priority_utxo_entries, multiplexer, final_transaction_destination, change_address, @@ -167,6 +177,7 @@ impl Generator { native::GeneratorSettings::try_new_with_iterator( network_id, Box::new(utxo_entries.into_iter()), + priority_utxo_entries, change_address, sig_op_count, minimum_signatures, @@ -182,6 +193,7 @@ impl Generator { native::GeneratorSettings::try_new_with_context( utxo_context.into(), + priority_utxo_entries, change_address, sig_op_count, minimum_signatures, @@ -244,6 +256,7 @@ enum GeneratorSource { struct GeneratorSettings { pub network_id: Option, pub source: GeneratorSource, + pub priority_utxo_entries: Option>, pub multiplexer: Option>>, pub final_transaction_destination: PaymentDestination, pub change_address: Option
, @@ -263,18 +276,20 @@ impl TryFrom for GeneratorSettings { let final_transaction_destination: PaymentDestination = if outputs.is_undefined() { PaymentDestination::Change } else { PaymentOutputs::try_owned_from(outputs)?.into() }; - let change_address = args.try_get_cast::
("changeAddress")?.map(Cast::into_owned); + let change_address = args.try_cast_into::
("changeAddress")?; let final_priority_fee = args.get::("priorityFee")?.try_into()?; - let generator_source = if let Ok(Some(context)) = args.try_get_cast::("entries") { - GeneratorSource::UtxoContext(context.into_owned()) + let generator_source = if let Ok(Some(context)) = args.try_cast_into::("entries") { + GeneratorSource::UtxoContext(context) } else if let Some(utxo_entries) = args.try_get_value("entries")? { GeneratorSource::UtxoEntries(utxo_entries.try_into_utxo_entry_references()?) } else { - return Err(Error::custom("'entries', 'context' or 'account' property is required for Generator")); + return Err(Error::custom("'entries' property is required for Generator")); }; + let priority_utxo_entries = args.try_get_value("priorityEntries")?.map(|v| v.try_into_utxo_entry_references()).transpose()?; + let sig_op_count = args.get_value("sigOpCount")?; let sig_op_count = if !sig_op_count.is_undefined() { sig_op_count.as_f64().expect("sigOpCount should be a number") as u8 } else { 1 }; @@ -291,6 +306,7 @@ impl TryFrom for GeneratorSettings { let settings = GeneratorSettings { network_id, source: generator_source, + priority_utxo_entries, multiplexer: None, final_transaction_destination, change_address, diff --git a/wallet/core/src/wasm/tx/generator/pending.rs b/wallet/core/src/wasm/tx/generator/pending.rs index dfa84c9bd7..ee3a9a5d9f 100644 --- a/wallet/core/src/wasm/tx/generator/pending.rs +++ b/wallet/core/src/wasm/tx/generator/pending.rs @@ -3,8 +3,10 @@ use crate::result::Result; use crate::tx::generator as native; use crate::wasm::PrivateKeyArrayT; use kaspa_consensus_client::{numeric, string}; -use kaspa_consensus_client::{ITransaction, Transaction}; +use kaspa_consensus_client::{Transaction, TransactionT}; +use kaspa_consensus_core::hashing::wasm::SighashType; use kaspa_wallet_keys::privatekey::PrivateKey; +use kaspa_wasm_core::types::{BinaryT, HexString}; use kaspa_wrpc_wasm::RpcClient; /// @category Wallet SDK @@ -70,16 +72,44 @@ impl PendingTransaction { self.inner.utxo_entries().values().map(|utxo_entry| JsValue::from(utxo_entry.clone())).collect() } + #[wasm_bindgen(js_name = createInputSignature)] + pub fn create_input_signature( + &self, + input_index: u8, + private_key: &PrivateKey, + sighash_type: Option, + ) -> Result { + let signature = self.inner.create_input_signature( + input_index.into(), + &private_key.secret_bytes(), + sighash_type.unwrap_or(SighashType::All).into(), + )?; + + Ok(signature.to_hex().into()) + } + + #[wasm_bindgen(js_name = fillInput)] + pub fn fill_input(&self, input_index: u8, signature_script: BinaryT) -> Result<()> { + self.inner.fill_input(input_index.into(), signature_script.try_as_vec_u8()?) + } + + #[wasm_bindgen(js_name = signInput)] + pub fn sign_input(&self, input_index: u8, private_key: &PrivateKey, sighash_type: Option) -> Result<()> { + self.inner.sign_input(input_index.into(), &private_key.secret_bytes(), sighash_type.unwrap_or(SighashType::All).into())?; + + Ok(()) + } + /// Sign transaction with supplied [`Array`] or [`PrivateKey`] or an array of /// raw private key bytes (encoded as `Uint8Array` or as hex strings) - pub fn sign(&self, js_value: PrivateKeyArrayT) -> Result<()> { + pub fn sign(&self, js_value: PrivateKeyArrayT, check_fully_signed: Option) -> Result<()> { if let Ok(keys) = js_value.dyn_into::() { let keys = keys .iter() - .map(PrivateKey::try_cast_from) + .map(PrivateKey::try_owned_from) .collect::, kaspa_wallet_keys::error::Error>>()?; - let mut keys = keys.iter().map(|key| key.as_ref().secret_bytes()).collect::>(); - self.inner.try_sign_with_keys(&keys)?; + let mut keys = keys.iter().map(|key| key.secret_bytes()).collect::>(); + self.inner.try_sign_with_keys(&keys, check_fully_signed)?; keys.zeroize(); Ok(()) } else { @@ -92,6 +122,14 @@ impl PendingTransaction { /// {@link UtxoContext} if one was used to create the transaction /// and will return UTXOs back to {@link UtxoContext} in case of /// a failed submission. + /// + /// # Important + /// + /// Make sure to consume the returned `txid` value. Always invoke this method + /// as follows `let txid = await pendingTransaction.submit(rpc);`. If you do not + /// consume the returned value and the rpc object is temporary, the GC will + /// collect the `rpc` object passed to submit() potentially causing a panic. + /// /// @see {@link RpcClient.submitTransaction} pub async fn submit(&self, wasm_rpc_client: &RpcClient) -> Result { let rpc: Arc = wasm_rpc_client.client().clone(); @@ -110,7 +148,7 @@ impl PendingTransaction { /// @see {@link ISerializableTransaction} /// @see {@link Transaction}, {@link ISerializableTransaction} #[wasm_bindgen(js_name = "serializeToObject")] - pub fn serialize_to_object(&self) -> Result { + pub fn serialize_to_object(&self) -> Result { Ok(numeric::SerializableTransaction::from_cctx_transaction(&self.inner.transaction(), self.inner.utxo_entries())? .serialize_to_object()? .into()) diff --git a/wallet/core/src/wasm/tx/mass.rs b/wallet/core/src/wasm/tx/mass.rs index cc522fd8e0..5dd09051f7 100644 --- a/wallet/core/src/wasm/tx/mass.rs +++ b/wallet/core/src/wasm/tx/mass.rs @@ -1,151 +1,43 @@ use crate::imports::NetworkParams; use crate::result::Result; use crate::tx::mass; -use crate::wasm::tx::*; use kaspa_consensus_client::*; use kaspa_consensus_core::config::params::Params; -use kaspa_consensus_core::tx as cctx; -use std::sync::Arc; +use kaspa_consensus_core::network::{NetworkId, NetworkIdT}; use wasm_bindgen::prelude::*; use workflow_wasm::convert::*; +/// `calculateTransactionMass()` returns the mass of the passed transaction. +/// If the transaction is invalid, the function throws an error. +/// If the mass is larger than the transaction mass allowed by the network, the function +/// returns `undefined` which can be treated as a mass overflow condition. +/// /// @category Wallet SDK -#[wasm_bindgen] -pub struct MassCalculator { - mc: Arc, +/// +#[wasm_bindgen(js_name = calculateTransactionMass)] +pub fn calculate_transaction_mass(network_id: NetworkIdT, tx: &TransactionT) -> Result> { + let tx = Transaction::try_cast_from(tx)?; + let network_id = NetworkId::try_owned_from(network_id)?; + let consensus_params = Params::from(network_id); + let network_params = NetworkParams::from(network_id); + let mc = mass::MassCalculator::new(&consensus_params, network_params); + mc.calc_tx_overall_mass(tx.as_ref()) } -#[wasm_bindgen] -impl MassCalculator { - #[wasm_bindgen(constructor)] - pub fn new(cp: ConsensusParams) -> Self { - let consensus_params = Params::from(cp); - let network_params = NetworkParams::from(consensus_params.net); - Self { mc: Arc::new(mass::MassCalculator::new(&consensus_params, &network_params)) } - } - - #[wasm_bindgen(js_name=isDust)] - pub fn is_dust(&self, amount: u64) -> bool { - self.mc.is_dust(amount) - } - - /// `isTransactionOutputDust()` returns whether or not the passed transaction output - /// amount is considered dust or not based on the configured minimum transaction - /// relay fee. - /// - /// Dust is defined in terms of the minimum transaction relay fee. In particular, - /// if the cost to the network to spend coins is more than 1/3 of the minimum - /// transaction relay fee, it is considered dust. - /// - /// It is exposed by `MiningManager` for use by transaction generators and wallets. - #[wasm_bindgen(js_name=isTransactionOutputDust)] - pub fn is_transaction_output_dust(transaction_output: &JsValue) -> Result { - let transaction_output = TransactionOutput::try_from(transaction_output)?; - let transaction_output = cctx::TransactionOutput::from(&transaction_output); - Ok(mass::is_transaction_output_dust(&transaction_output)) - } - - /// `minimumRelayTransactionFee()` specifies the minimum transaction fee for a transaction to be accepted to - /// the mempool and relayed. It is specified in sompi per 1kg (or 1000 grams) of transaction mass. - /// - /// `pub(crate) const MINIMUM_RELAY_TRANSACTION_FEE: u64 = 1000;` - #[wasm_bindgen(js_name=minimumRelayTransactionFee)] - pub fn minimum_relay_transaction_fee() -> u32 { - mass::MINIMUM_RELAY_TRANSACTION_FEE as u32 - } - - /// `maximumStandardTransactionMass()` is the maximum mass allowed for transactions that - /// are considered standard and will therefore be relayed and considered for mining. - /// - /// `pub const MAXIMUM_STANDARD_TRANSACTION_MASS: u64 = 100_000;` - #[wasm_bindgen(js_name=maximumStandardTransactionMass)] - pub fn maximum_standard_transaction_mass() -> u32 { - mass::MAXIMUM_STANDARD_TRANSACTION_MASS as u32 - } - - /// minimum_required_transaction_relay_fee returns the minimum transaction fee required - /// for a transaction with the passed mass to be accepted into the mempool and relayed. - #[wasm_bindgen(js_name=minimumRequiredTransactionRelayFee)] - pub fn calc_minimum_required_transaction_relay_fee(mass: u32) -> u32 { - mass::calc_minimum_required_transaction_relay_fee(mass as u64) as u32 - } - - #[wasm_bindgen(js_name=calcMassForTransaction)] - pub fn calc_mass_for_transaction(&self, tx: &JsValue) -> Result { - let tx = Transaction::try_cast_from(tx)?; - let tx = cctx::Transaction::from(tx.as_ref()); - Ok(self.mc.calc_mass_for_transaction(&tx) as u32) - } - - #[wasm_bindgen(js_name=blankTransactionSerializedByteSize)] - pub fn blank_transaction_serialized_byte_size() -> u32 { - mass::blank_transaction_serialized_byte_size() as u32 - } - - #[wasm_bindgen(js_name=blankTransactionMass)] - pub fn blank_transaction_mass(&self) -> u32 { - self.mc.blank_transaction_mass() as u32 - } - - #[wasm_bindgen(js_name=calcMassForPayload)] - pub fn calc_mass_for_payload(&self, payload_byte_size: usize) -> u32 { - self.mc.calc_mass_for_payload(payload_byte_size) as u32 - } - - #[wasm_bindgen(js_name=calcMassForOutputs)] - pub fn calc_mass_for_outputs(&self, outputs: JsValue) -> Result { - let outputs = outputs - .dyn_into::()? - .iter() - .map(TransactionOutput::try_from) - .collect::, kaspa_consensus_client::error::Error>>()?; - let outputs = outputs.iter().map(|output| self.calc_mass_for_output(output)).collect::>>()?; - Ok(outputs.iter().sum()) - } - - #[wasm_bindgen(js_name=calcMassForInputs)] - pub fn calc_mass_for_inputs(&self, inputs: JsValue) -> Result { - let inputs = inputs - .dyn_into::()? - .iter() - .map(TransactionInput::try_owned_from) - .collect::, kaspa_consensus_client::error::Error>>()?; - let inputs = inputs.iter().map(|input| self.calc_mass_for_input(input)).collect::>>()?; - Ok(inputs.iter().sum()) - } - - #[wasm_bindgen(js_name=calcMassForOutput)] - pub fn calc_mass_for_output(&self, output: &TransactionOutput) -> Result { - // let output = TransactionOutput::try_from(output)?; - let output = cctx::TransactionOutput::from(output); - Ok(self.mc.calc_mass_for_output(&output) as u32) - } - - #[wasm_bindgen(js_name=calcMassForInput)] - pub fn calc_mass_for_input(&self, input: &TransactionInput) -> Result { - // let input = TransactionInput::try_from(input)?; - let input = cctx::TransactionInput::from(input); - Ok(self.mc.calc_mass_for_input(&input) as u32) - } - - #[wasm_bindgen(js_name=calcSignatureMass)] - pub fn calc_signature_mass(&self, minimum_signatures: u16) -> u32 { - self.mc.calc_signature_mass(minimum_signatures) as u32 - } - - #[wasm_bindgen(js_name=calcSignatureMassForInputs)] - pub fn calc_signature_mass_for_inputs(&self, number_of_inputs: usize, minimum_signatures: u16) -> u32 { - self.mc.calc_signature_mass_for_inputs(number_of_inputs, minimum_signatures) as u32 - } - - #[wasm_bindgen(js_name=calcMinimumTransactionRelayFeeFromMass)] - pub fn calc_minimum_transaction_relay_fee_from_mass(&self, mass: u64) -> u32 { - self.mc.calc_minimum_transaction_fee_from_mass(mass) as u32 - } - - #[wasm_bindgen(js_name=calcMiniumTxRelayFee)] - pub fn calc_minimum_transaction_relay_fee(&self, transaction: &Transaction, minimum_signatures: u16) -> Result { - let tx = cctx::Transaction::from(transaction); - Ok(self.mc.calc_minium_transaction_relay_fee(&tx, minimum_signatures) as u32) - } +/// `calculateTransactionFee()` returns minimum fees needed for the transaction to be +/// accepted by the network. If the transaction is invalid, the function throws an error. +/// If the mass of the transaction is larger than the maximum allowed by the network, the +/// function returns `undefined` which can be treated as a mass overflow condition. +/// +/// @category Wallet SDK +/// +#[wasm_bindgen(js_name = calculateTransactionFee)] +pub fn calculate_transaction_fee(network_id: NetworkIdT, tx: &TransactionT) -> Result> { + let tx = Transaction::try_cast_from(tx)?; + let network_id = NetworkId::try_owned_from(network_id)?; + let consensus_params = Params::from(network_id); + let network_params = NetworkParams::from(network_id); + let mc = mass::MassCalculator::new(&consensus_params, network_params); + let fee = mc.calc_tx_overall_mass(tx.as_ref())?.map(|mass| mc.calc_fee_for_mass(mass)); + Ok(fee) } diff --git a/wallet/core/src/wasm/tx/mod.rs b/wallet/core/src/wasm/tx/mod.rs index df826a9971..742bb89cab 100644 --- a/wallet/core/src/wasm/tx/mod.rs +++ b/wallet/core/src/wasm/tx/mod.rs @@ -1,10 +1,8 @@ -pub mod consensus; pub mod fees; pub mod generator; pub mod mass; pub mod utils; -pub use self::consensus::*; pub use self::fees::*; pub use self::generator::*; pub use self::mass::*; diff --git a/wallet/core/src/wasm/tx/utils.rs b/wallet/core/src/wasm/tx/utils.rs index 0d911af76c..cb32c2689f 100644 --- a/wallet/core/src/wasm/tx/utils.rs +++ b/wallet/core/src/wasm/tx/utils.rs @@ -1,14 +1,11 @@ use crate::imports::*; use crate::result::Result; use crate::tx::{IPaymentOutputArray, PaymentOutputs}; -use crate::wasm::tx::consensus::get_consensus_params_by_address; use crate::wasm::tx::generator::*; -use crate::wasm::tx::mass::MassCalculator; -use kaspa_addresses::{Address, AddressT}; use kaspa_consensus_client::*; use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; -//use kaspa_consensus_wasm::*; use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; +use kaspa_wasm_core::types::BinaryT; use workflow_core::runtime::is_web; /// Create a basic transaction without any mass limit checks. @@ -17,32 +14,19 @@ use workflow_core::runtime::is_web; pub fn create_transaction_js( utxo_entry_source: IUtxoEntryArray, outputs: IPaymentOutputArray, - change_address: AddressT, priority_fee: BigInt, - payload: JsValue, - sig_op_count: JsValue, - minimum_signatures: JsValue, + payload: Option, + sig_op_count: Option, ) -> crate::result::Result { - let change_address = Address::try_cast_from(change_address)?; - let params = get_consensus_params_by_address(change_address.as_ref()); - let mc = MassCalculator::new(params); - let utxo_entries = if let Some(utxo_entries) = utxo_entry_source.dyn_ref::() { - utxo_entries.to_vec().iter().map(UtxoEntryReference::try_cast_from).collect::, _>>()? + utxo_entries.to_vec().iter().map(UtxoEntryReference::try_owned_from).collect::, _>>()? } else { return Err(Error::custom("utxo_entries must be an array")); }; let priority_fee: u64 = priority_fee.try_into().map_err(|err| Error::custom(format!("invalid fee value: {err}")))?; - let payload = payload.try_as_vec_u8().ok().unwrap_or_default(); + let payload = payload.and_then(|payload| payload.try_as_vec_u8().ok()).unwrap_or_default(); let outputs = PaymentOutputs::try_owned_from(outputs)?; - let sig_op_count = - if !sig_op_count.is_undefined() { sig_op_count.as_f64().expect("sigOpCount should be a number") as u8 } else { 1 }; - - let minimum_signatures = if !minimum_signatures.is_undefined() { - minimum_signatures.as_f64().expect("minimumSignatures should be a number") as u16 - } else { - 1 - }; + let sig_op_count = sig_op_count.unwrap_or(1); // --- @@ -53,10 +37,10 @@ pub fn create_transaction_js( .into_iter() .enumerate() .map(|(sequence, reference)| { - let UtxoEntryReference { utxo } = reference.as_ref(); + let UtxoEntryReference { utxo } = &reference; total_input_amount += utxo.amount(); - entries.push(reference.as_ref().clone()); - TransactionInput::new(utxo.outpoint.clone(), vec![], sequence as u64, sig_op_count, Some(reference.into_owned())) + entries.push(reference.clone()); + TransactionInput::new(utxo.outpoint.clone(), None, sequence as u64, sig_op_count, Some(reference)) }) .collect::>(); @@ -64,12 +48,8 @@ pub fn create_transaction_js( return Err(format!("priority fee({priority_fee}) > amount({total_input_amount})").into()); } - // TODO - Calculate mass and fees - let outputs: Vec = outputs.into(); let transaction = Transaction::new(None, 0, inputs, outputs, 0, SUBNETWORK_ID_NATIVE, 0, payload)?; - let _fee = mc.calc_minimum_transaction_relay_fee(&transaction, minimum_signatures); - //let mtx = SignableTransaction::new(transaction, entries.into()); Ok(transaction) } diff --git a/wallet/core/src/wasm/utxo/context.rs b/wallet/core/src/wasm/utxo/context.rs index 2b69eb0c47..3298a4829e 100644 --- a/wallet/core/src/wasm/utxo/context.rs +++ b/wallet/core/src/wasm/utxo/context.rs @@ -147,6 +147,7 @@ impl UtxoContext { self.inner().clear().await } + #[wasm_bindgen(getter, js_name = "isActive")] pub fn active(&self) -> bool { let processor = self.inner().processor(); processor.try_rpc_ctl().map(|ctl| ctl.is_connected()).unwrap_or(false) && processor.is_connected() && processor.is_running() @@ -251,7 +252,10 @@ impl From for native::UtxoContext { impl TryCastFromJs for UtxoContext { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { Ok(Self::try_ref_from_js_value_as_cast(value)?) } } @@ -265,15 +269,15 @@ impl TryFrom for UtxoContextCreateArgs { type Error = Error; fn try_from(value: IUtxoContextArgs) -> std::result::Result { if let Some(object) = Object::try_from(&value) { - let processor = object.get_cast::("processor")?; + let processor = object.cast_into::("processor")?; - let binding = if let Some(id) = object.try_get_cast::("id")? { - UtxoContextBinding::Id(UtxoContextId::new(id.into_owned())) + let binding = if let Some(id) = object.try_cast_into::("id")? { + UtxoContextBinding::Id(UtxoContextId::new(id)) } else { UtxoContextBinding::default() }; - Ok(UtxoContextCreateArgs { binding, processor: processor.into_owned() }) + Ok(UtxoContextCreateArgs { binding, processor }) } else { Err(Error::custom("UtxoProcessor: supplied value must be an object")) } diff --git a/wallet/core/src/wasm/utxo/processor.rs b/wallet/core/src/wasm/utxo/processor.rs index 0e41d8f773..d68f10f763 100644 --- a/wallet/core/src/wasm/utxo/processor.rs +++ b/wallet/core/src/wasm/utxo/processor.rs @@ -63,14 +63,14 @@ cfg_if! { /** * @param {UtxoProcessorNotificationCallback} callback */ - addEventListener(callback:UtxoProcessorNotificationCallback): void; + addEventListener(callback: UtxoProcessorNotificationCallback): void; /** * @param {UtxoProcessorEventType} event * @param {UtxoProcessorNotificationCallback} [callback] */ - addEventListener( - event: M, - callback: (eventData: UtxoProcessorEventMap[M]) => void + addEventListener( + event: E, + callback: UtxoProcessorNotificationCallback ) }"#; } @@ -153,11 +153,54 @@ impl UtxoProcessor { self.inner.processor.set_network_id(network_id.as_ref()); Ok(()) } + + #[wasm_bindgen(getter, js_name = "isActive")] + pub fn is_active(&self) -> bool { + let processor = &self.inner.processor; + processor.try_rpc_ctl().map(|ctl| ctl.is_connected()).unwrap_or(false) && processor.is_connected() && processor.is_running() + } + + /// + /// Set the coinbase transaction maturity period DAA score for a given network. + /// This controls the DAA period after which the user transactions are considered mature + /// and the wallet subsystem emits the transaction maturity event. + /// + /// @see {@link TransactionRecord} + /// @see {@link IUtxoProcessorEvent} + /// + /// @category Wallet SDK + /// + #[wasm_bindgen(js_name = "setCoinbaseTransactionMaturityDAA")] + pub fn set_coinbase_transaction_maturity_period_daa_js(network_id: &NetworkIdT, value: u64) -> Result<()> { + let network_id = NetworkId::try_cast_from(network_id)?.into_owned(); + crate::utxo::set_coinbase_transaction_maturity_period_daa(&network_id, value); + Ok(()) + } + + /// + /// Set the user transaction maturity period DAA score for a given network. + /// This controls the DAA period after which the user transactions are considered mature + /// and the wallet subsystem emits the transaction maturity event. + /// + /// @see {@link TransactionRecord} + /// @see {@link IUtxoProcessorEvent} + /// + /// @category Wallet SDK + /// + #[wasm_bindgen(js_name = "setUserTransactionMaturityDAA")] + pub fn set_user_transaction_maturity_period_daa_js(network_id: &NetworkIdT, value: u64) -> Result<()> { + let network_id = NetworkId::try_cast_from(network_id)?.into_owned(); + crate::utxo::set_user_transaction_maturity_period_daa(&network_id, value); + Ok(()) + } } impl TryCastFromJs for UtxoProcessor { type Error = workflow_wasm::error::Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { Self::try_ref_from_js_value_as_cast(value) } } diff --git a/wallet/core/src/wasm/wallet/account.rs b/wallet/core/src/wasm/wallet/account.rs deleted file mode 100644 index 976e1df622..0000000000 --- a/wallet/core/src/wasm/wallet/account.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::account as native; -use crate::imports::*; -use crate::tx::PaymentOutputs; -use crate::wasm::utxo::UtxoContext; -use kaspa_consensus_core::network::NetworkTypeT; -use kaspa_wallet_keys::keypair::Keypair; -use workflow_core::abortable::Abortable; - -/// -/// The `Account` class is a wallet account that can be used to send and receive payments. -/// -/// -/// @category Wallet API -#[wasm_bindgen(inspectable)] -#[derive(Clone, CastFromJs)] -pub struct Account { - inner: Arc, - #[wasm_bindgen(getter_with_clone)] - pub context: UtxoContext, -} - -impl Account { - pub async fn try_new(inner: Arc) -> Result { - let context = inner.utxo_context().clone(); - Ok(Self { inner, context: context.into() }) - } -} - -#[wasm_bindgen] -impl Account { - pub fn ctor(js_value: JsValue) -> Result { - let AccountCreateArgs {} = js_value.try_into()?; - - todo!(); - - // Ok(account) - } - - #[wasm_bindgen(getter)] - pub fn balance(&self) -> JsValue { - match self.inner.balance() { - Some(balance) => crate::wasm::Balance::from(balance).into(), - None => JsValue::UNDEFINED, - } - } - - #[wasm_bindgen(getter, js_name = "type")] - pub fn account_kind(&self) -> String { - self.inner.account_kind().to_string() - } - - #[wasm_bindgen(js_name = balanceStrings)] - pub fn balance_strings(&self, network_type: &NetworkTypeT) -> Result { - match self.inner.balance() { - Some(balance) => Ok(crate::wasm::Balance::from(balance).to_balance_strings(network_type)?.into()), - None => Ok(JsValue::UNDEFINED), - } - } - - #[wasm_bindgen(getter, js_name = "receiveAddress")] - pub fn receive_address(&self) -> Result { - Ok(self.inner.receive_address()?.to_string()) - } - - #[wasm_bindgen(getter, js_name = "changeAddress")] - pub fn change_address(&self) -> Result { - Ok(self.inner.change_address()?.to_string()) - } - - #[wasm_bindgen(js_name = "deriveReceiveAddress")] - pub async fn derive_receive_address(&self) -> Result
{ - let account = self.inner.clone().as_derivation_capable()?; - let receive_address = account.new_receive_address().await?; - Ok(receive_address) - } - - #[wasm_bindgen(js_name = "deriveChangeAddress")] - pub async fn derive_change_address(&self) -> Result
{ - let account = self.inner.clone().as_derivation_capable()?; - let change_address = account.new_change_address().await?; - Ok(change_address) - } - - pub async fn scan(&self) -> Result<()> { - self.inner.clone().scan(None, None).await - } - - pub async fn send(&self, js_value: JsValue) -> Result { - let _args = AccountSendArgs::try_from(js_value)?; - - todo!() - } -} - -impl From for Arc { - fn from(account: Account) -> Self { - account.inner - } -} - -impl TryFrom<&JsValue> for Account { - type Error = Error; - fn try_from(js_value: &JsValue) -> std::result::Result { - Ok(Account::try_ref_from_js_value(js_value)?.clone()) - } -} - -pub struct AccountSendArgs { - pub outputs: PaymentOutputs, - pub priority_fee_sompi: Option, - pub include_fees_in_amount: bool, - - pub wallet_secret: Secret, - pub payment_secret: Option, - pub abortable: Abortable, -} - -impl TryFrom for AccountSendArgs { - type Error = Error; - fn try_from(js_value: JsValue) -> std::result::Result { - if let Some(object) = Object::try_from(&js_value) { - let outputs = object.get_cast::("outputs")?.into_owned(); - - let priority_fee_sompi = object.get_u64("priorityFee").ok(); - let include_fees_in_amount = object.get_bool("includeFeesInAmount").unwrap_or(false); - let abortable = object.get("abortable").ok().and_then(|v| Abortable::try_from(&v).ok()).unwrap_or_default(); - - let wallet_secret = object.get_string("walletSecret")?.into(); - let payment_secret = object.get_value("paymentSecret")?.as_string().map(|s| s.into()); - - let send_args = - AccountSendArgs { outputs, priority_fee_sompi, include_fees_in_amount, wallet_secret, payment_secret, abortable }; - - Ok(send_args) - } else { - Err("Argument to Account::send() must be an object".into()) - } - } -} - -pub struct AccountCreateArgs {} - -impl TryFrom for AccountCreateArgs { - type Error = Error; - fn try_from(value: JsValue) -> std::result::Result { - if let Some(object) = Object::try_from(&value) { - let _keypair = object.try_get_cast::("keypair")?; - let _public_key = object.try_get_cast::("keypair")?; - - Ok(AccountCreateArgs {}) - } else { - Err(Error::custom("Account: supplied value must be an object")) - } - } -} diff --git a/wallet/core/src/wasm/wallet/mod.rs b/wallet/core/src/wasm/wallet/mod.rs index cacd73628d..2ae871c1e3 100644 --- a/wallet/core/src/wasm/wallet/mod.rs +++ b/wallet/core/src/wasm/wallet/mod.rs @@ -1,4 +1,3 @@ -pub mod account; pub mod keydata; #[allow(clippy::module_inception)] pub mod wallet; diff --git a/wallet/keys/Cargo.toml b/wallet/keys/Cargo.toml index 7300c1de57..b352f42a92 100644 --- a/wallet/keys/Cargo.toml +++ b/wallet/keys/Cargo.toml @@ -46,5 +46,5 @@ zeroize.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio.workspace = true -[lints.clippy] -empty_docs = "allow" +[lints] +workspace = true diff --git a/wallet/keys/src/derivation/gen0/hd.rs b/wallet/keys/src/derivation/gen0/hd.rs index dd85f8fe5b..3b7da0e758 100644 --- a/wallet/keys/src/derivation/gen0/hd.rs +++ b/wallet/keys/src/derivation/gen0/hd.rs @@ -176,7 +176,7 @@ impl PubkeyDerivationManagerV0 { return Ok(*key); } - Err(crate::error::Error::Custom("PubkeyDerivationManagerV0 initialization is pending (Error: 102).".into())) + Err(crate::error::Error::Custom("PubkeyDerivationManagerV0 initialization is pending (Error: 105).".into())) } pub fn create_address(key: &secp256k1::PublicKey, prefix: AddressPrefix, _ecdsa: bool) -> Result
{ diff --git a/wallet/keys/src/derivation_path.rs b/wallet/keys/src/derivation_path.rs index a4c3efe691..df220ee445 100644 --- a/wallet/keys/src/derivation_path.rs +++ b/wallet/keys/src/derivation_path.rs @@ -51,8 +51,11 @@ impl DerivationPath { impl TryCastFromJs for DerivationPath { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { let value = value.as_ref(); if let Some(path) = value.as_string() { Ok(DerivationPath::new(&path)?) diff --git a/wallet/keys/src/imports.rs b/wallet/keys/src/imports.rs index 6597aad3c3..a1c0a5e0d2 100644 --- a/wallet/keys/src/imports.rs +++ b/wallet/keys/src/imports.rs @@ -15,7 +15,7 @@ pub use borsh::{BorshDeserialize, BorshSerialize}; pub use js_sys::Array; pub use kaspa_addresses::{Address, Version as AddressVersion}; pub use kaspa_bip32::{ChildNumber, ExtendedPrivateKey, ExtendedPublicKey, SecretKey}; -pub use kaspa_consensus_core::network::NetworkTypeT; +pub use kaspa_consensus_core::network::{NetworkId, NetworkTypeT}; pub use kaspa_utils::hex::*; pub use kaspa_wasm_core::types::*; pub use serde::{Deserialize, Serialize}; diff --git a/wallet/keys/src/keypair.rs b/wallet/keys/src/keypair.rs index 3bed0152b7..f4b39f3d39 100644 --- a/wallet/keys/src/keypair.rs +++ b/wallet/keys/src/keypair.rs @@ -100,7 +100,10 @@ impl Keypair { impl TryCastFromJs for Keypair { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { Ok(Self::try_ref_from_js_value_as_cast(value)?) } } diff --git a/wallet/keys/src/privatekey.rs b/wallet/keys/src/privatekey.rs index 825bf395fa..84e2d2e3ba 100644 --- a/wallet/keys/src/privatekey.rs +++ b/wallet/keys/src/privatekey.rs @@ -93,8 +93,11 @@ impl PrivateKey { impl TryCastFromJs for PrivateKey { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { if let Some(hex_str) = value.as_ref().as_string() { Self::try_new(hex_str.as_str()) } else if Array::is_array(value.as_ref()) { diff --git a/wallet/keys/src/pubkeygen.rs b/wallet/keys/src/pubkeygen.rs index 272e638455..c05ae844fa 100644 --- a/wallet/keys/src/pubkeygen.rs +++ b/wallet/keys/src/pubkeygen.rs @@ -21,7 +21,7 @@ pub struct PublicKeyGenerator { #[wasm_bindgen] impl PublicKeyGenerator { #[wasm_bindgen(js_name=fromXPub)] - pub fn from_xpub(kpub: XPubT, cosigner_index: Option) -> Result { + pub fn from_xpub(kpub: &XPubT, cosigner_index: Option) -> Result { let kpub = XPub::try_cast_from(kpub)?; let xpub = kpub.as_ref().inner(); let hd_wallet = WalletDerivationManager::from_extended_public_key(xpub.clone(), cosigner_index)?; @@ -193,7 +193,7 @@ impl PublicKeyGenerator { #[wasm_bindgen(js_name=changeAddressAsString)] #[allow(non_snake_case)] pub fn change_address_as_string(&self, networkType: &NetworkTypeT, index: u32) -> Result { - Ok(PublicKey::from(self.hd_wallet.receive_pubkey_manager().derive_pubkey(index)?) + Ok(PublicKey::from(self.hd_wallet.change_pubkey_manager().derive_pubkey(index)?) .to_address(networkType.try_into()?)? .to_string()) } diff --git a/wallet/keys/src/publickey.rs b/wallet/keys/src/publickey.rs index f262a12f4a..513f292c43 100644 --- a/wallet/keys/src/publickey.rs +++ b/wallet/keys/src/publickey.rs @@ -138,8 +138,11 @@ extern "C" { impl TryCastFromJs for PublicKey { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { let value = value.as_ref(); if let Some(hex_str) = value.as_string() { Ok(PublicKey::try_new(hex_str.as_str())?) @@ -150,13 +153,13 @@ impl TryCastFromJs for PublicKey { } } -impl TryFrom for Vec { +impl TryFrom<&PublicKeyArrayT> for Vec { type Error = Error; - fn try_from(value: PublicKeyArrayT) -> Result { + fn try_from(value: &PublicKeyArrayT) -> Result { if value.is_array() { - let array = Array::from(&value); - let pubkeys = array.iter().map(PublicKey::try_cast_from).collect::>>()?; - Ok(pubkeys.iter().map(|pk| pk.as_ref().try_into()).collect::>>()?) + let array = Array::from(value); + let pubkeys = array.iter().map(PublicKey::try_owned_from).collect::>>()?; + Ok(pubkeys.iter().map(|pk| pk.try_into()).collect::>>()?) } else { Err(Error::InvalidPublicKeyArray) } diff --git a/wallet/keys/src/xprv.rs b/wallet/keys/src/xprv.rs index bc4b681cf8..3e120841b7 100644 --- a/wallet/keys/src/xprv.rs +++ b/wallet/keys/src/xprv.rs @@ -1,3 +1,5 @@ +use kaspa_bip32::{ChainCode, KeyFingerprint}; + use crate::imports::*; /// @@ -13,7 +15,7 @@ use crate::imports::*; /// #[derive(Clone, CastFromJs)] -#[wasm_bindgen] +#[wasm_bindgen(inspectable)] pub struct XPrv { inner: ExtendedPrivateKey, } @@ -41,9 +43,9 @@ impl XPrv { } #[wasm_bindgen(js_name=deriveChild)] - pub fn derive_child(&self, chile_number: u32, hardened: Option) -> Result { - let chile_number = ChildNumber::new(chile_number, hardened.unwrap_or(false))?; - let inner = self.inner.derive_child(chile_number)?; + pub fn derive_child(&self, child_number: u32, hardened: Option) -> Result { + let child_number = ChildNumber::new(child_number, hardened.unwrap_or(false))?; + let inner = self.inner.derive_child(child_number)?; Ok(Self { inner }) } @@ -70,6 +72,60 @@ impl XPrv { let public_key = self.inner.public_key(); Ok(public_key.into()) } + + #[wasm_bindgen(js_name = toPrivateKey)] + pub fn to_private_key(&self) -> Result { + let private_key = self.inner.private_key(); + Ok(private_key.into()) + } + + // ~~~~ Getters ~~~~ + + #[wasm_bindgen(getter)] + pub fn xprv(&self) -> Result { + let str = self.inner.to_extended_key("kprv".try_into()?).to_string(); + Ok(str) + } + + #[wasm_bindgen(getter, js_name = "privateKey")] + pub fn private_key_as_hex_string(&self) -> String { + use kaspa_bip32::PrivateKey; + self.inner.private_key().to_bytes().to_vec().to_hex() + } + + #[wasm_bindgen(getter)] + pub fn depth(&self) -> u8 { + self.inner.attrs().depth + } + + #[wasm_bindgen(getter, js_name = parentFingerprint)] + pub fn parent_fingerprint_as_hex_string(&self) -> String { + self.inner.attrs().parent_fingerprint.to_vec().to_hex() + } + + #[wasm_bindgen(getter, js_name = childNumber)] + pub fn child_number(&self) -> u32 { + self.inner.attrs().child_number.into() + } + + #[wasm_bindgen(getter, js_name = chainCode)] + pub fn chain_code_as_hex_string(&self) -> String { + self.inner.attrs().chain_code.to_vec().to_hex() + } +} + +impl XPrv { + pub fn private_key(&self) -> &SecretKey { + self.inner.private_key() + } + + pub fn parent_fingerprint(&self) -> KeyFingerprint { + self.inner.attrs().parent_fingerprint + } + + pub fn chain_code(&self) -> ChainCode { + self.inner.attrs().chain_code + } } impl<'a> From<&'a XPrv> for &'a ExtendedPrivateKey { @@ -86,8 +142,11 @@ extern "C" { impl TryCastFromJs for XPrv { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { if let Some(xprv) = value.as_ref().as_string() { Ok(XPrv::from_xprv_str(xprv)?) } else { diff --git a/wallet/keys/src/xpub.rs b/wallet/keys/src/xpub.rs index ded247edf0..551881d8ee 100644 --- a/wallet/keys/src/xpub.rs +++ b/wallet/keys/src/xpub.rs @@ -1,3 +1,6 @@ +use kaspa_bip32::{ChainCode, KeyFingerprint, Prefix}; +use std::{fmt, str::FromStr}; + use crate::imports::*; /// @@ -12,7 +15,7 @@ use crate::imports::*; /// @category Wallet SDK /// #[derive(Clone, CastFromJs)] -#[wasm_bindgen] +#[wasm_bindgen(inspectable)] pub struct XPub { inner: ExtendedPublicKey, } @@ -32,9 +35,9 @@ impl XPub { } #[wasm_bindgen(js_name=deriveChild)] - pub fn derive_child(&self, chile_number: u32, hardened: Option) -> Result { - let chile_number = ChildNumber::new(chile_number, hardened.unwrap_or(false))?; - let inner = self.inner.derive_child(chile_number)?; + pub fn derive_child(&self, child_number: u32, hardened: Option) -> Result { + let child_number = ChildNumber::new(child_number, hardened.unwrap_or(false))?; + let inner = self.inner.derive_child(child_number)?; Ok(Self { inner }) } @@ -55,6 +58,44 @@ impl XPub { pub fn public_key(&self) -> PublicKey { self.inner.public_key().into() } + + // ~~~~ Getters ~~~~ + + #[wasm_bindgen(getter)] + pub fn xpub(&self) -> Result { + let str = self.inner.to_extended_key("kpub".try_into()?).to_string(); + Ok(str) + } + + #[wasm_bindgen(getter)] + pub fn depth(&self) -> u8 { + self.inner.attrs().depth + } + + #[wasm_bindgen(getter, js_name = parentFingerprint)] + pub fn parent_fingerprint_as_hex_string(&self) -> String { + self.inner.attrs().parent_fingerprint.to_vec().to_hex() + } + + #[wasm_bindgen(getter, js_name = childNumber)] + pub fn child_number(&self) -> u32 { + self.inner.attrs().child_number.into() + } + + #[wasm_bindgen(getter, js_name = chainCode)] + pub fn chain_code_as_hex_string(&self) -> String { + self.inner.attrs().chain_code.to_vec().to_hex() + } +} + +impl XPub { + pub fn parent_fingerprint(&self) -> KeyFingerprint { + self.inner.attrs().parent_fingerprint + } + + pub fn chain_code(&self) -> ChainCode { + self.inner.attrs().chain_code + } } impl From> for XPub { @@ -71,8 +112,11 @@ extern "C" { impl TryCastFromJs for XPub { type Error = Error; - fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { - Self::resolve(&value, || { + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { if let Some(xpub) = value.as_ref().as_string() { Ok(XPub::try_new(xpub.as_str())?) } else { @@ -81,3 +125,26 @@ impl TryCastFromJs for XPub { }) } } + +pub struct NetworkTaggedXpub { + pub xpub: ExtendedPublicKey, + pub network_id: NetworkId, +} +// impl NetworkTaggedXpub { + +// } + +impl fmt::Display for NetworkTaggedXpub { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let obj: XPub = self.xpub.clone().into(); + write!(f, "{}", obj.inner.to_string(Some(Prefix::from(self.network_id)))) + } +} + +type TaggedXpub = (ExtendedPublicKey, NetworkId); + +impl From for NetworkTaggedXpub { + fn from(value: TaggedXpub) -> Self { + Self { xpub: value.0, network_id: value.1 } + } +} diff --git a/wallet/macros/src/wallet/client.rs b/wallet/macros/src/wallet/client.rs index 8bbc51b8a6..c942af013e 100644 --- a/wallet/macros/src/wallet/client.rs +++ b/wallet/macros/src/wallet/client.rs @@ -61,7 +61,7 @@ impl ToTokens for RpcTable { { match __self.codec { Codec::Borsh(ref codec) => { - Ok(#response_type::try_from_slice(&codec.call(op, request.try_to_vec()?).await?)?) + Ok(#response_type::try_from_slice(&codec.call(op, borsh::to_vec(&request)?).await?)?) }, Codec::Serde(ref codec) => { let request = serde_json::to_string(&request)?; diff --git a/wallet/macros/src/wallet/server.rs b/wallet/macros/src/wallet/server.rs index a1e7bf13e9..073d33021e 100644 --- a/wallet/macros/src/wallet/server.rs +++ b/wallet/macros/src/wallet/server.rs @@ -38,7 +38,7 @@ impl ToTokens for RpcTable { targets_borsh.push(quote! { #hash_64 => { - Ok(self.wallet_api().#fn_call(#request_type::try_from_slice(&request)?).await?.try_to_vec()?) + Ok(borsh::to_vec(&self.wallet_api().#fn_call(#request_type::try_from_slice(&request)?).await?)?) } }); diff --git a/wallet/pskt/Cargo.toml b/wallet/pskt/Cargo.toml index f2d82cf07c..b3fff1bfaf 100644 --- a/wallet/pskt/Cargo.toml +++ b/wallet/pskt/Cargo.toml @@ -19,6 +19,7 @@ wasm32-sdk = ["kaspa-consensus-client/wasm32-sdk"] wasm32-types = ["kaspa-consensus-client/wasm32-types"] [dependencies] +kaspa-addresses.workspace = true kaspa-bip32.workspace = true kaspa-consensus-client.workspace = true kaspa-consensus-core.workspace = true @@ -26,12 +27,20 @@ kaspa-txscript-errors.workspace = true kaspa-txscript.workspace = true kaspa-utils.workspace = true +bincode.workspace = true derive_builder.workspace = true +js-sys.workspace = true +futures.workspace = true +hex.workspace = true secp256k1.workspace = true +serde_repr.workspace = true serde-value.workspace = true serde.workspace = true -serde_repr.workspace = true thiserror.workspace = true +wasm-bindgen.workspace = true +serde_json.workspace = true +serde-wasm-bindgen.workspace = true +workflow-wasm.workspace = true [dev-dependencies] serde_json.workspace = true diff --git a/wallet/pskt/examples/multisig.rs b/wallet/pskt/examples/multisig.rs index a34bef9b55..fb011402fb 100644 --- a/wallet/pskt/examples/multisig.rs +++ b/wallet/pskt/examples/multisig.rs @@ -3,7 +3,9 @@ use kaspa_consensus_core::{ tx::{TransactionId, TransactionOutpoint, UtxoEntry}, }; use kaspa_txscript::{multisig_redeem_script, opcodes::codes::OpData65, pay_to_script_hash_script, script_builder::ScriptBuilder}; -use kaspa_wallet_pskt::{Combiner, Creator, Extractor, Finalizer, Inner, InputBuilder, SignInputOk, Signature, Signer, Updater, PSKT}; +use kaspa_wallet_pskt::prelude::{ + Combiner, Creator, Extractor, Finalizer, Inner, InputBuilder, SignInputOk, Signature, Signer, Updater, PSKT, +}; use secp256k1::{rand::thread_rng, Keypair}; use std::{iter, str::FromStr}; diff --git a/wallet/pskt/src/bundle.rs b/wallet/pskt/src/bundle.rs new file mode 100644 index 0000000000..6e8dc83506 --- /dev/null +++ b/wallet/pskt/src/bundle.rs @@ -0,0 +1,353 @@ +use crate::error::Error; +use crate::prelude::*; +use crate::pskt::{Inner as PSKTInner, PSKT}; +// use crate::wasm::result; + +use kaspa_addresses::{Address, Prefix}; +// use kaspa_bip32::Prefix; +use kaspa_consensus_core::network::{NetworkId, NetworkType}; +use kaspa_consensus_core::tx::{ScriptPublicKey, TransactionOutpoint, UtxoEntry}; + +use hex; +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; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Bundle(pub Vec); + +impl From> for Bundle { + fn from(pskt: PSKT) -> Self { + Bundle(vec![pskt.deref().clone()]) + } +} + +impl From>> for Bundle { + fn from(pskts: Vec>) -> Self { + let inner_list = pskts.into_iter().map(|pskt| pskt.deref().clone()).collect(); + Bundle(inner_list) + } +} + +impl Bundle { + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Adds an Inner instance to the bundle + pub fn add_inner(&mut self, inner: PSKTInner) { + self.0.push(inner); + } + + /// Adds a PSKT instance to the bundle + pub fn add_pskt(&mut self, pskt: PSKT) { + self.0.push(pskt.deref().clone()); + } + + /// Merges another bundle into the current bundle + pub fn merge(&mut self, other: Bundle) { + for inner in other.0 { + self.0.push(inner); + } + } + + /// Iterator over the inner PSKT instances + pub fn iter(&self) -> std::slice::Iter { + self.0.iter() + } + + pub fn serialize(&self) -> Result { + Ok(format!("PSKB{}", hex::encode(serde_json::to_string(self)?))) + } + + pub fn deserialize(hex_data: &str) -> Result { + if let Some(hex_data) = hex_data.strip_prefix("PSKB") { + Ok(serde_json::from_slice(hex::decode(hex_data)?.as_slice())?) + } else { + Err(Error::PskbPrefixError) + } + } + + pub fn display_format(&self, network_id: NetworkId, sompi_formatter: F) -> String + where + F: Fn(u64, &NetworkType) -> String, + { + let mut result = "".to_string(); + + for (pskt_index, bundle_inner) in self.0.iter().enumerate() { + let pskt: PSKT = PSKT::::from(bundle_inner.to_owned()); + + result.push_str(&format!("\r\nPSKT #{:02}\r\n", pskt_index + 1)); + + for (key_inner, input) in pskt.clone().inputs.iter().enumerate() { + result.push_str(&format!("Input #{:02}\r\n", key_inner + 1)); + + if let Some(utxo_entry) = &input.utxo_entry { + result.push_str(&format!(" amount: {}\r\n", sompi_formatter(utxo_entry.amount, &NetworkType::from(network_id)))); + result.push_str(&format!( + " address: {}\r\n", + extract_script_pub_key_address(&utxo_entry.script_public_key, Prefix::from(network_id)) + .expect("Input address") + )); + } + } + + result.push_str("---\r\n"); + + for (key_inner, output) in pskt.clone().outputs.iter().enumerate() { + result.push_str(&format!("Output #{:02}\r\n", key_inner + 1)); + result.push_str(&format!(" amount: {}\r\n", sompi_formatter(output.amount, &NetworkType::from(network_id)))); + result.push_str(&format!( + " address: {}\r\n", + extract_script_pub_key_address(&output.script_public_key, Prefix::from(network_id)).expect("Input address") + )); + } + } + result + } +} + +impl AsRef<[PSKTInner]> for Bundle { + fn as_ref(&self) -> &[PSKTInner] { + self.0.as_slice() + } +} + +impl TryFrom for Bundle { + type Error = Error; + fn try_from(value: String) -> Result { + Bundle::deserialize(&value) + } +} + +impl TryFrom<&str> for Bundle { + type Error = Error; + fn try_from(value: &str) -> Result { + Bundle::deserialize(value) + } +} +impl TryFrom for String { + type Error = Error; + fn try_from(value: Bundle) -> Result { + match Bundle::serialize(&value) { + Ok(output) => Ok(output.to_owned()), + Err(e) => Err(Error::PskbSerializeError(e.to_string())), + } + } +} + +impl Default for Bundle { + fn default() -> Self { + Self::new() + } +} + +pub fn lock_script_sig_templating(payload: String, pubkey_bytes: Option<&[u8]>) -> Result, Error> { + let mut payload_bytes: Vec = hex::decode(payload)?; + + if let Some(pubkey) = pubkey_bytes { + let placeholder = b"{{pubkey}}"; + + // Search for the placeholder in payload bytes to be replaced by public key. + if let Some(pos) = payload_bytes.windows(placeholder.len()).position(|window| window == placeholder) { + payload_bytes.splice(pos..pos + placeholder.len(), pubkey.iter().cloned()); + } + } + Ok(payload_bytes) +} + +pub fn script_sig_to_address(script_sig: &[u8], prefix: kaspa_addresses::Prefix) -> Result { + extract_script_pub_key_address(&pay_to_script_hash_script(script_sig), prefix).map_err(Error::P2SHExtractError) +} + +pub fn unlock_utxos_as_pskb( + utxo_references: Vec<(UtxoEntry, TransactionOutpoint)>, + recipient: &Address, + script_sig: Vec, + priority_fee_sompi_per_transaction: u64, +) -> Result { + // Fee per transaction. + // Check if each UTXO's amounts can cover priority fee. + utxo_references + .iter() + .map(|(entry, _)| { + if entry.amount <= priority_fee_sompi_per_transaction { + return Err(Error::ExcessUnlockFeeError); + } + Ok(()) + }) + .collect::, _>>()?; + + let recipient_spk = pay_to_address_script(recipient); + let (successes, errors): (Vec<_>, Vec<_>) = utxo_references + .into_iter() + .map(|(utxo_entry, outpoint)| { + unlock_utxo(&utxo_entry, &outpoint, &recipient_spk, &script_sig, priority_fee_sompi_per_transaction) + }) + .partition(Result::is_ok); + + let successful_bundles: Vec<_> = successes.into_iter().filter_map(Result::ok).collect(); + let error_list: Vec<_> = errors.into_iter().filter_map(Result::err).collect(); + + if !error_list.is_empty() { + return Err(Error::MultipleUnlockUtxoError(error_list)); + } + + let merged_bundle = successful_bundles.into_iter().fold(None, |acc: Option, bundle| match acc { + Some(mut merged_bundle) => { + merged_bundle.merge(bundle); + Some(merged_bundle) + } + None => Some(bundle), + }); + + match merged_bundle { + None => Err("Generating an empty PSKB".into()), + Some(bundle) => Ok(bundle), + } +} + +pub fn unlock_utxo( + utxo_entry: &UtxoEntry, + outpoint: &TransactionOutpoint, + script_public_key: &ScriptPublicKey, + script_sig: &[u8], + priority_fee_sompi: u64, +) -> Result { + let input = InputBuilder::default() + .utxo_entry(utxo_entry.to_owned()) + .previous_outpoint(outpoint.to_owned()) + .sig_op_count(1) + .redeem_script(script_sig.to_vec()) + .build()?; + + let output = OutputBuilder::default() + .amount(utxo_entry.amount - priority_fee_sompi) + .script_public_key(script_public_key.clone()) + .build()?; + + let pskt: PSKT = PSKT::::default().constructor().input(input).output(output); + Ok(pskt.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + use crate::role::Creator; + use crate::role::*; + use kaspa_consensus_core::tx::{TransactionId, TransactionOutpoint, UtxoEntry}; + use kaspa_txscript::{multisig_redeem_script, pay_to_script_hash_script}; + use secp256k1::Secp256k1; + use secp256k1::{rand::thread_rng, Keypair}; + use std::str::FromStr; + use std::sync::Once; + + static INIT: Once = Once::new(); + static mut CONTEXT: Option)>> = None; + + fn mock_context() -> &'static ([Keypair; 2], Vec) { + unsafe { + INIT.call_once(|| { + let kps = [Keypair::new(&Secp256k1::new(), &mut thread_rng()), Keypair::new(&Secp256k1::new(), &mut thread_rng())]; + let redeem_script: Vec = multisig_redeem_script(kps.iter().map(|pk| pk.x_only_public_key().0.serialize()), 2) + .expect("Test multisig redeem script"); + + CONTEXT = Some(Box::new((kps, redeem_script))); + }); + + CONTEXT.as_ref().unwrap() + } + } + + // Mock multisig PSKT from example + fn mock_pskt_constructor() -> PSKT { + let (_, redeem_script) = mock_context(); + let pskt = PSKT::::default().inputs_modifiable().outputs_modifiable(); + let input_0 = InputBuilder::default() + .utxo_entry(UtxoEntry { + amount: 12793000000000, + script_public_key: pay_to_script_hash_script(redeem_script), + block_daa_score: 36151168, + is_coinbase: false, + }) + .previous_outpoint(TransactionOutpoint { + transaction_id: TransactionId::from_str("63020db736215f8b1105a9281f7bcbb6473d965ecc45bb2fb5da59bd35e6ff84").unwrap(), + index: 0, + }) + .sig_op_count(2) + .redeem_script(redeem_script.to_owned()) + .build() + .expect("Mock PSKT constructor"); + + pskt.constructor().input(input_0) + } + + #[test] + fn test_pskb_serialization() { + let constructor = mock_pskt_constructor(); + let bundle = Bundle::from(constructor.clone()); + + println!("Bundle: {}", serde_json::to_string(&bundle).unwrap()); + + // Serialize Bundle + let serialized = bundle.serialize().map_err(|err| format!("Unable to serialize bundle: {err}")).unwrap(); + println!("Serialized: {}", serialized); + + assert!(!bundle.0.is_empty()); + + match Bundle::deserialize(&serialized) { + Ok(bundle_constructor_deser) => { + println!("Deserialized: {:?}", bundle_constructor_deser); + let pskt_constructor_deser: Option> = + bundle_constructor_deser.0.first().map(|inner| PSKT::from(inner.clone())); + match pskt_constructor_deser { + Some(_) => println!("PSKT deserialized successfully"), + None => println!("No elements in the inner list to deserialize"), + } + } + Err(e) => { + eprintln!("Failed to deserialize: {}", e); + panic!() + } + } + } + + #[test] + fn test_pskb_bundle_creation() { + let bundle = Bundle::new(); + assert!(bundle.0.is_empty()); + } + + #[test] + fn test_pskb_new_with_pskt() { + let pskt = PSKT::::default(); + let bundle = Bundle::from(pskt); + assert_eq!(bundle.0.len(), 1); + } + + #[test] + fn test_pskb_add_pskt() { + let mut bundle = Bundle::new(); + let pskt = PSKT::::default(); + bundle.add_pskt(pskt); + assert_eq!(bundle.0.len(), 1); + } + + #[test] + fn test_pskb_merge_bundles() { + let mut bundle1 = Bundle::new(); + let mut bundle2 = Bundle::new(); + + let inner1 = PSKTInner::default(); + let inner2 = PSKTInner::default(); + + bundle1.add_inner(inner1.clone()); + bundle2.add_inner(inner2.clone()); + + bundle1.merge(bundle2); + + assert_eq!(bundle1.0.len(), 2); + } +} diff --git a/wallet/pskt/src/convert.rs b/wallet/pskt/src/convert.rs new file mode 100644 index 0000000000..18acf94ed9 --- /dev/null +++ b/wallet/pskt/src/convert.rs @@ -0,0 +1,109 @@ +use crate::error::Error; +use crate::input::{Input, InputBuilder}; +use crate::output::{Output, OutputBuilder}; +use crate::pskt::{Global, Inner}; +use kaspa_consensus_client::{Transaction, TransactionInput, TransactionInputInner, TransactionOutput, TransactionOutputInner}; +use kaspa_consensus_core::tx as cctx; + +impl TryFrom for Inner { + type Error = Error; + fn try_from(_transaction: Transaction) -> Result { + Inner::try_from(cctx::Transaction::from(&_transaction)) + } +} + +impl TryFrom for Input { + type Error = Error; + fn try_from(input: TransactionInput) -> std::result::Result { + let TransactionInputInner { previous_outpoint, signature_script: _, sequence: _, sig_op_count, utxo } = &*input.inner(); + + let input = InputBuilder::default() + .utxo_entry(utxo.as_ref().ok_or(Error::MissingUtxoEntry)?.into()) + .previous_outpoint(previous_outpoint.into()) + // .sequence(*sequence) + // min_time + // partial_sigs + // sighash_type + // redeem_script + .sig_op_count(*sig_op_count) + // bip32_derivations + // final_script_sig + .build()?; + + Ok(input) + } +} + +impl TryFrom for Output { + type Error = Error; + fn try_from(output: TransactionOutput) -> std::result::Result { + // Self::Transaction(transaction) + + let TransactionOutputInner { value, script_public_key } = &*output.inner(); + + let output = OutputBuilder::default() + .amount(*value) + .script_public_key(script_public_key.clone()) + // .redeem_script + // .bip32_derivations + // .proprietaries + // .unknowns + .build()?; + + Ok(output) + } +} + +impl TryFrom<(cctx::Transaction, Vec<(&cctx::TransactionInput, &cctx::UtxoEntry)>)> for Inner { + type Error = Error; // Define your error type + + fn try_from( + (transaction, populated_inputs): (cctx::Transaction, Vec<(&cctx::TransactionInput, &cctx::UtxoEntry)>), + ) -> Result { + let inputs: Result, Self::Error> = populated_inputs + .into_iter() + .map(|(input, utxo)| { + InputBuilder::default() + .utxo_entry(utxo.to_owned().clone()) + .previous_outpoint(input.previous_outpoint) + .sig_op_count(input.sig_op_count) + .build() + .map_err(Error::TxToInnerConversionInputBuildingError) + // Handle the error + }) + .collect::>(); + + let outputs: Result, Self::Error> = transaction + .outputs + .iter() + .map(|output| { + Output::try_from(TransactionOutput::from(output.to_owned())).map_err(|e| Error::TxToInnerConversionError(Box::new(e))) + }) + .collect::>(); + + Ok(Inner { global: Global::default(), inputs: inputs?, outputs: outputs? }) + } +} + +impl TryFrom for Inner { + type Error = Error; + fn try_from(transaction: cctx::Transaction) -> Result { + let inputs = transaction + .inputs + .iter() + .map(|input| { + Input::try_from(TransactionInput::from(input.to_owned())).map_err(|e| Error::TxToInnerConversionError(Box::new(e))) + }) + .collect::>()?; + + let outputs = transaction + .outputs + .iter() + .map(|output| { + Output::try_from(TransactionOutput::from(output.to_owned())).map_err(|e| Error::TxToInnerConversionError(Box::new(e))) + }) + .collect::>()?; + + Ok(Inner { global: Global::default(), inputs, outputs }) + } +} diff --git a/wallet/pskt/src/error.rs b/wallet/pskt/src/error.rs index 5041190862..11303ae4a3 100644 --- a/wallet/pskt/src/error.rs +++ b/wallet/pskt/src/error.rs @@ -1,11 +1,46 @@ +use kaspa_txscript_errors::TxScriptError; + +use crate::input::InputBuilderError; + #[derive(thiserror::Error, Debug)] pub enum Error { + #[error("{0}")] + Custom(String), #[error(transparent)] ConstructorError(#[from] ConstructorError), #[error("OutputNotModifiable")] OutOfBounds, + #[error("Missing UTXO entry")] + MissingUtxoEntry, + #[error("Missing redeem script")] + MissingRedeemScript, + #[error(transparent)] + InputBuilder(#[from] crate::input::InputBuilderError), + #[error(transparent)] + OutputBuilder(#[from] crate::output::OutputBuilderError), + #[error("Serialization error: {0}")] + HexDecodeError(#[from] hex::FromHexError), + #[error("Json deserialize error: {0}")] + JsonDeserializeError(#[from] serde_json::Error), + #[error("Serialize error")] + PskbSerializeError(String), + #[error("Unlock utxo error")] + MultipleUnlockUtxoError(Vec), + #[error("Unlock fees exceed available amount")] + ExcessUnlockFeeError, + #[error("Transaction output to output conversion error")] + TxToInnerConversionError(#[source] Box), + #[error("Transaction input building error in conversion")] + TxToInnerConversionInputBuildingError(#[source] InputBuilderError), + #[error("P2SH extraction error")] + P2SHExtractError(#[source] TxScriptError), + #[error("PSKB hex serialization error: {0}")] + PskbSerializeToHexError(String), + #[error("PSKB serialization requires 'PSKB' prefix")] + PskbPrefixError, + #[error("PSKT serialization requires 'PSKT' prefix")] + PsktPrefixError, } - #[derive(thiserror::Error, Debug)] pub enum ConstructorError { #[error("InputNotModifiable")] @@ -13,3 +48,21 @@ pub enum ConstructorError { #[error("OutputNotModifiable")] OutputNotModifiable, } + +impl From for Error { + fn from(err: String) -> Self { + Self::Custom(err) + } +} + +impl From<&str> for Error { + fn from(err: &str) -> Self { + Self::Custom(err.to_string()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ConversionError { + #[error("Invalid output conversion")] + InvalidOutput, +} diff --git a/wallet/pskt/src/global.rs b/wallet/pskt/src/global.rs index 8e16b832bc..b79798776d 100644 --- a/wallet/pskt/src/global.rs +++ b/wallet/pskt/src/global.rs @@ -1,4 +1,5 @@ -use crate::{utils::combine_if_no_conflicts, KeySource, Version}; +use crate::pskt::{KeySource, Version}; +use crate::utils::combine_if_no_conflicts; use derive_builder::Builder; use kaspa_consensus_core::tx::TransactionId; use serde::{Deserialize, Serialize}; @@ -10,6 +11,7 @@ use std::{ type Xpub = kaspa_bip32::ExtendedPublicKey; #[derive(Debug, Clone, Builder, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] #[builder(default)] pub struct Global { /// The version number of this PSKT. @@ -33,6 +35,7 @@ pub struct Global { /// Proprietary key-value pairs for this output. pub proprietaries: BTreeMap, /// Unknown key-value pairs for this output. + #[serde(flatten)] pub unknowns: BTreeMap, } diff --git a/wallet/pskt/src/input.rs b/wallet/pskt/src/input.rs index 4c25600a1f..c99ae25426 100644 --- a/wallet/pskt/src/input.rs +++ b/wallet/pskt/src/input.rs @@ -1,7 +1,5 @@ -use crate::{ - utils::{combine_if_no_conflicts, Error as CombineMapErr}, - KeySource, PartialSigs, -}; +use crate::pskt::{KeySource, PartialSigs}; +use crate::utils::{combine_if_no_conflicts, Error as CombineMapErr}; use derive_builder::Builder; use kaspa_consensus_core::{ hashing::sighash_type::{SigHashType, SIG_HASH_ALL}, @@ -12,6 +10,7 @@ use std::{collections::BTreeMap, marker::PhantomData, ops::Add}; // todo add unknown field? combine them by deduplicating, if there are different values - return error? #[derive(Builder, Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] #[builder(default)] #[builder(setter(skip))] pub struct Input { @@ -47,7 +46,7 @@ pub struct Input { /// scripts necessary for this input to pass validation. pub final_script_sig: Option>, #[serde(skip_serializing, default)] - hidden: PhantomData<()>, // prevents manual filling of fields + pub(crate) hidden: PhantomData<()>, // prevents manual filling of fields #[builder(setter)] /// Proprietary key-value pairs for this output. pub proprietaries: BTreeMap, diff --git a/wallet/pskt/src/lib.rs b/wallet/pskt/src/lib.rs index e26d5c9ea3..ed57e9b1ed 100644 --- a/wallet/pskt/src/lib.rs +++ b/wallet/pskt/src/lib.rs @@ -1,458 +1,32 @@ -use kaspa_bip32::{secp256k1, DerivationPath, KeyFingerprint}; -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}; - -mod error; -mod global; -mod input; - -mod output; - -mod role; +//! +//! PSKT is a crate for working with Partially Signed Kaspa Transactions (PSKTs). +//! This crate provides following primitives: `PSKT`, `PSKTBuilder` and `Bundle`. +//! The `Bundle` struct is used for PSKT exchange payload serialization and carries +//! multiple `PSKT` instances allowing for exchange of Kaspa sweep transactions. +//! + +pub mod bundle; +pub mod error; +pub mod global; +pub mod input; +pub mod output; +pub mod pskt; +pub mod role; +pub mod wasm; + +mod convert; mod utils; -pub use error::Error; -pub use global::{Global, GlobalBuilder}; -pub use input::{Input, InputBuilder}; -use kaspa_consensus_core::tx::UtxoEntry; -use kaspa_consensus_core::{ - hashing::{sighash::SigHashReusedValues, sighash_type::SigHashType}, - subnets::SUBNETWORK_ID_NATIVE, - tx::{MutableTransaction, SignableTransaction, Transaction, TransactionId, TransactionInput, TransactionOutput}, -}; -use kaspa_txscript::{caches::Cache, TxScriptEngine}; -pub use output::{Output, OutputBuilder}; -pub use role::{Combiner, Constructor, Creator, Extractor, Finalizer, Signer, Updater}; - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct Inner { - /// The global map. - pub global: Global, - /// The corresponding key-value map for each input in the unsigned transaction. - pub inputs: Vec, - /// The corresponding key-value map for each output in the unsigned transaction. - pub outputs: Vec, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum Version { - #[default] - Zero = 0, -} - -impl Display for Version { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Version::Zero => write!(f, "{}", Version::Zero as u8), - } - } -} - -/// Full information on the used extended public key: fingerprint of the -/// master extended public key and a derivation path from it. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct KeySource { - #[serde(with = "kaspa_utils::serde_bytes_fixed")] - pub key_fingerprint: KeyFingerprint, - pub derivation_path: DerivationPath, -} - -impl KeySource { - pub fn new(key_fingerprint: KeyFingerprint, derivation_path: DerivationPath) -> Self { - Self { key_fingerprint, derivation_path } - } -} - -pub type PartialSigs = BTreeMap; - -#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Copy, Clone)] -pub enum Signature { - ECDSA(secp256k1::ecdsa::Signature), - Schnorr(secp256k1::schnorr::Signature), -} - -impl Signature { - pub fn into_bytes(self) -> [u8; 64] { - match self { - Signature::ECDSA(s) => s.serialize_compact(), - Signature::Schnorr(s) => s.serialize(), - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PSKT { - #[serde(flatten)] - inner_pskt: Inner, - #[serde(skip_serializing, default)] - role: PhantomData, -} - -impl Clone for PSKT { - fn clone(&self) -> Self { - PSKT { inner_pskt: self.inner_pskt.clone(), role: Default::default() } - } -} - -impl Deref for PSKT { - type Target = Inner; - - fn deref(&self) -> &Self::Target { - &self.inner_pskt - } -} - -impl PSKT { - fn unsigned_tx(&self) -> SignableTransaction { - let tx = Transaction::new( - self.global.tx_version, - self.inputs - .iter() - .map(|Input { previous_outpoint, sequence, sig_op_count, .. }| TransactionInput { - previous_outpoint: *previous_outpoint, - signature_script: vec![], - sequence: sequence.unwrap_or(u64::MAX), - sig_op_count: sig_op_count.unwrap_or(0), - }) - .collect(), - self.outputs - .iter() - .map(|Output { amount, script_public_key, .. }: &Output| TransactionOutput { - value: *amount, - script_public_key: script_public_key.clone(), - }) - .collect(), - self.determine_lock_time(), - SUBNETWORK_ID_NATIVE, - 0, - vec![], - ); - let entries = self.inputs.iter().filter_map(|Input { utxo_entry, .. }| utxo_entry.clone()).collect(); - SignableTransaction::with_entries(tx, entries) - } - - fn calculate_id_internal(&self) -> TransactionId { - self.unsigned_tx().tx.id() - } - - fn determine_lock_time(&self) -> u64 { - self.inputs.iter().map(|input: &Input| input.min_time).max().unwrap_or(self.global.fallback_lock_time).unwrap_or(0) - } -} - -impl Default for PSKT { - fn default() -> Self { - PSKT { inner_pskt: Default::default(), role: Default::default() } - } -} - -impl PSKT { - /// Sets the fallback lock time. - pub fn fallback_lock_time(mut self, fallback: u64) -> Self { - self.inner_pskt.global.fallback_lock_time = Some(fallback); - self - } - - // todo generic const - /// Sets the inputs modifiable bit in the transaction modifiable flags. - pub fn inputs_modifiable(mut self) -> Self { - self.inner_pskt.global.inputs_modifiable = true; - self - } - // todo generic const - /// Sets the outputs modifiable bit in the transaction modifiable flags. - pub fn outputs_modifiable(mut self) -> Self { - self.inner_pskt.global.outputs_modifiable = true; - self - } - - pub fn constructor(self) -> PSKT { - PSKT { inner_pskt: self.inner_pskt, role: Default::default() } - } -} - -impl PSKT { - // todo generic const - /// Marks that the `PSKT` can not have any more inputs added to it. - pub fn no_more_inputs(mut self) -> Self { - self.inner_pskt.global.inputs_modifiable = false; - self - } - // todo generic const - /// Marks that the `PSKT` can not have any more outputs added to it. - pub fn no_more_outputs(mut self) -> Self { - self.inner_pskt.global.outputs_modifiable = false; - self - } - - /// Adds an input to the PSKT. - pub fn input(mut self, input: Input) -> Self { - self.inner_pskt.inputs.push(input); - self.inner_pskt.global.input_count += 1; - self - } - - /// Adds an output to the PSKT. - pub fn output(mut self, output: Output) -> Self { - self.inner_pskt.outputs.push(output); - self.inner_pskt.global.output_count += 1; - self - } - - /// Returns a PSKT [`Updater`] once construction is completed. - pub fn updater(self) -> PSKT { - let pskt = self.no_more_inputs().no_more_outputs(); - PSKT { inner_pskt: pskt.inner_pskt, role: Default::default() } - } - - pub fn signer(self) -> PSKT { - self.updater().signer() - } - - pub fn combiner(self) -> PSKT { - PSKT { inner_pskt: self.inner_pskt, role: Default::default() } - } -} - -impl PSKT { - pub fn set_sequence(mut self, n: u64, input_index: usize) -> Result { - self.inner_pskt.inputs.get_mut(input_index).ok_or(Error::OutOfBounds)?.sequence = Some(n); - Ok(self) - } - - pub fn signer(self) -> PSKT { - PSKT { inner_pskt: self.inner_pskt, role: Default::default() } - } - - pub fn combiner(self) -> PSKT { - PSKT { inner_pskt: self.inner_pskt, role: Default::default() } - } -} - -impl PSKT { - // todo use iterator instead of vector - pub fn pass_signature_sync(mut self, sign_fn: SignFn) -> Result - where - E: Display, - SignFn: FnOnce(SignableTransaction, Vec) -> Result, E>, - { - let unsigned_tx = self.unsigned_tx(); - let sighashes = self.inputs.iter().map(|input| input.sighash_type).collect(); - self.inner_pskt.inputs.iter_mut().zip(sign_fn(unsigned_tx, sighashes)?).for_each( - |(input, SignInputOk { signature, pub_key, key_source })| { - input.bip32_derivations.insert(pub_key, key_source); - input.partial_sigs.insert(pub_key, signature); - }, - ); - - Ok(self) - } - // todo use iterator instead of vector - pub async fn pass_signature(mut self, sign_fn: SignFn) -> Result - where - E: Display, - Fut: Future, E>>, - SignFn: FnOnce(SignableTransaction, Vec) -> Fut, - { - let unsigned_tx = self.unsigned_tx(); - let sighashes = self.inputs.iter().map(|input| input.sighash_type).collect(); - self.inner_pskt.inputs.iter_mut().zip(sign_fn(unsigned_tx, sighashes).await?).for_each( - |(input, SignInputOk { signature, pub_key, key_source })| { - input.bip32_derivations.insert(pub_key, key_source); - input.partial_sigs.insert(pub_key, signature); - }, - ); - Ok(self) - } - - pub fn calculate_id(&self) -> TransactionId { - self.calculate_id_internal() - } - - pub fn finalizer(self) -> PSKT { - PSKT { inner_pskt: self.inner_pskt, role: Default::default() } - } - - pub fn combiner(self) -> PSKT { - PSKT { inner_pskt: self.inner_pskt, role: Default::default() } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SignInputOk { - pub signature: Signature, - pub pub_key: secp256k1::PublicKey, - pub key_source: Option, -} - -impl std::ops::Add> for PSKT { - type Output = Result; - - fn add(mut self, mut rhs: PSKT) -> Self::Output { - self.inner_pskt.global = (self.inner_pskt.global + rhs.inner_pskt.global)?; - macro_rules! combine { - ($left:expr, $right:expr, $err: ty) => { - if $left.len() > $right.len() { - $left.iter_mut().zip($right.iter_mut()).try_for_each(|(left, right)| -> Result<(), $err> { - *left = (std::mem::take(left) + std::mem::take(right))?; - Ok(()) - })?; - $left - } else { - $right.iter_mut().zip($left.iter_mut()).try_for_each(|(left, right)| -> Result<(), $err> { - *left = (std::mem::take(left) + std::mem::take(right))?; - Ok(()) - })?; - $right - } - }; - } - // todo add sort to build deterministic combination - self.inner_pskt.inputs = combine!(self.inner_pskt.inputs, rhs.inner_pskt.inputs, input::CombineError); - self.inner_pskt.outputs = combine!(self.inner_pskt.outputs, rhs.inner_pskt.outputs, output::CombineError); - Ok(self) - } -} - -impl PSKT { - pub fn signer(self) -> PSKT { - PSKT { inner_pskt: self.inner_pskt, role: Default::default() } - } - pub fn finalizer(self) -> PSKT { - PSKT { inner_pskt: self.inner_pskt, role: Default::default() } - } -} - -impl PSKT { - pub fn finalize_sync( - self, - final_sig_fn: impl FnOnce(&Inner) -> Result>, E>, - ) -> Result> { - let sigs = final_sig_fn(&self); - self.finalize_internal(sigs) - } - - pub async fn finalize(self, final_sig_fn: F) -> Result> - where - E: Display, - F: FnOnce(&Inner) -> Fut, - Fut: Future>, E>>, - { - let sigs = final_sig_fn(&self).await; - self.finalize_internal(sigs) - } - - pub fn id(&self) -> Option { - self.global.id - } - - pub fn extractor(self) -> Result, TxNotFinalized> { - if self.global.id.is_none() { - Err(TxNotFinalized {}) - } else { - Ok(PSKT { inner_pskt: self.inner_pskt, role: Default::default() }) - } - } - - fn finalize_internal(mut self, sigs: Result>, E>) -> Result> { - let sigs = sigs?; - if sigs.len() != self.inputs.len() { - return Err(FinalizeError::WrongFinalizedSigsCount { expected: self.inputs.len(), actual: sigs.len() }); - } - self.inner_pskt.inputs.iter_mut().enumerate().zip(sigs).try_for_each(|((idx, input), sig)| { - if sig.is_empty() { - return Err(FinalizeError::EmptySignature(idx)); - } - input.sequence = Some(input.sequence.unwrap_or(u64::MAX)); // todo discussable - input.final_script_sig = Some(sig); - Ok(()) - })?; - self.inner_pskt.global.id = Some(self.calculate_id_internal()); - Ok(self) - } -} - -impl PSKT { - pub fn extract_tx_unchecked(self) -> Result (Transaction, Vec>), TxNotFinalized> { - let tx = self.unsigned_tx(); - let entries = tx.entries; - let mut tx = tx.tx; - tx.inputs.iter_mut().zip(self.inner_pskt.inputs).try_for_each(|(dest, src)| { - dest.signature_script = src.final_script_sig.ok_or(TxNotFinalized {})?; - Ok(()) - })?; - Ok(move |mass| { - tx.set_mass(mass); - (tx, entries) - }) - } - - 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()); - use kaspa_consensus_core::tx::VerifiableTransaction; - { - let tx = tx.as_verifiable(); - let cache = Cache::new(10_000); - let mut reused_values = SigHashReusedValues::new(); - - tx.populated_inputs().enumerate().try_for_each(|(idx, (input, entry))| { - TxScriptEngine::from_transaction_input(&tx, input, idx, entry, &mut reused_values, &cache)?.execute()?; - >::Ok(()) - })?; - } - let entries = tx.entries; - let tx = tx.tx; - let closure = move |mass| { - tx.set_mass(mass); - (tx, entries) - }; - Ok(closure) - } -} - -/// Error combining pskt. -#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] -pub enum CombineError { - #[error(transparent)] - Global(#[from] global::CombineError), - #[error(transparent)] - Inputs(#[from] input::CombineError), - #[error(transparent)] - Outputs(#[from] output::CombineError), -} - -#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] -pub enum FinalizeError { - #[error("Signatures count mismatch")] - WrongFinalizedSigsCount { expected: usize, actual: usize }, - #[error("Signatures at index: {0} is empty")] - EmptySignature(usize), - #[error(transparent)] - FinalaziCb(#[from] E), -} - -#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] -pub enum ExtractError { - #[error(transparent)] - TxScriptError(#[from] kaspa_txscript_errors::TxScriptError), - #[error(transparent)] - TxNotFinalized(#[from] TxNotFinalized), -} - -#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] -#[error("Transaction is not finalized")] -pub struct TxNotFinalized {} - -#[cfg(test)] -mod tests { - - // #[test] - // fn it_works() { - // let result = add(2, 2); - // assert_eq!(result, 4); - // } +pub mod prelude { + pub use crate::bundle::Bundle; + pub use crate::bundle::*; + pub use crate::global::Global; + pub use crate::input::Input; + pub use crate::output::Output; + pub use crate::pskt::*; + + // not quite sure why it warns of unused imports, + // perhaps due to the fact that enums have no variants? + #[allow(unused_imports)] + pub use crate::role::*; } diff --git a/wallet/pskt/src/output.rs b/wallet/pskt/src/output.rs index 952b63d3fb..e873ce4a66 100644 --- a/wallet/pskt/src/output.rs +++ b/wallet/pskt/src/output.rs @@ -1,11 +1,12 @@ +use crate::pskt::KeySource; use crate::utils::combine_if_no_conflicts; -use crate::KeySource; use derive_builder::Builder; use kaspa_consensus_core::tx::ScriptPublicKey; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, ops::Add}; #[derive(Builder, Default, Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] #[builder(default)] pub struct Output { /// The output's amount (serialized as sompi). diff --git a/wallet/pskt/src/pskt.rs b/wallet/pskt/src/pskt.rs new file mode 100644 index 0000000000..245609803d --- /dev/null +++ b/wallet/pskt/src/pskt.rs @@ -0,0 +1,472 @@ +use kaspa_bip32::{secp256k1, DerivationPath, KeyFingerprint}; +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}; + +pub use crate::error::Error; +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::{ + hashing::{sighash::SigHashReusedValues, sighash_type::SigHashType}, + subnets::SUBNETWORK_ID_NATIVE, + tx::{MutableTransaction, SignableTransaction, Transaction, TransactionId, TransactionInput, TransactionOutput}, +}; +use kaspa_txscript::{caches::Cache, TxScriptEngine}; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Inner { + /// The global map. + pub global: Global, + /// The corresponding key-value map for each input in the unsigned transaction. + pub inputs: Vec, + /// The corresponding key-value map for each output in the unsigned transaction. + pub outputs: Vec, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum Version { + #[default] + Zero = 0, +} + +impl Display for Version { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Version::Zero => write!(f, "{}", Version::Zero as u8), + } + } +} + +/// Full information on the used extended public key: fingerprint of the +/// master extended public key and a derivation path from it. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct KeySource { + #[serde(with = "kaspa_utils::serde_bytes_fixed")] + pub key_fingerprint: KeyFingerprint, + pub derivation_path: DerivationPath, +} + +impl KeySource { + pub fn new(key_fingerprint: KeyFingerprint, derivation_path: DerivationPath) -> Self { + Self { key_fingerprint, derivation_path } + } +} + +pub type PartialSigs = BTreeMap; + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Copy, Clone)] +#[serde(rename_all = "camelCase")] +pub enum Signature { + ECDSA(secp256k1::ecdsa::Signature), + Schnorr(secp256k1::schnorr::Signature), +} + +impl Signature { + pub fn into_bytes(self) -> [u8; 64] { + match self { + Signature::ECDSA(s) => s.serialize_compact(), + Signature::Schnorr(s) => s.serialize(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PSKT { + #[serde(flatten)] + inner_pskt: Inner, + #[serde(skip_serializing, default)] + role: PhantomData, +} + +impl From for PSKT { + fn from(inner_pskt: Inner) -> Self { + PSKT { inner_pskt, role: Default::default() } + } +} + +impl Clone for PSKT { + fn clone(&self) -> Self { + PSKT { inner_pskt: self.inner_pskt.clone(), role: Default::default() } + } +} + +impl Deref for PSKT { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + &self.inner_pskt + } +} + +impl PSKT { + fn unsigned_tx(&self) -> SignableTransaction { + let tx = Transaction::new( + self.global.tx_version, + self.inputs + .iter() + .map(|Input { previous_outpoint, sequence, sig_op_count, .. }| TransactionInput { + previous_outpoint: *previous_outpoint, + signature_script: vec![], + sequence: sequence.unwrap_or(u64::MAX), + sig_op_count: sig_op_count.unwrap_or(0), + }) + .collect(), + self.outputs + .iter() + .map(|Output { amount, script_public_key, .. }: &Output| TransactionOutput { + value: *amount, + script_public_key: script_public_key.clone(), + }) + .collect(), + self.determine_lock_time(), + SUBNETWORK_ID_NATIVE, + 0, + vec![], + ); + let entries = self.inputs.iter().filter_map(|Input { utxo_entry, .. }| utxo_entry.clone()).collect(); + SignableTransaction::with_entries(tx, entries) + } + + fn calculate_id_internal(&self) -> TransactionId { + self.unsigned_tx().tx.id() + } + + fn determine_lock_time(&self) -> u64 { + self.inputs.iter().map(|input: &Input| input.min_time).max().unwrap_or(self.global.fallback_lock_time).unwrap_or(0) + } + + pub fn to_hex(&self) -> Result { + Ok(format!("PSKT{}", hex::encode(serde_json::to_string(self)?))) + } + + pub fn from_hex(hex_data: &str) -> Result { + if let Some(hex_data) = hex_data.strip_prefix("PSKT") { + Ok(serde_json::from_slice(hex::decode(hex_data)?.as_slice())?) + } else { + Err(Error::PsktPrefixError) + } + } +} + +impl Default for PSKT { + fn default() -> Self { + PSKT { inner_pskt: Default::default(), role: Default::default() } + } +} + +impl PSKT { + /// Sets the fallback lock time. + pub fn fallback_lock_time(mut self, fallback: u64) -> Self { + self.inner_pskt.global.fallback_lock_time = Some(fallback); + self + } + + // todo generic const + /// Sets the inputs modifiable bit in the transaction modifiable flags. + pub fn inputs_modifiable(mut self) -> Self { + self.inner_pskt.global.inputs_modifiable = true; + self + } + // todo generic const + /// Sets the outputs modifiable bit in the transaction modifiable flags. + pub fn outputs_modifiable(mut self) -> Self { + self.inner_pskt.global.outputs_modifiable = true; + self + } + + pub fn constructor(self) -> PSKT { + PSKT { inner_pskt: self.inner_pskt, role: Default::default() } + } +} + +impl PSKT { + // todo generic const + /// Marks that the `PSKT` can not have any more inputs added to it. + pub fn no_more_inputs(mut self) -> Self { + self.inner_pskt.global.inputs_modifiable = false; + self + } + // todo generic const + /// Marks that the `PSKT` can not have any more outputs added to it. + pub fn no_more_outputs(mut self) -> Self { + self.inner_pskt.global.outputs_modifiable = false; + self + } + + /// Adds an input to the PSKT. + pub fn input(mut self, input: Input) -> Self { + self.inner_pskt.inputs.push(input); + self.inner_pskt.global.input_count += 1; + self + } + + /// Adds an output to the PSKT. + pub fn output(mut self, output: Output) -> Self { + self.inner_pskt.outputs.push(output); + self.inner_pskt.global.output_count += 1; + self + } + + /// Returns a PSKT [`Updater`] once construction is completed. + pub fn updater(self) -> PSKT { + let pskt = self.no_more_inputs().no_more_outputs(); + PSKT { inner_pskt: pskt.inner_pskt, role: Default::default() } + } + + pub fn signer(self) -> PSKT { + self.updater().signer() + } + + pub fn combiner(self) -> PSKT { + PSKT { inner_pskt: self.inner_pskt, role: Default::default() } + } +} + +impl PSKT { + pub fn set_sequence(mut self, n: u64, input_index: usize) -> Result { + self.inner_pskt.inputs.get_mut(input_index).ok_or(Error::OutOfBounds)?.sequence = Some(n); + Ok(self) + } + + pub fn signer(self) -> PSKT { + PSKT { inner_pskt: self.inner_pskt, role: Default::default() } + } + + pub fn combiner(self) -> PSKT { + PSKT { inner_pskt: self.inner_pskt, role: Default::default() } + } +} + +impl PSKT { + // todo use iterator instead of vector + pub fn pass_signature_sync(mut self, sign_fn: SignFn) -> Result + where + E: Display, + SignFn: FnOnce(SignableTransaction, Vec) -> Result, E>, + { + let unsigned_tx = self.unsigned_tx(); + let sighashes = self.inputs.iter().map(|input| input.sighash_type).collect(); + self.inner_pskt.inputs.iter_mut().zip(sign_fn(unsigned_tx, sighashes)?).for_each( + |(input, SignInputOk { signature, pub_key, key_source })| { + input.bip32_derivations.insert(pub_key, key_source); + input.partial_sigs.insert(pub_key, signature); + }, + ); + + Ok(self) + } + // todo use iterator instead of vector + pub async fn pass_signature(mut self, sign_fn: SignFn) -> Result + where + E: Display, + Fut: Future, E>>, + SignFn: FnOnce(SignableTransaction, Vec) -> Fut, + { + let unsigned_tx = self.unsigned_tx(); + let sighashes = self.inputs.iter().map(|input| input.sighash_type).collect(); + self.inner_pskt.inputs.iter_mut().zip(sign_fn(unsigned_tx, sighashes).await?).for_each( + |(input, SignInputOk { signature, pub_key, key_source })| { + input.bip32_derivations.insert(pub_key, key_source); + input.partial_sigs.insert(pub_key, signature); + }, + ); + Ok(self) + } + + pub fn calculate_id(&self) -> TransactionId { + self.calculate_id_internal() + } + + pub fn finalizer(self) -> PSKT { + PSKT { inner_pskt: self.inner_pskt, role: Default::default() } + } + + pub fn combiner(self) -> PSKT { + PSKT { inner_pskt: self.inner_pskt, role: Default::default() } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignInputOk { + pub signature: Signature, + pub pub_key: secp256k1::PublicKey, + pub key_source: Option, +} + +impl std::ops::Add> for PSKT { + type Output = Result; + + fn add(mut self, mut rhs: PSKT) -> Self::Output { + self.inner_pskt.global = (self.inner_pskt.global + rhs.inner_pskt.global)?; + macro_rules! combine { + ($left:expr, $right:expr, $err: ty) => { + if $left.len() > $right.len() { + $left.iter_mut().zip($right.iter_mut()).try_for_each(|(left, right)| -> Result<(), $err> { + *left = (std::mem::take(left) + std::mem::take(right))?; + Ok(()) + })?; + $left + } else { + $right.iter_mut().zip($left.iter_mut()).try_for_each(|(left, right)| -> Result<(), $err> { + *left = (std::mem::take(left) + std::mem::take(right))?; + Ok(()) + })?; + $right + } + }; + } + // todo add sort to build deterministic combination + self.inner_pskt.inputs = combine!(self.inner_pskt.inputs, rhs.inner_pskt.inputs, crate::input::CombineError); + self.inner_pskt.outputs = combine!(self.inner_pskt.outputs, rhs.inner_pskt.outputs, crate::output::CombineError); + Ok(self) + } +} + +impl PSKT { + pub fn signer(self) -> PSKT { + PSKT { inner_pskt: self.inner_pskt, role: Default::default() } + } + pub fn finalizer(self) -> PSKT { + PSKT { inner_pskt: self.inner_pskt, role: Default::default() } + } +} + +impl PSKT { + pub fn finalize_sync( + self, + final_sig_fn: impl FnOnce(&Inner) -> Result>, E>, + ) -> Result> { + let sigs = final_sig_fn(&self); + self.finalize_internal(sigs) + } + + pub async fn finalize(self, final_sig_fn: F) -> Result> + where + E: Display, + F: FnOnce(&Inner) -> Fut, + Fut: Future>, E>>, + { + let sigs = final_sig_fn(&self).await; + self.finalize_internal(sigs) + } + + pub fn id(&self) -> Option { + self.global.id + } + + pub fn extractor(self) -> Result, TxNotFinalized> { + if self.global.id.is_none() { + Err(TxNotFinalized {}) + } else { + Ok(PSKT { inner_pskt: self.inner_pskt, role: Default::default() }) + } + } + + fn finalize_internal(mut self, sigs: Result>, E>) -> Result> { + let sigs = sigs?; + if sigs.len() != self.inputs.len() { + return Err(FinalizeError::WrongFinalizedSigsCount { expected: self.inputs.len(), actual: sigs.len() }); + } + self.inner_pskt.inputs.iter_mut().enumerate().zip(sigs).try_for_each(|((idx, input), sig)| { + if sig.is_empty() { + return Err(FinalizeError::EmptySignature(idx)); + } + input.sequence = Some(input.sequence.unwrap_or(u64::MAX)); // todo discussable + input.final_script_sig = Some(sig); + Ok(()) + })?; + self.inner_pskt.global.id = Some(self.calculate_id_internal()); + Ok(self) + } +} + +impl PSKT { + pub fn extract_tx_unchecked(self) -> Result (Transaction, Vec>), TxNotFinalized> { + let tx = self.unsigned_tx(); + let entries = tx.entries; + let mut tx = tx.tx; + tx.inputs.iter_mut().zip(self.inner_pskt.inputs).try_for_each(|(dest, src)| { + dest.signature_script = src.final_script_sig.ok_or(TxNotFinalized {})?; + Ok(()) + })?; + Ok(move |mass| { + tx.set_mass(mass); + (tx, entries) + }) + } + + 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()); + use kaspa_consensus_core::tx::VerifiableTransaction; + { + let tx = tx.as_verifiable(); + let cache = Cache::new(10_000); + let mut reused_values = SigHashReusedValues::new(); + + tx.populated_inputs().enumerate().try_for_each(|(idx, (input, entry))| { + TxScriptEngine::from_transaction_input(&tx, input, idx, entry, &mut reused_values, &cache)?.execute()?; + >::Ok(()) + })?; + } + let entries = tx.entries; + let tx = tx.tx; + let closure = move |mass| { + tx.set_mass(mass); + (tx, entries) + }; + Ok(closure) + } +} + +/// Error combining pskt. +#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] +pub enum CombineError { + #[error(transparent)] + Global(#[from] crate::global::CombineError), + #[error(transparent)] + Inputs(#[from] crate::input::CombineError), + #[error(transparent)] + Outputs(#[from] crate::output::CombineError), +} + +#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] +pub enum FinalizeError { + #[error("Signatures count mismatch")] + WrongFinalizedSigsCount { expected: usize, actual: usize }, + #[error("Signatures at index: {0} is empty")] + EmptySignature(usize), + #[error(transparent)] + FinalaziCb(#[from] E), +} + +#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] +pub enum ExtractError { + #[error(transparent)] + TxScriptError(#[from] kaspa_txscript_errors::TxScriptError), + #[error(transparent)] + TxNotFinalized(#[from] TxNotFinalized), +} + +#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] +#[error("Transaction is not finalized")] +pub struct TxNotFinalized {} + +#[cfg(test)] +mod tests { + + // #[test] + // fn it_works() { + // let result = add(2, 2); + // assert_eq!(result, 4); + // } +} diff --git a/wallet/pskt/src/wasm/bundle.rs b/wallet/pskt/src/wasm/bundle.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/wallet/pskt/src/wasm/bundle.rs @@ -0,0 +1 @@ + diff --git a/wallet/pskt/src/wasm/error.rs b/wallet/pskt/src/wasm/error.rs new file mode 100644 index 0000000000..77fb0d8b16 --- /dev/null +++ b/wallet/pskt/src/wasm/error.rs @@ -0,0 +1,64 @@ +use super::pskt::State; +use thiserror::Error; +use wasm_bindgen::prelude::*; + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Custom(String), + + #[error("Unexpected state: {0}")] + State(String), + + #[error("Constructor argument must be a valid payload, another PSKT instance, Transaction or undefined")] + Ctor(String), + + #[error("Invalid payload")] + InvalidPayload, + + #[error("Transaction not finalized")] + TxNotFinalized(#[from] crate::pskt::TxNotFinalized), + + #[error(transparent)] + Wasm(#[from] workflow_wasm::error::Error), + + #[error("Create state is not allowed for PSKT initialized from transaction or a payload")] + CreateNotAllowed, + + #[error("PSKT must be initialized with a payload or CREATE role")] + NotInitialized, + + #[error(transparent)] + ConsensusClient(#[from] kaspa_consensus_client::error::Error), + + #[error(transparent)] + Pskt(#[from] crate::error::Error), +} + +impl Error { + pub fn custom(msg: T) -> Self { + Error::Custom(msg.to_string()) + } + + pub fn state(state: impl AsRef) -> Self { + Error::State(state.as_ref().display().to_string()) + } +} + +impl From<&str> for Error { + fn from(msg: &str) -> Self { + Error::Custom(msg.to_string()) + } +} + +impl From for Error { + fn from(msg: String) -> Self { + Error::Custom(msg) + } +} + +impl From for JsValue { + fn from(err: Error) -> Self { + JsValue::from_str(&err.to_string()) + } +} diff --git a/wallet/pskt/src/wasm/input.rs b/wallet/pskt/src/wasm/input.rs new file mode 100644 index 0000000000..b6a827daf1 --- /dev/null +++ b/wallet/pskt/src/wasm/input.rs @@ -0,0 +1 @@ +// TODO - InputBuilder & Input diff --git a/wallet/pskt/src/wasm/mod.rs b/wallet/pskt/src/wasm/mod.rs new file mode 100644 index 0000000000..f5e9bea3fb --- /dev/null +++ b/wallet/pskt/src/wasm/mod.rs @@ -0,0 +1,6 @@ +pub mod bundle; +pub mod error; +pub mod input; +pub mod output; +pub mod pskt; +pub mod result; diff --git a/wallet/pskt/src/wasm/output.rs b/wallet/pskt/src/wasm/output.rs new file mode 100644 index 0000000000..eb91824d1a --- /dev/null +++ b/wallet/pskt/src/wasm/output.rs @@ -0,0 +1 @@ +// TODO - OutputBuilder & Output diff --git a/wallet/pskt/src/wasm/pskt.rs b/wallet/pskt/src/wasm/pskt.rs new file mode 100644 index 0000000000..8ee370a4b9 --- /dev/null +++ b/wallet/pskt/src/wasm/pskt.rs @@ -0,0 +1,320 @@ +use crate::pskt::PSKT as Native; +use crate::role::*; +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::sync::MutexGuard; +use std::sync::{Arc, Mutex}; +use workflow_wasm::{ + convert::{Cast, CastFromJs, TryCastFromJs}, + // extensions::object::*, + // error::Error as CastError, +}; + +use super::error::*; +use super::result::*; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(tag = "state", content = "payload")] +pub enum State { + NoOp(Option), + Creator(Native), + Constructor(Native), + Updater(Native), + Signer(Native), + Combiner(Native), + Finalizer(Native), + Extractor(Native), +} + +impl AsRef for State { + fn as_ref(&self) -> &State { + self + } +} + +impl State { + // this is not a Display trait intentionally + pub fn display(&self) -> &'static str { + match self { + State::NoOp(_) => "Init", + State::Creator(_) => "Creator", + State::Constructor(_) => "Constructor", + State::Updater(_) => "Updater", + State::Signer(_) => "Signer", + State::Combiner(_) => "Combiner", + State::Finalizer(_) => "Finalizer", + State::Extractor(_) => "Extractor", + } + } +} + +impl From for PSKT { + fn from(state: State) -> Self { + PSKT { state: Arc::new(Mutex::new(Some(state))) } + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "PSKT | Transaction | string | undefined")] + pub type CtorT; +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Payload { + data: String, +} + +impl TryFrom for Native { + type Error = Error; + + fn try_from(value: Payload) -> Result { + let Payload { data } = value; + if data.starts_with("PSKT") { + unimplemented!("PSKT binary serialization") + } else { + Ok(serde_json::from_str(&data).map_err(|err| format!("Invalid JSON: {err}"))?) + } + } +} + +#[wasm_bindgen(inspectable)] +#[derive(Clone, CastFromJs)] +pub struct PSKT { + state: Arc>>, +} + +impl TryCastFromJs for PSKT { + type Error = Error; + fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> + where + R: AsRef + 'a, + { + Self::resolve(value, || { + 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) { + let pskt_inner: Inner = transaction.try_into()?; + Ok(PSKT::from(State::NoOp(Some(pskt_inner)))) + } else { + Err(Error::InvalidPayload) + } + }) + } +} + +#[wasm_bindgen] +impl PSKT { + #[wasm_bindgen(constructor)] + pub fn new(payload: CtorT) -> Result { + PSKT::try_owned_from(payload.unchecked_into::().as_ref()).map_err(|err| Error::Ctor(err.to_string())) + } + + #[wasm_bindgen(getter, js_name = "role")] + pub fn role_getter(&self) -> String { + self.state().as_ref().unwrap().display().to_string() + } + + #[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() + } + + fn state(&self) -> MutexGuard> { + self.state.lock().unwrap() + } + + fn take(&self) -> State { + self.state.lock().unwrap().take().unwrap() + } + + fn replace(&self, state: State) -> Result { + self.state.lock().unwrap().replace(state); + Ok(self.clone()) + } + + /// Change role to `CREATOR` + /// #[wasm_bindgen(js_name = toCreator)] + pub fn creator(&self) -> Result { + let state = match self.take() { + State::NoOp(inner) => match inner { + None => State::Creator(Native::default()), + Some(_) => Err(Error::CreateNotAllowed)?, + }, + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `CONSTRUCTOR` + #[wasm_bindgen(js_name = toConstructor)] + pub fn constructor(&self) -> Result { + let state = match self.take() { + State::NoOp(inner) => State::Constructor(inner.ok_or(Error::NotInitialized)?.into()), + State::Creator(pskt) => State::Constructor(pskt.constructor()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `UPDATER` + #[wasm_bindgen(js_name = toUpdater)] + pub fn updater(&self) -> Result { + let state = match self.take() { + State::NoOp(inner) => State::Updater(inner.ok_or(Error::NotInitialized)?.into()), + State::Constructor(constructor) => State::Updater(constructor.updater()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `SIGNER` + #[wasm_bindgen(js_name = toSigner)] + pub fn signer(&self) -> Result { + let state = match self.take() { + State::NoOp(inner) => State::Signer(inner.ok_or(Error::NotInitialized)?.into()), + State::Constructor(pskt) => State::Signer(pskt.signer()), + State::Updater(pskt) => State::Signer(pskt.signer()), + State::Combiner(pskt) => State::Signer(pskt.signer()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `COMBINER` + #[wasm_bindgen(js_name = toCombiner)] + pub fn combiner(&self) -> Result { + let state = match self.take() { + State::NoOp(inner) => State::Combiner(inner.ok_or(Error::NotInitialized)?.into()), + State::Constructor(pskt) => State::Combiner(pskt.combiner()), + State::Updater(pskt) => State::Combiner(pskt.combiner()), + State::Signer(pskt) => State::Combiner(pskt.combiner()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `FINALIZER` + #[wasm_bindgen(js_name = toFinalizer)] + pub fn finalizer(&self) -> Result { + let state = match self.take() { + State::NoOp(inner) => State::Finalizer(inner.ok_or(Error::NotInitialized)?.into()), + State::Combiner(pskt) => State::Finalizer(pskt.finalizer()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + /// Change role to `EXTRACTOR` + #[wasm_bindgen(js_name = toExtractor)] + pub fn extractor(&self) -> Result { + let state = match self.take() { + State::NoOp(inner) => State::Extractor(inner.ok_or(Error::NotInitialized)?.into()), + State::Finalizer(pskt) => State::Extractor(pskt.extractor()?), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + #[wasm_bindgen(js_name = fallbackLockTime)] + 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))?, + }; + + self.replace(state) + } + + #[wasm_bindgen(js_name = inputsModifiable)] + pub fn inputs_modifiable(&self) -> Result { + let state = match self.take() { + State::Creator(pskt) => State::Creator(pskt.inputs_modifiable()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + #[wasm_bindgen(js_name = outputsModifiable)] + pub fn outputs_modifiable(&self) -> Result { + let state = match self.take() { + State::Creator(pskt) => State::Creator(pskt.outputs_modifiable()), + state => Err(Error::state(state))?, + }; + + self.replace(state) + } + + #[wasm_bindgen(js_name = noMoreInputs)] + 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))?, + }; + + self.replace(state) + } + + #[wasm_bindgen(js_name = noMoreOutputs)] + 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))?, + }; + + self.replace(state) + } + + pub fn input(&self, input: &TransactionInputT) -> Result { + 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))?, + }; + + self.replace(state) + } + + pub fn output(&self, output: &TransactionOutputT) -> Result { + 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))?, + }; + + self.replace(state) + } + + #[wasm_bindgen(js_name = setSequence)] + 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))?, + }; + + self.replace(state) + } + + #[wasm_bindgen(js_name = calculateId)] + pub fn calculate_id(&self) -> Result { + let state = self.state(); + match state.as_ref().unwrap() { + State::Signer(pskt) => Ok(pskt.calculate_id()), + state => Err(Error::state(state))?, + } + } +} diff --git a/wallet/pskt/src/wasm/result.rs b/wallet/pskt/src/wasm/result.rs new file mode 100644 index 0000000000..32f663388a --- /dev/null +++ b/wallet/pskt/src/wasm/result.rs @@ -0,0 +1 @@ +pub type Result = std::result::Result; diff --git a/wasm/CHANGELOG.md b/wasm/CHANGELOG.md index 5882d9cbf5..d572dfd1d1 100644 --- a/wasm/CHANGELOG.md +++ b/wasm/CHANGELOG.md @@ -1,4 +1,33 @@ Latest online documentation available at: https://kaspa.aspectron.org/docs/ + +### Latest Release + +- Replace `MassCalculator` with `calculateTransactionMass` and `calculateTransactionFee` functions. +- Change `createTransaction` function signature (remove requirement for change address). +- Make `ITransactionInput.signatureScript` optional (if not supplied, the signatureScript is assigned an empty vector). + +### Release 2024-07-17 + +- Fix issues with deserializing manually-created objects matching `IUtxoEntry` interface. +- Allow arguments expecting ScriptPublicKey to receive `{ version, script }` object or a hex string. +- Fix `Transaction::serializeToObject()` return type (now returning `ISerializeTransaction` interface). +- Adding `setUserTransactionMaturityDAA()` and `setCoinbaseTransactionMaturityDAA()` that allow customizing +the maturity DAA periods for user and coinbase transactions. + +### Release 2024-06-12 + +- Fix `PublicKeyGenerator::change_address_as_string()` that was returning the receive address. +- WASM SDK now builds as a GitHub artifact during the CI process. +- `State` renamed to `PoW` +- Docs now have a PoW section that unifies all PoW-related classes and functions. +- `TransactionRecord.data` (`TransactionData`) now has correct TypeScript bindings. + +### Release 2024-05-26 + +- Adding utility functions: `payToAddressScript()`, `payToScriptHashScript()`, `payToScriptHashSignatureScript()`, `addressFromScriptPublicKey()`, `isScriptPayToPubkey()`, `isScriptPayToPubkeyECDSA()`, `isScriptPayToScriptHash()`. +- Adding `UtxoProcessor::isActive` property to check if the processor is in active state (connected and running). This property can be used to validate the processor state before invoking it's functions (that can throw is the UtxoProcessor is offline). +- Rename `UtxoContext::active` to `UtxoContext::isActive` for consistency. + ### Release 2024-04-27 - IAccountsCreateRequest interface simplified by flattering it and now it is union for future expansion for multisig etc. - IWalletEvent interface updated for Events with TransactionRecord diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 78a747e19f..77b14c39dd 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -18,11 +18,13 @@ crate-type = ["cdylib"] cfg-if.workspace = true js-sys.workspace = true kaspa-addresses.workspace = true +kaspa-bip32.workspace = true kaspa-consensus-core.workspace = true kaspa-consensus-wasm.workspace = true kaspa-core.workspace = true kaspa-math.workspace = true kaspa-pow.workspace = true +kaspa-txscript.workspace = true kaspa-rpc-core.workspace = true kaspa-utils.workspace = true kaspa-wasm-core.workspace = true @@ -40,10 +42,12 @@ workflow-wasm.workspace = true wasm32-sdk = [ "kaspa-wallet-core/wasm32-sdk", "kaspa-pow/wasm32-sdk", + "kaspa-txscript/wasm32-sdk", ] wasm32-core = [ "kaspa-wallet-core/wasm32-core", "kaspa-pow/wasm32-sdk", + "kaspa-txscript/wasm32-sdk", ] wasm32-rpc = [ "kaspa-consensus-core/wasm32-sdk", diff --git a/wasm/build-node-dev b/wasm/build-node-dev index b8de2b6acf..6dc5446fd1 100755 --- a/wasm/build-node-dev +++ b/wasm/build-node-dev @@ -5,4 +5,5 @@ RED='\033[0;31m' NC='\033[0m' # No Color echo -e "${RED}WARNING: do not use resulting WASM binaries in production!${NC}" -wasm-pack build --weak-refs --dev --target nodejs --out-name kaspa --out-dir nodejs/kaspa --features wasm32-sdk $@ +# wasm-pack build --weak-refs --dev --target nodejs --out-name kaspa --out-dir nodejs/kaspa-dev --features wasm32-sdk $@ +wasm-pack build --weak-refs --dev --target nodejs --out-name kaspa --out-dir nodejs/kaspa-dev --features wasm32-sdk $@ diff --git a/wasm/core/Cargo.toml b/wasm/core/Cargo.toml index a8a49e0aa3..4c765e9bd6 100644 --- a/wasm/core/Cargo.toml +++ b/wasm/core/Cargo.toml @@ -15,6 +15,8 @@ wasm32-sdk = [] wasm-bindgen.workspace = true js-sys.workspace = true faster-hex.workspace = true +hexplay.workspace = true +workflow-wasm.workspace = true -[lints.clippy] -empty_docs = "allow" +[lints] +workspace = true diff --git a/wasm/core/src/hex.rs b/wasm/core/src/hex.rs new file mode 100644 index 0000000000..8c187e03f5 --- /dev/null +++ b/wasm/core/src/hex.rs @@ -0,0 +1,152 @@ +//! +//! Hex module provides a way to display binary data in a human-readable format. +//! + +use hexplay::{ + color::{Color, Spec}, + HexView, HexViewBuilder, +}; +use std::ops::Range; +use std::str::FromStr; +use wasm_bindgen::prelude::*; +use workflow_wasm::prelude::*; + +type Result = std::result::Result; + +#[derive(Default)] +pub struct HexViewConfig { + pub offset: Option, + pub replace_char: Option, + pub width: Option, + pub colors: Option)>>, +} + +impl HexViewConfig { + pub fn build(self, slice: &[u8]) -> HexView<'_> { + let mut builder = HexViewBuilder::new(slice); + + if let Some(offset) = self.offset { + builder = builder.address_offset(offset); + } + + if let Some(replace_char) = self.replace_char { + builder = builder.replacement_character(replace_char); + } + + if let Some(width) = self.width { + builder = builder.row_width(width); + } + + if let Some(colors) = self.colors { + if !colors.is_empty() { + builder = builder.add_colors(colors); + } + } + + builder.finish() + } +} + +pub struct ColorRange { + pub color: Option, + pub background: Option, + pub range: Range, +} + +impl ColorRange { + fn new(color: Option, background: Option, range: Range) -> Self { + Self { color, background, range } + } + + fn into_tuple(self) -> (Spec, Range) { + let mut spec = Spec::new(); + spec.set_fg(self.color); + spec.set_bg(self.background); + + (spec, self.range) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const TS_HEX_VIEW: &'static str = r#" +/** + * Color range configuration for Hex View. + * + * @category General + */ +export interface IHexViewColor { + start: number; + end: number; + color?: string; + background?: string; +} + +/** + * Configuration interface for Hex View. + * + * @category General + */ +export interface IHexViewConfig { + offset? : number; + replacementCharacter? : string; + width? : number; + colors? : IHexViewColor[]; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IHexViewColor")] + pub type HexViewColorT; + #[wasm_bindgen(extends = js_sys::Array, typescript_type = "IHexViewColor[]")] + pub type HexViewColorArrayT; + #[wasm_bindgen(typescript_type = "IHexViewConfig")] + pub type HexViewConfigT; +} + +impl TryFrom for ColorRange { + type Error = JsValue; + fn try_from(js_value: JsValue) -> Result { + if let Some(object) = js_sys::Object::try_from(&js_value) { + let start = object.get_u32("start")? as usize; + let end = object.get_u32("end")? as usize; + + let color = object.get_string("color").ok(); + let color = + color.map(|color| Color::from_str(color.as_str()).map_err(|e| JsValue::from_str(&e.to_string()))).transpose()?; + + let background = object.get_string("background").ok(); + let background = background + .map(|background| Color::from_str(background.as_str()).map_err(|e| JsValue::from_str(&e.to_string()))) + .transpose()?; + + Ok(ColorRange::new(color, background, start..end)) + } else { + Err(JsValue::from_str("color range must be an object")) + } + } +} + +pub fn try_to_color_vec(js_value: JsValue) -> Result)>> { + if js_value.is_array() { + let list = js_sys::Array::from(&js_value).iter().map(TryFrom::try_from).collect::>>()?; + Ok(list.into_iter().map(ColorRange::into_tuple).collect::>()) + } else { + let tuple = ColorRange::try_from(js_value).map(ColorRange::into_tuple)?; + Ok(vec![tuple]) + } +} + +impl TryFrom for HexViewConfig { + type Error = JsValue; + fn try_from(js_value: HexViewConfigT) -> Result { + let object = js_sys::Object::try_from(&js_value).ok_or_else(|| JsValue::from_str("HexView config must be an object"))?; + + let offset = object.get_u32("offset").ok().map(|v| v as usize); + let replace_char = object.get_string("replacementCharacter").ok().map(|s| s.chars().next().unwrap_or(' ')); + let width = object.get_u32("width").ok().map(|v| v as usize); + let colors = object.get_value("colors").ok().map(try_to_color_vec).transpose()?; + + Ok(HexViewConfig { offset, replace_char, width, colors }) + } +} diff --git a/wasm/core/src/lib.rs b/wasm/core/src/lib.rs index 1710c11fea..d99f297310 100644 --- a/wasm/core/src/lib.rs +++ b/wasm/core/src/lib.rs @@ -1,4 +1,3 @@ pub mod events; +pub mod hex; pub mod types; - -// pub use types::*; diff --git a/wasm/core/src/types.rs b/wasm/core/src/types.rs index fc898ee8b0..7e8e29335e 100644 --- a/wasm/core/src/types.rs +++ b/wasm/core/src/types.rs @@ -30,7 +30,7 @@ impl From for HexString { impl TryFrom for String { type Error = &'static str; - fn try_from(value: HexString) -> Result { + fn try_from(value: HexString) -> std::result::Result { value.as_string().ok_or("Supplied value is not a string") } } diff --git a/wasm/examples/nodejs/javascript/general/derivation.js b/wasm/examples/nodejs/javascript/general/derivation.js index f92508c889..942f665f98 100644 --- a/wasm/examples/nodejs/javascript/general/derivation.js +++ b/wasm/examples/nodejs/javascript/general/derivation.js @@ -21,6 +21,7 @@ kaspa.initConsolePanicHook(); let xPrv = new XPrv(seed); // derive full path upto second address of receive wallet let pubkey1 = xPrv.derivePath("m/44'/111111'/0'/0/1").toXPub().toPublicKey(); + console.log("publickey", pubkey1.toString()) console.log("address", pubkey1.toAddress(NetworkType.Mainnet)); // create receive wallet @@ -28,17 +29,25 @@ kaspa.initConsolePanicHook(); // derive receive wallet for second address let pubkey2 = receiveWalletXPub.deriveChild(1, false).toPublicKey(); console.log("address", pubkey2.toAddress(NetworkType.Mainnet)); + if (pubkey1.toString() != pubkey2.toString()){ + throw new Error("pubkey2 dont match") + } // create change wallet let changeWalletXPub = xPrv.derivePath("m/44'/111111'/0'/1").toXPub(); // derive change wallet for first address let pubkey3 = changeWalletXPub.deriveChild(0, false).toPublicKey(); - console.log("address", pubkey2.toAddress(NetworkType.Mainnet)); + console.log("change address", pubkey3.toAddress(NetworkType.Mainnet)); + // --- - if (pubkey1.toString() != pubkey2.toString()){ - throw new Error("pubkeyes dont match") + //drive address via private key + let privateKey = xPrv.derivePath("m/44'/111111'/0'/0/1").toPrivateKey(); + console.log("address via private key", privateKey.toAddress(NetworkType.Mainnet)) + console.log("privatekey", privateKey.toString()); + let pubkey4 = privateKey.toPublicKey(); + if (pubkey1.toString() != pubkey4.toString()){ + throw new Error("pubkey4 dont match") } - // --- // xprv with ktrv prefix const ktrv = xPrv.intoString("ktrv"); diff --git a/wasm/examples/nodejs/javascript/general/mining-state.js b/wasm/examples/nodejs/javascript/general/mining-pow.js similarity index 90% rename from wasm/examples/nodejs/javascript/general/mining-state.js rename to wasm/examples/nodejs/javascript/general/mining-pow.js index 1741fd5459..58bf2e70fe 100644 --- a/wasm/examples/nodejs/javascript/general/mining-state.js +++ b/wasm/examples/nodejs/javascript/general/mining-pow.js @@ -32,12 +32,12 @@ kaspa.initConsolePanicHook(); console.log("header.blueWork:", header.blueWork); console.log("header.blueWork.toString(16):", header.blueWork.toString(16)); - console.log("creating state"); - const state = new kaspa.State(header); + console.log("creating PoW"); + const pow = new kaspa.PoW(header); const nonce = BigInt("0xffffffffffffffff"); console.log("nonce:", nonce); - const [a, v] = state.checkPow(nonce); - console.log("state:", state); + const [a, v] = pow.checkWork(nonce); + console.log("pow:", pow); console.log("[a,v]:", a, v); console.log("v.toString(16):", v.toString(16)); })(); diff --git a/wasm/examples/nodejs/javascript/transactions/serialize.js b/wasm/examples/nodejs/javascript/transactions/serialize.js new file mode 100644 index 0000000000..977ed1e8ab --- /dev/null +++ b/wasm/examples/nodejs/javascript/transactions/serialize.js @@ -0,0 +1,48 @@ +const { + Address, + createTransactions, + initConsolePanicHook, + Mnemonic, + XPrv, + PrivateKeyGenerator, + payToAddressScript, +} = require('../../../../nodejs/kaspa'); + + +(async () => { + + const networkId = 'mainnet'; + + const mnemonic = Mnemonic.random(); + const xprv = new XPrv(mnemonic.toSeed()); + const privateKey = new PrivateKeyGenerator(xprv, false, 0n).receiveKey(1); + const address = privateKey.toAddress(networkId); + const scriptPublicKey = payToAddressScript(address); + const entries = [{ + address, + outpoint: { + transactionId: '1b84324c701b16c1cfbbd713a5ff87edf78bc5c92a92866f86d7e32ab5cd387d', + index: 0 + }, + scriptPublicKey, + amount: 50000000000n, + isCoinbase: true, + blockDaaScore: 342n + }]; + + const { transactions, summary } = await createTransactions({ + entries, + outputs: [{ + address: 'kaspa:qpamkvhgh0kzx50gwvvp5xs8ktmqutcy3dfs9dc3w7lm9rq0zs76vf959mmrp', + amount: 400000000n + }], + changeAddress: address, + priorityFee: 0n, + networkId + }); + + for (const pending of transactions) { + const tx = pending.serializeToObject(); + console.log(tx); + } +})(); diff --git a/wasm/examples/nodejs/javascript/transactions/simple-transaction.js b/wasm/examples/nodejs/javascript/transactions/simple-transaction.js index b215fd597d..fc9aa3b0ab 100644 --- a/wasm/examples/nodejs/javascript/transactions/simple-transaction.js +++ b/wasm/examples/nodejs/javascript/transactions/simple-transaction.js @@ -55,7 +55,7 @@ initConsolePanicHook(); let { transactions, summary } = await createTransactions({ entries, - outputs: [{ address : destinationAddress, amount : kaspaToSompi(0.00012)}], + outputs: [{ address : destinationAddress, amount : kaspaToSompi("0.00012")}], priorityFee: 0n, changeAddress: sourceAddress, }); diff --git a/wasm/examples/nodejs/javascript/transactions/single-transaction-demo.js b/wasm/examples/nodejs/javascript/transactions/single-transaction-demo.js index def206c158..0400c7d1c5 100644 --- a/wasm/examples/nodejs/javascript/transactions/single-transaction-demo.js +++ b/wasm/examples/nodejs/javascript/transactions/single-transaction-demo.js @@ -73,7 +73,13 @@ const { networkId, encoding } = require("../utils").parseArgs(); const changeAddress = address; console.log("changeAddress:", changeAddress) - const tx = createTransaction(utxos, outputs, changeAddress, 0n, 0, 1, 1); + + // utxo_entry_source: IUtxoEntry[], + // outputs: IPaymentOutput[], + // priority_fee: bigint, + // payload: HexString | Uint8Array, + // sig_op_count?: number + const tx = createTransaction(utxos, outputs, 0n, "", 1); console.info("Transaction before signing:", tx); diff --git a/wasm/examples/nodejs/typescript/src/scriptBuilder.ts b/wasm/examples/nodejs/typescript/src/scriptBuilder.ts new file mode 100644 index 0000000000..13f02b12bf --- /dev/null +++ b/wasm/examples/nodejs/typescript/src/scriptBuilder.ts @@ -0,0 +1,13 @@ +import { ScriptBuilder, Opcodes, addressFromScriptPublicKey, NetworkType } from "../../../../nodejs/kaspa" + +// An OpTrue is an always spendable script +const myScript = new ScriptBuilder() + .addOp(Opcodes.OpTrue) + +const P2SHScript = myScript.createPayToScriptHashScript() +const address = addressFromScriptPublicKey(P2SHScript, NetworkType.Mainnet) + +// Payable address +console.log(address!.toString()) +// Unlock signature script +console.log(myScript.encodePayToScriptHashSignatureScript("")) \ No newline at end of file diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 912ed9428b..77c5e16ea8 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -145,6 +145,7 @@ cfg_if::cfg_if! { pub use kaspa_addresses::{Address, Version as AddressVersion}; pub use kaspa_consensus_core::tx::{ScriptPublicKey, Transaction, TransactionInput, TransactionOutpoint, TransactionOutput}; pub use kaspa_pow::wasm::*; + pub use kaspa_txscript::wasm::*; pub mod rpc { //! Kaspa RPC interface @@ -171,6 +172,7 @@ cfg_if::cfg_if! { pub use kaspa_addresses::{Address, Version as AddressVersion}; pub use kaspa_consensus_core::tx::{ScriptPublicKey, Transaction, TransactionInput, TransactionOutpoint, TransactionOutput}; pub use kaspa_pow::wasm::*; + pub use kaspa_txscript::wasm::*; pub mod rpc { //! Kaspa RPC interface @@ -206,6 +208,7 @@ 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::*; } From 2306592af9f9900c64890a6db0e5166fa925b9ff Mon Sep 17 00:00:00 2001 From: KaffinPX <73744616+KaffinPX@users.noreply.github.com> Date: Tue, 27 Aug 2024 02:48:00 +0300 Subject: [PATCH 4/4] get_current_block_color RPC utility (#528) * getCurrentBlockColor algorithm and RPC functions * Add a small comment over RPC test * Move get_current_block_color to consensus and apply standard sorting * Apply msuttons suggestions except the block check * Remove not needed return and format * Variable name consistency * Check for block existence on get_current_block_color * Add extra DAG order checks to ensure about children * includes: 1. stylistic changes using ? 2. `is_dag_ancestor_of(a, b)` is different than `!is_dag_ancestor_of(b, a)` -- they are not negations of each other, bcs there's also the anticone * 1. bug fix: hash -> child 2. make store calls only where they are actually used (within the if) * style: 1. use struct unfloding syntax, 2. use a name such as decedent which reflects the relation to `hash` * important note * Fix Omega compatibility issues * Remove Borsh derivations * Fix gRPC message codes * Fix gRPC getCurrentBlockColorResponse * improve tests --------- Co-authored-by: Michael Sutton --- cli/src/modules/rpc.rs | 9 +++ components/consensusmanager/src/session.rs | 4 ++ consensus/core/src/api/mod.rs | 4 ++ consensus/src/consensus/mod.rs | 67 ++++++++++++++++++- rpc/core/src/api/ops.rs | 2 + rpc/core/src/api/rpc.rs | 10 +++ rpc/core/src/error.rs | 3 + rpc/core/src/model/message.rs | 48 +++++++++++++ rpc/core/src/wasm/message.rs | 38 +++++++++++ rpc/grpc/client/src/lib.rs | 1 + rpc/grpc/core/proto/messages.proto | 2 + rpc/grpc/core/proto/rpc.proto | 10 +++ rpc/grpc/core/src/convert/kaspad.rs | 2 + rpc/grpc/core/src/convert/message.rs | 20 ++++++ rpc/grpc/core/src/ops.rs | 1 + .../server/src/request_handler/factory.rs | 1 + rpc/grpc/server/src/tests/rpc_core_mock.rs | 8 +++ rpc/service/src/service.rs | 13 ++++ rpc/wrpc/client/src/client.rs | 1 + rpc/wrpc/server/src/router.rs | 1 + rpc/wrpc/wasm/src/client.rs | 3 + testing/integration/src/rpc_tests.rs | 20 +++++- wallet/core/src/tests/rpc_core_mock.rs | 8 +++ 23 files changed, 273 insertions(+), 3 deletions(-) diff --git a/cli/src/modules/rpc.rs b/cli/src/modules/rpc.rs index db4fd3383b..f32523c4a3 100644 --- a/cli/src/modules/rpc.rs +++ b/cli/src/modules/rpc.rs @@ -249,6 +249,15 @@ impl Rpc { let result = rpc.get_fee_estimate_experimental_call(None, GetFeeEstimateExperimentalRequest { verbose }).await?; self.println(&ctx, result); } + RpcApiOps::GetCurrentBlockColor => { + if argv.is_empty() { + return Err(Error::custom("Missing block hash argument")); + } + let hash = argv.remove(0); + let hash = RpcHash::from_hex(hash.as_str())?; + let result = rpc.get_current_block_color_call(None, GetCurrentBlockColorRequest { hash }).await?; + self.println(&ctx, result); + } _ => { tprintln!(ctx, "rpc method exists but is not supported by the cli: '{op_str}'\r\n"); return Ok(()); diff --git a/components/consensusmanager/src/session.rs b/components/consensusmanager/src/session.rs index 3e30783e60..81d5891488 100644 --- a/components/consensusmanager/src/session.rs +++ b/components/consensusmanager/src/session.rs @@ -247,6 +247,10 @@ impl ConsensusSessionOwned { self.clone().spawn_blocking(|c| c.get_sink_timestamp()).await } + pub async fn async_get_current_block_color(&self, hash: Hash) -> Option { + self.clone().spawn_blocking(move |c| c.get_current_block_color(hash)).await + } + /// source refers to the earliest block from which the current node has full header & block data pub async fn async_get_source(&self) -> Hash { self.clone().spawn_blocking(|c| c.get_source()).await diff --git a/consensus/core/src/api/mod.rs b/consensus/core/src/api/mod.rs index 6df33579c7..4833c7659a 100644 --- a/consensus/core/src/api/mod.rs +++ b/consensus/core/src/api/mod.rs @@ -133,6 +133,10 @@ pub trait ConsensusApi: Send + Sync { unimplemented!() } + fn get_current_block_color(&self, hash: Hash) -> Option { + unimplemented!() + } + fn get_virtual_state_approx_id(&self) -> VirtualStateApproxId { unimplemented!() } diff --git a/consensus/src/consensus/mod.rs b/consensus/src/consensus/mod.rs index 919929a8cd..8474a6864a 100644 --- a/consensus/src/consensus/mod.rs +++ b/consensus/src/consensus/mod.rs @@ -36,7 +36,10 @@ use crate::{ virtual_processor::{errors::PruningImportResult, VirtualStateProcessor}, ProcessingCounters, }, - processes::window::{WindowManager, WindowType}, + processes::{ + ghostdag::ordering::SortableBlock, + window::{WindowManager, WindowType}, + }, }; use kaspa_consensus_core::{ acceptance_data::AcceptanceData, @@ -64,7 +67,7 @@ use kaspa_consensus_core::{ pruning::{PruningPointProof, PruningPointTrustedData, PruningPointsList}, trusted::{ExternalGhostdagData, TrustedBlock}, tx::{MutableTransaction, Transaction, TransactionOutpoint, UtxoEntry}, - BlockHashSet, BlueWorkType, ChainPath, + BlockHashSet, BlueWorkType, ChainPath, HashMapCustomHasher, }; use kaspa_consensus_notify::root::ConsensusNotificationRoot; @@ -80,6 +83,8 @@ use kaspa_muhash::MuHash; use kaspa_txscript::caches::TxScriptCacheCounters; use std::{ + cmp::Reverse, + collections::BinaryHeap, future::Future, iter::once, ops::Deref, @@ -505,6 +510,64 @@ impl ConsensusApi for Consensus { self.headers_store.get_timestamp(self.get_sink()).unwrap() } + fn get_current_block_color(&self, hash: Hash) -> Option { + let _guard = self.pruning_lock.blocking_read(); + + // Verify the block exists and can be assumed to have relations and reachability data + self.validate_block_exists(hash).ok()?; + + // Verify that the block is in future(source), where Ghostdag data is complete + self.services.reachability_service.is_dag_ancestor_of(self.get_source(), hash).then_some(())?; + + let sink = self.get_sink(); + + // Optimization: verify that the block is in past(sink), otherwise the search will fail anyway + // (means the block was not merged yet by a virtual chain block) + self.services.reachability_service.is_dag_ancestor_of(hash, sink).then_some(())?; + + let mut heap: BinaryHeap> = BinaryHeap::new(); + let mut visited = BlockHashSet::new(); + + let initial_children = self.get_block_children(hash).unwrap(); + + for child in initial_children { + if visited.insert(child) { + let blue_work = self.ghostdag_primary_store.get_blue_work(child).unwrap(); + heap.push(Reverse(SortableBlock::new(child, blue_work))); + } + } + + while let Some(Reverse(SortableBlock { hash: decedent, .. })) = heap.pop() { + if self.services.reachability_service.is_chain_ancestor_of(decedent, sink) { + let decedent_data = self.get_ghostdag_data(decedent).unwrap(); + + if decedent_data.mergeset_blues.contains(&hash) { + return Some(true); + } else if decedent_data.mergeset_reds.contains(&hash) { + return Some(false); + } + + // Note: because we are doing a topological BFS up (from `hash` towards virtual), the first chain block + // found must also be our merging block, so hash will be either in blues or in reds, rendering this line + // unreachable. + kaspa_core::warn!("DAG topology inconsistency: {decedent} is expected to be a merging block of {hash}"); + // TODO: we should consider the option of returning Result> from this method + return None; + } + + let children = self.get_block_children(decedent).unwrap(); + + for child in children { + if visited.insert(child) { + let blue_work = self.ghostdag_primary_store.get_blue_work(child).unwrap(); + heap.push(Reverse(SortableBlock::new(child, blue_work))); + } + } + } + + None + } + fn get_virtual_state_approx_id(&self) -> VirtualStateApproxId { self.lkg_virtual_state.load().to_virtual_state_approx_id() } diff --git a/rpc/core/src/api/ops.rs b/rpc/core/src/api/ops.rs index 20a1d877c7..822798a1d2 100644 --- a/rpc/core/src/api/ops.rs +++ b/rpc/core/src/api/ops.rs @@ -130,6 +130,8 @@ pub enum RpcApiOps { GetFeeEstimate = 147, /// Fee estimation (experimental) GetFeeEstimateExperimental = 148, + /// Block color determination by iterating DAG. + GetCurrentBlockColor = 149, } impl RpcApiOps { diff --git a/rpc/core/src/api/rpc.rs b/rpc/core/src/api/rpc.rs index 9f92e3e5a6..85713e5477 100644 --- a/rpc/core/src/api/rpc.rs +++ b/rpc/core/src/api/rpc.rs @@ -457,6 +457,16 @@ pub trait RpcApi: Sync + Send + AnySync { request: GetFeeEstimateExperimentalRequest, ) -> RpcResult; + /// + async fn get_current_block_color(&self, hash: RpcHash) -> RpcResult { + Ok(self.get_current_block_color_call(None, GetCurrentBlockColorRequest { hash }).await?) + } + async fn get_current_block_color_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetCurrentBlockColorRequest, + ) -> RpcResult; + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/core/src/error.rs b/rpc/core/src/error.rs index 6e1083ab75..235ea639e7 100644 --- a/rpc/core/src/error.rs +++ b/rpc/core/src/error.rs @@ -77,6 +77,9 @@ pub enum RpcError { #[error("IP {0} is not registered as banned.")] IpIsNotBanned(IpAddress), + #[error("Block {0} doesn't have any merger block.")] + MergerNotFound(RpcHash), + #[error("Block was not submitted: {0}")] SubmitBlockError(SubmitBlockRejectReason), diff --git a/rpc/core/src/model/message.rs b/rpc/core/src/model/message.rs index 6a2253d8e8..779f7593a9 100644 --- a/rpc/core/src/model/message.rs +++ b/rpc/core/src/model/message.rs @@ -2613,6 +2613,54 @@ impl Deserializer for GetFeeEstimateExperimentalResponse { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCurrentBlockColorRequest { + pub hash: RpcHash, +} + +impl Serializer for GetCurrentBlockColorRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcHash, &self.hash, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetCurrentBlockColorRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let hash = load!(RpcHash, reader)?; + + Ok(Self { hash }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCurrentBlockColorResponse { + pub blue: bool, +} + +impl Serializer for GetCurrentBlockColorResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(bool, &self.blue, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetCurrentBlockColorResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let blue = load!(bool, reader)?; + + Ok(Self { blue }) + } +} + // ---------------------------------------------------------------------------- // Subscriptions & notifications // ---------------------------------------------------------------------------- diff --git a/rpc/core/src/wasm/message.rs b/rpc/core/src/wasm/message.rs index 639ba22aed..7c1a4c5d12 100644 --- a/rpc/core/src/wasm/message.rs +++ b/rpc/core/src/wasm/message.rs @@ -856,6 +856,44 @@ try_from! ( args: GetBlockTemplateResponse, IGetBlockTemplateResponse, { // --- +declare! { + IGetCurrentBlockColorRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetCurrentBlockColorRequest { + hash: HexString; + } + "#, +} + +try_from! ( args: IGetCurrentBlockColorRequest, GetCurrentBlockColorRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetCurrentBlockColorResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetCurrentBlockColorResponse { + blue: boolean; + } + "#, +} + +try_from! ( args: GetCurrentBlockColorResponse, IGetCurrentBlockColorResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + declare! { IGetDaaScoreTimestampEstimateRequest, r#" diff --git a/rpc/grpc/client/src/lib.rs b/rpc/grpc/client/src/lib.rs index 872b3105f5..74db82c4ec 100644 --- a/rpc/grpc/client/src/lib.rs +++ b/rpc/grpc/client/src/lib.rs @@ -276,6 +276,7 @@ impl RpcApi for GrpcClient { route!(get_daa_score_timestamp_estimate_call, GetDaaScoreTimestampEstimate); route!(get_fee_estimate_call, GetFeeEstimate); route!(get_fee_estimate_experimental_call, GetFeeEstimateExperimental); + route!(get_current_block_color_call, GetCurrentBlockColor); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/grpc/core/proto/messages.proto b/rpc/grpc/core/proto/messages.proto index 5380ba6587..2d6310d9e3 100644 --- a/rpc/grpc/core/proto/messages.proto +++ b/rpc/grpc/core/proto/messages.proto @@ -64,6 +64,7 @@ message KaspadRequest { GetSystemInfoRequestMessage getSystemInfoRequest = 1104; GetFeeEstimateRequestMessage getFeeEstimateRequest = 1106; GetFeeEstimateExperimentalRequestMessage getFeeEstimateExperimentalRequest = 1108; + GetCurrentBlockColorRequestMessage getCurrentBlockColorRequest = 1110; } } @@ -128,6 +129,7 @@ message KaspadResponse { GetSystemInfoResponseMessage getSystemInfoResponse= 1105; GetFeeEstimateResponseMessage getFeeEstimateResponse = 1107; GetFeeEstimateExperimentalResponseMessage getFeeEstimateExperimentalResponse = 1109; + GetCurrentBlockColorResponseMessage getCurrentBlockColorResponse = 1111; } } diff --git a/rpc/grpc/core/proto/rpc.proto b/rpc/grpc/core/proto/rpc.proto index d5c441927a..74d05e2ffc 100644 --- a/rpc/grpc/core/proto/rpc.proto +++ b/rpc/grpc/core/proto/rpc.proto @@ -959,3 +959,13 @@ message GetFeeEstimateExperimentalResponseMessage { RPCError error = 1000; } + +message GetCurrentBlockColorRequestMessage { + string hash = 1; +} + +message GetCurrentBlockColorResponseMessage { + bool blue = 1; + + RPCError error = 1000; +} diff --git a/rpc/grpc/core/src/convert/kaspad.rs b/rpc/grpc/core/src/convert/kaspad.rs index 9296a944ec..c3411545cc 100644 --- a/rpc/grpc/core/src/convert/kaspad.rs +++ b/rpc/grpc/core/src/convert/kaspad.rs @@ -62,6 +62,7 @@ pub mod kaspad_request_convert { impl_into_kaspad_request!(GetDaaScoreTimestampEstimate); impl_into_kaspad_request!(GetFeeEstimate); impl_into_kaspad_request!(GetFeeEstimateExperimental); + impl_into_kaspad_request!(GetCurrentBlockColor); impl_into_kaspad_request!(NotifyBlockAdded); impl_into_kaspad_request!(NotifyNewBlockTemplate); @@ -198,6 +199,7 @@ pub mod kaspad_response_convert { impl_into_kaspad_response!(GetDaaScoreTimestampEstimate); impl_into_kaspad_response!(GetFeeEstimate); impl_into_kaspad_response!(GetFeeEstimateExperimental); + impl_into_kaspad_response!(GetCurrentBlockColor); impl_into_kaspad_notify_response!(NotifyBlockAdded); impl_into_kaspad_notify_response!(NotifyNewBlockTemplate); diff --git a/rpc/grpc/core/src/convert/message.rs b/rpc/grpc/core/src/convert/message.rs index 5a0e946103..a04f9c863a 100644 --- a/rpc/grpc/core/src/convert/message.rs +++ b/rpc/grpc/core/src/convert/message.rs @@ -421,6 +421,15 @@ from!(item: RpcResult<&kaspa_rpc_core::GetFeeEstimateExperimentalResponse>, prot } }); +from!(item: &kaspa_rpc_core::GetCurrentBlockColorRequest, protowire::GetCurrentBlockColorRequestMessage, { + Self { + hash: item.hash.to_string() + } +}); +from!(item: RpcResult<&kaspa_rpc_core::GetCurrentBlockColorResponse>, protowire::GetCurrentBlockColorResponseMessage, { + Self { blue: item.blue, error: None } +}); + from!(&kaspa_rpc_core::PingRequest, protowire::PingRequestMessage); from!(RpcResult<&kaspa_rpc_core::PingResponse>, protowire::PingResponseMessage); @@ -896,6 +905,17 @@ try_from!(item: &protowire::GetFeeEstimateExperimentalResponseMessage, RpcResult } }); +try_from!(item: &protowire::GetCurrentBlockColorRequestMessage, kaspa_rpc_core::GetCurrentBlockColorRequest, { + Self { + hash: RpcHash::from_str(&item.hash)? + } +}); +try_from!(item: &protowire::GetCurrentBlockColorResponseMessage, RpcResult, { + Self { + blue: item.blue + } +}); + try_from!(&protowire::PingRequestMessage, kaspa_rpc_core::PingRequest); try_from!(&protowire::PingResponseMessage, RpcResult); diff --git a/rpc/grpc/core/src/ops.rs b/rpc/grpc/core/src/ops.rs index 3ae1f0a2d0..f3bc12c829 100644 --- a/rpc/grpc/core/src/ops.rs +++ b/rpc/grpc/core/src/ops.rs @@ -86,6 +86,7 @@ pub enum KaspadPayloadOps { GetDaaScoreTimestampEstimate, GetFeeEstimate, GetFeeEstimateExperimental, + GetCurrentBlockColor, // Subscription commands for starting/stopping notifications NotifyBlockAdded, diff --git a/rpc/grpc/server/src/request_handler/factory.rs b/rpc/grpc/server/src/request_handler/factory.rs index aa03be0b50..b6a5b4476f 100644 --- a/rpc/grpc/server/src/request_handler/factory.rs +++ b/rpc/grpc/server/src/request_handler/factory.rs @@ -80,6 +80,7 @@ impl Factory { GetDaaScoreTimestampEstimate, GetFeeEstimate, GetFeeEstimateExperimental, + GetCurrentBlockColor, NotifyBlockAdded, NotifyNewBlockTemplate, NotifyFinalityConflict, diff --git a/rpc/grpc/server/src/tests/rpc_core_mock.rs b/rpc/grpc/server/src/tests/rpc_core_mock.rs index 12d8fc949a..dd6de46d2a 100644 --- a/rpc/grpc/server/src/tests/rpc_core_mock.rs +++ b/rpc/grpc/server/src/tests/rpc_core_mock.rs @@ -230,6 +230,14 @@ impl RpcApi for RpcCoreMock { Err(RpcError::NotImplemented) } + async fn get_current_block_color_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetCurrentBlockColorRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + async fn get_block_count_call( &self, _connection: Option<&DynRpcConnection>, diff --git a/rpc/service/src/service.rs b/rpc/service/src/service.rs index 972006848b..d498f522f6 100644 --- a/rpc/service/src/service.rs +++ b/rpc/service/src/service.rs @@ -386,6 +386,19 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } + async fn get_current_block_color_call( + &self, + _connection: Option<&DynRpcConnection>, + request: GetCurrentBlockColorRequest, + ) -> RpcResult { + let session = self.consensus_manager.consensus().unguarded_session(); + + match session.async_get_current_block_color(request.hash).await { + Some(blue) => Ok(GetCurrentBlockColorResponse { blue }), + None => Err(RpcError::MergerNotFound(request.hash)), + } + } + async fn get_block_call(&self, _connection: Option<&DynRpcConnection>, request: GetBlockRequest) -> RpcResult { // TODO: test let session = self.consensus_manager.consensus().session().await; diff --git a/rpc/wrpc/client/src/client.rs b/rpc/wrpc/client/src/client.rs index a0a33444b8..2bd66b7cc4 100644 --- a/rpc/wrpc/client/src/client.rs +++ b/rpc/wrpc/client/src/client.rs @@ -614,6 +614,7 @@ impl RpcApi for KaspaRpcClient { GetBlockDagInfo, GetBlocks, GetBlockTemplate, + GetCurrentBlockColor, GetCoinSupply, GetConnectedPeerInfo, GetConnections, diff --git a/rpc/wrpc/server/src/router.rs b/rpc/wrpc/server/src/router.rs index 82590dca1e..4d0e206259 100644 --- a/rpc/wrpc/server/src/router.rs +++ b/rpc/wrpc/server/src/router.rs @@ -44,6 +44,7 @@ impl Router { GetBlockDagInfo, GetBlocks, GetBlockTemplate, + GetCurrentBlockColor, GetCoinSupply, GetConnectedPeerInfo, GetCurrentNetwork, diff --git a/rpc/wrpc/wasm/src/client.rs b/rpc/wrpc/wasm/src/client.rs index 631af0454f..81487172fe 100644 --- a/rpc/wrpc/wasm/src/client.rs +++ b/rpc/wrpc/wasm/src/client.rs @@ -996,6 +996,9 @@ build_wrpc_wasm_bindgen_interface!( /// Generates a new block template for mining. /// Returned information: Block template information. GetBlockTemplate, + /// Checks if block is blue or not. + /// Returned information: Block blueness. + GetCurrentBlockColor, /// Retrieves the estimated DAA (Difficulty Adjustment Algorithm) /// score timestamp estimate. /// Returned information: DAA score timestamp estimate. diff --git a/testing/integration/src/rpc_tests.rs b/testing/integration/src/rpc_tests.rs index c4e5b4ce0f..3c4df601b3 100644 --- a/testing/integration/src/rpc_tests.rs +++ b/testing/integration/src/rpc_tests.rs @@ -5,7 +5,7 @@ use futures_util::future::try_join_all; use kaspa_addresses::{Address, Prefix, Version}; use kaspa_consensus::params::SIMNET_GENESIS; use kaspa_consensus_core::{constants::MAX_SOMPI, header::Header, subnets::SubnetworkId, tx::Transaction}; -use kaspa_core::info; +use kaspa_core::{assert_match, info}; use kaspa_grpc_core::ops::KaspadPayloadOps; use kaspa_hashes::Hash; use kaspa_notify::{ @@ -161,6 +161,20 @@ async fn sanity_test() { .unwrap(); assert!(response.added_chain_block_hashes.contains(&block_hash)); assert!(response.removed_chain_block_hashes.is_empty()); + + let result = + rpc_client.get_current_block_color_call(None, GetCurrentBlockColorRequest { hash: SIMNET_GENESIS.hash }).await; + + // Genesis was merged by the new sink, so we're expecting a positive blueness response + assert_match!(result, Ok(GetCurrentBlockColorResponse { blue: true })); + + // The new sink has no merging block yet, so we expect a MergerNotFound error + let result = rpc_client.get_current_block_color_call(None, GetCurrentBlockColorRequest { hash: block_hash }).await; + assert!(result.is_err()); + + // Non-existing blocks should return an error + let result = rpc_client.get_current_block_color_call(None, GetCurrentBlockColorRequest { hash: 999.into() }).await; + assert!(result.is_err()); }) } @@ -168,6 +182,10 @@ async fn sanity_test() { tst!(op, "see SubmitBlock") } + KaspadPayloadOps::GetCurrentBlockColor => { + tst!(op, "see SubmitBlock") + } + KaspadPayloadOps::GetCurrentNetwork => { let rpc_client = client.clone(); tst!(op, { diff --git a/wallet/core/src/tests/rpc_core_mock.rs b/wallet/core/src/tests/rpc_core_mock.rs index 3e1c302dd4..4d10cdd9b1 100644 --- a/wallet/core/src/tests/rpc_core_mock.rs +++ b/wallet/core/src/tests/rpc_core_mock.rs @@ -371,6 +371,14 @@ impl RpcApi for RpcCoreMock { Err(RpcError::NotImplemented) } + async fn get_current_block_color_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetCurrentBlockColorRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API