Skip to content

feat(syncing): gate Core spending during initial restore sync - #995

Closed
llbartekll wants to merge 4 commits into
developfrom
codex/initial-restore-core-spend-gate
Closed

feat(syncing): gate Core spending during initial restore sync#995
llbartekll wants to merge 4 commits into
developfrom
codex/initial-restore-core-spend-gate

Conversation

@llbartekll

@llbartekll llbartekll commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • persist a per-wallet initial restore sync state and complete it from wallet-bound SPV events
  • block only new Core-funded spends while a restored wallet is pending, with committed-resume exceptions
  • keep Platform and Shielded flows governed by their own readiness checks
  • update send, BIP70, gift card, identity, internal transfer, and wallet lifecycle paths
  • add store/lifecycle and payment protocol regression coverage

Validation

  • dashpay Debug build for iPhone 17 Simulator: passed
  • staged diff validation: passed
  • dashwallet build: blocked locally because the scheme requires watchOS 26.5 while this machine has watchOS 26.2
  • XCTest execution: blocked before XCTest bootstrap by the local test-host/runtime configuration; the test sources compile in the isolated configuration

Notes

  • existing wallets without a marker remain allowed
  • later catch-up, reconnect, and rescan do not re-arm the restore gate
  • no commit includes the local .tmp-worktrees directory

Summary by CodeRabbit

  • New Features

    • Added per-wallet restore-sync tracking and improved wallet recovery handling.
    • Added clearer synchronization messages and disabled affected actions while spending or asset-lock proofs are unavailable.
    • Improved wallet creation, import, and migration flows.
  • Bug Fixes

    • Corrected sync completion detection and restore-state cleanup.
    • Gift-card merchant actions remain available when wallet syncing is incomplete.
    • Improved validation before payments, identity funding, transfers, and asset-lock recovery.
    • Preserved resumed, non-transparent, and sufficiently funded operations where appropriate.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds wallet-scoped initial restore tracking and shared Core spend and asset-lock proof availability policies. Core-funded payment, identity, transfer, and UI flows now enforce these policies. Wallet-origin APIs replace Boolean import flags, with lifecycle and payment preflight tests added.

Changes

Restore synchronization and Core spend gating

