Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,28 +1,132 @@
import Foundation
import DashSDKFFI

// This struct is not mapping all fields of FFITransactionRecord
// for the lack of wrappers
public enum TransactionType {
case standard
case coinJoin
case providerRegistration
case providerUpdateRegister
case providerUpdateService
case providerUpdateRevocation
case assetLock
case assetUnlock
case coinbase
case ignored

init(ffi: FFITransactionType) {
switch ffi {
case FFI_TRANSACTION_TYPE_STANDARD: self = .standard
case FFI_TRANSACTION_TYPE_COIN_JOIN: self = .coinJoin
case FFI_TRANSACTION_TYPE_PROVIDER_REGISTRATION: self = .providerRegistration
case FFI_TRANSACTION_TYPE_PROVIDER_UPDATE_REGISTRAR: self = .providerUpdateRegister
case FFI_TRANSACTION_TYPE_PROVIDER_UPDATE_SERVICE: self = .providerUpdateService
case FFI_TRANSACTION_TYPE_PROVIDER_UPDATE_REVOCATION: self = .providerUpdateRevocation
case FFI_TRANSACTION_TYPE_ASSET_LOCK: self = .assetLock
case FFI_TRANSACTION_TYPE_ASSET_UNLOCK: self = .assetUnlock
case FFI_TRANSACTION_TYPE_COINBASE: self = .coinbase
case FFI_TRANSACTION_TYPE_IGNORED: self = .ignored
default: fatalError("Unknown FFITransactionType value: \(ffi)")
}
}
}

public enum TransactionDirection {
case incoming
case outgoing
case internalDir
case coinjoin

init(ffi: FFITransactionDirection) {
switch ffi {
case FFI_TRANSACTION_DIRECTION_INCOMING: self = .incoming
case FFI_TRANSACTION_DIRECTION_OUTGOING: self = .outgoing
case FFI_TRANSACTION_DIRECTION_INTERNAL: self = .internalDir
case FFI_TRANSACTION_DIRECTION_COIN_JOIN: self = .coinjoin
default: fatalError("Unknown FFITransactionDirection value: \(ffi)")
}
}
}

public struct InputDetail {
public let index: UInt32
public let value: UInt64
public let address: String

public init(ffi: FFIInputDetail) {
self.index = ffi.index
self.value = ffi.value
self.address = ffi.address != nil
? String(cString: ffi.address)
: ""
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public enum OutputRole {
case received
case change
case sent
Comment on lines +62 to +67
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.

🟡 Suggestion: InputDetail.address erases FFI nullability into an empty string

FFIInputDetail.address is an optional C string pointer, but the new wrapper exposes it as a non-optional String and substitutes "" when the pointer is NULL. That collapses two different boundary states — 'no address was provided by the FFI record' and 'the address is the empty string' — into the same Swift value. This file already preserves that distinction for sibling fields like label, so the safer mapping here is also to keep the address optional on the Swift side instead of silently inventing a value.

💡 Suggested change
Suggested change
}
public enum OutputRole {
case received
case change
case sent
public struct InputDetail {
public let index: UInt32
public let value: UInt64
public let address: String?
public init(ffi: FFIInputDetail) {
self.index = ffi.index
self.value = ffi.value
self.address = ffi.address.map { String(cString: $0) }
}
}

source: ['claude', '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/KeyWallet/TransactionRecord.swift`:
- [SUGGESTION] lines 62-67: `InputDetail.address` erases FFI nullability into an empty string
  `FFIInputDetail.address` is an optional C string pointer, but the new wrapper exposes it as a non-optional `String` and substitutes `""` when the pointer is `NULL`. That collapses two different boundary states — 'no address was provided by the FFI record' and 'the address is the empty string' — into the same Swift value. This file already preserves that distinction for sibling fields like `label`, so the safer mapping here is also to keep the address optional on the Swift side instead of silently inventing a value.

case unspendable

init(ffi: FFIOutputRole) {
switch ffi {
case FFI_OUTPUT_ROLE_RECEIVED: self = .received
case FFI_OUTPUT_ROLE_CHANGE: self = .change
case FFI_OUTPUT_ROLE_SENT: self = .sent
case FFI_OUTPUT_ROLE_UNSPENDABLE: self = .unspendable
default: fatalError("Unknown FFIOutputRole value: \(ffi)")
Comment thread
ZocoLini marked this conversation as resolved.
}
}
}

public struct OutputDetail {
public let index: UInt32
public let role: OutputRole
Comment on lines +23 to +83
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: Unknown FFI enum values now crash transaction decoding

TransactionType.init(ffi:), TransactionDirection.init(ffi:), and OutputRole.init(ffi:) all end in fatalError for any discriminant the Swift source does not recognize. That is a bad failure mode on an FFI boundary: NotOwnedTransactionRecord(handle:) is used in both ManagedAccount.getTransactions() and the SPV event callback, so any enum value added on the Rust/C side before this Swift wrapper is updated will terminate the host app while decoding a transaction record. The rest of this SDK already treats raw FFI enums defensively — for example TransactionContextType(ffiContext:) falls back with rawValue ?? .mempool and several SPV wrappers map unknown raw values to .unknown — so this change introduces a new crash path instead of following the existing boundary contract.

source: ['claude', '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/KeyWallet/TransactionRecord.swift`:
- [BLOCKING] lines 23-83: Unknown FFI enum values now crash transaction decoding
  `TransactionType.init(ffi:)`, `TransactionDirection.init(ffi:)`, and `OutputRole.init(ffi:)` all end in `fatalError` for any discriminant the Swift source does not recognize. That is a bad failure mode on an FFI boundary: `NotOwnedTransactionRecord(handle:)` is used in both `ManagedAccount.getTransactions()` and the SPV event callback, so any enum value added on the Rust/C side before this Swift wrapper is updated will terminate the host app while decoding a transaction record. The rest of this SDK already treats raw FFI enums defensively — for example `TransactionContextType(ffiContext:)` falls back with `rawValue ?? .mempool` and several SPV wrappers map unknown raw values to `.unknown` — so this change introduces a new crash path instead of following the existing boundary contract.


public init(ffi: FFIOutputDetail) {
self.index = ffi.index
self.role = OutputRole(ffi: ffi.role)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public struct NotOwnedTransactionRecord {
let txid: Data
let net_amount: Int64
let context: TransactionContext
let fee: UInt64
let tx_data: Data
let label: String?
public let txid: Data
public let netAmount: Int64
public let context: TransactionContext
public let transactionType: TransactionType
public let direction: TransactionDirection
public let fee: UInt64
public let inputDetails: [InputDetail]
public let outputDetails: [OutputDetail]
public let txData: Data
public let label: String?

public init(handle: UnsafePointer<FFITransactionRecord>) {
let p = handle.pointee

self.txid = withUnsafeBytes(of: p.txid) { Data($0) }
self.net_amount = p.net_amount
self.netAmount = p.net_amount
self.fee = p.fee
self.tx_data = p.tx_data != nil
self.txData = p.tx_data != nil
? Data(bytes: p.tx_data, count: p.tx_len)
: Data()
self.label = p.label != nil
? String(cString: p.label)
: nil

self.context = TransactionContext(ffi: p.context)
self.transactionType = TransactionType(ffi: p.transaction_type)
self.direction = TransactionDirection(ffi: p.direction)

self.inputDetails = p.input_details != nil
? (0..<p.input_details_count).map { i in
InputDetail(ffi: p.input_details[i])
}
: []

self.outputDetails = p.output_details != nil
? (0..<p.output_details_count).map { i in
OutputDetail(ffi: p.output_details[i])
}
: []
}
}
Loading