Skip to content

SDK cutover — Platform SDK A: retire the legacy platform client (stop loading libsdklib) #1540

Description

@HashEngineering

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:

  1. 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.
  2. 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.
  3. 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:162SdkDashPayWrites:515) DASHPAY_WRITES
Get other users' profiles SDK, legacy fallback (SdkProfileQueries) DPNS_READS
Edit / save profile SDK write (PlatformBroadcastService.kt:442SdkDashPayWrites: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:103SdkTransparentTopUp) 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 allDashpay.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.getui/dashpay/GetUsernameDocumentTask.kt:31,33, ui/dashpay/PlatformRepo.kt:196, service/platform/IdentityRepository.kt:546 SdkUsernameQueries (parity documented at :264-265) adapter built
names.searchservice/platform/IdentityRepository.kt:544 SdkUsernameQueries adapter built
names.getByOwnerIdui/dashpay/PlatformRepo.kt:451 SdkUsernameQueries adapter built
names.getListservice/platform/PlatformSyncService.kt:1408 SdkUsernameQueries adapter built
names.getVoteContendersui/dashpay/PlatformRepo.kt:224 SdkVotingQueries (parity at :259) adapter built
names.getAllContestedNamesui/dashpay/CreateIdentityService.kt:1715 IdentityRegistration.contestedDpnsNames available
names.getCurrentVotePollsservice/platform/PlatformSyncService.kt:2264 Voting.votePollsByEndDate / contestedResources available
ContactRequests / Profilesservice/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
DapiClientservice/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,296platformService.platform.apps["dashpay"]?.contractId
  • service/platform/sdk/SdkIdentityVerifyQueries.kt:202 — same shape
  • service/platform/PlatformSyncService.kt:308platform.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

PR Author State Size Note
dashpay/platform#4091 bfoss765 closed original, v4.1 line
dashpay/platform#4186 bfoss765 closed v4.2-dev port, host-supplied index — the shape the wallet's old v41int9 pin carried
dashpay/platform#4194 bfoss765 closed Swift wrappers
dashpay/platform#4195 bfoss765 closed Rust-allocated index; title reads [SUPERSEDED by #4277]
dashpay/platform#4264 shumkov closed 2026-08-02 +5114/−38 "add hardened encrypted txMetadata documents" — open barely a day
dashpay/platform#4277 QuantumExplorer merged 2026-08-04 +3569/−9 landed on v4.2-dev
dashpay/platform#4279 QuantumExplorer merged 2026-08-04 −3569 revert of #4277, same day
dashpay/platform#4243 shumkov OPEN +10319/−67, 39 files, 42 commits the re-land
dashpay/platform#4317 bfoss765 closed 2026-08-10 +307 Swift wrappers, retry of #4194

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

  1. Wait on #4243. Indefinite — nothing currently indicates a date, and the PR needs a rebase and a review before it could land.
  2. 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.
  3. 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 publishChangeCachePublishTransactionMetadataWorker → 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions