Repository navigation
feat(kotlin-sdk): send, receive and accept DashPay private contact requests - #5813
QuantumExplorer wants to merge 48 commits into
Conversation
…de 68 Ports what Swift's `PrivateContactRequests.swift` exposes, over the same platform-wallet-ffi functions: - `Dashpay.syncPrivateContactRequests(coreSignerHandle, maxPages)` reads the private store for every identity of the wallet and returns a `PrivateContactSyncResult` (requests ingested, pages read, entries refused, store position, complete, identities not persisted). The requests reach Room through the persister, so the JNI bridge releases their handles instead of leaking them. - `PlatformWalletManager.sendPrivateContactRequest(...)` and `acceptPrivateContactRequest(walletId, request, ...)` are shielded spends, so they sit with the other manager-handle spends and use the manager's resolver and identity signer, the signer the public send and accept are given. The JNI passes the signer as `*mut SignerHandle`, as the public `sendContactRequest` does. Built with the `shielded` feature only. - `Dashpay.incomingContactRequest(ourIdentityId, senderId)` returns the request the private accept takes (Swift `ManagedIdentity.getIncomingContactRequest(senderId:)`). - Code 68 maps to `DashSdkError.PlatformWallet.PrivateContactRequestNeedsPrivateReply`, so a host routes a refused public accept to the private accept. The JNI refuses a shielded address that is not 43 bytes before the pointer crosses, since the FFI reads exactly 43. `managed_identity_get_private_contact` is not bridged: the Swift example app has no caller for it. Tests: `DashSdkErrorTest` pins code 68; the JNI crate pins the counter layout Kotlin decodes; `FfiSmokeTest` checks the three symbols bind and reject bad input. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…uses As the Swift app does (`ContactsView.swift`): - Accepting a request in Requests, or from Add Contact's collision path, goes through `acceptIncomingContactRequest`, which falls back to the private accept (paid from the wallet's shielded balance, signed by the same identity signer) when Rust refuses the public one with `PrivateContactRequestNeedsPrivateReply`. Rust decides which; the app only reacts to the refusal. - Pull-to-refresh on Requests also reads the private store, the only way private requests reach the wallet. Add Contact has no send-privately toggle yet. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds `dashpay.private_contact_requests` (store sync, shielded send, private-reply accept, private contact read, code 68 routing) with its four FFI symbols. Both hosts are partial with restart required: the sync, send and accept need a protocol version 15 network with shielded funds and have no recorded gate, no test covers a restart, Kotlin does not bridge the private contact read, and the Kotlin app has no send-privately toggle. `PARITY_SUMMARY.md` is regenerated by the checker. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
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
|
|
✅ Final review complete — no blockers (commit 2abc9b5) · triage: critical |
…in-private-contact-requests
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Verified the findings against head 655d1e3. The acceptance fallback introduces wallet orchestration in the Kotlin example app contrary to the repository's ownership rules; native-handle cleanup also lacks nonempty regression coverage. This was a static review with no local builds or tests; current CI reports the Kotlin SDK build and emulator tests passing, with only the automated review pending.
🔴 1 blocking | 🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
normalbygpt-6.1-sol(effort low) — The change spans JNI, Kotlin wrappers, and example-app flows, but delegates private sends, signing, and acceptance to existing platform-wallet implementations rather than changing intricate funds-movement or cryptographic logic. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort high); agentphase2-reviewer
🤖 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/DashPaySyncHelpers.kt`:
- [BLOCKING] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPaySyncHelpers.kt:73-76: Move public-to-private acceptance orchestration into platform-wallet
This catch re-reads the incoming request and automatically switches the Accept action from a refused public operation to a shielded-funded private operation. Rust enforces the privacy refusal, but the host still owns the subsequent workflow. The book's "Where code goes" section explicitly assigns mobile DashPay orchestration to rs-platform-wallet, and "FFI and mobile SDKs hold no business logic" requires hosts to call an existing Rust operation rather than make wallet decisions. Copying the Swift example's fallback adds another host-owned implementation; that existing implementation does not exempt the new code from the rule. Add a Rust acceptance entry point with the required spending context, resolve the incoming request and select the public/private route there, then expose it through FFI/JNI so this helper makes one call. Keep the explicitly public API's refusal. Test the unified workflow offline at its Rust owner, including public success, private acceptance, unrelated-error propagation, missing requests, and already-reciprocated requests; the current error-mapping and JNI-binding tests do not exercise this routing.
In `packages/rs-unified-sdk-jni/src/dashpay.rs`:
- [SUGGESTION] packages/rs-unified-sdk-jni/src/dashpay.rs:1520-1525: Exercise cleanup of populated request-handle arrays
The only cleanup test supplies ContactRequestHandleArray::empty(), so neither the per-handle destruction loop nor the allocated-buffer release executes. The FFI array-free function deliberately frees only the buffer: individual requests remain in CONTACT_REQUEST_STORAGE until contact_request_destroy removes them. Removing the destruction loop would therefore leak requests on every nonempty private sync while the new tests still passed. Add a JVM-independent test that creates at least two real handles through contact_request_create, puts them in a boxed-slice allocation matching the FFI array contract, and invokes release_contact_request_handles. Assert the returned count, null pointer and zero count afterward, and that getters reject both released handles. Use real registered handles rather than fabricated values so the test verifies destruction as well as buffer cleanup.
…in-private-contact-requests
`PlatformWalletManager.acceptContactRequestReplyingInKind(walletId, request, shieldedAddress, account)` bridges `platform_wallet_manager_accept_contact_request_replying_in_kind` through the new JNI export `DashpayNative.acceptContactRequestReplyingInKind` (`shielded` feature): the wallet replies privately to a request that came privately with no reply yet, and with a document (or by adopting the reply already sent) otherwise. It is the accept a host calls, so hosts no longer catch code 68 and call the private accept themselves. Port of Swift's `PlatformWalletManager.acceptContactRequestReplyingInKind`. Like the other manager-level shielded spends it signs with the manager's identity signer and takes the spend authority from the manager's resolver. The explicit public and private accepts stay; their KDoc, and code 68's, point at the new accept. `FfiSmokeTest` binds the new symbol. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…c bridge `release_contact_request_handles` destroys the handles the private store sync registers, since Kotlin reads those requests from Room. The new JVM-free test registers two real requests through platform-wallet-ffi's `contact_request_create`, puts their handles in a boxed slice as `ContactRequestHandleArray::from_requests` allocates one, releases the array, and checks the count, the null pointer and zero count left behind, and that `contact_request_get_sender_id` refuses both handles afterward. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…equest is replied to `acceptIncomingContactRequest` looks the request up and makes the one `acceptContactRequestReplyingInKind` call, instead of trying the public accept and falling back to the private one on code 68. Requests and Add Contact's collision accept keep calling it. Mirrors the Swift app. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ntact parity entry `dashpay.private_contact_requests` lists `platform_wallet_manager_accept_contact_request_replying_in_kind`, both hosts' reasons say the example apps accept with it, and the entry cites the wallet's in-kind test and the JNI handle-release test. Statuses are unchanged, so `PARITY_SUMMARY.md` regenerates identical. Co-Authored-By: Claude Opus 5.5 <[email protected]>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
The two prior findings are fixed: populated request-array cleanup is covered, and public/private reply selection now belongs to platform-wallet. One blocking regression remains: both example-app acceptance flows unconditionally invoke a shielded-only JNI symbol, breaking public acceptance on the documented transparent-only build. This was a static verification at 13f4784; current CI reports the Kotlin build/emulator job passing with shielded enabled, policy checks passing, and the review check pending.
🔴 1 blocking
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
normalbygpt-6.1-sol(effort low) — The cross-language change is substantial, but it adds JNI/Kotlin adapters to existing private-contact operations rather than changing funds-movement, cryptographic, signature, or consensus logic itself. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort high); agentphase2-reviewer
🤖 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/DashPaySyncHelpers.kt`:
- [BLOCKING] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPaySyncHelpers.kt:66-67: Preserve public acceptance on transparent-only builds
Requests and Add Contact now route every acceptance through this call, including ordinary public requests. However, `DashpayNative.acceptContactRequestReplyingInKind` has its JNI export behind `#[cfg(feature = "shielded")]` in `rs-unified-sdk-jni/src/dashpay.rs:1497`, and the underlying FFI export is similarly gated. The Kotlin README explicitly supports `./build_android.sh --no-shielded`, which disables that JNI feature. On that build, accepting a public request previously used the available public bridge but now throws `UnsatisfiedLinkError`; neither `mapNativeErrors` nor the screens' `catch (e: Exception)` handlers catches it. Make unified acceptance available without shielded support, retaining route selection in platform-wallet and returning a typed unsupported error only when a private reply requires the missing capability. Add public-acceptance coverage against the transparent-only native library: the passing emulator job builds with shielded enabled, while a Rust no-default-features compile cannot detect this missing-symbol failure at runtime.
…in-private-contact-requests
…des 69 and 70 - `DashpayNative.acceptContactRequestReplyingInKind` and the helpers it uses are no longer behind `shielded`, matching the FFI export, which now exists in every build: on `./build_android.sh --no-shielded` a public or adopted reply works, and only a request that needs a private reply is refused, typed. The private send and accept bridges stay shielded-only. - `DashSdkError.PlatformWallet.ShieldedBuildRequired` (69) and `ProtocolVersionNotActive` (70), pinned in `DashSdkErrorTest`. - The in-kind accept's KDoc names both and says a public or adopted reply needs neither shielded support nor a payment mnemonic read. The manager's private send and accepts already run under its teardown gate, so nothing changes there. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…d report private refresh failures As the Swift app does (`ContactRequestOutcomes.swift`): - An accept whose private reply could not be confirmed (`ShieldedSpendUnconfirmed`) stays pending, as the send screen treats an unconfirmed spend: the request row keeps its pending state without its buttons, and Add Contact shows a Done-only dialog and no longer enables Send. - The request list's private refresh stays quiet when the network does not serve the private store yet (code 70), and shows any other failure, and requests read but not saved, above the list. - `ContactRequestSubmissionOutcome` and `PrivateContactRefreshNotice` hold those readings, pinned by `ContactRequestOutcomesTest`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ded behaviour in the private contact parity entry Both hosts' reasons now say the in-kind accept works without shielded support for a public or adopted reply, codes 68 to 70 are typed, and the example apps keep an unconfirmed private submission pending and report private refresh failures. The entry cites the FFI test of an accept on a manager without shielded support and both apps' outcome tests. Statuses are unchanged, so `PARITY_SUMMARY.md` regenerates identical. Co-Authored-By: Claude Opus 5.5 <[email protected]>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Verified the findings against head 89d2330. All three prior findings are fixed, but one blocking issue remains: screen-local pending state allows an uncertain private acceptance to initiate another paid reply after navigation or through another screen. This was a static review; no local builds or tests were run, and the current Kotlin emulator build/test check passes.
🔴 1 blocking
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
normalbygpt-6.1-sol(effort low) — The cross-language feature is substantial but primarily marshals existing private-contact operations and handles application outcomes, without changing the underlying funds movement, cryptography, or signing algorithms. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort high); agentphase2-reviewer
🤖 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/ContactRequestsScreen.kt`:
- [BLOCKING] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ContactRequestsScreen.kt:95-98: Retain unresolved private acceptances in wallet-owned state
After `ShieldedSpendUnconfirmed`, this `remember` set prevents another acceptance only while this composition survives. Leaving and reopening Requests clears it, and Add Contact's separate `unconfirmedNotice` is not shared with Requests. The incoming-only row therefore offers Accept again before the first submission is reconciled.
The Rust path does not prevent the second payment: `send_private_contact_request` returns at `pay(entry).await?` before recording an outgoing request, while `needs_private_reply` and `accept_contact_request_replying_with` recognize only recorded outgoing or established requests. `reserve_notes_covering` excludes the original spend's reserved notes but allows another spend from other available notes. A second acceptance can consequently seal a different entry and pay another fee while the first transition can still land. `kickDashPaySync` runs the ordinary DashPay sweep, which does not read the private request store; the native shielded-sync poke likewise does not establish the missing contact-level pending state.
Retain the unresolved reply in `rs-platform-wallet`, keyed by wallet and identity pair and associated with its pending spend. Subsequent accepts must return the pending outcome rather than build another reply until reconciliation establishes whether replacement is safe. Expose that Rust-owned status to both screens instead of relying on their lifetimes. Add owner-level coverage where an ambiguous first payment is followed by another acceptance before private-store reconciliation, including reconstruction/re-entry, and assert that the payment callback runs only once. This PR introduces private acceptance into both screens and adds this local guard, so the gap is in scope; wallet-level retry decisions also belong in Rust under the book's mobile-orchestration ownership rule.
…in-private-contact-requests
…, and type code 71 - JNI and `DashpayNative` bridge `managed_identity_get_private_contact` (a compact JSON object, null when nothing went privately, as Swift's nil) and `managed_identity_get_private_reply_pending`. - `Dashpay.privateContact(ourIdentityId, contactId)` and `Dashpay.privateRepliesPending(ourIdentityId, contactIds)` read them from a fresh managed-identity snapshot. The second lets a host disable a second send or accept from wallet-owned state. Ports of Swift's `ManagedIdentity.privateContact(contactId:)` and `privateReplyPending(contactId:)`, which the Swift app now calls. - `DashSdkError.PlatformWallet.PrivateContactRequestPending` (71), pinned in `DashSdkErrorTest`. - `FfiSmokeTest` binds both new symbols. The JNI crate pins the JSON layout Kotlin decodes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
As the Swift app does: the Requests rows and Add Contact's Send and collision accept read whether the wallet has a private request to the contact in flight or not yet read back (`privateRepliesPending`), instead of keeping an unconfirmed accept in a `remember` set. While it is set, the row shows pending without its buttons, and Send and the collision accept are off. It clears when a private sync settles it and holds when the screen is left and reopened. Requests re-reads it on composition and row changes, after an accept, on refresh and after each sync pass; Add Contact re-reads it for the resolved recipient and after an accept. `PrivateContactRequestPending` (71) is the same outcome as an unconfirmed spend: pending, no fresh submission. `ContactRequestOutcomesTest` covers it and the row message. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…private contact parity entry `dashpay.private_contact_requests` lists `managed_identity_get_private_reply_pending`. Both hosts' reasons now say the private contact and pending reads are wrapped (Kotlin now bridges them), codes 68 to 71 are typed, and the example apps read the wallet's pending flag. The entry cites the FFI getter test and the Kotlin pending outcome test. Statuses are unchanged, so `PARITY_SUMMARY.md` regenerates identical. Co-Authored-By: Claude Opus 5.5 <[email protected]>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Verified head 2890ed8: wallet-owned acceptance routing, transparent-only exports, and populated-handle cleanup coverage address three prior findings. One blocking issue remains: process restart loses the contact-level reservation for an unresolved private reply, allowing another fee-paying acceptance before reconciliation. This was a static review with no local builds or tests; the current Kotlin SDK build and emulator check passed.
🔴 1 blocking
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
normalbygpt-6.1-sol(effort low) — The substantial cross-language change adds JNI and Kotlin orchestration for existing wallet operations rather than changing funds movement algorithms, cryptography, signing rules, or consensus itself. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort high); agentphase2-reviewer
🤖 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/ContactRequestsScreen.kt`:
- [BLOCKING] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ContactRequestsScreen.kt:165-169: Retain unresolved private acceptances in wallet-owned state
(existing thread: https://github.com/dashpay/platform/pull/5813#discussion_r4507144191)
The fresh wallet snapshot fixes screen re-entry, but it reports no pending reply after process restart. `DashPayState.pending_private_replies` is memory-only, and `Dashpay.privateRepliesPending` explicitly documents the reset at `Dashpay.kt:400–407`. Restoring the persisted incoming request does not restore this reservation, so Requests offers Accept again while Rust's `needs_private_reply` still sees no outgoing counterpart.
This can duplicate fees even when the first payment was confirmed: `send_reserved_private_contact_request` completes `pay(entry).await?`, fails to read the entry back from the private store, and returns `PrivateContactRequestPending` without recording an outgoing request. If the process then restarts before successful private-store reconciliation, another acceptance reserves the now-empty recipient slot, seals a fresh entry, and pays again. The ambiguous `ShieldedSpendUnconfirmed` path has the same gap while the original signed spend remains recoverable. Shielded note reservations protect only the selected notes; `reserve_notes_covering` can fund the second submission from other available notes. The private store is an append-only log, not contact-level deduplication.
The #5810 restoration work must be consumed here before this newly exposed acceptance flow is safe across process death. Persist the wallet/identity/recipient reservation and its entry/spend association before broadcast, restore it before pending reads and submissions become available, and retain it until authoritative reconciliation establishes settlement or safe replacement. Keep Kotlin as a consumer of that Rust-owned status. Add owner-level restart coverage for both an ambiguous payment and a successful payment followed by failed read-back, asserting that another acceptance does not invoke payment again.
…in-private-contact-requests
…in-private-contact-requests
…in-private-contact-requests
…in-private-contact-requests
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Verified the findings against head e050ab0. All four prior findings are fixed, but the new pending-state presentation helper still converts failed reads into an apparent absence of pending requests; a separate KDoc placement nitpick also remains. This was static verification only: the current Kotlin SDK build and emulator tests pass, and the review check is pending.
🔴 1 blocking | 💬 1 nitpick(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
normalbygpt-6.1-sol(effort low) — The cross-language implementation is substantial, but its JNI exports, Kotlin wrappers and UI state handling delegate signing and private transfers to existing wallet functionality rather than changing funds movement, cryptography or key-handling logic themselves. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort high); agentphase2-reviewer
🤖 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/DashPaySyncHelpers.kt`:
- [BLOCKING] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPaySyncHelpers.kt:56-57: Preserve unavailable pending state instead of reporting no pending requests
This catch makes an operational read failure indistinguishable from a successful snapshot containing no pending replies. The SDK already returns an empty result for an unmanaged identity while propagating other failures, and accessing a closed ManagedPlatformWallet throws. If a pending-state refresh fails, ContactRequestsScreen replaces walletPendingIds with this empty set, removing the pending notice and restoring Accept/Ignore; AddContactScreen similarly sets recipientPending to false and enables Send. No successful reconciliation established that the pending submission cleared. Rust's reservation and recovery guards prevent another private payment, but they do not make this report of wallet state correct. Preserve a distinct unavailable/error result, retain the last successful pending snapshot, and keep pending-sensitive controls unavailable until a successful read. Add coverage for a successful pending snapshot followed by a failed read, separately from a successful empty snapshot.
In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/DashpayNative.kt`:
- [NITPICK] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/DashpayNative.kt:331-338: Attach the in-kind acceptance KDoc to its declaration
This acceptance documentation is immediately followed by another KDoc block and managedIdentityPrivateContact, while acceptContactRequestReplyingInKind appears below both managed-identity reads without its documentation. Move this block directly above the acceptance declaration's @Suppress annotation so its requestHandle/account links and reply-routing explanation document the intended API.
The block documenting `acceptContactRequestReplyingInKind` sat above `managedIdentityPrivateContact`; it now sits directly above the accept's `@Suppress`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…g pending `privateRepliesPending` caught a failed wallet read and returned an empty set, so Requests brought back Accept and Ignore, and Add Contact enabled Send, with nothing showing the pending request had cleared. The read now returns null on a failure, and `PrivateRepliesPendingSnapshot` keeps the last successful snapshot and leaves those controls off for every contact until a read covering it succeeds. An unmanaged identity still reads as nothing pending. A pending private request (code 71), which the first public send or accept since load can now meet, gets its own outcome: nothing was sent, so Add Contact shows it as pending and offers "Sync private requests", after which Send comes back for the user to try again. Port of the Swift change on #5812. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…in-private-contact-requests Co-Authored-By: Claude Opus 5.5 <[email protected]>
The schema the Room compiler exports for version 16, with the two private contact request scan state tables; `DashDatabaseMigrationTest` validates the 15 to 16 migration against it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…y manifest Declares the `private_contact_request_scan_state` persistence capability, adds `dashpay.private_contact_request_scan_state` with the three extension slots and the tests on each layer, and notes the unavailable pending state and the code 71 sync offer in the private contact request entry. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The scan state migration (15 -> 16) moved the schema to version 16; the schema version unit test still asserted 15. Co-Authored-By: Claude Opus 5.5 <[email protected]>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
All six prior findings are addressed at head 31d393e. One blocking persistence-boundary issue remains, along with three non-blocking issues involving code-71 feedback, sync documentation, and callback test coverage. Static verification confirmed these against the source; the current Kotlin SDK build and emulator tests pass, and no local builds or tests were run.
🔴 1 blocking | 🟡 3 suggestion(s)
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The large cross-language change directly modifies storage migrations in DashDatabase.kt through MIGRATION_15_16 and adds native persistence callbacks for private contact scan state and shielded addresses. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 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/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt`:
- [BLOCKING] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt:4664-4668: Fail scan-state loading instead of silently dropping saved addresses
This filter converts malformed persisted addresses into a successful partial restore. Room accepts arbitrary blob lengths, so a saved 42-byte address can be omitted while packPrivatePositions returns its identity's valid cursor and key fingerprint. The FFI loader then restores that cursor successfully; with unchanged scanning keys and one scanning identity, subsequent private sync starts at the retained position rather than revisiting the entry containing the address. The contact consequently reports no shielded address instead of reporting unreadable persisted state. Silently selecting partial recovery also exceeds the adapter's marshalling role under the book's "FFI and mobile SDKs hold no business logic" rule. Reject malformed stored rows through the existing loadOrThrow/JNI load-error path. Any automatic cursor invalidation and reconstruction policy belongs in rs-platform-wallet. Add a test combining a valid saved position with a malformed address row and assert that loading fails rather than returning a partial snapshot.
In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/AddContactScreen.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/AddContactScreen.kt:294-295: Preserve code-71 feedback when no reply reservation exists
PrivateContactRequestPending does not guarantee that privateRepliesPending contains this contact. Rust's read_private_store_before_sending also returns code 71 when its startup store scan cannot finish, before creating a reservation; should_refuse_a_private_send_until_the_store_reads_to_its_end explicitly verifies that outcome with nothing reserved. Both unified acceptance and public sending pass through this scan gate. Here, the Unit arm discards the refusal, and a subsequent successful empty pending read enables submission without showing a notice or Add Contact's private-sync button. The public Send branch and ContactRequestsScreen make the same assumption. Preserve visible refusal feedback and a private-refresh path independently of whether a contact-level reservation exists. If distinct reasons are needed, expose them from platform-wallet rather than inferring them in Kotlin. Add coverage for code 71 followed by a successful empty pending snapshot.
In `packages/rs-unified-sdk-jni/src/persistence.rs`:
- [SUGGESTION] packages/rs-unified-sdk-jni/src/persistence.rs:1120-1126: Include scan-state callbacks in the JNI descriptor smoke check
Neither onPersistPrivateContactScanState nor onLoadPrivateContactScanState appears in BRIDGE_METHOD_TABLE, so persistenceBridgeDescriptorsAllResolve does not check these new descriptors against the compiled Kotlin methods. The added Rust unit test compares the constants with manually supplied type names and cannot detect Kotlin signature drift. A mismatch would make bridge_overrides return false and disable the scan-state callbacks and capability, allowing the existing smoke check to pass while relaunches lose cursor persistence. Add both callback names to BRIDGE_METHOD_TABLE using these descriptor constants so the existing emulator test verifies the actual Kotlin signatures.
In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/tokens/Dashpay.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/tokens/Dashpay.kt:352-355: Document capability-backed private scan-state persistence
This KDoc still says that store cursors are memory-only and every launch scans from the beginning. This PR adds Room v16 persistence and JNI load/store callbacks for PRIVATE_CONTACT_REQUEST_SCAN_STATE, and the Rust loader restores the saved cursor when the backend attests that capability. Update the documentation to distinguish capability-backed restoration from the fallback full scan when the capability is unavailable, so SDK consumers receive an accurate description of relaunch and sync behavior.
…in-private-contact-requests Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ross the ABI Room keeps a blob of any length, and the load packer filtered out rows of the wrong length while handing back the rest: a 42-byte address vanished while its identity's read position was restored, so the sync resumed past the entry that carried it. A malformed row now throws, which fails the load through `loadOrThrow`; what to do about such a store is the wallet's call, not the adapter's. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… and check their descriptors The scan-state slots are now wired only for a bridge that also overrides `onPersistContactUpsert`: a read position vouches for the contact requests read before it, which an inherited no-op upsert never keeps. Both scan-state callbacks join `BRIDGE_METHOD_TABLE`, so the descriptor smoke check resolves them against the compiled Kotlin methods; a drift would otherwise drop the slots and the capability without a failure. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…g replies `syncPrivateContactRequests` said the store cursors live in memory only; they are restored where the persister attests `PRIVATE_CONTACT_REQUEST_SCAN_STATE`, with the full read as the fallback. `privateRepliesPending` is restored too once the shielded store is configured after the load or the wallet binds. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… load A wallet that bound a shielded account in an earlier process has its public contact requests refused (code 71) until Rust restores its unconfirmed private payments, which configuring the shielded store does with no keys. The app configured it only when binding, which needs the mnemonic; it now configures the same per-network path right after `loadPersistedWallets`. Port of the Swift change on #5812. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…te sync Code 71 does not mean the wallet holds a reservation for the contact: it is also returned, with nothing reserved, when the private store could not be read to its end before the first send or accept since load. Add Contact's public Send, its collision accept and the Requests rows dropped the refusal and read the pending state, which then found nothing and brought the controls back with no notice. `PrivateRepliesPendingSnapshot` now keeps the refused contact pending, with the private sync offered, until a private sync completes. Port of the Swift change on #5812. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ity manifest Co-Authored-By: Claude Opus 5.5 <[email protected]>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
At head 4763d41, the ten prior findings are addressed, but the new code-71 recovery mechanism incorrectly infers wallet readiness from private-document sync completion, violating the Rust-owned orchestration boundary. Two additional test-coverage suggestions remain. This was a static review without local builds or tests; current Kotlin emulator and policy checks pass, with the review check pending.
🔴 1 blocking | 🟡 2 suggestion(s)
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The cross-language persistence changes are intricate and directly introduce a storage migration in packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabase.kt together with private-contact scan-state persistence and restoration. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 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/ContactRequestOutcomes.kt`:
- [BLOCKING] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ContactRequestOutcomes.kt:95-99: Keep code-71 readiness and recovery decisions in platform-wallet
The app clears every code-71 refusal after a private-document sync, although that operation cannot resolve every condition represented by code 71. In `rs-platform-wallet-ffi/src/error.rs`, `PrivateContactPaymentsNotRestored` also maps to 71. That condition is a wallet-wide `WalletPersister` hold, not a contact reservation, so `privateRepliesPending` can successfully return an empty set while the hold remains.
After shielded-store configuration or saved-payment restoration fails, the app can still expose the loaded wallet. A subsequent private-document sync can succeed independently: `sync_private_contact_requests` does not release the restoration hold; `restore_private_contact_payments` does. Both screens nevertheless call `afterPrivateSync()` when the refresh produces no notice, discard the refusal, and enable submission after an empty pending read. The pending notice disappears without authoritative recovery, and the next submission encounters the same 71. Rust still prevents an unsafe send, but the app has reconstructed an unsupported readiness conclusion.
The book assigns mobile DashPay orchestration to `rs-platform-wallet` and requires hosts to marshal rather than decide. Expose authoritative readiness from that owner, distinguishing contact reservations, incomplete private-store scans, and unrestored payments; marshal it through FFI/JNI and render it in the screens instead of deciding that sync releases every refusal. Add owner-level coverage showing that private-document sync does not release the payment-restoration hold and successful restoration does.
In `packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/FfiSmokeTest.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/FfiSmokeTest.kt:114-124: Gate shielded-only symbol probes without skipping universal acceptance
The README supports a transparent-only native library built with `./build_android.sh --no-shielded`, but these two JNI exports are deliberately guarded by `#[cfg(feature = "shielded")]`. Against that supported library, the assertions receive `UnsatisfiedLinkError`, not `DashSDKException`, and the test stops before checking the always-exported in-kind acceptance symbol. Gate only the explicit private-send and private-accept probes on `Sdk.hasShielded()`, or move them into a shielded-only test. Keep sync, managed-identity reads, and in-kind acceptance exercised in every build.
In `packages/rs-unified-sdk-jni/src/persistence.rs`:
- [SUGGESTION] packages/rs-unified-sdk-jni/src/persistence.rs:5413-5420: Exercise the scan-state override checks rather than supplying their result
This test supplies `private_contact_scan_state: wired` directly to `extension_for`, so it checks slot assignment after the decision has already been made. It does not exercise the three `bridge_overrides` checks in `build_extension`: removing the contact-upsert requirement or accepting an inherited scan-state callback would leave it passing. Those checks protect the invariant that a durable cursor must not skip requests an inherited no-op contact writer never stored. The descriptor smoke test checks method resolution, not concrete overrides. Add JVM-backed cases declaring the capability while omitting each required override in turn, and verify that the effective capability is absent; also verify that a fully implemented bridge retains it.
…in-private-contact-requests Co-Authored-By: Claude Opus 5.5 <[email protected]>
…t marker with the read position `private_contact_requests_positions` (Room 16, unreleased, so edited in place with its migration and exported schema) gains `privatePaymentsUnresolved`, 0 by default. A packed position row grows by one byte (0 or 1; any other value is not a row), JNI carries it to and from `PrivateContactRequestsPositionFFI`, and the handler stores it with the position and hands it back at load. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…private contact readiness `DashSdkError.PlatformWallet.PrivateContactPaymentsNotRestored` binds `ErrorPrivateContactPaymentsNotRestored` (72), apart from a contact's pending request (71). `Dashpay.privateContactReadiness` binds `platform_wallet_get_private_contact_readiness` through a new JNI export returning its three flags. Port of the Swift bindings on #5812. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…let's readiness The screens cleared every code 71 refusal once a private sync finished, though a sync releases neither a contact's reservation nor the wallet's unrestored payments. They now render the wallet's own state: - code 72 shows "restoring private payments" for every contact, apart from a pending request (71), and only a readiness read that says restored lifts it; a private sync is not offered for it; - a code 71 refusal stays pending until the readiness says the private store was read to its end, after which the pending read decides; - the readiness is read with the pending state and again when the shielded account binds. Port of the Swift change on #5812. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…-only probes The scan-state slot test supplied the override decision to `extension_for` itself. A new JNI hook, `nativeEffectivePersistenceCapabilityBits`, builds a bridge's vtable and extension as manager creation does and returns the effective capability bits, and an instrumented test declares the scan-state capability with each required override (contact upsert, scan-state store, scan-state load) left out in turn: each loses it, while a bridge with all three and the Room handler keep it. The private contact symbol probe now calls the explicit private send and private-reply accept only in a shielded build (`Sdk.hasShielded()`), as `build_android.sh --no-shielded` does not export them; the sync, the reads, the new readiness read and the in-kind accept are probed in every build. The syncFaultDetected KDoc moves back onto its own test. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…fest Co-Authored-By: Claude Opus 5.5 <[email protected]>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Verified the complete diff at 7bc3a03 and confirmed that all 13 prior findings are addressed. Two documentation-placement nitpicks remain; no blocking defects were confirmed. This was a static review with no local builds or tests; the Kotlin SDK build and x86_64 emulator CI passed on the exact reviewed head.
💬 2 nitpick(s)
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The change is large and cross-language, and DashDatabase.kt directly adds the v15-to-v16 storage migration for persisted private-contact scan positions and shielded addresses. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 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/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/FfiSmokeTest.kt`:
- [NITPICK] packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/FfiSmokeTest.kt:157-163: Move the descriptor-lockstep KDoc back to its test
This block describes persistenceBridgeDescriptorsAllResolve, but the new capability test and its own KDoc now separate it from that declaration. Move this block directly above persistenceBridgeDescriptorsAllResolve so the descriptor-resolution explanation documents the intended test rather than preceding an unrelated capability test.
In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/tokens/Dashpay.kt`:
- [NITPICK] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/tokens/Dashpay.kt:434-447: Attach the pending-replies KDoc to privateRepliesPending
This block describes privateRepliesPending, including its contactIds argument and pending-payment restoration behavior, but privateContactReadiness and its own KDoc now intervene before that method. Move the block directly above privateRepliesPending so consumers see the pending-reply contract on the correct API and its parameter links resolve against the intended declaration.
…in-private-contact-requests Keeps this branch's code 72 registry row with the Kotlin typing and takes the code 73 row. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Room handler skipped a contact request (or an ignored sender) whose owner identity was not persisted and acknowledged the round. This store declares the private contact request scan state, so native then stored a read position past a request that was never saved, and no later scan would read it again. The staged write now throws, which fails the round at its end (or the callback outside a round): the read position stays where it was, and the next scan stores the request. Port of the Swift fix on #5812. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… error `DashSdkError.PlatformWallet.PrivateContactRecoveryUnsupported` binds `ErrorPrivateContactRecoveryUnsupported` (73). The Room handler declares atomic changesets and the scan state, and JNI wires the begin and end callbacks, so it should not occur; KotlinExampleApp shows it as "this storage backend can't protect private payments". Port of the Swift binding on #5812. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e override test The JVM-backed capability test now checks the wallet's rule for keeping the private contact request scan state, the scan state with atomic changesets: a bridge declaring the scan state without atomic rounds has neither, beside the cases that leave an override inherited. JNI always wires the changeset begin and end callbacks, so the atomic half is the declaration. The descriptor-lockstep KDoc moves back onto `persistenceBridgeDescriptorsAllResolve`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…nding The block documenting `privateRepliesPending` sat above `privateContactReadiness`; it now sits directly above its own declaration. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…nifest Co-Authored-By: Claude Opus 5.5 <[email protected]>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
At head 2abc9b5, all 15 prior findings are fixed, and reply routing, payment reservations, and recovery readiness remain Rust-owned. One non-blocking error-propagation gap remains in the newly exposed explicit private-send and private-accept APIs. Verification was static only; the current Kotlin SDK build and x86_64 emulator check passes.
🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 6: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This large, cross-language change directly modifies storage migrations in DashDatabase.kt and persistent private-contact scan state handling in PlatformWalletPersistenceHandler.kt and rs-unified-sdk-jni/src/persistence.rs. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 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/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/PlatformWalletManager.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/PlatformWalletManager.kt:2173-2174: Preserve code 73 through the explicit private request exports
With shielded support configured and an account bound, a persistence backend lacking the required scan-state/atomic capability causes the Rust private-send path to return PrivateContactRecoveryUnsupported. Both explicit send and private-accept FFI exports pass that error through map_spend_result in rs-platform-wallet-ffi/src/shielded_send.rs, which preserves codes 71 and 72 but lacks an arm for this variant. It therefore returns ErrorWalletOperation (6), so these new Kotlin APIs receive PlatformWallet.WalletOperation instead of PrivateContactRecoveryUnsupported (73). The in-kind accept preserves 73 through its pre-payment blanket conversion, leaving sibling APIs inconsistent. Preserve PrivateContactRecoveryUnsupported in the owning FFI mapper and add a regression assertion against map_spend_result; the Kotlin mapping test constructs code 73 directly and cannot detect its loss before JNI.
Out-of-scope follow-up suggestions (1)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- Release discarded request handles in the existing public-sync bridges — The unchanged syncContactRequests and fetchSentContactRequests JNI exports free only their returned array buffers. ContactRequestHandleArray::from_requests registers each request in CONTACT_REQUEST_STORAGE, and the array-free function deliberately leaves those handles alive. Because these bridges return no handles to Kotlin, repeated nonempty calls accumulate unreachable native allocations. This behavior exists unchanged at the base revision; the new private-sync bridge correctly destroys its handles.
- Follow-up: Address both existing public-sync bridges in a separate follow-up PR by destroying every discarded request handle before freeing the array, with populated-array cleanup coverage.
| val handle = mapNativeErrors { | ||
| DashpayNative.sendPrivateContactRequest( |
There was a problem hiding this comment.
🟡 Suggestion: Preserve code 73 through the explicit private request exports
With shielded support configured and an account bound, a persistence backend lacking the required scan-state/atomic capability causes the Rust private-send path to return PrivateContactRecoveryUnsupported. Both explicit send and private-accept FFI exports pass that error through map_spend_result in rs-platform-wallet-ffi/src/shielded_send.rs, which preserves codes 71 and 72 but lacks an arm for this variant. It therefore returns ErrorWalletOperation (6), so these new Kotlin APIs receive PlatformWallet.WalletOperation instead of PrivateContactRecoveryUnsupported (73). The in-kind accept preserves 73 through its pre-payment blanket conversion, leaving sibling APIs inconsistent. Preserve PrivateContactRecoveryUnsupported in the owning FFI mapper and add a regression assertion against map_spend_result; the Kotlin mapping test constructs code 73 directly and cannot detect its loss before JNI.
source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, platform-versioning, rust-quality, security-auditor)
Basic explanation
What this does: Android apps can now sync, send and accept DashPay private contact requests through the Kotlin SDK, over the same platform-wallet FFI the Swift SDK uses. The request is signed by the identity signer the app already uses for public requests. The wallet picks how an accepted request is replied to, and result codes 68 to 71 become typed errors. KotlinExampleApp shows a private request still in flight as pending, as the wallet reports it.
Value: parity with iOS for private contact requests.
Risks: low.
Issue being fixed or feature implemented
Part of anonymous DashPay contact requests for 5.1, on top of #5812.
What was done?
JNI (
rs-unified-sdk-jni/src/dashpay.rs)syncPrivateContactRequests(walletHandle, coreSignerHandle, maxPages)returns the pass's six counters as along[]. It destroys every returned request handle, since Kotlin reads the requests from Room.sendPrivateContactRequest(...)andacceptPrivateContactRequest(...):shieldedfeature;SignerHandlethe publicsendContactRequesttakes;Kotlin SDK
Dashpay.syncPrivateContactRequests(coreSignerHandle, maxPages)returns aPrivateContactSyncResult.Dashpay.incomingContactRequest(ourIdentityId, senderId)(Swift'sgetIncomingContactRequest(senderId:)) gives the private accept its request.PlatformWalletManager.sendPrivateContactRequest(...)andacceptPrivateContactRequest(...)sit with the other shielded spends. They use the manager's resolver and identity signer.DashSdkError.PlatformWallet.PrivateContactRequestNeedsPrivateReplyis code 68.PrivateContactRequests.swift.KotlinExampleApp
DashPaySyncHelpers.acceptIncomingContactRequestfalls back to the private accept on code 68, asContactsView.swiftdoes. Requests and Add Contact's collision accept use it.Parity
dashpay.private_contact_requestsindocs/sdk/sdk-parity-manifest.json, with the new FFI symbols inshared_symbols.partial. A send or accept needs a protocol version 15 network with shielded funds, and no gate is recorded yet.PARITY_SUMMARY.mdregenerated withscripts/check_sdk_parity_manifest.py --write-summary.Changes from review
One accept. The JNI bridges
platform_wallet_manager_accept_contact_request_replying_in_kind, exported in every build. KotlinExampleApp accepts only through it, so the wallet picks the reply.Pending state read from the wallet.
Dashpay.privateContact(ourIdentityId, contactId)reads how a contact's requests came, throughmanaged_identity_get_private_contact.Dashpay.privateRepliesPending(ourIdentityId, contactIds)reads which contacts have a private request in flight, throughmanaged_identity_get_private_reply_pending, from a fresh managed-identity snapshot.rememberset, so leaving and reopening a screen keeps a pending request pending.New typed codes:
ShieldedBuildRequired: a private reply on a build withoutshielded.ProtocolVersionNotActive: the network is below protocol version 15.PrivateContactRequestPending: a private request is in flight; the app shows it as pending and offers no new submission.Other changes. Private refresh failures are reported. The JNI sync bridge releases real request handles. The parity entry tracks the new symbols.
Faster private sync, host side. The JNI wires the new scan-state persistence slots, and Room goes to version 16 (
MIGRATION_15_16, two tables, a new DAO). The handler declares the scan-state capability, stages the writes into the changeset round and purges them with the wallet, so a relaunch resumes the private store read.Pending state read failures. A failed pending-state read is kept apart from "nothing pending" (
PrivateRepliesPendingSnapshotkeeps the last good one).Docs. The in-kind accept KDoc is attached to its declaration.
How Has This Been Tested?
cargo clippy -p rs-unified-sdk-jni --all-targets -- -D warnings, with default features and--no-default-features: clean.cargo test -p rs-unified-sdk-jni --lib -- dashpay::tests: 5 passed (counter order, saturating position, empty array release, real handle release, the private contact JSON layout).:sdk:compileDebugKotlin,:sdk:compileDebugAndroidTestKotlin,:app:compileDebugKotlin, and:sdk:testDebugUnitTest --tests '*DashSdkErrorTest*'(29 passed, including codes 68 to 71), andContactRequestOutcomesTest(7 passed, including 71 as pending and the row message).python3 scripts/check_sdk_parity_manifest.py: passes.FfiSmokeTest(the private contact request symbols, and the private contact and pending reads) needs an emulator; the instrumented CI job runs it.Breaking Changes
None.
Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Generated with Claude Code