From 3b7b8f2c97f4830b757bea190b3468d1b211f0ab Mon Sep 17 00:00:00 2001 From: KaffinPX Date: Sun, 27 Sep 2026 01:33:27 +0300 Subject: [PATCH 1/6] feat(wallet): Add address indexes and refactor send transaction for clarity --- .../src/mutator_set/removal_record/chunk.rs | 4 +- wallet/cli/src/core/sync.rs | 3 + wallet/sdk/src/state/address_book.rs | 114 +++++++++--------- wallet/sdk/src/wallet.rs | 103 +++++++++++----- 4 files changed, 138 insertions(+), 86 deletions(-) diff --git a/consensus/src/mutator_set/removal_record/chunk.rs b/consensus/src/mutator_set/removal_record/chunk.rs index 56875a8..8a2568c 100644 --- a/consensus/src/mutator_set/removal_record/chunk.rs +++ b/consensus/src/mutator_set/removal_record/chunk.rs @@ -30,9 +30,7 @@ const LONG_LENGTH_FLAG: u32 = 1 << 11; #[derive(Debug, Clone, Copy, Error, PartialEq, Eq)] pub(crate) enum ChunkUnpackError { - #[error( - "payload is too large -- packed chunk can never be more than {MAX_PACKED_LENGTH} u32s" - )] + #[error("payload is too large -- packed chunk can never be more than {MAX_PACKED_LENGTH} u32s")] PayloadTooBig, #[error("actual length is inconsistent relative to length indicator")] diff --git a/wallet/cli/src/core/sync.rs b/wallet/cli/src/core/sync.rs index 0eae9ba..6acd4a3 100644 --- a/wallet/cli/src/core/sync.rs +++ b/wallet/cli/src/core/sync.rs @@ -54,6 +54,9 @@ pub async fn run(wallet: Wallet, storage: Storage) { WalletEvent::UtxosOutgoing { id, utxos } => { info!("{} UTXOs being spent on transaction {}.", utxos.len(), id); } + WalletEvent::AddressGenerated { key_type, address } => { + info!("Generated {key_type:?} wallet address: {address:?}."); + } } } diff --git a/wallet/sdk/src/state/address_book.rs b/wallet/sdk/src/state/address_book.rs index 605262c..0c021ff 100644 --- a/wallet/sdk/src/state/address_book.rs +++ b/wallet/sdk/src/state/address_book.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use nyks_standards::wallet::keys::address::Address; use nyks_standards::wallet::keys::key::Key; @@ -9,54 +9,50 @@ use nyks_standards::wallet::keys::viewing_key::ViewingKey; use nyks_wallet_core::entropy::wallet_entropy::WalletEntropy; /// Owns the wallet's entropy and derives/tracks its addresses and keys. -/// -/// Addresses are grouped by key type in derivation order - index `i` is -/// the `i`-th derived key of that type, with index 0 being the special key. -/// `view_keys` mirrors that same order and stays in sync as addresses are -/// added. #[derive(Debug)] pub(crate) struct AddressBook { entropy: WalletEntropy, - addresses: HashMap>, - view_keys: Vec, + addresses: HashMap>, + viewing_keys: Vec, } impl AddressBook { - /// Seeds each key type with the special key and index-1 address. - pub(crate) fn new(entropy: WalletEntropy) -> Self { - let mut addresses: HashMap> = HashMap::new(); - - addresses.insert( - KeyType::Generation, - vec![ - entropy.special_generation_key().to_address().into(), - entropy.nth_generation_address(1).into(), - ], - ); - addresses.insert( - KeyType::Symmetric, - vec![ - entropy.special_symmetric_key().to_address().into(), - entropy.nth_symmetric_address(1).into(), - ], - ); - - let view_keys = vec![ - entropy.special_generation_key().to_viewing_key().into(), - entropy.nth_generation_key(1).to_viewing_key().into(), - entropy.special_symmetric_key().to_viewing_key().into(), - entropy.nth_symmetric_key(1).to_viewing_key().into(), - ]; + /// Creates an address book tracking every derivation index through the + /// requested index for each key type. Index 0 is reserved for the special + /// key and is always included. + pub(crate) fn with_address_indexes( + entropy: WalletEntropy, + generation_index: u64, + symmetric_index: u64, + ) -> Self { + let mut addresses = HashMap::new(); + let mut viewing_keys = Vec::new(); + + for (key_type, index) in [ + (KeyType::Generation, generation_index), + (KeyType::Symmetric, symmetric_index), + ] { + let mut by_index = BTreeMap::new(); + + for index in 0..=index { + let (address, viewing_key) = derive(&entropy, key_type, index); + + by_index.insert(index, address); + viewing_keys.push(viewing_key); + } + + addresses.insert(key_type, by_index); + } AddressBook { entropy, addresses, - view_keys, + viewing_keys, } } - pub(crate) fn view_keys(&self) -> &[ViewingKey] { - &self.view_keys + pub(crate) fn viewing_keys(&self) -> &[ViewingKey] { + &self.viewing_keys } /// Escape hatch for entropy-derived stuff that isn't really about @@ -69,8 +65,8 @@ impl AddressBook { pub(crate) fn latest(&self, key_type: KeyType) -> Address { self.addresses .get(&key_type) - .and_then(|v| v.last()) - .cloned() + .and_then(|v| v.last_key_value()) + .map(|(_, address)| address.clone()) .unwrap() } @@ -78,23 +74,13 @@ impl AddressBook { /// it along with its viewing key. pub(crate) fn next_address(&mut self, key_type: KeyType) -> (Address, ViewingKey) { let next_index = self.next_index(key_type); - - let (address, view_key): (Address, ViewingKey) = match key_type { - KeyType::Generation => { - let key = self.entropy.nth_generation_key(next_index); - (key.to_address().into(), key.to_viewing_key().into()) - } - KeyType::Symmetric => { - let key = self.entropy.nth_symmetric_key(next_index); - (key.to_address().into(), key.to_viewing_key().into()) - } - }; + let (address, view_key) = derive(&self.entropy, key_type, next_index); self.addresses .entry(key_type) .or_default() - .push(address.clone()); - self.view_keys.push(view_key.clone()); + .insert(next_index, address.clone()); + self.viewing_keys.push(view_key.clone()); (address, view_key) } @@ -104,18 +90,21 @@ impl AddressBook { let (key_type, index) = self.find(matches)?; Some(match key_type { - KeyType::Generation => self.entropy.nth_generation_key(index as u64).into(), - KeyType::Symmetric => self.entropy.nth_symmetric_key(index as u64).into(), + KeyType::Generation => self.entropy.nth_generation_key(index).into(), + KeyType::Symmetric => self.entropy.nth_symmetric_key(index).into(), }) } fn next_index(&self, key_type: KeyType) -> u64 { - self.addresses.get(&key_type).map(|v| v.len()).unwrap_or(0) as u64 + self.addresses + .get(&key_type) + .and_then(|v| v.last_key_value().map(|(&index, _)| index + 1)) + .unwrap_or(1) } - fn find(&self, matches: impl Fn(&Address) -> bool) -> Option<(KeyType, usize)> { + fn find(&self, matches: impl Fn(&Address) -> bool) -> Option<(KeyType, u64)> { for (key_type, addrs) in self.addresses.iter() { - for (index, address) in addrs.iter().enumerate() { + for (&index, address) in addrs.iter() { if matches(address) { return Some((*key_type, index)); } @@ -124,3 +113,16 @@ impl AddressBook { None } } + +fn derive(entropy: &WalletEntropy, key_type: KeyType, index: u64) -> (Address, ViewingKey) { + match key_type { + KeyType::Generation => { + let key = entropy.nth_generation_key(index); + (key.to_address().into(), key.to_viewing_key().into()) + } + KeyType::Symmetric => { + let key = entropy.nth_symmetric_key(index); + (key.to_address().into(), key.to_viewing_key().into()) + } + } +} diff --git a/wallet/sdk/src/wallet.rs b/wallet/sdk/src/wallet.rs index aaa95f2..9c9e591 100644 --- a/wallet/sdk/src/wallet.rs +++ b/wallet/sdk/src/wallet.rs @@ -15,6 +15,7 @@ use nyks_standards::wallet::keys::address::Recipient; use nyks_standards::wallet::keys::key::KeyType; use nyks_standards::wallet::keys::key::Spender; use nyks_wallet_core::entropy::wallet_entropy::WalletEntropy; +use nyks_wallet_core::transaction::BuilderTransaction; use nyks_wallet_core::transaction::builder::TransactionBuilder; use nyks_wallet_core::transaction::builder::output::TxOutput; pub use nyks_wallet_core::transaction::primitive_witness::ProvingStage; @@ -30,6 +31,7 @@ use crate::state::address_book::AddressBook; use crate::state::utxos::MonitoredUtxo; use crate::state::utxos::UtxoKey; use crate::state::utxos::pool::UtxoPool; +use crate::state::utxos::pool::UtxosSelection; const BATCH_SIZE: usize = 100; @@ -60,6 +62,9 @@ pub enum WalletEvent { id: TransactionKernelId, utxos: Vec, }, + + /// A new address was derived by the wallet. + AddressGenerated { key_type: KeyType, address: Address }, } impl WalletEvent { @@ -98,10 +103,24 @@ impl Wallet { height: Option, network: Network, ) -> Self { - let addresses = AddressBook::new(entropy); + Self::new_with_address_indexes(rpc, entropy, height, network, 1, 1) + } + + /// Creates a wallet that scans every index from 0 through the selected + /// index per key type. + pub fn new_with_address_indexes( + rpc: HttpClient, + entropy: WalletEntropy, + height: Option, + network: Network, + generation_index: u64, + symmetric_index: u64, + ) -> Self { + let addresses = + AddressBook::with_address_indexes(entropy, generation_index, symmetric_index); let utxos = UtxoPool::new(rpc.clone()); - let view_keys = addresses.view_keys().to_vec(); + let view_keys = addresses.viewing_keys().to_vec(); Wallet { rpc, @@ -308,9 +327,8 @@ impl Wallet { /// proof-proving stage begins; the channel closes on its own once /// proving completes. Pass `None` to skip progress reporting. /// - /// Any `UtxoInvalidated` events raised while selecting inputs (UTXOs - /// found spent during proof-syncing) are queued and surfaced on the - /// next call to [`Wallet::sync`], rather than returned here directly. + /// Events raised while sending are queued and surfaced on the next call + /// to [`Wallet::sync`]. pub async fn send( &self, recipient: Address, @@ -318,10 +336,25 @@ impl Wallet { fee: NativeCurrencyAmount, progress: Option>, ) -> Result { - let height = self.tip_height().await; let timestamp = Timestamp::now(); + let selection = self.select_inputs(amount + fee, timestamp).await; + let height = self.tip_height().await; + let transaction = self + .build_transaction(recipient, amount, fee, timestamp, height, selection) + .await; + let transaction = Self::prove_transaction(transaction, progress).await; + let transaction_kernel_id = transaction.txid(); + + self.rpc.submit_transaction(transaction.into()).await?; + + Ok(transaction_kernel_id) + } - let mut utxos = self.utxos.write().await; + async fn select_inputs( + &self, + target: NativeCurrencyAmount, + timestamp: Timestamp, + ) -> UtxosSelection { let excluded_utxos = self .mempool_scanner .read() @@ -329,34 +362,53 @@ impl Wallet { .pending_spend_utxos() .copied() .collect(); - let selection = utxos - .select_utxos(amount + fee, timestamp, Some(excluded_utxos)) + let selection = self + .utxos + .write() + .await + .select_utxos(target, timestamp, Some(excluded_utxos)) .await; if !selection.invalidated_utxos.is_empty() { - let mut pending = self.pending_events.write().await; - pending.extend( + self.pending_events.write().await.extend( selection .invalidated_utxos - .into_iter() - .map(|(key, utxo)| WalletEvent::utxo_invalidated(key, utxo)), + .iter() + .map(|(key, utxo)| WalletEvent::utxo_invalidated(*key, utxo.clone())), ); } + selection + } + async fn build_transaction( + &self, + recipient: Address, + amount: NativeCurrencyAmount, + fee: NativeCurrencyAmount, + timestamp: Timestamp, + height: BlockHeight, + selection: UtxosSelection, + ) -> BuilderTransaction { let inputs = self.unlock_utxos(selection.utxos).await; + let change_address = self.next_address(KeyType::Symmetric).await; + self.pending_events + .write() + .await + .push(WalletEvent::AddressGenerated { + key_type: KeyType::Symmetric, + address: change_address.clone(), + }); - let change_address = self.address(KeyType::Symmetric).await; // TODO: increment symmetric address count let (sender_randomness, change_sender_randomness) = { let addresses = self.addresses.read().await; let entropy = addresses.entropy(); - ( entropy.generate_sender_randomness(height, recipient.privacy_digest()), entropy.generate_sender_randomness(height, change_address.privacy_digest()), ) }; - let transaction = TransactionBuilder::new() + TransactionBuilder::new() .inputs(inputs.into()) .outputs( vec![ @@ -364,7 +416,7 @@ impl Wallet { TxOutput::onchain_native_currency_as_change( selection.change, change_sender_randomness, - self.address(KeyType::Symmetric).await, + change_address, ), ] .into(), @@ -373,10 +425,13 @@ impl Wallet { .timestamp(timestamp) .mutator_set_accumulator(selection.msa) .build() - .unwrap(); + .unwrap() + } - // Proving is CPU-heavy; do it off the async executor and stream - // stage updates back through `progress`, if supplied. + async fn prove_transaction( + transaction: BuilderTransaction, + progress: Option>, + ) -> Transaction { let transaction = tokio::task::spawn_blocking(move || { transaction.upgrade_with_progress(move |stage| { if let Some(tx) = &progress { @@ -386,13 +441,7 @@ impl Wallet { }) .await .expect("spawned task for proving transaction should not panic"); - - let transaction: Transaction = transaction.try_into().unwrap(); - let transaction_kernel_id = transaction.txid(); - - self.rpc.submit_transaction(transaction.into()).await?; - - Ok(transaction_kernel_id) + transaction.try_into().unwrap() } async fn unlock_utxos(&self, utxos: Vec) -> Vec { From 0552a5a692ef018a7a36814d8ddab6016f48e7b1 Mon Sep 17 00:00:00 2001 From: KaffinPX Date: Sun, 27 Sep 2026 02:41:50 +0300 Subject: [PATCH 2/6] style(wallet-sdk): Move events into its own file --- wallet/sdk/src/wallet/event.rs | 43 ++++++++++++++++++++ wallet/sdk/src/{wallet.rs => wallet/mod.rs} | 45 +++------------------ 2 files changed, 48 insertions(+), 40 deletions(-) create mode 100644 wallet/sdk/src/wallet/event.rs rename wallet/sdk/src/{wallet.rs => wallet/mod.rs} (91%) diff --git a/wallet/sdk/src/wallet/event.rs b/wallet/sdk/src/wallet/event.rs new file mode 100644 index 0000000..1920439 --- /dev/null +++ b/wallet/sdk/src/wallet/event.rs @@ -0,0 +1,43 @@ +use nyks_consensus::transaction::transaction_kernel_id::TransactionKernelId; +use nyks_standards::wallet::keys::address::Address; +use nyks_standards::wallet::keys::key::KeyType; + +use crate::state::utxos::MonitoredUtxo; +use crate::state::utxos::UtxoKey; + +/// Events emitted by the wallet as a result of sync and scan activity. +#[derive(Debug, Clone)] +pub enum WalletEvent { + /// A new UTXO was discovered and added to the wallet's UTXO pool. + UtxoReceived { key: UtxoKey, utxo: MonitoredUtxo }, + + /// A previously-tracked UTXO was found to be spent (or otherwise + /// invalid) while syncing membership proofs, and was evicted from the + /// pool. + UtxoInvalidated { key: UtxoKey, utxo: MonitoredUtxo }, + + /// A mempool transaction was found to spend one or more of the + /// wallet's UTXOs. Emitted once per transaction, the first time it's + /// observed as relevant. + UtxosOutgoing { + id: TransactionKernelId, + utxos: Vec, + }, + + /// A new address was derived by the wallet. + AddressGenerated { key_type: KeyType, address: Address }, +} + +impl WalletEvent { + pub fn utxo_received(key: UtxoKey, utxo: MonitoredUtxo) -> Self { + WalletEvent::UtxoReceived { key, utxo } + } + + pub fn utxo_invalidated(key: UtxoKey, utxo: MonitoredUtxo) -> Self { + WalletEvent::UtxoInvalidated { key, utxo } + } + + pub fn utxos_outgoing(id: TransactionKernelId, utxos: Vec) -> Self { + WalletEvent::UtxosOutgoing { id, utxos } + } +} diff --git a/wallet/sdk/src/wallet.rs b/wallet/sdk/src/wallet/mod.rs similarity index 91% rename from wallet/sdk/src/wallet.rs rename to wallet/sdk/src/wallet/mod.rs index 9c9e591..50a8481 100644 --- a/wallet/sdk/src/wallet.rs +++ b/wallet/sdk/src/wallet/mod.rs @@ -1,3 +1,5 @@ +pub mod event; + use std::sync::Arc; use num_traits::CheckedSub; @@ -29,9 +31,9 @@ use crate::scanners::chain::ChainScanner; use crate::scanners::mempool::MempoolScanner; use crate::state::address_book::AddressBook; use crate::state::utxos::MonitoredUtxo; -use crate::state::utxos::UtxoKey; use crate::state::utxos::pool::UtxoPool; use crate::state::utxos::pool::UtxosSelection; +pub use event::WalletEvent; const BATCH_SIZE: usize = 100; @@ -44,43 +46,6 @@ pub enum SyncError { Advance(#[from] AdvanceError), } -/// Events emitted by the wallet as a result of sync and scan activity. -#[derive(Debug, Clone)] -pub enum WalletEvent { - /// A new UTXO was discovered and added to the wallet's UTXO pool. - UtxoReceived { key: UtxoKey, utxo: MonitoredUtxo }, - - /// A previously-tracked UTXO was found to be spent (or otherwise - /// invalid) while syncing membership proofs, and was evicted from the - /// pool. - UtxoInvalidated { key: UtxoKey, utxo: MonitoredUtxo }, - - /// A mempool transaction was found to spend one or more of the - /// wallet's UTXOs. Emitted once per transaction, the first time it's - /// observed as relevant. - UtxosOutgoing { - id: TransactionKernelId, - utxos: Vec, - }, - - /// A new address was derived by the wallet. - AddressGenerated { key_type: KeyType, address: Address }, -} - -impl WalletEvent { - pub fn utxo_received(key: UtxoKey, utxo: MonitoredUtxo) -> Self { - WalletEvent::UtxoReceived { key, utxo } - } - - pub fn utxo_invalidated(key: UtxoKey, utxo: MonitoredUtxo) -> Self { - WalletEvent::UtxoInvalidated { key, utxo } - } - - pub fn utxos_outgoing(id: TransactionKernelId, utxos: Vec) -> Self { - WalletEvent::UtxosOutgoing { id, utxos } - } -} - #[derive(Clone)] pub struct Wallet { rpc: HttpClient, @@ -103,12 +68,12 @@ impl Wallet { height: Option, network: Network, ) -> Self { - Self::new_with_address_indexes(rpc, entropy, height, network, 1, 1) + Self::new_with_indexes(rpc, entropy, height, network, 1, 1) } /// Creates a wallet that scans every index from 0 through the selected /// index per key type. - pub fn new_with_address_indexes( + pub fn new_with_indexes( rpc: HttpClient, entropy: WalletEntropy, height: Option, From 3e3c1a065834d2a4bc8b33e956cef5719e48636e Mon Sep 17 00:00:00 2001 From: KaffinPX Date: Sun, 27 Sep 2026 22:40:00 +0300 Subject: [PATCH 3/6] feat(wallet): Reflect indexes to storage and send() uses a fresh address --- wallet/cli/src/core/storage.rs | 13 +++-- wallet/cli/src/core/sync.rs | 7 ++- wallet/cli/src/main.rs | 10 +++- wallet/sdk/src/state/address_book.rs | 76 ++++++++++++---------------- wallet/sdk/src/wallet/event.rs | 6 ++- wallet/sdk/src/wallet/mod.rs | 48 +++++++++--------- 6 files changed, 82 insertions(+), 78 deletions(-) diff --git a/wallet/cli/src/core/storage.rs b/wallet/cli/src/core/storage.rs index d9ec07d..9849f39 100644 --- a/wallet/cli/src/core/storage.rs +++ b/wallet/cli/src/core/storage.rs @@ -110,20 +110,19 @@ impl KeysStore { write_json(&self.path, data); } - /// Returns the next derivation index (stored index + 1) for display. - pub fn next_index(&self, key: KeyType) -> u64 { + pub fn last_index(&self, key: KeyType) -> u64 { let data = self.read(); match key { - KeyType::Generation => data.generation_index + 1, - KeyType::Symmetric => data.symmetric_index + 1, + KeyType::Generation => data.generation_index, + KeyType::Symmetric => data.symmetric_index, } } - pub fn increment(&self, key: KeyType) { + pub fn set_index(&self, key: KeyType, index: u64) { let mut data = self.read(); match key { - KeyType::Generation => data.generation_index += 1, - KeyType::Symmetric => data.symmetric_index += 1, + KeyType::Generation => data.generation_index = index, + KeyType::Symmetric => data.symmetric_index = index, } self.write(&data); } diff --git a/wallet/cli/src/core/sync.rs b/wallet/cli/src/core/sync.rs index 6acd4a3..76f9d14 100644 --- a/wallet/cli/src/core/sync.rs +++ b/wallet/cli/src/core/sync.rs @@ -54,8 +54,13 @@ pub async fn run(wallet: Wallet, storage: Storage) { WalletEvent::UtxosOutgoing { id, utxos } => { info!("{} UTXOs being spent on transaction {}.", utxos.len(), id); } - WalletEvent::AddressGenerated { key_type, address } => { + WalletEvent::AddressGenerated { + key_type, + index, + address, + } => { info!("Generated {key_type:?} wallet address: {address:?}."); + storage.keys.set_index(key_type, index); } } } diff --git a/wallet/cli/src/main.rs b/wallet/cli/src/main.rs index 46a6796..e16e6e6 100644 --- a/wallet/cli/src/main.rs +++ b/wallet/cli/src/main.rs @@ -7,6 +7,7 @@ use anyhow::Result; use clap::Parser; use nyks_consensus::network::Network; use nyks_rpc_client::http::HttpClient; +use nyks_standards::wallet::keys::key::KeyType; use nyks_wallet_core::entropy::wallet_entropy::WalletEntropy; use nyks_wallet_sdk::wallet::Wallet; @@ -54,7 +55,14 @@ async fn main() -> Result<()> { None => setup::run(&storage)?, }; - let wallet = Wallet::new(rpc, entropy, storage.chain.get_height(), args.network); + let wallet = Wallet::new_with_indexes( + rpc, + entropy, + storage.chain.get_height(), + args.network, + storage.keys.last_index(KeyType::Generation), + storage.keys.last_index(KeyType::Symmetric), + ); // Import initial state of wallet. wallet.import_utxos(storage.utxos.iter().collect()).await; diff --git a/wallet/sdk/src/state/address_book.rs b/wallet/sdk/src/state/address_book.rs index 0c021ff..2938b4d 100644 --- a/wallet/sdk/src/state/address_book.rs +++ b/wallet/sdk/src/state/address_book.rs @@ -1,4 +1,5 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; +use std::collections::HashMap; use nyks_standards::wallet::keys::address::Address; use nyks_standards::wallet::keys::key::Key; @@ -10,23 +11,20 @@ use nyks_wallet_core::entropy::wallet_entropy::WalletEntropy; /// Owns the wallet's entropy and derives/tracks its addresses and keys. #[derive(Debug)] -pub(crate) struct AddressBook { +pub struct AddressBook { entropy: WalletEntropy, - addresses: HashMap>, - viewing_keys: Vec, + addresses: HashMap>, } impl AddressBook { - /// Creates an address book tracking every derivation index through the - /// requested index for each key type. Index 0 is reserved for the special - /// key and is always included. - pub(crate) fn with_address_indexes( + /// Tracks every used derivation index, plus the next unused address for + /// each key type. Index 0 is reserved for the special key and is included. + pub fn with_address_indexes( entropy: WalletEntropy, generation_index: u64, symmetric_index: u64, ) -> Self { let mut addresses = HashMap::new(); - let mut viewing_keys = Vec::new(); for (key_type, index) in [ (KeyType::Generation, generation_index), @@ -34,59 +32,54 @@ impl AddressBook { ] { let mut by_index = BTreeMap::new(); - for index in 0..=index { - let (address, viewing_key) = derive(&entropy, key_type, index); - - by_index.insert(index, address); - viewing_keys.push(viewing_key); + for index in 0..=index + 1 { + by_index.insert(index, derive(&entropy, key_type, index)); } addresses.insert(key_type, by_index); } - AddressBook { - entropy, - addresses, - viewing_keys, - } + AddressBook { entropy, addresses } } - pub(crate) fn viewing_keys(&self) -> &[ViewingKey] { - &self.viewing_keys + pub fn viewing_keys(&self) -> impl Iterator { + self.addresses + .values() + .flat_map(|by_index| by_index.values().map(|(_, viewing_key)| viewing_key)) } - /// Escape hatch for entropy-derived stuff that isn't really about - /// addresses (e.g. sender randomness), so this struct doesn't need a - /// proxy method per use. pub(crate) fn entropy(&self) -> &WalletEntropy { &self.entropy } - pub(crate) fn latest(&self, key_type: KeyType) -> Address { + /// Returns the highest tracked address, which is the wallet's current + /// unused address. + pub fn latest_address(&self, key_type: KeyType) -> Address { self.addresses .get(&key_type) - .and_then(|v| v.last_key_value()) - .map(|(_, address)| address.clone()) + .and_then(|by_index| by_index.last_key_value()) + .map(|(_, (address, _))| address.clone()) .unwrap() } - /// Derives the next address for a key type, registers it, and returns - /// it along with its viewing key. - pub(crate) fn next_address(&mut self, key_type: KeyType) -> (Address, ViewingKey) { - let next_index = self.next_index(key_type); - let (address, view_key) = derive(&self.entropy, key_type, next_index); - + /// Derives and records a new address, then returns it with its index and + /// viewing key. + pub fn next_address(&mut self, key_type: KeyType) -> (Address, u64, ViewingKey) { + let index = self.addresses[&key_type] + .last_key_value() + .map(|(&index, _)| index + 1) + .unwrap_or(1); + let (address, viewing_key) = derive(&self.entropy, key_type, index); self.addresses .entry(key_type) .or_default() - .insert(next_index, address.clone()); - self.viewing_keys.push(view_key.clone()); + .insert(index, (address.clone(), viewing_key.clone())); - (address, view_key) + (address, index, viewing_key) } /// Derives the spending key for whichever address matches, if any. - pub(crate) fn spending_key(&self, matches: impl Fn(&Address) -> bool) -> Option { + pub fn spending_key(&self, matches: impl Fn(&Address) -> bool) -> Option { let (key_type, index) = self.find(matches)?; Some(match key_type { @@ -95,17 +88,10 @@ impl AddressBook { }) } - fn next_index(&self, key_type: KeyType) -> u64 { - self.addresses - .get(&key_type) - .and_then(|v| v.last_key_value().map(|(&index, _)| index + 1)) - .unwrap_or(1) - } - fn find(&self, matches: impl Fn(&Address) -> bool) -> Option<(KeyType, u64)> { for (key_type, addrs) in self.addresses.iter() { for (&index, address) in addrs.iter() { - if matches(address) { + if matches(&address.0) { return Some((*key_type, index)); } } diff --git a/wallet/sdk/src/wallet/event.rs b/wallet/sdk/src/wallet/event.rs index 1920439..72bad0f 100644 --- a/wallet/sdk/src/wallet/event.rs +++ b/wallet/sdk/src/wallet/event.rs @@ -25,7 +25,11 @@ pub enum WalletEvent { }, /// A new address was derived by the wallet. - AddressGenerated { key_type: KeyType, address: Address }, + AddressGenerated { + key_type: KeyType, + index: u64, + address: Address, + }, } impl WalletEvent { diff --git a/wallet/sdk/src/wallet/mod.rs b/wallet/sdk/src/wallet/mod.rs index 50a8481..064d391 100644 --- a/wallet/sdk/src/wallet/mod.rs +++ b/wallet/sdk/src/wallet/mod.rs @@ -54,10 +54,7 @@ pub struct Wallet { mempool_scanner: Arc>, utxos: Arc>, pub network: Network, - - /// Events raised outside of `sync` (e.g. by `send`, when it evicts spent - /// UTXOs) that haven't been handed to a caller yet. Drained and merged - /// into the next `sync()` call's returned events. + /// Events queued for delivery by the next `sync()` call. pending_events: Arc>>, } @@ -68,11 +65,11 @@ impl Wallet { height: Option, network: Network, ) -> Self { - Self::new_with_indexes(rpc, entropy, height, network, 1, 1) + Self::new_with_indexes(rpc, entropy, height, network, 0, 0) } - /// Creates a wallet that scans every index from 0 through the selected - /// index per key type. + /// Creates a wallet that scans every index through the last used index + /// and the current unused address. pub fn new_with_indexes( rpc: HttpClient, entropy: WalletEntropy, @@ -83,16 +80,18 @@ impl Wallet { ) -> Self { let addresses = AddressBook::with_address_indexes(entropy, generation_index, symmetric_index); + let scanner = ChainScanner::new( + height, + None, + addresses.viewing_keys().cloned().collect(), + network, + ); let utxos = UtxoPool::new(rpc.clone()); - let view_keys = addresses.viewing_keys().to_vec(); - Wallet { rpc, addresses: Arc::new(RwLock::new(addresses)), - scanner: Arc::new(RwLock::new(ChainScanner::new( - height, None, view_keys, network, - ))), + scanner: Arc::new(RwLock::new(scanner)), mempool_scanner: Arc::new(RwLock::new(MempoolScanner::new(utxos.index()))), utxos: Arc::new(RwLock::new(utxos)), network, @@ -121,15 +120,24 @@ impl Wallet { } pub async fn address(&self, key_type: KeyType) -> Address { - self.addresses.read().await.latest(key_type) + self.addresses.read().await.latest_address(key_type) } pub async fn next_address(&self, key_type: KeyType) -> Address { let mut addresses = self.addresses.write().await; let mut scanner = self.scanner.write().await; - let (address, view_key) = addresses.next_address(key_type); - scanner.add_key(view_key); + let (address, index, viewing_key) = addresses.next_address(key_type); + scanner.add_key(viewing_key); + + self.pending_events + .write() + .await + .push(WalletEvent::AddressGenerated { + key_type, + index, + address: address.clone(), + }); address } @@ -355,14 +363,8 @@ impl Wallet { selection: UtxosSelection, ) -> BuilderTransaction { let inputs = self.unlock_utxos(selection.utxos).await; - let change_address = self.next_address(KeyType::Symmetric).await; - self.pending_events - .write() - .await - .push(WalletEvent::AddressGenerated { - key_type: KeyType::Symmetric, - address: change_address.clone(), - }); + let change_address = self.address(KeyType::Symmetric).await; + self.next_address(KeyType::Symmetric).await; let (sender_randomness, change_sender_randomness) = { let addresses = self.addresses.read().await; From 4a576e7dccbc09ce828c266fa1b0122ec1b27b7b Mon Sep 17 00:00:00 2001 From: KaffinPX Date: Tue, 29 Sep 2026 00:31:17 +0300 Subject: [PATCH 4/6] feat(wallet-cli): Make address index 1 default for consistency --- wallet/cli/src/core/storage.rs | 12 +++++++++++- wallet/sdk/src/state/address_book.rs | 1 - 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/wallet/cli/src/core/storage.rs b/wallet/cli/src/core/storage.rs index 9849f39..f7a0501 100644 --- a/wallet/cli/src/core/storage.rs +++ b/wallet/cli/src/core/storage.rs @@ -86,13 +86,23 @@ impl Storage { } /// Typed on-disk representation for `keys.json`. -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] struct KeysData { generation_index: u64, symmetric_index: u64, mnemonic: Option, } +impl Default for KeysData { + fn default() -> Self { + Self { + generation_index: 1, + symmetric_index: 1, + mnemonic: None, + } + } +} + pub struct KeysStore { path: PathBuf, } diff --git a/wallet/sdk/src/state/address_book.rs b/wallet/sdk/src/state/address_book.rs index 2938b4d..e395aaf 100644 --- a/wallet/sdk/src/state/address_book.rs +++ b/wallet/sdk/src/state/address_book.rs @@ -6,7 +6,6 @@ use nyks_standards::wallet::keys::key::Key; use nyks_standards::wallet::keys::key::KeyType; use nyks_standards::wallet::keys::key::Spender; use nyks_standards::wallet::keys::viewing_key::ViewingKey; - use nyks_wallet_core::entropy::wallet_entropy::WalletEntropy; /// Owns the wallet's entropy and derives/tracks its addresses and keys. From f1d66092ea3d23b1b8de55b94cae47fa94517db0 Mon Sep 17 00:00:00 2001 From: KaffinPX Date: Tue, 29 Sep 2026 03:52:25 +0300 Subject: [PATCH 5/6] feat(wallet-cli): Revert default indexes to zero --- wallet/cli/src/core/storage.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/wallet/cli/src/core/storage.rs b/wallet/cli/src/core/storage.rs index f7a0501..9849f39 100644 --- a/wallet/cli/src/core/storage.rs +++ b/wallet/cli/src/core/storage.rs @@ -86,23 +86,13 @@ impl Storage { } /// Typed on-disk representation for `keys.json`. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] struct KeysData { generation_index: u64, symmetric_index: u64, mnemonic: Option, } -impl Default for KeysData { - fn default() -> Self { - Self { - generation_index: 1, - symmetric_index: 1, - mnemonic: None, - } - } -} - pub struct KeysStore { path: PathBuf, } From ff23ad1daa8c36d91c3b612b061a076e82fb4d7c Mon Sep 17 00:00:00 2001 From: KaffinPX Date: Tue, 29 Sep 2026 04:07:03 +0300 Subject: [PATCH 6/6] fix(consensus): Fix BlockProgram snapshot hash tests --- consensus/src/block/validity/block_program.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/src/block/validity/block_program.rs b/consensus/src/block/validity/block_program.rs index 40998d9..55af9b7 100644 --- a/consensus/src/block/validity/block_program.rs +++ b/consensus/src/block/validity/block_program.rs @@ -334,6 +334,6 @@ pub mod tests { test_program_snapshot!( BlockProgram, - "2d3fe8ddca93ac8be7f92a53c541f6dbb971ab2817cbc1743c8b5a3b3c99b6caf0102385258a828d" + "f74b4d364a4ec880057f0ca1cde0beff5904ff67d6e0530aa844f92112c6d0cad5ba6097f6732766" ); }