Skip to content

feat(swift-sdk): add ZK sync, local Docker support, and account management#3393

Open
QuantumExplorer wants to merge 11 commits intov3.1-devfrom
feat/ios-zk-integration
Open

feat(swift-sdk): add ZK sync, local Docker support, and account management#3393
QuantumExplorer wants to merge 11 commits intov3.1-devfrom
feat/ios-zk-integration

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented Mar 20, 2026

Issue being fixed or feature implemented

Adds ZK shielded sync, local Docker network support, account management UI, and several UX improvements to the iOS SwiftExampleApp.

What was done?

ZK Shielded Sync

  • New ZKSyncService with periodic sync loop (notes + nullifiers every 30s)
  • "ZK Shielded Sync" section in Sync Status view with balance, stats, and controls
  • Persists shielded balance across app launches via UserDefaults

Local Docker Network Support

  • FFI: dash_sdk_create_trusted now auto-detects local/regtest and uses quorum sidecar at localhost:22444
  • UI: Unified "Use Docker Setup" toggle (only visible on Local network, auto-disables on network switch)
  • Default local DAPI address: http://127.0.0.1:2443
  • Local faucet button on Receive screen (calls Core RPC sendtoaddress)
  • Faucet RPC password configurable in Settings

Account Management

  • Restored AddAccountView for adding accounts to wallets (BIP44, BIP32, CoinJoin, Platform Payment, Identity Top-up)
  • Added platformPayment, dashPayReceivingFunds, dashPayExternalAccount to AccountCategory and AccountType
  • Platform Payment accounts listed in wallet detail view
  • CoreWalletManager.addAccount() with proper wallet_add_platform_payment_account() for DIP-17 accounts
  • Local/Regtest option in wallet creation network picker

UX Improvements

  • Collapsible Platform Sync and ZK Sync sections (chevron toggle)
  • Tab persistence across app launches
  • Regtest SPV config (case 2) instead of falling through to testnet
  • Default local Core P2P peer set to 127.0.0.1:20001

Dependency Updates

  • Updated for key-wallet-manager merge into key-wallet crate
  • Removed dashcore std feature (removed upstream)
  • Updated WalletTransactionChecker for new API signature

How Has This Been Tested?

  • Built and run on iOS Simulator (iPhone 17 Pro, iOS 26.3)
  • Verified platform sync works with local Docker (quorum sidecar)
  • Verified ZK sync runs periodic note + nullifier sync
  • Verified account creation UI and Platform Payment account listing
  • Verified Docker toggle only appears on Local network
  • Verified faucet button sends 10 DASH via Core RPC

Breaking Changes

None

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added ZK shielded pool synchronization with UI status display and balance tracking
    • Added in-app account creation workflow for standard, platform payment, and DashPay account types
    • Added local faucet request functionality for Docker-based development environments
    • Persistent tab navigation across app sessions
  • Improvements

    • Enhanced account management UI with account type badges and expanded details
    • Improved local Docker setup configuration for regtest environments

…d nullifier sync

- Create ZKSyncService with periodic sync loop (30s interval)
- Display shielded balance, orchard address, note/nullifier stats in CoreContentView
- Persist balance and address across app launches via UserDefaults
- Wire into UnifiedAppState lifecycle and pull-to-refresh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

The pull request consolidates the key-wallet-manager dependency into key-wallet, removes it from workspace dependencies, updates imports across Rust crates, and adds significant new features to the Swift SDK including ZK shielded pool syncing, new account types, platform payment account management, and UI enhancements for account creation and Docker setup configuration.

Changes

Cohort / File(s) Summary
Dependency Consolidation
Cargo.toml, packages/rs-dpp/Cargo.toml, packages/rs-platform-wallet/Cargo.toml
Removed key-wallet-manager from workspace dependencies and package configurations. Updated core_key_wallet_manager feature in rs-dpp to depend on key-wallet instead. Changed rs-platform-wallet manager feature to empty list. Removed "std" feature flag from dashcore in rs-dpp.
Module Re-exports and Public API Updates
packages/rs-dpp/src/lib.rs, packages/rs-platform-wallet/src/lib.rs, packages/rs-platform-wallet/examples/basic_usage.rs
Removed key_wallet_manager public re-export from rs-dpp. Updated rs-platform-wallet library root and example to re-export key_wallet instead of key_wallet_manager. Changed WalletManager import path in basic_usage.rs example.
Wallet Interface Extensions
packages/rs-platform-wallet/src/platform_wallet_info/wallet_info_interface.rs
Added mark_instant_send_utxos(&mut self, txid: &dashcore::Txid) -> bool method to WalletInfoInterface implementation for PlatformWalletInfo, delegating to underlying wallet_info.
Transaction Checker API Update
packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs
Updated check_core_transaction method signature to accept mutable wallet: &mut Wallet instead of immutable reference and added new is_from_mempool: bool parameter for distinguishing mempool-origin transactions.
SDK Initialization Logic
packages/rs-sdk-ffi/src/sdk.rs
Enhanced dash_sdk_create_trusted to select TrustedHttpContextProvider initialization strategy based on network configuration: uses localized http://127.0.0.1:22444 endpoint for SDKLocal/SDKRegtest networks with specialized logging, retains default behavior for other networks.
Account Type Enumerations
packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/HDWalletModels.swift, packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWalletTypes.swift
Extended AccountCategory and AccountType enums with three new cases (raw values 11–13): dashPayReceivingFunds, dashPayExternalAccount, and platformPayment for supporting DIP-17 platform payment accounts.
Wallet Manager Extensions
packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift
Added addAccount(to:type:index:keyClass:) method and extended account handling for new categories. Updated getAccountDetails and derivationPath logic to support new account types. Expanded getAccounts to enumerate Platform Payment accounts with per-network address queries.
SPV Client Configuration
packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift
Added FFI config case 2 for regtest network (local Docker). Updated default peer address fallback from "127.0.0.1" to "127.0.0.1:20001".
SDK and App State Consolidation
packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift
Updated local platform DAPI endpoint from port 1443 to 2443. Replaced useLocalPlatform/useLocalCore booleans with unified useDockerSetup property. Changed UserDefaults feature flag from "useLocalhostPlatform" to "useDockerSetup". Added migration logic for legacy configuration keys.
ZK Shielded Sync Service
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/ZKSyncService.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift
Introduced new ZKSyncService class for periodic ZK shielded pool syncing with published status properties (isSyncing, shieldedBalance, orchardAddress, sync statistics, lastError). Added integration into UnifiedAppState with startZKSync() and performZKSync() methods running 30-second periodic sync loops.
Account Creation UI
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddAccountView.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift
Added new AddAccountView providing UI workflow for account creation with type/index/keyClass inputs, derivation path previews, and error handling. Extended AccountListView with "+" button triggering sheet-based account creation and account list refresh on completion. Updated AccountRowView to display new account types with appropriate badges and icons.
Platform and ZK Sync UI
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletsContentView.swift
Added collapsible "Platform Sync Status" details section. Introduced new "ZK Shielded Sync" section with status display, shielded balance, orchard address, sync statistics, and action buttons (Sync Now, Clear). Extended refresh action to include ZK sync alongside platform sync.
Local Docker Setup UI
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift
Added faucet request functionality with RPC Basic auth support for Docker setup. Extended CreateWalletView to conditionally show regtest wallet creation option. Updated OptionsView to show "Use Docker Setup" toggle only for regtest network with auto-disable on network switch, added "Faucet RPC Password" field for Docker configuration.
UI State Persistence and Environment
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift
Made RootTab enum String-backed for stable persistence. Added UserDefaults restoration of selected tab with .sync fallback and onChange handler to persist tab selection across sessions. Injected zkSyncService into SwiftUI environment for app-wide access.

