Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1796,6 +1796,8 @@
CB9000022FE1000000000002 /* CoinbaseTransactionMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9000012FE1000000000001 /* CoinbaseTransactionMetadataTests.swift */; };
CB9100022FE2000000000002 /* CoinbaseTransferAmountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9100012FE2000000000001 /* CoinbaseTransferAmountTests.swift */; };
CB9200022FE3000000000002 /* PassiveWalletStateUITailTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9200012FE3000000000001 /* PassiveWalletStateUITailTests.swift */; };
CB9200042FE3000000000004 /* InitialRestoreSyncStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9200032FE3000000000003 /* InitialRestoreSyncStoreTests.swift */; };
B17000022FE4000000000002 /* PaymentProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17000012FE4000000000001 /* PaymentProtocolTests.swift */; };
CC0000112DUMMYID001234567 /* PiggyCardsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0000062DUMMYID001234567 /* PiggyCardsAPI.swift */; };
CC0000122DUMMYID001234567 /* PiggyCardsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0000062DUMMYID001234567 /* PiggyCardsAPI.swift */; };
CC0000132DUMMYID001234567 /* PiggyCardsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0000072DUMMYID001234567 /* PiggyCardsCache.swift */; };
Expand Down Expand Up @@ -3538,6 +3540,8 @@
CB9000012FE1000000000001 /* CoinbaseTransactionMetadataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinbaseTransactionMetadataTests.swift; sourceTree = "<group>"; };
CB9100012FE2000000000001 /* CoinbaseTransferAmountTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinbaseTransferAmountTests.swift; sourceTree = "<group>"; };
CB9200012FE3000000000001 /* PassiveWalletStateUITailTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassiveWalletStateUITailTests.swift; sourceTree = "<group>"; };
CB9200032FE3000000000003 /* InitialRestoreSyncStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialRestoreSyncStoreTests.swift; sourceTree = "<group>"; };
B17000012FE4000000000001 /* PaymentProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentProtocolTests.swift; sourceTree = "<group>"; };
CC0000062DUMMYID001234567 /* PiggyCardsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiggyCardsAPI.swift; sourceTree = "<group>"; };
CC0000072DUMMYID001234567 /* PiggyCardsCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiggyCardsCache.swift; sourceTree = "<group>"; };
CC0000082DUMMYID001234567 /* PiggyCardsEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiggyCardsEndpoint.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7074,6 +7078,8 @@
CB9100012FE2000000000001 /* CoinbaseTransferAmountTests.swift */,
7A30000130A1000000000001 /* TransactionDirectionTests.swift */,
CB9200012FE3000000000001 /* PassiveWalletStateUITailTests.swift */,
CB9200032FE3000000000003 /* InitialRestoreSyncStoreTests.swift */,
B17000012FE4000000000001 /* PaymentProtocolTests.swift */,
AA0003032CA0F58E00A1B402 /* SwapAddressValidatorTests.swift */,
AA00F1002FF0A10000A1B402 /* ExchangeAddressLookupContextTests.swift */,
AA00100D2CA0B10001A0B10D /* SwapKitQuoteDecodingTests.swift */,
Expand Down Expand Up @@ -10252,6 +10258,8 @@
CB9100022FE2000000000002 /* CoinbaseTransferAmountTests.swift in Sources */,
7A30000230A1000000000002 /* TransactionDirectionTests.swift in Sources */,
CB9200022FE3000000000002 /* PassiveWalletStateUITailTests.swift in Sources */,
CB9200042FE3000000000004 /* InitialRestoreSyncStoreTests.swift in Sources */,
B17000022FE4000000000002 /* PaymentProtocolTests.swift in Sources */,
AA0003042CA0F58E00A1B402 /* SwapAddressValidatorTests.swift in Sources */,
AA00F1012FF0A10000A1B402 /* ExchangeAddressLookupContextTests.swift in Sources */,
AA00100E2CA0B10001A0B10E /* SwapKitQuoteDecodingTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,6 @@ class SyncingActivityMonitor: NSObject, NetworkReachabilityHandling {
@objc
public var state: State = .unknown {
didSet {
if state == .syncDone {
DWGlobalOptions.sharedInstance().isResyncingWallet = false
}

guard oldValue != state else {
return
}
Expand Down Expand Up @@ -289,7 +285,7 @@ extension SyncingActivityMonitor {
// window (progress.rs — overall is Synced only while ALL managers
// are simultaneously Synced). WaitForEvents is also the pre-start
// default, so disambiguate on progress: fully caught up → done.
if sdkProgress >= 0.999 {
if sdkState.isEffectivelyComplete(progress: sdkProgress) {
mapped = .syncDone
} else {
mapped = (state == .syncing) ? .syncing : .unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ extension BIP70PaymentService {
BIP70PaymentService(
wallet: SwiftDashSDKWalletSending(),
receiveAddress: SwiftDashSDKReceiveAddressProvider(),
auth: BIP70SendAuthorizer())
auth: BIP70SendAuthorizer(),
coreSpendPreflight: {
do {
try await CoreSpendAvailability.shared.requireAllowed()
} catch CoreSpendAvailabilityError.initialRestoreSync {
throw BIP70Error.initialRestoreSync
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -484,10 +484,20 @@ final class DWIdentityRegistrationCoordinator: ObservableObject {
let recoveryLock = lookupRegistrationRecoveryLock(
walletId: wallet.walletId,
modelContainer: modelContainer)
let existingIdentityId = lookupExistingIdentityId(
walletId: wallet.walletId,
modelContainer: modelContainer)
if let recoveryLock {
Self.logger.info("🪪 IDENT-COORD :: recoverable Core registration found status=\(recoveryLock.statusRaw, privacy: .public)")
}

// A fresh Core-funded registration creates a new asset lock. A
// persisted recovery lock resumes its exact outpoint and remains
// allowed; any additional top-up is checked separately below.
if recoveryLock == nil, existingIdentityId == nil, fundingSource == .core {
try CoreSpendAvailability.shared.requireAllowed()
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Single-flight guard. The FFI calls we're about to make
// (`registerIdentityWithFunding` / `registerIdentityFromAddresses`
// / `registerDpnsName`) can't be cancelled — `resetState()`
Expand Down Expand Up @@ -604,10 +614,7 @@ final class DWIdentityRegistrationCoordinator: ObservableObject {
let identityId: Identifier
var shouldTopUpRecoveredIdentity = false
do {
if let existingId = lookupExistingIdentityId(
walletId: wallet.walletId,
modelContainer: modelContainer)
{
if let existingId = existingIdentityId {
Self.logger.info("🪪 IDENT-COORD :: recovery — local identity exists at index \(Self.pinnedIdentityIndex, privacy: .public), skipping IdentityCreate")
identityId = existingId
reconcileConsumedRecoveryLock(
Expand Down Expand Up @@ -926,6 +933,17 @@ final class DWIdentityRegistrationCoordinator: ObservableObject {
throw CoordinatorError.noModelContainer
}

// Determine before PIN whether this purchase needs fresh Core
// funding. An already-funded identity buys with Platform credits and
// is not a Core spend.
let requiredCredits = Self.requiredCreditsForUsernamePurchase(priceCredits: priceCredits)
if purchaseRequiresCoreFunding(
requiredCredits: requiredCredits,
walletId: wallet.walletId,
modelContainer: modelContainer) {
try CoreSpendAvailability.shared.requireAllowed()
}

// Single-flight — same rationale as `startCreateUsername`: the
// funding FFI calls race to their terminal even if we stop
// observing, and two funding attempts must never overlap.
Expand Down Expand Up @@ -964,8 +982,6 @@ final class DWIdentityRegistrationCoordinator: ObservableObject {
// same 0.03-DASH headroom a fresh registration funds itself with,
// covering the purchase transition fee (and Core-side asset-lock
// conversion losses).
let headroomDuffs = DWDP_MIN_BALANCE_TO_CREATE_USERNAME
let requiredCredits = priceCredits + headroomDuffs * 1_000
let signer = KeychainSigner(modelContainer: modelContainer)

let identityId: Identifier
Expand Down Expand Up @@ -1441,6 +1457,39 @@ final class DWIdentityRegistrationCoordinator: ObservableObject {
return (try? context.fetch(descriptor))?.first?.identityId
}

/// Whether buying a listed username needs a new transparent asset lock.
/// An existing identity that already holds the price plus fee headroom
/// spends Platform credits only and remains available during Core restore.
func purchaseRequiresCoreFunding(priceCredits: UInt64) -> Bool {
guard let walletId = SwiftDashSDKHost.shared.wallet?.walletId,
let modelContainer = SwiftDashSDKHost.shared.modelContainer else {
return true
}
return purchaseRequiresCoreFunding(
requiredCredits: Self.requiredCreditsForUsernamePurchase(priceCredits: priceCredits),
walletId: walletId,
modelContainer: modelContainer)
}

private static func requiredCreditsForUsernamePurchase(priceCredits: UInt64) -> UInt64 {
priceCredits + DWDP_MIN_BALANCE_TO_CREATE_USERNAME * 1_000
}

private func purchaseRequiresCoreFunding(
requiredCredits: UInt64,
walletId: Data,
modelContainer: ModelContainer
) -> Bool {
guard let identityId = lookupExistingIdentityId(
walletId: walletId,
modelContainer: modelContainer) else {
return true
}
return UsernameMarketplaceService.identityBalanceCredits(
identityId: identityId,
container: modelContainer) < requiredCredits
}

/// Bring a previously-created identity up to the amount this name
/// requires before resuming at DPNS registration.
///
Expand Down Expand Up @@ -1469,6 +1518,9 @@ final class DWIdentityRegistrationCoordinator: ObservableObject {

switch fundingSource {
case .core:
// Resuming the original asset lock is allowed, but this is an
// additional transparent top-up and therefore a NEW Core spend.
try CoreSpendAvailability.shared.requireAllowed()
let roundedShortfallDuffs =
(missingCredits + PlatformPaymentIdentityFundingPolicy.creditsPerDuff - 1)
/ PlatformPaymentIdentityFundingPolicy.creditsPerDuff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,9 @@ public final class PlatformAddressSyncCoordinator: NSObject, ObservableObject {
/// Rust side carves from the single remainder recipient (the wallet's
/// own next unused Platform address).
public func fundFromCore(amountDuffs: UInt64) async throws {
// Defensive boundary for callers that bypass the transfer coordinator.
// A committed lock uses `resumeFundFromCore` and is exempt.
try CoreSpendAvailability.shared.requireAllowed()
let (addressWallet, container, recipient, accountIndex) = try resolveFundEnvironment()
let signer = KeychainSigner(modelContainer: container, network: runningNetwork!)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// - `start(network:)` is idempotent. Re-entering with the same network is
// a no-op (preserves running SPV / BLAST state). A different network
// tears down and rebuilds.
// - `createOrImportWallet(mnemonic:network:isImported:)` is the only path
// - `createOrImportWallet(mnemonic:network:origin:)` is the only path
// that creates wallet rows and stores the mnemonic in WalletStorage.
// - `stop()` releases the manager handle. Wipe-time persisted-row cleanup is
// owned by `PlatformAddressSyncCoordinator` before BLAST stops.
Expand Down Expand Up @@ -367,13 +367,13 @@ final class SwiftDashSDKHost {
/// publishes it as bound. Onboarding's first wallet uses this.
///
/// For adding a wallet ALONGSIDE existing ones without rebinding the
/// active wallet, use `addWallet(mnemonic:isImported:)` instead — this path replaces
/// active wallet, use `addWallet(mnemonic:origin:)` instead — this path replaces
/// the running runtime and is not additive.
@discardableResult
func createOrImportWallet(
mnemonic: String,
network: Network,
isImported: Bool
origin: WalletMaterialOrigin
) async throws -> ManagedPlatformWallet {
guard !mnemonic.isEmpty, Mnemonic.validate(mnemonic) else {
throw HostError.invalidMnemonic
Expand All @@ -394,7 +394,7 @@ final class SwiftDashSDKHost {
// `importedWalletBirthHeight`). Freshly generated
// mnemonics keep nil — nothing can predate them, so
// the scan anchors at the tip.
birthHeight: isImported
birthHeight: origin.scansHistoricalRange
? Self.importedWalletBirthHeight(for: handles.network)
: nil)
} catch {
Expand All @@ -405,17 +405,19 @@ final class SwiftDashSDKHost {
throw error
}

if origin.armsInitialRestoreSync {
InitialRestoreSyncStore.shared.markImportedIfNeeded(walletId: createdWallet.walletId)
}
if let kind = registryNetworkKind(for: network) {
WalletEnvironment.setActiveWalletId(createdWallet.walletId, for: kind)
}
publish(handles: handles, wallet: createdWallet)

let origin = isImported ? "imported" : "created"
Self.logger.info("🪺 HOST :: \(origin, privacy: .public) managed wallet for \(network.rawValue, privacy: .public)")
Self.logger.info("🪺 HOST :: \(String(describing: origin), privacy: .public) managed wallet for \(network.rawValue, privacy: .public)")
return createdWallet
}

/// Outcome of `addWallet(mnemonic:isImported:)`.
/// Outcome of `addWallet(mnemonic:origin:)`.
enum AddWalletResult {
/// The wallet was created and its mnemonic persisted; the running
/// runtime is unchanged (the caller switches to it explicitly).
Expand All @@ -441,7 +443,7 @@ final class SwiftDashSDKHost {
/// `createOrImportWallet` (`createAndPersist`); differs only in that it
/// uses the LIVE manager and does not publish or set-active.
@discardableResult
func addWallet(mnemonic: String, isImported: Bool) async throws -> AddWalletResult {
func addWallet(mnemonic: String, origin: WalletMaterialOrigin) async throws -> AddWalletResult {
guard !mnemonic.isEmpty, Mnemonic.validate(mnemonic) else {
throw HostError.invalidMnemonic
}
Expand All @@ -468,10 +470,14 @@ final class SwiftDashSDKHost {
// Same semantics as `createOrImportWallet`: imports scan
// from the network's import floor, freshly generated
// wallets from the tip.
birthHeight: isImported
birthHeight: origin.scansHistoricalRange
? Self.importedWalletBirthHeight(for: network)
: nil)

if origin.armsInitialRestoreSync {
InitialRestoreSyncStore.shared.markImportedIfNeeded(walletId: createdWallet.walletId)
}

Self.logger.info("🪺 HOST :: added managed wallet for \(network.rawValue, privacy: .public) (additive)")
return .added(walletId: createdWallet.walletId)
}
Expand Down Expand Up @@ -842,6 +848,10 @@ final class SwiftDashSDKHost {
continue
}
do {
let derivedId = try Wallet(
mnemonic: entry.mnemonic,
network: handles.network).id
let wasMissingLocally = handles.manager.wallets[derivedId] == nil
let created = try handles.manager.createWallet(
mnemonic: entry.mnemonic,
network: handles.network,
Expand All @@ -859,6 +869,10 @@ final class SwiftDashSDKHost {
birthHeight: Self.importedWalletBirthHeight(for: handles.network))
if created.walletId != entry.walletId {
try? storage.storeMnemonic(entry.mnemonic, for: created.walletId)
InitialRestoreSyncStore.shared.remove(walletId: entry.walletId)
}
if wasMissingLocally {
InitialRestoreSyncStore.shared.markReconstructed(walletId: created.walletId)
}
} catch {
Self.logger.error("🪺 HOST :: keychain wallet recovery failed for one entry: \(String(describing: error), privacy: .public)")
Expand All @@ -876,6 +890,7 @@ final class SwiftDashSDKHost {
wallet = resolvedWallet
modelContainer = handles.modelContainer
runningNetwork = handles.network
CoreSpendAvailability.shared.refresh()
}

// MARK: - ModelContainer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,7 @@ final class SwiftDashSDKKeyMigrator: NSObject {
}
let sdkWalletId = try createWalletOnHost(
mnemonic: mnemonic,
network: network,
isImported: true)
network: network)
let prefix = sdkWalletId.prefix(4).map { String(format: "%02x", $0) }.joined()
logger.info("🔑 KEYMIG :: migrated \(walletID, privacy: .public) → \(prefix, privacy: .public)… on \(String(describing: network), privacy: .public)")

Expand Down Expand Up @@ -235,8 +234,7 @@ final class SwiftDashSDKKeyMigrator: NSObject {

private static func createWalletOnHost(
mnemonic: String,
network: Network,
isImported: Bool
network: Network
) throws -> Data {
guard !Thread.isMainThread else {
throw MigrationError.hostCreateOnMainThread
Expand All @@ -251,7 +249,7 @@ final class SwiftDashSDKKeyMigrator: NSObject {
result = .success(try await SwiftDashSDKHost.shared.createOrImportWallet(
mnemonic: mnemonic,
network: network,
isImported: isImported
origin: .legacyMigration
).walletId)
} catch {
result = .failure(error)
Expand Down
Loading
Loading