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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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