Skip to content

feat(swift-sdk): send transaction#3130

Open
ZocoLini wants to merge 1 commit intov3.1-devfrom
feat/send-transaction-swift
Open

feat(swift-sdk): send transaction#3130
ZocoLini wants to merge 1 commit intov3.1-devfrom
feat/send-transaction-swift

Conversation

@ZocoLini
Copy link
Collaborator

@ZocoLini ZocoLini commented Feb 20, 2026

Need other branch to get merged first and to update the rust-dashcore targeted revision once the open PRS with the needed changes gets merge

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

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • New Features

    • Wallets can broadcast transactions and build/sign transactions with selectable fee rates (Economy, Normal, Priority).
    • Send UI: numeric amount input, spendable-balance display, live fee calculation, and real-time transaction preview.
  • Bug Fixes / Improvements

    • Improved error reporting for broadcast failures.
    • Block header progress now reports tipHeight for more accurate sync status.

@ZocoLini ZocoLini marked this pull request as draft February 20, 2026 21:38
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds transaction building and broadcasting APIs and types: new buildSignedTransaction in wallet managers (one missing return, one duplicated), broadcastTransaction on SPV client (FFI call + SPVError.transactionBroadcastFailed), renames SPVBlockHeadersProgress.currentHeight → tipHeight, replaces BuildAndSignResult with FeeRate, and updates example send UI to build and broadcast transactions.

Changes

Cohort / File(s) Summary
SPV Client & Types
packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift, packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift
Adds broadcastTransaction(_:) throws which calls the FFI and throws SPVError.transactionBroadcastFailed(String) on non‑zero result; getSyncProgress now calls dash_spv_ffi_client_get_manager_sync_progress. Renames SPVBlockHeadersProgress.currentHeighttipHeight and maps from ffi.tip_height.
Service Layer
packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift
Exposes public func broadcastTransaction(_ data: Data) throws delegating to spvClient.broadcastTransaction.
Core Wallet Manager
packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift
Adds public func buildSignedTransaction(for:accIndex:outputs:) throws -> (Data, UInt64) but the implementation calls the underlying API without returning its result (missing return).
Key Wallet Manager
packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
Adds func buildSignedTransaction(for:accIndex:outputs:) throws -> (Data, UInt64) with FFI preparation, error handling, and memory cleanup; the same method appears twice in the file (duplicate definitions).
Transaction Types
packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift
Removes BuildAndSignResult, introduces public FeeRate enum (economy,normal,priority) with intoDuffPerKB() mapping; Output.toFFI() now allocates C string with strdup(address) (TODO regarding freeing).
Example App UI
packages/swift-sdk/SwiftExampleApp/.../CoreContentView.swift, packages/swift-sdk/SwiftExampleApp/.../SendTransactionView.swift
Updates header progress display to use tipHeight. Reworks send UI to calculate fees, build signed transactions via walletManager, validate spendable balance, and broadcast via walletService/spvClient; removes memo and previous sending overlay logic.
Minor cleanup
packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift
Removed a comment header; no behavioral change.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as SendTransactionView
    participant WalletMgr as WalletManager
    participant SPV as SPVClient
    participant FFI as FFI Layer

    User->>UI: enter recipient, amount, choose fee rate
    UI->>WalletMgr: buildSignedTransaction(wallet, accIndex, outputs)
    WalletMgr->>FFI: prepare outputs & call build/sign FFI
    FFI-->>WalletMgr: (txBytes, fee) or error
    WalletMgr-->>UI: return (transactionData, fee) or throw
    UI->>UI: validate amount + fee ≤ spendable balance
    User->>UI: confirm send
    UI->>SPV: broadcastTransaction(transactionData)
    SPV->>FFI: call broadcast FFI
    FFI-->>SPV: success or error
    SPV-->>UI: success / throws transactionBroadcastFailed
    UI-->>User: show success or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped a byte and signed it neat,
I picked a FeeRate with tiny feet,
I asked the SPV to send it through,
Duff counts danced and headers grew.
🌿✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(swift-sdk): send transaction' clearly and concisely summarizes the main objective of the pull request, which is to implement send-transaction functionality in the Swift SDK.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/send-transaction-swift
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Base automatically changed from feat/iOSSupport to v3.1-dev March 4, 2026 02:23
@github-actions github-actions bot added this to the v3.1.0 milestone Mar 4, 2026
@ZocoLini ZocoLini force-pushed the feat/send-transaction-swift branch 5 times, most recently from e52c1dc to 9d6e256 Compare March 17, 2026 16:26
@ZocoLini ZocoLini marked this pull request as ready for review March 17, 2026 17:04
@ZocoLini ZocoLini requested a review from shumkov as a code owner March 17, 2026 17:04
Copy link
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/SPVTypes.swift (1)

54-66: ⚠️ Potential issue | 🟠 Major

Keep a deprecated currentHeight shim for this public rename.

Changing a public SDK property from currentHeight to tipHeight is source-breaking for any downstream app that has not been updated with this PR. Unless this is intentionally shipping as a breaking release, add a deprecated alias so existing callers keep compiling.

