From 62e17796e242f6357f249fbfde1d381e01ad32e6 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:45:55 +0200 Subject: [PATCH 1/4] docs(book): add platform wallet chapter Introduces a new Rust SDK chapter covering the rs-platform-wallet crate: dual-layer (UTXO + Platform identity) architecture, PlatformWalletManager / PlatformWallet / PlatformWalletInfo core types, BDK-style changeset persistence model, identity and asset lock lifecycle, SPV sync and address derivation, FFI integration pointer, and cross-links to platform-addresses, evo-sdk wallet utilities, and the architecture overview. Wires the new page into SUMMARY.md under the existing Rust SDK section. Co-Authored-By: Claude Sonnet 4.6 --- book/src/SUMMARY.md | 1 + book/src/sdk/wallet.md | 420 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 book/src/sdk/wallet.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 0966c69e631..badb36e4493 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -87,6 +87,7 @@ - [Put Operations](sdk/put-operations.md) - [Identity Keys Deep Dive](sdk/identity-keys.md) - [BLAST Sync](sdk/blast-sync.md) +- [Platform Wallet](sdk/wallet.md) # WASM diff --git a/book/src/sdk/wallet.md b/book/src/sdk/wallet.md new file mode 100644 index 00000000000..06d1e1b48c3 --- /dev/null +++ b/book/src/sdk/wallet.md @@ -0,0 +1,420 @@ +# Platform Wallet + +The `rs-platform-wallet` crate (`packages/rs-platform-wallet`) is the Rust wallet +implementation for Dash Platform client applications. It bridges two distinct asset +layers: the Layer 1 UTXO chain and the Layer 2 Platform identity system. + +## Overview + +Traditional Dash wallets track coins — UTXOs, addresses, balances. Platform adds a +second layer on top: identities that hold credits, sign documents, and interact with +decentralized applications. Managing both layers together requires state that neither a +plain key-wallet nor the SDK alone provides. + +`rs-platform-wallet` fills that gap. A single `PlatformWallet` handle gives you: + +- **Layer 1 (Core):** HD accounts, UTXO tracking, address derivation, transaction + broadcast via SPV, and a lock-free balance cache for UI rendering. +- **Layer 2 (Platform):** Multiple managed identities per wallet, asset lock lifecycle + tracking (the on-ramp from UTXO to Platform credits), DPNS name records, DashPay + contact management, and per-platform-address credit balances. + +The design goal is a client that an application can embed without running a full node: +SPV provides chain data, the `dash-sdk` provides proof-verified Platform queries, and +the wallet ties the two together with a changeset-based persistence model that avoids +lock contention. + +For the JavaScript/TypeScript equivalent — offline key derivation utilities used by the +Evo SDK — see [Wallet Utilities](../evo-sdk/wallet-utilities.md). + +## Quick Start + +The example below is derived from `packages/rs-platform-wallet/examples/basic_usage.rs`. +It uses a minimal no-op persister and event handler so you can run it without +infrastructure. + +```rust +use std::sync::Arc; +use dash_sdk::Sdk; +use key_wallet::wallet::initialization::WalletAccountCreationOptions; +use key_wallet::Network; +use platform_wallet::changeset::PlatformWalletPersistence; +use platform_wallet::events::PlatformEventHandler; +use platform_wallet::PlatformWalletManager; + +// 1. Implement PlatformWalletPersistence (no-op for illustration) +struct NoopPersister; +impl PlatformWalletPersistence for NoopPersister { + fn store( + &self, + _wallet_id: platform_wallet::wallet::platform_wallet::WalletId, + _changeset: platform_wallet::changeset::PlatformWalletChangeSet, + ) -> Result<(), platform_wallet::changeset::PersistenceError> { + Ok(()) + } + fn flush( + &self, + _wallet_id: platform_wallet::wallet::platform_wallet::WalletId, + ) -> Result<(), platform_wallet::changeset::PersistenceError> { + Ok(()) + } + fn load( + &self, + ) -> Result { + Ok(platform_wallet::changeset::ClientStartState::default()) + } +} + +// 2. Implement PlatformEventHandler (no-op for illustration) +struct NoopEventHandler; +impl platform_wallet::events::EventHandler for NoopEventHandler {} +impl PlatformEventHandler for NoopEventHandler {} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let sdk = Arc::new(Sdk::new_mock()); + let persister = Arc::new(NoopPersister); + let event_handler: Arc = Arc::new(NoopEventHandler); + + // 3. Create the manager + let manager = PlatformWalletManager::new(sdk, persister, event_handler); + + // 4. Create a wallet from seed bytes (or use create_wallet_from_mnemonic) + let seed_bytes = [0u8; 64]; + let wallet = manager + .create_wallet_from_seed_bytes( + Network::Testnet, + seed_bytes, + WalletAccountCreationOptions::Default, + ) + .await?; + + println!("Wallet ID: {}", hex::encode(wallet.wallet_id())); + + // 5. Read the lock-free balance + let bal = wallet.balance(); + println!("confirmed={} unconfirmed={}", bal.confirmed(), bal.unconfirmed()); + + // 6. Derive a receive address + let addr = wallet.core().next_receive_address_for_account(0).await?; + println!("Receive address: {addr}"); + + // 7. Read wallet state under a read guard + { + let state = wallet.state().await; + println!("Birth height: {:?}", state.core_wallet.birth_height()); + println!("Managed identities: {}", state.identity_manager.identity_count()); + } + + Ok(()) +} +``` + +Production code replaces `Sdk::new_mock()` with a real `SdkBuilder` configuration +(see [Builder Pattern](builder-pattern.md)) and replaces `NoopPersister` with a +database-backed implementation of `PlatformWalletPersistence`. + +## Core Types + +### `PlatformWalletManager` + +`PlatformWalletManager