Sequence Diagram(s)

sequenceDiagram
    participant UI as SwiftUI View
    participant ZKSync as ZKSyncService
    participant UnifiedState as UnifiedAppState
    participant SDK as Dash SDK
    participant ShieldedService as ShieldedService

    UI->>UnifiedState: performZKSync() (manual trigger or periodic)
    UnifiedState->>ZKSync: performSync(sdk, shieldedService)
    
    alt Check Preconditions
        ZKSync-->>UnifiedState: Return if isSyncing or poolClient nil
    else Proceed
        ZKSync->>ZKSync: Set isSyncing = true
        ZKSync->>SDK: syncNotes()
        SDK->>ShieldedService: Sync notes from pool
        ShieldedService-->>SDK: Return synced notes count
        ZKSync->>SDK: syncNullifiers()
        SDK->>ShieldedService: Sync nullifiers from pool
        ShieldedService-->>SDK: Return spent nullifiers count
        
        ZKSync->>ZKSync: Update shieldedBalance, orchardAddress
        ZKSync->>ZKSync: Update counters and lastSyncTime
        ZKSync->>ZKSync: Persist to UserDefaults
        
        alt Success
            ZKSync->>ZKSync: Clear lastError, Log success
        else Error
            ZKSync->>ZKSync: Set lastError, Log error
        end
        
        ZKSync->>ZKSync: Set isSyncing = false
    end
    
    ZKSync-->>UnifiedState: Complete
    UnifiedState-->>UI: Update published properties
    UI->>UI: Re-render sync status display
Loading
sequenceDiagram
    participant User as User
    participant UI as AddAccountView
    participant WalletManager as CoreWalletManager
    participant SDK as Dash SDK
    participant Wallet as HDWallet

    User->>UI: Select account type, index, keyClass
    User->>UI: Tap "Create Account"
    
    UI->>UI: Validate inputs
    UI->>UI: Set isCreating = true
    
    UI->>WalletManager: addAccount(wallet, type, index, keyClass)
    
    alt type == .platformPayment
        WalletManager->>SDK: addPlatformPaymentAccount(accountIndex, keyClass)
        SDK->>Wallet: Create platform payment account
        Wallet-->>SDK: Account created
    else Other types
        WalletManager->>SDK: addAccount(type, index)
        SDK->>Wallet: Create standard account
        Wallet-->>SDK: Account created
    end
    
    SDK-->>WalletManager: Success or Error
    
    alt Success
        WalletManager-->>UI: Return
        UI->>UI: Set isCreating = false
        UI->>UI: Dismiss sheet
        UI->>UI: Call loadAccounts() to refresh
        UI-->>User: Account list updated
    else Error
        WalletManager-->>UI: Throw WalletError
        UI->>UI: Set isCreating = false, isErrorShown = true
        UI->>UI: Display error alert
        UI-->>User: Error message shown
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 The wallet dances with a new refrain,
Key-manager's role absorbed with care,
ZK shielded pools sync through the main,
New accounts bloom in Docker's fair air,
Tab selections now remembered stay—
Happy code hops through the changing day! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main features added in the changeset: ZK sync, local Docker support, and account management, which align with the primary changes across multiple packages.
Docstring Coverage ✅ Passed Docstring coverage is 90.32% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ios-zk-integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions bot added this to the v3.1.0 milestone Mar 20, 2026
QuantumExplorer and others added 10 commits March 20, 2026 11:43
…ations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use TrustedHttpContextProvider::new_with_url for local/regtest networks,
  connecting to quorum sidecar at localhost:22444 (dashmate Docker default)
- Replace separate "Use Local DAPI" and "Use Local Core" toggles with
  unified "Use Docker Setup" toggle
- Default DAPI address changed to https://127.0.0.1:2443 (envoy gateway)
- Persist selected tab across app launches
- SDK.init uses dash_sdk_create_trusted for all networks (Rust auto-detects local)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… account types

- Restore AddAccountView from feat/platformSync branch for adding accounts to wallets
- Add platformPayment, dashPayReceivingFunds, dashPayExternalAccount to AccountCategory and AccountType
- Use wallet_add_platform_payment_account() for Platform Payment accounts (requires key_class)
- Add CoreWalletManager.addAccount() public method
- Add regtest (case 2) to SPV client network config instead of falling through to testnet
- Default local Core P2P peer to 127.0.0.1:20001 (Docker dashmate port)
- Add Local/Regtest option to wallet creation network picker

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ounts

- Change default local DAPI from https to http (Docker envoy self-signed cert rejected)
- Enumerate platform payment accounts in getAccounts() for wallet detail view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add "Get 10 DASH from Faucet" button on Receive screen (Core tab, Docker mode only)
- Calls seed node Core RPC sendtoaddress at 127.0.0.1:20302
- Add Faucet RPC Password field in Settings when Docker Setup is enabled
- Password persisted in UserDefaults for subsequent faucet requests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove key-wallet-manager workspace dependency (merged into key-wallet)
- Update rs-dpp and rs-platform-wallet to use key-wallet directly
- Remove dashcore "std" feature (removed upstream)
- Update WalletTransactionChecker for new check_core_transaction signature
  (added &mut Wallet and is_from_mempool params)
- Add mark_instant_send_utxos to WalletInfoInterface impl

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e on switch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer changed the title feat(swift-sdk): add ZK shielded sync status UI feat(swift-sdk): add ZK sync, local Docker support, and account management Mar 26, 2026
@QuantumExplorer QuantumExplorer marked this pull request as ready for review March 26, 2026 11:33
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift (1)

44-133: ⚠️ Potential issue | 🟠 Major

Ensure FFI pointers are always released on initializer failure.

If initialization throws after configPtr is allocated (at line 95 during dataDir configuration, line 117 during user agent setup, or line 128 if client creation fails), the FFI config and client pointers leak because cleanup only happens in destroy(), which is only called from deinit when the instance fully initializes. Since the initializer throws before assignment to self, the instance is never created and deinit is never invoked.

Add a defer block at the start of init to ensure failure-path cleanup:

Proposed fix
 init(network: Network = DashSDKNetwork(rawValue: 1), dataDir: String?, startHeight: UInt32) throws {
+    var createdConfig: UnsafeMutablePointer<FFIClientConfig>?
+    var createdClient: UnsafeMutablePointer<FFIDashSpvClient>?
+    var didInitialize = false
+    defer {
+        if !didInitialize {
+            if let createdClient { dash_spv_ffi_client_destroy(createdClient) }
+            if let createdConfig { dash_spv_ffi_config_destroy(createdConfig) }
+        }
+    }
+
     if swiftLoggingEnabled {
         let level = (ProcessInfo.processInfo.environment["SPV_LOG"] ?? "off")
         print("[SPV][Log] Initialized SPV logging level=\(level)")
     }
 
     let configPtr: UnsafeMutablePointer<FFIClientConfig>? = {
         switch network.rawValue {

After configPtr guard (line 63):

     guard let configPtr = configPtr else {
         throw SPVError.configurationFailed
     }
+    createdConfig = configPtr

After client guard (line 129):

     guard let client = client else {
         print("[SPV][Init] Failed to create client: \(SPVClient.getLastDashFFIError())")
         throw SPVError.initializationFailed
     }
+    createdClient = client

At end of init (line 133):

     self.client = client
     config = configPtr
+    didInitialize = true
 }

Per coding guidelines: "Swift code must properly wrap FFI functions with correct memory management across Swift/Rust boundaries".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift` around
lines 44 - 133, The initializer for SPVClient must free FFI resources on all
failure paths; add a defer immediately after unwrapping configPtr in init to
free the config pointer if the initializer throws (call
dash_spv_ffi_config_free(configPtr) from the defer), and when creating the
client handle, if dash_spv_ffi_client_new returns nil or any subsequent throw
occurs ensure any allocated client is freed (dash_spv_ffi_client_free(client))
and the config is freed as well; do this instead of relying on
instance.destroy()/deinit since self is not assigned on init failure. Ensure you
reference SPVClient.init, the local configPtr and client variables, and use the
FFI free functions (dash_spv_ffi_config_free / dash_spv_ffi_client_free) inside
the defer/cleanup paths.
🧹 Nitpick comments (7)
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift (2)

243-317: Consider storing the status-clearing Task to cancel on view dismissal.

The nested Task at lines 313-316 that clears faucetStatus after 5 seconds will continue running even if the view is dismissed. While SwiftUI handles this gracefully (no crash), it's unnecessary work. Consider storing the Task in a property and canceling it in .onDisappear or using a different pattern.

Additionally, the defer { isFaucetLoading = false } at line 248 correctly ensures loading state is cleared, which is good error handling.

💡 Optional: Cancel pending status clear on view dismissal
// Add property
`@State` private var statusClearTask: Task<Void, Never>?

// In requestFromFaucet, replace lines 313-316:
statusClearTask?.cancel()
statusClearTask = Task {
    try? await Task.sleep(nanoseconds: 5_000_000_000)
    if !Task.isCancelled {
        faucetStatus = nil
    }
}

// Add to view body:
.onDisappear {
    statusClearTask?.cancel()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift`
around lines 243 - 317, The nested Task in requestFromFaucet continues running
after the view is dismissed; add a cancellable Task property (e.g. `@State`
private var statusClearTask: Task<Void, Never>?) and replace the inline Task at
the end of requestFromFaucet with: cancel any existing statusClearTask, assign a
new Task that sleeps 5s and checks Task.isCancelled before clearing
faucetStatus; also cancel statusClearTask in the view's .onDisappear to avoid
doing unnecessary work. Ensure you reference requestFromFaucet, faucetStatus,
and the new statusClearTask property when making these changes.

303-303: Consider directing users to Settings for faucet password configuration.

The error message "Auth failed — set faucetRPCPassword in UserDefaults" references UserDefaults directly, but users configure this in Settings. Consider updating to "Auth failed — check Faucet RPC Password in Settings" for better UX.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift`
at line 303, The faucetStatus message in ReceiveAddressView is misleading;
update the assignment that sets faucetStatus (in ReceiveAddressView) from "Auth
failed — set faucetRPCPassword in UserDefaults" to a user-facing string like
"Auth failed — check Faucet RPC Password in Settings" so users are directed to
the Settings UI rather than UserDefaults.
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift (1)

294-299: Unused selectedNetworks array beyond primaryNetwork.

The code builds a selectedNetworks array but only uses the first element. Based on line 305-311 and the comment "SDK enforces unique wallet per mnemonic", only one wallet is created. Consider simplifying to just determine the primary network directly, or add a comment explaining why the array is built (e.g., future multi-network wallet support).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift`
around lines 294 - 299, The code builds a selectedNetworks array but only uses
its first element (primaryNetwork), which is unnecessary; either simplify by
computing primaryNetwork directly from the boolean flags (createForMainnet,
createForTestnet, createForDevnet with shouldShowDevnet, createForRegtest with
shouldShowRegtest) or add a brief comment above selectedNetworks explaining that
the array is intentionally built for potential future multi-network support
while only primaryNetwork is used today by the wallet creation logic (see
selectedNetworks and primaryNetwork usage in CreateWalletView.swift), and remove
compactMap if you simplify.
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/ZKSyncService.swift (2)

98-144: Consider using defer to ensure isSyncing is reset.

While the current placement of isSyncing = false at line 144 works correctly, using defer immediately after setting isSyncing = true would be more idiomatic Swift and defensive against future code changes that might add early returns.

Proposed refactor using defer
         isSyncing = true
+        defer { isSyncing = false }
         lastError = nil

         do {
             // ... sync logic ...
         } catch {
             lastError = error.localizedDescription
             SDKLogger.log(
                 "ZK sync error: \(error.localizedDescription)",
                 minimumLevel: .medium
             )
         }
-
-        isSyncing = false
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/ZKSyncService.swift`
around lines 98 - 144, Move the isSyncing reset into a defer block immediately
after setting isSyncing = true so it always runs even if the function returns
early; i.e., after setting isSyncing = true add defer { isSyncing = false } and
remove the trailing standalone isSyncing = false at the end, keeping the rest of
the logic (calls to poolClient.syncNotes(sdk: sdk),
poolClient.syncNullifiers(sdk: sdk), updates to
notesSynced/totalNotesSynced/nullifiersSpent/totalNullifiersSpent/shieldedBalance/orchardAddress/persistedBalance/persistedOrchardAddress/lastSyncTime/syncCountSinceLaunch
and the catch that sets lastError and logs) unchanged.

52-55: Potential overflow when persisting large UInt64 balance values.

UserDefaults.integer(forKey:) returns Int, and Int on 32-bit platforms is only 32 bits. While iOS devices are now 64-bit, storing UInt64 as Int can still lose the high bit (values ≥ 2^63 become negative). Consider using UserDefaults.object(forKey:) as? Int64 or storing the value as a string.

Proposed fix using Double for safer storage
     /// Persisted shielded balance (credits).
     private var persistedBalance: UInt64 {
-        get { UInt64(UserDefaults.standard.integer(forKey: "\(keyPrefix)_balance")) }
-        set { UserDefaults.standard.set(Int(newValue), forKey: "\(keyPrefix)_balance") }
+        get { UInt64(UserDefaults.standard.double(forKey: "\(keyPrefix)_balance")) }
+        set { UserDefaults.standard.set(Double(newValue), forKey: "\(keyPrefix)_balance") }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/ZKSyncService.swift`
around lines 52 - 55, The current persistedBalance getter/setter uses
UserDefaults.integer(...) and casts to UInt64, which can overflow on large
balances; change it to store as an NSNumber (or string) via
UserDefaults.set(NSNumber(value: ...), forKey: ...) and read via
UserDefaults.object(forKey:) as? NSNumber then access .uint64Value (or parse the
string) to round-trip full UInt64 safely; update the persistedBalance computed
property and any use of keyPrefix to use these object-based reads/writes instead
of integer(forKey:).
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift (1)

291-301: Inconsistent disabled state for "Clear" buttons.

The Platform sync "Clear" button (lines 291-301) is not disabled during sync, while the ZK sync "Clear" button (line 451) is disabled when zkSyncService.isSyncing. Consider making this behavior consistent across both sections.

Option: Disable Platform Clear button during sync
                             Button {
                                 platformBalanceSyncService.reset()
                             } label: {
                                 Text("Clear")
                                     .font(.caption)
                                     .fontWeight(.medium)
                             }
                             .buttonStyle(.borderedProminent)
                             .tint(.red)
                             .controlSize(.mini)
+                            .disabled(platformBalanceSyncService.isSyncing)
                         }

Also applies to: 441-452

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift`
around lines 291 - 301, The Platform "Clear" button currently calls
platformBalanceSyncService.reset() but does not disable during sync, causing
inconsistent behavior compared to the ZK clear button which uses
zkSyncService.isSyncing; update the Platform clear Button(s) to mirror the ZK
behavior by adding the .disabled(platformBalanceSyncService.isSyncing) modifier
to the Button that invokes platformBalanceSyncService.reset(), and similarly
ensure the other Platform clear instance (the one matching lines 441-452) also
uses .disabled with platformBalanceSyncService.isSyncing so both sections behave
consistently with the ZK clear button that uses zkSyncService.isSyncing.
packages/rs-dpp/Cargo.toml (1)

95-95: Feature name is now semantically misleading.

The core_key_wallet_manager feature now enables dep:key-wallet instead of the former key-wallet-manager crate. While this maintains backwards compatibility for feature flags, consumers enabling this feature expecting key_wallet_manager symbols (per the relevant snippet in rs-sdk/Cargo.toml) will need to update their imports to use key_wallet::manager::... paths instead.

Consider documenting this migration or renaming the feature to core_key_wallet_with_manager in a follow-up to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/Cargo.toml` at line 95, The feature flag
core_key_wallet_manager now enables dep:key-wallet (providing
key_wallet::manager::... symbols) but the name still implies the old
key_wallet_manager crate; update the crate to either (A) rename the feature to
core_key_wallet_with_manager and update all feature references (e.g., in any
Cargo.toml that used core_key_wallet_manager) or (B) add explicit
documentation/migration notes explaining that core_key_wallet_manager maps to
dep:key-wallet and that consumers must change imports from
key_wallet_manager::... to key_wallet::manager::...; ensure the chosen change
updates the feature list, README or migration docs, and any references to the
old symbol paths or feature name so consumers are not misled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/rs-dpp/src/lib.rs`:
- Around line 12-16: Add a backwards-compatible re-export for the removed
key_wallet_manager symbol so downstream crates using
dpp::key_wallet_manager::... keep compiling: restore a conditional alias by
adding a cfg-guarded re-export for the feature core_key_wallet_manager (e.g.,
under the same cfg style as the existing core_key_wallet feature) that maps
key_wallet to the old name (key_wallet_manager); alternatively, if you prefer
removal, delete the core_key_wallet_manager feature from Cargo.toml and update
documentation and downstream usages to import key_wallet::manager::... directly.

In `@packages/rs-platform-wallet/src/lib.rs`:
- Around line 24-25: The `pub use key_wallet;` re-export guarded by the
"manager" feature is unused; either remove the re-export from lib.rs (delete the
line `pub use key_wallet;` under #[cfg(feature = "manager")]) or, if intended
for external consumers, add a clear public doc comment above it and ensure the
"manager" feature actually exposes meaningful functionality in Cargo.toml (not
an empty `manager = []`) so external users can rely on
`platform_wallet::key_wallet`; references to `PlatformWalletInfo` and internal
imports should keep using direct `key_wallet` imports if you remove the
re-export.

In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift`:
- Around line 470-481: The Platform Payment loop in CoreWalletManager is using a
hardcoded Balance(confirmed: 0, unconfirmed: 0) which causes zero balances to be
shown; replace that with the actual balance from the account model by calling
m.getBalance() (or the equivalent method used for other account types), extract
confirmed/unconfirmed values (guarding for errors/optionals the same way as
other branches), and pass those values into AccountInfo(category:
.platformPayment, index: accountIdx, label: ..., balance: <actual balance>,
addressCount: ...); ensure you use
collection.getPlatformPaymentAccount(accountIndex:) and handle the optional
result and any thrown errors consistently with existing patterns in this file.
- Around line 378-384: The derivation path for the platformPayment case in
CoreWalletManager.swift is using purpose 15' but should be 17' to match DIP-17
and the other references; update the string returned in the platformPayment
branch (the platformPayment enum/case and its return
"m/9'/\(coinType)/15'/\(index ?? 0)'/x") to use "m/9'/\(coinType)/17'/\(index ??
0)'/x". While editing, review the dashPayReceivingFunds and
dashPayExternalAccount cases (they currently both return
"m/9'/\(coinType)/5'/0'/x") and confirm whether they are intentionally identical
or need different paths, adjusting them if necessary to reflect the intended
design.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddAccountView.swift`:
- Around line 95-109: The derivationPath(index:keyClass:isTestnet:)
implementation is using purpose 17' for platformPayment but
CoreWalletManager.derivationPath(for:index:network:) uses 15'; update the
platformPayment return to use "15'" (i.e.
"m/9'/\(coinType)/15'/\(index)'/\(keyClass)'/..." or equivalent) so both
derivationPath implementations are consistent; verify the same purpose value is
used in both CoreWalletManager and this AddAccountView method.

---

Outside diff comments:
In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift`:
- Around line 44-133: The initializer for SPVClient must free FFI resources on
all failure paths; add a defer immediately after unwrapping configPtr in init to
free the config pointer if the initializer throws (call
dash_spv_ffi_config_free(configPtr) from the defer), and when creating the
client handle, if dash_spv_ffi_client_new returns nil or any subsequent throw
occurs ensure any allocated client is freed (dash_spv_ffi_client_free(client))
and the config is freed as well; do this instead of relying on
instance.destroy()/deinit since self is not assigned on init failure. Ensure you
reference SPVClient.init, the local configPtr and client variables, and use the
FFI free functions (dash_spv_ffi_config_free / dash_spv_ffi_client_free) inside
the defer/cleanup paths.

