Skip to content

Commit e731fb4

Browse files
key-wallet: improve FFI safety in Transaction and add helpers
- Use stable C strings for output addresses and free them to avoid dangling pointers - Switch to typed withUnsafeBytes pointers for clarity and safety - Add buildAndSign(managedWallet:wallet:...) convenience API - Add getTxid(from:) helper to extract transaction ID
1 parent bb2c145 commit e731fb4

File tree

1 file changed

+139
-15
lines changed

1 file changed

+139
-15
lines changed

packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift

Lines changed: 139 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import Darwin
23
import DashSDKFFI
34

45
/// Transaction utilities for wallet operations
@@ -39,10 +40,23 @@ public class Transaction {
3940
var error = FFIError()
4041
var txBytesPtr: UnsafeMutablePointer<UInt8>?
4142
var txLen: size_t = 0
42-
43-
// Convert outputs to FFI format
44-
let ffiOutputs = outputs.map { $0.toFFI() }
45-
43+
// Convert outputs to FFI format with stable C strings
44+
var cStrings: [UnsafeMutablePointer<CChar>] = []
45+
cStrings.reserveCapacity(outputs.count)
46+
var ffiOutputs: [FFITxOutput] = []
47+
ffiOutputs.reserveCapacity(outputs.count)
48+
49+
for output in outputs {
50+
let cstr = output.address.withCString { strdup($0) }
51+
guard let cstr else {
52+
// Free any previously allocated strings before throwing
53+
for ptr in cStrings { free(ptr) }
54+
throw KeyWalletError.invalidInput("Failed to allocate C string for address")
55+
}
56+
cStrings.append(cstr)
57+
ffiOutputs.append(FFITxOutput(address: UnsafePointer(cstr), amount: output.amount))
58+
}
59+
4660
let success = ffiOutputs.withUnsafeBufferPointer { outputsPtr in
4761
wallet_build_transaction(
4862
wallet.ffiHandle,
@@ -57,6 +71,8 @@ public class Transaction {
5771
}
5872

5973
defer {
74+
// Free allocated C strings
75+
for ptr in cStrings { free(ptr) }
6076
if error.message != nil {
6177
error_message_free(error.message)
6278
}
@@ -89,8 +105,7 @@ public class Transaction {
89105
var signedTxPtr: UnsafeMutablePointer<UInt8>?
90106
var signedLen: size_t = 0
91107

92-
let success = transactionData.withUnsafeBytes { txBytes in
93-
let txPtr = txBytes.bindMemory(to: UInt8.self).baseAddress
108+
let success = transactionData.withUnsafeBytes { (txPtr: UnsafePointer<UInt8>) in
94109
return wallet_sign_transaction(
95110
wallet.ffiHandle,
96111
NetworkSet(wallet.network).ffiNetworks,
@@ -113,10 +128,122 @@ public class Transaction {
113128

114129
// Copy the signed transaction data before freeing
115130
let signedData = Data(bytes: ptr, count: signedLen)
116-
131+
117132
return signedData
118133
}
119-
134+
135+
/// Build and sign a transaction in one step using managed wallet
136+
/// - Parameters:
137+
/// - managedWallet: The managed wallet with UTXO information
138+
/// - wallet: The wallet with private keys for signing
139+
/// - accountIndex: The account index to use
140+
/// - outputs: The transaction outputs
141+
/// - feePerKB: Fee per kilobyte in satoshis
142+
/// - currentHeight: Current blockchain height for UTXO selection
143+
/// - Returns: The signed transaction bytes ready for broadcast
144+
public static func buildAndSign(managedWallet: ManagedWallet,
145+
wallet: Wallet,
146+
accountIndex: UInt32 = 0,
147+
outputs: [Output],
148+
feePerKB: UInt64,
149+
currentHeight: UInt32) throws -> Data {
150+
guard !outputs.isEmpty else {
151+
throw KeyWalletError.invalidInput("Transaction must have at least one output")
152+
}
153+
154+
guard !wallet.isWatchOnly else {
155+
throw KeyWalletError.invalidState("Cannot sign with watch-only wallet")
156+
}
157+
158+
var error = FFIError()
159+
var txBytesPtr: UnsafeMutablePointer<UInt8>?
160+
var txLen: size_t = 0
161+
162+
// Get managed wallet handle
163+
guard let managedHandle = managedWallet.getInfoHandle() else {
164+
throw KeyWalletError.invalidState("Failed to get managed wallet handle")
165+
}
166+
167+
// Convert outputs to FFI format with stable C strings
168+
var cStrings: [UnsafeMutablePointer<CChar>] = []
169+
cStrings.reserveCapacity(outputs.count)
170+
var ffiOutputs: [FFITxOutput] = []
171+
ffiOutputs.reserveCapacity(outputs.count)
172+
173+
for output in outputs {
174+
let cstr = output.address.withCString { strdup($0) }
175+
guard let cstr else {
176+
for ptr in cStrings { free(ptr) }
177+
throw KeyWalletError.invalidInput("Failed to allocate C string for address")
178+
}
179+
cStrings.append(cstr)
180+
ffiOutputs.append(FFITxOutput(address: UnsafePointer(cstr), amount: output.amount))
181+
}
182+
183+
let success = ffiOutputs.withUnsafeBufferPointer { outputsPtr in
184+
wallet_build_and_sign_transaction(
185+
managedHandle,
186+
wallet.ffiHandle,
187+
wallet.network.ffiValue,
188+
accountIndex,
189+
outputsPtr.baseAddress,
190+
outputs.count,
191+
feePerKB,
192+
currentHeight,
193+
&txBytesPtr,
194+
&txLen,
195+
&error)
196+
}
197+
198+
defer {
199+
for ptr in cStrings { free(ptr) }
200+
if error.message != nil {
201+
error_message_free(error.message)
202+
}
203+
if let ptr = txBytesPtr {
204+
transaction_bytes_free(ptr)
205+
}
206+
}
207+
208+
guard success, let ptr = txBytesPtr else {
209+
throw KeyWalletError(ffiError: error)
210+
}
211+
212+
// Copy the transaction data before freeing
213+
let txData = Data(bytes: ptr, count: txLen)
214+
215+
return txData
216+
}
217+
218+
/// Extract TXID from raw transaction bytes
219+
/// - Parameter transactionData: The transaction bytes
220+
/// - Returns: The transaction ID as a hex string
221+
public static func getTxid(from transactionData: Data) throws -> String {
222+
var error = FFIError()
223+
var txidPtr: UnsafeMutablePointer<CChar>?
224+
225+
let success = transactionData.withUnsafeBytes { txBytes in
226+
let txPtr = txBytes.bindMemory(to: UInt8.self).baseAddress
227+
txidPtr = transaction_get_txid_from_bytes(txPtr, transactionData.count, &error)
228+
return txidPtr != nil
229+
}
230+
231+
defer {
232+
if error.message != nil {
233+
error_message_free(error.message)
234+
}
235+
if let ptr = txidPtr {
236+
string_free(ptr)
237+
}
238+
}
239+
240+
guard success, let ptr = txidPtr else {
241+
throw KeyWalletError(ffiError: error)
242+
}
243+
244+
return String(cString: ptr)
245+
}
246+
120247
/// Check if a transaction belongs to a wallet
121248
/// - Parameters:
122249
/// - wallet: The wallet to check against
@@ -137,12 +264,10 @@ public class Transaction {
137264
var error = FFIError()
138265
var result = FFITransactionCheckResult()
139266

140-
let success = transactionData.withUnsafeBytes { txBytes in
141-
let txPtr = txBytes.bindMemory(to: UInt8.self).baseAddress
267+
let success = transactionData.withUnsafeBytes { (txPtr: UnsafePointer<UInt8>) in
142268

143269
if let hash = blockHash {
144-
return hash.withUnsafeBytes { hashBytes in
145-
let hashPtr = hashBytes.bindMemory(to: UInt8.self).baseAddress
270+
return hash.withUnsafeBytes { (hashPtr: UnsafePointer<UInt8>) in
146271

147272
return wallet_check_transaction(
148273
wallet.ffiHandle,
@@ -181,8 +306,7 @@ public class Transaction {
181306
public static func classify(_ transactionData: Data) throws -> String {
182307
var error = FFIError()
183308

184-
let classificationPtr = transactionData.withUnsafeBytes { txBytes in
185-
let txPtr = txBytes.bindMemory(to: UInt8.self).baseAddress
309+
let classificationPtr = transactionData.withUnsafeBytes { (txPtr: UnsafePointer<UInt8>) in
186310
return transaction_classify(txPtr, transactionData.count, &error)
187311
}
188312

@@ -201,4 +325,4 @@ public class Transaction {
201325

202326
return classification
203327
}
204-
}
328+
}

0 commit comments

Comments
 (0)