Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions docs/sdk/sdk-parity-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
15 changes: 8 additions & 7 deletions packages/kotlin-sdk/PARITY_SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading