Goal
Stop using the legacy platform SDK (org.dashj.platform:dash-sdk-{java,kotlin,android}) as a network client. Route every Platform query and write through the Kotlin SDK, so org.dashj.platform.sdk.platform.Platform is never constructed and its native library is never loaded — then drop the dash-sdk-android dependency.
This is a different dependency from dashj (org.bitcoinj / org.dashj:dashj-core) and a separate track from Phases 1–3. It can run in parallel with Phase 2; only one item is shared (see Prerequisites).
Scope is deliberately the network client only. The legacy dpp types (Identifier, Document, the username-status enums, TxMetadataItem) stay on the classpath and are retired in Platform SDK B.
Why this cut is clean
Three facts, verified against the 4.0.0 artifacts and feat/kotlin-sdk-phase1 @ 34e520e70:
- The native payload is in exactly one artifact.
dash-sdk-android-4.0.0.aar is 172 MB and contains nothing but libsdklib.so — its classes.jar is 22 bytes. With abiFilters 'arm64-v8a', 'x86_64' (wallet/build.gradle:299-305), the shipped cost is 43.7 MB on arm64 (47.6 MB x86_64), carried alongside the Kotlin SDK's own 64.8 MB libdash_sdk_jni.so. The app currently ships two Rust platform SDKs.
- Exactly one class loads it. The only
System.loadLibrary in the entire legacy stack is in org.dashj.platform.sdk.platform.Platform. Nothing else — not the SWIG wrappers in dash-sdk-java, not dpp, not dashpay. If Platform is never constructed, the library is never loaded.
- The types left behind are pure JVM.
org.dashj.platform.dashpay.UsernameStatus has zero references into the JNI layer. dpp.Identifier and dpp.Document touch it in exactly two members each — a bridging constructor and toNative() — and the wallet calls neither (no toNative() anywhere in wallet/src). So dash-sdk-java + dash-sdk-kotlin can stay as compile-time-only jars with no .so.
Current state by user-facing function
Audited on feat/kotlin-sdk-phase1 @ 34e520e70. Every USE_KOTLIN_SDK_* flag is seeded ON for all builds including prodRelease (ui/dashpay/utils/DashPayConfig.kt:567-575, per the 2026-07-30 directive that QA, mainnet and release must not diverge), so where a flag exists the SDK is the default and legacy is the fallback. Several in-code comments still read "default off" — they are stale.
| Function |
Today |
Gate |
| Contact list — display |
neither; pure Room read (service/platform/IdentityRepository.kt:684) over dashpay_contact_request + dashpay_profile |
— |
| Contact list — contact requests fill |
legacy platform.contactRequests.get (PlatformSyncService.kt:954, :981) |
none; no SDK path wired |
| Contact list — profile / username fill |
SDK, legacy fallback (PlatformSyncService.kt:1400, :1409) |
DPNS_READS |
| Send contact request |
SDK, legacy fallback (PlatformBroadcastService.kt:162 → SdkDashPayWrites:515) |
DASHPAY_WRITES |
| Get other users' profiles |
SDK, legacy fallback (SdkProfileQueries) |
DPNS_READS |
| Edit / save profile |
SDK write (PlatformBroadcastService.kt:442 → SdkDashPayWrites:589); read-back still legacy platform.profiles.get (:456) |
DASHPAY_WRITES |
| Search usernames |
SDK for the name lookup only (IdentityRepository.kt:543); profiles, contacts and contested filtering stay legacy |
DPNS_READS |
| Publish txMetadata |
legacy only (PlatformSyncService.kt:1941 publish, :1599 fetch) |
none; no SDK route exists |
| Buy Credits |
SDK only, no fallback (PerformTopUpWorker.kt:103 → SdkTransparentTopUp) |
none |
| Username registration — shielded funding |
SDK (RequestUserNameViewModel.kt:1092) |
SHIELDED |
| Username registration — Dash funding, post-cutover |
SDK (RequestUserNameViewModel.kt:1139) |
cutover committed |
| Username registration — pre-cutover, invite, or reuse-tx |
legacy three-step (PlatformRepo.kt:358, :386, :396) |
— |
| Username voting — proTxHash discovery |
dashj first, SDK fallback (PlatformBroadcastService.kt:367-377) |
none |
| Username voting — identity lookup + broadcast |
legacy (:384, :389) |
none |
| Username voting — contender / vote-poll reads |
SDK (SdkVotingQueries) |
DPNS_READS |
Note the write paths do not mirror the reads: on SdkWriteResult.Ambiguous the DashPay writes throw rather than retry on dashj (PlatformBroadcastService.kt:465-470), because a possibly-broadcast write must not be duplicated. Only a definitive not-broadcast falls back.
Two consequences for this issue's scope. Contact requests are the largest legacy read with no SDK path wired at all — Dashpay.syncContactRequests() / contacts() exist in the SDK with zero call sites in wallet/src, so that is greenfield work rather than a repoint. And the legacy client is still reached from inside SDK paths — the profile write reads back through platform.profiles.get, and three already-migrated files resolve contract ids through platform.apps (below).
Scope: 26 files (22 in wallet/src, 4 in wallet/test)
Out of 79 files that reference org.dashj.platform today. The rest are type-only and belong to Platform SDK B.
Reads — Platform queries
| Legacy call site |
SDK route |
State |
names.get — ui/dashpay/GetUsernameDocumentTask.kt:31,33, ui/dashpay/PlatformRepo.kt:196, service/platform/IdentityRepository.kt:546 |
SdkUsernameQueries (parity documented at :264-265) |
adapter built |
names.search — service/platform/IdentityRepository.kt:544 |
SdkUsernameQueries |
adapter built |
names.getByOwnerId — ui/dashpay/PlatformRepo.kt:451 |
SdkUsernameQueries |
adapter built |
names.getList — service/platform/PlatformSyncService.kt:1408 |
SdkUsernameQueries |
adapter built |
names.getVoteContenders — ui/dashpay/PlatformRepo.kt:224 |
SdkVotingQueries (parity at :259) |
adapter built |
names.getAllContestedNames — ui/dashpay/CreateIdentityService.kt:1715 |
IdentityRegistration.contestedDpnsNames |
available |
names.getCurrentVotePolls — service/platform/PlatformSyncService.kt:2264 |
Voting.votePollsByEndDate / contestedResources |
available |
ContactRequests / Profiles — service/platform/PlatformService.kt:31-32 |
tokens.Dashpay.* |
SDK surface exists but has zero wallet call sites — greenfield, not a repoint |
IdentityVerify get — service/platform/PlatformService.kt:42 |
SdkIdentityVerifyQueries |
adapter built |
credit balance — BlockchainIdentity.creditBalance |
Identities.fetchBalance |
available |
DapiClient — service/platform/PlatformService.kt:153, service/platform/PlatformSyncService.kt:1248 |
Sdk + Sdk.discoverActiveMasternodes |
needs rework |
Writes — Platform state transitions
| Legacy call site |
SDK route |
State |
identity / DPNS registration — ui/dashpay/CreateIdentityService.kt, BlockchainIdentity.register* |
IdentityRegistration.registerWithWalletFunding, registerDpnsName |
available |
profile create/update — BlockchainIdentity.registerProfile / updateProfile |
Dashpay.createOrUpdateProfile |
available |
contact requests — service/platform/PlatformBroadcastService.kt |
Dashpay.sendContactRequest / acceptContactRequest |
available |
invitations — service/platform/TopUpRepository.kt, ui/invite/* |
Dashpay.createInvitation, IdentityRegistration.claimInvitation |
available (note: createInvitation sits on Dashpay in v42int4, not IdentityRegistration) |
identity-verify document — service/platform/PlatformBroadcastService.kt |
SdkIdentityVerifyWrites |
adapter built |
username vote — service/platform/PlatformBroadcastService.kt:389 (names.broadcastVote) |
VoteCasting.castVote |
available; voting-key lookup already SDK-backed via a shim (see 2) |
txMetadata publish / fetch — service/platform/PlatformSyncService.kt:1941 / :1599 |
— |
blocked, see 1 below |
Already-migrated files that still reach back through Platform
These are on the SDK but resolve their contract id via the legacy client, so they break the moment Platform goes:
service/platform/sdk/SdkProfileQueries.kt:293,296 — platformService.platform.apps["dashpay"]?.contractId
service/platform/sdk/SdkIdentityVerifyQueries.kt:202 — same shape
service/platform/PlatformSyncService.kt:308 — platform.apps["dashpay"]?.contract != null as the contract-loaded gate
Contracts.loadKnownContracts exists on v42int4, so this is closeable now — it does not wait on anything upstream.
Footgun while doing it: packages/wallet-utils-contract/src/v1/mod.rs declares NAME: &str = "tx_metadata", but the schema JSON key — and the actual document type on the wire — is txMetadata. The constant is unused anywhere in the platform repo, so nothing is broken today, but a caller trusting it would pass the wrong type name.
Blockers and SDK-side asks
1. txMetadata has no SDK route on the pinned build — gate
DocumentTransactions on 0.1.0-v42int4 exposes only purchase, setPrice, create, replace, delete, transfer. There is no createEncryptedDocument and no fetchEncryptedDocuments. This is already recorded in-tree at service/platform/sdk/SdkTxMetadataDecryptProbe.kt:116-125:
a qa5-only SDK surface that never merged upstream (the v4.2-dev DocumentTransactions exposes only purchase/create/replace/…)
So on the pinned SDK, txMetadata cannot move off BlockchainIdentity at all — which means Platform stays constructed, which defeats this issue.
Upstream history — seven attempts, one landed, reverted in 14 hours
The live PR is not close. #4243 was created 2026-07-28 and last touched 2026-08-05 — 12 days stale as of writing. mergeable=CONFLICTING, reviewDecision=REVIEW_REQUIRED, targeting v4.2-dev from fix/txmetadata-decrypt-plaintext-lifetime. It has no human review comments, only automated ones, and needs a rebase before it can be evaluated.
Neither #4279 nor #4264 records a human reason for the revert/closure — worth asking, since there is no public account of why this surface keeps failing to land.
Verified against v4.2-dev @ 6495991c5e (well past where v42int4 was cut): no encrypted-txMetadata implementation in rs-platform-wallet, rs-platform-wallet-ffi, or kotlin-sdk. The only trace is a leftover comment at packages/rs-platform-wallet/Cargo.toml:64 referencing "the encrypted-txMetadata change". (wallet-lib matches are the JS library's unrelated txMetadata concept.)
So this should not be planned as an imminent unblock.
Options
- Wait on #4243. Indefinite — nothing currently indicates a date, and the PR needs a rebase and a review before it could land.
- Carve txMetadata out of this issue. Does not work as a partial: if txMetadata stays on
BlockchainIdentity, Platform stays constructed, libsdklib.so does not drop, and the issue's acceptance criteria cannot be met. Choosing this means rescoping or closing the issue, not shrinking it.
- Fold the qa5 txMetadata commits onto our own v42 integration build. The wallet already consumes locally published
org.dashj:dash-sdk-android snapshots, so this needs no upstream cooperation and is the only option with a timeline we control. Cost: we own and maintain the port until upstream lands, and the pin leaves the non-snapshot v42int4 line.
This is a decision for the team — it is not resolvable inside the wallet code.
Wallet-side work, if the surface lands
Independent of which option is taken, the port itself requires:
- Batching / document splitting. The legacy
BlockchainIdentity.createTxMetadata takes an unbounded List<TxMetadataItem> and splits it internally, filling to TxMetadataDocument.MAX_ENCRYPTED_SIZE (4064). The SDK is one payload → one document with a 4063-byte plaintext ceiling. That loop moves into the app, retargeted 4064 → 4063.
- Progress reporting. Legacy
publishTxMetaData fires a per-document listener that drives publishChangeCache → PublishTransactionMetadataWorker → WorkManager setProgress → the user-visible sync notification. The SDK has no progress hook, so this plumbing is rewritten rather than re-pointed.
- Version dispatch. Legacy
decryptTxMetadata switches on the version byte and CBOR-decodes version 0. The production fetch path must dispatch on version rather than assuming protobuf, or metadata from very old installs is silently dropped.
encryptionKeyIndex allocation. The wallet's counter (1 + transactionMetadataDocumentDao.countAllRequests()) advances by item count; the Rust allocator in #4243 advances by document count. The two series will not line up. Not a correctness problem — each document stores its own index and every ciphertext carries a fresh IV — but confirm no consumer treats the index as a sequence number before switching.
$updatedAt nullability. The SDK returns updatedAt as number|null; the current sweep resolves fields against a non-null assertion and would crash rather than skip.
- Typed return → JSON string. Legacy
getTxMetaData returns Map<TxMetadataDocument, List<TxMetadataItem>>, fully parsed; the SDK returns a JSON array with payload as base64 of the decrypted plaintext, which the app parses itself. Note the decrypted financial plaintext (memos, gift-card numbers, tax categories) lands in a JVM String that cannot be scrubbed — parse promptly, never log it, do not retain it.
identity_index must be 0. The SDK's wire compatibility with existing documents holds only at identity_index == 0; the legacy stack has no identity-index component. A restored wallet whose primary identity lands at a nonzero slot would stop decrypting previously written metadata silently. Assert it, or surface a hard error.
The TxMetadataItem model and its protobuf schema are not part of this issue — they are pure JVM, they survive the client cutover, and they belong to Platform SDK B.
2. masternodesByVotingKey reachable only through a reflection shim — cleanup ask, NOT a blocker
(Corrected — an earlier revision of this issue listed this as a blocker. It is not: the wallet already reaches the function.)
names.broadcastVote (service/platform/PlatformBroadcastService.kt:389) needs voting-key resolution, and it already works post-cutover. PlatformBroadcastService.kt:367-377 tries the dashj masternode list first and falls back to SdkMasternodeQueries.proTxHashesByVotingKey, which reaches the SDK at SdkMasternodeQueries.kt:75.
It gets there through service/platform/sdk/MasternodeVotingNative.java, a deliberate Java shim that does two things Kotlin cannot:
- links directly against
WalletManagerNative.INSTANCE.masternodesByVotingKey(...) — legal at the JVM level because Kotlin internal is metadata-only and javac links against bytecode (the member itself is public inside the internal object, so there is no $sdk_release mangling)
- reflects the private
PlatformWalletManager.managerHandle field (:73-79) — its synthetic accessor is ACC_SYNTHETIC and javac refuses to resolve it
CoreSendAllNative uses the same pattern, so this is an established workaround here rather than a one-off.
The ask is cleanup, not capability. A public wrapper (e.g. on PlatformWalletManager) would let both shims be deleted. Worth requesting because the reflection is a runtime hazard on any AAR layout change — the direct linkage fails loudly at compile time, but the reflected field does not — yet nothing is blocked on it today.
Shared with Phase 2 (#1521) item 3, which was corrected the same way.
3. DPNS label helpers — SDK-side ask (small)
Names.normalizeString — 46 call sites. normalizeDpnsLabel exists in v42int4 on PlatformWalletPersistenceHandlerKt as a JVM-public static, but is Kotlin-internal, so wallet Kotlin cannot call it. A visibility change upstream, or a small Java shim here.
Names.isUsernameContestable — 22 call sites. Does not exist anywhere in the SDK on any branch reviewed. Either add it upstream or port it into the wallet.
Both are pure functions on Names$Companion with no JNI dependency, so they do not block this issue — the calls can keep resolving against the legacy jar while the client is retired. Flagged here because they are the first thing Platform SDK B needs.
4. Consensus-error taxonomy disappears silently — behavioral risk
ui/dashpay/CreateIdentityService.kt branches on IdentityAssetLockTransactionOutPointAlreadyExistsException, InvalidInstantAssetLockProofException, InvalidInstantAssetLockProofSignatureException around registration. Those classes are pure dpp, so they keep compiling — but once the legacy client is gone nothing throws them and the retry / user-messaging logic quietly stops firing.
v42int4's DashSdkError has no consensus codes. PlatformWallet.InsufficientIdentityCredits plausibly replaces BalanceIsNotEnoughException, and PlatformWallet.AssetLockAlreadyConsumed may fire where Drive returns outpoint-already-exists, but that is an assumption about the Rust-side mapping, not something the Kotlin surface states.
To do: confirm the mapping with the SDK team, then rewrite those branches. This will not surface as a compile error — it must be handled deliberately.
5. names.deserialize has no counterpart (and needs none)
7 sites: ui/dashpay/CreateIdentityService.kt:1310,1331,1791,1833, service/platform/PlatformSyncService.kt:2288,2379, ui/username/request/RequestUserNameViewModel.kt:1244. The SDK returns JSON, so there is nothing to deserialize — but these callers currently receive a Document and wrap it in DomainDocument, so they need re-typing here. This is the one place Platform SDK B's model work is pulled forward.
Also worth changing while here
service/platform/sdk/SdkVotingQueries.kt and SdkIdentityVerifyWrites.kt were written to return legacy dpp types (Contenders, Document, DataContract) so their callers would not have to change. That was the right call at the time; it means the adapter boundary itself is re-typed in Platform SDK B, not here.
Acceptance
org.dashj.platform.sdk.platform.Platform is never constructed. Assert it, or verify by the absence of the libsdklib load in a launch log.
dash-sdk-android is removed from the flavor dependency block at wallet/build.gradle:567-574; dash-sdk-java and dash-sdk-kotlin remain as compile-time-only jars.
- No
DapiClient, no ContactRequests / Profiles / Identities / IdentityVerify legacy client, no BlockchainIdentity network calls.
- Contract ids resolve without
platform.apps.
- Identity registration error handling is verified against real SDK errors, not the dead dpp exception types.
- APK/AAB arm64 native payload drops by ~43.7 MB.
Prerequisites:
Related: Platform SDK B removes the remaining org.dashj.platform types. #1522 item 12 ("Identity-verify document signing — WalletSignerCallback") is filed under Phase 3 but WalletSignerCallback is org.dashj.platform.dashpay.callback.WalletSignerCallback — a legacy platform SDK class. It is retired here, not in Phase 3, and should be moved.
Goal
Stop using the legacy platform SDK (
org.dashj.platform:dash-sdk-{java,kotlin,android}) as a network client. Route every Platform query and write through the Kotlin SDK, soorg.dashj.platform.sdk.platform.Platformis never constructed and its native library is never loaded — then drop thedash-sdk-androiddependency.This is a different dependency from dashj (
org.bitcoinj/org.dashj:dashj-core) and a separate track from Phases 1–3. It can run in parallel with Phase 2; only one item is shared (see Prerequisites).Scope is deliberately the network client only. The legacy dpp types (
Identifier,Document, the username-status enums,TxMetadataItem) stay on the classpath and are retired in Platform SDK B.Why this cut is clean
Three facts, verified against the 4.0.0 artifacts and
feat/kotlin-sdk-phase1@34e520e70:dash-sdk-android-4.0.0.aaris 172 MB and contains nothing butlibsdklib.so— itsclasses.jaris 22 bytes. WithabiFilters 'arm64-v8a', 'x86_64'(wallet/build.gradle:299-305), the shipped cost is 43.7 MB on arm64 (47.6 MB x86_64), carried alongside the Kotlin SDK's own 64.8 MBlibdash_sdk_jni.so. The app currently ships two Rust platform SDKs.System.loadLibraryin the entire legacy stack is inorg.dashj.platform.sdk.platform.Platform. Nothing else — not the SWIG wrappers indash-sdk-java, notdpp, notdashpay. IfPlatformis never constructed, the library is never loaded.org.dashj.platform.dashpay.UsernameStatushas zero references into the JNI layer.dpp.Identifieranddpp.Documenttouch it in exactly two members each — a bridging constructor andtoNative()— and the wallet calls neither (notoNative()anywhere inwallet/src). Sodash-sdk-java+dash-sdk-kotlincan stay as compile-time-only jars with no.so.Current state by user-facing function
Audited on
feat/kotlin-sdk-phase1@34e520e70. EveryUSE_KOTLIN_SDK_*flag is seeded ON for all builds including prodRelease (ui/dashpay/utils/DashPayConfig.kt:567-575, per the 2026-07-30 directive that QA, mainnet and release must not diverge), so where a flag exists the SDK is the default and legacy is the fallback. Several in-code comments still read "default off" — they are stale.service/platform/IdentityRepository.kt:684) overdashpay_contact_request+dashpay_profileplatform.contactRequests.get(PlatformSyncService.kt:954,:981)PlatformSyncService.kt:1400,:1409)DPNS_READSPlatformBroadcastService.kt:162→SdkDashPayWrites:515)DASHPAY_WRITESSdkProfileQueries)DPNS_READSPlatformBroadcastService.kt:442→SdkDashPayWrites:589); read-back still legacyplatform.profiles.get(:456)DASHPAY_WRITESIdentityRepository.kt:543); profiles, contacts and contested filtering stay legacyDPNS_READSPlatformSyncService.kt:1941publish,:1599fetch)PerformTopUpWorker.kt:103→SdkTransparentTopUp)RequestUserNameViewModel.kt:1092)SHIELDEDRequestUserNameViewModel.kt:1139)PlatformRepo.kt:358,:386,:396)PlatformBroadcastService.kt:367-377):384,:389)SdkVotingQueries)DPNS_READSNote the write paths do not mirror the reads: on
SdkWriteResult.Ambiguousthe DashPay writes throw rather than retry on dashj (PlatformBroadcastService.kt:465-470), because a possibly-broadcast write must not be duplicated. Only a definitive not-broadcast falls back.Two consequences for this issue's scope. Contact requests are the largest legacy read with no SDK path wired at all —
Dashpay.syncContactRequests()/contacts()exist in the SDK with zero call sites inwallet/src, so that is greenfield work rather than a repoint. And the legacy client is still reached from inside SDK paths — the profile write reads back throughplatform.profiles.get, and three already-migrated files resolve contract ids throughplatform.apps(below).Scope: 26 files (22 in
wallet/src, 4 inwallet/test)Out of 79 files that reference
org.dashj.platformtoday. The rest are type-only and belong to Platform SDK B.Reads — Platform queries
names.get—ui/dashpay/GetUsernameDocumentTask.kt:31,33,ui/dashpay/PlatformRepo.kt:196,service/platform/IdentityRepository.kt:546SdkUsernameQueries(parity documented at:264-265)names.search—service/platform/IdentityRepository.kt:544SdkUsernameQueriesnames.getByOwnerId—ui/dashpay/PlatformRepo.kt:451SdkUsernameQueriesnames.getList—service/platform/PlatformSyncService.kt:1408SdkUsernameQueriesnames.getVoteContenders—ui/dashpay/PlatformRepo.kt:224SdkVotingQueries(parity at:259)names.getAllContestedNames—ui/dashpay/CreateIdentityService.kt:1715IdentityRegistration.contestedDpnsNamesnames.getCurrentVotePolls—service/platform/PlatformSyncService.kt:2264Voting.votePollsByEndDate/contestedResourcesContactRequests/Profiles—service/platform/PlatformService.kt:31-32tokens.Dashpay.*IdentityVerifyget —service/platform/PlatformService.kt:42SdkIdentityVerifyQueriesBlockchainIdentity.creditBalanceIdentities.fetchBalanceDapiClient—service/platform/PlatformService.kt:153,service/platform/PlatformSyncService.kt:1248Sdk+Sdk.discoverActiveMasternodesWrites — Platform state transitions
ui/dashpay/CreateIdentityService.kt,BlockchainIdentity.register*IdentityRegistration.registerWithWalletFunding,registerDpnsNameBlockchainIdentity.registerProfile/updateProfileDashpay.createOrUpdateProfileservice/platform/PlatformBroadcastService.ktDashpay.sendContactRequest/acceptContactRequestservice/platform/TopUpRepository.kt,ui/invite/*Dashpay.createInvitation,IdentityRegistration.claimInvitationcreateInvitationsits onDashpayin v42int4, notIdentityRegistration)service/platform/PlatformBroadcastService.ktSdkIdentityVerifyWritesservice/platform/PlatformBroadcastService.kt:389(names.broadcastVote)VoteCasting.castVoteservice/platform/PlatformSyncService.kt:1941/:1599Already-migrated files that still reach back through
PlatformThese are on the SDK but resolve their contract id via the legacy client, so they break the moment
Platformgoes:service/platform/sdk/SdkProfileQueries.kt:293,296—platformService.platform.apps["dashpay"]?.contractIdservice/platform/sdk/SdkIdentityVerifyQueries.kt:202— same shapeservice/platform/PlatformSyncService.kt:308—platform.apps["dashpay"]?.contract != nullas the contract-loaded gateContracts.loadKnownContractsexists on v42int4, so this is closeable now — it does not wait on anything upstream.Footgun while doing it:
packages/wallet-utils-contract/src/v1/mod.rsdeclaresNAME: &str = "tx_metadata", but the schema JSON key — and the actual document type on the wire — istxMetadata. The constant is unused anywhere in the platform repo, so nothing is broken today, but a caller trusting it would pass the wrong type name.Blockers and SDK-side asks
1. txMetadata has no SDK route on the pinned build — gate
DocumentTransactionson0.1.0-v42int4exposes onlypurchase,setPrice,create,replace,delete,transfer. There is nocreateEncryptedDocumentand nofetchEncryptedDocuments. This is already recorded in-tree atservice/platform/sdk/SdkTxMetadataDecryptProbe.kt:116-125:So on the pinned SDK, txMetadata cannot move off
BlockchainIdentityat all — which meansPlatformstays constructed, which defeats this issue.Upstream history — seven attempts, one landed, reverted in 14 hours
v41int9pin carried[SUPERSEDED by #4277]The live PR is not close. #4243 was created 2026-07-28 and last touched 2026-08-05 — 12 days stale as of writing.
mergeable=CONFLICTING,reviewDecision=REVIEW_REQUIRED, targetingv4.2-devfromfix/txmetadata-decrypt-plaintext-lifetime. It has no human review comments, only automated ones, and needs a rebase before it can be evaluated.Neither #4279 nor #4264 records a human reason for the revert/closure — worth asking, since there is no public account of why this surface keeps failing to land.
Verified against
v4.2-dev@6495991c5e(well past wherev42int4was cut): no encrypted-txMetadata implementation inrs-platform-wallet,rs-platform-wallet-ffi, orkotlin-sdk. The only trace is a leftover comment atpackages/rs-platform-wallet/Cargo.toml:64referencing "the encrypted-txMetadata change". (wallet-libmatches are the JS library's unrelatedtxMetadataconcept.)So this should not be planned as an imminent unblock.
Options
BlockchainIdentity,Platformstays constructed,libsdklib.sodoes not drop, and the issue's acceptance criteria cannot be met. Choosing this means rescoping or closing the issue, not shrinking it.org.dashj:dash-sdk-androidsnapshots, so this needs no upstream cooperation and is the only option with a timeline we control. Cost: we own and maintain the port until upstream lands, and the pin leaves the non-snapshotv42int4line.This is a decision for the team — it is not resolvable inside the wallet code.
Wallet-side work, if the surface lands
Independent of which option is taken, the port itself requires:
BlockchainIdentity.createTxMetadatatakes an unboundedList<TxMetadataItem>and splits it internally, filling toTxMetadataDocument.MAX_ENCRYPTED_SIZE(4064). The SDK is one payload → one document with a 4063-byte plaintext ceiling. That loop moves into the app, retargeted 4064 → 4063.publishTxMetaDatafires a per-document listener that drivespublishChangeCache→PublishTransactionMetadataWorker→ WorkManagersetProgress→ the user-visible sync notification. The SDK has no progress hook, so this plumbing is rewritten rather than re-pointed.decryptTxMetadataswitches on the version byte and CBOR-decodes version 0. The production fetch path must dispatch onversionrather than assuming protobuf, or metadata from very old installs is silently dropped.encryptionKeyIndexallocation. The wallet's counter (1 + transactionMetadataDocumentDao.countAllRequests()) advances by item count; the Rust allocator in #4243 advances by document count. The two series will not line up. Not a correctness problem — each document stores its own index and every ciphertext carries a fresh IV — but confirm no consumer treats the index as a sequence number before switching.$updatedAtnullability. The SDK returnsupdatedAtasnumber|null; the current sweep resolves fields against a non-null assertion and would crash rather than skip.getTxMetaDatareturnsMap<TxMetadataDocument, List<TxMetadataItem>>, fully parsed; the SDK returns a JSON array withpayloadas base64 of the decrypted plaintext, which the app parses itself. Note the decrypted financial plaintext (memos, gift-card numbers, tax categories) lands in a JVMStringthat cannot be scrubbed — parse promptly, never log it, do not retain it.identity_indexmust be 0. The SDK's wire compatibility with existing documents holds only atidentity_index == 0; the legacy stack has no identity-index component. A restored wallet whose primary identity lands at a nonzero slot would stop decrypting previously written metadata silently. Assert it, or surface a hard error.The
TxMetadataItemmodel and its protobuf schema are not part of this issue — they are pure JVM, they survive the client cutover, and they belong to Platform SDK B.2.
masternodesByVotingKeyreachable only through a reflection shim — cleanup ask, NOT a blocker(Corrected — an earlier revision of this issue listed this as a blocker. It is not: the wallet already reaches the function.)
names.broadcastVote(service/platform/PlatformBroadcastService.kt:389) needs voting-key resolution, and it already works post-cutover.PlatformBroadcastService.kt:367-377tries the dashj masternode list first and falls back toSdkMasternodeQueries.proTxHashesByVotingKey, which reaches the SDK atSdkMasternodeQueries.kt:75.It gets there through
service/platform/sdk/MasternodeVotingNative.java, a deliberate Java shim that does two things Kotlin cannot:WalletManagerNative.INSTANCE.masternodesByVotingKey(...)— legal at the JVM level because Kotlininternalis metadata-only and javac links against bytecode (the member itself is public inside the internal object, so there is no$sdk_releasemangling)PlatformWalletManager.managerHandlefield (:73-79) — its synthetic accessor isACC_SYNTHETICand javac refuses to resolve itCoreSendAllNativeuses the same pattern, so this is an established workaround here rather than a one-off.The ask is cleanup, not capability. A public wrapper (e.g. on
PlatformWalletManager) would let both shims be deleted. Worth requesting because the reflection is a runtime hazard on any AAR layout change — the direct linkage fails loudly at compile time, but the reflected field does not — yet nothing is blocked on it today.Shared with Phase 2 (#1521) item 3, which was corrected the same way.
3. DPNS label helpers — SDK-side ask (small)
Names.normalizeString— 46 call sites.normalizeDpnsLabelexists in v42int4 onPlatformWalletPersistenceHandlerKtas a JVM-public static, but is Kotlin-internal, so wallet Kotlin cannot call it. A visibility change upstream, or a small Java shim here.Names.isUsernameContestable— 22 call sites. Does not exist anywhere in the SDK on any branch reviewed. Either add it upstream or port it into the wallet.Both are pure functions on
Names$Companionwith no JNI dependency, so they do not block this issue — the calls can keep resolving against the legacy jar while the client is retired. Flagged here because they are the first thing Platform SDK B needs.4. Consensus-error taxonomy disappears silently — behavioral risk
ui/dashpay/CreateIdentityService.ktbranches onIdentityAssetLockTransactionOutPointAlreadyExistsException,InvalidInstantAssetLockProofException,InvalidInstantAssetLockProofSignatureExceptionaround registration. Those classes are pure dpp, so they keep compiling — but once the legacy client is gone nothing throws them and the retry / user-messaging logic quietly stops firing.v42int4's
DashSdkErrorhas no consensus codes.PlatformWallet.InsufficientIdentityCreditsplausibly replacesBalanceIsNotEnoughException, andPlatformWallet.AssetLockAlreadyConsumedmay fire where Drive returns outpoint-already-exists, but that is an assumption about the Rust-side mapping, not something the Kotlin surface states.To do: confirm the mapping with the SDK team, then rewrite those branches. This will not surface as a compile error — it must be handled deliberately.
5.
names.deserializehas no counterpart (and needs none)7 sites:
ui/dashpay/CreateIdentityService.kt:1310,1331,1791,1833,service/platform/PlatformSyncService.kt:2288,2379,ui/username/request/RequestUserNameViewModel.kt:1244. The SDK returns JSON, so there is nothing to deserialize — but these callers currently receive aDocumentand wrap it inDomainDocument, so they need re-typing here. This is the one place Platform SDK B's model work is pulled forward.Also worth changing while here
service/platform/sdk/SdkVotingQueries.ktandSdkIdentityVerifyWrites.ktwere written to return legacy dpp types (Contenders,Document,DataContract) so their callers would not have to change. That was the right call at the time; it means the adapter boundary itself is re-typed in Platform SDK B, not here.Acceptance
org.dashj.platform.sdk.platform.Platformis never constructed. Assert it, or verify by the absence of thelibsdklibload in a launch log.dash-sdk-androidis removed from the flavor dependency block atwallet/build.gradle:567-574;dash-sdk-javaanddash-sdk-kotlinremain as compile-time-only jars.DapiClient, noContactRequests/Profiles/Identities/IdentityVerifylegacy client, noBlockchainIdentitynetwork calls.platform.apps.Prerequisites:
masternodesByVotingKeypublic wrapper (blocker 2), shared with SDK cutover — Phase 2: retire dashj as the consensus / network engine #1521 item 3.Related: Platform SDK B removes the remaining
org.dashj.platformtypes. #1522 item 12 ("Identity-verify document signing —WalletSignerCallback") is filed under Phase 3 butWalletSignerCallbackisorg.dashj.platform.dashpay.callback.WalletSignerCallback— a legacy platform SDK class. It is retired here, not in Phase 3, and should be moved.