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
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class SPVClient: @unchecked Sendable {
}

func getSyncProgress() -> SPVSyncProgress {
guard let ptr = dash_spv_ffi_client_get_sync_progress(client) else {
guard let ptr = dash_spv_ffi_client_get_manager_sync_progress(client) else {
print("[SPV][GetSyncProgress] Failed to get sync progress (Should only fail if client is nil, but client is not nil)")
return SPVSyncProgress.default()
}
Expand Down Expand Up @@ -266,7 +266,7 @@ class SPVClient: @unchecked Sendable {
func destroy() {
dash_spv_ffi_client_destroy(client)
dash_spv_ffi_config_destroy(config)

client = nil
config = nil
}
Expand All @@ -293,6 +293,24 @@ class SPVClient: @unchecked Sendable {
}
}

// MARK: - Broadcast transactions
func broadcastTransaction(_ transactionData: Data) throws {
try transactionData.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in
guard let txBytes = ptr.bindMemory(to: UInt8.self).baseAddress else {
throw SPVError.transactionBroadcastFailed("Invalid transaction data pointer")
}
let result = dash_spv_ffi_client_broadcast_transaction(
client,
txBytes,
UInt(transactionData.count),
)

if result != 0 {
throw SPVError.transactionBroadcastFailed(SPVClient.getLastDashFFIError())
}
}
}

// MARK: - Wallet Manager Access

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

public var errorDescription: String? {
switch self {
Expand All @@ -335,6 +354,8 @@ public enum SPVError: LocalizedError {
return "Sync failed: \(reason)"
case let .storageOperationFailed(reason):
return reason
case let .transactionBroadcastFailed(reason):
return "Transaction broadcast failed: \(reason)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public enum SPVSyncState: UInt32, Sendable {

public struct SPVBlockHeadersProgress: Sendable {
public let state: SPVSyncState
public let currentHeight: UInt32
public let tipHeight: UInt32
public let targetHeight: UInt32
public let processed: UInt32
public let buffered: UInt32
Expand All @@ -61,7 +61,7 @@ public struct SPVBlockHeadersProgress: Sendable {

public init(_ ffi: FFIBlockHeadersProgress) {
state = SPVSyncState(rawValue: ffi.state.rawValue) ?? .unknown
currentHeight = ffi.tip_height
tipHeight = ffi.tip_height
targetHeight = ffi.target_height
processed = ffi.processed
buffered = ffi.buffered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ func print(_ items: Any..., separator: String = " ", terminator: String = "\n")
Swift.print(output, terminator: terminator)
}

// DESIGN NOTE: This class feels like something that should be in the example app,
// DESIGN NOTE: This class feels like something that should be in the example app,
// we, as sdk developers, provide the tools and ffi wrappers, but how to
// use them depends on the sdk user, for example, by implementing the SPV event
// use them depends on the sdk user, for example, by implementing the SPV event
// handlers, the user can decide what to do with the events, but if we implement them in the sdk
// we are taking that decision for them, and maybe not all users want the same thing
@MainActor
Expand All @@ -96,82 +96,82 @@ public class WalletService: ObservableObject {
@Published public var masternodesEnabled = true
@Published public var lastSyncError: Error?
@Published var network: AppNetwork

// Internal properties
private var modelContainer: ModelContainer

// SPV Client and Wallet wrappers
private var spvClient: SPVClient
public private(set) var walletManager: CoreWalletManager

public init(modelContainer: ModelContainer, network: AppNetwork) {
self.modelContainer = modelContainer
self.network = network

LoggingPreferences.configure()

let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SPV").appendingPathComponent(network.rawValue).path

// For simplicity, lets unwrap the error. This can only fail due to
// IO errors when working with the internal storage system, I don't
// IO errors when working with the internal storage system, I don't
// see how we can recover from that right now easily
let spvClient = try! SPVClient(
network: network.sdkNetwork,
dataDir: dataDir,
startHeight: 0,
)

self.spvClient = spvClient

// Create the SDK wallet manager by reusing the SPV client's shared manager
// TODO: Investigate this error
self.walletManager = try! CoreWalletManager(spvClient: spvClient, modelContainer: modelContainer)

spvClient.setProgressUpdateEventHandler(SPVProgressUpdateEventHandlerImpl(walletService: self))
spvClient.setSyncEventsHandler(SPVSyncEventsHandlerImpl(walletService: self))
spvClient.setNetworkEventsHandler(SPVNetworkEventsHandlerImpl(walletService: self))
spvClient.setWalletEventsHandler(SPVWalletEventsHandlerImpl(walletService: self))
}

deinit {
spvClient.stopSync()
spvClient.destroy()
}

private func initializeNewSPVClient() {
SDKLogger.log("Initializing SPV Client for \(self.self.network.rawValue)...", minimumLevel: .medium)

let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SPV").appendingPathComponent(self.network.rawValue).path

// This ensures no memory leaks when creating a new client
// and unlocks the storage in case we are about to use the same (we probably are)
self.spvClient.destroy()

// For simplicity, lets unwrap the error. This can only fail due to
// IO errors when working with the internal storage system, I don't
// IO errors when working with the internal storage system, I don't
// see how we can recover from that right now easily
self.spvClient = try! SPVClient(
network: self.self.network.sdkNetwork,
dataDir: dataDir,
startHeight: 0,
)

self.spvClient.setProgressUpdateEventHandler(SPVProgressUpdateEventHandlerImpl(walletService: self))
self.spvClient.setSyncEventsHandler(SPVSyncEventsHandlerImpl(walletService: self))
self.spvClient.setNetworkEventsHandler(SPVNetworkEventsHandlerImpl(walletService: self))
self.spvClient.setWalletEventsHandler(SPVWalletEventsHandlerImpl(walletService: self))

try! self.spvClient.setMasternodeSyncEnabled(self.masternodesEnabled)

SDKLogger.log("✅ SPV Client initialized successfully for \(self.network.rawValue) (deferred start)", minimumLevel: .medium)

// Create the SDK wallet manager by reusing the SPV client's shared manager
// TODO: Investigate this error
self.walletManager = try! CoreWalletManager(spvClient: self.spvClient, modelContainer: self.modelContainer)

SDKLogger.log("✅ WalletManager wrapper initialized successfully", minimumLevel: .medium)
}

// MARK: - Trusted Mode / Masternode Sync
public func setMasternodesEnabled(_ enabled: Bool) {
masternodesEnabled = enabled
Expand Down Expand Up @@ -203,10 +203,14 @@ public class WalletService: ObservableObject {
// pausing and resuming is not supported so, the trick is the following,
// stop the old client and create a new one in its initial state xd
spvClient.stopSync()

self.initializeNewSPVClient()
}

public func broadcastTransaction(_ data: Data) throws {
try self.spvClient.broadcastTransaction(data)
}

public func clearSpvStorage() {
if syncProgress.state.isRunning() {
print("[SPV][Clear] Sync task is running, cannot clear storage")
Expand Down Expand Up @@ -236,17 +240,17 @@ public class WalletService: ObservableObject {

public func switchNetwork(to network: AppNetwork) async {
guard network != self.network else { return }

print("=== WalletService.switchNetwork START ===")
print("Switching from \(self.network.rawValue) to \(network.rawValue)")

self.network = network

self.stopSync()

print("=== WalletService.switchNetwork END ===")
}

// MARK: - SPV Event Handlers implementations

internal final class SPVProgressUpdateEventHandlerImpl: SPVProgressUpdateEventHandler, Sendable {
Expand Down Expand Up @@ -329,7 +333,7 @@ public class WalletService: ObservableObject {
_ amount: Int64,
_ addresses: [String]
) {}

func onBalanceUpdated(
_ walletId: String,
_ spendable: UInt64,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ public class CoreWalletManager: ObservableObject {

// MARK: - Account Management

/// Build a signed transaction
/// - Parameters:
/// - accountIndex: The account index to use
/// - outputs: The transaction outputs
/// - Returns: The signed transaction bytes
public func buildSignedTransaction(for wallet: HDWallet, accIndex: UInt32, outputs: [Transaction.Output]) throws -> (Data, UInt64) {
try sdkWalletManager.buildSignedTransaction(for: wallet, accIndex: accIndex, outputs: outputs)
}

/// Get transactions for a wallet
/// - Parameters:
/// - wallet: The wallet to get transactions for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ public class ManagedAccount {
return managed_core_account_get_utxo_count(handle)
}

// MARK: - Transactions

/// Get all transactions for this account
/// - Returns: Array of transactions
public func getTransactions() -> [WalletTransaction] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import Foundation
import DashSDKFFI

/// Result of building and signing a transaction
public struct BuildAndSignResult: Sendable {
/// The signed transaction bytes
public let transactionData: Data
/// The fee paid in duffs
public let fee: UInt64
}

/// Transaction utilities for wallet operations
public class Transaction {

Expand All @@ -23,9 +15,11 @@ public class Transaction {
}

func toFFI() -> FFITxOutput {
return address.withCString { addressCStr in
FFITxOutput(address: addressCStr, amount: amount)
}
// TODO: This memory is not being freed, FFI must free FFITxOutput
// or expose a method to do it
let cString = strdup(address)

return FFITxOutput(address: cString, amount: amount)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,64 @@ public class WalletManager {
return success
}

/// Build a signed transaction
/// - Parameters:
/// - accIndex: The account index to use
/// - outputs: The transaction outputs
/// - Returns: The signed transaction bytes and the fee
public func buildSignedTransaction(for wallet: HDWallet, accIndex: UInt32, outputs: [Transaction.Output]) throws -> (Data, UInt64) {
guard !outputs.isEmpty else {
throw KeyWalletError.invalidInput("Transaction must have at least one output")
}

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

var fee: UInt64 = 0

guard let wallet = try self.getWallet(id: wallet.walletId) else {
throw KeyWalletError.walletError("Wallet not found in manager")
}

let ffiOutputs = outputs.map { $0.toFFI() }

let success = ffiOutputs.withUnsafeBufferPointer { outputsPtr in
wallet_build_and_sign_transaction(
self.handle,
wallet.ffiHandle,
accIndex,
outputsPtr.baseAddress,
outputs.count,
1000,
&fee,
&txBytesPtr,
&txLen,
&error)
}

defer {
if error.message != nil {
error_message_free(error.message)
}
for _ in ffiOutputs {
// TODO: Memory leak, FFI doesnt expose a way to free the address
}
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, fee)
}

// MARK: - Block Height Management

/// Get the current block height for a network
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct CoreContentView: View {
// Display helpers
private var headerHeightsDisplay: String? {
let headers = walletService.syncProgress.headers
let cur = (headers?.currentHeight ?? 0) + (headers?.buffered ?? 0)
let cur = (headers?.tipHeight ?? 0) + (headers?.buffered ?? 0)
let tot = headers?.targetHeight ?? 0

return heightDisplay(numerator: cur, denominator: tot)
Expand Down
Loading
Loading