Compatibility shim
 public struct SPVBlockHeadersProgress: Sendable {
     public let state: SPVSyncState
     public let tipHeight: UInt32
+    `@available`(*, deprecated, renamed: "tipHeight")
+    public var currentHeight: UInt32 { tipHeight }
     public let targetHeight: UInt32
🤖 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/SPVTypes.swift` around lines
54 - 66, The public rename removed the previously exposed currentHeight and will
break downstream code; add a deprecated computed property currentHeight to
SPVBlockHeadersProgress that returns tipHeight (mark it `@available`(*,
deprecated, message: "Use tipHeight instead")) so existing callers continue to
compile, and ensure the init and other code continue to set tipHeight as before;
reference struct SPVBlockHeadersProgress and property tipHeight when adding the
shim.
🤖 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/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift`:
- Line 407: The send flow in WalletManager currently force-unwraps the result of
getWallet(id:) (let wallet = try self.getWallet(id: wallet.walletId)!), which
can be nil after a client reset or during re-import and will crash; change this
to safely handle the nil by using optional binding (guard/if let) or by throwing
a descriptive error (e.g., WalletNotFound or WalletUnavailable) from the send
method when getWallet(id:) returns nil, and ensure downstream code uses the
non-optional wallet only after the safe check so the send becomes a recoverable
error path instead of a runtime crash.
- Around line 409-432: The FFI outputs created by outputs.map { $0.toFFI() }
allocate duplicated C strings for each output address via strdup(), but they are
never freed; in the defer block (after the wallet_build_and_sign_transaction
call) iterate the ffiOutputs array and free each duplicated address pointer
(e.g. call free on each ffiOutput.address or the actual field name returned by
Transaction.Output.toFFI()), then proceed to free error.message and txBytesPtr
as already done so no C string leaks remain.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift`:
- Around line 130-148: recalculateTransaction() currently swallows errors by
clearing fee and tx on failure; add a stored error state (e.g.,
transactionError: Error?) and set it in the catch block instead of silently
clearing so the view can present the failure; keep clearing transactionError
only when inputs are empty (the guard branch) or when a subsequent successful
createTransaction() returns, and update the UI bindings to show transactionError
alongside fee/tx (references: recalculateTransaction(), createTransaction(),
fee, tx, recipientAddress, amount, balance.spendable).
- Around line 21-24: The computed property amount must stop using Double; parse
amountString into a Decimal, validate that the fractional scale is at most 8 (no
more than 8 decimal places), multiply the Decimal by 100_000_000 exactly (use
Decimal arithmetic/NSDecimalNumber to avoid floating-point rounding), round if
necessary using .plain rounding only after validating scale, then safely convert
the resulting integral Decimal to UInt64; update the amount computed property
(referencing amountString and amount) to return 0 or handle parse/precision
failures consistently.
- Around line 33-35: The balance computed property uses
walletService.walletManager.getBalance with incorrect parameter names; update
the call in the balance getter to call CoreWalletManager.getBalance(for: wallet,
accountIndex: 0) (replace accType:/.standardBIP44 and accIndex: with the single
accountIndex: parameter) so it matches the getBalance(for:accountIndex:)
signature.

---

Outside diff comments:
In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift`:
- Around line 54-66: The public rename removed the previously exposed
currentHeight and will break downstream code; add a deprecated computed property
currentHeight to SPVBlockHeadersProgress that returns tipHeight (mark it
`@available`(*, deprecated, message: "Use tipHeight instead")) so existing callers
continue to compile, and ensure the init and other code continue to set
tipHeight as before; reference struct SPVBlockHeadersProgress and property
tipHeight when adding the shim.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c37e876f-9947-4e1d-b010-187d9c0b977d

📥 Commits

Reviewing files that changed from the base of the PR and between d3791b2 and 9d6e256.

📒 Files selected for processing (9)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift
💤 Files with no reviewable changes (1)
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift

Copy link
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

This PR adds transaction sending to the Swift SDK with FFI-based transaction building/signing, fee rate selection, and broadcast. Three blocking issues prevent merge: a memory leak from strdup'd C strings never freed (leaking on every keystroke), a force-unwrap that crashes when a wallet isn't found, and a call to a getBalance overload that doesn't exist (won't compile).

Reviewed commit: 9d6e256

🔴 3 blocking | 🟡 2 suggestion(s) | 💬 2 nitpick(s)

🤖 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/Sources/SwiftDashSDK/KeyWallet/Transaction.swift`:
- [BLOCKING] lines 31-34: Memory leak: strdup'd C strings in toFFI() are never freed
  `strdup(address)` at line 32 allocates heap memory for each `FFITxOutput`. In `WalletManager.buildSignedTransaction` (line 409), `outputs.map { $0.toFFI() }` creates these allocations, but the `defer` block only frees `error.message` and `txBytesPtr` — the `ffiOutputs[].address` pointers are never freed.

`SendTransactionView.recalculateTransaction()` fires on every keystroke via `.onChange` handlers (lines 109-117), so each character typed leaks one strdup allocation per output.

In `packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift`:
- [BLOCKING] line 407: Force-unwrap on getWallet() will crash when wallet is not found
  `let wallet = try self.getWallet(id: wallet.walletId)!` — `getWallet(id:)` returns `Wallet?` and explicitly returns `nil` for NOT_FOUND errors (line 163). Force-unwrapping crashes the app. The parameter name `wallet` (an `HDWallet`) also shadows the result, making the force-unwrap easy to miss during review.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift`:
- [BLOCKING] lines 33-35: getBalance call uses a non-existent method signature — won't compile
  Line 34 calls `walletService.walletManager.getBalance(for: wallet, accType: .standardBIP44, accIndex: 0)` but `CoreWalletManager.getBalance` (line 171) has the signature `getBalance(for wallet: HDWallet, accountIndex: UInt32 = 0) -> Balance`. There is no `accType` parameter. Other call sites in this repo (CoreContentView.swift:460, WalletDetailView.swift:487) use the correct signature. This will fail to compile.
- [SUGGESTION] lines 21-23: Floating-point arithmetic causes off-by-one duff rounding errors
  `UInt64(double * 100_000_000)` uses IEEE 754 double-precision which cannot exactly represent many decimal fractions. For example, `Double("0.29")! * 100_000_000` evaluates to `28999999.999999996`, truncated to `28999999` duffs — one duff less than intended. Use `Decimal` for exact base-10 arithmetic.
- [SUGGESTION] lines 109-117: Full transaction build+sign fires on every keystroke
  `recalculateTransaction()` fires on every character typed in address and amount fields. Each call goes through FFI to `wallet_build_and_sign_transaction`, which selects UTXOs, constructs, and signs a transaction. Consider debouncing (e.g., with a short `Task.sleep` delay that cancels on the next keystroke) to avoid unnecessary FFI calls and the associated strdup leaks.

@ZocoLini ZocoLini force-pushed the feat/send-transaction-swift branch from 9d6e256 to 9d604c0 Compare March 17, 2026 18:32
Copy link
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: 1

♻️ Duplicate comments (4)
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift (2)

21-24: ⚠️ Potential issue | 🟠 Major

Don't convert DASH amounts through Double — use Decimal for exact arithmetic.

IEEE 754 double-precision cannot exactly represent many decimal fractions. For example, Double("0.29")! * 100_000_000 evaluates to 28999999.999999996, which truncates to 28999999 duffs — one duff less than intended.

🔧 Proposed fix using Decimal
 private var amount: UInt64 {
-    guard let double = Double(amountString) else { return 0 }
-    return UInt64(double * 100_000_000) // Convert DASH to duffs
+    guard let decimal = Decimal(string: amountString) else { return 0 }
+    let duffs = decimal * 100_000_000
+    return NSDecimalNumber(decimal: duffs).uint64Value
 }