Layer / File(s) Summary
Wallet origin and restore-state lifecycle
DashWallet/Sources/Infrastructure/SwiftDashSDK/*, DashWallet/Sources/Application/Syncyng Activity Monitor/*, DashWallet/Sources/UI/Menu/Security/Wallets/*, DashWalletTests/InitialRestoreSyncStoreTests.swift, DashWallet.xcodeproj/project.pbxproj
Wallet origins replace import flags. Wallet-scoped restore markers persist across lifecycle events. SPV completion updates restore state. Wallet deletion and wiping remove markers.
Payment preflight enforcement
DashWallet/Sources/Models/PaymentProtocol/*, DashWallet/Sources/Models/Transactions/WalletSendService.swift, DashWallet/Sources/Infrastructure/SwiftDashSDK/BIP70PaymentService+App.swift, DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletSending.swift, DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m, DashWalletTests/PaymentProtocolTests.swift, scripts/bip70_manual_test/main.swift
Payment and send paths check Core spend availability before wallet work, authorization, transaction construction, or broadcast. Initial restore failures use dedicated error cases and messages.
Core-funded identity and transfer checks
DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/*, DashWallet/Sources/Infrastructure/SwiftDashSDK/PlatformAddressSyncCoordinator.swift, DashWallet/Sources/Infrastructure/SwiftDashSDK/AssetLockRecoveryService.swift, DashWallet/Sources/UI/Payments/InternalTransfer/ShieldedTransferCoordinator.swift
Fresh Core-funded registrations, purchases, top-ups, recoveries, and transfers require availability. Committed-lock resumes and non-Core operations remain exempt from new Core-spend checks.
Payment and action UI gating
DashWallet/Sources/UI/Payments/*, DashWallet/Sources/UI/DashPay/*, DashWallet/Sources/UI/Home/*, DashWallet/Sources/UI/Explore Dash/*, DashWallet/Sources/UI/Tx/*
Views and view models observe Core spend and asset-lock proof availability. Blocked states update validation, button enablement, recovery actions, and synchronization guidance. Gift-card actions no longer depend on sync completion.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Wallet
  participant SwiftDashSDKHost
  participant InitialRestoreSyncStore
  participant SwiftDashSDKSPVCoordinator
  participant CoreSpendAvailability
  Wallet->>SwiftDashSDKHost: create or restore wallet
  SwiftDashSDKHost->>InitialRestoreSyncStore: record pending restore
  SwiftDashSDKSPVCoordinator->>InitialRestoreSyncStore: record effective completion
  InitialRestoreSyncStore->>CoreSpendAvailability: publish availability change
  CoreSpendAvailability-->>Wallet: enable or block Core-funded actions
Loading
sequenceDiagram
  participant PaymentClient
  participant BIP70PaymentService
  participant CoreSpendAvailability
  participant WalletSendService
  PaymentClient->>BIP70PaymentService: confirm or headless send
  BIP70PaymentService->>CoreSpendAvailability: run preflight
  CoreSpendAvailability-->>BIP70PaymentService: allow or initialRestoreSync
  BIP70PaymentService->>WalletSendService: build and send transaction
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: gating Core spending during initial restore synchronization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/initial-restore-core-spend-gate

Comment @coderabbitai help to get the list of available commands.

@llbartekll llbartekll changed the title Gate Core spending during initial restore sync feat(syncing): gate Core spending during initial restore sync Aug 12, 2026
@llbartekll
llbartekll requested a review from romchornyi August 12, 2026 11:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (7)
DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletSending.swift (1)

22-26: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Two Core-spend preflights map every error to BIP70Error.initialRestoreSync. Both sites wrap CoreSpendAvailability.shared.requireAllowed() in a catch-all. requireAllowed() throws only CoreSpendAvailabilityError.initialRestoreSync today, so the labels are correct now. If CoreSpendAvailability.Decision gains a second blocked case, both sites will report the wrong reason to the user. Match the concrete case instead.

  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletSending.swift#L22-L26: replace the catch-all with catch CoreSpendAvailabilityError.initialRestoreSync and rethrow other errors unchanged.
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/BIP70PaymentService+App.swift#L19-L26: apply the same specific catch inside the coreSpendPreflight closure.
♻️ Proposed fix for both sites
-        do {
-            try await CoreSpendAvailability.shared.requireAllowed()
-        } catch {
-            throw BIP70Error.initialRestoreSync
-        }
+        do {
+            try await CoreSpendAvailability.shared.requireAllowed()
+        } catch CoreSpendAvailabilityError.initialRestoreSync {
+            throw BIP70Error.initialRestoreSync
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletSending.swift`
around lines 22 - 26, Replace the catch-all around
CoreSpendAvailability.shared.requireAllowed() in
SwiftDashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletSending.swift:22-26
with a specific catch for CoreSpendAvailabilityError.initialRestoreSync, while
rethrowing other errors unchanged. Apply the same handling inside the
coreSpendPreflight closure in
DashWallet/Sources/Infrastructure/SwiftDashSDK/BIP70PaymentService+App.swift:19-26.
DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKHost.swift (1)

408-415: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

publish already refreshes CoreSpendAvailability, so line 415 repeats the work.

publish(handles:wallet:) calls CoreSpendAvailability.shared.refresh() at line 894. The explicit call at line 415 runs a second resolution and a second migrateLegacyFlagIfNeeded pass in the same turn. The result is identical, so this is a redundancy, not a defect. Remove one call to keep a single refresh owner.

♻️ Proposed cleanup
         publish(handles: handles, wallet: createdWallet)
-        CoreSpendAvailability.shared.refresh()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKHost.swift` around
lines 408 - 415, Remove the redundant CoreSpendAvailability.shared.refresh()
call after publish(handles:wallet:) in the wallet creation flow; keep the
refresh owned by publish and preserve the existing publish behavior.
DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift (1)

174-176: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider merging this main-queue hop into the existing commit block.

performWipe already runs a DispatchQueue.main.sync block at lines 159-167 for the main-actor commit work. This second hop adds another synchronous main-queue round trip for a single UserDefaults write. Move InitialRestoreSyncStore.shared.removeAll() into the earlier block if the ordering relative to SPVChainResyncMarker.resetForWipe() does not matter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift`
around lines 174 - 176, Update performWipe by moving
InitialRestoreSyncStore.shared.removeAll() into its existing
DispatchQueue.main.sync commit block alongside the main-actor wipe work,
eliminating the second synchronous main-queue hop. Preserve the current ordering
relative to SPVChainResyncMarker.resetForWipe() unless that ordering is
required.
DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift (1)

190-194: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve the SwiftLint warnings on the new declarations.

SwiftLint reports three issues in this hunk: closure end indentation at line 193, attribute placement at line 214, and static_over_final_class at line 214. The repository guidelines require SwiftLint conventions.

♻️ Proposed fix
                 observers.append(notificationCenter.addObserver(
                     forName: name, object: nil, queue: .main) { [weak self] _ in
-                        Task { `@MainActor` in self?.refresh() }
-                    })
+                    Task { `@MainActor` in self?.refresh() }
+                })
-    `@objc` nonisolated class func coreSpendBlockedError() -> NSError? {
+    `@objc`
+    nonisolated static func coreSpendBlockedError() -> NSError? {
         guard blockedSnapshot else { return nil }
         return CoreSpendAvailabilityError.initialRestoreSync as NSError
     }

As per coding guidelines: "Use SwiftFormat and SwiftLint conventions".

Also applies to: 214-214

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift`
around lines 190 - 194, Apply SwiftFormat/SwiftLint conventions in
WalletEnvironment: correct the closing observer closure indentation, place the
declaration attribute in the required position, and resolve
static_over_final_class on the declaration around line 214 by using the
appropriate type design for its static-only API. Preserve the existing observer
behavior and MainActor refresh flow.

Sources: Coding guidelines, Linters/SAST tools

DashWalletTests/PassiveWalletStateUITailTests.swift (1)

169-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the new SwiftLint warnings.

Move InitialRestoreSyncStoreTests to its own test file. Replace the implicitly unwrapped properties with initialized or safely optional test fixtures.

As per coding guidelines: “Use SwiftFormat and SwiftLint conventions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWalletTests/PassiveWalletStateUITailTests.swift` around lines 169 - 172,
Remove the SwiftLint warnings in InitialRestoreSyncStoreTests by moving the test
class into its own test file and replacing the implicitly unwrapped suiteName,
defaults, and store properties with initialized values or safely handled
optionals. Preserve the test fixture setup and behavior while following
SwiftFormat and SwiftLint conventions.

Sources: Coding guidelines, Linters/SAST tools

DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated CoreSpendAvailability observation boilerplate across three view models. SendViewModel, InternalTransferViewModel, and DashSpendPayViewModel each hand-roll the same Combine subscription to mirror CoreSpendAvailability.shared.decision into a local flag and re-trigger their own validation. The shared root cause is the lack of a common helper for this pattern.

  • DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift#L107-118: extract the CoreSpendAvailability.shared.$decision.map(\.isBlocked).removeDuplicates().sink { ... } subscription into a small reusable helper (e.g. a protocol default or a shared Combine extension) that any @MainActor ObservableObject view model can call with its own update closure.
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferViewModel.swift#L550-560: reuse the same helper instead of repeating the identical subscription.
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift#L273-278: reuse the same helper; this site can also drop the redundant .receive(on: RunLoop.main), since CoreSpendAvailability mutations already happen on the main actor.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift` at line 1, Extract
the repeated
CoreSpendAvailability.shared.$decision.map(\.isBlocked).removeDuplicates()
subscription into a reusable helper for `@MainActor` ObservableObject view models,
accepting each caller’s local update closure and preserving its validation
trigger. Replace the duplicated subscriptions in SendViewModel,
InternalTransferViewModel, and DashSpendPayViewModel with the helper, and remove
DashSpendPayViewModel’s redundant receive(on: RunLoop.main).
DashWallet/Sources/Models/PaymentProtocol/BIP70PaymentService.swift (1)

178-185: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Remove the default coreSpendPreflight closure.

The tracked scripts/bip70_manual_test/main.swift factory omits coreSpendPreflight and relies on the no-op default. Remove the default and pass {} explicitly there if the manual test intentionally bypasses the app preflight gate. Production and unit-test construction sites already provide the closure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/Sources/Models/PaymentProtocol/BIP70PaymentService.swift` around
lines 178 - 185, Remove the default value from the coreSpendPreflight parameter
in the PaymentService initializer, making the closure required at all call
sites. Update the tracked bip70_manual_test factory to pass an explicit no-op
closure where it intentionally bypasses preflight, while preserving the existing
closures supplied by production and unit-test callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swift`:
- Around line 491-497: Update the Core availability guard in the registration
coordinator to require Core availability only when both recoveryLock and the
existing identity lookup are absent. Reuse lookupExistingIdentityId (or its
existing result) so pending recoveries with a consumed or removed asset-lock row
reach topUpResumedIdentityIfNeeded, while preserving the later Core top-up check
for resumes that actually need funding.

In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKSPVCoordinator.swift`:
- Around line 66-68: Use SyncingActivityMonitor.state == .syncDone as the sole
restore-completion authority: remove the raw-SPV/progress logic from
isEffectivelyComplete in
SwiftDashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKSPVCoordinator.swift
(lines 66-68), complete InitialRestoreSyncStore from a lifecycle observer of
.syncDone at lines 626-633, and update
DashWalletTests/PassiveWalletStateUITailTests.swift lines 249-255 to verify the
monitor completion contract rather than the raw SPV predicate.

In
`@DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewController.swift`:
- Around line 282-283: Replace the direct-purchase checks in
CreateUsernameViewController with one operation-specific predicate owned by an
`@MainActor` ObservableObject ViewModel or shared policy, matching
startPurchaseUsername’s requireAllowed() conditions (no identity or insufficient
heldCredits). Reuse that predicate for button availability, SyncGateNote
visibility, and coordinator flow at the referenced locations, rather than
applying coreSpendAvailability.isBlocked unconditionally.

In `@DashWallet/Sources/UI/Payments/Pay/SendScreen.swift`:
- Around line 1134-1143: Update SyncGateNote to accept operation-specific
explanatory text instead of hardcoding the Transparent-balance sending message.
Provide appropriate messages at its send, CreateUsernameViewController, and
SDKIdentityProfileSheet call sites so each flow accurately describes the blocked
operation.

---

Nitpick comments:
In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKHost.swift`:
- Around line 408-415: Remove the redundant
CoreSpendAvailability.shared.refresh() call after publish(handles:wallet:) in
the wallet creation flow; keep the refresh owned by publish and preserve the
existing publish behavior.

In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletSending.swift`:
- Around line 22-26: Replace the catch-all around
CoreSpendAvailability.shared.requireAllowed() in
SwiftDashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletSending.swift:22-26
with a specific catch for CoreSpendAvailabilityError.initialRestoreSync, while
rethrowing other errors unchanged. Apply the same handling inside the
coreSpendPreflight closure in
DashWallet/Sources/Infrastructure/SwiftDashSDK/BIP70PaymentService+App.swift:19-26.

In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift`:
- Around line 174-176: Update performWipe by moving
InitialRestoreSyncStore.shared.removeAll() into its existing
DispatchQueue.main.sync commit block alongside the main-actor wipe work,
eliminating the second synchronous main-queue hop. Preserve the current ordering
relative to SPVChainResyncMarker.resetForWipe() unless that ordering is
required.

In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift`:
- Around line 190-194: Apply SwiftFormat/SwiftLint conventions in
WalletEnvironment: correct the closing observer closure indentation, place the
declaration attribute in the required position, and resolve
static_over_final_class on the declaration around line 214 by using the
appropriate type design for its static-only API. Preserve the existing observer
behavior and MainActor refresh flow.

In `@DashWallet/Sources/Models/PaymentProtocol/BIP70PaymentService.swift`:
- Around line 178-185: Remove the default value from the coreSpendPreflight
parameter in the PaymentService initializer, making the closure required at all
call sites. Update the tracked bip70_manual_test factory to pass an explicit
no-op closure where it intentionally bypasses preflight, while preserving the
existing closures supplied by production and unit-test callers.

In `@DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift`:
- Line 1: Extract the repeated
CoreSpendAvailability.shared.$decision.map(\.isBlocked).removeDuplicates()
subscription into a reusable helper for `@MainActor` ObservableObject view models,
accepting each caller’s local update closure and preserving its validation
trigger. Replace the duplicated subscriptions in SendViewModel,
InternalTransferViewModel, and DashSpendPayViewModel with the helper, and remove
DashSpendPayViewModel’s redundant receive(on: RunLoop.main).

In `@DashWalletTests/PassiveWalletStateUITailTests.swift`:
- Around line 169-172: Remove the SwiftLint warnings in
InitialRestoreSyncStoreTests by moving the test class into its own test file and
replacing the implicitly unwrapped suiteName, defaults, and store properties
with initialized values or safely handled optionals. Preserve the test fixture
setup and behavior while following SwiftFormat and SwiftLint conventions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74a9f33b-e231-4504-9ef7-d1cb767b8753

📥 Commits

Reviewing files that changed from the base of the PR and between 3fcfb28 and 8e345b9.

📒 Files selected for processing (30)
  • DashWallet.xcodeproj/project.pbxproj
  • DashWallet/Sources/Application/Syncyng Activity Monitor/SyncingActivityMonitor.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/BIP70PaymentService+App.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/PlatformAddressSyncCoordinator.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKHost.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKKeyMigrator.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKSPVCoordinator.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletCreator.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletSending.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift
  • DashWallet/Sources/Models/PaymentProtocol/BIP70Error.swift
  • DashWallet/Sources/Models/PaymentProtocol/BIP70PaymentService.swift
  • DashWallet/Sources/Models/Transactions/WalletSendService.swift
  • DashWallet/Sources/UI/DashPay/Profile/SDKIdentityProfileSheet.swift
  • DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsView.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift
  • DashWallet/Sources/UI/Menu/Security/Wallets/WalletsScreen.swift
  • DashWallet/Sources/UI/Menu/Security/Wallets/WalletsViewModel.swift
  • DashWallet/Sources/UI/Payments/Amount/Model/Send/SendAmountModel.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferViewModel.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/ShieldedTransferCoordinator.swift
  • DashWallet/Sources/UI/Payments/Pay/SendScreen.swift
  • DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift
  • DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m
  • DashWallet/Sources/UI/Setup/RecoverWallet/DWRecoverWalletCommand.m
  • DashWalletTests/PassiveWalletStateUITailTests.swift
  • DashWalletTests/PaymentProtocolTests.swift
💤 Files with no reviewable changes (1)
  • DashWallet/Sources/UI/Setup/RecoverWallet/DWRecoverWalletCommand.m

Comment thread DashWallet/Sources/UI/Payments/Pay/SendScreen.swift Outdated
@llbartekll

Copy link
Copy Markdown
Contributor Author

Addressed the review in 251c25a43.

Also applied all seven hidden nitpick cleanups: specific BIP70 error mapping, removal of redundant refresh/main-queue work, SwiftLint declaration formatting, a shared blocked-state publisher, explicit BIP70 preflight injection, and moving restore-store tests into a dedicated file without implicitly unwrapped fixtures.

Validation:

  • dashpay Debug build on iPhone 17 Simulator: passed
  • dashpay build-for-testing: passed
  • standalone BIP70 harness: 87 passed, 0 failed
  • project.pbxproj plist validation and git diff --check: passed

The suggestion to complete restore state from the global SyncingActivityMonitor.state was intentionally not applied; the existing shared effective-completion predicate is consumed from the wallet-bound SPV subscription so late events cannot complete another wallet after a switch.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
scripts/bip70_manual_test/main.swift (1)

220-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the new preflight contract in the harness.

Line 220 passes an always-successful closure. The harness cannot detect whether confirmAndSend skips preflight or calls it after buildSignedTransaction. Record a preflight event and assert that it occurs before build. Add a throwing-preflight case that asserts neither build nor broadcast occurs.

Suggested test instrumentation
 func makeService(_ transport: FakeTransport, _ wallet: FakeWallet,
                  receive: FakeReceive = FakeReceive(), auth: FakeAuth = FakeAuth(),
-                 allowUntrusted: Bool = true) -> BIP70PaymentService {
+                 allowUntrusted: Bool = true,
+                 recordPreflight: Bool = false) -> BIP70PaymentService {
     BIP70PaymentService(transport: transport, verifier: PaymentRequestVerifier(), wallet: wallet,
-                        receiveAddress: receive, auth: auth, coreSpendPreflight: {},
+                        receiveAddress: receive, auth: auth,
+                        coreSpendPreflight: {
+                            if recordPreflight { wallet.calls.append("preflight") }
+                        },
                         allowUntrustedUnsigned: allowUntrusted)
 }

-// Call order build → post/ACK → broadcast.
+// Call order preflight → build → post/ACK → broadcast.
...
-    let svc = makeService(t, w)
+    let svc = makeService(t, w, recordPreflight: true)
...
-    check("confirmAndSend order == [build, post, broadcast]", w.calls == ["build", "post", "broadcast"])
+    check(
+        "confirmAndSend order == [preflight, build, post, broadcast]",
+        w.calls == ["preflight", "build", "post", "broadcast"])

Also applies to: 234-242

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bip70_manual_test/main.swift` around lines 220 - 221, Update the
manual-test harness around confirmAndSend to use an instrumented preflight
closure that records a “preflight” event, then assert the event precedes
“build”. Add a separate throwing-preflight case and verify that neither
buildSignedTransaction nor broadcast is invoked, while preserving the existing
successful send assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@scripts/bip70_manual_test/main.swift`:
- Around line 220-221: Update the manual-test harness around confirmAndSend to
use an instrumented preflight closure that records a “preflight” event, then
assert the event precedes “build”. Add a separate throwing-preflight case and
verify that neither buildSignedTransaction nor broadcast is invoked, while
preserving the existing successful send assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e7d61df6-a2ba-410a-bd0e-ccafcb49e71f

📥 Commits

Reviewing files that changed from the base of the PR and between 8e345b9 and 251c25a.

📒 Files selected for processing (17)
  • DashWallet.xcodeproj/project.pbxproj
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/BIP70PaymentService+App.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKHost.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletSending.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift
  • DashWallet/Sources/Models/PaymentProtocol/BIP70PaymentService.swift
  • DashWallet/Sources/UI/DashPay/Profile/SDKIdentityProfileSheet.swift
  • DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewController.swift
  • DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewModel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferViewModel.swift
  • DashWallet/Sources/UI/Payments/Pay/SendScreen.swift
  • DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift
  • DashWalletTests/InitialRestoreSyncStoreTests.swift
  • scripts/bip70_manual_test/main.swift
💤 Files with no reviewable changes (1)
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKHost.swift
🚧 Files skipped from review as they are similar to previous changes (12)
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift
  • DashWallet/Sources/UI/Payments/Pay/SendScreen.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletSending.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/BIP70PaymentService+App.swift
  • DashWallet/Sources/UI/DashPay/Profile/SDKIdentityProfileSheet.swift
  • DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewController.swift
  • DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift
  • DashWallet/Sources/Models/PaymentProtocol/BIP70PaymentService.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferViewModel.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swift

@llbartekll

Copy link
Copy Markdown
Contributor Author

Addressed the incremental CodeRabbit nitpick in 0d45387ce.

The standalone BIP70 harness now records and asserts preflight → build → POST/ACK → broadcast, and includes a throwing .initialRestoreSync preflight case proving that build/sign and broadcast are never invoked. Validation: 88 passed, 0 failed.

@romchornyi

Copy link
Copy Markdown
Contributor

The gate fails open, and one of the two ways it does that looks unintentional.

refresh() resolves to .allowed both when there's no wallet and when store.isPending(walletId:) returns false. For legacy wallets that's deliberate and stated in the PR notes — no marker, no block. But a genuine initial restore whose marker was never written, or was lost, is indistinguishable from a legacy wallet at that call, so it takes the same .allowed branch silently. That's the one case the gate exists for.

migrateLegacyFlagIfNeeded already shows the ordering was thought about — "Crash-safe order: durable scoped marker, sentinel, legacy clear". The question is whether the same guarantee holds at the real restore entry points: is markPending/markImportedIfNeeded committed before the wallet can reach any spend path, and is that write durable across a kill between the two?

If yes, this is closed and worth a line in the code saying so, since the fail-open branch is otherwise indistinguishable from the legacy one. If there's a window where the wallet is spendable and the marker isn't yet persisted, that window is exactly an unblocked restore.

@romchornyi romchornyi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the narrowing is the right call.

The old gate blocked every Core spend until SyncingActivityMonitor.state == .syncDone, which folded in masternode/quorum sync. A mainnet user of ours is sitting on exactly that: Headers/Filters/Blocks at 100%, masternode list stuck at 0, and every send refused even though the UTXO set was fully discovered. The stale-UTXO reasoning behind that gate really only describes the initial restore scan, where the wallet does not yet know its own outputs — a routine catch-up carries the same residual risk as the second after .syncDone. So the gate now matches its actual justification instead of over-reaching.

Also good: the check runs before authentication and before any input reservation, and it covers the paths that matter rather than just the obvious send screen — BIP70, gift cards, identity funding, internal transfer, and sendToContact.

Two things left as comments, neither blocking: the fail-open branch in refresh() (posted separately), and the fact that the 217 lines of new tests have not been executed by anyone — the author reports XCTest could not bootstrap locally, and this repo runs no test job in CI. Worth a local run on a working configuration before merge, given what this code gates.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift`:
- Line 152: Update the AssetLockProofAvailability type documentation to
explicitly justify the lifecycle of its static shared singleton, or introduce a
protocol seam and inject the policy instead. Prefer the smallest change that
satisfies the guideline while preserving existing
AssetLockProofAvailability.shared usage.
- Around line 201-213: Update WalletEnvironment.decision to replace the
masternodes.state == .synced gate with SyncingActivityMonitor’s .syncDone
status, while preserving the boundWalletId/progressWalletId check and existing
height validation. Use the established monitor instance or synchronization
result rather than introducing a separate SPV-state condition.
- Around line 132-136: Update AssetLockProofAvailabilityError.errorDescription
to use operation-neutral wording that applies to transfers, identity
registration, and platform funding, while preserving the existing explanation
that operations become available after masternode list synchronization
completes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f7b4b8e-a6fe-4b6d-accf-0f86e4dfb03e

📥 Commits

Reviewing files that changed from the base of the PR and between 0d45387 and 5f4125f.

📒 Files selected for processing (16)
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/AssetLockRecoveryService.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/PlatformAddressSyncCoordinator.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKSPVCoordinator.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift
  • DashWallet/Sources/UI/DashPay/Profile/SDKIdentityProfileSheet.swift
  • DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewController.swift
  • DashWallet/Sources/UI/Home/Views/CoinJoinMoveFundsSheet.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferConfirmSheet.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferScreen.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferViewModel.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/ShieldedTransferCoordinator.swift
  • DashWallet/Sources/UI/Payments/Pay/SendScreen.swift
  • DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift
  • DashWallet/Sources/UI/Tx/Details/TxDetailViewController.swift
  • DashWalletTests/InitialRestoreSyncStoreTests.swift
🚧 Files skipped from review as they are similar to previous changes (5)
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/PlatformAddressSyncCoordinator.swift
  • DashWallet/Sources/UI/DashPay/Profile/SDKIdentityProfileSheet.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferViewModel.swift
  • DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swift

Comment on lines +132 to +136
var errorDescription: String? {
NSLocalizedString(
"The masternode list is still syncing. Transfers that require InstantSend will be available once it finishes.",
comment: "Asset-lock operation blocked while the masternode list syncs")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use operation-neutral error text.

AssetLockProofAvailabilityError also blocks identity registration and platform funding. The current text says only “Transfers,” so it gives an incorrect action description in those flows.

Proposed fix
-            "The masternode list is still syncing. Transfers that require InstantSend will be available once it finishes.",
+            "The masternode list is still syncing. Operations that require InstantSend will be available once it finishes.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var errorDescription: String? {
NSLocalizedString(
"The masternode list is still syncing. Transfers that require InstantSend will be available once it finishes.",
comment: "Asset-lock operation blocked while the masternode list syncs")
}
var errorDescription: String? {
NSLocalizedString(
"The masternode list is still syncing. Operations that require InstantSend will be available once it finishes.",
comment: "Asset-lock operation blocked while the masternode list syncs")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift`
around lines 132 - 136, Update AssetLockProofAvailabilityError.errorDescription
to use operation-neutral wording that applies to transfers, identity
registration, and platform funding, while preserving the existing explanation
that operations become available after masternode list synchronization
completes.

var isBlocked: Bool { self == .blockedMasternodeSync }
}

static let shared = AssetLockProofAvailability()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the singleton justification or add a protocol seam.

AssetLockProofAvailability.shared is a new Infrastructure singleton. The type documentation describes its policy, but it does not justify the singleton lifecycle. Add that justification or inject the policy through a protocol.

As per coding guidelines: “Do not add a static let shared singleton without a protocol seam or written justification in the type documentation.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift` at
line 152, Update the AssetLockProofAvailability type documentation to explicitly
justify the lifecycle of its static shared singleton, or introduce a protocol
seam and inject the policy instead. Prefer the smallest change that satisfies
the guideline while preserving existing AssetLockProofAvailability.shared usage.

Source: Coding guidelines

Comment on lines +201 to +213
static func decision(
boundWalletId: Data?,
progressWalletId: Data?,
masternodes: MasternodesSubProgress?
) -> Decision {
guard let boundWalletId,
boundWalletId == progressWalletId,
let masternodes,
masternodes.state == .synced,
masternodes.currentHeight > 0,
masternodes.targetHeight > 0,
masternodes.currentHeight >= masternodes.targetHeight
else { return .blockedMasternodeSync }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Replace the SPV-state gate with SyncingActivityMonitor.

Line 209 makes proof availability depend on masternodes.state == .synced. Keep the wallet-ID binding, but derive synchronization completion from SyncingActivityMonitor and .syncDone. The current condition can disagree with the app-wide synchronization contract and leave asset-lock operations blocked or enabled at the wrong time.

As per coding guidelines: “Never gate synchronization on SPV state == .synced; use SyncingActivityMonitor and .syncDone.”

🧰 Tools
🪛 SwiftLint (0.65.0)

[Warning] 204-204: Declaration masternodes contains the term "master" which is not considered inclusive

(inclusive_language)


[Warning] 208-208: Declaration masternodes contains the term "master" which is not considered inclusive

(inclusive_language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletEnvironment.swift`
around lines 201 - 213, Update WalletEnvironment.decision to replace the
masternodes.state == .synced gate with SyncingActivityMonitor’s .syncDone
status, while preserving the boundWalletId/progressWalletId check and existing
height validation. Use the established monitor instance or synchronization
result rather than introducing a separate SPV-state condition.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants