feat(sdk): add the DashPay migration to the Kotlin SDK and KotlinExampleApp#4036
Conversation
Reviewed spec (5 lenses: feasibility, scope, security, adversarial, domain-fit) for porting the PR #3841 DashPay completion to the Kotlin SDK, in three stacked milestones (K1 persistence+reads, K2 sync service+seedless unlock+writes, K3 DashPay tab UI). PARITY.md: the FriendsView row claimed parity against a Swift view that PR #3841 deleted; mark the DashPay surface as in-migration instead of counting it ported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First milestone of the DashPay Swift→Kotlin migration (docs/dashpay/KOTLIN_MIGRATION_SPEC.md). Persistence — the two missing DashPay stores, mirroring the SwiftData models added by #3841: - DashpayContactProfileEntity (cached contact profiles; persister- projected with is_present=false tombstone DELETEs so a removed on-chain profile can't leave a stale name/avatar) and DashpayPaymentEntity (payment history; pull-persisted via the new refreshDashPayPayments — the sweep reconciles in-memory only). - Room v2→v3 additive migration + exported schema 3.json. - Both bridge directions: identity-entry persist now delivers contact-profile deltas (new onPersistContactProfileDelta bridge slot), and the wallet-list restore path marshals the payments / contact_profiles arrays that were previously null-stubbed in rs-unified-sdk-jni/src/persistence.rs (staging/seal/free extended). Also corrects the stale tombstone doc comments in identity_persistence.rs that contradicted the projection code. Read bridges — new rs-unified-sdk-jni/src/dashpay.rs + DashpayNative.kt (6 exports, JSON-string convention per getDashPayProfile): managed_identity_get_dashpay_payments, platform_wallet_get_contact_profile, managed_identity_get_dashpay_profile, managed_identity_get_dashpay_sync_state, platform_wallet_search_dpns_names (wallet-scoped — the AddContactView call path), platform_wallet_manager_get_account_balances. Kotlin surface: Dashpay.{payments,syncState,getContactProfile,searchDpnsNames}, PlatformWalletManager.{accountBalances,refreshDashPayPayments}. Tests: red-tier coverage per the reviewed spec — 3 new JVM handler tests (profile delta upsert, tombstone delete, payments+profiles restore round-trip; 27/27 green), a new instrumented migration test validating MIGRATION_2_3 against the exported schema, and an instrumented DashPay persist→wipe→restore→re-read round-trip through the real JNI staging/seal/free pipeline (runs on the CI emulator; JVM tests cannot reach that code). build_android.sh --verify not run locally (no NDK on this machine) — covered by kotlin-sdk-build.yml. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three-lens review of 007af13 (JNI memory safety, persistence correctness, test rigor) — no must-fixes; folded the should-fixes: - refreshDashPayPayments now preserves createdAt and skips unchanged rows (a Room @upsert rewrites every column, so the unconditional upsert clobbered createdAt with 'now' and re-fired the payments Flow on every refresh — Swift's persistDashpayPayments deliberately change-detects; now mirrored). - Empty-txid payment rows are filtered before restore staging (the Rust restore fold inserts an "" map key rather than skipping; the build_payment_restore comment claimed otherwise — comment corrected). - Rollback test for onPersistContactProfileDelta (a delta that wrote eagerly instead of staging would have passed every existing test). - Instrumented round-trip grew a tombstone-restore leg (deleted profile row restores as an absent Rust cache entry; exercises the empty-array marshaling path) and an accountBalances smoke assertion (that FFI array marshal/free path previously had no coverage at any tier); searchDpnsNames deferral to the testnet tier is now documented in the test docstring. - Clarified the owner-missing skip comment on the delta handler (the identity upsert is staged into the same buffer first, so the lookup succeeds at replay — the skip is defensive, not a retry mechanism). JVM suite: 28/28 green. Instrumented tests compile; execution remains CI-emulator-bound (no NDK locally), as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second milestone of the DashPay Swift→Kotlin migration (docs/dashpay/KOTLIN_MIGRATION_SPEC.md §K2). Security pre-req — mnemonic out-buffer discipline (spec review must-fix): the resolver contract is now resolveMnemonicInto(byte[], byte[]): Int — Kotlin writes raw UTF-8 phrase bytes into a caller-sized buffer and zeroes every intermediate copy; the trampoline copies into Rust-owned memory and scrubs the Java buffer. No java.lang.String of the seed phrase exists anywhere on the resolver path (WalletStorage.retrieveMnemonicUtf8 + hasMnemonic existence check; KeystoreSigner capability check no longer decrypts). signWithMnemonicAndPath now scrubs its mnemonic CString on every path, including the NulError one. Bridges (13 exports in dashpay.rs/DashpayNative.kt): the 7 platform_wallet_manager_dashpay_sync_* fns, the seedless trio (verify_seed_binds_to_wallet, pending_contact_crypto_count, drain_pending_contact_crypto), profile/contactInfo writes, and dash_sdk_resolver_supports_key_type. Manager surface (PlatformWalletManager, following the codebase convention of manager-owned native loops — Swift keeps these in a manager extension too): - start/stop/isRunning/isSyncing/lastSync/setInterval/syncNow + dashPaySyncIsSyncing StateFlow via the 1 Hz change-gated poll (matching startSpvProgressPolling; poll-not-events is deliberate, it is exactly how iOS does it). - unlockWalletFromKeystore: verify → drain, auto-run per restored wallet inside loadPersistedWallets (load-bearing: the deferred contact-crypto queue is in-memory by design and self-heals only if every launch runs load → unlock → sweep). Seed-mismatch published from the verify result only (ErrorInvalidParameter scoped to that call); draining re-entrancy guard; per-wallet dashPayUnlockStatus StateFlow (draining / seedMismatch / pendingAccountBuilds). - Breadcrumb backfill deliberately NOT ported (no pre-breadcrumb Android installs exist — recorded in the spec). Dashpay surface: createOrUpdateProfile (avatar bytes hashed Rust-side) and setContactInfo returning ContactInfoPublishOutcome (the DIP-15 deferred-until-two-contacts state the UI must surface). Tests: instrumented DashPayUnlockAndSyncTest — the happy-path unlock is the end-to-end proof of the out-buffer resolver (verify derives the BIP44 xpub through the new vtable), the wrong-seed leg pins the seedMismatch contract with a foreign mnemonic fixture, and the sync-lifecycle leg pins start/stop/idempotent-start and manager-swap disposal. dashPaySyncNow and the write paths are network-bound and stay in the -Ptestnet=true tier. JVM suite 28/28 green; instrumented tests compile (CI emulator executes them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three-lens review of f0ebdfc (mnemonic security, concurrency/ lifecycle, JNI bridge correctness) — two must-fixes and the actionable should-fixes folded: - Drain use-after-free (concurrency must-fix): the background drain now creates its OWN resolver + signer, owned by the coroutine and closed in its finally (the Swift Task.detached + withExtendedLifetime shape). Borrowing the manager's shared children risked dereferencing freed vtables when close()/manager-swap raced a slow blocking drain — resolver/signer handles are raw boxes, not storage-keyed like wallet handles. - dashpay.rs read_id32 threw through DashSDKException's non-existent (String) constructor → NoSuchMethodError instead of a DashSdkError (or a silent guard-default: a malformed contactId reported the benign DEFERRED outcome). Now throw_sdk_exception + null guard, matching the sibling readers. - signer.rs seed scrubbing completed: non-secret args decode first so a sibling failure can't orphan a decoded phrase, and reserve_exact(1) before CString::new prevents the NUL-append realloc from freeing the original plaintext allocation unscrubbed. - Drain re-entrancy guard is now a single atomic false→true transition on the status flow (the separate check-then-set let two concurrent unlocks stack drains). - loadPersistedWallets publishes each wallet into [wallets] BEFORE unlocking (Swift's ordering) — the 1 Hz poll's stale-key prune could otherwise silently drop a just-published seedMismatch; the status poll now also starts on load (banner state no longer depends on the sweep service running), and stopDashPaySync leaves it running. - RESOLVE_OTHER (-3) channel added across bridge/trampoline/resolver: a Keystore/decrypt failure is no longer reported as 'no seed stored' (the iOS .other discipline); zero-capacity out buffer rejected up front (removes a 1-byte OOB NUL edge, unreachable via the real caller). - Wording/marker fixes: 'wrong seed can never sign' scoped to network-valid signatures (enforcement is on-chain key ownership, not a signer gate); the String-shaped platform-address signing path is marked as the tracked residual exposure Kotlin-side; interior-NUL clear-vs-truncate divergence documented. Gates: cargo check/clippy/fmt clean; JVM 28/28; instrumented compile green (CI emulator executes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
platform_wallet_build_auto_accept_qr and platform_wallet_send_contact_request_from_qr, first consumed by the K3 DashPay tab (profile QR display + scan-to-send in AddContact) — the last two of the migration's bridged exports. Kotlin surface: Dashpay.buildAutoAcceptQr / sendContactRequestFromQr (returns a ContactRequestRef). QR generation/parsing/proof crypto is entirely Rust-side (auto_accept.rs); the app only renders/scans the URI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce A) Restructure the KotlinExampleApp bottom tabs to make DashPay first-class, mirroring the iOS ContentView.swift RootTab (SYNC, WALLETS, IDENTITIES, DASHPAY, SETTINGS), and land the device-local contact-meta + avatar infra the DashPay screens (slices B/C) build on. UI hub itself is a placeholder here. - Coil: add io.coil-kt:coil-compose (the app's one approved new dependency) for avatar loading — libs.versions.toml + app build.gradle.kts. - Navigation: RootTab CONTRACTS -> DASHPAY (route DashPayHome, testTag rootTab.dashpay, Group icon, "DashPay" label). Contracts is demoted into Settings' Platform section (first row, testTag settings.contracts, navigating the shared ContractsHome route) exactly as iOS OptionsView hosts it. Register a placeholder DashPayTabScreen (testTag dashpay.tab.root) so navigation compiles; slice B replaces it. - Retire FriendsScreen: delete the screen, its Friends route, and repoint its only entry point (IdentityDetailScreen's DashPay row) to the DashPay tab. FriendsView.swift was deleted upstream. - DashPayContactMeta port (<- DashPayContactMeta.swift): SharedPreferences- backed DashPayContactMetaStore with the same key shape (dashpay.meta.<field>.<network>.<owner>.<contact>) and a version StateFlow for Compose invalidation, the dashPayContactDisplayName precedence helper, and a Coil-backed DashPayAvatar with an initials-circle fallback. - Lifecycle: start/stop DashPay sync in AppContainer.rebindWalletScopedServices alongside the address/shielded loops, matching iOS. - Update AppSmokeTest tab list (rootTab.contracts -> rootTab.dashpay). Build gate: :app:compileDebugKotlin + :sdk:compileDebugKotlin both pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eens (K3 slice B) Port the four interactive DashPay screens the tab is built around, plus the JSON parsers and sync helpers the whole family shares. - DashPayJson.kt: data classes + org.json parsers for DashPayProfile, DpnsSearchResult, AccountBalance and DashPayPayment (slice C consumes the payment parser) — total/lenient, per-row failures skipped. - DashPayTabScreen.kt: replaces the slice-A placeholder. Wallet-backed identity picker (in-memory, resets on network switch), received-from- contacts balance (accountBalances typeTag 12, re-read after each sweep), the seedless-unlock banner off dashPayUnlockStatus (seed-mismatch / draining / pending-account-builds → unlockWalletFromKeystore), and EntityRow sections navigating to the per-section routes. Pull-to-refresh and a toolbar refresh both run dashPaySyncNow(). - ContactsScreen.kt: established-contacts (both-direction join, hidden excluded) from Room, searchable, avatar + display-name precedence, Hidden recovery link, pull-to-refresh. - ContactRequestsScreen.kt: incoming (Accept via acceptIncomingRequest / Ignore via ignoreContactSender, per-row in-flight + inline error + optimistic-removal overlay) and outgoing pending; incoming avatars are never loaded (IP-leak avoidance). - AddContactScreen.kt: 300 ms-debounced DPNS search, paste-id (32-byte base58 gate), preview card, optional account label, collision dialog (Accept vs Continue anyway), and scan-to-send via sendContactRequestFromQr. - DashPaySyncHelpers.kt: attachOrStartSync / kickDashPaySync. - Routes.kt + AppNavHost.kt: DashPay child routes + graph wiring. - Stub screens (ContactDetail / Profile / Ignored / Hidden) with fixed signatures + routes so the hub navigates end-to-end now; K3 slice C replaces their bodies. Structural deviation from iOS: the hub navigates to per-section routes rather than embedding a Contacts/Requests segmented control, and the cross-screen optimistic-sent overlay is dropped (Add Contact is its own route). Signing uses the manager's signer + mnemonic-resolver handles directly (the retired FriendsScreen's approach). Build gate: :app:compileDebugKotlin passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… parity (K3 slice C) Fill in the four stub screens and add the payment sheet, completing the DashPay tab port; delete the stub helper and rewrite PARITY for Views/DashPay/. - ContactDetailScreen: header (cached profile + DIP-15 account label), Send Dash (opens the payment sheet, disabled on broken channel), Room-driven payment history via the durable refreshDashPayPayments → observePayments path (sorted newest-first client-side), and alias/note editors + hide toggle via setContactInfo — surfacing the DEFERRED_UNTIL_TWO_CONTACTS / SKIPPED_WATCH_ONLY publish notices. - SendDashPayPaymentSheet (new): amount (decimal DASH → duffs) + balance check, sendPayment → txid (txidDisplayHex reversed-hex, added to the meta file), then always refreshDashPayPayments (the payment-durability invariant). No memo field (matches iOS); Balance.confirmed used as spendable. Hosted as a ModalBottomSheet from ContactDetail. - DashPayProfileScreen: read-only display + DIP-15 auto-accept QR (buildAutoAcceptQr rendered with the ZXing generateQrBitmap helper), plus an inline editor calling createOrUpdateProfile (doCreate when no profile exists). - IgnoredContactsScreen / HiddenContactsScreen: observeIgnoredSenders + unignoreContactSender / client-side hidden grouping + unhide (setContactInfo displayHidden=false, preserving alias/note). - Delete DashPayStub.kt (no stub remains). - androidTest/DashPayTabUITest.kt: ports the network-free launch-and-render flows from DashPayTabUITests.swift, adapted for the hub nav-section deviation. - PARITY.md: new Views/DashPay/ section (one row per view + deviations); FriendsView row retired; totals updated (ported 87 → 97). Gates: :app:compileDebugKotlin, :app:compileDebugAndroidTestKotlin, :sdk:testDebugUnitTest all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two-lens review (Compose concurrency + Swift parity) fixes on the DashPay tab. No behavior regressions; gates green. MUST-FIX - M1 payment sheet: wrap the send + post-broadcast bookkeeping (txid, onSent → durable refreshDashPayPayments) in withContext(NonCancellable), and block interactive dismissal of the host ModalBottomSheet while a send is in flight (sheetState confirmValueChange + guarded onDismissRequest via a new onSendingChange callback). Prevents a swipe-dismiss-mid-send from losing the confirmation/refresh after the (uncancellable) broadcast → double-payment risk. SHOULD-FIX - S1: register a NativeCleaner GC backstop on ContactRequestRef / EstablishedContactRef (matching Sdk.kt) so a cancellation-dropped ref can't leak its native handle for the process lifetime. - S2: drop ContactRequestsScreen's blanket LaunchedEffect(isSyncing) overlay wipe; the rows-driven prune already scopes removal to the action's own change, so an unrelated sweep no longer flashes accepted/ignored rows back. - S3: make DashPayContactMetaStore a single shared instance on AppContainer; the five screens now read it, so a write's version bump invalidates every reader (a per-screen instance only recomposed its own creator). - S4: guard the unlock banner with a local isUnlocking (disable the button while in flight) so a double-tap can't stack concurrent unlockWalletFromKeystore. - Parity S1: Dashpay.buildAutoAcceptQr username String? → non-optional String (Swift parity; the FFI check_ptr!-rejects null so the default was a latent throw); corrected the DashpayNative.kt + rs-unified-sdk-jni JNI doc claims. NOTES folded in: N1 (unconditional composable calls in UnlockBanner), N2 (remember the fallback dashPaySyncIsSyncing flow), N6 (prune Ignored/Hidden overlays against Room like Requests), N7 (encode the profile QR off the main thread via produceState). PARITY.md N7: added dashpay.profileHeader.setup to the not-reproduced tags + noted the AlertDialog alias/note editors. Gates: cargo check -p rs-unified-sdk-jni + :app:compileDebugKotlin :app:compileDebugAndroidTestKotlin :sdk:compileDebugKotlin :sdk:testDebugUnitTest all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ard, contract-ref cleaner)
Three hardening fixes on the DashPay migration, each verified absent from the
base Kotlin SDK PR before landing here:
- Seed hygiene: the platform-address signing path now passes the mnemonic across
JNI as a scrubbable UTF-8 byte[] (signWithMnemonicAndPathInto) the caller zeroes
after use, never an un-scrubbable java.lang.String. Rust holds the phrase in a
Zeroizing buffer end-to-end (no CString laundering), so the plaintext is scrubbed
on drop and on any panic unwind, and is never orphaned by a realloc.
- Payment double-send guard: extracted the send flow to performDashPaySend + a
PaymentSender seam so the withContext(NonCancellable) guard — which keeps the
broadcast and its durability bookkeeping atomic against a dispose-mid-send — is
covered by a deterministic regression test.
- DataContractRef: registers a NativeCleaner GC backstop like every other owned
handle, so a leaked (never-closed) ref no longer leaks the native contract handle.
Tests would have caught these in CI (both go red on the unfixed code):
- SignerMnemonicScrubTest: 2/2 fail without the finally { fill(0) } scrub.
- PerformDashPaySendDoubleSendGuardTest.disposeMidSend_stillFiresOnSent: fails
(onSent count 0) without withContext(NonCancellable); happyPath is a complementary
wiring pin.
Verified: cargo check -p rs-unified-sdk-jni clean; :sdk + :app testDebugUnitTest
green. Design and the deferred item D (durable contact-crypto queue, blocked
upstream on the wallet start-state restore path) are documented under docs/dashpay/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Review complete (commit a11c843) |
… into feat/kotlin-sdk-dashpay-migration # Conflicts: # packages/rs-unified-sdk-jni/src/persistence.rs
…N + fix PARITY count The Kotlin TEST_PLAN §4.10 DashPay section was generated before the K3 DashPay-tab port and had gone stale: only DP-01..06 + DP-11, still pointing at the retired FriendsScreen/IdentityDetailScreen, DP-06 mislabelled "Reject" (now Ignore), and missing DP-07/08/09/10. Ported the full DP-01..11 from the iOS TEST_PLAN, adapted to the Android app — real ui/dashpay/*Screen.kt entry points, verified dashpay.* testTags, Kotlin bridge fns, and the K3 adaptations (nav-section rows, own AddContact route, ContactInfoPublishOutcome). Updated the category index (DP-01..11), the regression tag (+DP-10), and the Platform/Cross counts. Also corrected KOTLIN_MIGRATION_LEFTOVERS.md: PARITY is 97 ported / 2 partial / 0 deferred (neither partial is DashPay), not the stale "8 partial / 7 deferred" copied from an older PR body. Docs only — no code/behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… into feat/kotlin-sdk-dashpay-migration # Conflicts: # packages/rs-unified-sdk-jni/src/persistence.rs
There was a problem hiding this comment.
Code Review
Carried-forward prior-1 is STILL VALID at a11c843: the current Kotlin DashPay UI still renders remote avatar URLs without checking the profile's advertised avatar hash. I found no new in-scope findings in the latest 19ca1a5..a11c843 delta, and there were no concrete CodeRabbit inline findings to react to.
Source: reviewers = claude general opus (failed: extra-usage limit), codex general gpt-5.5; claude security-auditor opus (failed: extra-usage limit), codex security-auditor gpt-5.5; claude ffi-engineer opus (failed: extra-usage limit), codex ffi-engineer gpt-5.5; verifier = codex gpt-5.5; specialists = security-auditor, ffi-engineer.
Prior Findings Reconciliation:
- STILL VALID: prior-1,
DashPayContactMeta.kt:148-156, Verify DashPay avatar bytes before rendering remote images.
Carried-Forward Prior Findings:
- Verify DashPay avatar bytes before rendering remote images.
New Findings In Latest Delta:
- None.
🟡 1 suggestion(s)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayContactMeta.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayContactMeta.kt:148-156: Verify DashPay avatar bytes before rendering remote images
Carried forward from prior-1 and still valid. The native profile JSON includes `avatarHash` and `avatarFingerprint`, and Room persists those fields, but `parseDashPayProfile` drops them and `DashPayAvatar` passes the remaining `avatarUrl` directly to Coil. Because this PR adds the DashPay profile/contact/payment UI paths that render those avatars, a contact can publish a Platform-authenticated profile with one avatar hash and later replace the remote bytes at the same URL, causing the UI to display image content that no longer matches the authenticated profile metadata. Plumb `avatarHash` through the UI model and render/cache the fetched image only after its bytes match the advertised hash, falling back to initials when absent or unverifiable.
| fun DashPayAvatar(avatarUrl: String?, displayName: String, size: Dp = 40.dp) { | ||
| val url = avatarUrl?.trim()?.takeIf { it.isNotEmpty() } | ||
| if (url != null) { | ||
| SubcomposeAsyncImage( | ||
| model = url, | ||
| contentDescription = null, | ||
| contentScale = ContentScale.Crop, | ||
| modifier = Modifier.size(size).clip(CircleShape), | ||
| loading = { InitialsCircle(displayName, size) }, |
There was a problem hiding this comment.
🟡 Suggestion: Verify DashPay avatar bytes before rendering remote images
Carried forward from prior-1 and still valid. The native profile JSON includes avatarHash and avatarFingerprint, and Room persists those fields, but parseDashPayProfile drops them and DashPayAvatar passes the remaining avatarUrl directly to Coil. Because this PR adds the DashPay profile/contact/payment UI paths that render those avatars, a contact can publish a Platform-authenticated profile with one avatar hash and later replace the remote bytes at the same URL, causing the UI to display image content that no longer matches the authenticated profile metadata. Plumb avatarHash through the UI model and render/cache the fetched image only after its bytes match the advertised hash, falling back to initials when absent or unverifiable.
source: ['codex']
… into feat/kotlin-sdk-dashpay-migration # Conflicts: # packages/rs-unified-sdk-jni/src/persistence.rs
Issue being fixed or feature implemented
Completes the DashPay port to the Kotlin SDK + KotlinExampleApp, consolidating the
three stacked migration PRs (#4025 K1, #4027 K2, #4030 K3) into a single reviewable
unit stacked on the base Kotlin SDK PR (#3999), plus three follow-up hardening fixes.
Closes #4025, #4027, #4030 (folded here).
This is the Kotlin/Android counterpart of the iOS DashPay completion (#3841).
What was done?
K1 — persistence + read bridges. DashPay contact/profile/payment Room entities,
DashDatabasev3 migration with tombstone semantics, and the read bridges backingthe ContactDetail/Contacts/Requests surfaces.
K2 — sync service, seedless unlock, writes. Manager-owned 1 Hz change-gated
DashPay sync loop; seedless auto-unlock (verify → drain) run per restored wallet in
loadPersistedWallets; the mnemonic out-buffer discipline (resolveMnemonicInto,RESOLVE_OTHERchannel, signer scrub); profile + contact-info writes with the DIP-15ContactInfoPublishOutcome.K3 — the DashPay tab. All 10 Compose screens (hub, contacts, requests,
add-contact, contact detail, payment, profile, lists), DashPay promoted to a
first-class tab (Contracts demoted into Settings), DIP-15 auto-accept QR pairing,
PARITY.mdrewritten.Follow-up hardening (this PR):
scrubbable UTF-8
byte[](signWithMnemonicAndPathInto) the caller zeroes afteruse, never an un-scrubbable
java.lang.String. Rust holds the phrase in aZeroizingbuffer end-to-end (noCStringlaundering), so the plaintext isscrubbed on drop and on any panic unwind, and is never orphaned by a realloc
(relevant on Android's Scudo allocator).
performDashPaySendPaymentSenderseam so thewithContext(NonCancellable)guard (broadcast +durability bookkeeping stay atomic against a dispose-mid-send) has a deterministic
regression test.
DataContractRefGC backstop — registers aNativeCleanerlike every otherowned handle, so a leaked ref no longer leaks the native contract handle.
Known leftovers and the deferred durable-contact-crypto-queue item (item D, blocked
upstream on the wallet start-state restore path) are documented in
docs/dashpay/KOTLIN_MIGRATION_LEFTOVERS.md; the follow-ups design is indocs/dashpay/KOTLIN_MIGRATION_FOLLOWUPS_SPEC.md.How Has This Been Tested?
cargo check -p rs-unified-sdk-jni— clean (host)../gradlew :sdk:testDebugUnitTest :app:testDebugUnitTest— JVM/Robolectric suitegreen, including the two new red→green follow-up regression tests:
SignerMnemonicScrubTest— pins that the signing path zeroes the phrase bufferon every exit (fails 2/2 without the scrub).
PerformDashPaySendDoubleSendGuardTest— pins that a dispose-mid-send still firesthe durability refresh (fails without
withContext(NonCancellable)).connectedDebugAndroidTest, API-35 emulator) from the folded PRs:WalletManagerRoundTripTest,DashPayUnlockAndSyncTest,AppSmokeTest.--features, JNI local-frame leaks,negative amount/index/selector validation) were re-verified already-fixed at the
base HEAD before consolidation.
Not run (documented in KOTLIN_MIGRATION_LEFTOVERS.md): end-to-end send→accept→pay
testnet UAT; an emulator platform-address sign smoke to pin the new JNI symbol
binding.
Breaking Changes
None for the public SDK. Within the unreleased Kotlin SDK: the internal
SignerNative.signWithMnemonicAndPath(String, …)is replaced bysignWithMnemonicAndPathInto(ByteArray, …)(the seed-hygiene fix);DashDatabaseremains at the schema version introduced by the migration.
Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code