🤖 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/SendTransactionView.swift`
around lines 21 - 24, The computed property `amount` currently parses
`amountString` to a Double which causes precision loss; change it to parse using
Decimal (e.g., Decimal(string: amountString)) and perform exact multiplication
by 100_000_000 with appropriate rounding (use .plain or .bankers as desired)
before converting to UInt64, and ensure you handle nil/invalid input and
overflow safely; update the `amount` getter to use `Decimal` arithmetic and
rounding rather than `Double` to produce the correct duffs value.

140-148: ⚠️ Potential issue | 🟠 Major

Surface transaction-build failures to the user.

When createTransaction() fails, the error is swallowed and the user only sees a disabled Send button with no indication of what went wrong (invalid address, insufficient balance, signing failure, etc.). The view already has an error state — use it here.

🔧 Proposed fix
+    `@State` private var transactionError: Error? = nil
+
     private func recalculateTransaction() {
         guard !recipientAddress.isEmpty,
               amount > 0,
               amount <= balance.spendable
         else {
             self.fee = 0
             self.tx = nil
+            self.transactionError = nil
             return
         }
 
         do {
             let (tx, fee) = try createTransaction()
 
             self.fee = fee
             self.tx = tx
+            self.transactionError = nil
         } catch {
             self.fee = 0
             self.tx = nil
+            self.transactionError = error
         }
     }

Then display transactionError in the UI when non-nil (e.g., as footer text below the fee section).

As per coding guidelines, "Implement proper loading and error states in SwiftUI views".

🤖 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/SendTransactionView.swift`
around lines 140 - 148, The catch block for createTransaction() currently
swallows errors; update it to assign the thrown error to the view's error state
(e.g., set self.error or self.transactionError = error), and still clear
self.fee and self.tx as before. Then ensure the view displays that error state
(render transactionError/error as footer text under the fee section) so users
see a meaningful message (invalid address, insufficient balance, signing
failure) instead of only a disabled Send button.
packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift (2)

409-432: ⚠️ Potential issue | 🟠 Major

Free strdup-allocated output addresses in the defer cleanup.

outputs.map { $0.toFFI() } allocates C strings for each output address via strdup, but Line 425-432 does not release them. This leaks memory on each transaction build.

#!/bin/bash
# Verify allocation in Transaction.Output.toFFI and missing free in buildSignedTransaction cleanup.
rg -n -C2 'func toFFI\(\)|strdup\(' packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift
rg -n -C5 'let ffiOutputs = outputs\.map \{ \$0\.toFFI\(\) \}|defer \{|transaction_bytes_free|error_message_free|free\(' packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
Proposed fix
         defer {
             if error.message != nil {
                 error_message_free(error.message)
             }
+            for output in ffiOutputs {
+                if let address = output.address {
+                    free(UnsafeMutableRawPointer(address))
+                }
+            }
             if let ptr = txBytesPtr {
                 transaction_bytes_free(ptr)
             }
         }

As 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/KeyWallet/WalletManager.swift` around
lines 409 - 432, The defer block after calling wallet_build_and_sign_transaction
leaks strdup-allocated C strings created by Transaction.Output.toFFI; update the
cleanup to iterate ffiOutputs and free each allocated address string (the C
pointer inside each FFI output struct produced by outputs.map { $0.toFFI() })
before or inside the existing defer that already frees error.message and
txBytesPtr so all FFI-allocated memory is released; ensure you reference the
same ffiOutputs array used to build outputsPtr and call the appropriate C free
function for those strdup pointers.

407-407: ⚠️ Potential issue | 🔴 Critical

Replace force-unwrap with a recoverable wallet-not-found error.

Line 407 can crash when getWallet(id:) returns nil (which is an expected path for NOT_FOUND). This should be guarded and thrown as a wallet error instead of force-unwrapped.

#!/bin/bash
# Verify nil-return behavior and force-unwrap use in this file.
rg -n -C2 'func getWallet\(id walletId: Data\) throws -> Wallet\?' packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
rg -n -C2 'NOT_FOUND|return nil' packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
rg -n -C2 'getWallet\(id: wallet\.walletId\)!\b' packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
Proposed fix
-        let wallet = try self.getWallet(id: wallet.walletId)!
+        guard let sdkWallet = try self.getWallet(id: wallet.walletId) else {
+            throw KeyWalletError.walletError("Wallet not found in manager")
+        }
@@
-                wallet.ffiHandle,
+                sdkWallet.ffiHandle,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift` at
line 407, The force-unwrap on the result of getWallet(id:) can crash when it
returns nil; replace the force-unwrap with a guard that throws a recoverable
wallet-not-found error (e.g. throw WalletError.notFound or
WalletManagerError.walletNotFound) including the walletId for context: call let
wallet = try self.getWallet(id: wallet.walletId) and if it is nil throw the
new/appropriate error; ensure the error type you use exists or add it to the
WalletManager/KeyWallet error enum and keep call sites that handle thrown errors
unchanged.
🤖 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/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift`:
- Around line 390-396: The docstring for
WalletManager.buildSignedTransaction(for: accIndex: outputs: feeRate:) is out of
sync: update the parameter list and description to reflect the actual API
(rename or document accIndex as accountIndex if you prefer consistent naming),
replace the stale `feePerKB` entry with `feeRate` (describe FeeRate
units/meaning), change the function summary to say it builds and returns a
signed transaction, and update the Returns line to indicate it returns signed
transaction bytes (Data) and the fee (UInt64). Ensure the parameter names in the
doc tags match the function signature (wallet, accIndex/accountIndex, outputs,
feeRate) and the wording makes clear the payload is signed.

---

Duplicate comments:
In `@packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift`:
- Around line 409-432: The defer block after calling
wallet_build_and_sign_transaction leaks strdup-allocated C strings created by
Transaction.Output.toFFI; update the cleanup to iterate ffiOutputs and free each
allocated address string (the C pointer inside each FFI output struct produced
by outputs.map { $0.toFFI() }) before or inside the existing defer that already
frees error.message and txBytesPtr so all FFI-allocated memory is released;
ensure you reference the same ffiOutputs array used to build outputsPtr and call
the appropriate C free function for those strdup pointers.
- Line 407: The force-unwrap on the result of getWallet(id:) can crash when it
returns nil; replace the force-unwrap with a guard that throws a recoverable
wallet-not-found error (e.g. throw WalletError.notFound or
WalletManagerError.walletNotFound) including the walletId for context: call let
wallet = try self.getWallet(id: wallet.walletId) and if it is nil throw the
new/appropriate error; ensure the error type you use exists or add it to the
WalletManager/KeyWallet error enum and keep call sites that handle thrown errors
unchanged.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift`:
- Around line 21-24: The computed property `amount` currently parses
`amountString` to a Double which causes precision loss; change it to parse using
Decimal (e.g., Decimal(string: amountString)) and perform exact multiplication
by 100_000_000 with appropriate rounding (use .plain or .bankers as desired)
before converting to UInt64, and ensure you handle nil/invalid input and
overflow safely; update the `amount` getter to use `Decimal` arithmetic and
rounding rather than `Double` to produce the correct duffs value.
- Around line 140-148: The catch block for createTransaction() currently
swallows errors; update it to assign the thrown error to the view's error state
(e.g., set self.error or self.transactionError = error), and still clear
self.fee and self.tx as before. Then ensure the view displays that error state
(render transactionError/error as footer text under the fee section) so users
see a meaningful message (invalid address, insufficient balance, signing
failure) instead of only a disabled Send button.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b9e83c22-e335-4a37-b8d8-683ba156a5a0

