diff --git a/Cargo.lock b/Cargo.lock index 4a46c8e326b..7e3c00895df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1637,7 +1637,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" dependencies = [ "bincode", "bincode_derive", @@ -1648,7 +1648,7 @@ dependencies = [ [[package]] name = "dash-network-seeds" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" dependencies = [ "dash-network", ] @@ -1725,7 +1725,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" dependencies = [ "async-trait", "chrono", @@ -1754,7 +1754,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" dependencies = [ "anyhow", "base64-compat", @@ -1780,12 +1780,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" [[package]] name = "dashcore-rpc" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" dependencies = [ "dashcore-rpc-json", "hex", @@ -1798,7 +1798,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" dependencies = [ "bincode", "dashcore", @@ -1813,7 +1813,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" dependencies = [ "bincode", "dashcore-private", @@ -2867,7 +2867,7 @@ dependencies = [ [[package]] name = "git-state" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" [[package]] name = "glob" @@ -4034,7 +4034,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" dependencies = [ "aes", "async-trait", @@ -4063,7 +4063,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4079,7 +4079,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=647fa9820f3614090e4e5f5f2b709961d68e538b#647fa9820f3614090e4e5f5f2b709961d68e538b" +source = "git+https://github.com/dashpay/rust-dashcore?rev=1860089eddea27f66e5b3e637d18a73ae138478c#1860089eddea27f66e5b3e637d18a73ae138478c" dependencies = [ "async-trait", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 3fbbd5c2552..4f81cab57c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,14 +50,14 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "647fa9820f3614090e4e5f5f2b709961d68e538b" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "647fa9820f3614090e4e5f5f2b709961d68e538b" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "647fa9820f3614090e4e5f5f2b709961d68e538b" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "647fa9820f3614090e4e5f5f2b709961d68e538b" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "647fa9820f3614090e4e5f5f2b709961d68e538b" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "647fa9820f3614090e4e5f5f2b709961d68e538b" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "647fa9820f3614090e4e5f5f2b709961d68e538b" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "647fa9820f3614090e4e5f5f2b709961d68e538b" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "1860089eddea27f66e5b3e637d18a73ae138478c" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "1860089eddea27f66e5b3e637d18a73ae138478c" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "1860089eddea27f66e5b3e637d18a73ae138478c" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "1860089eddea27f66e5b3e637d18a73ae138478c" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "1860089eddea27f66e5b3e637d18a73ae138478c" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "1860089eddea27f66e5b3e637d18a73ae138478c" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "1860089eddea27f66e5b3e637d18a73ae138478c" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "1860089eddea27f66e5b3e637d18a73ae138478c" } tokio-metrics = "0.5" diff --git a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs index e8a38200715..23ac5f3323b 100644 --- a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs +++ b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs @@ -2,6 +2,7 @@ use std::sync::Arc; +use dash_spv::chain::CheckpointManager; use key_wallet::mnemonic::{Language, Mnemonic}; use key_wallet::wallet::initialization::WalletAccountCreationOptions; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; @@ -64,7 +65,8 @@ impl PlatformWalletManager

{ /// anything funded before init is invisible — **but** when SPV is /// not running yet or header state is unavailable (e.g. wallet /// created before the SPV client is started), it falls back to - /// `0`, i.e. a full historical scan from genesis. `Some(0)` + /// the latest network checkpoint height, keeping the scan near + /// the chain head instead of rescanning from genesis. `Some(0)` /// always requests a full historical scan from genesis (use /// sparingly — expensive on long-lived chains, but required when /// an address may have received funds before the wallet was first @@ -94,7 +96,7 @@ impl PlatformWalletManager

{ /// See [`Self::create_wallet_from_mnemonic`] for the /// `birth_height_override` semantics. `None` scans from the /// current SPV tip forward when SPV is running, otherwise from - /// genesis; `Some(h)` is for callers that need to see funding + /// the latest network checkpoint; `Some(h)` is for callers that need to see funding /// deposited before the wallet was registered (e.g. a long-lived /// bank address pre-funded with testnet duffs). pub async fn create_wallet_from_seed_bytes( @@ -149,18 +151,30 @@ impl PlatformWalletManager

{ // persisted id verbatim, so it stays self-consistent across // launches. - // Birth height resolution: explicit override wins; otherwise - // fall back to SPV's confirmed header tip (default for fresh - // wallets — they only need to see funding from now on); 0 if - // SPV isn't running yet. + // Birth height resolution: explicit override wins; otherwise fall back + // to SPV's confirmed header tip (default for fresh wallets — they only + // need to see funding from now on). Before SPV has synced any headers + // the tip is 0, so a brand-new wallet created at startup would otherwise + // anchor at genesis and rescan the whole chain. Fall back to the latest + // hardcoded checkpoint instead, keeping the scan near the chain head. let birth_height: u32 = match birth_height_override { Some(h) => h, - None => self - .spv_manager - .sync_progress() - .await - .and_then(|p| p.headers().ok().map(|h| h.tip_height())) - .unwrap_or(0), + None => { + let tip = self + .spv_manager + .sync_progress() + .await + .and_then(|p| p.headers().ok().map(|h| h.tip_height())) + .unwrap_or(0); + if tip > 0 { + tip + } else { + CheckpointManager::for_network(self.sdk.network) + .last_checkpoint() + .map(|checkpoint| checkpoint.height) + .unwrap_or(0) + } + } }; let wallet_info = ManagedWalletInfo::from_wallet(&wallet, birth_height); diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift index f3f0662b992..540cf0adb8c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift @@ -652,7 +652,7 @@ public class WalletManager { /// - Parameters: /// - walletBytes: The serialized wallet data /// - Returns: The wallet ID of the imported wallet - public func importWallet(from walletBytes: Data) throws -> Data { + public func importWallet(from walletBytes: Data, birthHeight: UInt32 = 0) throws -> Data { guard !walletBytes.isEmpty else { throw KeyWalletError.invalidInput("Wallet bytes cannot be empty") } @@ -665,6 +665,7 @@ public class WalletManager { handle, bytes.bindMemory(to: UInt8.self).baseAddress, size_t(walletBytes.count), + birthHeight, &walletId, &error ) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift index 7e025a8df3b..8c19ad13e58 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift @@ -12,6 +12,7 @@ struct CreateWalletView: View { @State private var walletLabel: String = "" @State private var showImportOption: Bool = false @State private var importMnemonic: String = "" + @State private var importBirthHeight: String = "" @State private var walletPin: String = "" @State private var confirmPin: String = "" @State private var isCreating: Bool = false @@ -195,6 +196,15 @@ struct CreateWalletView: View { } footer: { Text("Enter your 12-word recovery phrase separated by spaces") } + + Section { + TextField("Birth height (optional)", text: $importBirthHeight) + .keyboardType(.numberPad) + } header: { + Text("Birth Height") + } footer: { + Text("Block height the wallet first received funds. Sync anchors at the nearest checkpoint at or before it, avoiding a full-chain scan. Leave empty to scan from genesis.") + } } } .navigationTitle("Create Wallet") @@ -352,19 +362,33 @@ struct CreateWalletView: View { // the user so a partial create isn't reported as // success. var failures: [(network: Network, message: String)] = [] + // For an imported wallet, honour a user-entered birth height: + // sync anchors at the nearest checkpoint at or before it, + // avoiding a full-chain scan. Left empty it falls back to + // genesis (0) so a wallet of unknown age still sees all its + // history. A freshly generated wallet passes nil (tip). + let trimmedImportBirthHeight = importBirthHeight.trimmingCharacters(in: .whitespaces) + let importBirth = UInt32(trimmedImportBirthHeight) ?? 0 + // Birth height is chain-local: a single value can't be + // correct for more than one network, so reject a non-empty + // height when importing to multiple networks. Left blank the + // safe genesis (0) fallback still applies to each network. + if showImportOption, !trimmedImportBirthHeight.isEmpty, selectedNetworks.count > 1 { + struct MultiNetworkBirthHeightUnsupported: LocalizedError { + var errorDescription: String? { + "Birth height is network-specific. Select one network or leave birth height empty when importing to multiple networks." + } + } + throw MultiNetworkBirthHeightUnsupported() + } for net in selectedNetworks { do { let mgr = try walletManagerStore.backgroundManager(for: net) - // An imported mnemonic may already have on-chain - // history (incl. DashPay payments) from before this - // device — scan from genesis (birthHeight 0) so it - // is seen. A freshly generated mnemonic has nothing - // before now, so scan from the tip (nil). let managed = try mgr.createWallet( mnemonic: mnemonicPhrase, network: net, name: walletLabel, - birthHeight: showImportOption ? 0 : nil + birthHeight: showImportOption ? importBirth : nil ) createdWallets.append((net, managed.walletId)) } catch {