diff --git a/Cargo.lock b/Cargo.lock index 98d435d7ae..7c5524b3f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1637,7 +1637,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" dependencies = [ "bincode", "bincode_derive", @@ -1648,7 +1648,7 @@ dependencies = [ [[package]] name = "dash-network-seeds" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" dependencies = [ "dash-network", ] @@ -1725,7 +1725,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" dependencies = [ "async-trait", "chrono", @@ -1754,7 +1754,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" dependencies = [ "anyhow", "base64-compat", @@ -1780,12 +1780,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" [[package]] name = "dashcore-rpc" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" dependencies = [ "dashcore-rpc-json", "hex", @@ -1798,7 +1798,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" dependencies = [ "bincode", "dashcore", @@ -1813,7 +1813,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" dependencies = [ "bincode", "dashcore-private", @@ -2867,7 +2867,7 @@ dependencies = [ [[package]] name = "git-state" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" [[package]] name = "glob" @@ -4034,7 +4034,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" dependencies = [ "aes", "async-trait", @@ -4063,7 +4063,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4079,7 +4079,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3#1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" +source = "git+https://github.com/dashpay/rust-dashcore?rev=d4692bb29c626942ddf51204d50d90206c1a5b0c#d4692bb29c626942ddf51204d50d90206c1a5b0c" dependencies = [ "async-trait", "bincode", diff --git a/Cargo.toml b/Cargo.toml index b7db6e4495..efb87c098d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,14 +50,14 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "1ee1c94840f14c63fa34e9d19f6eaaa1d29847c3" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "d4692bb29c626942ddf51204d50d90206c1a5b0c" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "d4692bb29c626942ddf51204d50d90206c1a5b0c" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "d4692bb29c626942ddf51204d50d90206c1a5b0c" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "d4692bb29c626942ddf51204d50d90206c1a5b0c" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "d4692bb29c626942ddf51204d50d90206c1a5b0c" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "d4692bb29c626942ddf51204d50d90206c1a5b0c" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "d4692bb29c626942ddf51204d50d90206c1a5b0c" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "d4692bb29c626942ddf51204d50d90206c1a5b0c" } tokio-metrics = "0.5" diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionDecoder.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionDecoder.swift new file mode 100644 index 0000000000..380deeeeff --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionDecoder.swift @@ -0,0 +1,128 @@ +import Foundation +import DashSDKFFI + +/// A consensus-decoded Dash transaction: per-output `(address, value, script)` +/// plus per-input previous outpoints with a best-effort P2PKH sender address. +/// +/// Wallet persistence stores every transaction's raw bytes but only +/// materializes TXO rows for the wallet's own outputs, so matching arbitrary +/// transactions against external addresses (e.g. the CrowdNode on-chain API: +/// "did a tx pay amount X to address Y?") requires decoding the stored bytes. +/// `TransactionDecoder.decode` is that opener. +public struct DecodedTransaction: Sendable, Equatable { + public struct Input: Sendable, Equatable { + /// Previous output's txid in consensus (internal) byte order — + /// reverse for explorer-style display (see `prevTxidDisplayHex`). + public let prevTxid: Data + /// Previous output's index. + public let prevVout: UInt32 + /// Sender address recovered from a P2PKH-shaped scriptSig; nil for + /// coinbase / non-P2PKH / unparseable script sigs. Unauthenticated — + /// derived from a script push the spender fully controls, with no + /// signature verification against the spent UTXO. Use it for display + /// or as a matching hint only, never for authentication or + /// authorization; the attacker-resistant matching primitive is the + /// per-output `(address, valueDuffs)` pair. + public let address: String? + + /// Explorer-style (reversed, hex) rendering of `prevTxid`. + public var prevTxidDisplayHex: String { + prevTxid.reversed().map { String(format: "%02x", $0) }.joined() + } + } + + public struct Output: Sendable, Equatable { + /// Destination address for standard scripts (P2PKH / P2SH); nil for + /// non-standard scripts (OP_RETURN, bare multisig, …). + public let address: String? + /// Output value in duffs. + public let valueDuffs: UInt64 + /// Raw scriptPubKey bytes. + public let scriptPubkey: Data + } + + /// Transaction id in consensus (internal) byte order — reverse for + /// explorer-style display (see `txidDisplayHex`). + public let txid: Data + public let inputs: [Input] + public let outputs: [Output] + + /// Explorer-style (reversed, hex) rendering of `txid`. + public var txidDisplayHex: String { + txid.reversed().map { String(format: "%02x", $0) }.joined() + } +} + +/// Pure utility — decodes raw transaction bytes via `transaction_decode` +/// (key-wallet-ffi). No wallet handle or state involved. +public enum TransactionDecoder { + /// Consensus-decode raw transaction bytes. + /// - Parameters: + /// - txData: Serialized transaction bytes (e.g. from persisted rows). + /// - network: Network used to render addresses (base58 version bytes). + /// - Returns: The decoded transaction. + /// - Throws: `KeyWalletError.invalidInput` for malformed or empty bytes + /// (including trailing garbage). + public static func decode(_ txData: Data, network: Network) throws -> DecodedTransaction { + var error = FFIError() + var outDecoded: UnsafeMutablePointer? = nil + let success = txData.withUnsafeBytes { (raw: UnsafeRawBufferPointer) -> Bool in + transaction_decode( + raw.baseAddress?.assumingMemoryBound(to: UInt8.self), + txData.count, + network.ffiValue, + &outDecoded, + &error + ) + } + + defer { + if error.message != nil { + error_message_free(error.message) + } + } + + guard success, let decoded = outDecoded else { + throw KeyWalletError(ffiError: error) + } + defer { decoded_transaction_free(decoded) } + + var entry = decoded.pointee + let txid = withUnsafeBytes(of: &entry.txid) { Data($0) } + + var inputs: [DecodedTransaction.Input] = [] + if let ptr = entry.inputs, entry.inputs_count > 0 { + inputs = (0.. 0 { + outputs = (0.. 0 { + script = Data(bytes: sptr, count: Int(output.script_pubkey_len)) + } else { + script = Data() + } + return DecodedTransaction.Output( + address: address, + valueDuffs: output.value_duffs, + scriptPubkey: script + ) + } + } + + return DecodedTransaction(txid: txid, inputs: inputs, outputs: outputs) + } +} diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/TransactionDecoderTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/TransactionDecoderTests.swift new file mode 100644 index 0000000000..a6154d0f5e --- /dev/null +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/TransactionDecoderTests.swift @@ -0,0 +1,80 @@ +import XCTest +@testable import SwiftDashSDK + +/// Pure-function tests for `TransactionDecoder` — no wallet handle or network +/// access. The fixture is the same synthetic transaction exercised by the +/// Rust-side unit tests in `key-wallet-ffi/src/tx_decode.rs` (rust-dashcore): +/// one P2PKH input (spending 1111…11:3, scriptSig = push(sig) push(pubkey)), +/// one P2PKH output of 151 072 duffs, and one OP_RETURN output. +final class TransactionDecoderTests: XCTestCase { + private static let fixtureHex = + "01000000011111111111111111111111111111111111111111111111111111111111111111" + + "030000006a4730303030303030303030303030303030303030303030303030303030303030" + + "30303030303030303030303030303030303030303030303030303030303030303030303030" + + "303030210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c" + + "ffffffff02204e0200000000001976a91414db4138d56a2ecfb10881a9be394d9f321985b2" + + "88ac0000000000000000066a04aaaaaaaa00000000" + + private static let fixtureAddress = "yNDj28QBMm5sY6bLjFcNdWRNef24KLQNuQ" + private static let fixtureTxidDisplay = + "bf7479216e5ba76f60bf11654c881824c6f9cdbb64eebe332cf835a3391cb5d5" + + private var fixtureData: Data { + var data = Data() + let hex = Self.fixtureHex + var index = hex.startIndex + while index < hex.endIndex { + let next = hex.index(index, offsetBy: 2) + data.append(UInt8(hex[index..