📥 Commits

Reviewing files that changed from the base of the PR and between 9d6e256 and 9d604c0.

📒 Files selected for processing (9)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift
💤 Files with no reviewable changes (1)
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift

@github-actions
Copy link
Contributor

github-actions bot commented Mar 17, 2026

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "3598343e6b170dea4b4050bd3be7f80ae17199a7b92b7c83d95db8226bbbaf62"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

Copy link
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

Trivial incremental push: 1-line change in example app (SendTransactionView.swift) adapting getBalance() call to updated API signature — removes accType parameter, renames accIndex to accountIndex. No new issues introduced.

Reviewed commit: 9d604c0

@ZocoLini ZocoLini force-pushed the feat/send-transaction-swift branch from 9d604c0 to 063b7cc Compare March 17, 2026 19:18
Copy link
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: 1

♻️ Duplicate comments (4)
packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift (1)

145-153: ⚠️ Potential issue | 🟡 Minor

Sync the doc comment with the new API surface.

The comment still documents accountIndex / feePerKB and only mentions returning transaction bytes, but the public signature takes accIndex / feeRate and returns (Data, UInt64). That mismatch will mislead callers and generated docs.

Suggested fix
     /// Build a signed transaction
     /// - Parameters:
-    ///   - accountIndex: The account index to use
+    ///   - wallet: The wallet to build the transaction for
+    ///   - accIndex: The account index to use
     ///   - outputs: The transaction outputs
-    ///   - feePerKB: Fee per kilobyte in satoshis
-    /// - Returns: The signed transaction bytes
+    ///   - feeRate: The fee priority to use for fee estimation
+    /// - Returns: The signed transaction bytes and computed fee in duffs
🤖 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 145 - 153, Update the doc comment on
CoreWalletManager.buildSignedTransaction to match the new API: replace outdated
parameter names/accountation (accountIndex, feePerKB) with accIndex and feeRate
and clearly describe what each represents (accIndex = account index used;
feeRate = FeeRate object used for fee calculation), and update the Returns
section to state it returns a tuple (Data, UInt64) where Data is the signed
transaction bytes and UInt64 is the computed fee in satoshis; ensure the param
and returns tags reference accIndex, outputs, feeRate and the (Data, UInt64)
return shape.
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift (2)

141-149: ⚠️ Potential issue | 🟠 Major

Don't collapse build failures into a disabled button.

This catch path clears fee and tx and drops the actual error, so invalid addresses, fee-too-high cases, and missing wallet state all look the same to the user. Keep a transaction-build error in view state and render it inline or through the existing alert.

As per coding guidelines, "Implement proper loading and error states in SwiftUI views".

🤖 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/SendTransactionView.swift`
around lines 141 - 149, The catch currently swallows createTransaction() errors
and resets fee/tx, making all failures invisible to the UI; instead add and set
a view state like transactionError (e.g. transactionBuildError: Error? or
String?) inside the catch and assign the caught error to it, and only clear
fee/tx if truly invalid—do not drop the error. Update the catch block that
surrounds createTransaction() to set self.transactionError = error (and
optionally self.tx = nil), leave or set fee appropriately, and make the UI
render transactionError inline or via the existing alert so the user sees
specific build failures.

21-24: ⚠️ Potential issue | 🔴 Critical

Reject inputs that are not an exact duff value.

This still accepts more than 8 fractional digits and then silently changes the amount during uint64Value conversion. In a send flow, values like 0.123456789 should fail validation, not turn into a smaller payment.

Suggested fix
     private var amount: UInt64 {
-        guard let dashAmount = Decimal(string: amountString) else { return 0 }
-        let duffAmount = dashAmount * 100_000_000
-        return NSDecimalNumber(decimal: duffAmount).uint64Value
+        guard let dashAmount = Decimal(string: amountString),
+              dashAmount >= 0
+        else { return 0 }
+
+        var value = dashAmount
+        var rounded = Decimal()
+        NSDecimalRound(&rounded, &value, 8, .plain)
+        guard rounded == dashAmount else { return 0 }
+
+        let duffAmount = rounded * 100_000_000
+        let number = NSDecimalNumber(decimal: duffAmount)
+        guard number != NSDecimalNumber.notANumber,
+              number.compare(NSDecimalNumber(value: UInt64.max)) != .orderedDescending
+        else { return 0 }
+
+        return number.uint64Value
     }
🤖 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/SendTransactionView.swift`
around lines 21 - 24, The computed property amount currently accepts
amountString values with >8 fractional digits and silently truncates when
converting to UInt64; update SendTransactionView.amount to explicitly validate
that Decimal(string: amountString) converts exactly to an integer number of
duffs (i.e., fractional digits ≤ 8 and decimal * 100_000_000 has no fractional
part) and reject/flag the input when it does not (for example by returning nil
or setting a validation error instead of silently returning 0). Use the existing
symbols amount, amountString and the duff multiplier (100_000_000) to locate the
logic and perform the exactness check before creating
NSDecimalNumber.uint64Value.
packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift (1)

411-437: ⚠️ Potential issue | 🔴 Critical

Free the duplicated output addresses in Swift.

Transaction.Output.toFFI() now allocates each address with strdup(), but this defer never releases those pointers. Because Swift performed the allocation, Swift should free them after wallet_build_and_sign_transaction returns.

Suggested fix
         defer {
             if error.message != nil {
                 error_message_free(error.message)
             }
             for output in ffiOutputs {
-              // TODO: Memory leak, FFI doesnt expose a way to free the address
+                if let address = output.address {
+                    free(UnsafeMutableRawPointer(address))
+                }
             }
             if let ptr = txBytesPtr {
                 transaction_bytes_free(ptr)
             }
         }

As 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/KeyWallet/WalletManager.swift` around
lines 411 - 437, The ffiOutputs array contains FFI structs produced by
Transaction.Output.toFFI() that strdup()'d the address string, but the defer
currently doesn't free those duplicated address pointers; after the
wallet_build_and_sign_transaction call you should free each allocated address
(e.g. iterate ffiOutputs and call free(...) on the struct's address field)
inside the defer (alongside the existing error_message_free and
transaction_bytes_free) so Swift reclaims the strdup()'d memory — use the
platform free (Darwin.C.free or Glibc.free) or libc.free as appropriate.
🤖 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/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift`:
- Around line 27-31: The canSend computed property risks overflow by evaluating
amount + fee; change its funds check to a subtraction-based guard such as
ensuring fee <= balance.spendable and amount <= balance.spendable - fee (or
extract a shared hasSufficientFunds(amount:fee:) helper and use it in canSend
and the footer) so the button gating and footer share the same safe logic;
update usages of canSend/footer insufficient-funds logic to call that helper or
the new subtraction-style condition and keep recipientAddress, amount, fee,
balance.spendable and tx checks intact.

---

Duplicate comments:
In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift`:
- Around line 145-153: Update the doc comment on
CoreWalletManager.buildSignedTransaction to match the new API: replace outdated
parameter names/accountation (accountIndex, feePerKB) with accIndex and feeRate
and clearly describe what each represents (accIndex = account index used;
feeRate = FeeRate object used for fee calculation), and update the Returns
section to state it returns a tuple (Data, UInt64) where Data is the signed
transaction bytes and UInt64 is the computed fee in satoshis; ensure the param
and returns tags reference accIndex, outputs, feeRate and the (Data, UInt64)
return shape.

In `@packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift`:
- Around line 411-437: The ffiOutputs array contains FFI structs produced by
Transaction.Output.toFFI() that strdup()'d the address string, but the defer
currently doesn't free those duplicated address pointers; after the
wallet_build_and_sign_transaction call you should free each allocated address
(e.g. iterate ffiOutputs and call free(...) on the struct's address field)
inside the defer (alongside the existing error_message_free and
transaction_bytes_free) so Swift reclaims the strdup()'d memory — use the
platform free (Darwin.C.free or Glibc.free) or libc.free as appropriate.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift`:
- Around line 141-149: The catch currently swallows createTransaction() errors
and resets fee/tx, making all failures invisible to the UI; instead add and set
a view state like transactionError (e.g. transactionBuildError: Error? or
String?) inside the catch and assign the caught error to it, and only clear
fee/tx if truly invalid—do not drop the error. Update the catch block that
surrounds createTransaction() to set self.transactionError = error (and
optionally self.tx = nil), leave or set fee appropriately, and make the UI
render transactionError inline or via the existing alert so the user sees
specific build failures.
- Around line 21-24: The computed property amount currently accepts amountString
values with >8 fractional digits and silently truncates when converting to
UInt64; update SendTransactionView.amount to explicitly validate that
Decimal(string: amountString) converts exactly to an integer number of duffs
(i.e., fractional digits ≤ 8 and decimal * 100_000_000 has no fractional part)
and reject/flag the input when it does not (for example by returning nil or
setting a validation error instead of silently returning 0). Use the existing
symbols amount, amountString and the duff multiplier (100_000_000) to locate the
logic and perform the exactness check before creating
NSDecimalNumber.uint64Value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89c037cd-2969-41c4-9d1f-c6327baa9102

📥 Commits

Reviewing files that changed from the base of the PR and between 9d604c0 and 063b7cc.

📒 Files selected for processing (9)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift
💤 Files with no reviewable changes (1)
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift

Copy link
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

Incremental push addressing 5 of 7 prior findings. The force-unwrap crash is fixed (guard let), floating-point rounding now uses Decimal, doc comments corrected, and the ~ prefix properly scoped to fee display only.

The memory leak in toFFI() is acknowledged with TODO comments but not actually freed — the for output in ffiOutputs { } loop body is empty. This is a deliberate deferral since the FFI doesn't expose a free method, which is reasonable, but the allocation still leaks on every recalculateTransaction() call.

The keystroke debouncing suggestion remains unaddressed.

Reviewed commit: 063b7cc

@ZocoLini ZocoLini force-pushed the feat/send-transaction-swift branch 2 times, most recently from 30e0c0e to a5a1e31 Compare March 17, 2026 20:17
Copy link
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: 2

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)

132-133: ⚠️ Potential issue | 🟠 Major

destroy() must be idempotent to prevent double-teardown in the SPVClient lifecycle.

When WalletService.initializeNewSPVClient() calls destroy() at line 148 and then reassigns spvClient, the old instance deinitializes and calls destroy() again at line 133. The current implementation unconditionally passes pointers to FFI functions without nil checks, so the second call invokes FFI teardown with nil pointers, causing undefined behavior. Guard the FFI calls with if let checks to make destroy() safe to call multiple times.

Proposed fix
    func destroy() {
-        dash_spv_ffi_client_destroy(client)
-        dash_spv_ffi_config_destroy(config)
-
-        client = nil
-        config = nil
+        if let client {
+            dash_spv_ffi_client_destroy(client)
+            self.client = nil
+        }
+        if let config {
+            dash_spv_ffi_config_destroy(config)
+            self.config = nil
+        }
    }