` is the top-level coordinator. It is generic over a +persistence backend `P: PlatformWalletPersistence`. One manager instance owns: + +- An `Arc` for Platform queries. +- An `Arc>>` that holds all wallets' key + material and per-account UTXO state. +- An `Arc>>>` for lightweight handle + lookup without acquiring the SPV-contended manager lock. +- A `SpvRuntime` that drives block-filter sync across all registered wallets. +- A `PlatformAddressSyncManager` for periodic credit-balance refresh (BLAST sync). + +The manager is not `Clone` — it is meant to be held in a single `Arc` and shared +across threads. + +**Creating wallets:** + +```rust +// From a BIP-39 mnemonic (language auto-detected across all supported wordlists) +let wallet = manager + .create_wallet_from_mnemonic( + "abandon abandon abandon ...", + Network::Testnet, + WalletAccountCreationOptions::Default, + ) + .await?; + +// From raw 64-byte seed material +let wallet = manager + .create_wallet_from_seed_bytes(Network::Testnet, seed, WalletAccountCreationOptions::Default) + .await?; +``` + +### `PlatformWallet` + +`PlatformWallet` is a lightweight, cheaply cloneable handle to a single wallet's +shared state. Clones are **shared references** — not independent copies. All clones +see the same UTXOs, balances, and identities through the `Arc>` inside +`PlatformWalletManager`. + +`PlatformWallet` exposes four sub-wallet facets: + +| Accessor | Type | Purpose | +|----------|------|---------| +| `wallet.core()` | `CoreWallet` | Addresses, UTXOs, transaction broadcast | +| `wallet.identity` | `IdentityWallet` | Identity registration and top-ups | +| `wallet.platform` | `PlatformAddressWallet` | Platform address credit balances | +| `wallet.tokens` | `TokenWallet` | Platform token balances | + +The `wallet.asset_locks()` accessor returns an `Arc` for tracking +the lifecycle of asset lock transactions (the on-ramp from UTXO to Platform credits). + +**Reading state:** Use `wallet.state().await` to acquire a read guard that derefs to +`PlatformWalletInfo`. + +**Lock-free balance:** `wallet.balance()` returns `Arc` without +acquiring any lock. It is updated by the SPV event handler after each block. Use it +for UI rendering where you cannot await. + +### `PlatformWalletInfo` + +`PlatformWalletInfo` is the mutable state struct that lives inside the shared +`WalletManager`. It is not accessed directly by application code — you reach it +through the `wallet.state()` guard. Its fields are: + +```rust +pub struct PlatformWalletInfo { + pub core_wallet: ManagedWalletInfo, // accounts, UTXOs, transaction history + pub balance: Arc, // lock-free balance cache + pub identity_manager: IdentityManager, + pub tracked_asset_locks: BTreeMap, + pub token_watched: BTreeMap>, + pub token_balances: BTreeMap<(Identifier, Identifier), TokenAmount>, +} +``` + +`ManagedWalletInfo` (from `key-wallet`) holds the HD accounts. `IdentityManager` +holds all `ManagedIdentity` records. The token maps index by `(contract_id, +identity_id)`. + +## Wallet Lifecycle + +### Creation + +Both creation paths — mnemonic and seed bytes — end up calling `register_wallet` +internally. That function: + +1. Creates a `ManagedWalletInfo` from the `Wallet` key material. +2. Inserts the new wallet into the `WalletManager` under a write lock. +3. Builds a `PlatformWallet` handle with all sub-wallet facets. +4. Loads persisted state from the `PlatformWalletPersistence` backend and applies + any stored changeset. +5. Inserts the handle into the wallets map and returns an `Arc`. + +### Persistence Model + +The crate uses a **BDK-style changeset approach**: every mutation method produces a +`PlatformWalletChangeSet` describing what changed. An external persister consumes +these changesets asynchronously, translating them to storage at its own pace. + +```rust +pub struct PlatformWalletChangeSet { + pub core: Option, + pub identities: Option, + pub asset_locks: Option, + pub platform_addresses: Option, + pub token_balances: Option, + // ... contact changesets +} +``` + +The changeset is **idempotent**: applying the same changeset twice produces the same +result as applying it once. This makes it safe to replay on startup to reconstruct +state. The `Merge` trait (also re-exported from this crate) lets you accumulate +multiple changesets into one before writing to storage. + +**Recovery:** On startup, call `persister.load()` to retrieve a `ClientStartState`, +then pass it to `PlatformWalletManager`'s load path. The manager applies each stored +changeset to restore wallet and identity state without re-fetching from the network. + +See `PERSISTENCE_REDESIGN.md` in `packages/rs-platform-wallet/` for the full design +rationale, including why the previous lock-contention approach was replaced. + +## Identity Flow + +The lifecycle from seed phrase to a funded Platform identity runs through three phases: + +### 1. Register the Identity + +Registration converts a UTXO into an asset lock transaction that funds a new identity +on Platform. The asset lock is created, signed, and broadcast on the Core chain; the +Platform state transition is sent to DAPI once the asset lock is confirmed. + + + +```rust +// Sketch — verify method names against src/wallet/identity/ +use platform_wallet::IdentityFundingMethod; + +let funding = IdentityFundingMethod::UseSpecificUtxo(outpoint); +// wallet.identity.register_identity(funding, keys, sdk).await?; +``` + +### 2. Top Up Credits + +Once an identity exists, you can top it up by burning another UTXO: + + + +```rust +use platform_wallet::TopUpFundingMethod; +// wallet.identity.top_up_identity(identity_id, TopUpFundingMethod::UseAvailableUtxo, sdk).await?; +``` + +### 3. Withdraw Credits + +Credits can be withdrawn back to the Core chain as Dash. The withdrawal creates a +Platform state transition; the Core chain processes it in the next withdrawal cycle. + + + +### Asset Lock Lifecycle + +Asset locks are tracked via `AssetLockManager`. Each lock moves through a state +machine: + +```text +Created → Broadcast → Seen in mempool → InstantLocked / ChainLocked → Spent (on Platform) +``` + +`TrackedAssetLock` holds the current `AssetLockStatus` and the `OutPoint` of the +funding UTXO. When an InstantLock event arrives via SPV, the manager notifies any +`AssetLockManager` waiters that are blocking on confirmation. + +```rust +let asset_locks = wallet.asset_locks(); +let locked = asset_locks.list_tracked_locks_blocking(); +for lock in locked { + println!("outpoint={} status={:?}", lock.outpoint, lock.status); +} +``` + +## Balance and UTXO Tracking + +### SPV Sync + +`SpvRuntime` drives the block-filter sync loop across all registered wallets. It is +started automatically when the first wallet is registered. The sync loop: + +1. Downloads compact block filters from the peer network. +2. Matches filters against each wallet's monitored addresses. +3. Fetches full blocks for matches, extracts relevant transactions. +4. Updates UTXOs, transaction history, and the lock-free `WalletBalance` atomic. +5. Emits a balance-changed event so the UI can re-render without holding a lock. + +### Address Derivation + +Addresses follow BIP-44 derivation. The default account is `0` for standard +payments. A separate account index is used internally for identity funding UTXOs. + +```rust +// Derive the next unused receive address for account 0 +let addr = wallet.core().next_receive_address_for_account(0).await?; + +// Inspect UTXOs under the read guard +let state = wallet.state().await; +let synced_height = state.core_wallet.synced_height(); +if let Some(account) = state.core_wallet.accounts.standard_bip44_accounts.get(&0) { + let utxos = account.spendable_utxos(synced_height); + println!("{} spendable UTXOs", utxos.len()); +} +``` + +For the Platform address system (Bech32m P2PKH/P2SH/Orchard), see +[Platform Addresses](../addresses/platform-addresses.md). Credit balances for +platform addresses are tracked in `PlatformWalletInfo.token_balances` and refreshed +by `PlatformAddressSyncManager`. + +### Transaction Broadcast + +`CoreWallet` holds a `SpvBroadcaster` that routes signed transactions to connected +SPV peers. Asset lock transactions are broadcast through the same path, but tracked +separately by `AssetLockManager` so their confirmation status drives the identity +registration flow. + +## Event Handling + +`PlatformWalletManager` dispatches all SPV and Platform events through a +`PlatformEventManager`, which fans out to every registered `PlatformEventHandler`. +The application-supplied handler passed to `PlatformWalletManager::new` is one +subscriber; two internal handlers are always registered: + +- `LockNotifyHandler` — wakes `AssetLockManager` async waiters when an + InstantLock or ChainLock is seen. +- `BalanceUpdateHandler` — updates the lock-free `WalletBalance` atomics after + each block, using a separate `wallets` map so it never contends with the + SPV `wallet_manager` lock. + +Implement `PlatformEventHandler` to react to balance changes, lock events, identity +sync completions, and contact updates: + +```rust +use platform_wallet::events::{EventHandler, PlatformEventHandler}; + +struct MyHandler; +impl EventHandler for MyHandler { + // override event methods as needed +} +impl PlatformEventHandler for MyHandler {} +``` + +## FFI Integration + +`packages/rs-platform-wallet-ffi` exposes the wallet and identity API through a +C-compatible FFI layer, enabling integration with Swift, Kotlin, C++, and any +language that can call C functions. + +The FFI surface uses an opaque **handle-based** model: all Rust objects are kept +alive in a thread-safe handle store; callers receive integer handles and pass them +back to subsequent API calls. Memory is freed by calling the matching `_destroy()` +function. + +**Key entry points:** + +| Function | Purpose | +|----------|---------| +| `platform_wallet_info_create_from_mnemonic` | Create wallet from BIP-39 phrase | +| `platform_wallet_info_create_from_seed` | Create wallet from 64-byte seed | +| `identity_manager_create` | Create a new identity manager | +| `managed_identity_create_from_identity_bytes` | Wrap a DPP-serialized identity | +| `managed_identity_get_balance` | Read identity credit balance | +| `platform_wallet_string_free` | Free C string returned by the library | + +All functions return a `PlatformWalletFFIResult` status code. Check for +`PLATFORM_WALLET_FFI_SUCCESS` before reading output parameters. + +The iOS Swift binding is built on top of this FFI layer and lives at +`packages/swift-sdk/`. For build instructions, see +[`packages/swift-sdk/BUILD_GUIDE_FOR_AI.md`](../../packages/swift-sdk/BUILD_GUIDE_FOR_AI.md). +For the full FFI API surface and C header details, see the +[`rs-platform-wallet-ffi` README](../../packages/rs-platform-wallet-ffi/README.md). + +## Further Reading + +- [Platform Addresses](../addresses/platform-addresses.md) — Bech32m address types + used for Platform credit tracking. +- [Wallet Utilities (Evo SDK)](../evo-sdk/wallet-utilities.md) — JavaScript/TypeScript + offline key derivation utilities for the same key hierarchy. +- [Architecture Overview](../architecture/overview.md) — Where `rs-platform-wallet` + sits in the full crate map. +- [Builder Pattern](builder-pattern.md) — How to construct the `Sdk` instance that + `PlatformWalletManager` requires. +- [BLAST Sync](blast-sync.md) — Details of the platform-address credit-balance sync + that `PlatformAddressSyncManager` drives. From 472a0ee78c7567efbf1b18e1adfa18cc322b4899 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:56:47 +0200 Subject: [PATCH 2/4] docs(rs-platform-wallet): rustdoc public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fill in missing rustdoc on public items across the rs-platform-wallet crate so the crate builds clean under `-W missing_docs`. Documented: - crate-level lib.rs and module-level docs for `error`, `wallet`, `wallet/core`, `wallet/tokens`, `spv`. - every variant + structured field on `PlatformWalletError`. - `PlatformWalletInfo` field docs (identity manager, asset locks, token cache). - asset-lock tracking types (`AssetLockStatus`, `TrackedAssetLock`). - key-storage types (`PrivateKeyData`, `IdentityStatus`, `DpnsNameInfo`) with security notes on key material. - core balance accessors (`WalletBalance::confirmed/unconfirmed/...`). - changeset entry types (`PlatformAddressBalanceEntry`, `PlatformAddressChangeSet`, `ClientStartState::is_empty`). - broadcaster trait + DAPI/SPV constructors, persister/bridge ctors, platform-address sync helpers, balance/lock event handlers. - top-level lifecycle entry points (`PlatformWalletManager::new`, `create_wallet_from_seed_bytes`, `create_wallet_from_mnemonic`) with Arguments / Returns / Errors / Security / Examples sections. Documentation only — no signatures, behaviour, or visibility changed. `cargo doc -p platform-wallet` drops from 103 missing-docs warnings to zero with no new intra-doc link warnings introduced; clippy passes with `-D warnings`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../rs-platform-wallet/src/broadcaster.rs | 15 ++++ .../src/changeset/changeset.rs | 8 ++ .../src/changeset/client_start_state.rs | 2 + .../src/changeset/core_bridge.rs | 3 + packages/rs-platform-wallet/src/error.rs | 89 ++++++++++++++++++- packages/rs-platform-wallet/src/lib.rs | 33 ++++++- .../rs-platform-wallet/src/manager/mod.rs | 27 +++++- .../src/manager/wallet_lifecycle.rs | 74 +++++++++++++-- .../src/platform_address_sync.rs | 8 ++ packages/rs-platform-wallet/src/spv/mod.rs | 8 ++ .../wallet/asset_lock/lock_notify_handler.rs | 2 + .../src/wallet/asset_lock/tracked.rs | 18 ++++ .../src/wallet/core/balance.rs | 9 ++ .../src/wallet/core/balance_handler.rs | 3 + .../rs-platform-wallet/src/wallet/core/mod.rs | 3 + .../src/wallet/identity/state/manager/mod.rs | 2 + .../src/wallet/identity/types/key_storage.rs | 21 +++++ packages/rs-platform-wallet/src/wallet/mod.rs | 3 + .../src/wallet/persister.rs | 3 + .../src/wallet/platform_wallet.rs | 8 ++ .../src/wallet/tokens/mod.rs | 3 + 21 files changed, 329 insertions(+), 13 deletions(-) diff --git a/packages/rs-platform-wallet/src/broadcaster.rs b/packages/rs-platform-wallet/src/broadcaster.rs index eba802e0ee9..811dbb6ee67 100644 --- a/packages/rs-platform-wallet/src/broadcaster.rs +++ b/packages/rs-platform-wallet/src/broadcaster.rs @@ -20,6 +20,19 @@ use crate::spv::SpvRuntime; /// Implementations may use DAPI (gRPC), SPV (P2P peers), or Core RPC. #[async_trait] pub trait TransactionBroadcaster: Send + Sync { + /// Push a fully signed transaction to the Dash network. + /// + /// # Returns + /// + /// The transaction id, computed before broadcast — callers can use + /// it to wait for confirmation or InstantSend lock without a round + /// trip back through the broadcaster. + /// + /// # Errors + /// + /// Returns [`PlatformWalletError::TransactionBroadcast`] if the + /// underlying transport rejects the transaction (mempool conflict, + /// network error, peer disconnect, …). async fn broadcast(&self, transaction: &Transaction) -> Result; } @@ -31,6 +44,7 @@ pub struct DapiBroadcaster { } impl DapiBroadcaster { + /// Build a DAPI-backed broadcaster from an `Sdk` handle. pub fn new(sdk: Arc) -> Self { Self { sdk } } @@ -72,6 +86,7 @@ pub struct SpvBroadcaster { } impl SpvBroadcaster { + /// Build an SPV-backed broadcaster from a running [`SpvRuntime`]. pub fn new(spv: Arc) -> Self { Self { spv } } diff --git a/packages/rs-platform-wallet/src/changeset/changeset.rs b/packages/rs-platform-wallet/src/changeset/changeset.rs index 930bfab5285..1635ef76968 100644 --- a/packages/rs-platform-wallet/src/changeset/changeset.rs +++ b/packages/rs-platform-wallet/src/changeset/changeset.rs @@ -460,13 +460,21 @@ impl Merge for ContactChangeSet { /// HD slot it belongs to. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PlatformAddressBalanceEntry { + /// Wallet that owns the underlying HD account. pub wallet_id: WalletId, + /// DIP-17 platform-payment account index. pub account_index: u32, + /// Derivation index inside the account (BIP-32 leaf). pub address_index: u32, + /// The platform P2PKH address that received the funds. pub address: PlatformP2PKHAddress, + /// Funds snapshot for `address` (balance in credits + nonce). pub funds: AddressFunds, } +/// Diff produced by a platform-address sync pass: the addresses whose +/// funds changed plus the watermark that lets the next pass resume +/// incrementally instead of rescanning from genesis. #[derive(Debug, Clone, Default, PartialEq)] pub struct PlatformAddressChangeSet { /// Updated platform addresses produced by the last sync pass. diff --git a/packages/rs-platform-wallet/src/changeset/client_start_state.rs b/packages/rs-platform-wallet/src/changeset/client_start_state.rs index 3a85d0c9915..74a41f0d570 100644 --- a/packages/rs-platform-wallet/src/changeset/client_start_state.rs +++ b/packages/rs-platform-wallet/src/changeset/client_start_state.rs @@ -33,6 +33,8 @@ pub struct ClientStartState { } impl ClientStartState { + /// `true` when no persisted state was returned for any wallet — the + /// platform wallet has nothing to hydrate and should boot fresh. pub fn is_empty(&self) -> bool { self.platform_addresses.is_empty() && self.wallets.is_empty() } diff --git a/packages/rs-platform-wallet/src/changeset/core_bridge.rs b/packages/rs-platform-wallet/src/changeset/core_bridge.rs index c7e80752c8c..9b14c89c510 100644 --- a/packages/rs-platform-wallet/src/changeset/core_bridge.rs +++ b/packages/rs-platform-wallet/src/changeset/core_bridge.rs @@ -27,6 +27,9 @@ pub struct CorePersistenceBridge { } impl CorePersistenceBridge { + /// Wrap a [`PlatformWalletPersistence`] so it can be passed to + /// [`key_wallet_manager::WalletManager::new_with_persister`] as a + /// [`WalletPersistence`] implementor. pub fn new(inner: Arc) -> Self { Self { inner } } diff --git a/packages/rs-platform-wallet/src/error.rs b/packages/rs-platform-wallet/src/error.rs index 006e9b01331..9ee30b0cfc3 100644 --- a/packages/rs-platform-wallet/src/error.rs +++ b/packages/rs-platform-wallet/src/error.rs @@ -1,138 +1,223 @@ +//! Unified error type returned across the public API. + use dpp::identifier::Identifier; use key_wallet::Network; -/// Errors that can occur in platform wallet operations +/// Errors that can occur in platform wallet operations. +/// +/// This is the unified error type returned from public APIs across the +/// crate (manager, wallet, identity, asset lock, broadcaster, SPV). Most +/// variants wrap an opaque `String` describing the underlying failure; +/// structured fields (`Identifier`, `Network`, `account_index`) are used +/// only when callers are expected to branch on the data. #[derive(Debug, thiserror::Error)] pub enum PlatformWalletError { + /// Wallet creation failed (e.g. seed import, account derivation). #[error("Wallet creation failed: {0}")] WalletCreation(String), + /// No wallet with the given identifier is registered with the manager. #[error("Wallet not found: {0}")] WalletNotFound(String), + /// A wallet with the same `WalletId` (derived from the seed) is already + /// registered. Importing the same seed twice would otherwise fork state. #[error("Wallet already exists: {0}")] WalletAlreadyExists(String), + /// An identity with the given identifier is already managed by this + /// wallet (registration replay or duplicate import). #[error("Identity already exists: {0}")] IdentityAlreadyExists(Identifier), + /// No managed identity matches the given identifier. #[error("Identity not found: {0}")] IdentityNotFound(Identifier), + /// An operation that needs a primary (default) identity was attempted + /// before one was set. #[error("No primary identity set")] NoPrimaryIdentity, + /// The supplied identity payload failed validation (missing keys, + /// malformed structure, etc.). #[error("Invalid identity data: {0}")] InvalidIdentityData(String), + /// No contact request found with the given identifier on the wallet's + /// active identity. #[error("Contact request not found: {0}")] ContactRequestNotFound(Identifier), + /// The HD `identity_index` for the given identity has not been set — + /// register the identity (or discover it) before signing. #[error("Identity index not set for identity {0} — register or discover the identity first")] IdentityIndexNotSet(Identifier), + /// A DashPay receiving (incoming-funds) account already exists for the + /// `(identity, contact)` pair on the given network at `account_index`. #[error( "DashPay receiving account already exists for identity {identity} with contact {contact} on network {network:?} (account index {account_index})" )] DashpayReceivingAccountAlreadyExists { + /// The identity that owns the receiving account. identity: Identifier, + /// The contact whose payments would land in this account. contact: Identifier, + /// Network the account was derived for. network: Network, + /// BIP44/DIP-15 account index of the existing account. account_index: u32, }, + /// A DashPay external (outgoing-funds) account already exists for the + /// `(identity, contact)` pair on the given network at `account_index`. #[error( "DashPay external account already exists for identity {identity} with contact {contact} on network {network:?} (account index {account_index})" )] DashpayExternalAccountAlreadyExists { + /// The identity that owns the external account. identity: Identifier, + /// The contact this account sends payments to. contact: Identifier, + /// Network the account was derived for. network: Network, + /// BIP44/DIP-15 account index of the existing account. account_index: u32, }, + /// Building or signing the asset-lock transaction failed. #[error("Asset lock transaction failed: {0}")] AssetLockTransaction(String), + /// The broadcaster reported a failure pushing the transaction onto the + /// network (DAPI / SPV / RPC, depending on the implementation). #[error("Transaction broadcast failed: {0}")] TransactionBroadcast(String), + /// Transaction construction failed before broadcast (insufficient + /// funds, fee estimation, signing, etc.). #[error("Transaction building failed: {0}")] TransactionBuild(String), + /// Waiting for an InstantSend / ChainLock proof on an asset lock + /// timed out or failed before a usable proof became available. #[error("Asset lock proof waiting failed: {0}")] AssetLockProofWait(String), + /// Generic SDK failure surfaced from `dash-sdk` (Drive, DAPI, proof + /// verification, etc.). #[error("SDK error: {0}")] Sdk(#[from] dash_sdk::Error), + /// Platform-address sync (the periodic balance refresh) failed. #[error("Address sync failed: {0}")] AddressSync(String), + /// A platform-address operation (transfer, withdrawal, derivation) + /// failed for a reason that doesn't fit a more specific variant. #[error("Address operation failed: {0}")] AddressOperation(String), + /// The given platform address is not part of this wallet's managed set. #[error("Platform address not found in wallet: {0}")] AddressNotFound(String), + /// HD key derivation failed (path out of range, locked seed, malformed + /// derivation context). #[error("Key derivation failed: {0}")] KeyDerivation(String), + /// The wallet is locked and the requested operation needs the seed. + /// Unlock the wallet (e.g. via `Wallet::unlock`) before retrying. #[error("Wallet is locked — unlock it before performing this operation")] WalletLocked, + /// `start_spv` was called while the SPV runtime was already up. #[error("SPV is already running — stop it before starting again")] SpvAlreadyRunning, + /// SPV cannot start because no wallets have been registered with the + /// manager yet. #[error("No wallets configured — add a wallet before starting SPV")] NoWalletsConfigured, + /// An operation that requires the SPV runtime was attempted while it + /// was stopped. #[error("SPV client is not running")] SpvNotRunning, + /// Generic SPV-layer failure (peer disconnect, header validation, + /// filter mismatch, etc.). #[error("SPV error: {0}")] SpvError(String), + /// Token operation failed (mint, transfer, burn, configuration). #[error("Token operation failed: {0}")] TokenError(String), + /// Timed out waiting for an InstantSend or ChainLock finality proof + /// for the given transaction. #[error("Timed out waiting for finality proof for transaction {0}")] FinalityTimeout(dashcore::Txid), + /// The InstantSend proof is too old to use and no ChainLock proof has + /// been produced yet — the asset lock cannot be redeemed until a + /// ChainLock arrives or the lock is rebuilt. #[error("Asset lock proof expired (IS proof too old, CL not yet available): {0}")] AssetLockExpired(String), + /// The asset-lock transaction has not been ChainLocked, so we can't + /// fall back to a CL proof when the IS proof is rejected. #[error("Asset lock transaction not chain-locked, cannot fall back to CL proof: {0}")] AssetLockNotChainLocked(String), // --- Shielded pool errors (feature-gated) --- + /// No unspent shielded notes are available to fund the requested + /// shielded operation. #[error("No unspent shielded notes available")] ShieldedNoUnspentNotes, + /// The selected shielded notes don't add up to the required amount. #[error("Insufficient shielded balance: available {available}, required {required}")] - ShieldedInsufficientBalance { available: u64, required: u64 }, + ShieldedInsufficientBalance { + /// Total spendable shielded balance, in duffs. + available: u64, + /// Amount the operation needed, in duffs. + required: u64, + }, + /// Building the shielded transaction (proof construction, balance + /// commitment) failed. #[error("Shielded build error: {0}")] ShieldedBuildError(String), + /// Broadcasting a shielded transaction failed. #[error("Shielded broadcast failed: {0}")] ShieldedBroadcastFailed(String), + /// Syncing the shielded note set against the chain failed. #[error("Shielded sync failed: {0}")] ShieldedSyncFailed(String), + /// Updating the shielded commitment tree failed. #[error("Shielded commitment tree update failed: {0}")] ShieldedTreeUpdateFailed(String), + /// The shielded note store reported a failure (read/write/integrity). #[error("Shielded store error: {0}")] ShieldedStoreError(String), + /// Syncing the nullifier set (to detect spent notes) failed. #[error("Shielded nullifier sync failed: {0}")] ShieldedNullifierSyncFailed(String), + /// The Merkle witness needed to spend a shielded note is missing or + /// stale (the commitment tree advanced past the note's anchor). #[error("Shielded Merkle witness unavailable: {0}")] ShieldedMerkleWitnessUnavailable(String), + /// Deriving the shielded spending / viewing keys failed. #[error("Shielded key derivation failed: {0}")] ShieldedKeyDerivation(String), } diff --git a/packages/rs-platform-wallet/src/lib.rs b/packages/rs-platform-wallet/src/lib.rs index 50a28e85f7e..5a0aa9ee862 100644 --- a/packages/rs-platform-wallet/src/lib.rs +++ b/packages/rs-platform-wallet/src/lib.rs @@ -1,4 +1,35 @@ -//! Platform wallet with identity management +//! Platform wallet with identity management. +//! +//! `platform-wallet` ties together a `key-wallet` HD wallet (UTXO state, +//! key derivation), a Dash Platform identity manager, asset locks, +//! platform-address tracking, and an optional SPV runtime into a single +//! coherent abstraction. Most callers should drive it through +//! [`PlatformWalletManager`], which owns one or more +//! [`PlatformWallet`]s and the shared SPV runtime. +//! +//! # Module map +//! +//! - [`manager`] — top-level [`PlatformWalletManager`] (wallet +//! lifecycle, SPV start/stop, accessors). +//! - [`wallet`] — the [`PlatformWallet`] aggregate plus the +//! sub-wallets that make it up: `core`, `identity`, `platform_addresses`, +//! `tokens`, `asset_lock`, and the optional `shielded` pool. +//! - [`changeset`] — delta types persisted on every mutation; the apply +//! path replays them to rebuild in-memory state on startup. +//! - [`broadcaster`] — pluggable [`TransactionBroadcaster`](broadcaster::TransactionBroadcaster) +//! (DAPI or SPV). +//! - [`spv`] — SPV runtime used by the SPV broadcaster and by sync. +//! - [`events`] — fan-out event manager and handlers. +//! - [`platform_address_sync`] — periodic platform-address balance sync +//! coordinator. +//! - [`error`] — the unified [`PlatformWalletError`] type. +//! +//! # Quick start +//! +//! See `examples/basic_usage.rs` for an end-to-end walkthrough — create a +//! manager with `PlatformWalletManager::new`, build a wallet from a seed +//! with `create_wallet_from_seed_bytes`, then read balances or derive +//! receive addresses through the returned [`PlatformWallet`] handle. // The crate's error enum wraps several large variants (SDK errors, DPP // consensus errors, etc.). Shrinking it (e.g. boxing variants) would be a diff --git a/packages/rs-platform-wallet/src/manager/mod.rs b/packages/rs-platform-wallet/src/manager/mod.rs index 55520284a54..08600b38f61 100644 --- a/packages/rs-platform-wallet/src/manager/mod.rs +++ b/packages/rs-platform-wallet/src/manager/mod.rs @@ -42,11 +42,30 @@ pub struct PlatformWalletManager { } impl PlatformWalletManager

