Skip to content

Commit 9d604c0

Browse files
Borja CastellanoZocoLini
authored andcommitted
send transaction implemented in swift example app
1 parent d3791b2 commit 9d604c0

9 files changed

Lines changed: 222 additions & 98 deletions

File tree

packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ class SPVClient: @unchecked Sendable {
144144
}
145145

146146
func getSyncProgress() -> SPVSyncProgress {
147-
guard let ptr = dash_spv_ffi_client_get_sync_progress(client) else {
147+
guard let ptr = dash_spv_ffi_client_get_manager_sync_progress(client) else {
148148
print("[SPV][GetSyncProgress] Failed to get sync progress (Should only fail if client is nil, but client is not nil)")
149149
return SPVSyncProgress.default()
150150
}
@@ -266,7 +266,7 @@ class SPVClient: @unchecked Sendable {
266266
func destroy() {
267267
dash_spv_ffi_client_destroy(client)
268268
dash_spv_ffi_config_destroy(config)
269-
269+
270270
client = nil
271271
config = nil
272272
}
@@ -293,6 +293,24 @@ class SPVClient: @unchecked Sendable {
293293
}
294294
}
295295

296+
// MARK: - Broadcast transactions
297+
func broadcastTransaction(_ transactionData: Data) throws {
298+
try transactionData.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in
299+
guard let txBytes = ptr.bindMemory(to: UInt8.self).baseAddress else {
300+
throw SPVError.transactionBroadcastFailed("Invalid transaction data pointer")
301+
}
302+
let result = dash_spv_ffi_client_broadcast_transaction(
303+
client,
304+
txBytes,
305+
UInt(transactionData.count),
306+
)
307+
308+
if result != 0 {
309+
throw SPVError.transactionBroadcastFailed(SPVClient.getLastDashFFIError())
310+
}
311+
}
312+
}
313+
296314
// MARK: - Wallet Manager Access
297315

298316
/// Produce a Swift wallet manager that shares the SPV client's underlying wallet state.
@@ -316,6 +334,7 @@ public enum SPVError: LocalizedError {
316334
case alreadySyncing
317335
case syncFailed(String)
318336
case storageOperationFailed(String)
337+
case transactionBroadcastFailed(String)
319338

320339
public var errorDescription: String? {
321340
switch self {
@@ -335,6 +354,8 @@ public enum SPVError: LocalizedError {
335354
return "Sync failed: \(reason)"
336355
case let .storageOperationFailed(reason):
337356
return reason
357+
case let .transactionBroadcastFailed(reason):
358+
return "Transaction broadcast failed: \(reason)"
338359
}
339360
}
340361
}

packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVTypes.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public enum SPVSyncState: UInt32, Sendable {
5353

5454
public struct SPVBlockHeadersProgress: Sendable {
5555
public let state: SPVSyncState
56-
public let currentHeight: UInt32
56+
public let tipHeight: UInt32
5757
public let targetHeight: UInt32
5858
public let processed: UInt32
5959
public let buffered: UInt32
@@ -62,7 +62,7 @@ public struct SPVBlockHeadersProgress: Sendable {
6262

6363
public init(_ ffi: FFIBlockHeadersProgress) {
6464
state = SPVSyncState(rawValue: ffi.state.rawValue) ?? .unknown
65-
currentHeight = ffi.tip_height
65+
tipHeight = ffi.tip_height
6666
targetHeight = ffi.target_height
6767
processed = ffi.processed
6868
buffered = ffi.buffered

packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ func print(_ items: Any..., separator: String = " ", terminator: String = "\n")
8484
Swift.print(output, terminator: terminator)
8585
}
8686

87-
// DESIGN NOTE: This class feels like something that should be in the example app,
87+
// DESIGN NOTE: This class feels like something that should be in the example app,
8888
// we, as sdk developers, provide the tools and ffi wrappers, but how to
89-
// use them depends on the sdk user, for example, by implementing the SPV event
89+
// use them depends on the sdk user, for example, by implementing the SPV event
9090
// handlers, the user can decide what to do with the events, but if we implement them in the sdk
9191
// we are taking that decision for them, and maybe not all users want the same thing
9292
@MainActor
@@ -96,82 +96,82 @@ public class WalletService: ObservableObject {
9696
@Published public var masternodesEnabled = true
9797
@Published public var lastSyncError: Error?
9898
@Published var network: AppNetwork
99-
99+
100100
// Internal properties
101101
private var modelContainer: ModelContainer
102-
102+
103103
// SPV Client and Wallet wrappers
104104
private var spvClient: SPVClient
105105
public private(set) var walletManager: CoreWalletManager
106106

107107
public init(modelContainer: ModelContainer, network: AppNetwork) {
108108
self.modelContainer = modelContainer
109109
self.network = network
110-
110+
111111
LoggingPreferences.configure()
112-
112+
113113
let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SPV").appendingPathComponent(network.rawValue).path
114-
114+
115115
// For simplicity, lets unwrap the error. This can only fail due to
116-
// IO errors when working with the internal storage system, I don't
116+
// IO errors when working with the internal storage system, I don't
117117
// see how we can recover from that right now easily
118118
let spvClient = try! SPVClient(
119119
network: network.sdkNetwork,
120120
dataDir: dataDir,
121121
startHeight: 0,
122122
)
123-
123+
124124
self.spvClient = spvClient
125-
125+
126126
// Create the SDK wallet manager by reusing the SPV client's shared manager
127127
// TODO: Investigate this error
128128
self.walletManager = try! CoreWalletManager(spvClient: spvClient, modelContainer: modelContainer)
129-
129+
130130
spvClient.setProgressUpdateEventHandler(SPVProgressUpdateEventHandlerImpl(walletService: self))
131131
spvClient.setSyncEventsHandler(SPVSyncEventsHandlerImpl(walletService: self))
132132
spvClient.setNetworkEventsHandler(SPVNetworkEventsHandlerImpl(walletService: self))
133133
spvClient.setWalletEventsHandler(SPVWalletEventsHandlerImpl(walletService: self))
134134
}
135-
135+
136136
deinit {
137137
spvClient.stopSync()
138138
spvClient.destroy()
139139
}
140-
140+
141141
private func initializeNewSPVClient() {
142142
SDKLogger.log("Initializing SPV Client for \(self.self.network.rawValue)...", minimumLevel: .medium)
143-
143+
144144
let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SPV").appendingPathComponent(self.network.rawValue).path
145-
145+
146146
// This ensures no memory leaks when creating a new client
147147
// and unlocks the storage in case we are about to use the same (we probably are)
148148
self.spvClient.destroy()
149-
149+
150150
// For simplicity, lets unwrap the error. This can only fail due to
151-
// IO errors when working with the internal storage system, I don't
151+
// IO errors when working with the internal storage system, I don't
152152
// see how we can recover from that right now easily
153153
self.spvClient = try! SPVClient(
154154
network: self.self.network.sdkNetwork,
155155
dataDir: dataDir,
156156
startHeight: 0,
157157
)
158-
158+
159159
self.spvClient.setProgressUpdateEventHandler(SPVProgressUpdateEventHandlerImpl(walletService: self))
160160
self.spvClient.setSyncEventsHandler(SPVSyncEventsHandlerImpl(walletService: self))
161161
self.spvClient.setNetworkEventsHandler(SPVNetworkEventsHandlerImpl(walletService: self))
162162
self.spvClient.setWalletEventsHandler(SPVWalletEventsHandlerImpl(walletService: self))
163-
163+
164164
try! self.spvClient.setMasternodeSyncEnabled(self.masternodesEnabled)
165-
165+
166166
SDKLogger.log("✅ SPV Client initialized successfully for \(self.network.rawValue) (deferred start)", minimumLevel: .medium)
167-
167+
168168
// Create the SDK wallet manager by reusing the SPV client's shared manager
169169
// TODO: Investigate this error
170170
self.walletManager = try! CoreWalletManager(spvClient: self.spvClient, modelContainer: self.modelContainer)
171-
171+
172172
SDKLogger.log("✅ WalletManager wrapper initialized successfully", minimumLevel: .medium)
173173
}
174-
174+
175175
// MARK: - Trusted Mode / Masternode Sync
176176
public func setMasternodesEnabled(_ enabled: Bool) {
177177
masternodesEnabled = enabled
@@ -203,10 +203,14 @@ public class WalletService: ObservableObject {
203203
// pausing and resuming is not supported so, the trick is the following,
204204
// stop the old client and create a new one in its initial state xd
205205
spvClient.stopSync()
206-
206+
207207
self.initializeNewSPVClient()
208208
}
209209

210+
public func broadcastTransaction(_ data: Data) throws {
211+
try self.spvClient.broadcastTransaction(data)
212+
}
213+
210214
public func clearSpvStorage() {
211215
if syncProgress.state.isRunning() {
212216
print("[SPV][Clear] Sync task is running, cannot clear storage")
@@ -236,17 +240,17 @@ public class WalletService: ObservableObject {
236240

237241
public func switchNetwork(to network: AppNetwork) async {
238242
guard network != self.network else { return }
239-
243+
240244
print("=== WalletService.switchNetwork START ===")
241245
print("Switching from \(self.network.rawValue) to \(network.rawValue)")
242-
246+
243247
self.network = network
244248

245249
self.stopSync()
246-
250+
247251
print("=== WalletService.switchNetwork END ===")
248252
}
249-
253+
250254
// MARK: - SPV Event Handlers implementations
251255

252256
internal final class SPVProgressUpdateEventHandlerImpl: SPVProgressUpdateEventHandler, Sendable {
@@ -329,7 +333,7 @@ public class WalletService: ObservableObject {
329333
_ amount: Int64,
330334
_ addresses: [String]
331335
) {}
332-
336+
333337
func onBalanceUpdated(
334338
_ walletId: String,
335339
_ spendable: UInt64,

packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ public class CoreWalletManager: ObservableObject {
142142

143143
// MARK: - Account Management
144144

145+
/// Build a signed transaction
146+
/// - Parameters:
147+
/// - accountIndex: The account index to use
148+
/// - outputs: The transaction outputs
149+
/// - feePerKB: Fee per kilobyte in satoshis
150+
/// - Returns: The unsigned transaction bytes
151+
public func buildSignedTransaction(for wallet: HDWallet, accIndex: UInt32, outputs: [Transaction.Output], feeRate: FeeRate) throws -> (Data, UInt64) {
152+
try sdkWalletManager.buildSignedTransaction(for: wallet, accIndex: accIndex, outputs: outputs, feeRate: feeRate)
153+
}
154+
145155
/// Get transactions for a wallet
146156
/// - Parameters:
147157
/// - wallet: The wallet to get transactions for

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ public class ManagedAccount {
5050
return managed_core_account_get_utxo_count(handle)
5151
}
5252

53-
// MARK: - Transactions
54-
5553
/// Get all transactions for this account
5654
/// - Returns: Array of transactions
5755
public func getTransactions() -> [WalletTransaction] {

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import Foundation
22
import DashSDKFFI
33

4-
/// Result of building and signing a transaction
5-
public struct BuildAndSignResult: Sendable {
6-
/// The signed transaction bytes
7-
public let transactionData: Data
8-
/// The fee paid in duffs
9-
public let fee: UInt64
4+
public enum FeeRate {
5+
case economy
6+
case normal
7+
case priority
8+
9+
func intoDuffPerKB() -> UInt64 {
10+
switch self {
11+
case .economy: return 500
12+
case .normal: return 1000
13+
case .priority: return 2000
14+
}
15+
}
1016
}
1117

1218
/// Transaction utilities for wallet operations
@@ -23,9 +29,8 @@ public class Transaction {
2329
}
2430

2531
func toFFI() -> FFITxOutput {
26-
return address.withCString { addressCStr in
27-
FFITxOutput(address: addressCStr, amount: amount)
28-
}
32+
let cString = strdup(address)
33+
return FFITxOutput(address: cString, amount: amount)
2934
}
3035
}
3136

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,60 @@ public class WalletManager {
387387
return success
388388
}
389389

390+
/// Build a signed transaction
391+
/// - Parameters:
392+
/// - accountIndex: The account index to use
393+
/// - outputs: The transaction outputs
394+
/// - feePerKB: Fee per kilobyte in satoshis
395+
/// - Returns: The unsigned transaction bytes and the fee
396+
public func buildSignedTransaction(for wallet: HDWallet, accIndex: UInt32, outputs: [Transaction.Output], feeRate: FeeRate) throws -> (Data, UInt64) {
397+
guard !outputs.isEmpty else {
398+
throw KeyWalletError.invalidInput("Transaction must have at least one output")
399+
}
400+
401+
var error = FFIError()
402+
var txBytesPtr: UnsafeMutablePointer<UInt8>?
403+
var txLen: size_t = 0
404+
405+
var fee: UInt64 = 0
406+
407+
let wallet = try self.getWallet(id: wallet.walletId)!
408+
409+
let ffiOutputs = outputs.map { $0.toFFI() }
410+
411+
let success = ffiOutputs.withUnsafeBufferPointer { outputsPtr in
412+
wallet_build_and_sign_transaction(
413+
self.handle,
414+
wallet.ffiHandle,
415+
accIndex,
416+
outputsPtr.baseAddress,
417+
outputs.count,
418+
feeRate.intoDuffPerKB(),
419+
&fee,
420+
&txBytesPtr,
421+
&txLen,
422+
&error)
423+
}
424+
425+
defer {
426+
if error.message != nil {
427+
error_message_free(error.message)
428+
}
429+
if let ptr = txBytesPtr {
430+
transaction_bytes_free(ptr)
431+
}
432+
}
433+
434+
guard success, let ptr = txBytesPtr else {
435+
throw KeyWalletError(ffiError: error)
436+
}
437+
438+
// Copy the transaction data before freeing
439+
let txData = Data(bytes: ptr, count: txLen)
440+
441+
return (txData, fee)
442+
}
443+
390444
// MARK: - Block Height Management
391445

392446
/// Get the current block height for a network

packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ struct CoreContentView: View {
1818
// Display helpers
1919
private var headerHeightsDisplay: String? {
2020
let headers = walletService.syncProgress.headers
21-
let cur = (headers?.currentHeight ?? 0) + (headers?.buffered ?? 0)
21+
let cur = (headers?.tipHeight ?? 0) + (headers?.buffered ?? 0)
2222
let tot = headers?.targetHeight ?? 0
2323

2424
return heightDisplay(numerator: cur, denominator: tot)

0 commit comments

Comments
 (0)