Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ee6546a
feat(kotlin-sdk): wire-compatible encrypted txMetadata document creat…
bfoss765 Jul 10, 2026
2571bbb
test(platform-wallet): pin txMetadata wire-compat to a dashj-generate…
bfoss765 Jul 11, 2026
dfc3c23
test(platform-wallet): pin encrypted-txMetadata FETCH to the real tes…
bfoss765 Jul 11, 2026
edaa67f
debug(platform-wallet): warn-level stage breadcrumbs on the encrypted…
bfoss765 Jul 11, 2026
bcafc60
fix(platform-wallet): dual-emit encrypted-document breadcrumbs throug…
bfoss765 Jul 11, 2026
8b4290b
fix(platform-wallet): derive txMetadata keys through the mnemonic res…
bfoss765 Jul 11, 2026
ebfc9c4
fix(platform-wallet): nonzero-identity wire vector; keep master xprv …
bfoss765 Jul 11, 2026
988368a
perf(platform-wallet): share the fetched DataContract Arc instead of …
bfoss765 Jul 12, 2026
87b6f37
fix(kotlin-sdk): reject txMetadata version bytes the legacy stack can…
bfoss765 Jul 12, 2026
7f8ac4d
fix(platform-wallet): enforce txMetadata wire version (0|1) in Rust c…
bfoss765 Jul 12, 2026
8a74912
docs(txmetadata): scope legacy wire-compat to identity_index=0; relab…
bfoss765 Jul 12, 2026
5e5220d
test(txmetadata): check in a real-DerivationPathFactory provenance ve…
bfoss765 Jul 12, 2026
194ec86
fix(kotlin-sdk): gate encrypted-document create/fetch through the Tea…
bfoss765 Jul 16, 2026
d456822
fix(platform-wallet): pre-check txMetadata payload size before deriva…
bfoss765 Jul 21, 2026
0c5527a
fix(platform-wallet-ffi): zeroize + drop txMetadata plaintext before …
bfoss765 Jul 21, 2026
c8063e4
docs(txmetadata): mark the fetch regression test as a manual/testnet-…
bfoss765 Jul 21, 2026
9af3ece
chore(txmetadata): strip internal tracker tokens from comments
bfoss765 Jul 21, 2026
5accc5d
test(platform-wallet): anchor ENCRYPTED_METADATA_FIELD_MAX to the con…
bfoss765 Jul 21, 2026
64bec68
test(txmetadata): pin an independent legacy-INSTALL wire-compat vector
bfoss765 Jul 21, 2026
983154d
fix(kotlin-sdk): zeroize + early-drop the JNI txMetadata plaintext copy
bfoss765 Jul 22, 2026
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package org.dashfoundation.dashsdk.documents

import org.dashfoundation.dashsdk.wallet.op

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.dashfoundation.dashsdk.errors.mapNativeErrors
import org.dashfoundation.dashsdk.ffi.TransactionsNative

Expand Down Expand Up @@ -251,4 +248,117 @@ class DocumentTransactions internal constructor(
)
}
}

/**
* Create + broadcast an ENCRYPTED wallet-contract document (the wire-
* compatible `txMetadata` shape) on [contractId]'s [documentType], owned by
* [ownerId] — signed via [signerHandle]. Implements the create half of the
* legacy `BlockchainIdentity.publishTxMetaData` retirement
* (dashpay/platform#4086): the SDK derives the identity encryption key,
* seals [payload] into the legacy `version ‖ IV ‖ AES-256-CBC` blob, and
* writes `{keyIndex, encryptionKeyIndex, encryptedMetadata}`.
*
* Batching stays app-side: the caller serializes its items into [payload]
* (a protobuf `TxMetadataBatch`) and supplies its own per-document
* [encryptionKeyIndex] (dash-wallet's `1 + countAllRequests()` counter).
* The identity encryption key id (the `keyIndex` field) is chosen SDK-side
* to match the legacy stack, so the key never crosses the FFI boundary.
*
* @param encryptionKeyIndex per-document index; non-negative.
* @param version payload version byte (`1` = protobuf, as the wallet writes).
* @param payload already-serialized opaque plaintext; the SDK does not
* parse it.
* [mnemonicResolverHandle] is the host mnemonic-resolver handle
* ([org.dashfoundation.dashsdk.wallet.PlatformWalletManager.mnemonicResolverHandle]):
* required for external-signable wallets (the app's shape — the AES key
* derives on demand through the resolver), ignored for wallets with
* resident private keys.
*
* @return the confirmed document's canonical JSON (its 32-byte id is the
* base58 `$id` field).
*/
suspend fun createEncryptedDocument(
walletHandle: Long,
mnemonicResolverHandle: Long,
ownerId: ByteArray,
contractId: ByteArray,
documentType: String,
encryptionKeyIndex: Int,
version: Int,
payload: ByteArray,
signerHandle: Long,
): String = gate.op {
require(ownerId.size == 32) { "ownerId must be 32 bytes" }
require(contractId.size == 32) { "contractId must be 32 bytes" }
require(encryptionKeyIndex >= 0) {
"encryptionKeyIndex must be non-negative, got $encryptionKeyIndex"
}
// Only 0 (CBOR) and 1 (protobuf) are wire-meaningful: `seal_tx_metadata`
// writes this byte verbatim into the envelope and the legacy dashj stack
// (decryptTxMetadata) switches on exactly those two values. Accepting 2..255
// would silently seal a document the legacy stack can't decode, breaking the
// bidirectional wire-compat guarantee (dashpay/platform#4091).
require(version == 0 || version == 1) {
"version must be 0 (CBOR) or 1 (protobuf), got $version"
}
mapNativeErrors {
TransactionsNative.documentCreateEncrypted(
walletHandle,
mnemonicResolverHandle,
ownerId,
contractId,
documentType,
encryptionKeyIndex,
version,
payload,
signerHandle,
)
}
}