As 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 132 - 133, The destroy() implementation in SPVClient and its deinit must
be made idempotent: guard against nil/invalid pointers before calling any FFI
teardown functions so a second invocation is a no-op; update destroy() to check
optional FFI handles (e.g., the SPVClient's underlying pointer(s) and any
related handles used by the FFI teardown functions) with if let / guard let and
only call the FFI free/teardown once, then nil out/clear those stored pointers,
and leave deinit calling destroy() unchanged so
WalletService.initializeNewSPVClient() can safely call destroy() and the
subsequent deinit won’t re-invoke FFI with nil pointers.
🧹 Nitpick comments (1)
packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift (1)

390-425: Either expose feeRate here or drop the new abstraction for now.

Line 420 still hardcodes 1000, so .economy and .priority never influence transaction building even though Transaction.swift now defines FeeRate and this doc block still describes a feeRate input. Thread feeRate through this API, or keep a fixed-fee API until that end-to-end wiring exists.

Possible fix
-    public func buildSignedTransaction(for wallet: HDWallet, accIndex: UInt32, outputs: [Transaction.Output]) throws -> (Data, UInt64) {
+    public func buildSignedTransaction(
+        for wallet: HDWallet,
+        accIndex: UInt32,
+        outputs: [Transaction.Output],
+        feeRate: FeeRate = .normal
+    ) throws -> (Data, UInt64) {
@@
-                1000,
+                feeRate.intoDuffPerKB(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift` around
lines 390 - 425, The doc and Transaction.FeeRate enum indicate feeRate should be
configurable, but buildSignedTransaction(for:accIndex:outputs:) still hardcodes
1000 when calling wallet_build_and_sign_transaction; thread a FeeRate parameter
through this API (e.g., add feeRate: Transaction.FeeRate to
buildSignedTransaction) and map the enum to the corresponding duffs-per-kb
numeric value to replace the hardcoded 1000 in the
wallet_build_and_sign_transaction call, update the function signature and all
callers, and adjust the doc comment to reflect the new parameter; alternatively,
if you want fixed-fee behavior, remove the feeRate mention from the doc and keep
the hardcoded value.
🤖 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/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift`:
- Around line 107-128: Replace the force-throws in initializeNewSPVClient by
wrapping the SPVClient(...) construction, CoreWalletManager(spvClient:...,
modelContainer:...), and any setMasternodeSyncEnabled(...) calls in do-catch
blocks: call them with try, catch the thrown Error, assign it to lastSyncError,
and return/abort the rest of initialization so the app doesn’t crash; keep
existing stopSync()/clearSpvStorage()/switchNetwork paths intact but ensure
initializeNewSPVClient sets lastSyncError instead of crashing. Target the
initializeNewSPVClient function and the specific constructors/methods SPVClient,
CoreWalletManager, and setMasternodeSyncEnabled when applying the changes.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift`:
- Around line 20-24: The computed amount property silently truncates precision
beyond 8 fractional DASH digits and allows negative inputs; update the amount
getter (which parses amountString into a Decimal and multiplies by 100_000_000)
to validate before converting: first ensure the parsed Decimal is non‑negative,
then multiply by 100_000_000 into a Decimal/NSDecimalNumber and reject the input
if that product has any fractional part (i.e., more precision than whole duffs)
or if the original string has >8 fractional digits; if validation fails return 0
(or signal invalid input) rather than truncating, and only then safely convert
NSDecimalNumber(...).uint64Value. Use the existing amountString, amount getter,
dashAmount/duffAmount variables to locate where to add these checks.

---

Outside diff comments:
In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift`:
- Around line 132-133: The destroy() implementation in SPVClient and its deinit
must be made idempotent: guard against nil/invalid pointers before calling any
FFI teardown functions so a second invocation is a no-op; update destroy() to
check optional FFI handles (e.g., the SPVClient's underlying pointer(s) and any
related handles used by the FFI teardown functions) with if let / guard let and
only call the FFI free/teardown once, then nil out/clear those stored pointers,
and leave deinit calling destroy() unchanged so
WalletService.initializeNewSPVClient() can safely call destroy() and the
subsequent deinit won’t re-invoke FFI with nil pointers.

---

Nitpick comments:
In `@packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift`:
- Around line 390-425: The doc and Transaction.FeeRate enum indicate feeRate
should be configurable, but buildSignedTransaction(for:accIndex:outputs:) still
hardcodes 1000 when calling wallet_build_and_sign_transaction; thread a FeeRate
parameter through this API (e.g., add feeRate: Transaction.FeeRate to
buildSignedTransaction) and map the enum to the corresponding duffs-per-kb
numeric value to replace the hardcoded 1000 in the
wallet_build_and_sign_transaction call, update the function signature and all
callers, and adjust the doc comment to reflect the new parameter; alternatively,
if you want fixed-fee behavior, remove the feeRate mention from the doc and keep
the hardcoded value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32c4cb22-636e-41ab-a00f-24a44ef402ef

📥 Commits

Reviewing files that changed from the base of the PR and between 063b7cc and 30e0c0e.

📒 Files selected for processing (9)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift
💤 Files with no reviewable changes (1)
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift

Copy link
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.

♻️ Duplicate comments (2)
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift (1)

127-135: ⚠️ Potential issue | 🟠 Major

Do not silently swallow transaction-build failures in recalculation.

When build/sign fails, Lines 133-134 clear fee/tx but do not surface any reason, leaving users with a disabled Send button and no actionable feedback.

Suggested direction
+    `@State` private var transactionBuildError: String?
@@
         do {
             let (tx, fee) = try createTransaction()
 
             self.fee = fee
             self.tx = tx
+            self.transactionBuildError = nil
         } catch {
             self.fee = 0
             self.tx = nil
+            self.transactionBuildError = error.localizedDescription
         }

As per coding guidelines, "Implement proper loading and error states in SwiftUI views".

🤖 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/SendTransactionView.swift`
around lines 127 - 135, The recalculation catch block currently swallows errors
and clears fee/tx; instead capture and surface the error so the UI can show a
proper loading/error state: in SendTransactionView add or use an `@State`
transactionError (or errorMessage) and set it inside the catch with the
localized error string, keep previous fee/tx or set them to nil intentionally,
and trigger an alert/inline error view and disable the Send button based on that
error state; ensure the catch for createTransaction() stores
error.localizedDescription (or Error) so build/sign failures are visible to the
user.
packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift (1)

411-433: ⚠️ Potential issue | 🟠 Major

Free FFI output-address buffers during cleanup (currently left as TODO).

Line 432 leaves address cleanup unimplemented. If Transaction.Output.toFFI() allocates address C strings, this leaks one buffer per output per transaction build.

Proposed fix
         defer {
             if error.message != nil {
                 error_message_free(error.message)
             }
             for output in ffiOutputs {
-              // TODO: Memory leak, FFI doesnt expose a way to free the address
+                if let address = output.address {
+                    free(UnsafeMutableRawPointer(mutating: address))
+                }
             }
             if let ptr = txBytesPtr {
                 transaction_bytes_free(ptr)
             }
         }
#!/bin/bash
# Verify allocation/free symmetry for transaction output FFI structs.
rg -nP --type swift -C4 'func\s+toFFI\s*\(|strdup\s*\(' packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift
rg -nP --type swift -C6 'ffiOutputs|wallet_build_and_sign_transaction|transaction_bytes_free|free\s*\(' packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift

As 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/KeyWallet/WalletManager.swift` around
lines 411 - 433, The ffiOutputs produced by outputs.map { $0.toFFI() } allocate
C string memory for each output address which is not freed in the defer block;
update the cleanup to free those buffers by invoking the appropriate C free on
each ffiOutput.address (i.e., free(output.address) or the matching FFI-provided
deallocator if one exists) for every element of ffiOutputs inside the defer
(alongside the existing error_message_free); ensure you reference
Transaction.Output.toFFI(), the ffiOutputs array, and the
wallet_build_and_sign_transaction call so the free is colocated with the
existing cleanup for txBytesPtr/error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift`:
- Around line 411-433: The ffiOutputs produced by outputs.map { $0.toFFI() }
allocate C string memory for each output address which is not freed in the defer
block; update the cleanup to free those buffers by invoking the appropriate C
free on each ffiOutput.address (i.e., free(output.address) or the matching
FFI-provided deallocator if one exists) for every element of ffiOutputs inside
the defer (alongside the existing error_message_free); ensure you reference
Transaction.Output.toFFI(), the ffiOutputs array, and the
wallet_build_and_sign_transaction call so the free is colocated with the
existing cleanup for txBytesPtr/error.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift`:
- Around line 127-135: The recalculation catch block currently swallows errors
and clears fee/tx; instead capture and surface the error so the UI can show a
proper loading/error state: in SendTransactionView add or use an `@State`
transactionError (or errorMessage) and set it inside the catch with the
localized error string, keep previous fee/tx or set them to nil intentionally,
and trigger an alert/inline error view and disable the Send button based on that
error state; ensure the catch for createTransaction() stores
error.localizedDescription (or Error) so build/sign failures are visible to the
user.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 835777ca-5dee-49c8-9ebe-17e6f8cb57c2

📥 Commits

Reviewing files that changed from the base of the PR and between 30e0c0e and a5a1e31.

📒 Files selected for processing (9)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift
💤 Files with no reviewable changes (1)
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift

@ZocoLini ZocoLini force-pushed the feat/send-transaction-swift branch from a5a1e31 to c8acaef Compare March 17, 2026 22:58
Copy link
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

This push simplifies the API surface (async→sync, throws→optional) and switches to whole-wallet balance aggregation, but introduces a correctness regression: the send flow validates against the aggregate wallet balance while still building transactions from account 0 only. The preconditionFailure replacing throw at the FFI boundary is also problematic — one corrupt account handle crashes the entire app.

Reviewed commit: c8acaef

🔴 2 blocking | 🟡 1 suggestion(s) | 💬 1 nitpick(s)

2 additional findings

🔴 blocking: preconditionFailure on FFI failure crashes the app with no recovery

packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift (lines 117-125)

managed_core_account_get_balance is a C FFI function — failure modes depend on the Rust implementation and handle validity. Replacing throw with preconditionFailure means any FFI failure (dangling handle, Rust-side panic, memory corruption) terminates the process. This function is called in a loop over all account types in getAccounts() — a single bad account handle takes down the entire wallet UI.

The previous code used try? at call sites to gracefully skip failing accounts. Now those same code paths crash instead. Consider returning a zero Balance on failure (or Balance?) to maintain the non-throwing simplification without the crash risk.

💡 Suggested change
    public func getBalance() -> Balance {
        var ffiBalance = FFIBalance()
        let success = managed_core_account_get_balance(handle, &ffiBalance)

        guard success else {
            // Return zero balance if FFI call fails unexpectedly
            return Balance(confirmed: 0, unconfirmed: 0, immature: 0, locked: 0)
        }

        return Balance(ffiBalance: ffiBalance)
    }
🟡 suggestion: getManagedAccountCollection silently discards FFI error information

packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift (lines 539-561)

Changed from throwing KeyWalletError(ffiError: error) to returning nil. The FFI error message is freed (line 553) without being logged or surfaced. Callers now return empty arrays (getAccounts), nil (getAccountDetails), or zero balances (getBalance) — all indistinguishable from 'wallet has no accounts'. A corrupted wallet, locked database, or Rust-side failure becomes invisible.

Consider at minimum logging the FFI error before returning nil to preserve diagnostic information.

🤖 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/SendTransactionView.swift`:
- [BLOCKING] lines 29-35: Send form validates against whole-wallet balance but builds transaction from account 0 only
  `balance` (line 34) now calls `getBalance(for: wallet)` which aggregates confirmed/unconfirmed/immature/locked across **every** managed account (BIP44, BIP32, CoinJoin, Identity, Provider). But `createTransaction()` (line 158) still passes `accIndex: 0` to `buildSignedTransaction`, which only selects UTXOs from a single BIP44 account.

In a wallet with funds split across accounts, the UI shows enough balance and enables the Send button, but the FFI transaction builder fails or builds a transaction the user didn't intend because account 0 may not have sufficient funds.

Either restrict `getBalance()` to the same account used for signing, or update the transaction builder to support whole-wallet coin selection.

In `packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift`:
- [BLOCKING] lines 117-125: preconditionFailure on FFI failure crashes the app with no recovery
  `managed_core_account_get_balance` is a C FFI function — failure modes depend on the Rust implementation and handle validity. Replacing `throw` with `preconditionFailure` means any FFI failure (dangling handle, Rust-side panic, memory corruption) terminates the process. This function is called in a loop over all account types in `getAccounts()` — a single bad account handle takes down the entire wallet UI.

The previous code used `try?` at call sites to gracefully skip failing accounts. Now those same code paths crash instead. Consider returning a zero `Balance` on failure (or `Balance?`) to maintain the non-throwing simplification without the crash risk.

In `packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift`:
- [SUGGESTION] lines 539-561: getManagedAccountCollection silently discards FFI error information
  Changed from throwing `KeyWalletError(ffiError: error)` to returning `nil`. The FFI error message is freed (line 553) without being logged or surfaced. Callers now return empty arrays (`getAccounts`), `nil` (`getAccountDetails`), or zero balances (`getBalance`) — all indistinguishable from 'wallet has no accounts'. A corrupted wallet, locked database, or Rust-side failure becomes invisible.

Consider at minimum logging the FFI error before returning `nil` to preserve diagnostic information.

@ZocoLini ZocoLini marked this pull request as draft March 18, 2026 05:28
@ZocoLini ZocoLini marked this pull request as draft March 18, 2026 05:28
@ZocoLini ZocoLini force-pushed the feat/send-transaction-swift branch 2 times, most recently from a9870fe to 7440763 Compare March 18, 2026 13:46
@ZocoLini ZocoLini marked this pull request as ready for review March 18, 2026 13:48
Copy link
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

The send-transaction feature has a confirmed memory leak at the Swift→FFI boundary that is amplified by per-keystroke transaction rebuilding. The FeeRate enum described in the PR is defined but never wired into the actual transaction-building path. No critical security or correctness issues beyond the memory leak.

Reviewed commit: 7440763

🔴 1 blocking | 🟡 3 suggestion(s) | 💬 1 nitpick(s)

🤖 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/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift`:
- [BLOCKING] lines 426-436: strdup'd address strings are never freed — memory leak on every call
  `toFFI()` allocates each output address with `strdup` (Transaction.swift:34), but the defer block at lines 430-432 iterates `ffiOutputs` with an empty body — nothing calls `free()`. The TODO comment says "FFI doesnt expose a way to free the address", but since `strdup` allocates on the C/Swift heap, plain `free()` is the correct deallocator. Combined with the per-keystroke rebuild from `SendTransactionView`, this leaks one C string per output on every character typed.

In `packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift`:
- [SUGGESTION] lines 4-16: FeeRate enum is defined but never used — fee hardcoded to 1000
  The `FeeRate` enum with `.economy`/`.normal`/`.priority` variants is added but never referenced. `buildSignedTransaction` hardcodes `1000` (WalletManager.swift:419) and doesn't accept a `FeeRate` parameter. Either wire the enum into the build path or remove it to avoid dead code.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift`:
- [SUGGESTION] lines 99-104: Transaction build+sign on every keystroke with no debounce
  `.onChange(of: recipientAddress)` and `.onChange(of: amountString)` call `recalculateTransaction()` on every character. Each invocation crosses into Rust for UTXO selection and cryptographic signing — CPU-intensive work on the main thread. Consider debouncing (e.g., a short `Task.sleep` cancellation pattern) or triggering recalculation only when both fields have plausible values.

In `packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift`:
- [SUGGESTION] lines 210-212: Synchronous broadcast on @MainActor may freeze UI
  `WalletService` is `@MainActor` and `broadcastTransaction` is synchronous. The underlying FFI call `dash_spv_ffi_client_broadcast_transaction` may perform blocking I/O. If it does, the UI thread freezes until the broadcast completes. Consider making this `async` and dispatching the FFI call off the main actor.

@ZocoLini ZocoLini force-pushed the feat/send-transaction-swift branch from 7440763 to 5adc466 Compare March 18, 2026 15:43
Copy link
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

Incremental push removes the unused FeeRate enum from Transaction.swift (14 lines deleted, 0 added). This directly addresses the prior suggestion about dead code. No new code introduced — remaining prior findings (memory leak in strdup, no debounce on keystroke builds, synchronous broadcast on @MainActor, silent error swallowing) are still valid from the previous review and carry forward.

Reviewed commit: 5adc466

@ZocoLini ZocoLini force-pushed the feat/send-transaction-swift branch from 5adc466 to e86f385 Compare March 18, 2026 19:57
Copy link
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

The delta fixes a sighash mismatch in bundle building, realigns the Swift SPVSyncState enum with the updated dash-spv-ffi dependency (rev 42eb1d6, updated in commit c4e64a7), refactors the wallet list UI, and adds the empty-loop suppression in WalletManager. The codex-ffi BLOCKING claim that raw values were remapped before the FFI producer changed is a false positive — the dependency was updated earlier in the same PR. No blocking issues remain in this delta.

Reviewed commit: e86f385

🟡 1 suggestion(s) | 💬 2 nitpick(s)

2 additional findings

🟡 suggestion: waitingForConnections is no longer treated as an active sync state

packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift (lines 28-44)

Both isSyncing() and isRunning() now return false for .waitingForConnections. This was intentional (commit a0efb0c says "match dash-spv implementation"), but it changes behavior for callers: WalletService.clearSpvStorage() gates on isRunning(), so storage can now be cleared while a sync task is alive but waiting for peer connections. Similarly, the UI "Clear" button in CoreContentView becomes enabled during this state. If the upstream Rust semantics treat this state as inactive, this is correct — but worth confirming the SPV client safely handles a storage clear while in waitingForConnections.

💬 nitpick: navigationTitle("Wallets") removed — navigation bar has no title

packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift (lines 60-215)

The refactor from toolbar-based "+" button to section-header-based "+" button also removed .navigationTitle("Wallets") and the .toolbar modifier from the List. Since CoreContentView is embedded in a NavigationStack (via CoreWalletView in ContentView.swift:158), the navigation bar will render without a title. The section header text "Wallets (network)" partially replaces the visual function, but the nav bar itself is blank.

🤖 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/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift`:
- [SUGGESTION] lines 28-44: waitingForConnections is no longer treated as an active sync state
  Both `isSyncing()` and `isRunning()` now return `false` for `.waitingForConnections`. This was intentional (commit a0efb0ca8 says "match dash-spv implementation"), but it changes behavior for callers: `WalletService.clearSpvStorage()` gates on `isRunning()`, so storage can now be cleared while a sync task is alive but waiting for peer connections. Similarly, the UI "Clear" button in CoreContentView becomes enabled during this state. If the upstream Rust semantics treat this state as inactive, this is correct — but worth confirming the SPV client safely handles a storage clear while in `waitingForConnections`.

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.

3 participants