feat(platform-wallet)!: support DashPay shielded tips with dedicated accounts - #5288
PastaPastaPasta wants to merge 23 commits into
Conversation
Resolves the conflicts with the 99 commits v4.2-dev gained since the branch point and adapts the branch to them: - platform-wallet-storage: the profile-encoding migration is renumbered V008 -> V019 (v4.2-dev already ships V008-V018). The stamp columns now DEFAULT 1 and the migration marks the rows it finds 0, so only pre-V019 rows are ever decoded as legacy. The identities writer and both readers on the hard-delete schema (#4496) stamp and dispatch on `entry_format`; the dashpay writer stamps `profile_format`. The migrated-database walk moved to tests/sqlite_profile_address_encoding.rs (the crate's retired-table-name scan covers src/), backed by test-only legacy encoders. SCHEMA.md and the migration fingerprints updated. - swift-sdk: V4 is frozen through scripts/freeze_schema_models.py (FREEZES row at 787cac0) instead of a hand-written copy; the dash-v5.store fixture is written by this build; the migration tests join the fixture-based suite from #4644. - drive / drive-abci: PV14 fee and root-hash pins recomputed for DashPay contract v2 on top of the contract version item (#4749). - platform-wallet: base's re-seeded shield regression fixture kept; both sides' viewing-key bind tests kept; the FFI account-indices exports re-appended after base's new test modules. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tip send guard - platform-wallet-ffi: `platform_wallet_manager_prepare_shielded_tip_address` and `platform_wallet_resolve_shielded_tip` run their worker call inside `catch_query_panic` (ErrorWalletOperation, retryable); the raw output writes happen only on success so the zeroed buffers survive a panic. A test-only fault injected inside the worker future drives the three real `extern "C"` tip entry points, so removing a guard at a call site fails `exported_tip_entry_points_contain_a_worker_panic`. - SwiftExampleApp: `ShieldedTipSubmissions`, an app-owned per network and wallet guard, keeps an in-flight or uncertain tip send locked across sheet dismissal and both entry points (parity with the Kotlin app); tests. - KotlinExampleApp: the tip account is derived from the live identity's optional derivation index (`ManagedPlatformWallet.identityIndex`), not Room's non-null `identityIndex` whose 0 is a placeholder that aliases identity 0's tip pool. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings the branch up to date with v5.1-dev (1ebcedb). The DashPay contract field (#4768) and the DPNS resolver fix (#4769) are now in the base, so the branch's byte-identical copies of those files drop out of the diff. Conflict resolution: - Room: v5.1-dev already ships versions 12-14, so the profile payment address migration becomes MIGRATION_14_15 (database version 15, schema 15.json exported by Room); the migration tests walk 14->15 and 10->15. - SwiftData: v5.1-dev replaced the development V4/V5 chain with App Store release snapshots (#4818, #4910) and published schema 3.0.0. Instead of the branch's hand-registered V4 freeze and V5, DashSchemaV3 is now bound to its generated release snapshot (DashSchemaSnapshotV3) and a new live DashSchemaV4 adds PersistentDashpayPaymentAddresses, with V2->V3->V4 and V1->V3->V4 lightweight stages. The DashSchemaV4 freeze files and the dash-v5.store fixture are dropped; the model is added to schema-models.json and SCHEMA_RELEASES.md describes the new routes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- FFI: platform_wallet_resolve_shielded_tip clones the identity wallet under the registry guard and releases it before the DPNS/profile network lookups, so a slow resolution no longer blocks handle creation and teardown for unrelated wallets. - Kotlin: tag the produceState tip-account result with the (wallet, manager, identity) it was resolved for; produceState keeps its previous value when its keys change, so identity A's account could otherwise be offered to the tip sheet while B's lookup runs. - Swift: payment-address rows are keyed by owner id with no relationship cascade, so PersistentIdentity.remove, the identity list's local removal and the orphan-identity deletion now remove them too (removeOwned is public for host deletion paths). - Storage: pin the pre-V019 profile record with hand-written bincode bytes, independent of the frozen legacy declarations. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…mock query v5.1-dev added DocumentQuery::integer_range_clauses; the branch's DPNS mock query in the profile tests predates it and no longer compiled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ormat stamp patch_payments_into_entry_blobs read entry_blob without entry_format, decoded it as the current IdentityEntry and wrote current-format bytes under the old stamp. A pre-V019 identity whose first post-migration write was a payment overlay round (changeset::core_bridge) would fail to decode, or with empty profiles silently end up as current bytes stamped 0. Decode through decode_identity and restamp the row 1 in the same UPDATE; the regression test walks a V007 legacy identity through a payment-only round and reads it back through both identity readers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DashPayTabScreen used produceState without importing it, and DashPayProfileScreen smart-cast the delegated tipAccount property, which Kotlin rejects. Both predate this round; CI skips Kotlin for fork PRs, so neither had been compiled. Verified with :app:compileDebugKotlin, :sdk/:app compileDebugUnitTestKotlin and :sdk:compileDebugAndroidTestKotlin. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ShieldedService rebuilt its bind request from the native shieldedAccountIndices snapshot (after identity discovery and on the post-Clear resume), and that snapshot includes every discovered and retired tip account Rust binds on its own. With account 0 plus 64 tip accounts the request tripped the 64-entry cap in the Swift wrapper and the C export, so discovery could no longer register new accounts. Both sites now go through ordinaryBindRequest, which drops tip accounts and falls back to [0]; Rust still unions the tip accounts in. Kotlin is unaffected: it binds with the default [0] request. Tests: a Rust test binds 70 persisted tip accounts from a [0] request (seedless and seeded) and shows the resulting snapshot exceeds the cap; an app unit test covers ordinaryBindRequest with a 71-entry snapshot. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ugh load Drive FFIPersister::load with a host wallet-restore callback whose identity carries a non-null dashpay_profile with distinct values for every string and all three payment addresses, plus an identity with a null profile pointer. The free callback scribbles over the host buffers before the restored ManagedIdentity is asserted, so a restored profile still borrowing host memory would fail. Removing the hydration branch fails the test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ip account Identity load and discovery prove an identity's index by deriving its MASTER key, but for an identity already known (observed out-of-wallet, or restored at a host placeholder slot) they only attached wallet_id and left identity_index unset. That snapshot persists without an index, and Kotlin/Swift, which cannot store "unknown", restore it as index 0, so the dedicated tip account mapping would hand that identity identity 0's tip pool. - IdentityManager::adopt_into_wallet moves such an identity into the verified wallet slot and sets its index (never evicting a different identity from the slot); load and discovery call it, so new snapshots carry the real index and placeholder rows self-heal on rediscovery. - prepare_shielded_tip_address now requires the seed to reproduce the identity's MASTER key at the claimed index before it allocates or returns a tip address, failing closed for unverified indices. Tests: adopt_into_wallet re-slotting (observed, placeholder, occupied slot); a cold-reload regression where a wallet-attached identity sits at placeholder slot 0 alongside bound identity-0 tip funds is refused and then accepted after rediscovery proves index 5. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pied slot adopt_into_wallet returned false when the verified slot held a different identity, and both load and discovery ignored it, then marked the unmoved identity active, set wallet_id and persisted key breadcrumbs at the new index, leaving bucket, identity_index and persisted keys in disagreement. - adopt_into_wallet returns a typed IdentityIndexOccupied error and changes nothing; place_verified_identity (add when unknown, adopt otherwise) is the single step both callers run before touching status or keys, and reports Added vs Adopted. - Load is an explicit single-identity operation and propagates the error. - Discovery skips the conflicting index without touching either identity, counts the hit for the gap limit, and records the index as unanswered so the verdict is incomplete and the next launch rescans. Aborting would also stop the stale record from reaching its own verified slot later in the walk, which is what frees the contested one. Tests: placement adds, adopts (not reported as new) and refuses an occupied slot with the typed error and zero persisted stores; the scan tally keeps walking and leaves the verdict incomplete on a conflict. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…very Discovery reports only newly added identities, but it can also promote an already-known identity into its verified slot, whose dedicated tip account then needs registering. Rebind whenever shielded support is available, keeping a bind failure non-fatal. The Swift screen already rebinds unconditionally. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
add_identity rejected duplicate ids but inserted into the target slot unconditionally, so an unknown identity verified at a slot held by another (e.g. a restored placeholder) overwrote the occupant while the occupant's reverse-index entry kept resolving to the newcomer; a later adoption of the occupant then moved the wrong identity. The occupied-slot check now lives in add_identity itself, before any change or persist, returning the same IdentityIndexOccupied error, so place_verified_identity refuses both the Added and Adopted paths and discovery skips the index and marks it unanswered either way. Every production caller (registration, invitation, register-from-addresses, shielded-pool create) already treats an add_identity error as non-fatal local bookkeeping that heals on the next sync; registration targets a free slot. Test: an unknown id targeting an occupied slot is refused through both add_identity and place_verified_identity with both lookups, the bucket and the persister write count unchanged; a free slot still works. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Waiting for bot review — coderabbitai not yet · thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
|
🔍 Review in progress — actively reviewing now (commit 6c88c31) · triage: critical |
Live V4 adds PersistentDashpayPaymentAddresses, which sorts ahead of PersistentIdentity and renumbers it from 13 to 14. Core Data embeds the ordinal in generated column names, so the token-balance table's Z13TOKENBALANCES foreign key becomes Z14TOKENBALANCES with its values intact. validatePreservation normalized Z_<n> join-table names but not the Z<n><NAME> column form, so the V1 -> V3 -> V4 and historical V2 -> V3 -> V4 migrations were reported as removing a column (DashModelMigrationTests, CI on #5288). validatePreservation now keys columns by entity name for the cross-store comparison; a dropped or type-converted column is still rejected. layout()/evidence() keep their existing keys so recovery journals written by earlier builds still verify. Test: a renumbered copy of the dash-v1 fixture (entity inserted at 8, join-table and generated columns renamed) passes; the same copy with the column dropped or converted to VARCHAR is rejected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Verified the supplied findings against head 02460d6 and confirmed four issues involving identity-placement persistence, restart-safe submissions, identity-specific spending-account selection, and concurrent account binding. These are client-side defects, classified as suggestions under the supplied policy reserving blocking severity for consensus invariants. Validation was static: the supplied CI snapshot reports successful Rust workspace and Kotlin checks, skipped dedicated wallet tests, and Swift build/tests still in progress.
🟡 4 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); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The large, cross-language diff directly changes key derivation and dedicated account handling in packages/rs-platform-wallet/src/wallet/shielded/tips.rs, funds submission in packages/rs-platform-wallet-ffi/src/shielded_send.rs, and persisted-data migrations in SQLite, SwiftData, and Room. - 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 below 15% reserve: weekly 15% left, 5h 100% left),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/rs-platform-wallet/src/wallet/identity/state/manager/lifecycle.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/state/manager/lifecycle.rs:206-207: Preserve corrected identity indices through changeset merging
The new adoption path changes identity_index, but IdentityChangeSet::merge never copies that field into an existing entry. With SQLite FlushMode::Manual, a snapshot buffered at placeholder index 0 can therefore absorb the subsequent discovery snapshot at verified index 5 while retaining index 0. Both Buffer::normalize_identities and the final buffered merge use these semantics, so flush/reload restores the incorrect placement. The same stale slot also remains occupied during buffered admission checks, potentially rejecting another identity that discovery places into the freed slot.
Make identity_index follow the latest snapshot alongside wallet_id. Add a SQLite regression that buffers an old-slot snapshot, adopts the identity into its verified slot, persists the resulting snapshot, then flushes and reloads; also verify that another identity can occupy the freed slot.
In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift:28-32: Restore outstanding tip submissions before enabling another send
The submission guard and wallet map are memory-only, and each new process initializes the guard to ready. Kotlin's ShieldedTipSubmissions has the same lifecycle. Restarting after a broadcast with an unresolved outcome therefore removes the warning and permits the user to retry the same intended payment as a new transfer. A relay withholding confirmation is sufficient to create this ambiguity; process termination during broadcast also exposes it.
Rust records pending transfer activity and can redrive the original transition, but transfer reserves individual input notes rather than a payment intent. Additional spendable notes can fund the retry, allowing both transfers to execute. Persist an unresolved intent before entering the native send, restore its guard before enabling submission, and reconcile it with the original activity or transition. Add process-recreation coverage with an ambiguous original transfer and additional spendable notes; the existing reopening tests retain the same in-memory owner.
In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/DashPayProfileView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/DashPayProfileView.swift:39-44: Verify the identity index before selecting its tip spending account
Rust's optional index getter returns recorded metadata, not a verified identity-to-account association. The mobile restore path constructs ManagedIdentity::new(identity, spec.identity_index), so a persisted placeholder zero becomes Some(0). The existing should_refuse_tip_account_for_a_placeholder_index_until_it_is_verified regression demonstrates an identity belonging at index 5 restored at index 0 while index 0's tip account is bound.
Preparation rejects this association by checking the seed, but this balance/spending selector and Kotlin's corresponding selectors bypass that check. They can display index 0's funds as belonging to the restored identity and pass that account to the send API, which validates account keys but receives no source identity to verify. Resolve identity-specific tip accounts through a verified wallet operation, or disable these selectors until the association is verified. Extend the placeholder regression to cover balance lookup and spending-account selection, not only address preparation.
In `packages/rs-platform-wallet/src/wallet/shielded/tips.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/tips.rs:142-147: Make tip preparation an atomic augmentation of the bound account set
The account snapshot and replacement bind are separate operations. Preparation can snapshot ordinary account 0, another task can finish binding accounts 0 and 1, and preparation can then bind its stale snapshot plus the tip account. bind_shielded automatically retains discovered and persisted tip accounts, but not omitted ordinary accounts. Consequently, register_locked removes account 1 from registration and purges its live subwallet state, contradicting preparation's ordinary-account preservation guarantee.
The shield guard and coordinator install transaction serialize individual installations, not the preceding read-modify-write sequence. Serialize configuration changes across the snapshot and bind, or provide an augmentation operation that merges with the current registration inside the installation transaction. Add a deterministic interleaving regression proving that an ordinary account added during preparation remains bound.
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.
- Seed verification runs while holding the wallet-manager read guard — The guard covers bounded synchronous derivation and verification with no network call or await inside the guarded block. Writers are briefly excluded, but the finding supplies no demonstrated hot-path or material contention problem; moving this work outside the lock is an optional optimization rather than a confirmed review defect.
- Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.
| @Published private(set) var status: Status = .ready | ||
| @Published private(set) var message: String? | ||
|
|
||
| var busy: Bool { status == .sending } | ||
| var submitted: Bool { status != .ready } |
There was a problem hiding this comment.
🟡 Suggestion: Restore outstanding tip submissions before enabling another send
The submission guard and wallet map are memory-only, and each new process initializes the guard to ready. Kotlin's ShieldedTipSubmissions has the same lifecycle. Restarting after a broadcast with an unresolved outcome therefore removes the warning and permits the user to retry the same intended payment as a new transfer. A relay withholding confirmation is sufficient to create this ambiguity; process termination during broadcast also exposes it.
Rust records pending transfer activity and can redrive the original transition, but transfer reserves individual input notes rather than a payment intent. Additional spendable notes can fund the retry, allowing both transfers to execute. Persist an unresolved intent before entering the native send, restore its guard before enabling submission, and reconcile it with the original activity or transition. Add process-recreation coverage with an ambiguous original transfer and additional spendable notes; the existing reopening tests retain the same in-memory owner.
source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, platform-versioning, rust-quality, security-auditor)
…index IdentityChangeSet::merge copied wallet_id and the other scalars but not identity_index. Since discovery now re-slots an identity restored at a placeholder index into its verified one, a SQLite Manual-flush buffer holding the old-slot snapshot absorbed the re-slot snapshot and kept index 0, both in Buffer::normalize_identities and the final merge: the flush wrote the stale slot back and the freed slot refused its rightful owner (IdentityIndexConflict). identity_index now follows the latest snapshot like wallet_id. SQLite regression: buffer slot 0, re-slot to 5, admit another identity into slot 0, flush and reload; both identities land in their slots. It fails with IdentityIndexConflict without the fix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…owners - Tip preparation read the bound account set and then bound an augmented copy as two steps; a bind of ordinary accounts landing in between was undone by the stale snapshot, dropping those accounts and purging their live state. A per-wallet shielded_config_lock is now taken by every bind entry point and held by preparation across snapshot and bind (bind_shielded_locked). - send_shielded_tip now refuses to spend from a dedicated tip account whose slot holds an identity the seed does not prove at that index, before any recipient lookup or proving. Hosts pick the source account from the recorded index, and a host-restored placeholder 0 would otherwise spend identity 0's tip pool on another identity's behalf. An empty slot (a retired identity's account) stays spendable. Tests: a deterministic interleaving (preparation paused after its snapshot while another task binds accounts 0 and 1) keeps account 1 bound and registered, and fails without the lock; the placeholder-index regression now also covers spending from identity 0's tip account. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Static verification confirms the original concurrent-bind race and buffered identity-index merge defect are fixed, and the reported occupied-placeholder spending case is now rejected before recipient resolution. Six client-side suggestions remain concerning restart recovery, stale source-account selection, identity replay, account preservation, and test isolation; no consensus-blocking defect was confirmed. No builds or tests were run, and the supplied exact-head CI snapshot still had most validation queued or pending.
🟡 6 suggestion(s)
1 finding(s) not shown inline (the lines are not part of this PR's diff)
🟡 Suggestion: Serialize shielded_add_account with the new shielded config lock
packages/rs-platform-wallet/src/wallet/platform_wallet.rs:1154-1160
Tip preparation now holds shielded_config_lock across its account snapshot and replacement bind, but shielded_add_account writes the same shielded_keys map without participating in that lock. Preparation can snapshot [0], an addition can persist and insert ordinary account 1, and preparation can then install its stale snapshot plus the tip account, overwriting the completed insertion while leaving its viewing-key row persisted. This breaks the new preparation helper's ordinary-account preservation guarantee even though the concurrent-bind case is fixed. Acquire the same configuration lock for the complete add operation, preserving the existing rule that host callbacks run outside the key-slot guard. The add API intentionally does not register accounts with the coordinator itself, so this finding concerns loss of its in-memory insertion, not a claim that it creates a coordinator registration. Extend the interleaving coverage to race preparation with this writer.
source: muse-spark-1.3-contributor (phase1-reviewer: rust-quality)
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
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) — This large, intricate diff directly changes funds movement and dedicated ZIP-32 key/account handling in packages/rs-platform-wallet/src/wallet/shielded/tips.rs and packages/rs-platform-wallet-ffi/src/shielded_send.rs, as well as persisted-data migrations. - 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 below 15% reserve: weekly 15% left, 5h 100% left),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/DashPayTabScreen.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayTabScreen.kt:336-345: Invalidate and reverify the source account when an identity is re-slotted
The lookup keys include the wallet wrapper, manager, and identity ID, but not the identity's placement. This PR allows discovery to move that same identity from placeholder index 0 to verified index 5 without replacing those objects. If discovery completes while this composition is alive, the cached result remains `0x40000000` and is still offered as that identity's dedicated source account. A blocking discovery can continue natively after its search screen is dismissed, so returning to DashPay before it finishes can expose this interleaving. The new Rust check does not reject the stale source: `verify_tip_account_owner` succeeds when slot 0 has become empty, and also succeeds if the rightful identity 0 later occupies it. A confirmed send can thus spend the retained account-0 funds rather than the selected identity's account-5 funds. Invalidate the lookup when verified placement changes and recheck the selected source identity/account association at submission; checking only the account's current occupant is insufficient. Add a regression resolving at placeholder 0, adopting into 5, and attempting a send with the old selection. The profile-screen lookup also needs placement-change invalidation.
In `packages/rs-platform-wallet/src/wallet/identity/state/manager/lifecycle.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/state/manager/lifecycle.rs:195-207: Carry identity relocation through changeset replay
Adoption now changes bucket placement and identity coordinates, but `IdentityManager::apply_identity_entry` still updates an existing identity in place and returns without applying `wallet_id`, `identity_index`, or `location_index`. Replaying valid snapshots A@0, then adopted A@5, then newly admitted B@0 through the public `PlatformWallet::apply` or `PlatformWalletInfo::apply_changeset` API leaves A at slot 0 until B's fresh insertion overwrites it. A's state is lost and its reverse-index entry still points to slot 0, so `identity(A)` returns B. Promotion from the observed bucket likewise fails to reproduce the adoption. Normal startup reconstruction uses a different path, which limits the current impact, but the public replay API no longer reproduces the mutations introduced here. Apply relocation while preserving managed state and reverse-index consistency, reject unrelated occupied targets, and handle merged batches independently of identity-ID ordering when one entry frees another's slot. Add round-trip coverage for observed-identity promotion and A@0 → A@5 followed by B@0.
In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/identity/SearchWalletsForIdentitiesScreen.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/identity/SearchWalletsForIdentitiesScreen.kt:147-151: Preserve ordinary accounts during discovery-triggered rebinding
This new refresh calls the replacement bind API with its default account list `[0]`. For a wallet already bound to ordinary accounts `[0, 1]`, Rust adds discovered and persisted tip accounts but does not retain omitted ordinary account 1. `NetworkShieldedCoordinator::register_locked` consequently removes account 1's registration and purges its live notes and watermark, so discovering identities unexpectedly stops an existing ordinary account from scanning. Swift preserves a snapshot of ordinary accounts, but its snapshot and replacement call are also outside the native configuration lock. Provide a refresh operation that reads the current configuration and adds discovered tip accounts atomically under `shielded_config_lock`, and use it from discovery rather than treating `[0]` as a complete replacement configuration. Keep full bind available for intentional replacement. Cover discovery refresh starting from `[0, 1]` and verify that both ordinary registrations survive.
In `packages/rs-platform-wallet/src/wallet/platform_wallet.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/platform_wallet.rs:1154-1160: Serialize shielded_add_account with the new shielded config lock
Tip preparation now holds `shielded_config_lock` across its account snapshot and replacement bind, but `shielded_add_account` writes the same `shielded_keys` map without participating in that lock. Preparation can snapshot `[0]`, an addition can persist and insert ordinary account 1, and preparation can then install its stale snapshot plus the tip account, overwriting the completed insertion while leaving its viewing-key row persisted. This breaks the new preparation helper's ordinary-account preservation guarantee even though the concurrent-bind case is fixed. Acquire the same configuration lock for the complete add operation, preserving the existing rule that host callbacks run outside the key-slot guard. The add API intentionally does not register accounts with the coordinator itself, so this finding concerns loss of its in-memory insertion, not a claim that it creates a coordinator registration. Extend the interleaving coverage to race preparation with this writer.
In `packages/rs-platform-wallet/src/wallet/shielded/tips.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/tips.rs:258-263: Isolate the preparation pause hook by wallet instance
The global pause hook is consumed using only the deterministic wallet ID. The concurrency regression, placeholder-index regression, and discovery/restoration regression all create independent testnet wallet instances from `MESSAGE_SIGNING_TEST_MNEMONIC`, so they share that ID. Under parallel execution, another test's preparation can remove the concurrency test's hook and signal `reached` while holding its own configuration lock. The intended preparation then runs without pausing, allowing the competing bind to complete inside the timeout and failing the regression despite correct production locking. Attach the hook to shared state belonging to the actual wallet instance, use an instance-specific token, or give the concurrency fixture a distinct deterministic mnemonic. The current wallet-ID key does not provide the parallel-test isolation claimed by the comment.
In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift:28-32: Restore outstanding tip submissions before enabling another send
(existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4185823786)
The shared owner preserves the submission guard across sheet dismissal, but a new process reconstructs an empty wallet map and a `.ready` submission. Kotlin has the same lifecycle. After termination during an accepted-but-unconfirmed send, reopening therefore removes the uncertainty warning and permits the user to confirm the same payment again. Native pending activity and redrive records do not prevent this: note reservations exclude particular inputs, while `reserve_unspent_notes` can select other funded notes for a fresh transfer. The redrive transaction itself is armed only after an ambiguous broadcast result returns, so process loss during the call also cannot be treated as a recovered safe retry. Persist a network/wallet-scoped unresolved intent before entering native spending, restore its warning before enabling confirmation, and reconcile native activity or require explicit acknowledgment that another submission is a separate payment. Add process-recreation tests using newly constructed owners and sufficient independent notes to fund both attempts. The PR's pending maintainer decision is not an implemented recovery boundary.
| val tipAccountKey = listOf(managed, tipManager, identityHex) | ||
| val taggedTipAccount by produceState<Pair<List<Any?>, Result<Int>>?>( | ||
| initialValue = null, managed, tipManager, identityHex, | ||
| ) { | ||
| value = tipAccountKey to runCatching { | ||
| val index = requireNotNull(managed) { "Wallet is not loaded" } | ||
| .identityIndex(identity.identityId) | ||
| ?: error("Tip account requires a recoverable identity index") | ||
| requireNotNull(tipManager).shieldedTipAccountIndex(index) | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: Invalidate and reverify the source account when an identity is re-slotted
The lookup keys include the wallet wrapper, manager, and identity ID, but not the identity's placement. This PR allows discovery to move that same identity from placeholder index 0 to verified index 5 without replacing those objects. If discovery completes while this composition is alive, the cached result remains 0x40000000 and is still offered as that identity's dedicated source account. A blocking discovery can continue natively after its search screen is dismissed, so returning to DashPay before it finishes can expose this interleaving. The new Rust check does not reject the stale source: verify_tip_account_owner succeeds when slot 0 has become empty, and also succeeds if the rightful identity 0 later occupies it. A confirmed send can thus spend the retained account-0 funds rather than the selected identity's account-5 funds. Invalidate the lookup when verified placement changes and recheck the selected source identity/account association at submission; checking only the account's current occupant is insufficient. Add a regression resolving at placeholder 0, adopting into 5, and attempting a send with the old selection. The profile-screen lookup also needs placement-change invalidation.
source: gpt-6.1-sol (phase2-reviewer: general)
There was a problem hiding this comment.
Partly valid; fixed in 257fa0e. The lookup keys didn't include placement, so a re-slot while the screen was open left the old account cached. Both the DashPay tab and the profile lookup now also key on Room's identityIndex, which the persistence callback rewrites on a re-slot, so the lookup reruns against the live index. The tip sheet reads the account when you confirm, so it gets the corrected one.
We're not adding a separate check at submission:
- A placeholder slot-0 spend while slot 0 is occupied is already refused by
verify_tip_account_owner. - Once the re-slot empties slot 0,
0x40000000is this wallet's own unowned (retired) tip account. Spending from it is allowed by design, and it doesn't touch another identity's funds.
🤖 Posted autonomously by Claude on behalf of pasta.
| self.wallet_identities | ||
| .entry(wallet_id) | ||
| .or_default() | ||
| .insert(identity_index, managed); | ||
| self.location_index_insert(*identity_id, target); | ||
| } | ||
| if let Some(managed) = self | ||
| .wallet_identities | ||
| .get_mut(&wallet_id) | ||
| .and_then(|bucket| bucket.get_mut(&identity_index)) | ||
| { | ||
| managed.wallet_id = Some(wallet_id); | ||
| managed.identity_index = Some(identity_index); |
There was a problem hiding this comment.
🟡 Suggestion: Carry identity relocation through changeset replay
Adoption now changes bucket placement and identity coordinates, but IdentityManager::apply_identity_entry still updates an existing identity in place and returns without applying wallet_id, identity_index, or location_index. Replaying valid snapshots A@0, then adopted A@5, then newly admitted B@0 through the public PlatformWallet::apply or PlatformWalletInfo::apply_changeset API leaves A at slot 0 until B's fresh insertion overwrites it. A's state is lost and its reverse-index entry still points to slot 0, so identity(A) returns B. Promotion from the observed bucket likewise fails to reproduce the adoption. Normal startup reconstruction uses a different path, which limits the current impact, but the public replay API no longer reproduces the mutations introduced here. Apply relocation while preserving managed state and reverse-index consistency, reject unrelated occupied targets, and handle merged batches independently of identity-ID ordering when one entry frees another's slot. Add round-trip coverage for observed-identity promotion and A@0 → A@5 followed by B@0.
source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality); gpt-6.1-sol (phase2-reviewer: rust-quality)
There was a problem hiding this comment.
Confirmed and fixed in 93d4d3b. Production restore doesn't go through this API, but replay should still reproduce the re-slot. Changes:
apply_identity_entrymoves an existing identity to the(wallet_id, identity_index)its entry carries, viaadopt_into_wallet, which keeps the reverse index in lockstep. If another identity still holds that slot, it logs and updates in place.- A fresh entry whose slot is occupied is skipped instead of overwriting the occupant.
apply_changesetapplies batches order-independently throughapply_identity_entries. Pure removals run first. Known identities run next, each deferred until a blocker moves out (a cycle falls back to an in-place update). New identities are inserted last. Removal still wins for an id that is both snapshotted and removed.
New tests in wallet/apply.rs:
- A@0 → A@5 → B@0, applied one at a time and as a merged batch that visits B first
- an observed identity promoted into the wallet
- an occupied-slot insert being refused
- a same-batch removal freeing a slot
🤖 Posted autonomously by Claude on behalf of pasta.
| // Rust adds tip accounts itself, so the default [0] request | ||
| // suffices; a bind failure stays non-fatal. | ||
| if (container.shieldedService.isAvailable) { | ||
| try { | ||
| mgr.bindShielded(wallet.walletId) |
There was a problem hiding this comment.
🟡 Suggestion: Preserve ordinary accounts during discovery-triggered rebinding
This new refresh calls the replacement bind API with its default account list [0]. For a wallet already bound to ordinary accounts [0, 1], Rust adds discovered and persisted tip accounts but does not retain omitted ordinary account 1. NetworkShieldedCoordinator::register_locked consequently removes account 1's registration and purges its live notes and watermark, so discovering identities unexpectedly stops an existing ordinary account from scanning. Swift preserves a snapshot of ordinary accounts, but its snapshot and replacement call are also outside the native configuration lock. Provide a refresh operation that reads the current configuration and adds discovered tip accounts atomically under shielded_config_lock, and use it from discovery rather than treating [0] as a complete replacement configuration. Keep full bind available for intentional replacement. Cover discovery refresh starting from [0, 1] and verify that both ordinary registrations survive.
source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, platform-versioning, rust-quality, security-auditor)
There was a problem hiding this comment.
Doesn't apply to the Kotlin app: it only ever binds ordinary account 0. These bind paths all request the default [0], and nothing in the Kotlin app or SDK adds other ordinary accounts:
AppContainerShieldedService.bind/bindEngine/clearLocalStatePlatformWalletManager.bindShielded- this discovery rebind
So the rebind can't drop an ordinary account, and Rust keeps the discovered and persisted tip accounts. Mirroring Swift's snapshot would need a new JNI binding for shielded_account_indices that nothing in Kotlin needs yet, so that's out of scope here.
🤖 Posted autonomously by Claude on behalf of pasta.
| pub(crate) async fn after_snapshot(wallet: [u8; 32]) { | ||
| let hook = AFTER_SNAPSHOT | ||
| .lock() | ||
| .unwrap() | ||
| .as_mut() | ||
| .and_then(|hooks| hooks.remove(&wallet)); |
There was a problem hiding this comment.
🟡 Suggestion: Isolate the preparation pause hook by wallet instance
The global pause hook is consumed using only the deterministic wallet ID. The concurrency regression, placeholder-index regression, and discovery/restoration regression all create independent testnet wallet instances from MESSAGE_SIGNING_TEST_MNEMONIC, so they share that ID. Under parallel execution, another test's preparation can remove the concurrency test's hook and signal reached while holding its own configuration lock. The intended preparation then runs without pausing, allowing the competing bind to complete inside the timeout and failing the regression despite correct production locking. Attach the hook to shared state belonging to the actual wallet instance, use an instance-specific token, or give the concurrency fixture a distinct deterministic mnemonic. The current wallet-ID key does not provide the parallel-test isolation claimed by the comment.
source: gpt-6.1-sol (phase2-reviewer: rust-quality)
There was a problem hiding this comment.
Confirmed and fixed in 93d4d3b. The pause hook is now keyed by wallet instance (Arc::as_ptr of its shielded_config_lock, which clones share), not by the deterministic wallet id. Tests built from the same mnemonic no longer share a hook. The concurrency regressions now share one tip_race_fixture / race_tip_preparation helper.
🤖 Posted autonomously by Claude on behalf of pasta.
…lots - `shielded_add_account` takes `shielded_config_lock`, so tip preparation's snapshot-then-rebind can no longer drop an account added in between. - Changeset replay follows identity re-slotting: an existing identity moves to the slot its entry carries, batches apply moves before new inserts and removals before both, and a new entry never overwrites an occupied slot. - The tip-preparation test pause is keyed by wallet instance rather than wallet id, since tests sharing a mnemonic share the id. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s re-slotted Discovery moves an identity from a placeholder index to its verified one without replacing the wallet or manager objects the lookups were keyed on, so the DashPay tab and profile kept the old tip account. Key them on Room's identity index too, which the persistence callback rewrites. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Re: the review's non-inline finding "Serialize shielded_add_account with the new shielded config lock" ( Confirmed and fixed in 93d4d3b. The new regression 🤖 Posted autonomously by Claude on behalf of pasta. |
|
Waiting for bot review — coderabbitai not yet · thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
`c_char` is `u8` on aarch64 Linux, where `as *mut u8` trips clippy::unnecessary_cast; `cast::<u8>()` compiles cleanly on every target. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Waiting for bot review — coderabbitai not yet · thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
Issue being fixed or feature implemented
Let a DashPay user publish a reusable shielded tip address and receive payments by username. Wallet-generated addresses use a dedicated ZIP-32 account per identity, separating tip viewing keys from ordinary wallet activity. Users can also publish an externally generated Orchard address.
This follows the transparent profile-address work in #4380 and the 43-byte format in dashpay/dips#188.
Split (2026-09-16): the contract field shipped separately in #4768 and the DPNS resolver fix in #4769; both are in
v5.1-dev. This PR now carries only the wallet, persistence, native bindings, and Swift/Kotlin example flows, and targetsv5.1-dev(milestone v5.1.0). Nothing that remains here is consensus code.What was done?
profile.shieldedAddresson DashPay v2 at position 7, 43 raw bytes including the full diversifier; consensus enforces the byte length, clients validate the Orchard decoding) landed in feat(dpp)!: dashpay profile shielded address field #4768 together with the protocol 13 to 14 upgrade tests and the fee/root fixtures. Since the 2026-10-05 merge ofv5.1-devthose files are no longer in this diff.0x40000000 + index. Preparation verifies the seed, binds viewing keys, and flushes persistence before returning an address. Identity discovery reconstructs the account without relying on the current profile; retired tip accounts keep scanning. Ordinary balance/spending choices exclude tip accounts, with explicit access to tip funds.How Has This Been Tested?
Local macOS builds and targeted validation:
StateFlow.valuecomposition finding inSyncStatusScreen.kt:215.git diff --checkpasses. Storage explorer coverage passes for all 36 models, with a negative fixture verifying inherited-model omissions are detected.GitHub skips Rust and mobile runner jobs for this fork under the existing workflow trust policy; the results above are local validation.
Not exercised: a live funded network tip transaction, Android NDK/native
.sobuild, or Android device/instrumented execution. Full network end-to-end validation remains a release validation step.Breaking Changes
Client-side only: there is no consensus or protocol change here (the protocol-14 DashPay v2 contract bytes and their fee/root baselines shipped in #4768). The client changes are breaking:
#[repr(C)]restore structsIdentityEntryFFIandContactProfileRowFFIgained the three payment addresses and their presence flags (IdentityEntryFFIgrows from 224 to 312 bytes), and the native profile/update structs widened too. Every C ABI consumer, the Swift xcframework, and the Android JNI library must be rebuilt together with the host code; mixing old and new binaries misreads these structs. The existing FFI profile setter stays as a wrapper with keep-address semantics.DashPayProfile, the profile patch types) gained fields.Checklist:
For repository code-owners and collaborators only
This pull request was created by Codex.
Fix-up (2026-09-16, maintainer push)
Merged
v4.2-dev(ffb6e53f20) into the branch and addressed the open review threads; head = merge47e9e1bc95+a637a0e2bd.v4.2-devalready ships V008 to V018. Theentry_format/profile_formatstamps nowDEFAULT 1and the migration marks the rows it finds0, so only pre-V019 rows are decoded as the legacy shape; both identity readers on the hard-delete schema dispatch on the stamp.tests/sqlite_profile_address_encoding.rswalks a V007 database through the whole chain. SCHEMA.md documents both columns.scripts/freeze_schema_models.py, FREEZES row at787cac09e7) instead of a hand-written copy;DashSchemaV5addsPersistentDashpayPaymentAddresses;dash-v5.storewritten by this build for the fixture-based hash test from feat(swift-sdk): generate the frozen SwiftData schema models, and guard them with real stores #4644.catch_query_panicaround the non-spending tip exports plus an entry-point-level panic regression test; app-ownedShieldedTipSubmissionson iOS (parity with Android); Kotlin tip account derived from the live identity index rather than Room's placeholder 0.Verified locally: storage crate suite, wallet tip/profile/bind tests (87), FFI panic tests, Drive check-tx/document/root-hash tests, Swift package persistence tests (18) and the example app build with warnings as errors, Kotlin SDK/app compile and tip unit tests.
Update (2026-10-05, maintainer push)
Merged
v5.1-dev(1ebcedb028): the #4768/#4769 copies dropped out (143 to 99 files). Persistence numbering moved to follow the base:v5.1-devships versions 12 to 14, so the profile address columns are nowMIGRATION_14_15(database version 15,15.json); migration tests walk 14 to 15 and 10 to 15.v5.1-devreplaced the development V4/V5 chain with App Store release snapshots (feat(swift-sdk)!: freeze schemas only after App Store publication #4818, fix(swift-sdk)!: restore historical V2 migration to live V3 #4910) and published schema 3.0.0. The hand-registered V4 freeze and V5 from the earlier fix-up are gone:DashSchemaV3is bound to its generated release snapshot and a new liveDashSchemaV4addsPersistentDashpayPaymentAddresses(V2 to V3 to V4, and V1 to V3 to V4). SCHEMA_RELEASES.md lists the routes.Also addressed in the remaining review threads: the tip resolver releases the wallet registry lock before its network lookups; Kotlin ties the tip account to the identity it was resolved for; Swift identity deletion paths remove payment-address rows; literal-bytes coverage for the pre-V019 profile record.
🤖 Generated with Claude Code
PR Hygiene ·
6c88c31/skip-botsproceeds without the ones not yet reported/self-reviewedkotlin-sdk(packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/di/AppContainer.kt,packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayJson.kt,packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayProfileScreen.ktand 28 more) — HashEngineeringrs-platform-wallet-ffi(packages/rs-platform-wallet-ffi/src/dashpay_profile.rs,packages/rs-platform-wallet-ffi/src/identity_persistence.rs,packages/rs-platform-wallet-ffi/src/persistence.rsand 3 more) — HashEngineering or ZocoLini or llbartekll or romchornyiwallet-storage(packages/rs-platform-wallet-storage/SCHEMA.md,packages/rs-platform-wallet-storage/migrations/V019__profile_address_encoding.rs,packages/rs-platform-wallet-storage/src/sqlite/mod.rsand 9 more) — lklimekrs-platform-wallet(packages/rs-platform-wallet/docs/SHIELDED_TIPS.md,packages/rs-platform-wallet/src/changeset/changeset.rs,packages/rs-platform-wallet/src/error.rsand 18 more) — HashEngineering or ZocoLini or llbartekll or romchornyipackages/rs-unified-sdk-jni/src/dashpay.rs,packages/rs-unified-sdk-jni/src/funding.rs,packages/rs-unified-sdk-jni/src/persistence.rsand 2 more) — QuantumExplorer or shumkovswift-sdk(packages/swift-sdk/SCHEMA_RELEASES.md,packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashLegacyStoreSQLite.swift,packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swiftand 35 more) — llbartekll or romchornyiWhen every box is checked the
PR Hygienecheck passes and this can merge.