---

Nitpick comments:
In `@packages/rs-dpp/Cargo.toml`:
- Line 95: The feature flag core_key_wallet_manager now enables dep:key-wallet
(providing key_wallet::manager::... symbols) but the name still implies the old
key_wallet_manager crate; update the crate to either (A) rename the feature to
core_key_wallet_with_manager and update all feature references (e.g., in any
Cargo.toml that used core_key_wallet_manager) or (B) add explicit
documentation/migration notes explaining that core_key_wallet_manager maps to
dep:key-wallet and that consumers must change imports from
key_wallet_manager::... to key_wallet::manager::...; ensure the chosen change
updates the feature list, README or migration docs, and any references to the
old symbol paths or feature name so consumers are not misled.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/ZKSyncService.swift`:
- Around line 98-144: Move the isSyncing reset into a defer block immediately
after setting isSyncing = true so it always runs even if the function returns
early; i.e., after setting isSyncing = true add defer { isSyncing = false } and
remove the trailing standalone isSyncing = false at the end, keeping the rest of
the logic (calls to poolClient.syncNotes(sdk: sdk),
poolClient.syncNullifiers(sdk: sdk), updates to
notesSynced/totalNotesSynced/nullifiersSpent/totalNullifiersSpent/shieldedBalance/orchardAddress/persistedBalance/persistedOrchardAddress/lastSyncTime/syncCountSinceLaunch
and the catch that sets lastError and logs) unchanged.
- Around line 52-55: The current persistedBalance getter/setter uses
UserDefaults.integer(...) and casts to UInt64, which can overflow on large
balances; change it to store as an NSNumber (or string) via
UserDefaults.set(NSNumber(value: ...), forKey: ...) and read via
UserDefaults.object(forKey:) as? NSNumber then access .uint64Value (or parse the
string) to round-trip full UInt64 safely; update the persistedBalance computed
property and any use of keyPrefix to use these object-based reads/writes instead
of integer(forKey:).

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift`:
- Around line 291-301: The Platform "Clear" button currently calls
platformBalanceSyncService.reset() but does not disable during sync, causing
inconsistent behavior compared to the ZK clear button which uses
zkSyncService.isSyncing; update the Platform clear Button(s) to mirror the ZK
behavior by adding the .disabled(platformBalanceSyncService.isSyncing) modifier
to the Button that invokes platformBalanceSyncService.reset(), and similarly
ensure the other Platform clear instance (the one matching lines 441-452) also
uses .disabled with platformBalanceSyncService.isSyncing so both sections behave
consistently with the ZK clear button that uses zkSyncService.isSyncing.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift`:
- Around line 294-299: The code builds a selectedNetworks array but only uses
its first element (primaryNetwork), which is unnecessary; either simplify by
computing primaryNetwork directly from the boolean flags (createForMainnet,
createForTestnet, createForDevnet with shouldShowDevnet, createForRegtest with
shouldShowRegtest) or add a brief comment above selectedNetworks explaining that
the array is intentionally built for potential future multi-network support
while only primaryNetwork is used today by the wallet creation logic (see
selectedNetworks and primaryNetwork usage in CreateWalletView.swift), and remove
compactMap if you simplify.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift`:
- Around line 243-317: The nested Task in requestFromFaucet continues running
after the view is dismissed; add a cancellable Task property (e.g. `@State`
private var statusClearTask: Task<Void, Never>?) and replace the inline Task at
the end of requestFromFaucet with: cancel any existing statusClearTask, assign a
new Task that sleeps 5s and checks Task.isCancelled before clearing
faucetStatus; also cancel statusClearTask in the view's .onDisappear to avoid
doing unnecessary work. Ensure you reference requestFromFaucet, faucetStatus,
and the new statusClearTask property when making these changes.
- Line 303: The faucetStatus message in ReceiveAddressView is misleading; update
the assignment that sets faucetStatus (in ReceiveAddressView) from "Auth failed
— set faucetRPCPassword in UserDefaults" to a user-facing string like "Auth
failed — check Faucet RPC Password in Settings" so users are directed to the
Settings UI rather than UserDefaults.
🪄 Autofix (Beta)

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

