Skip to content

feat(sdk): add the DashPay migration to the Kotlin SDK and KotlinExampleApp#4036

Merged
shumkov merged 15 commits into
feat/kotlin-sdk-and-example-appfrom
feat/kotlin-sdk-dashpay-migration
Jul 7, 2026
Merged

feat(sdk): add the DashPay migration to the Kotlin SDK and KotlinExampleApp#4036
shumkov merged 15 commits into
feat/kotlin-sdk-and-example-appfrom
feat/kotlin-sdk-dashpay-migration

Conversation

@shumkov

@shumkov shumkov commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

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,
DashDatabase v3 migration with tombstone semantics, and the read bridges backing
the 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_OTHER channel, signer scrub); profile + contact-info writes with the DIP-15
ContactInfoPublishOutcome.

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.md rewritten.

Follow-up hardening (this PR):

  • Seed hygiene — the platform-address signing path now passes the mnemonic 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
    (relevant on Android's Scudo allocator).
  • Payment double-send guard — the send flow was extracted to performDashPaySend
    • a PaymentSender seam so the withContext(NonCancellable) guard (broadcast +
      durability bookkeeping stay atomic against a dispose-mid-send) has a deterministic
      regression test.
  • DataContractRef GC backstop — registers a NativeCleaner like every other
    owned 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 in
docs/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 suite
    green, including the two new red→green follow-up regression tests:
    • SignerMnemonicScrubTest — pins that the signing path zeroes the phrase buffer
      on every exit (fails 2/2 without the scrub).
    • PerformDashPaySendDoubleSendGuardTest — pins that a dispose-mid-send still fires
      the durability refresh (fails without withContext(NonCancellable)).
  • Instrumented (connectedDebugAndroidTest, API-35 emulator) from the folded PRs:
    WalletManagerRoundTripTest, DashPayUnlockAndSyncTest, AppSmokeTest.
  • Base-PR review findings (invalid Cargo --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 by
signWithMnemonicAndPathInto(ByteArray, …) (the seed-hygiene fix); DashDatabase
remains at the schema version introduced by the migration.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

shumkov and others added 11 commits July 7, 2026 08:53
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>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e400e64d-eea8-42e1-b42c-d5bbd1288d0c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/kotlin-sdk-dashpay-migration

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit a11c843)

… into feat/kotlin-sdk-dashpay-migration

# Conflicts:
#	packages/rs-unified-sdk-jni/src/persistence.rs
@shumkov shumkov changed the title feat(sdk): DashPay migration for the Kotlin SDK and KotlinExampleApp feat(sdk): add the DashPay migration to the Kotlin SDK and KotlinExampleApp Jul 7, 2026
shumkov and others added 2 commits July 7, 2026 19:02
…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

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +148 to +156
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) },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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
@shumkov shumkov merged commit 803c67c into feat/kotlin-sdk-and-example-app Jul 7, 2026
3 checks passed
@shumkov shumkov deleted the feat/kotlin-sdk-dashpay-migration branch July 7, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants