From 6f70092a62a81b91b0e542c36cf8a9537d78454a Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:56:12 +0300 Subject: [PATCH 1/6] feat(core-wallet): expose OP_RETURN, output-order and VIN0-change controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAYAChain requires a UTXO deposit shaped as VOUT0=vault, VOUT1=OP_RETURN memo, VOUT2=change paid back to the VIN0 address, with no output reordering, and it identifies the depositor by VIN0 for refunds. https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/sending-transactions `CoreTransactionBuilder.buildSigned` builds and signs in one FFI call, so none of this can be applied after the fact — it has to be expressed on the builder. FFI (rs-platform-wallet-ffi): - core_wallet_tx_builder_add_op_return / _preserve_output_order / _change_to_first_input, mirroring the existing setter style - an over-long payload is rejected before take_builder() runs, so a refused memo cannot leave the slot holding a mem::take default and silently drop outputs the caller already configured - core_wallet_signed_transaction_v2_bytes: read the finalized transaction bytes without broadcasting, so the deposit shape can be asserted pre-broadcast Swift SDK: - addOpReturn / preserveOutputOrder / changeToFirstInput - FinalizedCoreTransaction.serializedData() Tests: MayaDepositVerificationIntegrationTests builds short- and long-memo deposits and asserts output count/order, the OP_RETURN payload, VOUT2 == VIN0 scriptPubKey, the memo ceiling, the dust floor and a >= 1 duff/byte fee, then checks fee parity for ordinary, multi-recipient, selected-input, drain and asset-lock shapes so the precise output sizing does not move existing fees. CI: fail the workspace workflow if the local rust-dashcore [patch] override is still present in Cargo.toml. Depends on key-wallet gaining add_op_return / preserve_output_order / change_to_first_input (dashpay/rust-dashcore, branch feat/tx-builder-op-return). Until that lands and the rev in Cargo.toml is bumped, building this needs a local [patch] override, which is deliberately NOT committed. --- .github/workflows/tests-rs-workspace.yml | 7 + .../src/core_wallet/broadcast.rs | 22 + .../src/core_wallet/transaction_builder.rs | 89 ++- .../CoreWallet/CoreTransactionBuilder.swift | 50 ++ ...aDepositVerificationIntegrationTests.swift | 602 ++++++++++++++++++ 5 files changed, 769 insertions(+), 1 deletion(-) create mode 100644 packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index 7fa35e87614..7107daeace4 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -21,6 +21,13 @@ jobs: with: clean: false + - name: Fail on local rust-dashcore patch override + run: | + if grep -q '^\[patch\."https://github.com/dashpay/rust-dashcore"\]' Cargo.toml; then + echo "::error::Remove the local rust-dashcore [patch] override before merging" + exit 1 + fi + - name: Prune macOS runner disk before tests run: | for path in ../target-backup-before-*-clean-* target/llvm-cov-target; do diff --git a/packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs b/packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs index 26fa825fa50..4202fc1d685 100644 --- a/packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs +++ b/packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs @@ -135,6 +135,28 @@ pub unsafe extern "C" fn core_wallet_signed_transaction_v2_fee( PlatformWalletFFIResult::ok() } +#[no_mangle] +pub unsafe extern "C" fn core_wallet_signed_transaction_v2_bytes( + transaction_handle: Handle, + out_bytes: *mut *mut u8, + out_len: *mut usize, +) -> PlatformWalletFFIResult { + check_ptr!(out_bytes); + check_ptr!(out_len); + *out_bytes = std::ptr::null_mut(); + *out_len = 0; + + let bytes = unwrap_option_or_return!(CORE_SIGNED_TRANSACTION_V2_STORAGE + .with_item(transaction_handle, |tx| dashcore::consensus::serialize( + tx.transaction.transaction() + ))); + let len = bytes.len(); + let boxed = bytes.into_boxed_slice(); + *out_bytes = Box::into_raw(boxed) as *mut u8; + *out_len = len; + PlatformWalletFFIResult::ok() +} + /// Broadcast a transaction built by `core_wallet_tx_builder_build_signed`. /// /// `account_type`/`account_index` identify the funding account handed to diff --git a/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs b/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs index 8b79efcf2ab..2487644391b 100644 --- a/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs +++ b/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs @@ -13,7 +13,9 @@ use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy; use key_wallet::wallet::managed_wallet_info::fee::FeeRate; -use key_wallet::wallet::managed_wallet_info::transaction_builder::TransactionBuilder; +use key_wallet::wallet::managed_wallet_info::transaction_builder::{ + TransactionBuilder, MAX_STANDARD_OP_RETURN_BYTES, +}; use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use rs_sdk_ffi::{MnemonicResolverCoreSigner, MnemonicResolverHandle}; @@ -270,6 +272,57 @@ pub unsafe extern "C" fn core_wallet_tx_builder_add_output( PlatformWalletFFIResult::ok() } +/// Add a zero-value OP_RETURN output carrying `data`. +/// +/// # Safety +/// `builder` must be a valid, non-destroyed pointer; `data` must reference a +/// readable buffer of `data_len` bytes when `data_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn core_wallet_tx_builder_add_op_return( + builder: *mut FFITransactionBuilder, + data: *const u8, + data_len: usize, +) -> PlatformWalletFFIResult { + check_ptr!(builder); + if data_len > 0 { + check_ptr!(data); + } + + let bytes = if data_len == 0 { + &[] + } else { + std::slice::from_raw_parts(data, data_len) + }; + + // `add_op_return` takes the builder by value, so a rejected payload drops it and leaves + // `take_builder`'s `mem::take` default behind — silently discarding outputs and options + // the caller already configured. Reject an over-long payload *before* taking the builder + // so the slot keeps its real state. `add_op_return` re-checks; this is the same policy + // constant, not a second opinion. + if data_len > MAX_STANDARD_OP_RETURN_BYTES { + return PlatformWalletFFIResult::err( + PlatformWalletFFIResultCode::ErrorInvalidParameter, + format!( + "OP_RETURN payload too large: {data_len} bytes (max {MAX_STANDARD_OP_RETURN_BYTES})" + ), + ); + } + + let b = (*builder).take_builder(); + let b = match b.add_op_return(bytes) { + Ok(b) => b, + Err(err) => { + return PlatformWalletFFIResult::err( + PlatformWalletFFIResultCode::ErrorWalletOperation, + err.to_string(), + ); + } + }; + (*builder).store_builder(b); + + PlatformWalletFFIResult::ok() +} + /// # Safety /// `builder` must be a valid, non-destroyed pointer; `address` a valid NUL-terminated C string. #[no_mangle] @@ -300,6 +353,40 @@ pub unsafe extern "C" fn core_wallet_tx_builder_set_change_address( PlatformWalletFFIResult::ok() } +/// Preserve outputs in the order they were added instead of applying BIP-69 sorting. +/// +/// # Safety +/// `builder` must be a valid, non-destroyed pointer. +#[no_mangle] +pub unsafe extern "C" fn core_wallet_tx_builder_preserve_output_order( + builder: *mut FFITransactionBuilder, +) -> PlatformWalletFFIResult { + check_ptr!(builder); + + let b = (*builder).take_builder(); + let b = b.preserve_output_order(); + (*builder).store_builder(b); + + PlatformWalletFFIResult::ok() +} + +/// Route change to the address of the first selected input (VIN0). +/// +/// # Safety +/// `builder` must be a valid, non-destroyed pointer. +#[no_mangle] +pub unsafe extern "C" fn core_wallet_tx_builder_change_to_first_input( + builder: *mut FFITransactionBuilder, +) -> PlatformWalletFFIResult { + check_ptr!(builder); + + let b = (*builder).take_builder(); + let b = b.change_to_first_input(); + (*builder).store_builder(b); + + PlatformWalletFFIResult::ok() +} + /// # Safety /// `builder` must be a valid, non-destroyed pointer. #[no_mangle] diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift index 64f0eb4b5b6..528dbbcd31b 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift @@ -68,6 +68,26 @@ public final class FinalizedCoreTransaction { } func takeForAbandon() throws -> Handle { try takeForBroadcast() } + + /// Consensus-serialized signed transaction bytes (copied out) without + /// consuming the ownership token. + public func serializedData() throws -> Data { + guard nativeHandle != 0 else { + throw PlatformWalletError.unknown("FinalizedCoreTransaction already consumed") + } + + var bytesPtr: UnsafeMutablePointer? = nil + var bytesLen: UInt = 0 + try core_wallet_signed_transaction_v2_bytes(nativeHandle, &bytesPtr, &bytesLen).check() + + guard let bytesPtr, bytesLen > 0 else { + throw PlatformWalletError.unknown( + "FFI returned success but finalized transaction bytes were empty" + ) + } + defer { platform_wallet_bytes_free(bytesPtr, bytesLen) } + return Data(bytes: bytesPtr, count: Int(bytesLen)) + } } /// key-wallet transaction builder over FFI. Add outputs and options, then call @@ -185,6 +205,20 @@ public final class CoreTransactionBuilder { return self } + /// Add a zero-value OP_RETURN output carrying `data` for a MAYACHAIN-style + /// deposit. See https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/sending-transactions + @discardableResult + public func addOpReturn(_ data: Data) throws -> CoreTransactionBuilder { + try data.withUnsafeBytes { buf in + try core_wallet_tx_builder_add_op_return( + handle, + buf.baseAddress?.assumingMemoryBound(to: UInt8.self), + UInt(data.count) + ).check() + } + return self + } + @discardableResult public func setChangeAddress(_ address: String) throws -> CoreTransactionBuilder { let c = strdup(address) @@ -193,6 +227,22 @@ public final class CoreTransactionBuilder { return self } + /// Preserve outputs in insertion order for a MAYACHAIN-style deposit. + /// See https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/sending-transactions + @discardableResult + public func preserveOutputOrder() throws -> CoreTransactionBuilder { + try core_wallet_tx_builder_preserve_output_order(handle).check() + return self + } + + /// Route change to the first selected input address (VIN0) for a MAYACHAIN-style deposit. + /// See https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/sending-transactions + @discardableResult + public func changeToFirstInput() throws -> CoreTransactionBuilder { + try core_wallet_tx_builder_change_to_first_input(handle).check() + return self + } + @discardableResult public func setFeeRate(satPerKb: UInt64) throws -> CoreTransactionBuilder { try core_wallet_tx_builder_set_fee_rate(handle, satPerKb).check() diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift new file mode 100644 index 00000000000..a233710e135 --- /dev/null +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift @@ -0,0 +1,602 @@ +import Foundation +import XCTest +@testable import SwiftDashSDK + +@MainActor +final class MayaDepositVerificationIntegrationTests: IntegrationTestCase { + private enum Constants { + static let bip44TypeTag: UInt8 = 0 + static let bip44StandardTag: UInt8 = 0 + static let bip44AccountIndex: UInt32 = 0 + static let feeRateSatPerKb: UInt64 = 1_000 + static let minimumDepositDuffs: UInt64 = 10_000 + static let maxMemoBytes = 80 + static let longMemo = + "=:ARB.GLD:0x51a1449b3B6D635EddeC781cD47a99221712De97:344233230e4/1/0:_/def:15/0" + static let shortMemo = + "=:r:thor166n4w5039meulfa3p6ydg60ve6ueac7tlt0jws:669458827/1/0:_/def:15/0" + } + + private struct DepositObservation { + let name: String + let txHex: String + let serializedSize: Int + let inputCount: Int + let outputCount: Int + let vaultAddress: String + let depositAmount: UInt64 + let actualVaultAmount: UInt64 + let memo: String + let decodedMemo: String + let memoBytes: Int + let outputOneIsOpReturn: Bool + let inputZeroAddress: String? + let outputTwoMatchesInputZeroScript: Bool + let feeDuffs: UInt64 + let feeRateDuffsPerByte: Double + } + + private struct LegacyFeeObservation { + let name: String + let actualFeeDuffs: UInt64 + let legacyExpectedFeeDuffs: UInt64 + let serializedSize: Int + let inputCount: Int + let outputCount: Int + let payloadLength: Int + } + + private struct ParsedTransactionLayout { + let inputCount: Int + let outputCount: Int + let payloadLength: Int + } + + func testPrompt04StaticProofAndLegacyFeeParity() async throws { + try env.walletManager.startSpv(config: env.spvConfig) + + let shortDeposit = try await buildDepositObservation( + name: "short-memo-single-input", + fundingDashAmounts: [0.5], + depositAmountDuffs: 200_000, + memo: Constants.shortMemo + ) + let longDeposit = try await buildDepositObservation( + name: "long-memo-multi-input", + fundingDashAmounts: [0.2, 0.2], + depositAmountDuffs: 35_000_000, + memo: Constants.longMemo + ) + + assertDepositObservation(shortDeposit) + assertDepositObservation(longDeposit) + + let ordinary = try await legacyFeeObservationForOrdinarySend() + let multiRecipient = try await legacyFeeObservationForMultiRecipientSend() + let selectedInput = try await legacyFeeObservationForSelectedInputShape() + let sweep = try await legacyFeeObservationForDrainShape() + let addressFunding = try await legacyFeeObservationForAssetLockShape( + name: "asset-lock-address-top-up", + fundingType: .assetLockAddressTopUp + ) + let identityFunding = try await legacyFeeObservationForAssetLockShape( + name: "asset-lock-identity-registration", + fundingType: .identityRegistration + ) + + let feeObservations = [ + ordinary, + multiRecipient, + selectedInput, + sweep, + addressFunding, + identityFunding, + ] + + for observation in feeObservations { + XCTAssertEqual( + observation.actualFeeDuffs, + observation.legacyExpectedFeeDuffs, + "\(observation.name) fee moved: actual \(observation.actualFeeDuffs) vs legacy \(observation.legacyExpectedFeeDuffs)" + ) + } + + print(renderDepositObservation(shortDeposit)) + print(renderDepositObservation(longDeposit)) + for observation in feeObservations { + print(renderLegacyFeeObservation(observation)) + } + } + + private func buildDepositObservation( + name: String, + fundingDashAmounts: [Double], + depositAmountDuffs: UInt64, + memo: String + ) async throws -> DepositObservation { + let wallet = try await env.makeTestWallet(name: "maya-\(name)") + let coreWallet = wallet.getCoreWallet() + let platformWallet = wallet.getPlatformWallet() + + for amount in fundingDashAmounts { + let address = try coreWallet.nextReceiveAddress() + _ = try await fundByMining(address: address, dash: amount) + } + + let expectedSpendable = fundingDashAmounts.reduce(UInt64(0)) { partial, amount in + partial + UInt64((amount * 100_000_000).rounded()) + } + try await wallet.waitForSpendable(exactly: expectedSpendable, timeout: 90) + + let utxosBeforeBuild = try bip44Utxos(for: platformWallet) + let vaultAddress = try await env.coreRPC.getNewAddress() + let memoData = Data(memo.utf8) + + let builder = try CoreTransactionBuilder(network: .regtest) + try builder.addOutput(address: vaultAddress, amountDuffs: depositAmountDuffs) + try builder.addOpReturn(memoData) + try builder.preserveOutputOrder() + try builder.changeToFirstInput() + let tx = try builder.finalizeAtomic( + wallet: platformWallet, + accountType: .bip44, + accountIndex: Constants.bip44AccountIndex + ) + let txData = try tx.serializedData() + + let decoded = try TransactionDecoder.decode(txData, network: .regtest) + let memoOutput = decoded.outputs[1] + let decodedMemoData = try XCTUnwrap(opReturnPayload(from: memoOutput.scriptPubkey)) + let decodedMemo = try XCTUnwrap(String(data: decodedMemoData, encoding: .utf8)) + + let inputZeroMatch = try findMatchedUTXO(for: decoded.inputs[0], in: utxosBeforeBuild) + let outputTwoMatchesInputZeroScript: Bool + if decoded.outputs.count == 3 { + outputTwoMatchesInputZeroScript = decoded.outputs[2].scriptPubkey == inputZeroMatch.scriptPubkey + } else { + outputTwoMatchesInputZeroScript = false + } + + return DepositObservation( + name: name, + txHex: hex(txData), + serializedSize: txData.count, + inputCount: decoded.inputs.count, + outputCount: decoded.outputs.count, + vaultAddress: vaultAddress, + depositAmount: depositAmountDuffs, + actualVaultAmount: decoded.outputs[0].valueDuffs, + memo: memo, + decodedMemo: decodedMemo, + memoBytes: memoData.count, + outputOneIsOpReturn: memoOutput.scriptPubkey.first == 0x6a, + inputZeroAddress: decoded.inputs.first?.address, + outputTwoMatchesInputZeroScript: outputTwoMatchesInputZeroScript, + feeDuffs: tx.fee, + feeRateDuffsPerByte: Double(tx.fee) / Double(txData.count) + ) + } + + private func assertDepositObservation(_ observation: DepositObservation) { + XCTAssertGreaterThanOrEqual(observation.outputCount, 2, "\(observation.name) output count below Maya minimum") + XCTAssertLessThanOrEqual(observation.outputCount, 3, "\(observation.name) output count above Maya maximum") + XCTAssertEqual(observation.actualVaultAmount, observation.depositAmount, "\(observation.name) VOUT0 amount mismatch") + XCTAssertTrue(observation.outputOneIsOpReturn, "\(observation.name) VOUT1 is not OP_RETURN") + XCTAssertEqual(observation.decodedMemo, observation.memo, "\(observation.name) memo payload mismatch") + XCTAssertGreaterThanOrEqual(observation.depositAmount, Constants.minimumDepositDuffs, "\(observation.name) deposit fell below Maya dust floor") + XCTAssertLessThanOrEqual(observation.memoBytes, Constants.maxMemoBytes, "\(observation.name) memo exceeded 80 bytes") + XCTAssertGreaterThanOrEqual(observation.feeDuffs, UInt64(observation.serializedSize), "\(observation.name) fee fell below 1 duff/byte") + if observation.outputCount == 3 { + XCTAssertTrue(observation.outputTwoMatchesInputZeroScript, "\(observation.name) VOUT2 does not return change to VIN0 scriptPubKey") + } + } + + private func legacyFeeObservationForOrdinarySend() async throws -> LegacyFeeObservation { + let wallet = try await env.makeTestWallet(name: "legacy-ordinary") + let coreWallet = wallet.getCoreWallet() + let platformWallet = wallet.getPlatformWallet() + + let fundingAddress = try coreWallet.nextReceiveAddress() + _ = try await fundByMining(address: fundingAddress, dash: 0.5) + try await wallet.waitForSpendable(exactly: 50_000_000, timeout: 90) + + let utxosBeforeBuild = try bip44Utxos(for: platformWallet) + let recipient = try await env.coreRPC.getNewAddress() + + let builder = try CoreTransactionBuilder(network: .regtest) + try builder.addOutput(address: recipient, amountDuffs: 1_000_000) + let tx = try builder.finalizeAtomic( + wallet: platformWallet, + accountType: .bip44, + accountIndex: Constants.bip44AccountIndex + ) + let txData = try tx.serializedData() + + return try makeLegacyFeeObservation( + name: "ordinary-single-recipient-send", + txData: txData, + actualFee: tx.fee, + utxosBeforeBuild: utxosBeforeBuild + ) + } + + private func legacyFeeObservationForMultiRecipientSend() async throws -> LegacyFeeObservation { + let wallet = try await env.makeTestWallet(name: "legacy-multi-recipient") + let coreWallet = wallet.getCoreWallet() + let platformWallet = wallet.getPlatformWallet() + + let fundingAddress = try coreWallet.nextReceiveAddress() + _ = try await fundByMining(address: fundingAddress, dash: 0.5) + try await wallet.waitForSpendable(exactly: 50_000_000, timeout: 90) + + let utxosBeforeBuild = try bip44Utxos(for: platformWallet) + let recipientA = try await env.coreRPC.getNewAddress() + let recipientB = try await env.coreRPC.getNewAddress() + + let builder = try CoreTransactionBuilder(network: .regtest) + try builder.addOutput(address: recipientA, amountDuffs: 1_000_000) + try builder.addOutput(address: recipientB, amountDuffs: 2_000_000) + let tx = try builder.finalizeAtomic( + wallet: platformWallet, + accountType: .bip44, + accountIndex: Constants.bip44AccountIndex + ) + let txData = try tx.serializedData() + + return try makeLegacyFeeObservation( + name: "multi-recipient-bip70-shape", + txData: txData, + actualFee: tx.fee, + utxosBeforeBuild: utxosBeforeBuild + ) + } + + private func legacyFeeObservationForSelectedInputShape() async throws -> LegacyFeeObservation { + let wallet = try await env.makeTestWallet(name: "legacy-selected-input") + let coreWallet = wallet.getCoreWallet() + let platformWallet = wallet.getPlatformWallet() + + let fundingAddress = try coreWallet.nextReceiveAddress() + _ = try await fundByMining(address: fundingAddress, dash: 0.5) + try await wallet.waitForSpendable(exactly: 50_000_000, timeout: 90) + + let selectedUtxos = try bip44Utxos(for: platformWallet) + let recipient = try await env.coreRPC.getNewAddress() + + let builder = try CoreTransactionBuilder(network: .regtest) + try builder.addInputs( + wallet: platformWallet, + accountType: .bip44, + accountIndex: Constants.bip44AccountIndex, + utxos: selectedUtxos + ) + try builder.addOutput(address: recipient, amountDuffs: 1_000_000) + try builder.setChangeAddress(fundingAddress) + try builder.setFeeRate(satPerKb: Constants.feeRateSatPerKb) + let tx = try builder.finalizeAtomic( + wallet: platformWallet, + accountType: .bip44, + accountIndex: Constants.bip44AccountIndex + ) + let txData = try tx.serializedData() + + return try makeLegacyFeeObservation( + name: "selected-input-send-shape", + txData: txData, + actualFee: tx.fee, + utxosBeforeBuild: selectedUtxos + ) + } + + private func legacyFeeObservationForDrainShape() async throws -> LegacyFeeObservation { + let wallet = try await env.makeTestWallet(name: "legacy-drain") + let coreWallet = wallet.getCoreWallet() + let platformWallet = wallet.getPlatformWallet() + + let fundingAddress = try coreWallet.nextReceiveAddress() + _ = try await fundByMining(address: fundingAddress, dash: 0.5) + try await wallet.waitForSpendable(exactly: 50_000_000, timeout: 90) + + let utxosBeforeBuild = try bip44Utxos(for: platformWallet) + let recipient = try await env.coreRPC.getNewAddress() + + let builder = try CoreTransactionBuilder(network: .regtest) + try builder.addInputs( + wallet: platformWallet, + accountType: .bip44, + accountIndex: Constants.bip44AccountIndex, + utxos: utxosBeforeBuild + ) + try builder.setSelectionStrategy(.all) + try builder.setFeeRate(satPerKb: Constants.feeRateSatPerKb) + try builder.addOutput(address: recipient, amountDuffs: 0) + let tx = try builder.finalizeAtomic( + wallet: platformWallet, + accountType: .bip44, + accountIndex: Constants.bip44AccountIndex + ) + let txData = try tx.serializedData() + + return try makeLegacyFeeObservation( + name: "drain-shape-coinjoin-sweep-equivalent", + txData: txData, + actualFee: tx.fee, + utxosBeforeBuild: utxosBeforeBuild + ) + } + + private func legacyFeeObservationForAssetLockShape( + name: String, + fundingType: ManagedAssetLockManager.FundingType + ) async throws -> LegacyFeeObservation { + let wallet = try await env.makeTestWallet(name: name) + let coreWallet = wallet.getCoreWallet() + let platformWallet = wallet.getPlatformWallet() + + let fundingAddress = try coreWallet.nextReceiveAddress() + _ = try await fundByMining(address: fundingAddress, dash: 0.5) + try await wallet.waitForSpendable(exactly: 50_000_000, timeout: 90) + + let utxosBeforeBuild = try bip44Utxos(for: platformWallet) + let manager = try platformWallet.assetLockManager() + let resolver = MnemonicResolver() + let built = try manager.buildTransaction( + amountDuffs: 10_000_000, + accountIndex: Constants.bip44AccountIndex, + fundingType: fundingType, + identityIndex: 0, + resolver: resolver + ) + + let decoded = try TransactionDecoder.decode(built.transaction, network: .regtest) + let actualFee = try sumSelectedInputs(decoded.inputs, from: utxosBeforeBuild) + - decoded.outputs.reduce(UInt64(0)) { $0 + $1.valueDuffs } + + return try makeLegacyFeeObservation( + name: name, + txData: built.transaction, + actualFee: actualFee, + utxosBeforeBuild: utxosBeforeBuild + ) + } + + private func makeLegacyFeeObservation( + name: String, + txData: Data, + actualFee: UInt64, + utxosBeforeBuild: [PlatformWalletManager.AccountUtxo] + ) throws -> LegacyFeeObservation { + let decoded = try TransactionDecoder.decode(txData, network: .regtest) + let layout = try parseTransactionLayout(txData) + let selectedInputsValue = try sumSelectedInputs(decoded.inputs, from: utxosBeforeBuild) + let paidOutputs = decoded.outputs.reduce(UInt64(0)) { $0 + $1.valueDuffs } + XCTAssertEqual( + actualFee, + selectedInputsValue - paidOutputs, + "\(name) reported fee does not match selected-input minus output value" + ) + + let outputsLegacyBytes = layout.outputCount * 34 + let legacyExpected = 8 + + varIntSize(layout.inputCount) + + layout.inputCount * 148 + + varIntSize(layout.outputCount) + + outputsLegacyBytes + + (layout.payloadLength > 0 ? varIntSize(layout.payloadLength) + layout.payloadLength : 0) + + return LegacyFeeObservation( + name: name, + actualFeeDuffs: actualFee, + legacyExpectedFeeDuffs: UInt64(legacyExpected), + serializedSize: txData.count, + inputCount: layout.inputCount, + outputCount: layout.outputCount, + payloadLength: layout.payloadLength + ) + } + + @discardableResult + private func fundByMining(address: String, dash: Double) async throws -> String { + let txid = try await env.coreRPC.sendToAddress(amount: dash, address: address) + _ = try await env.mine(1) + return txid + } + + private func bip44Utxos(for wallet: ManagedPlatformWallet) throws -> [PlatformWalletManager.AccountUtxo] { + let walletId = wallet.walletId + guard let balance = env.walletManager.accountBalances(for: walletId).first(where: { + $0.typeTag == Constants.bip44TypeTag + && $0.standardTag == Constants.bip44StandardTag + && $0.index == Constants.bip44AccountIndex + }) else { + throw XCTSkip("BIP44 account balance was not materialized") + } + return env.walletManager.accountUtxos(for: walletId, balance: balance).filter { !$0.isLocked } + } + + private func findMatchedUTXO( + for input: DecodedTransaction.Input, + in utxos: [PlatformWalletManager.AccountUtxo] + ) throws -> PlatformWalletManager.AccountUtxo { + guard let match = utxos.first(where: { + $0.outpointTxid == input.prevTxid && $0.outpointVout == input.prevVout + }) else { + throw NSError(domain: "MayaVerification", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Could not match input \(hex(input.prevTxid)):\(input.prevVout) to a pre-build UTXO" + ]) + } + return match + } + + private func sumSelectedInputs( + _ inputs: [DecodedTransaction.Input], + from utxos: [PlatformWalletManager.AccountUtxo] + ) throws -> UInt64 { + var total: UInt64 = 0 + for input in inputs { + total += try findMatchedUTXO(for: input, in: utxos).valueDuffs + } + return total + } + + private func parseTransactionLayout(_ txData: Data) throws -> ParsedTransactionLayout { + var offset = 0 + + guard txData.count >= 4 else { + throw NSError(domain: "MayaVerification", code: 2, userInfo: [ + NSLocalizedDescriptionKey: "Serialized transaction too short" + ]) + } + offset += 4 + + let inputCount = try Int(readVarInt(from: txData, offset: &offset)) + for _ in 0.. UInt64 { + guard offset < data.count else { + throw NSError(domain: "MayaVerification", code: 4, userInfo: [ + NSLocalizedDescriptionKey: "Unexpected end of transaction while reading varint" + ]) + } + + let prefix = data[offset] + offset += 1 + + switch prefix { + case 0x00...0xfc: + return UInt64(prefix) + case 0xfd: + guard offset + 2 <= data.count else { throw truncatedVarIntError() } + let value = UInt64(data[offset]) | (UInt64(data[offset + 1]) << 8) + offset += 2 + return value + case 0xfe: + guard offset + 4 <= data.count else { throw truncatedVarIntError() } + let value = UInt64(data[offset]) + | (UInt64(data[offset + 1]) << 8) + | (UInt64(data[offset + 2]) << 16) + | (UInt64(data[offset + 3]) << 24) + offset += 4 + return value + default: + guard offset + 8 <= data.count else { throw truncatedVarIntError() } + var value: UInt64 = 0 + for shift in 0..<8 { + value |= UInt64(data[offset + shift]) << (8 * UInt64(shift)) + } + offset += 8 + return value + } + } + + private func truncatedVarIntError() -> NSError { + NSError(domain: "MayaVerification", code: 5, userInfo: [ + NSLocalizedDescriptionKey: "Unexpected end of transaction while reading extended varint" + ]) + } + + private func opReturnPayload(from script: Data) -> Data? { + guard script.count >= 2, script[0] == 0x6a else { return nil } + let pushOpcode = script[1] + + switch pushOpcode { + case 0x01...0x4b: + let length = Int(pushOpcode) + guard script.count == 2 + length else { return nil } + return script.subdata(in: 2..<(2 + length)) + case 0x4c: + guard script.count >= 3 else { return nil } + let length = Int(script[2]) + guard script.count == 3 + length else { return nil } + return script.subdata(in: 3..<(3 + length)) + case 0x4d: + guard script.count >= 4 else { return nil } + let length = Int(script[2]) | (Int(script[3]) << 8) + guard script.count == 4 + length else { return nil } + return script.subdata(in: 4..<(4 + length)) + default: + return nil + } + } + + private func varIntSize(_ value: Int) -> Int { + switch value { + case 0...0xfc: + return 1 + case 0xfd...0xffff: + return 3 + case 0x1_0000...0xffff_ffff: + return 5 + default: + return 9 + } + } + + private func hex(_ data: Data) -> String { + data.map { String(format: "%02x", $0) }.joined() + } + + private func renderDepositObservation(_ observation: DepositObservation) -> String { + [ + "PROMPT04_DEPOSIT \(observation.name)", + " tx_hex=\(observation.txHex)", + " serialized_size=\(observation.serializedSize)", + " input_count=\(observation.inputCount)", + " output_count=\(observation.outputCount)", + " vault_address=\(observation.vaultAddress)", + " deposit_amount=\(observation.depositAmount)", + " actual_vout0_amount=\(observation.actualVaultAmount)", + " memo_bytes=\(observation.memoBytes)", + " input0_address=\(observation.inputZeroAddress ?? "nil")", + " output2_matches_input0_script=\(observation.outputTwoMatchesInputZeroScript)", + " fee_duffs=\(observation.feeDuffs)", + String(format: " fee_rate_duffs_per_byte=%.6f", observation.feeRateDuffsPerByte), + ].joined(separator: "\n") + } + + private func renderLegacyFeeObservation(_ observation: LegacyFeeObservation) -> String { + [ + "PROMPT04_FEE \(observation.name)", + " actual_fee_duffs=\(observation.actualFeeDuffs)", + " legacy_expected_fee_duffs=\(observation.legacyExpectedFeeDuffs)", + " serialized_size=\(observation.serializedSize)", + " input_count=\(observation.inputCount)", + " output_count=\(observation.outputCount)", + " payload_length=\(observation.payloadLength)", + ].joined(separator: "\n") + } +} From 8370b6a2589a486e7c73561b22bf2a8c06ea30b8 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:55:25 +0300 Subject: [PATCH 2/6] test(sdk): tighten the Maya deposit assertions and gate the suite Review follow-ups on the deposit verification test: - Require exactly three outputs and always assert VOUT2 against VIN0. Both fixtures leave millions of duffs after the vault payment and fee, so change is mandatory; accepting two outputs let a regression that suppresses change pass the very test that exists to prove change-to-VIN0. - Assert the output and input counts before indexing, so a wrong shape fails readably instead of trapping on an out-of-range subscript and taking the test process down. - Cover the 80/81-byte OP_RETURN boundary rather than just the fixture, and reuse the same builder after a rejected payload. That pins the FFI guarantee this branch adds: the size check runs before `take_builder()`, so a refused memo must leave already-configured outputs and options intact. - Gate the suite behind MAYA_DEPOSIT_VERIFICATION=1. `run_tests.sh` runs this bundle in CI, and these tests sit behind several 90-second waits on top of a full SPV bootstrap, so a bootstrap stall would hang the job rather than fail it. Also renames MAX_STANDARD_OP_RETURN_BYTES to DEFAULT_MAX_OP_RETURN_BYTES, following key-wallet making the ceiling configurable per builder. --- .../src/core_wallet/transaction_builder.rs | 6 +- ...aDepositVerificationIntegrationTests.swift | 105 ++++++++++++++++-- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs b/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs index 2487644391b..1ab638e1b10 100644 --- a/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs +++ b/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs @@ -14,7 +14,7 @@ use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy; use key_wallet::wallet::managed_wallet_info::fee::FeeRate; use key_wallet::wallet::managed_wallet_info::transaction_builder::{ - TransactionBuilder, MAX_STANDARD_OP_RETURN_BYTES, + TransactionBuilder, DEFAULT_MAX_OP_RETURN_BYTES, }; use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; @@ -299,11 +299,11 @@ pub unsafe extern "C" fn core_wallet_tx_builder_add_op_return( // the caller already configured. Reject an over-long payload *before* taking the builder // so the slot keeps its real state. `add_op_return` re-checks; this is the same policy // constant, not a second opinion. - if data_len > MAX_STANDARD_OP_RETURN_BYTES { + if data_len > DEFAULT_MAX_OP_RETURN_BYTES { return PlatformWalletFFIResult::err( PlatformWalletFFIResultCode::ErrorInvalidParameter, format!( - "OP_RETURN payload too large: {data_len} bytes (max {MAX_STANDARD_OP_RETURN_BYTES})" + "OP_RETURN payload too large: {data_len} bytes (max {DEFAULT_MAX_OP_RETURN_BYTES})" ), ); } diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift index a233710e135..42e6723ba47 100644 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift @@ -52,7 +52,23 @@ final class MayaDepositVerificationIntegrationTests: IntegrationTestCase { let payloadLength: Int } + /// Opt-in gate. `run_tests.sh` (which CI runs for `swift-sdk-build`) executes this bundle, + /// and these tests sit behind several 90-second `waitForSpendable` windows on top of a full + /// SPV bootstrap. A bootstrap stall would hang the job rather than fail it, so they run only + /// when explicitly requested until the bootstrap path is reliable. + private static let isEnabled = + ProcessInfo.processInfo.environment["MAYA_DEPOSIT_VERIFICATION"] == "1" + + private func skipUnlessEnabled() throws { + try XCTSkipUnless( + Self.isEnabled, + "Set MAYA_DEPOSIT_VERIFICATION=1 to run the Maya deposit verification suite " + + "(requires a local dashmate devnet and a completed SPV bootstrap)." + ) + } + func testPrompt04StaticProofAndLegacyFeeParity() async throws { + try skipUnlessEnabled() try env.walletManager.startSpv(config: env.spvConfig) let shortDeposit = try await buildDepositObservation( @@ -108,6 +124,64 @@ final class MayaDepositVerificationIntegrationTests: IntegrationTestCase { } } + /// The 80-byte ceiling is the reason the whole memo path can refuse a swap, so prove the + /// boundary rather than the fixture: 80 bytes must be accepted, 81 rejected. + /// + /// Also pins the FFI guarantee this PR introduced — `core_wallet_tx_builder_add_op_return` + /// validates the payload *before* `take_builder()`, so a rejected memo must leave the + /// builder's already-configured outputs and options intact rather than replacing it with a + /// `mem::take` default. The rejected builder is therefore reused here and finalized, and the + /// resulting transaction must still carry the vault output and the ordering flags set before + /// the failed call. + func testOpReturnCeilingBoundaryAndRejectionPreservesBuilder() async throws { + try skipUnlessEnabled() + try env.walletManager.startSpv(config: env.spvConfig) + + let wallet = try await env.makeTestWallet(name: "maya-op-return-boundary") + let coreWallet = wallet.getCoreWallet() + let platformWallet = wallet.getPlatformWallet() + + let fundingAddress = try coreWallet.nextReceiveAddress() + _ = try await fundByMining(address: fundingAddress, dash: 0.5) + try await wallet.waitForSpendable(exactly: 50_000_000, timeout: 90) + + let depositAmount: UInt64 = 200_000 + let vaultAddress = try await env.coreRPC.getNewAddress() + let atCeiling = Data(repeating: 0x4d, count: Constants.maxMemoBytes) + let overCeiling = Data(repeating: 0x4d, count: Constants.maxMemoBytes + 1) + + let builder = try CoreTransactionBuilder(network: .regtest) + try builder.addOutput(address: vaultAddress, amountDuffs: depositAmount) + try builder.preserveOutputOrder() + try builder.changeToFirstInput() + + XCTAssertThrowsError(try builder.addOpReturn(overCeiling)) { error in + XCTAssertTrue( + "\(error)".lowercased().contains("op_return"), + "expected an OP_RETURN size error, got \(error)" + ) + } + + // Same builder instance: if the rejection had consumed it, this would build a + // transaction missing the vault output and the ordering flags. + try builder.addOpReturn(atCeiling) + let tx = try builder.finalizeAtomic( + wallet: platformWallet, + accountType: .bip44, + accountIndex: Constants.bip44AccountIndex + ) + + let decoded = try TransactionDecoder.decode(try tx.serializedData(), network: .regtest) + XCTAssertEqual(decoded.outputs.count, 3, "vault + memo + change survived the rejection") + XCTAssertEqual(decoded.outputs[0].address, vaultAddress, "VOUT0 lost after the rejected memo") + XCTAssertEqual(decoded.outputs[0].valueDuffs, depositAmount) + XCTAssertEqual(decoded.outputs[1].valueDuffs, 0) + XCTAssertEqual( + opReturnPayload(from: decoded.outputs[1].scriptPubkey), atCeiling, + "an exactly-80-byte payload must be accepted and carried verbatim" + ) + } + private func buildDepositObservation( name: String, fundingDashAmounts: [Double], @@ -149,14 +223,23 @@ final class MayaDepositVerificationIntegrationTests: IntegrationTestCase { let decodedMemoData = try XCTUnwrap(opReturnPayload(from: memoOutput.scriptPubkey)) let decodedMemo = try XCTUnwrap(String(data: decodedMemoData, encoding: .utf8)) - let inputZeroMatch = try findMatchedUTXO(for: decoded.inputs[0], in: utxosBeforeBuild) - let outputTwoMatchesInputZeroScript: Bool - if decoded.outputs.count == 3 { - outputTwoMatchesInputZeroScript = decoded.outputs[2].scriptPubkey == inputZeroMatch.scriptPubkey - } else { - outputTwoMatchesInputZeroScript = false + // Both fixtures fund far more than the deposit plus fee, so change is always well above + // dust and the transaction must be exactly vault + memo + change. Assert the shape here, + // before any subscripting: a wrong shape must surface as a readable failure rather than + // trapping on an out-of-range index and taking the whole test process down. + XCTAssertEqual( + decoded.outputs.count, 3, + "\(name) must contain vault, memo and change outputs" + ) + XCTAssertFalse(decoded.inputs.isEmpty, "\(name) has no inputs") + guard decoded.outputs.count == 3, let firstInput = decoded.inputs.first else { + throw XCTSkip("\(name) produced an unexpected transaction shape; assertions above hold the detail") } + let inputZeroMatch = try findMatchedUTXO(for: firstInput, in: utxosBeforeBuild) + let outputTwoMatchesInputZeroScript = + decoded.outputs[2].scriptPubkey == inputZeroMatch.scriptPubkey + return DepositObservation( name: name, txHex: hex(txData), @@ -178,17 +261,17 @@ final class MayaDepositVerificationIntegrationTests: IntegrationTestCase { } private func assertDepositObservation(_ observation: DepositObservation) { - XCTAssertGreaterThanOrEqual(observation.outputCount, 2, "\(observation.name) output count below Maya minimum") - XCTAssertLessThanOrEqual(observation.outputCount, 3, "\(observation.name) output count above Maya maximum") + // Exactly three, not a range: both fixtures leave millions of duffs after the vault + // payment and fee, so change is mandatory. Accepting two outputs would let a regression + // that suppresses change pass the very test that exists to prove change goes to VIN0. + XCTAssertEqual(observation.outputCount, 3, "\(observation.name) must be vault + memo + change") XCTAssertEqual(observation.actualVaultAmount, observation.depositAmount, "\(observation.name) VOUT0 amount mismatch") XCTAssertTrue(observation.outputOneIsOpReturn, "\(observation.name) VOUT1 is not OP_RETURN") XCTAssertEqual(observation.decodedMemo, observation.memo, "\(observation.name) memo payload mismatch") XCTAssertGreaterThanOrEqual(observation.depositAmount, Constants.minimumDepositDuffs, "\(observation.name) deposit fell below Maya dust floor") XCTAssertLessThanOrEqual(observation.memoBytes, Constants.maxMemoBytes, "\(observation.name) memo exceeded 80 bytes") XCTAssertGreaterThanOrEqual(observation.feeDuffs, UInt64(observation.serializedSize), "\(observation.name) fee fell below 1 duff/byte") - if observation.outputCount == 3 { - XCTAssertTrue(observation.outputTwoMatchesInputZeroScript, "\(observation.name) VOUT2 does not return change to VIN0 scriptPubKey") - } + XCTAssertTrue(observation.outputTwoMatchesInputZeroScript, "\(observation.name) VOUT2 does not return change to VIN0 scriptPubKey") } private func legacyFeeObservationForOrdinarySend() async throws -> LegacyFeeObservation { From 27b8502fb226a2e11ea570ebe5ad45726330f60b Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 4 Aug 2026 13:57:06 -0700 Subject: [PATCH 3/6] feat(kotlin-sdk): bind OP_RETURN, output-order and VIN0-change builder controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kotlin/Android parity for the Swift-only surface #4286 added: four thin JNI trampolines over core_wallet_tx_builder_add_op_return / _preserve_output_order / _change_to_first_input and core_wallet_signed_transaction_v2_bytes, the matching WalletManagerNative declarations, CoreTransactionBuilder.addOpReturn/preserveOutputOrder/ changeToFirstInput, and FinalizedCoreTransaction.serializedData() — a non-consuming read so callers can assert the MAYACHAIN deposit shape (vault VOUT0, memo VOUT1, change VOUT2) before broadcasting. Instrumented binding test needs no funded wallet: symbols resolve, the Maya option sequence succeeds, an 81-byte memo throws while the builder survives, and the bytes reader rejects a null handle. Co-Authored-By: Claude Fable 5 --- .../CoreTxBuilderOpReturnBindingTest.kt | 91 ++++++++++++ .../dashsdk/ffi/WalletManagerNative.kt | 31 +++++ .../dashsdk/wallet/CoreTransactionBuilder.kt | 41 ++++++ .../rs-unified-sdk-jni/src/wallet_manager.rs | 130 ++++++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/CoreTxBuilderOpReturnBindingTest.kt diff --git a/packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/CoreTxBuilderOpReturnBindingTest.kt b/packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/CoreTxBuilderOpReturnBindingTest.kt new file mode 100644 index 00000000000..9d32c4e04d6 --- /dev/null +++ b/packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/CoreTxBuilderOpReturnBindingTest.kt @@ -0,0 +1,91 @@ +package org.dashfoundation.dashsdk.wallet + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.dashfoundation.dashsdk.ffi.DashSDKException +import org.dashfoundation.dashsdk.ffi.NativeLoader +import org.dashfoundation.dashsdk.ffi.WalletManagerNative +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Binding-level coverage for the MAYACHAIN-deposit builder controls + * (`add_op_return`, `preserve_output_order`, `change_to_first_input`, + * `signed_transaction_v2_bytes`) — the Android counterpart of the gated + * Swift `MayaDepositVerificationIntegrationTests`, minus everything that + * needs a funded wallet. Proves the four new JNI symbols resolve, happy-path + * calls succeed against a live builder, and the FFI's error paths surface as + * [DashSDKException] instead of aborting. + * + * No network, no wallet, no funds: a builder handle alone accepts outputs + * and options; only funding/finalizing needs a wallet. The full + * deposit-shape assertion (vault VOUT0 / memo VOUT1 / change VOUT2 on a + * really-funded transaction) stays with the Swift integration suite and the + * wallet-side testnet verification. + */ +@RunWith(AndroidJUnit4::class) +class CoreTxBuilderOpReturnBindingTest { + + // Any syntactically valid testnet P2PKH address works — the builder + // validates encoding/network only; nothing is funded or sent. Same + // address the FFI's own persistence tests use. + private val testnetAddress = "yMqShkrgjTRuReBGFpQr7FozEF1QcNBBYA" + + private fun withBuilder(block: (Long) -> Unit) { + NativeLoader.ensureLoaded() + val builder = WalletManagerNative.coreTxBuilderNew(network = 1) + assertNotEquals("builder handle must be live", 0L, builder) + try { + block(builder) + } finally { + WalletManagerNative.coreTxBuilderDestroy(builder) + } + } + + @Test + fun mayaShapeOptionsBindAndAccept() { + withBuilder { builder -> + // The canonical Maya deposit sequence, sans funding: vault output, + // memo, insertion-order + VIN0-change options. + WalletManagerNative.coreTxBuilderAddOutput(builder, vaultAddressForTest(), 100_000) + WalletManagerNative.coreTxBuilderAddOpReturn( + builder, + "=:ETH.ETH:0x1c7b17362c84287bd1184447e6dfeaf920c31bbe".toByteArray(Charsets.UTF_8), + ) + WalletManagerNative.coreTxBuilderPreserveOutputOrder(builder) + WalletManagerNative.coreTxBuilderChangeToFirstInput(builder) + } + } + + @Test + fun opReturnAcceptsExactly80Bytes() { + withBuilder { builder -> + WalletManagerNative.coreTxBuilderAddOpReturn(builder, ByteArray(80)) + } + } + + @Test + fun opReturnRejects81BytesAndBuilderSurvives() { + withBuilder { builder -> + assertThrows(DashSDKException::class.java) { + WalletManagerNative.coreTxBuilderAddOpReturn(builder, ByteArray(81)) + } + // The FFI rejects the payload BEFORE consuming builder state, so + // the same handle must still accept further configuration. + WalletManagerNative.coreTxBuilderAddOutput(builder, vaultAddressForTest(), 100_000) + } + } + + @Test + fun signedTransactionBytesSymbolBindsAndRejectsNullHandle() { + NativeLoader.ensureLoaded() + // Handle 0 can never be a finalized transaction; the call must throw + // (not crash), which also proves the JNI symbol resolves. + assertThrows(DashSDKException::class.java) { + WalletManagerNative.coreSignedTransactionV2Bytes(0L) + } + } + + private fun vaultAddressForTest(): String = testnetAddress +} diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt index 205fe225ba0..0710990e796 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt @@ -140,6 +140,14 @@ internal object WalletManagerNative { */ external fun coreTxBuilderAddOutput(builder: Long, address: String, amount: Long) + /** + * `core_wallet_tx_builder_add_op_return` — append a zero-value OP_RETURN + * output carrying [data] (a MAYACHAIN-style deposit memo). Rejected + * Rust-side over the 80-byte standardness limit — BEFORE the builder's + * state is consumed, so a refused memo leaves prior outputs intact. + */ + external fun coreTxBuilderAddOpReturn(builder: Long, data: ByteArray) + /** * `core_wallet_tx_builder_set_change_address` — override the change * address (network-checked). Optional; the Core→Core send relies on @@ -147,6 +155,21 @@ internal object WalletManagerNative { */ external fun coreTxBuilderSetChangeAddress(builder: Long, address: String) + /** + * `core_wallet_tx_builder_preserve_output_order` — keep outputs in + * insertion order instead of BIP-69 sorting them at build time + * (MAYACHAIN deposits require vault = VOUT0, memo = VOUT1). + */ + external fun coreTxBuilderPreserveOutputOrder(builder: Long) + + /** + * `core_wallet_tx_builder_change_to_first_input` — route change to the + * address of the first selected input (VIN0). MAYACHAIN identifies the + * depositor by VIN0 and pays refunds there. Overrides the change address + * [coreTxBuilderSetFunding] assigned. + */ + external fun coreTxBuilderChangeToFirstInput(builder: Long) + /** `core_wallet_tx_builder_set_fee_rate` — fee rate in duffs/kB (> 0). */ external fun coreTxBuilderSetFeeRate(builder: Long, satPerKb: Long) @@ -239,6 +262,14 @@ internal object WalletManagerNative { /** Read the finalized transaction's fee before consumption. */ external fun coreSignedTransactionV2Fee(transaction: Long): Long + /** + * `core_wallet_signed_transaction_v2_bytes` — the consensus-serialized + * signed transaction bytes, read WITHOUT consuming the ownership token. + * Lets the caller assert the deposit shape (e.g. MAYACHAIN's + * vault/OP_RETURN/change ordering) before deciding to broadcast. + */ + external fun coreSignedTransactionV2Bytes(transaction: Long): ByteArray + /** `core_wallet_destroy` — release a core handle from [platformWalletGetCore]. Safe on 0. */ external fun coreWalletDestroy(coreHandle: Long) diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt index 9a988601d30..35296000266 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt @@ -61,11 +61,40 @@ class CoreTransactionBuilder internal constructor(network: Network) : AutoClosea WalletManagerNative.coreTxBuilderAddOutput(handle, address, amountDuffs) } + /** + * Add a zero-value OP_RETURN output carrying [data] for a MAYACHAIN-style + * deposit memo (mirror of Swift's `addOpReturn`). Payloads over the + * 80-byte standardness limit are rejected Rust-side without disturbing + * outputs already added. + * See https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/sending-transactions + */ + internal fun addOpReturn(data: ByteArray): CoreTransactionBuilder = apply { + WalletManagerNative.coreTxBuilderAddOpReturn(handle, data) + } + /** Override the change address (network-checked Rust-side). */ internal fun setChangeAddress(address: String): CoreTransactionBuilder = apply { WalletManagerNative.coreTxBuilderSetChangeAddress(handle, address) } + /** + * Preserve outputs in insertion order (skip BIP-69 sorting) for a + * MAYACHAIN-style deposit — vault must stay VOUT0, memo VOUT1 (mirror of + * Swift's `preserveOutputOrder`). + */ + internal fun preserveOutputOrder(): CoreTransactionBuilder = apply { + WalletManagerNative.coreTxBuilderPreserveOutputOrder(handle) + } + + /** + * Route change to the first selected input's address (VIN0) for a + * MAYACHAIN-style deposit — MAYAChain identifies the depositor by VIN0 + * and pays refunds there (mirror of Swift's `changeToFirstInput`). + */ + internal fun changeToFirstInput(): CoreTransactionBuilder = apply { + WalletManagerNative.coreTxBuilderChangeToFirstInput(handle) + } + /** Set the fee rate in duffs/kB (> 0). */ internal fun setFeeRate(satPerKb: Long): CoreTransactionBuilder = apply { WalletManagerNative.coreTxBuilderSetFeeRate(handle, satPerKb) @@ -185,6 +214,18 @@ class FinalizedCoreTransaction internal constructor(handle: Long, val fee: Long) internal fun takeForAbandon(): Long = takeForBroadcast() + /** + * Consensus-serialized signed transaction bytes (copied out) WITHOUT + * consuming the ownership token — mirror of Swift's `serializedData()`. + * Lets the caller assert the deposit shape (e.g. MAYACHAIN's + * vault/OP_RETURN/change output order) before deciding to broadcast. + */ + fun serializedData(): ByteArray { + val handle = handleRef.get() + check(handle != 0L) { "FinalizedCoreTransaction has already been consumed" } + return WalletManagerNative.coreSignedTransactionV2Bytes(handle) + } + override fun close() = cleanable.clean() private class Cleanup(private val handleRef: AtomicLong) : Runnable { diff --git a/packages/rs-unified-sdk-jni/src/wallet_manager.rs b/packages/rs-unified-sdk-jni/src/wallet_manager.rs index 78c25061fa6..5cf95ad61f5 100644 --- a/packages/rs-unified-sdk-jni/src/wallet_manager.rs +++ b/packages/rs-unified-sdk-jni/src/wallet_manager.rs @@ -701,6 +701,40 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_c }) } +/// `core_wallet_tx_builder_add_op_return` — append a zero-value OP_RETURN +/// output carrying `data` (a MAYACHAIN-style deposit memo). The FFI rejects +/// a payload over the 80-byte standardness limit BEFORE consuming the +/// builder's state, so a refused memo leaves outputs/options intact. +#[no_mangle] +pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_coreTxBuilderAddOpReturn( + mut env: JNIEnv, + _class: JClass, + builder: jlong, + data: JByteArray, +) { + guard(&mut env, (), |env| { + if builder == 0 { + throw_sdk_exception(env, 1, "builder handle is 0"); + return; + } + let bytes = match env.convert_byte_array(&data) { + Ok(b) => b, + Err(_) => { + throw_sdk_exception(env, 1, "data must be a byte[]"); + return; + } + }; + let result = unsafe { + platform_wallet_ffi::core_wallet_tx_builder_add_op_return( + builder as *mut platform_wallet_ffi::FFITransactionBuilder, + bytes.as_ptr(), + bytes.len(), + ) + }; + let _ = take_pwffi_error(env, result); + }) +} + /// `core_wallet_tx_builder_set_change_address` — override the change /// address (network-checked Rust-side). Optional: `set_funding` also sets a /// change address, so the `.coreToCore` send path does not call this. @@ -729,6 +763,53 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_c }) } +/// `core_wallet_tx_builder_preserve_output_order` — keep outputs in +/// insertion order instead of BIP-69 sorting them at build time. Required +/// for MAYACHAIN-style deposits (vault must stay VOUT0, memo VOUT1). +#[no_mangle] +pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_coreTxBuilderPreserveOutputOrder( + mut env: JNIEnv, + _class: JClass, + builder: jlong, +) { + guard(&mut env, (), |env| { + if builder == 0 { + throw_sdk_exception(env, 1, "builder handle is 0"); + return; + } + let result = unsafe { + platform_wallet_ffi::core_wallet_tx_builder_preserve_output_order( + builder as *mut platform_wallet_ffi::FFITransactionBuilder, + ) + }; + let _ = take_pwffi_error(env, result); + }) +} + +/// `core_wallet_tx_builder_change_to_first_input` — route change to the +/// address of the first selected input (VIN0). Required for MAYACHAIN-style +/// deposits: MAYAChain identifies the depositor by VIN0 and pays refunds +/// there. Overrides any change address `set_funding` assigned. +#[no_mangle] +pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_coreTxBuilderChangeToFirstInput( + mut env: JNIEnv, + _class: JClass, + builder: jlong, +) { + guard(&mut env, (), |env| { + if builder == 0 { + throw_sdk_exception(env, 1, "builder handle is 0"); + return; + } + let result = unsafe { + platform_wallet_ffi::core_wallet_tx_builder_change_to_first_input( + builder as *mut platform_wallet_ffi::FFITransactionBuilder, + ) + }; + let _ = take_pwffi_error(env, result); + }) +} + /// `core_wallet_tx_builder_set_fee_rate` — set the fee rate in duffs/kB. /// Rejects a non-positive value at the boundary (a negative jlong would /// otherwise bit-cast to a huge u64). @@ -1182,6 +1263,55 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_c }) } +/// `core_wallet_signed_transaction_v2_bytes` — the consensus-serialized +/// signed transaction bytes of a finalized-transaction handle from +/// [coreTxBuilderFinalize], WITHOUT consuming the ownership token (mirror of +/// Swift's `FinalizedCoreTransaction.serializedData()`). Lets the caller +/// assert the deposit shape (e.g. MAYACHAIN's vault/OP_RETURN/change output +/// order) before deciding to broadcast. The FFI-owned buffer is copied into +/// the returned `byte[]` and freed here. +#[no_mangle] +pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_coreSignedTransactionV2Bytes( + mut env: JNIEnv, + _class: JClass, + transaction_handle: jlong, +) -> jni::sys::jbyteArray { + guard(&mut env, ptr::null_mut(), |env| { + if transaction_handle == 0 { + throw_sdk_exception(env, 1, "transaction handle is 0"); + return ptr::null_mut(); + } + let mut bytes_ptr: *mut u8 = ptr::null_mut(); + let mut bytes_len: usize = 0; + let result = unsafe { + platform_wallet_ffi::core_wallet_signed_transaction_v2_bytes( + transaction_handle as Handle, + &mut bytes_ptr, + &mut bytes_len, + ) + }; + if take_pwffi_error(env, result) { + return ptr::null_mut(); + } + if bytes_ptr.is_null() || bytes_len == 0 { + // A signed transaction is never 0 bytes — same check Swift makes. + throw_sdk_exception( + env, + 1, + "FFI returned success but finalized transaction bytes were empty", + ); + return ptr::null_mut(); + } + // Copy into a JVM array, then free the FFI-owned buffer on every path. + let array = { + let slice = unsafe { std::slice::from_raw_parts(bytes_ptr, bytes_len) }; + env.byte_array_from_slice(slice) + }; + unsafe { platform_wallet_ffi::platform_wallet_bytes_free(bytes_ptr, bytes_len) }; + array.map(|a| a.into_raw()).unwrap_or(ptr::null_mut()) + }) +} + /// `core_wallet_destroy` — release a transient core-wallet handle from /// [platformWalletGetCore]. Safe on 0. #[no_mangle] From 677819f9c42586a7e85ad01c61fbaebd1d554000 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 6 Aug 2026 23:08:50 +0700 Subject: [PATCH 4/6] feat(kotlin-sdk): expose the MAYACHAIN builder controls through buildSignedPayment The three builder controls, the builder itself, and WalletManagerNative are all internal, so a consumer of the published dash-sdk-android artifact could not add the OP_RETURN memo, preserve output order, route change to VIN0, or inspect the result before broadcast. Thread opReturnData / preserveOutputOrder / changeToFirstInput through the public ManagedPlatformWallet.buildSignedPayment: one atomic native select+reserve+sign+register returning a SignedCoreTransaction whose rawTxBytes lets the caller assert the deposit shape (vault VOUT0 / memo VOUT1 / change VOUT2) before broadcastSigned or releaseReservation. The deprecated setFunding/buildSigned split stays inaccessible. Instrumented coverage drives only the public overload: on an unfunded wallet the canonical Maya option set must reach atomic selection (CoreInsufficientFunds), an 81-byte memo must fail before selection, and the wallet must survive to run a well-formed build afterwards. Registers the core.maya_op_return_deposit parity capability over the four shared FFI symbols, verified by the gated Swift Maya integration suite and the Kotlin instrumented tests. Co-Authored-By: Claude Fable 5 --- docs/sdk/sdk-parity-manifest.json | 72 +++++++++ packages/kotlin-sdk/PARITY_SUMMARY.md | 15 +- .../BuildSignedPaymentMayaOptionsTest.kt | 149 ++++++++++++++++++ .../dashsdk/wallet/CoreTransactionBuilder.kt | 21 ++- .../dashsdk/wallet/ManagedPlatformWallet.kt | 39 +++++ 5 files changed, 283 insertions(+), 13 deletions(-) create mode 100644 packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/BuildSignedPaymentMayaOptionsTest.kt diff --git a/docs/sdk/sdk-parity-manifest.json b/docs/sdk/sdk-parity-manifest.json index d07107d4c76..08ac2ee640e 100644 --- a/docs/sdk/sdk-parity-manifest.json +++ b/docs/sdk/sdk-parity-manifest.json @@ -19,7 +19,11 @@ "core_wallet_signed_payment_broadcast": "packages/rs-platform-wallet-ffi/src/core_wallet/signed_payment.rs", "core_wallet_signed_payment_finalize": "packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs", "core_wallet_signed_payment_release": "packages/rs-platform-wallet-ffi/src/core_wallet/signed_payment.rs", + "core_wallet_signed_transaction_v2_bytes": "packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs", + "core_wallet_tx_builder_add_op_return": "packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs", + "core_wallet_tx_builder_change_to_first_input": "packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs", "core_wallet_tx_builder_finalize": "packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs", + "core_wallet_tx_builder_preserve_output_order": "packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs", "dash_sdk_sign_async_completion": "packages/rs-sdk-ffi/src/signer.rs", "dpns_name_array_free": "packages/rs-platform-wallet-ffi/src/dpns.rs", "managed_identity_get_contested_dpns_names": "packages/rs-platform-wallet-ffi/src/dpns.rs", @@ -255,6 +259,74 @@ } ] }, + { + "id": "core.maya_op_return_deposit", + "title": "MAYACHAIN-shaped deposit build: OP_RETURN memo, preserved output order, change to VIN0, pre-broadcast bytes", + "area": "correctness", + "shared_apis": [ + "core_wallet_tx_builder_add_op_return", + "core_wallet_tx_builder_preserve_output_order", + "core_wallet_tx_builder_change_to_first_input", + "core_wallet_signed_transaction_v2_bytes" + ], + "required_persistence_capabilities": [], + "hosts": { + "swift": { + "sdk": "supported", + "example_app": "not-applicable", + "restart": "not_applicable", + "reason": null + }, + "kotlin": { + "sdk": "supported", + "example_app": "not-applicable", + "restart": "not_applicable", + "reason": null + } + }, + "verification": [ + { + "host": "swift", + "kind": "integration", + "file": "packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift", + "id": "testPrompt04StaticProofAndLegacyFeeParity", + "command": "RUN_INTEGRATION_TESTS=1 swift test --package-path packages/swift-sdk --filter MayaDepositVerificationIntegrationTests", + "covers_restart": false + }, + { + "host": "swift", + "kind": "integration", + "file": "packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift", + "id": "testOpReturnCeilingBoundaryAndRejectionPreservesBuilder", + "command": "RUN_INTEGRATION_TESTS=1 swift test --package-path packages/swift-sdk --filter MayaDepositVerificationIntegrationTests", + "covers_restart": false + }, + { + "host": "kotlin", + "kind": "device", + "file": "packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/CoreTxBuilderOpReturnBindingTest.kt", + "id": "mayaShapeOptionsBindAndAccept", + "command": "cd packages/kotlin-sdk && ./gradlew :sdk:connectedDebugAndroidTest", + "covers_restart": false + }, + { + "host": "kotlin", + "kind": "device", + "file": "packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/BuildSignedPaymentMayaOptionsTest.kt", + "id": "mayaOptionsThreadThroughThePublicBuildSignedPayment", + "command": "cd packages/kotlin-sdk && ./gradlew :sdk:connectedDebugAndroidTest", + "covers_restart": false + }, + { + "host": "kotlin", + "kind": "device", + "file": "packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/BuildSignedPaymentMayaOptionsTest.kt", + "id": "oversizeMemoFailsBeforeSelectionAndWalletSurvives", + "command": "cd packages/kotlin-sdk && ./gradlew :sdk:connectedDebugAndroidTest", + "covers_restart": false + } + ] + }, { "id": "tokens.full_u64_domain", "title": "Token amounts and costs preserve the full u64 domain", diff --git a/packages/kotlin-sdk/PARITY_SUMMARY.md b/packages/kotlin-sdk/PARITY_SUMMARY.md index 7f7b2a0b8bb..a63ff74953d 100644 --- a/packages/kotlin-sdk/PARITY_SUMMARY.md +++ b/packages/kotlin-sdk/PARITY_SUMMARY.md @@ -2,23 +2,23 @@ # Kotlin/Swift executable parity summary Audit baseline: `PR #3999 @ 6dbc72a54df72d26eb9c4a014b425d2b95134e4e` -Capabilities tracked: **24** +Capabilities tracked: **25** ## Status counts | Host | Surface | Supported | Partial | Unsupported | Not applicable | | --- | --- | ---: | ---: | ---: | ---: | -| Swift | SDK | 14 | 8 | 1 | 1 | -| Swift | Example app | 4 | 12 | 1 | 7 | -| Kotlin | SDK | 12 | 12 | 0 | 0 | -| Kotlin | Example app | 5 | 12 | 0 | 7 | +| Swift | SDK | 15 | 8 | 1 | 1 | +| Swift | Example app | 4 | 12 | 1 | 8 | +| Kotlin | SDK | 13 | 12 | 0 | 0 | +| Kotlin | Example app | 5 | 12 | 0 | 8 | ## Restart coverage | Host | Tested | Required | Not applicable | | --- | ---: | ---: | ---: | -| Swift | 0 | 7 | 17 | -| Kotlin | 4 | 6 | 14 | +| Swift | 0 | 7 | 18 | +| Kotlin | 4 | 6 | 15 | ## Capability status @@ -27,6 +27,7 @@ Capabilities tracked: **24** | `persistence.platform_address_identity` | partial / not-applicable / required | supported / not-applicable / tested | | `core.atomic_send` | supported / supported / not_applicable | supported / supported / not_applicable | | `core.deferred_signed_payment` | supported / not-applicable / not_applicable | supported / not-applicable / not_applicable | +| `core.maya_op_return_deposit` | supported / not-applicable / not_applicable | supported / not-applicable / not_applicable | | `tokens.full_u64_domain` | partial / partial / required | supported / supported / tested | | `shielded.seedless_restart` | supported / partial / required | partial / partial / required | | `dashpay.invitations` | supported / partial / required | supported / partial / required | diff --git a/packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/BuildSignedPaymentMayaOptionsTest.kt b/packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/BuildSignedPaymentMayaOptionsTest.kt new file mode 100644 index 00000000000..4673dd354b9 --- /dev/null +++ b/packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/wallet/BuildSignedPaymentMayaOptionsTest.kt @@ -0,0 +1,149 @@ +package org.dashfoundation.dashsdk.wallet + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.runBlocking +import org.dashfoundation.dashsdk.Network +import org.dashfoundation.dashsdk.Sdk +import org.dashfoundation.dashsdk.config.SdkConfig +import org.dashfoundation.dashsdk.errors.DashSdkError +import org.dashfoundation.dashsdk.persistence.DashDatabase +import org.dashfoundation.dashsdk.security.WalletStorage +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Consumer-surface coverage for the MAYACHAIN builder controls: drives ONLY + * the public [ManagedPlatformWallet.buildSignedPayment] overload (the API an + * app consuming the published `dash-sdk-android` artifact can reach), not the + * internal [CoreTransactionBuilder] / `WalletManagerNative` surface that + * [CoreTxBuilderOpReturnBindingTest] pins. + * + * Runs offline against an UNFUNDED wallet, so the deepest reachable outcome + * is key-wallet's atomic selection failing with + * [DashSdkError.PlatformWallet.CoreInsufficientFunds] — which is exactly the + * point: reaching that error through the option-carrying call proves the + * memo/order/change options were accepted and threaded into the build (an + * option failure surfaces earlier as a different error), while an oversize + * memo must fail BEFORE selection with something other than + * insufficient-funds and leave the wallet usable. The full funded + * deposit-shape assertion (vault VOUT0 / memo VOUT1 / change VOUT2) stays + * with the gated Swift `MayaDepositVerificationIntegrationTests` and the + * wallet-side testnet verification. + */ +@RunWith(AndroidJUnit4::class) +class BuildSignedPaymentMayaOptionsTest { + + // BIP39 English test vector (all-zero entropy) — same as + // WalletManagerRoundTripTest; nothing is funded or broadcast. + private val testMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon " + + "abandon abandon abandon about" + + // Syntactically valid testnet P2PKH standing in for a Maya vault; the + // builder validates encoding/network only. + private val vaultAddress = "yMqShkrgjTRuReBGFpQr7FozEF1QcNBBYA" + + private val mayaMemo = + "=:ETH.ETH:0x1c7b17362c84287bd1184447e6dfeaf920c31bbe".toByteArray(Charsets.UTF_8) + + private lateinit var db: DashDatabase + private lateinit var walletStorage: WalletStorage + private lateinit var sdk: Sdk + + @Before + fun setUp() = runBlocking { + val context = InstrumentationRegistry.getInstrumentation().targetContext + db = DashDatabase.createInMemory(context) + walletStorage = WalletStorage(context) + // Testnet, no overrides → offline client build (no connection made). + sdk = Sdk.create(SdkConfig(network = Network.TESTNET)) + } + + @After + fun tearDown() { + runCatching { db.close() } + runCatching { sdk.close() } + } + + private fun withUnfundedWallet( + block: suspend (ManagedPlatformWallet, Long) -> Unit, + ) = runBlocking { + PlatformWalletManager(sdk, Network.TESTNET, db, walletStorage).use { manager -> + val created = manager.createWallet( + mnemonic = testMnemonic, + name = "maya-options", + createDefaultAccounts = true, + ) + val wallet = manager.wallet(forWalletId = created.walletId) + assertNotNull("created wallet is addressable", wallet) + block(wallet!!, manager.mnemonicResolverHandle) + } + } + + @Test + fun mayaOptionsThreadThroughThePublicBuildSignedPayment() = withUnfundedWallet { wallet, signer -> + // The canonical Maya sequence through the public API. On an unfunded + // wallet the first possible failure point past option application is + // atomic selection — so insufficient-funds here means the memo and + // both shape flags were accepted and threaded into the build. + val error = runCatching { + wallet.buildSignedPayment( + recipients = listOf(vaultAddress to 100_000L), + network = Network.TESTNET, + coreSignerHandle = signer, + opReturnData = mayaMemo, + preserveOutputOrder = true, + changeToFirstInput = true, + ) + }.exceptionOrNull() + + assertTrue( + "unfunded Maya-shaped build must fail at selection, got: $error", + error is DashSdkError.PlatformWallet.CoreInsufficientFunds, + ) + } + + @Test + fun oversizeMemoFailsBeforeSelectionAndWalletSurvives() = withUnfundedWallet { wallet, signer -> + val error = runCatching { + wallet.buildSignedPayment( + recipients = listOf(vaultAddress to 100_000L), + network = Network.TESTNET, + coreSignerHandle = signer, + opReturnData = ByteArray(81), + preserveOutputOrder = true, + changeToFirstInput = true, + ) + }.exceptionOrNull() + + assertNotNull("81-byte memo must be rejected", error) + assertFalse( + "oversize memo must fail before selection, got: $error", + error is DashSdkError.PlatformWallet.CoreInsufficientFunds, + ) + + // The rejection happened before anything was reserved; the same + // wallet must still drive a well-formed build to the selection stage. + val retry = runCatching { + wallet.buildSignedPayment( + recipients = listOf(vaultAddress to 100_000L), + network = Network.TESTNET, + coreSignerHandle = signer, + opReturnData = mayaMemo, + preserveOutputOrder = true, + changeToFirstInput = true, + ) + }.exceptionOrNull() + + assertTrue( + "wallet survives the rejected memo, got: $retry", + retry is DashSdkError.PlatformWallet.CoreInsufficientFunds, + ) + } +} diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt index 20751167728..6aace10f3b2 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt @@ -18,9 +18,14 @@ import java.util.concurrent.atomic.AtomicLong * backstop; the class is NOT thread-safe (the FFI builder must be used from * one thread at a time). * - * ## Not a public API — drive only through [ManagedPlatformWallet.sendToAddresses] + * ## Not a public API — drive only through [ManagedPlatformWallet] * - * The old [setFunding] + [buildSigned] sequence remains only as a deprecated ABI + * SDK consumers reach this builder through + * [ManagedPlatformWallet.sendToAddresses] (immediate broadcast) and + * [ManagedPlatformWallet.buildSignedPayment] (deferred BIP70/BIP270 flows and + * MAYACHAIN-style deposits — the option parameters there thread [addOpReturn], + * [preserveOutputOrder] and [changeToFirstInput] into the build). The old + * [setFunding] + [buildSigned] sequence remains only as a deprecated ABI * compatibility path and is not used by SDK convenience sends. * * @param network the wallet network — output and change addresses are @@ -63,7 +68,8 @@ class CoreTransactionBuilder internal constructor(network: Network) : AutoClosea /** * Add a zero-value OP_RETURN output carrying [data] for a MAYACHAIN-style - * deposit memo (mirror of Swift's `addOpReturn`). Payloads over the + * deposit memo (mirror of Swift's `addOpReturn` in + * packages/swift-sdk/.../CoreWallet/CoreTransactionBuilder.swift). Payloads over the * 80-byte standardness limit are rejected Rust-side without disturbing * outputs already added. * See https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/sending-transactions @@ -80,7 +86,8 @@ class CoreTransactionBuilder internal constructor(network: Network) : AutoClosea /** * Preserve outputs in insertion order (skip BIP-69 sorting) for a * MAYACHAIN-style deposit — vault must stay VOUT0, memo VOUT1 (mirror of - * Swift's `preserveOutputOrder`). + * Swift's `preserveOutputOrder` in + * packages/swift-sdk/.../CoreWallet/CoreTransactionBuilder.swift). */ internal fun preserveOutputOrder(): CoreTransactionBuilder = apply { WalletManagerNative.coreTxBuilderPreserveOutputOrder(handle) @@ -89,7 +96,8 @@ class CoreTransactionBuilder internal constructor(network: Network) : AutoClosea /** * Route change to the first selected input's address (VIN0) for a * MAYACHAIN-style deposit — MAYAChain identifies the depositor by VIN0 - * and pays refunds there (mirror of Swift's `changeToFirstInput`). + * and pays refunds there (mirror of Swift's `changeToFirstInput` in + * packages/swift-sdk/.../CoreWallet/CoreTransactionBuilder.swift). */ internal fun changeToFirstInput(): CoreTransactionBuilder = apply { WalletManagerNative.coreTxBuilderChangeToFirstInput(handle) @@ -267,7 +275,8 @@ class FinalizedCoreTransaction internal constructor(handle: Long, val fee: Long) /** * Consensus-serialized signed transaction bytes (copied out) WITHOUT - * consuming the ownership token — mirror of Swift's `serializedData()`. + * consuming the ownership token — mirror of Swift's `serializedData()` in + * packages/swift-sdk/.../CoreWallet/CoreTransactionBuilder.swift. * Lets the caller assert the deposit shape (e.g. MAYACHAIN's * vault/OP_RETURN/change output order) before deciding to broadcast. */ diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedPlatformWallet.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedPlatformWallet.kt index 40a35e06531..48564d25184 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedPlatformWallet.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedPlatformWallet.kt @@ -307,9 +307,35 @@ class ManagedPlatformWallet internal constructor( * this call and [broadcastSigned] drops the reservation on restart (the * UTXOs become spendable again) — the same property dashj has. * + * ## MAYACHAIN-style deposits + * + * The optional builder controls ([opReturnData], [preserveOutputOrder], + * [changeToFirstInput]) exist for MAYACHAIN/THORChain-style swap deposits + * (see https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/sending-transactions), + * where Swift consumers drive the public `CoreTransactionBuilder` directly + * (packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift: + * `addOpReturn` / `preserveOutputOrder` / `changeToFirstInput`). On + * Android the builder is not public, so this is the supported path: pass + * the vault as the single recipient, the swap memo as [opReturnData], and + * enable both flags — the built transaction then has the vault at VOUT0, + * the memo at VOUT1, and change (paid back to the first input's address, + * which MAYAChain uses to identify the depositor and refund) at VOUT2. + * Assert that shape from [SignedCoreTransaction.rawTxBytes] before + * deciding: [broadcastSigned] to submit, or [releaseReservation] / + * [SignedCoreTransaction.close] to abandon without ever broadcasting. + * * @param network the wallet network — see [sendToAddresses]. * @param coreSignerHandle the manager's `MnemonicResolverHandle` — see * [sendToAddresses]. No private key crosses the boundary. + * @param opReturnData if non-null, append a zero-value OP_RETURN output + * carrying these bytes after the recipient outputs. Size limits are + * enforced Rust-side (the 80-byte standardness ceiling); an oversize + * payload fails the build without reserving anything. + * @param preserveOutputOrder keep outputs in insertion order (skip the + * default BIP-69 sort) — required when a protocol assigns meaning to + * output indices, as MAYAChain does. + * @param changeToFirstInput route change back to the first selected + * input's address (VIN0) instead of a fresh change address. */ suspend fun buildSignedPayment( recipients: List>, @@ -317,6 +343,9 @@ class ManagedPlatformWallet internal constructor( coreSignerHandle: Long, accountType: AccountType = AccountType.BIP44, accountIndex: Int = 0, + opReturnData: ByteArray? = null, + preserveOutputOrder: Boolean = false, + changeToFirstInput: Boolean = false, ): SignedCoreTransaction = gate.opWithCleanupOnCancellation( // Native finalization mints the token and transfers reservation ownership // to it before the blocking JNI call returns, so the token already exists @@ -350,6 +379,16 @@ class ManagedPlatformWallet internal constructor( for ((address, amount) in recipients) { builder.addOutput(address, amount) } + // Canonical MAYACHAIN sequence: memo after the recipient + // outputs, then the shape flags — with preserveOutputOrder the + // built transaction keeps vault=VOUT0 / memo=VOUT1 / change last. + opReturnData?.let { builder.addOpReturn(it) } + if (preserveOutputOrder) { + builder.preserveOutputOrder() + } + if (changeToFirstInput) { + builder.changeToFirstInput() + } builder.finalizeSignedPayment( this@ManagedPlatformWallet, builderAccountType, From a1db38df2a02b116dad731744b3bbc95ba24c2b5 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 6 Aug 2026 23:54:08 +0700 Subject: [PATCH 5/6] fix(sdk): follow key-wallet's revert to MAX_STANDARD_OP_RETURN_BYTES The engine PR dropped the configurable ceiling (set_max_op_return_bytes / DEFAULT_MAX_OP_RETURN_BYTES) and went back to the plain MAX_STANDARD_OP_RETURN_BYTES constant; the FFI pre-check tracks the rename. Co-Authored-By: Claude Fable 5 --- .../src/core_wallet/transaction_builder.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs b/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs index 59f447115eb..9b37485330d 100644 --- a/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs +++ b/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs @@ -14,7 +14,7 @@ use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy; use key_wallet::wallet::managed_wallet_info::fee::FeeRate; use key_wallet::wallet::managed_wallet_info::transaction_builder::{ - TransactionBuilder, DEFAULT_MAX_OP_RETURN_BYTES, + TransactionBuilder, MAX_STANDARD_OP_RETURN_BYTES, }; use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; @@ -537,11 +537,11 @@ pub unsafe extern "C" fn core_wallet_tx_builder_add_op_return( // the caller already configured. Reject an over-long payload *before* taking the builder // so the slot keeps its real state. `add_op_return` re-checks; this is the same policy // constant, not a second opinion. - if data_len > DEFAULT_MAX_OP_RETURN_BYTES { + if data_len > MAX_STANDARD_OP_RETURN_BYTES { return PlatformWalletFFIResult::err( PlatformWalletFFIResultCode::ErrorInvalidParameter, format!( - "OP_RETURN payload too large: {data_len} bytes (max {DEFAULT_MAX_OP_RETURN_BYTES})" + "OP_RETURN payload too large: {data_len} bytes (max {MAX_STANDARD_OP_RETURN_BYTES})" ), ); } From b3f280143e325e224ac9caf0bf421bae76963401 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 7 Aug 2026 00:22:14 +0700 Subject: [PATCH 6/6] chore(deps): bump rust-dashcore to the merged OP_RETURN builder rev Points the eight workspace pins at dca5b05b (rust-dashcore's merged tx-builder OP_RETURN/output-order/change-routing support), replacing the local patch override the CI guard forbids committing. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 46 +++++++++++++++++++++++----------------------- Cargo.toml | 16 ++++++++-------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0bc4290724..a20399021bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1229,7 +1229,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1662,7 +1662,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" dependencies = [ "bincode", "bincode_derive", @@ -1673,7 +1673,7 @@ dependencies = [ [[package]] name = "dash-network-seeds" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" dependencies = [ "dash-network", ] @@ -1750,7 +1750,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" dependencies = [ "async-trait", "chrono", @@ -1779,7 +1779,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" dependencies = [ "anyhow", "base64-compat", @@ -1805,12 +1805,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" [[package]] name = "dashcore-rpc" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" dependencies = [ "dashcore-rpc-json", "hex", @@ -1823,7 +1823,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" dependencies = [ "bincode", "dashcore", @@ -1838,7 +1838,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" dependencies = [ "bincode", "dashcore-private", @@ -2474,7 +2474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2535,7 +2535,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2904,7 +2904,7 @@ dependencies = [ [[package]] name = "git-state" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" [[package]] name = "glob" @@ -3839,7 +3839,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4095,7 +4095,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" dependencies = [ "aes", "async-trait", @@ -4124,7 +4124,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4140,7 +4140,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=08bf729de819f52973002f754342fedac14a06db#08bf729de819f52973002f754342fedac14a06db" +source = "git+https://github.com/dashpay/rust-dashcore?rev=dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29#dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" dependencies = [ "async-trait", "bincode", @@ -4651,7 +4651,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5750,7 +5750,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6557,7 +6557,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6570,7 +6570,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6629,7 +6629,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7489,7 +7489,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8938,7 +8938,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 41ec72c3f04..41182275c4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,14 +52,14 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "08bf729de819f52973002f754342fedac14a06db" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "08bf729de819f52973002f754342fedac14a06db" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "08bf729de819f52973002f754342fedac14a06db" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "08bf729de819f52973002f754342fedac14a06db" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "08bf729de819f52973002f754342fedac14a06db" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "08bf729de819f52973002f754342fedac14a06db" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "08bf729de819f52973002f754342fedac14a06db" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "08bf729de819f52973002f754342fedac14a06db" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" } tokio-metrics = "0.5"