Run ID: eb38884d-eab8-4b91-8933-312bd3579744

📥 Commits

Reviewing files that changed from the base of the PR and between a0dddb0 and f1ef279.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • Cargo.toml
  • packages/rs-dpp/Cargo.toml
  • packages/rs-dpp/src/lib.rs
  • packages/rs-platform-wallet/Cargo.toml
  • packages/rs-platform-wallet/examples/basic_usage.rs
  • packages/rs-platform-wallet/src/lib.rs
  • packages/rs-platform-wallet/src/platform_wallet_info/wallet_info_interface.rs
  • packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs
  • packages/rs-sdk-ffi/src/sdk.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/HDWalletModels.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWalletTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/ZKSyncService.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddAccountView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletsContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift
💤 Files with no reviewable changes (1)
  • Cargo.toml

Comment on lines 12 to 16
#[cfg(feature = "core_key_wallet")]
pub use key_wallet;

#[cfg(feature = "core_key_wallet_manager")]
pub use key_wallet_manager;
// key_wallet_manager was merged into key_wallet crate

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for usages of dpp::key_wallet_manager in the codebase
rg -n "dpp::key_wallet_manager" --type rust

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check if core_key_wallet_manager feature exists in rs-dpp Cargo.toml
cat packages/rs-dpp/Cargo.toml | grep -A 20 "\[features\]"

Repository: dashpay/platform

Length of output: 800


🏁 Script executed:

#!/bin/bash
# Search for all references to key_wallet_manager in Cargo.toml files
rg "key_wallet_manager" --type toml -n

Repository: dashpay/platform

Length of output: 370


🏁 Script executed:

#!/bin/bash
# Look for references to core_key_wallet_manager feature in the codebase
rg "core_key_wallet_manager" --type toml -n

Repository: dashpay/platform

Length of output: 370


🏁 Script executed:

#!/bin/bash
# Check if key_wallet_manager module/directory still exists
fd "key_wallet_manager" --type f --type d packages/rs-dpp/

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check the structure of key_wallet module to understand what was merged
ls -la packages/rs-dpp/src/ | grep -i wallet

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for any re-export patterns with key_wallet_manager to understand the merge better
rg "key_wallet_manager" packages/rs-dpp/src/ -n

Repository: dashpay/platform

Length of output: 144


🏁 Script executed:

#!/bin/bash
# Check the current state of packages/rs-dpp/src/lib.rs around the removed export
sed -n '1,30p' packages/rs-dpp/src/lib.rs

Repository: dashpay/platform

Length of output: 668


🏁 Script executed:

