Skip to content
Open
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion packages/rs-sdk-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ rs-sdk-trusted-context-provider = { path = "../rs-sdk-trusted-context-provider",
simple-signer = { path = "../simple-signer" }

# Core SDK integration (always included for unified SDK)
dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", tag = "v0.40.0", optional = true }
dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", branch = "v0.41-dev", optional = true }

# FFI and serialization
serde = { version = "1.0", features = ["derive"] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,9 +419,9 @@ public class ManagedWallet {
return utxos
}

// MARK: - Private Helpers
private func getInfoHandle() -> UnsafeMutablePointer<FFIManagedWalletInfo>? {
// MARK: - Internal Helpers

internal func getInfoHandle() -> UnsafeMutablePointer<FFIManagedWalletInfo>? {
// The handle is an FFIManagedWalletInfo* (opaque C handle)
return handle
}
Expand Down
154 changes: 139 additions & 15 deletions packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import Darwin
import DashSDKFFI

/// Transaction utilities for wallet operations
Expand Down Expand Up @@ -39,10 +40,23 @@ public class Transaction {
var error = FFIError()
var txBytesPtr: UnsafeMutablePointer<UInt8>?
var txLen: size_t = 0

// Convert outputs to FFI format
let ffiOutputs = outputs.map { $0.toFFI() }

// Convert outputs to FFI format with stable C strings
var cStrings: [UnsafeMutablePointer<CChar>] = []
cStrings.reserveCapacity(outputs.count)
var ffiOutputs: [FFITxOutput] = []
ffiOutputs.reserveCapacity(outputs.count)

for output in outputs {
let cstr = output.address.withCString { strdup($0) }
guard let cstr else {
// Free any previously allocated strings before throwing
for ptr in cStrings { free(ptr) }
throw KeyWalletError.invalidInput("Failed to allocate C string for address")
}
cStrings.append(cstr)
ffiOutputs.append(FFITxOutput(address: UnsafePointer(cstr), amount: output.amount))
}

let success = ffiOutputs.withUnsafeBufferPointer { outputsPtr in
wallet_build_transaction(
wallet.ffiHandle,
Expand All @@ -57,6 +71,8 @@ public class Transaction {
}

defer {
// Free allocated C strings
for ptr in cStrings { free(ptr) }
if error.message != nil {
error_message_free(error.message)
}
Expand Down Expand Up @@ -89,8 +105,7 @@ public class Transaction {
var signedTxPtr: UnsafeMutablePointer<UInt8>?
var signedLen: size_t = 0

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

// Copy the signed transaction data before freeing
let signedData = Data(bytes: ptr, count: signedLen)

return signedData
}


/// Build and sign a transaction in one step using managed wallet
/// - Parameters:
/// - managedWallet: The managed wallet with UTXO information
/// - wallet: The wallet with private keys for signing
/// - accountIndex: The account index to use
/// - outputs: The transaction outputs
/// - feePerKB: Fee per kilobyte in satoshis
/// - currentHeight: Current blockchain height for UTXO selection
/// - Returns: The signed transaction bytes ready for broadcast
public static func buildAndSign(managedWallet: ManagedWallet,
wallet: Wallet,
accountIndex: UInt32 = 0,
outputs: [Output],
feePerKB: UInt64,
currentHeight: UInt32) throws -> Data {
guard !outputs.isEmpty else {
throw KeyWalletError.invalidInput("Transaction must have at least one output")
}

guard !wallet.isWatchOnly else {
throw KeyWalletError.invalidState("Cannot sign with watch-only wallet")
}

var error = FFIError()
var txBytesPtr: UnsafeMutablePointer<UInt8>?
var txLen: size_t = 0

// Get managed wallet handle
guard let managedHandle = managedWallet.getInfoHandle() else {
throw KeyWalletError.invalidState("Failed to get managed wallet handle")
}

// Convert outputs to FFI format with stable C strings
var cStrings: [UnsafeMutablePointer<CChar>] = []
cStrings.reserveCapacity(outputs.count)
var ffiOutputs: [FFITxOutput] = []
ffiOutputs.reserveCapacity(outputs.count)

for output in outputs {
let cstr = output.address.withCString { strdup($0) }
guard let cstr else {
for ptr in cStrings { free(ptr) }
throw KeyWalletError.invalidInput("Failed to allocate C string for address")
}
cStrings.append(cstr)
ffiOutputs.append(FFITxOutput(address: UnsafePointer(cstr), amount: output.amount))
}

let success = ffiOutputs.withUnsafeBufferPointer { outputsPtr in
wallet_build_and_sign_transaction(
managedHandle,
wallet.ffiHandle,
wallet.network.ffiValue,
accountIndex,
outputsPtr.baseAddress,
outputs.count,
feePerKB,
currentHeight,
&txBytesPtr,
&txLen,
&error)
}

defer {
for ptr in cStrings { free(ptr) }
if error.message != nil {
error_message_free(error.message)
}
if let ptr = txBytesPtr {
transaction_bytes_free(ptr)
}
}

guard success, let ptr = txBytesPtr else {
throw KeyWalletError(ffiError: error)
}

// Copy the transaction data before freeing
let txData = Data(bytes: ptr, count: txLen)

return txData
}

/// Extract TXID from raw transaction bytes
/// - Parameter transactionData: The transaction bytes
/// - Returns: The transaction ID as a hex string
public static func getTxid(from transactionData: Data) throws -> String {
var error = FFIError()
var txidPtr: UnsafeMutablePointer<CChar>?

let success = transactionData.withUnsafeBytes { txBytes in
let txPtr = txBytes.bindMemory(to: UInt8.self).baseAddress
txidPtr = transaction_get_txid_from_bytes(txPtr, transactionData.count, &error)
return txidPtr != nil
}

defer {
if error.message != nil {
error_message_free(error.message)
}
if let ptr = txidPtr {
string_free(ptr)
}
}

guard success, let ptr = txidPtr else {
throw KeyWalletError(ffiError: error)
}

return String(cString: ptr)
}

/// Check if a transaction belongs to a wallet
/// - Parameters:
/// - wallet: The wallet to check against
Expand All @@ -137,12 +264,10 @@ public class Transaction {
var error = FFIError()
var result = FFITransactionCheckResult()

let success = transactionData.withUnsafeBytes { txBytes in
let txPtr = txBytes.bindMemory(to: UInt8.self).baseAddress
let success = transactionData.withUnsafeBytes { (txPtr: UnsafePointer<UInt8>) in

if let hash = blockHash {
return hash.withUnsafeBytes { hashBytes in
let hashPtr = hashBytes.bindMemory(to: UInt8.self).baseAddress
return hash.withUnsafeBytes { (hashPtr: UnsafePointer<UInt8>) in

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

let classificationPtr = transactionData.withUnsafeBytes { txBytes in
let txPtr = txBytes.bindMemory(to: UInt8.self).baseAddress
let classificationPtr = transactionData.withUnsafeBytes { (txPtr: UnsafePointer<UInt8>) in
return transaction_classify(txPtr, transactionData.count, &error)
}

Expand All @@ -201,4 +325,4 @@ public class Transaction {

return classification
}
}
}
Loading
Loading