{ - /// Create a new PlatformWalletManager. + /// Build a new `PlatformWalletManager`. /// - /// `app_handler` receives all SPV and platform events by reference. - /// Internally, a `LockNotifyHandler` is also registered to wake - /// `AssetLockManager` async waiters on lock events. + /// The manager is created empty: no wallets are registered, the SPV + /// runtime is not yet started, and the platform-address sync loop is + /// idle. Use [`create_wallet_from_seed_bytes`](Self::create_wallet_from_seed_bytes) + /// or [`create_wallet_from_mnemonic`](Self::create_wallet_from_mnemonic) + /// to register a wallet. + /// + /// # Arguments + /// + /// * `sdk` — Dash Platform SDK handle, shared with every wallet the + /// manager creates. + /// * `persister` — backing store for changesets; consult + /// [`PlatformWalletPersistence`] for the trait shape. + /// * `app_handler` — application-level event sink. Receives every + /// SPV and platform event by reference (no cloning). Internally, + /// a [`LockNotifyHandler`] and a [`BalanceUpdateHandler`] are + /// chained alongside it so async asset-lock waiters and lock-free + /// balance atomics stay current without the application handler + /// having to forward those events. + /// + /// # Examples + /// + /// See `examples/basic_usage.rs` for a runnable end-to-end example. pub fn new( sdk: Arc, persister: Arc

, diff --git a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs index bb1faeb0f04..8c36a6bdf72 100644 --- a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs +++ b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs @@ -46,13 +46,44 @@ fn parse_mnemonic_any_language(phrase: &str) -> Result { } impl PlatformWalletManager

{ - /// Create a PlatformWallet from a BIP39 mnemonic phrase. + /// Create a `PlatformWallet` from a BIP-39 mnemonic phrase. /// /// The mnemonic's language is auto-detected by trying each - /// supported BIP-39 wordlist in turn (see - /// [`parse_mnemonic_any_language`]). For passphrase-only flows or - /// out-of-band seed material, derive the seed externally and use + /// supported BIP-39 wordlist in turn (`parse_mnemonic_any_language`, + /// internal). For passphrase-only flows or out-of-band seed + /// material, derive the seed externally and use /// [`Self::create_wallet_from_seed_bytes`]. + /// + /// # Arguments + /// + /// * `mnemonic_phrase` — BIP-39 mnemonic in any of the 10 supported + /// wordlists (English, Spanish, French, Italian, Japanese, + /// Korean, Chinese Simplified/Traditional, Czech, Portuguese). + /// * `network` — `Network::Dash` (mainnet), `Network::Testnet`, or + /// `Network::Devnet` / `Network::Regtest`. + /// * `accounts` — BIP-44 account creation policy + /// (`Default` creates the standard set; supply a + /// [`WalletAccountCreationOptions`] variant to customize). + /// + /// # Returns + /// + /// An `Arc` pointing at the freshly registered + /// wallet. The same handle is also stored inside the manager and + /// can be retrieved by `WalletId` later. + /// + /// # Errors + /// + /// Returns [`PlatformWalletError::WalletCreation`] if the phrase + /// doesn't parse against any supported wordlist or if the + /// underlying `Wallet::from_mnemonic` rejects the seed. + /// + /// # Security + /// + /// The mnemonic phrase is treated as a high-value secret. Callers + /// should pass it in via a buffer they control and zeroize after + /// the call; this function does not hold onto the input string, + /// but the resulting wallet **does** retain seed material in + /// memory (encrypted only when the wallet is locked). pub async fn create_wallet_from_mnemonic( &self, mnemonic_phrase: &str, @@ -70,8 +101,39 @@ impl PlatformWalletManager

{ self.register_wallet(wallet).await } - /// Create a PlatformWallet from raw seed bytes, initialize persisted - /// state, register it with the manager and return an `Arc` handle. + /// Create a `PlatformWallet` from raw 64-byte BIP-39 seed material. + /// + /// Equivalent to [`create_wallet_from_mnemonic`](Self::create_wallet_from_mnemonic) + /// for callers that already own the seed (e.g. derived from a + /// hardware wallet or a passphrase-protected mnemonic). After + /// construction the wallet is registered with the manager, + /// persisted-state hydration runs, and an `Arc` handle is returned. + /// + /// # Arguments + /// + /// * `network` — target Dash network. + /// * `seed_bytes` — 64-byte BIP-39 seed (PBKDF2 output, **not** the + /// raw mnemonic phrase). + /// * `accounts` — BIP-44 account creation policy. + /// + /// # Errors + /// + /// Returns [`PlatformWalletError::WalletCreation`] if the seed is + /// rejected by `Wallet::from_seed_bytes`, if the wallet can't be + /// inserted into the underlying `WalletManager`, or if persisted + /// state hydration fails. + /// + /// # Security + /// + /// The 64-byte seed is taken by value; the caller's copy is moved + /// in and the resulting wallet retains derived key material. + /// Callers handling the seed before this call should keep it in a + /// `zeroize::Zeroizing` wrapper and let it drop immediately + /// afterwards. + /// + /// # Examples + /// + /// See `examples/basic_usage.rs` for a runnable example. pub async fn create_wallet_from_seed_bytes( &self, network: Network, diff --git a/packages/rs-platform-wallet/src/platform_address_sync.rs b/packages/rs-platform-wallet/src/platform_address_sync.rs index d5e093d2ca4..055edf3f8a4 100644 --- a/packages/rs-platform-wallet/src/platform_address_sync.rs +++ b/packages/rs-platform-wallet/src/platform_address_sync.rs @@ -47,6 +47,7 @@ pub enum WalletSyncOutcome { } impl WalletSyncOutcome { + /// Returns `true` if the wallet's sync pass completed successfully. pub fn is_ok(&self) -> bool { matches!(self, WalletSyncOutcome::Ok(_)) } @@ -63,14 +64,18 @@ pub struct PlatformAddressSyncSummary { } impl PlatformAddressSyncSummary { + /// `true` if no wallets were synced in this pass (e.g. a concurrent + /// pass was already in flight, or no wallets are registered yet). pub fn is_empty(&self) -> bool { self.wallet_results.is_empty() } + /// Number of wallets whose sync completed successfully in this pass. pub fn success_count(&self) -> usize { self.wallet_results.values().filter(|o| o.is_ok()).count() } + /// Number of wallets whose sync failed in this pass. pub fn error_count(&self) -> usize { self.wallet_results.len() - self.success_count() } @@ -110,6 +115,9 @@ pub struct PlatformAddressSyncManager { } impl PlatformAddressSyncManager { + /// Build a sync manager bound to the manager's `wallets` map and + /// `event_manager`. The returned manager is **not** running — call + /// [`start`](Self::start) once SPV is up. pub fn new( wallets: Arc>>>, event_manager: Arc, diff --git a/packages/rs-platform-wallet/src/spv/mod.rs b/packages/rs-platform-wallet/src/spv/mod.rs index 53c85dec53d..ca916859f0a 100644 --- a/packages/rs-platform-wallet/src/spv/mod.rs +++ b/packages/rs-platform-wallet/src/spv/mod.rs @@ -1,3 +1,11 @@ +//! SPV runtime wrapper used by [`SpvBroadcaster`](crate::broadcaster::SpvBroadcaster) +//! and the platform-address sync coordinator. +//! +//! The runtime owns the connection to dashcore peers, header chain +//! state, filter sync, and transaction broadcast over P2P. It is built +//! lazily by `PlatformWalletManager::start_spv` and shared across every +//! wallet the manager owns. + mod runtime; pub use runtime::SpvRuntime; diff --git a/packages/rs-platform-wallet/src/wallet/asset_lock/lock_notify_handler.rs b/packages/rs-platform-wallet/src/wallet/asset_lock/lock_notify_handler.rs index ab4336acd3e..d3c1de0892d 100644 --- a/packages/rs-platform-wallet/src/wallet/asset_lock/lock_notify_handler.rs +++ b/packages/rs-platform-wallet/src/wallet/asset_lock/lock_notify_handler.rs @@ -15,6 +15,8 @@ pub struct LockNotifyHandler { } impl LockNotifyHandler { + /// Build a notify-handler that wakes waiters on the given `Notify` + /// when an InstantSend or ChainLock event arrives from SPV. pub fn new(notify: Arc) -> Self { Self { notify } } diff --git a/packages/rs-platform-wallet/src/wallet/asset_lock/tracked.rs b/packages/rs-platform-wallet/src/wallet/asset_lock/tracked.rs index 7939e67d03c..143c89eb249 100644 --- a/packages/rs-platform-wallet/src/wallet/asset_lock/tracked.rs +++ b/packages/rs-platform-wallet/src/wallet/asset_lock/tracked.rs @@ -13,11 +13,20 @@ use key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockFundin use crate::changeset::AssetLockEntry; /// Asset lock status on Core chain. Tracked until consumed, then removed. +/// +/// Lifecycle: `Built` → `Broadcast` → either `InstantSendLocked` (fast +/// path, IS proof available) and/or `ChainLocked` (final fallback). #[derive(Debug, Clone, PartialEq)] pub enum AssetLockStatus { + /// Transaction has been built and signed but not yet broadcast. Built, + /// Transaction has been pushed to the network; awaiting finality. Broadcast, + /// An InstantSend lock has been observed — usable as an asset-lock + /// proof immediately. InstantSendLocked, + /// The transaction is included in a ChainLocked block — usable as a + /// fallback proof when the IS lock isn't available or has aged out. ChainLocked, } @@ -27,12 +36,21 @@ pub enum AssetLockStatus { pub struct TrackedAssetLock { /// The outpoint identifying this credit output (txid + vout). pub out_point: OutPoint, + /// The full asset-lock transaction. Kept around so the proof + /// builder and recovery paths can re-derive outputs without a + /// network round trip. pub transaction: Transaction, /// BIP44 account index that funded this asset lock (UTXO source). pub account_index: u32, + /// Whether this lock was built for identity registration or a + /// top-up of an existing identity (drives the derivation path the + /// signer uses to redeem it). pub funding_type: AssetLockFundingType, + /// HD identity index this lock targets (BIP-9 inner key). pub identity_index: u32, + /// Locked amount, in duffs. pub amount: u64, + /// Current status on the Core chain; see [`AssetLockStatus`]. pub status: AssetLockStatus, /// The proof, available once IS-locked or ChainLocked. pub proof: Option, diff --git a/packages/rs-platform-wallet/src/wallet/core/balance.rs b/packages/rs-platform-wallet/src/wallet/core/balance.rs index db606eb4a7a..c63008aca1b 100644 --- a/packages/rs-platform-wallet/src/wallet/core/balance.rs +++ b/packages/rs-platform-wallet/src/wallet/core/balance.rs @@ -36,6 +36,7 @@ impl Default for WalletBalance { } impl WalletBalance { + /// Construct a fresh, all-zero balance. pub fn new() -> Self { Self { confirmed: AtomicU64::new(0), @@ -45,22 +46,30 @@ impl WalletBalance { } } + /// Confirmed (spendable) balance, in duffs. pub fn confirmed(&self) -> u64 { self.confirmed.load(Ordering::Relaxed) } + /// Mempool / unconfirmed balance, in duffs. Becomes part of + /// [`Self::confirmed`] once the funding transaction is mined. pub fn unconfirmed(&self) -> u64 { self.unconfirmed.load(Ordering::Relaxed) } + /// Immature coinbase balance, in duffs (e.g. masternode rewards + /// still inside the maturity window). pub fn immature(&self) -> u64 { self.immature.load(Ordering::Relaxed) } + /// Funds locked in CoinJoin / asset-lock outputs that are not + /// freely spendable, in duffs. pub fn locked(&self) -> u64 { self.locked.load(Ordering::Relaxed) } + /// Sum of every balance bucket, in duffs. pub fn total(&self) -> u64 { self.confirmed() + self.unconfirmed() + self.immature() + self.locked() } diff --git a/packages/rs-platform-wallet/src/wallet/core/balance_handler.rs b/packages/rs-platform-wallet/src/wallet/core/balance_handler.rs index 5894bb9d385..a5829b70544 100644 --- a/packages/rs-platform-wallet/src/wallet/core/balance_handler.rs +++ b/packages/rs-platform-wallet/src/wallet/core/balance_handler.rs @@ -28,6 +28,9 @@ pub struct BalanceUpdateHandler { } impl BalanceUpdateHandler { + /// Build a balance-update handler bound to the manager's `wallets` + /// map. Register the handler on the [`PlatformEventManager`] so SPV + /// `BalanceUpdated` events flow through it. pub fn new(wallets: Arc>>>) -> Self { Self { wallets } } diff --git a/packages/rs-platform-wallet/src/wallet/core/mod.rs b/packages/rs-platform-wallet/src/wallet/core/mod.rs index 106a4108c22..7f1c83264a1 100644 --- a/packages/rs-platform-wallet/src/wallet/core/mod.rs +++ b/packages/rs-platform-wallet/src/wallet/core/mod.rs @@ -1,3 +1,6 @@ +//! Core (UTXO) wallet: balances, addresses, broadcast helpers, and the +//! lock-free [`WalletBalance`] used by UI layers. + pub mod balance; pub mod balance_handler; mod broadcast; diff --git a/packages/rs-platform-wallet/src/wallet/identity/state/manager/mod.rs b/packages/rs-platform-wallet/src/wallet/identity/state/manager/mod.rs index 663e58f0639..b0fe4bb4aa2 100644 --- a/packages/rs-platform-wallet/src/wallet/identity/state/manager/mod.rs +++ b/packages/rs-platform-wallet/src/wallet/identity/state/manager/mod.rs @@ -57,7 +57,9 @@ pub enum IdentityLocation { OutOfWallet, /// The identity lives in `wallet_identities[wallet_id][registration_index]`. InWallet { + /// Wallet that owns the identity (outer bucket key). wallet_id: WalletId, + /// HD registration index inside the wallet bucket (inner key). registration_index: RegistrationIndex, }, } diff --git a/packages/rs-platform-wallet/src/wallet/identity/types/key_storage.rs b/packages/rs-platform-wallet/src/wallet/identity/types/key_storage.rs index 4813dca6de5..90d93535c06 100644 --- a/packages/rs-platform-wallet/src/wallet/identity/types/key_storage.rs +++ b/packages/rs-platform-wallet/src/wallet/identity/types/key_storage.rs @@ -7,6 +7,13 @@ use std::collections::BTreeMap; use zeroize::Zeroizing; /// How a private key is stored/resolved. +/// +/// # Security +/// +/// `Clear` material is zeroized on drop via [`Zeroizing`]. Prefer +/// [`AtWalletDerivationPath`](Self::AtWalletDerivationPath) wherever +/// possible — that variant carries no key bytes at all and lets the +/// signer re-derive on demand from the encrypted wallet seed. #[derive(Debug, Clone, PartialEq, Eq)] pub enum PrivateKeyData { /// Raw key bytes in memory (zeroized on drop). @@ -16,7 +23,9 @@ pub enum PrivateKeyData { /// materialized `derivation_path` so callers that need either /// form get it without reparsing. AtWalletDerivationPath { + /// Wallet that owns the seed used for derivation. wallet_id: [u8; 32], + /// Fully materialized BIP-32 derivation path. derivation_path: DerivationPath, /// DIP-9 identity index. identity_index: u32, @@ -28,18 +37,30 @@ pub enum PrivateKeyData { /// Identity lifecycle status on Platform. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum IdentityStatus { + /// Status has not been determined yet (e.g. fresh import, no sync + /// has confirmed presence on Platform). #[default] Unknown, + /// Registration state transition has been broadcast; the identity + /// is awaiting confirmation. PendingCreation, + /// Identity is registered and confirmed on Platform. Active, + /// Registration was attempted and failed terminally (e.g. asset + /// lock proof rejected). The identity will not appear on chain. FailedCreation, + /// Platform confirmed the identity does not exist (lookup miss + /// after registration window closed). NotFound, } /// DPNS username associated with an identity. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DpnsNameInfo { + /// The DPNS label registered for the identity (e.g. `"alice"` for + /// `alice.dash`). pub label: String, + /// Unix-second timestamp the name was acquired, when known. pub acquired_at: Option, } diff --git a/packages/rs-platform-wallet/src/wallet/mod.rs b/packages/rs-platform-wallet/src/wallet/mod.rs index 9ff83211147..f542cf2cee0 100644 --- a/packages/rs-platform-wallet/src/wallet/mod.rs +++ b/packages/rs-platform-wallet/src/wallet/mod.rs @@ -1,3 +1,6 @@ +//! [`PlatformWallet`] aggregate plus its sub-wallets (core, identity, +//! platform addresses, tokens, asset locks, optional shielded pool). + pub mod apply; pub mod asset_lock; pub mod core; diff --git a/packages/rs-platform-wallet/src/wallet/persister.rs b/packages/rs-platform-wallet/src/wallet/persister.rs index 2bd787e2efa..1e92f84abdd 100644 --- a/packages/rs-platform-wallet/src/wallet/persister.rs +++ b/packages/rs-platform-wallet/src/wallet/persister.rs @@ -23,6 +23,9 @@ pub struct WalletPersister { } impl WalletPersister { + /// Build a per-wallet persistence handle that binds `wallet_id` to + /// the shared `inner` persister. Subsequent `store`/`flush` calls go + /// through `inner` with `wallet_id` already attached. pub fn new(wallet_id: WalletId, inner: Arc) -> Self { Self { wallet_id, inner } } diff --git a/packages/rs-platform-wallet/src/wallet/platform_wallet.rs b/packages/rs-platform-wallet/src/wallet/platform_wallet.rs index 114fb291ec0..df5e25f1a0c 100644 --- a/packages/rs-platform-wallet/src/wallet/platform_wallet.rs +++ b/packages/rs-platform-wallet/src/wallet/platform_wallet.rs @@ -40,9 +40,17 @@ pub struct PlatformWalletInfo { /// Lock-free balance for UI reads. Updated from `ManagedWalletInfo` after /// each SPV block/mempool processing and RPC refresh. pub balance: Arc, + /// Identities owned (or observed) by this wallet — see + /// [`IdentityManager`] for the bucket layout. pub identity_manager: IdentityManager, + /// Live asset-lock transactions tracked from build through + /// finality, keyed by their credit `OutPoint`. pub tracked_asset_locks: BTreeMap, + /// `identity_id -> set` — token contracts each + /// identity is watching, used to gate balance refresh. pub token_watched: BTreeMap>, + /// Cached `(identity_id, contract_id) -> balance` for the watched + /// token positions. pub token_balances: BTreeMap<(Identifier, Identifier), TokenAmount>, } diff --git a/packages/rs-platform-wallet/src/wallet/tokens/mod.rs b/packages/rs-platform-wallet/src/wallet/tokens/mod.rs index 221b3c05f2e..5ab21fa3546 100644 --- a/packages/rs-platform-wallet/src/wallet/tokens/mod.rs +++ b/packages/rs-platform-wallet/src/wallet/tokens/mod.rs @@ -1,3 +1,6 @@ +//! Token wallet — state-transition helpers for token mint, transfer, +//! burn, and balance refresh. + mod wallet; pub use wallet::TokenWallet; From 33ed6760327da9a2dbd2dfd6f45818092e209a9b Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:28:39 +0200 Subject: [PATCH 3/4] docs(book): resolve wallet identity flow stubs Replace three HTML comments in the Identity Flow section with verified prose and working code samples derived directly from packages/rs-platform-wallet/src/wallet/identity/network/. All method names, signatures, and type names match the current source. Co-Authored-By: Claude Sonnet 4.6 --- book/src/sdk/wallet.md | 136 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 120 insertions(+), 16 deletions(-) diff --git a/book/src/sdk/wallet.md b/book/src/sdk/wallet.md index 06d1e1b48c3..434d2a59f2c 100644 --- a/book/src/sdk/wallet.md +++ b/book/src/sdk/wallet.md @@ -251,29 +251,94 @@ Registration converts a UTXO into an asset lock transaction that funds a new ide on Platform. The asset lock is created, signed, and broadcast on the Core chain; the Platform state transition is sent to DAPI once the asset lock is confirmed. - +Registration is handled through `IdentityWallet`, which is accessed via +`platform_wallet.identity()`. The convenience method funds the identity directly from +wallet UTXOs: ```rust -// Sketch — verify method names against src/wallet/identity/ -use platform_wallet::IdentityFundingMethod; +use platform_wallet::PlatformWallet; + +let identity = platform_wallet + .identity() + .register_identity( + 100_000_000, // amount_duffs + 0, // identity_index (BIP-9 hardened path) + 3, // key_count + None, // settings (PutSettings) + ) + .await?; +``` -let funding = IdentityFundingMethod::UseSpecificUtxo(outpoint); -// wallet.identity.register_identity(funding, keys, sdk).await?; +For explicit control over the funding source, use `register_identity_with_funding`: + +```rust +use platform_wallet::{IdentityFundingMethod, PlatformWallet}; + +let funding = IdentityFundingMethod::FundWithWallet { amount_duffs: 100_000_000 }; +// or: IdentityFundingMethod::UseAssetLock { proof, private_key } + +let identity = platform_wallet + .identity() + .register_identity_with_funding(funding, 0, 3, None) + .await?; +``` + +For external-signer setups (hardware wallets, watch-only wallets, or iOS/Swift +integrations) use `register_identity_with_funding_external_signer`. This is the +**recommended path** for new code — the internal `IdentitySigner` variant is being +deprecated due to incompatibility with watch-only wallets and potential Tokio +worker deadlocks: + +```rust +use platform_wallet::{IdentityFundingMethod, PlatformWallet}; +use dpp::identity::signer::Signer; + +let funding = IdentityFundingMethod::FundWithWallet { amount_duffs: 100_000_000 }; + +let identity = platform_wallet + .identity() + .register_identity_with_funding_external_signer( + funding, + 0, // identity_index + 3, // key_count + &my_signer, + None, // settings + ) + .await?; ``` ### 2. Top Up Credits -Once an identity exists, you can top it up by burning another UTXO: +Once an identity exists, you can add more credits to it by locking another UTXO. The +convenience method selects UTXOs automatically: + +```rust +use platform_wallet::PlatformWallet; + +platform_wallet + .identity() + .top_up_identity( + &identity_id, // &Identifier + 1, // topup_index (incrementing per top-up) + 50_000_000, // amount_duffs + None, // settings + ) + .await?; +``` - +For explicit control over the funding source, use `top_up_identity_with_funding`. +`TopUpFundingMethod` mirrors `IdentityFundingMethod` with the same two variants: ```rust -use platform_wallet::TopUpFundingMethod; -// wallet.identity.top_up_identity(identity_id, TopUpFundingMethod::UseAvailableUtxo, sdk).await?; +use platform_wallet::{TopUpFundingMethod, PlatformWallet}; + +let funding = TopUpFundingMethod::FundWithWallet { amount_duffs: 50_000_000 }; +// or: TopUpFundingMethod::UseAssetLock { proof, private_key } + +platform_wallet + .identity() + .top_up_identity_with_funding(&identity_id, funding, 1, None) + .await?; ``` ### 3. Withdraw Credits @@ -281,9 +346,48 @@ use platform_wallet::TopUpFundingMethod; Credits can be withdrawn back to the Core chain as Dash. The withdrawal creates a Platform state transition; the Core chain processes it in the next withdrawal cycle. - +The convenience method looks up the identity in the local `IdentityManager` and signs +using the wallet's internal signer: + +```rust +use platform_wallet::PlatformWallet; +use dashcore::Address as DashAddress; + +platform_wallet + .identity() + .withdraw_credits( + &identity_id, // &Identifier + 10_000_000, // amount (in platform credits) + &to_address, // &DashAddress (Core P2PKH address) + None, // settings + ) + .await?; +``` + +For external-signer setups, use `withdraw_credits_with_external_signer`. This is the +**recommended path** for new code — the same internal `IdentitySigner` deprecation +applies here as for registration: + +```rust +use platform_wallet::PlatformWallet; +use dashcore::Address as DashAddress; + +platform_wallet + .identity() + .withdraw_credits_with_external_signer( + &identity_id, + 10_000_000, + &to_address, + &my_signer, + None, + ) + .await?; +``` + +The `withdraw_credits_with_signer` variant is available for callers that manage +identities outside the `IdentityManager` (for example, evo-tool's +`QualifiedIdentity`): it accepts an `&Identity` and asset-lock key directly and +returns the remaining credit balance as `u64`. ### Asset Lock Lifecycle From afea248e4f8454b28d3bc8c8ee4c3ab46dbceb70 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:08:11 +0200 Subject: [PATCH 4/4] docs(rs-platform-wallet): address PR #3547 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply Copilot and human review feedback on the platform-wallet book chapter and rustdoc: - spv/mod.rs, error.rs: rename non-existent `start_spv` references to the real `SpvRuntime::start` / `spawn_in_background` API obtained via `manager.spv()` / `spv_arc()`. - wallet.md Quick Start: add the missing `WalletInfoInterface` import so the snippet compiles. - wallet.md sub-wallet table: switch field-style accessors (`wallet.identity`, `wallet.platform`, `wallet.tokens`) to the actual method calls (`wallet.identity()`, etc.). - wallet.md PlatformWalletInfo: correct the token-map key ordering to `(identity_id, contract_id)` — the layout used by all call sites. - wallet.md asset-lock snippet: `lock.outpoint` -> `lock.out_point`. - wallet.md SPV Sync: drop the false claim that SPV starts on `register_wallet`; document the explicit `SpvRuntime::start` / `spawn_in_background` requirement. - wallet.md Address Derivation: stop conflating platform-address credits with `token_balances`; point at `PlatformPaymentAddressProvider` and `PlatformAddressSyncManager`. - wallet.md Identity Flow: collapse the long step-by-step code blocks into a method overview with a pointer to `examples/basic_usage.rs` and a note that worked examples for register/top-up/withdraw are follow-up work tracked against PR #3549. - wallet.md: add a new "Private Key Storage" section covering in-process seeds, OS-managed secret storage (Keychain, Android Keystore, libsecret/DPAPI), and external signers (hardware wallets, HSMs, custodial). Co-Authored-By: Claude Opus 4.7 (1M context) --- book/src/sdk/wallet.md | 253 ++++++++------------- packages/rs-platform-wallet/src/error.rs | 3 +- packages/rs-platform-wallet/src/spv/mod.rs | 7 +- 3 files changed, 104 insertions(+), 159 deletions(-) diff --git a/book/src/sdk/wallet.md b/book/src/sdk/wallet.md index 434d2a59f2c..2d7315d5547 100644 --- a/book/src/sdk/wallet.md +++ b/book/src/sdk/wallet.md @@ -37,6 +37,7 @@ infrastructure. use std::sync::Arc; use dash_sdk::Sdk; use key_wallet::wallet::initialization::WalletAccountCreationOptions; +use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use key_wallet::Network; use platform_wallet::changeset::PlatformWalletPersistence; use platform_wallet::events::PlatformEventHandler; @@ -163,9 +164,9 @@ see the same UTXOs, balances, and identities through the `Arc>` insi | Accessor | Type | Purpose | |----------|------|---------| | `wallet.core()` | `CoreWallet` | Addresses, UTXOs, transaction broadcast | -| `wallet.identity` | `IdentityWallet` | Identity registration and top-ups | -| `wallet.platform` | `PlatformAddressWallet` | Platform address credit balances | -| `wallet.tokens` | `TokenWallet` | Platform token balances | +| `wallet.identity()` | `IdentityWallet` | Identity registration and top-ups | +| `wallet.platform()` | `PlatformAddressWallet` | Platform address credit balances | +| `wallet.tokens()` | `TokenWallet` | Platform token balances | The `wallet.asset_locks()` accessor returns an `Arc` for tracking the lifecycle of asset lock transactions (the on-ramp from UTXO to Platform credits). @@ -195,8 +196,8 @@ pub struct PlatformWalletInfo { ``` `ManagedWalletInfo` (from `key-wallet`) holds the HD accounts. `IdentityManager` -holds all `ManagedIdentity` records. The token maps index by `(contract_id, -identity_id)`. +holds all `ManagedIdentity` records. The token maps index by `(identity_id, +contract_id)`. ## Wallet Lifecycle @@ -241,153 +242,93 @@ changeset to restore wallet and identity state without re-fetching from the netw See `PERSISTENCE_REDESIGN.md` in `packages/rs-platform-wallet/` for the full design rationale, including why the previous lock-contention approach was replaced. -## Identity Flow - -The lifecycle from seed phrase to a funded Platform identity runs through three phases: - -### 1. Register the Identity - -Registration converts a UTXO into an asset lock transaction that funds a new identity -on Platform. The asset lock is created, signed, and broadcast on the Core chain; the -Platform state transition is sent to DAPI once the asset lock is confirmed. - -Registration is handled through `IdentityWallet`, which is accessed via -`platform_wallet.identity()`. The convenience method funds the identity directly from -wallet UTXOs: - -```rust -use platform_wallet::PlatformWallet; - -let identity = platform_wallet - .identity() - .register_identity( - 100_000_000, // amount_duffs - 0, // identity_index (BIP-9 hardened path) - 3, // key_count - None, // settings (PutSettings) - ) - .await?; -``` - -For explicit control over the funding source, use `register_identity_with_funding`: - -```rust -use platform_wallet::{IdentityFundingMethod, PlatformWallet}; +## Private Key Storage + +`rs-platform-wallet` is deliberately agnostic about where seed material and signing +keys live. The crate never persists the seed itself: `PlatformWalletPersistence` +captures only the changeset (UTXOs, identity records, balances, watch-only xpubs), +so an embedding application is responsible for choosing a storage strategy that +matches its threat model. Three patterns cover most deployments. + +### In-process seed (development and dApps) + +The simplest setup: load a BIP-39 mnemonic or 64-byte seed in memory and call +`create_wallet_from_mnemonic` / `create_wallet_from_seed_bytes`. The `Wallet` +retains the seed for HD derivation while the process is running. Drop the seed +material as soon as you no longer need it. + +This is appropriate for tests, server-side bots, or dApps where the user explicitly +provides a seed per session. **Do not** ship this pattern to end-user mobile or +desktop apps without an OS-level secret store underneath it. + +### OS-managed secret storage (mobile and desktop) + +For shipped applications, route the seed through the platform's native secret +store. The wallet stays watch-only at rest and the seed is read on-demand for +signing: + +- **iOS / macOS** — store the encoded mnemonic (or seed bytes) in the iOS + Keychain via the Security framework, gated behind biometrics. The Swift SDK + examples in `packages/swift-sdk/SwiftExampleApp` demonstrate the unlock-then-sign + pattern. +- **Android** — use the Android Keystore plus `EncryptedSharedPreferences`. +- **Linux / Windows** — Secret Service (libsecret) or DPAPI / Windows Credential + Manager respectively, fronted by a small Rust crate such as `keyring`. + +The integration shape is the same in all three cases: +1. Persist only public material via `PlatformWalletPersistence` (xpubs, address + pools, identity records). +2. Keep the wallet watch-only in the manager for routine queries and balance + display — no seed material in memory. +3. On a signing path, fetch the seed from the OS secret store, register a fully + keyed wallet for the operation, perform the signing, then drop the in-memory + key material. + +### External signers (hardware wallets, HSMs, custodial) + +Hardware wallets (Trezor, Ledger), remote signers, or custodial services do not +expose private keys at all. Integrate them by implementing the +`dpp::identity::signer::Signer` trait and routing identity operations through the +external-signer variants on `IdentityWallet` (`register_identity_with_funding_external_signer`, +`top_up_identity_with_signer`, `withdraw_credits_with_external_signer`). +For UTXO-side spends, build transactions using the wallet's PSBT helpers and sign +externally before broadcasting through `SpvBroadcaster`. + +This is the **recommended path for production**: the platform wallet handles state, +balances, and changeset persistence, while the signer module isolates key custody. +The internal `IdentitySigner` flow is being phased out for the same reason — it +forces the seed into the wallet process and is incompatible with watch-only setups. -let funding = IdentityFundingMethod::FundWithWallet { amount_duffs: 100_000_000 }; -// or: IdentityFundingMethod::UseAssetLock { proof, private_key } - -let identity = platform_wallet - .identity() - .register_identity_with_funding(funding, 0, 3, None) - .await?; -``` - -For external-signer setups (hardware wallets, watch-only wallets, or iOS/Swift -integrations) use `register_identity_with_funding_external_signer`. This is the -**recommended path** for new code — the internal `IdentitySigner` variant is being -deprecated due to incompatibility with watch-only wallets and potential Tokio -worker deadlocks: - -```rust -use platform_wallet::{IdentityFundingMethod, PlatformWallet}; -use dpp::identity::signer::Signer; - -let funding = IdentityFundingMethod::FundWithWallet { amount_duffs: 100_000_000 }; - -let identity = platform_wallet - .identity() - .register_identity_with_funding_external_signer( - funding, - 0, // identity_index - 3, // key_count - &my_signer, - None, // settings - ) - .await?; -``` - -### 2. Top Up Credits - -Once an identity exists, you can add more credits to it by locking another UTXO. The -convenience method selects UTXOs automatically: - -```rust -use platform_wallet::PlatformWallet; - -platform_wallet - .identity() - .top_up_identity( - &identity_id, // &Identifier - 1, // topup_index (incrementing per top-up) - 50_000_000, // amount_duffs - None, // settings - ) - .await?; -``` - -For explicit control over the funding source, use `top_up_identity_with_funding`. -`TopUpFundingMethod` mirrors `IdentityFundingMethod` with the same two variants: - -```rust -use platform_wallet::{TopUpFundingMethod, PlatformWallet}; - -let funding = TopUpFundingMethod::FundWithWallet { amount_duffs: 50_000_000 }; -// or: TopUpFundingMethod::UseAssetLock { proof, private_key } - -platform_wallet - .identity() - .top_up_identity_with_funding(&identity_id, funding, 1, None) - .await?; -``` - -### 3. Withdraw Credits - -Credits can be withdrawn back to the Core chain as Dash. The withdrawal creates a -Platform state transition; the Core chain processes it in the next withdrawal cycle. - -The convenience method looks up the identity in the local `IdentityManager` and signs -using the wallet's internal signer: - -```rust -use platform_wallet::PlatformWallet; -use dashcore::Address as DashAddress; - -platform_wallet - .identity() - .withdraw_credits( - &identity_id, // &Identifier - 10_000_000, // amount (in platform credits) - &to_address, // &DashAddress (Core P2PKH address) - None, // settings - ) - .await?; -``` - -For external-signer setups, use `withdraw_credits_with_external_signer`. This is the -**recommended path** for new code — the same internal `IdentitySigner` deprecation -applies here as for registration: - -```rust -use platform_wallet::PlatformWallet; -use dashcore::Address as DashAddress; - -platform_wallet - .identity() - .withdraw_credits_with_external_signer( - &identity_id, - 10_000_000, - &to_address, - &my_signer, - None, - ) - .await?; -``` +## Identity Flow -The `withdraw_credits_with_signer` variant is available for callers that manage -identities outside the `IdentityManager` (for example, evo-tool's -`QualifiedIdentity`): it accepts an `&Identity` and asset-lock key directly and -returns the remaining credit balance as `u64`. +The lifecycle from seed phrase to a funded Platform identity runs through three +phases — **register**, **top up**, and **withdraw** — each exposed as a method on +`IdentityWallet` (accessed via `platform_wallet.identity()`): + +- **Register** converts a UTXO into an asset lock transaction that funds a new + identity on Platform. The asset lock is created, signed, and broadcast on the Core + chain; the Platform state transition is sent to DAPI once the asset lock is + confirmed. See `register_identity`, `register_identity_with_funding`, and + `register_identity_with_funding_external_signer`. +- **Top up** locks another UTXO and adds credits to an existing identity. See + `top_up_identity` and `top_up_identity_with_funding`. +- **Withdraw** creates a Platform state transition that returns credits to the Core + chain as Dash. See `withdraw_credits`, `withdraw_credits_with_external_signer`, + and `withdraw_credits_with_signer`. + +Each operation has a convenience variant that funds from wallet UTXOs and signs with +the wallet's internal signer, plus an `_with_funding` / `_external_signer` variant +for explicit control over the funding source and signer. **For new code, prefer the +`_external_signer` variants** — the internal `IdentitySigner` is being deprecated due +to incompatibility with watch-only wallets and potential Tokio worker deadlocks. + +End-to-end code samples for each flow are not yet checked in alongside this chapter; +see [`packages/rs-platform-wallet/examples/basic_usage.rs`][basic_usage] for the +manager bring-up path. Worked examples for register / top-up / withdraw are tracked +as a follow-up against the integration-test framework PR +([dashpay/platform#3549](https://github.com/dashpay/platform/pull/3549)). + +[basic_usage]: https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-platform-wallet/examples/basic_usage.rs ### Asset Lock Lifecycle @@ -406,7 +347,7 @@ funding UTXO. When an InstantLock event arrives via SPV, the manager notifies an let asset_locks = wallet.asset_locks(); let locked = asset_locks.list_tracked_locks_blocking(); for lock in locked { - println!("outpoint={} status={:?}", lock.outpoint, lock.status); + println!("outpoint={} status={:?}", lock.out_point, lock.status); } ``` @@ -414,8 +355,9 @@ for lock in locked { ### SPV Sync -`SpvRuntime` drives the block-filter sync loop across all registered wallets. It is -started automatically when the first wallet is registered. The sync loop: +`SpvRuntime` drives the block-filter sync loop across all registered wallets. After +registering wallets, explicitly start the runtime with `SpvRuntime::start` or +`SpvRuntime::spawn_in_background` to begin syncing. The sync loop: 1. Downloads compact block filters from the peer network. 2. Matches filters against each wallet's monitored addresses. @@ -443,8 +385,9 @@ if let Some(account) = state.core_wallet.accounts.standard_bip44_accounts.get(&0 For the Platform address system (Bech32m P2PKH/P2SH/Orchard), see [Platform Addresses](../addresses/platform-addresses.md). Credit balances for -platform addresses are tracked in `PlatformWalletInfo.token_balances` and refreshed -by `PlatformAddressSyncManager`. +platform addresses are tracked in the platform-address provider state +(`PlatformPaymentAddressProvider`, per-account `found` map) and refreshed by +`PlatformAddressSyncManager`. ### Transaction Broadcast diff --git a/packages/rs-platform-wallet/src/error.rs b/packages/rs-platform-wallet/src/error.rs index 9ee30b0cfc3..cdd1aacb2a5 100644 --- a/packages/rs-platform-wallet/src/error.rs +++ b/packages/rs-platform-wallet/src/error.rs @@ -133,7 +133,8 @@ pub enum PlatformWalletError { #[error("Wallet is locked — unlock it before performing this operation")] WalletLocked, - /// `start_spv` was called while the SPV runtime was already up. + /// `SpvRuntime::start` or `SpvRuntime::spawn_in_background` was called + /// while the SPV runtime was already up. #[error("SPV is already running — stop it before starting again")] SpvAlreadyRunning, diff --git a/packages/rs-platform-wallet/src/spv/mod.rs b/packages/rs-platform-wallet/src/spv/mod.rs index ca916859f0a..a1896adfc9d 100644 --- a/packages/rs-platform-wallet/src/spv/mod.rs +++ b/packages/rs-platform-wallet/src/spv/mod.rs @@ -2,9 +2,10 @@ //! and the platform-address sync coordinator. //! //! The runtime owns the connection to dashcore peers, header chain -//! state, filter sync, and transaction broadcast over P2P. It is built -//! lazily by `PlatformWalletManager::start_spv` and shared across every -//! wallet the manager owns. +//! state, filter sync, and transaction broadcast over P2P. It is obtained +//! lazily from the manager via `spv()` / `spv_arc()` and started with +//! [`SpvRuntime::start`] or [`SpvRuntime::spawn_in_background`], then shared +//! across every wallet the manager owns. mod runtime;