#!/bin/bash
# Check how rs-sdk handles core_key_wallet_manager feature
grep -A 5 -B 5 "core_key_wallet_manager" packages/rs-sdk/src/lib.rs

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check if there's any re-export of key_wallet or key_wallet_manager in rs-sdk
rg "pub use.*key_wallet" packages/rs-sdk/src/ -n

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check the git history to understand what was removed
git log --oneline -n 30 -- packages/rs-dpp/src/lib.rs

Repository: dashpay/platform

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Check if key_wallet crate has a manager module or submodule
rg "mod.*manager|pub.*manager" packages/rs-dpp/Cargo.lock -A 2 | head -50

Repository: dashpay/platform

Length of output: 126


🏁 Script executed:

#!/bin/bash
# Search for any imports of key_wallet in test files or examples
rg "use key_wallet" packages/ -n --type rust | head -20

Repository: dashpay/platform

Length of output: 2541


core_key_wallet_manager feature no longer provides re-export for backwards compatibility.

The core_key_wallet_manager feature still exists in Cargo.toml and is referenced by downstream crates (e.g., rs-sdk marks it as a default feature), but the corresponding pub use key_wallet_manager; re-export has been removed. While the comment indicates an intentional merge of key_wallet_manager into the key_wallet crate, external consumers that relied on dpp::key_wallet_manager::... paths will now encounter compilation errors.

