Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
38 changes: 26 additions & 12 deletions packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,7 +65,8 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
/// 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
Expand Down Expand Up @@ -94,7 +96,7 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
/// 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(
Expand Down Expand Up @@ -149,18 +151,30 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
// 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)
}
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let wallet_info = ManagedWalletInfo::from_wallet(&wallet, birth_height);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -665,6 +665,7 @@ public class WalletManager {
handle,
bytes.bindMemory(to: UInt8.self).baseAddress,
size_t(walletBytes.count),
birthHeight,
&walletId,
&error
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
Loading