/**
* Fetch + DECRYPT every encrypted wallet-contract document owned by
* [ownerId] on [contractId]'s [documentType] updated at or after [sinceMs]
* (epoch-millis). Implements the read half of the legacy
* `BlockchainIdentity.getTxMetaData(since, key)` retirement
* (dashpay/platform#4087): the SDK fetches the owner-scoped, since-timestamp
* documents and decrypts each with the identity's derived key. Documents
* that fail to decrypt are skipped Rust-side (a bad document never aborts
* the fetch).
*
* @return a JSON array; each element is `{ "id", "ownerId" (base58),
* "keyIndex", "encryptionKeyIndex", "version", "updatedAt" (number|null),
* "payload" (base64 of the decrypted opaque plaintext) }`. The caller
* parses each `payload` itself (a protobuf `TxMetadataBatch` for
* `version == 1`) and reconciles memo / taxCategory / exchangeRate /
* service / giftCard fields into its local store.
*
* [mnemonicResolverHandle] is the host mnemonic-resolver handle
* ([org.dashfoundation.dashsdk.wallet.PlatformWalletManager.mnemonicResolverHandle]):
* required for external-signable wallets (the app's shape — the AES key
* derives on demand through the resolver), ignored for wallets with
* resident private keys.
*/
suspend fun fetchEncryptedDocuments(
walletHandle: Long,
mnemonicResolverHandle: Long,
ownerId: ByteArray,
contractId: ByteArray,
documentType: String,
sinceMs: Long,
): String = gate.op {
require(ownerId.size == 32) { "ownerId must be 32 bytes" }
require(contractId.size == 32) { "contractId must be 32 bytes" }
require(sinceMs >= 0) { "sinceMs must be non-negative, got $sinceMs" }
mapNativeErrors {
TransactionsNative.documentFetchEncrypted(
walletHandle,
mnemonicResolverHandle,
ownerId,
contractId,
documentType,
sinceMs,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,67 @@ internal object TransactionsNative {
signerHandle: Long,
): String

/**
* Create + broadcast an ENCRYPTED wallet-contract document (the wire-
* compatible `txMetadata` shape) on [contractId]'s [documentType], owned by
* [ownerId], signed via [signerHandle]. Bridges
* `platform_wallet_create_encrypted_document_with_signer`.
*
* The Rust side selects the identity's ENCRYPTION key id (the `keyIndex`
* field), derives the AES key from the wallet HD tree, and seals [payload]
* into the legacy `version ‖ IV ‖ AES-256-CBC` blob — decryptable by the
* legacy `org.dashj.platform` stack and vice versa.
*
* @param mnemonicResolverHandle the host mnemonic-resolver handle
* ([org.dashfoundation.dashsdk.wallet.PlatformWalletManager.mnemonicResolverHandle]);
* required (non-zero) for external-signable wallets — the app's shape —
* whose txMetadata AES key derives on demand through the resolver.
* Ignored for wallets with resident private keys.
* @param encryptionKeyIndex the app's per-document index (dash-wallet's
* monotonic `1 + countAllRequests()` counter); non-negative.
* @param version payload version byte (`1` = protobuf, as the wallet writes).
* @param payload the already-serialized opaque plaintext (a protobuf
* `TxMetadataBatch`); the SDK does not parse it.
* @return the confirmed document's canonical JSON (its 32-byte id is the
* base58 `$id` field).
*/
external fun documentCreateEncrypted(
walletHandle: Long,
mnemonicResolverHandle: Long,
ownerId: ByteArray,
contractId: ByteArray,
documentType: String,
encryptionKeyIndex: Int,
version: Int,
payload: ByteArray,
signerHandle: Long,
): String

/**
* Fetch + DECRYPT every encrypted wallet-contract document owned by
* [ownerId] on [contractId]'s [documentType] updated at or after [sinceMs]
* (epoch-millis). Bridges `platform_wallet_fetch_encrypted_documents` — the
* wire-compatible read counterpart of the legacy `getTxMetaData(since, key)`.
*
* @param mnemonicResolverHandle the host mnemonic-resolver handle
* ([org.dashfoundation.dashsdk.wallet.PlatformWalletManager.mnemonicResolverHandle]);
* required (non-zero) for external-signable wallets — the app's shape —
* whose txMetadata AES key derives on demand through the resolver.
* Ignored for wallets with resident private keys.
* @return a JSON array; each element is `{ "id", "ownerId" (base58),
* "keyIndex", "encryptionKeyIndex", "version", "updatedAt" (number|null),
* "payload" (base64 of the decrypted opaque plaintext) }`. Documents that
* fail to decrypt are skipped Rust-side.
*/
external fun documentFetchEncrypted(
walletHandle: Long,
mnemonicResolverHandle: Long,
ownerId: ByteArray,
contractId: ByteArray,
documentType: String,
sinceMs: Long,
): String

/**
* Cast a masternode contested-resource vote and wait for the response.
* Bridges `dash_sdk_contested_resource_cast_vote` (Swift
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.dashfoundation.dashsdk.documents

import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Test

/**
* Version-byte validation for [DocumentTransactions.createEncryptedDocument]
* (dashpay/platform#4091). Only 0 (CBOR) and 1 (protobuf) are wire-meaningful —
* `seal_tx_metadata` writes the byte verbatim and the legacy dashj
* `decryptTxMetadata` switches on exactly those two values, so an out-of-range
* byte would silently seal a document the legacy stack can't decode.
*
* The `require` runs before any native call (`TransactionsNative`), so the
* REJECTION paths are exercised on the JVM without the JNI library loaded. The
* accepted values 0/1 would proceed into native and can't be unit-tested here.
*/
class DocumentTransactionsVersionValidationTest {

private val id32 = ByteArray(32)
private val payload = ByteArray(4) { it.toByte() }

private fun createWithVersion(version: Int) = runBlocking {
DocumentTransactions().createEncryptedDocument(
walletHandle = 0L,
mnemonicResolverHandle = 0L,
ownerId = id32,
contractId = id32,
documentType = "txMetadata",
encryptionKeyIndex = 0,
version = version,
payload = payload,
signerHandle = 0L,
)
}

/** Bytes 2..255 (previously accepted by the `0..255` range) are now rejected. */
@Test
fun rejectsVersionBytesTheLegacyStackCannotDecode() {
for (version in intArrayOf(2, 3, 127, 255)) {
val e = assertThrows(
"version=$version must be rejected",
IllegalArgumentException::class.java,
) { createWithVersion(version) }
assertTrue(
"message should name the wire-meaningful versions, got: ${e.message}",
e.message!!.contains("0 (CBOR) or 1 (protobuf)"),
)
}
}

/** A negative version byte is likewise rejected. */
@Test
fun rejectsNegativeVersion() {
assertThrows(IllegalArgumentException::class.java) { createWithVersion(-1) }
}
}
4 changes: 4 additions & 0 deletions packages/rs-platform-wallet-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ anyhow = { version = "1.0.81" }
# Swift decodes via Codable. See `tokens/group_queries.rs`.
serde_json = "1.0"
bs58 = "0.5"
# Base64-encode the decrypted (opaque) txMetadata payload in the fetch JSON,
# matching the codebase's binary-in-JSON convention. See `document.rs`
# `platform_wallet_fetch_encrypted_documents`.
base64 = "0.22.1"

# Zeroize intermediate key material crossing the FFI boundary.
zeroize = { version = "1", features = ["derive"] }
Expand Down
Loading
Loading