Consider either:

  1. Adding a re-export alias for backwards compatibility: #[cfg(feature = "core_key_wallet_manager")] pub use key_wallet as key_wallet_manager;
  2. Removing the core_key_wallet_manager feature entirely if it is no longer needed
  3. Documenting the migration path clearly (consumers should use key_wallet::manager::... directly)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/src/lib.rs` around lines 12 - 16, Add a backwards-compatible
re-export for the removed key_wallet_manager symbol so downstream crates using
dpp::key_wallet_manager::... keep compiling: restore a conditional alias by
adding a cfg-guarded re-export for the feature core_key_wallet_manager (e.g.,
under the same cfg style as the existing core_key_wallet feature) that maps
key_wallet to the old name (key_wallet_manager); alternatively, if you prefer
removal, delete the core_key_wallet_manager feature from Cargo.toml and update
documentation and downstream usages to import key_wallet::manager::... directly.

Comment on lines 24 to +25
#[cfg(feature = "manager")]
pub use key_wallet_manager;
pub use key_wallet;
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if key-wallet is used elsewhere in the crate without the manager feature gate
rg -n "use key_wallet" packages/rs-platform-wallet/src --type rust

Repository: dashpay/platform

Length of output: 3546


🏁 Script executed:

# Check Cargo.toml for key-wallet dependency configuration
cat -n packages/rs-platform-wallet/Cargo.toml | head -40

Repository: dashpay/platform

Length of output: 1120


🏁 Script executed:

# Search for "manager" feature definition in Cargo.toml
rg -n "manager" packages/rs-platform-wallet/Cargo.toml

Repository: dashpay/platform

Length of output: 116


🏁 Script executed:

# Check if platform_wallet_info is gated by manager feature
rg -n "#\[cfg.*manager" packages/rs-platform-wallet/src --type rust

Repository: dashpay/platform

Length of output: 129


🏁 Script executed:

# Check what platform_wallet_info module exposes publicly
rg -n "^pub " packages/rs-platform-wallet/src/platform_wallet_info/mod.rs | head -20

Repository: dashpay/platform

Length of output: 94


🏁 Script executed:

# Check if any of the crate's public API depends on the re-export being available
rg -n "platform_wallet::key_wallet" . --type rust

Repository: dashpay/platform

Length of output: 42


Remove the unused re-export or clarify its purpose.

The pub use key_wallet; re-export at line 25 is feature-gated under manager, but the manager feature is empty (defined as manager = [] in Cargo.toml) and included in default features. More critically, this re-export is never used—no code in the codebase references platform_wallet::key_wallet. Internal modules import directly from key_wallet, and the public API (PlatformWalletInfo) does not depend on this re-export. Either remove it or document its intended purpose if it's meant for external consumers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-platform-wallet/src/lib.rs` around lines 24 - 25, The `pub use
key_wallet;` re-export guarded by the "manager" feature is unused; either remove
the re-export from lib.rs (delete the line `pub use key_wallet;` under
#[cfg(feature = "manager")]) or, if intended for external consumers, add a clear
public doc comment above it and ensure the "manager" feature actually exposes
meaningful functionality in Cargo.toml (not an empty `manager = []`) so external
users can rely on `platform_wallet::key_wallet`; references to
`PlatformWalletInfo` and internal imports should keep using direct `key_wallet`
imports if you remove the re-export.

Comment on lines +378 to 384
case .dashPayReceivingFunds:
return "m/9'/\(coinType)/5'/0'/x"
case .dashPayExternalAccount:
return "m/9'/\(coinType)/5'/0'/x"
case .platformPayment:
return "m/9'/\(coinType)/15'/\(index ?? 0)'/x"
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for DIP-17 derivation path specifications in the codebase
rg -n "DIP.?17|purpose.?17|purpose.?15" --type rust --type swift -C2

# Search for platform payment derivation path constants
rg -n "m/9.*17|m/9.*15" -C2

Repository: dashpay/platform

Length of output: 14400


Fix derivation path for platformPayment account type.

The path here uses purpose 15' but should use 17' to match the DIP-17 standard and be consistent with AddAccountView.derivationPath() and Wallet.addPlatformPaymentAccount() documentation, which both specify m/9'/coinType/17'/account'/key_class'/....

Additionally, verify whether .dashPayReceivingFunds and .dashPayExternalAccount should return identical paths or if they were meant to differ.

Proposed fix
        case .platformPayment:
-            return "m/9'/\(coinType)/15'/\(index ?? 0)'/x"
+            return "m/9'/\(coinType)/17'/\(index ?? 0)'/x"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift`
around lines 378 - 384, The derivation path for the platformPayment case in
CoreWalletManager.swift is using purpose 15' but should be 17' to match DIP-17
and the other references; update the string returned in the platformPayment
branch (the platformPayment enum/case and its return
"m/9'/\(coinType)/15'/\(index ?? 0)'/x") to use "m/9'/\(coinType)/17'/\(index ??
0)'/x". While editing, review the dashPayReceivingFunds and
dashPayExternalAccount cases (they currently both return
"m/9'/\(coinType)/5'/0'/x") and confirm whether they are intentionally identical
or need different paths, adjusting them if necessary to reflect the intended
design.

Comment on lines +470 to +481
// Platform Payment (DIP-17)
if collection.hasPlatformPaymentAccounts {
for accountIdx in 0..<collection.platformPaymentCount {
if let m = collection.getPlatformPaymentAccount(accountIndex: accountIdx, keyClass: 0) {
var addrCount = 0
if let pool = m.getAddressPool(), let infos = try? pool.getAddresses(from: 0, to: 1000) {
addrCount = infos.count
}
list.append(AccountInfo(category: .platformPayment, index: accountIdx, label: "Platform Payment \(accountIdx)", balance: Balance(confirmed: 0, unconfirmed: 0), addressCount: (addrCount, 0)))
}
}
}
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.

⚠️ Potential issue | 🟠 Major

Platform Payment accounts always show zero balance.

The balance is hardcoded to Balance(confirmed: 0, unconfirmed: 0) instead of calling m.getBalance() like other account types in this method. This will cause Platform Payment accounts to always display a zero balance in the UI regardless of their actual balance.

Proposed fix
         if collection.hasPlatformPaymentAccounts {
             for accountIdx in 0..<collection.platformPaymentCount {
                 if let m = collection.getPlatformPaymentAccount(accountIndex: accountIdx, keyClass: 0) {
+                    let b = m.getBalance()
                     var addrCount = 0
                     if let pool = m.getAddressPool(), let infos = try? pool.getAddresses(from: 0, to: 1000) {
                         addrCount = infos.count
                     }
-                    list.append(AccountInfo(category: .platformPayment, index: accountIdx, label: "Platform Payment \(accountIdx)", balance: Balance(confirmed: 0, unconfirmed: 0), addressCount: (addrCount, 0)))
+                    list.append(AccountInfo(category: .platformPayment, index: accountIdx, label: "Platform Payment \(accountIdx)", balance: b, addressCount: (addrCount, 0)))
                 }
             }
         }
📝 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
// Platform Payment (DIP-17)
if collection.hasPlatformPaymentAccounts {
for accountIdx in 0..<collection.platformPaymentCount {
if let m = collection.getPlatformPaymentAccount(accountIndex: accountIdx, keyClass: 0) {
var addrCount = 0
if let pool = m.getAddressPool(), let infos = try? pool.getAddresses(from: 0, to: 1000) {
addrCount = infos.count
}
list.append(AccountInfo(category: .platformPayment, index: accountIdx, label: "Platform Payment \(accountIdx)", balance: Balance(confirmed: 0, unconfirmed: 0), addressCount: (addrCount, 0)))
}
}
}
// Platform Payment (DIP-17)
if collection.hasPlatformPaymentAccounts {
for accountIdx in 0..<collection.platformPaymentCount {
if let m = collection.getPlatformPaymentAccount(accountIndex: accountIdx, keyClass: 0) {
let b = m.getBalance()
var addrCount = 0
if let pool = m.getAddressPool(), let infos = try? pool.getAddresses(from: 0, to: 1000) {
addrCount = infos.count
}
list.append(AccountInfo(category: .platformPayment, index: accountIdx, label: "Platform Payment \(accountIdx)", balance: b, addressCount: (addrCount, 0)))
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift`
around lines 470 - 481, The Platform Payment loop in CoreWalletManager is using
a hardcoded Balance(confirmed: 0, unconfirmed: 0) which causes zero balances to
be shown; replace that with the actual balance from the account model by calling
m.getBalance() (or the equivalent method used for other account types), extract
confirmed/unconfirmed values (guarding for errors/optionals the same way as
other branches), and pass those values into AccountInfo(category:
.platformPayment, index: accountIdx, label: ..., balance: <actual balance>,
addressCount: ...); ensure you use
collection.getPlatformPaymentAccount(accountIndex:) and handle the optional
result and any thrown errors consistently with existing patterns in this file.

Comment on lines +95 to +109
func derivationPath(index: UInt32, keyClass: UInt32, isTestnet: Bool) -> String {
let coinType = isTestnet ? "1'" : "5'"
switch self {
case .bip44:
return "m/44'/\(coinType)/\(index)'"
case .bip32:
return "m/\(index)'"
case .coinjoin:
return "m/9'/\(coinType)/4'/\(index)'"
case .platformPayment:
return "m/9'/\(coinType)/17'/\(index)'/\(keyClass)'/..."
case .identityTopup:
return "m/9'/\(coinType)/5'/2'/\(index)'/..."
}
}
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.

⚠️ Potential issue | 🟡 Minor

Derivation path uses purpose 17 here but 15 in CoreWalletManager.

This derivationPath method uses 17' for platformPayment (line 105), which aligns with "DIP-17" naming, but CoreWalletManager.derivationPath(for:index:network:) uses 15'. These should be consistent—see the related comment in CoreWalletManager.swift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddAccountView.swift`
around lines 95 - 109, The derivationPath(index:keyClass:isTestnet:)
implementation is using purpose 17' for platformPayment but
CoreWalletManager.derivationPath(for:index:network:) uses 15'; update the
platformPayment return to use "15'" (i.e.
"m/9'/\(coinType)/15'/\(index)'/\(keyClass)'/..." or equivalent) so both
derivationPath implementations are consistent; verify the same purpose value is
used in both CoreWalletManager and this AddAccountView method.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Verified 7 findings against the PR source. Five are functional correctness issues that should block approval; two are low-severity localhost/dev-tooling security concerns that are real but should be treated as comments, not release blockers for this example app.

Reviewed commit: f1ef279

🔴 3 blocking | 💬 4 nitpick(s)

2 additional findings

🔴 blocking: Creating a wallet does not initialize the new ZK sync path

packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift (lines 306-316)

The wallet creation flow only calls walletService.walletManager.createWallet(...) and dismisses the sheet. The only places that call initializeShieldedService() are app startup and network switches in UnifiedAppState (UnifiedAppState.swift:96-103, 133-145, 245-256). performZKSync() also returns early until shieldedService.poolClient exists (UnifiedAppState.swift:233-240). On a clean launch with no wallets, creating the first wallet in-app leaves the shielded service uninitialized, so ZK sync never starts until the app is restarted or the network is switched.

🔴 blocking: Platform payment accounts are navigable even though their detail lookup is unimplemented

packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift (lines 23-26)

The account list wraps every account in a NavigationLink to AccountDetailView. But CoreWalletManager.getAccountDetails(...) explicitly sets .platformPayment to managed = nil (CoreWalletManager.swift:259-260) and still returns an AccountDetailInfo with default FFIAccountType(rawValue: 0) and no real address/account metadata (CoreWalletManager.swift:265-302). Tapping a platform payment account therefore opens a detail screen backed by fabricated/default account-type information instead of actual DIP-17 data.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift`:
- [BLOCKING] lines 306-316: Creating a wallet does not initialize the new ZK sync path
  The wallet creation flow only calls `walletService.walletManager.createWallet(...)` and dismisses the sheet. The only places that call `initializeShieldedService()` are app startup and network switches in `UnifiedAppState` (`UnifiedAppState.swift:96-103`, `133-145`, `245-256`). `performZKSync()` also returns early until `shieldedService.poolClient` exists (`UnifiedAppState.swift:233-240`). On a clean launch with no wallets, creating the first wallet in-app leaves the shielded service uninitialized, so ZK sync never starts until the app is restarted or the network is switched.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift`:
- [BLOCKING] lines 23-26: Platform payment accounts are navigable even though their detail lookup is unimplemented
  The account list wraps every account in a `NavigationLink` to `AccountDetailView`. But `CoreWalletManager.getAccountDetails(...)` explicitly sets `.platformPayment` to `managed = nil` (`CoreWalletManager.swift:259-260`) and still returns an `AccountDetailInfo` with default `FFIAccountType(rawValue: 0)` and no real address/account metadata (`CoreWalletManager.swift:265-302`). Tapping a platform payment account therefore opens a detail screen backed by fabricated/default account-type information instead of actual DIP-17 data.

In `packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift`:
- [BLOCKING] lines 167-176: Regtest/local SDK initialization fails unless Docker mode injects DAPI addresses
  `SDK.init(network:)` only supplies `config.dapi_addresses` when `useDockerSetup` is true. Otherwise it calls `dash_sdk_create_trusted(&config)` with a null address list. On the Rust side, null addresses only have defaults for mainnet and testnet; regtest/local returns `InvalidParameter` (`packages/rs-sdk-ffi/src/sdk.rs:379-447`). That makes non-Docker regtest/local initialization fail outright.

Comment on lines +74 to +86
func startPeriodicSync(network: AppNetwork) {
networkName = network.rawValue

// Restore persisted state from previous session
let savedBalance = persistedBalance
if savedBalance > 0 {
shieldedBalance = savedBalance
}

let savedAddress = persistedOrchardAddress
if let addr = savedAddress, !addr.isEmpty {
orchardAddress = addr
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 Medium: Network switches can leave stale shielded balance and address visible

startPeriodicSync(network:) changes the key prefix and restores persisted state only when the new network has a non-zero saved balance or a non-empty saved address. If the target network has no saved ZK state yet, shieldedBalance and orchardAddress are left unchanged, so the previous network's values remain visible. UnifiedAppState.handleNetworkSwitch() restarts ZK sync but does not clear ZKSyncService first (UnifiedAppState.swift:133-145), so the stale values persist until a later successful sync overwrites them.

source: ['codex']

Comment on lines +470 to +479
// Platform Payment (DIP-17)
if collection.hasPlatformPaymentAccounts {
for accountIdx in 0..<collection.platformPaymentCount {
if let m = collection.getPlatformPaymentAccount(accountIndex: accountIdx, keyClass: 0) {
var addrCount = 0
if let pool = m.getAddressPool(), let infos = try? pool.getAddresses(from: 0, to: 1000) {
addrCount = infos.count
}
list.append(AccountInfo(category: .platformPayment, index: accountIdx, label: "Platform Payment \(accountIdx)", balance: Balance(confirmed: 0, unconfirmed: 0), addressCount: (addrCount, 0)))
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 Medium: Platform payment account enumeration drops valid non-default account keys

The new listing logic synthesizes platform payment accounts as 0..<platformPaymentCount and always fetches keyClass: 0. The same collection type exposes getPlatformPaymentKeys() with the real (account, keyClass) pairs (ManagedAccountCollection.swift:263-281), and the add-account UI allows users to create platform payment accounts with arbitrary key classes. As written, any account whose key class is not 0 will never appear, and sparse account indices can also be skipped if platformPaymentCount is smaller than the highest account index.

source: ['codex']

Comment on lines +167 to 176
let forceLocal = UserDefaults.standard.bool(forKey: "useDockerSetup")
if forceLocal {
let localAddresses = Self.platformDAPIAddresses
print("🔵 SDK.init: Using local DAPI addresses: \(localAddresses)")
result = localAddresses.withCString { addressesCStr -> DashSDKResult in
var mutableConfig = config
mutableConfig.dapi_addresses = addressesCStr
print("🔵 SDK.init: Calling dash_sdk_create_trusted...")
return dash_sdk_create_trusted(&mutableConfig)
}
} else {
print("🔵 SDK.init: Using default network addresses")
result = dash_sdk_create_trusted(&config)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 Blocking: Regtest/local SDK initialization fails unless Docker mode injects DAPI addresses

SDK.init(network:) only supplies config.dapi_addresses when useDockerSetup is true. Otherwise it calls dash_sdk_create_trusted(&config) with a null address list. On the Rust side, null addresses only have defaults for mainnet and testnet; regtest/local returns InvalidParameter (packages/rs-sdk-ffi/src/sdk.rs:379-447). That makes non-Docker regtest/local initialization fail outright.

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift`:
- [BLOCKING] lines 167-176: Regtest/local SDK initialization fails unless Docker mode injects DAPI addresses
  `SDK.init(network:)` only supplies `config.dapi_addresses` when `useDockerSetup` is true. Otherwise it calls `dash_sdk_create_trusted(&config)` with a null address list. On the Rust side, null addresses only have defaults for mainnet and testnet; regtest/local returns `InvalidParameter` (`packages/rs-sdk-ffi/src/sdk.rs:379-447`). That makes non-Docker regtest/local initialization fail outright.

Comment on lines +339 to +344
let trusted_provider = if is_local {
info!("dash_sdk_create_trusted: using local quorum sidecar for regtest");
match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new_with_url(
network,
"http://127.0.0.1:22444".to_string(),
std::num::NonZeroUsize::new(100).unwrap(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 Low: Local trusted mode accepts quorum context over unauthenticated localhost HTTP

For SDKLocal/SDKRegtest, trusted mode switches to TrustedHttpContextProvider::new_with_url(..., "http://127.0.0.1:22444", ...). In trusted mode the app is intentionally relying on that sidecar instead of proof verification, so any local process that can bind or intercept that port can influence the returned quorum/context data. This is limited to localhost developer setups, so it is a real trust-boundary concern but not a production-severity issue for this example app.

source: ['codex']

Comment on lines +256 to +285
// Read RPC port and password from UserDefaults, with dashmate defaults
let rpcPort = UserDefaults.standard.string(forKey: "faucetRPCPort") ?? "20302"
let rpcUser = UserDefaults.standard.string(forKey: "faucetRPCUser") ?? "dashmate"
let rpcPassword = UserDefaults.standard.string(forKey: "faucetRPCPassword") ?? "dashmate"

guard let url = URL(string: "http://127.0.0.1:\(rpcPort)/") else {
faucetStatus = "Invalid RPC URL"
return
}

let body: [String: Any] = [
"jsonrpc": "1.0",
"id": "faucet",
"method": "sendtoaddress",
"params": [address, 10]
]

guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else {
faucetStatus = "Failed to encode request"
return
}

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")

let credentials = "\(rpcUser):\(rpcPassword)"
if let credData = credentials.data(using: .utf8) {
request.setValue("Basic \(credData.base64EncodedString())", forHTTPHeaderField: "Authorization")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 Low: The local faucet flow uses persistent plaintext RPC credentials and sends them to any localhost listener on the configured port

The faucet feature reads faucetRPCUser/faucetRPCPassword from UserDefaults, falls back to dashmate:dashmate, and sends them as HTTP Basic auth to 127.0.0.1 (ReceiveAddressView.swift:256-285). The paired options UI stores faucetRPCPassword directly in UserDefaults through a plain TextField (Views/OptionsView.swift:58-62). Because this is only exposed for regtest Docker usage, the severity should stay low, but the finding itself is correct.

source: ['codex']

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