fix(platform-wallet): address sync review feedback for #3990#3991
fix(platform-wallet): address sync review feedback for #3990#3991thepastaclaw wants to merge 3 commits into
Conversation
- commit_reconciliation: drop cross-pool index conflicts before mutating the found seed or emitting the entry - file store: purge shielded_pending_spends rows on wallet / all-subwallet purge; mirror redrive-row deletion on mark_spent for any known nullifier - SubwalletState::mark_spent: always clear pending/redrive for a known nullifier, returning true only on the first spent transition - anchor probe: report depth>0 store errors as a distinct skipped outcome instead of conflating them with an uncoverable note - register_from_addresses: best-effort local add_identity so a local persistence failure cannot suppress the accepted registration result - SwiftExampleApp: generation-token invalidation of in-flight balance snapshot refreshes during Clear, plus isClearing gating on the Sync Now / Clear buttons Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR hardens durability across rs-platform-wallet: identity registration persistence becomes best-effort, address reconciliation now reports whether persistence succeeded and gates asset-lock consumption and provider bijection updates on it, and shielded store/operations logic distinguishes store errors from unwitnessable notes and fixes SQLite purge/mark-spent consistency. Swift example app clear flow adds race-prevention state. ChangesRust Platform Wallet Durability and Shielded Store Fixes
Estimated code review effort: 4 (Complex) | ~60 minutes Swift Example App Clear-Sync Race Fix
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant CoreContentView
participant PlatformBalanceSyncService
User->>CoreContentView: tap Clear
CoreContentView->>PlatformBalanceSyncService: clearLocalState()
PlatformBalanceSyncService->>PlatformBalanceSyncService: bump refreshGeneration, isClearing=true
PlatformBalanceSyncService->>PlatformBalanceSyncService: in-flight refreshBalanceSnapshot captures old generation
PlatformBalanceSyncService-->>PlatformBalanceSyncService: wipe completes, defer bumps generation, isClearing=false
PlatformBalanceSyncService->>PlatformBalanceSyncService: stale result generation mismatch -> discarded
sequenceDiagram
participant FundFromAssetLock
participant PlatformAddressWallet
participant Persister
FundFromAssetLock->>PlatformAddressWallet: reconcile_address_infos_with_persistence()
PlatformAddressWallet->>Persister: store(entries)
Persister-->>PlatformAddressWallet: success or error
PlatformAddressWallet-->>FundFromAssetLock: (changeset, persisted)
alt tracked_out_point exists and not persisted
FundFromAssetLock-->>FundFromAssetLock: log error, return Ok(changeset), skip consume_asset_lock
else persisted or no tracked_out_point
FundFromAssetLock->>FundFromAssetLock: consume_asset_lock
end
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Review complete (commit cd9bf65) |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/rs-platform-wallet/src/wallet/shielded/operations.rs`:
- Around line 1786-1832: The new ShieldedMerkleWitnessUnavailable probe failure
is currently being flattened into the generic wallet-operation error path, which
hides its retryable/non-broadcast meaning. Update the FFI handling in
shielded_send.rs to add a dedicated host result code for this error and map
PlatformWalletError::ShieldedMerkleWitnessUnavailable explicitly instead of
routing it through ErrorWalletOperation, preserving the distinct caller
behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 774ebae4-08b8-4566-9239-06b6a023aba0
📒 Files selected for processing (9)
packages/rs-platform-wallet/src/wallet/identity/network/register_from_addresses.rspackages/rs-platform-wallet/src/wallet/platform_addresses/fund_from_asset_lock.rspackages/rs-platform-wallet/src/wallet/platform_addresses/provider.rspackages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rspackages/rs-platform-wallet/src/wallet/shielded/file_store.rspackages/rs-platform-wallet/src/wallet/shielded/operations.rspackages/rs-platform-wallet/src/wallet/shielded/store.rspackages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/PlatformBalanceSyncService.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift
| Ok(DepthProbe::Built(spends, anchor)) | ||
| }; | ||
|
|
||
| // Fast path: a fully-synced wallet's depth-0 root is a recorded anchor. | ||
| let (spends, anchor) = match build_at_depth(0, true)? { | ||
| Some(pair) => pair, | ||
| // Unreachable — a strict build returns `Some` or errors — but stay | ||
| let DepthProbe::Built(spends, anchor) = build_at_depth(0, true)? else { | ||
| // Unreachable — a strict build returns `Built` or errors — but stay | ||
| // fund-safe (a clean error, never a panic) if that invariant breaks. | ||
| None => { | ||
| return Err(PlatformWalletError::ShieldedMerkleWitnessUnavailable( | ||
| "depth-0 witness probe returned no witness for a selected note".to_string(), | ||
| )); | ||
| } | ||
| return Err(PlatformWalletError::ShieldedMerkleWitnessUnavailable( | ||
| "depth-0 witness probe returned no witness for a selected note".to_string(), | ||
| )); | ||
| }; | ||
| if recorded.contains(&anchor.to_bytes()) { | ||
| return Ok((spends, anchor)); | ||
| } | ||
|
|
||
| // Otherwise walk older checkpoints newest→oldest for the shallowest | ||
| // recorded root. | ||
| let mut skipped_store_error: Option<String> = None; | ||
| for depth in 1..MAX_ANCHOR_PROBE_DEPTH { | ||
| match build_at_depth(depth, false)? { | ||
| Some((spends, anchor)) if recorded.contains(&anchor.to_bytes()) => { | ||
| DepthProbe::Built(spends, anchor) if recorded.contains(&anchor.to_bytes()) => { | ||
| return Ok((spends, anchor)); | ||
| } | ||
| // A root exists at this depth but Platform didn't record it — try an | ||
| // older checkpoint. | ||
| Some(_) => continue, | ||
| DepthProbe::Built(..) => continue, | ||
| // A selected note isn't witnessable this deep; every deeper | ||
| // checkpoint is older still, so none can cover it either. | ||
| None => break, | ||
| DepthProbe::NoteNotCovered => break, | ||
| // A store failure only invalidates this one depth's probe — a | ||
| // deeper checkpoint may still cover the notes, so keep walking, | ||
| // but remember the failure so an exhausted probe surfaces it | ||
| // instead of the misleading "no recorded anchor" outcome. | ||
| DepthProbe::StoreError(e) => { | ||
| skipped_store_error = Some(e); | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if let Some(e) = skipped_store_error { | ||
| return Err(PlatformWalletError::ShieldedMerkleWitnessUnavailable( | ||
| format!( | ||
| "anchor probe skipped checkpoint depths on a store failure (last: {e}); retry the spend" | ||
| ), | ||
| )); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect FFI mapping + error-code docs for ShieldedMerkleWitnessUnavailable
rg -nP -C4 'ShieldedMerkleWitnessUnavailable' packages/rs-platform-wallet-ffi/src
echo '--- error code enum ---'
rg -nP -C2 'ErrorShielded' packages/rs-platform-wallet-ffi/src/error.rsRepository: dashpay/platform
Length of output: 3534
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate ShieldedMerkleWitnessUnavailable =="
rg -n -C3 'ShieldedMerkleWitnessUnavailable' packages/rs-platform-wallet-ffi/src packages/rs-platform-wallet/src || true
echo
echo "== inspect FFI error code enum and mapping =="
sed -n '1,320p' packages/rs-platform-wallet-ffi/src/error.rs
echo
echo "== inspect core error definition =="
rg -n -C3 'enum PlatformWalletError|ShieldedMerkleWitnessUnavailable' packages/rs-platform-wallet/src || trueRepository: dashpay/platform
Length of output: 22563
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== search FFI shielded send path =="
rg -n -C4 'ShieldedMerkleWitnessUnavailable|ShieldedNoRecordedAnchor|ErrorShieldedNoRecordedAnchor|platform_wallet_manager_shielded_identity_create_from_pool|shielded_send' packages/rs-platform-wallet-ffi/src || trueRepository: dashpay/platform
Length of output: 11551
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== inspect shielded send conversion path =="
sed -n '460,540p' packages/rs-platform-wallet-ffi/src/shielded_send.rs
echo
echo "== inspect identity-create conversion path =="
sed -n '660,730p' packages/rs-platform-wallet-ffi/src/shielded_send.rs
echo
echo "== grep for generic From<PlatformWalletError> use in shielded_send.rs =="
rg -n -C2 'PlatformWalletFFIResult::from\(e\)|into\(\)|ErrorUnknown|ShieldedMerkleWitnessUnavailable' packages/rs-platform-wallet-ffi/src/shielded_send.rsRepository: dashpay/platform
Length of output: 7717
Add a dedicated FFI mapping for ShieldedMerkleWitnessUnavailable
packages/rs-platform-wallet-ffi/src/shielded_send.rs currently routes this new probe failure through the generic ErrorWalletOperation path, so callers lose the retryable/non-broadcast distinction. Add a specific host result code for it instead of flattening it into the generic wallet error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/rs-platform-wallet/src/wallet/shielded/operations.rs` around lines
1786 - 1832, The new ShieldedMerkleWitnessUnavailable probe failure is currently
being flattened into the generic wallet-operation error path, which hides its
retryable/non-broadcast meaning. Update the FFI handling in shielded_send.rs to
add a dedicated host result code for this error and map
PlatformWalletError::ShieldedMerkleWitnessUnavailable explicitly instead of
routing it through ErrorWalletOperation, preserving the distinct caller
behavior.
There was a problem hiding this comment.
Code Review
Follow-up PR that lands six focused fixes on top of #3990: address-reconciliation conflict handling, shielded pending-spend cleanup on purge/already-spent transitions, anchor-probe error discrimination, best-effort local identity persistence, persist-before-consume ordering for asset-lock funding, and a Swift Clear/Sync race guarded by a generation token. Changes are internally consistent with existing lock ordering and error-handling conventions in this codebase. One in-scope suggestion around the new conflict guard's handling of zero-balance (removal) entries, plus one test-coverage note for the new guard branch.
🟡 2 suggestion(s)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs:1002-1033: Index-conflict guard drops removal entries too, leaving stale balances that can resurrect after restart
The new guard at provider.rs:1008-1020 `continue`s past *everything* on conflict — the `is_removal` branch that would call `state.found.remove(&entry.address)`, and the `outcome.entries.push(entry)` that would emit a durable zero-out to the persister. It fires whenever `entry.address` is not in the bijection but `entry.address_index` already maps to a different address (the drift condition).
For the non-removal (credit) branch this is correct: an orphaned `found` insert with no bijection pairing is invisible via `current_balances()` (which filters on the bijection), so dropping it changes nothing observable.
For the `is_removal` branch it is not: `found.remove` never needed a pairing to be correct, and `PlatformAddressWallet::initialize_from_persisted` (wallet.rs:144-146) re-seeds `account.address_credit_balance` directly from persisted `found`, bypassing the bijection filter. So if a stale non-zero row already exists on disk for `entry.address` from before the pairing drift, a proof-attested zero-balance entry for that same address is silently discarded — the disk row survives, and the next app restart resurrects a balance Platform has already proven is gone.
A closely related consequence: when every resolved entry is dropped by this guard, `outcome.entries` is empty but `outcome.resolved > 0`, so `reconcile_address_infos_with_persistence` returns `(default, true)` via the 'nothing new to store' branch (wallet.rs:311-315). `fund_from_asset_lock` then reads `persisted=true` and proceeds to `consume_asset_lock` — exactly the ordering the PR is trying to prevent (Consumed lock paired with un-updated durable rows).
Both issues share the same root cause: the guard is placed too broadly. Gating only the bijection insert (or splitting into `is_removal` / non-removal branches) preserves the guard's intent — never clobber another pairing — while restoring correct emission of removals and making the `persisted=true` path only claim persistence when there was genuinely nothing to write.
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs:1002-1033: No regression test for the new index-conflict guard branch
The existing `commit_reconciliation_*` tests do not construct the state-drift precondition the new guard targets (an `address_index` in the bijection paired with a *different* address than the entry's). A targeted test — especially one combining that precondition with a zero-balance removal entry — would both pin the guard's intent and would have caught the removal-branch behavior flagged above. Worth adding one credit-side and one removal-side case.
Source: reviewers: opus (general), claude-sonnet-5 (general), gpt-5.5[high] (general, failed_or_unparseable), opus (security-auditor), claude-sonnet-5 (security-auditor), gpt-5.5[high] (security-auditor, failed_or_unparseable), opus (rust-quality), claude-sonnet-5 (rust-quality), gpt-5.5[high] (rust-quality, failed_or_unparseable), opus (ffi-engineer), claude-sonnet-5 (ffi-engineer), gpt-5.5[high] (ffi-engineer, failed_or_unparseable); verifier: opus; specialists: security-auditor, rust-quality, ffi-engineer
| // Drop the entry outright when its derivation index is | ||
| // already paired with a DIFFERENT address — committing it | ||
| // would either evict that pairing (`BiBTreeMap::insert` | ||
| // drops conflicting pairs, orphaning the other address's | ||
| // `found` entry) or persist an address `current_balances` | ||
| // can't round-trip back out of the committed seed. | ||
| if state.addresses.get_by_right(&entry.address).is_none() | ||
| && state.addresses.contains_left(&entry.address_index) | ||
| { | ||
| tracing::error!( | ||
| account_index = entry.account_index, | ||
| address_index = entry.address_index, | ||
| address = %entry.address, | ||
| "commit_reconciliation: derivation index already \ | ||
| maps to a different address — state drift; \ | ||
| dropping the reconciliation entry" | ||
| ); | ||
| continue; | ||
| } | ||
| if is_removal { | ||
| state.found.remove(&entry.address); | ||
| } else { | ||
| state.found.insert(entry.address, entry.funds); | ||
| } | ||
| // Merge pool-resolved addresses into the bijection so | ||
| // `current_balances` can pair the fresh funds with a | ||
| // derivation index. Never overwrite an existing pairing — | ||
| // `BiBTreeMap::insert` evicts conflicting pairs, which | ||
| // would orphan another address's `found` entry. | ||
| // derivation index. The conflict guard above already | ||
| // ensured this never overwrites an existing pairing. | ||
| if state.addresses.get_by_right(&entry.address).is_none() { | ||
| if state.addresses.contains_left(&entry.address_index) { | ||
| tracing::error!( | ||
| account_index = entry.account_index, | ||
| address_index = entry.address_index, | ||
| address = %entry.address, | ||
| "commit_reconciliation: derivation index already \ | ||
| maps to a different address — state drift; \ | ||
| leaving the bijection untouched" | ||
| ); | ||
| } else { | ||
| state.addresses.insert(entry.address_index, entry.address); | ||
| } | ||
| state.addresses.insert(entry.address_index, entry.address); | ||
| } | ||
| outcome.entries.push(entry); |
There was a problem hiding this comment.
🟡 Suggestion: Index-conflict guard drops removal entries too, leaving stale balances that can resurrect after restart
The new guard at provider.rs:1008-1020 continues past everything on conflict — the is_removal branch that would call state.found.remove(&entry.address), and the outcome.entries.push(entry) that would emit a durable zero-out to the persister. It fires whenever entry.address is not in the bijection but entry.address_index already maps to a different address (the drift condition).
For the non-removal (credit) branch this is correct: an orphaned found insert with no bijection pairing is invisible via current_balances() (which filters on the bijection), so dropping it changes nothing observable.
For the is_removal branch it is not: found.remove never needed a pairing to be correct, and PlatformAddressWallet::initialize_from_persisted (wallet.rs:144-146) re-seeds account.address_credit_balance directly from persisted found, bypassing the bijection filter. So if a stale non-zero row already exists on disk for entry.address from before the pairing drift, a proof-attested zero-balance entry for that same address is silently discarded — the disk row survives, and the next app restart resurrects a balance Platform has already proven is gone.
A closely related consequence: when every resolved entry is dropped by this guard, outcome.entries is empty but outcome.resolved > 0, so reconcile_address_infos_with_persistence returns (default, true) via the 'nothing new to store' branch (wallet.rs:311-315). fund_from_asset_lock then reads persisted=true and proceeds to consume_asset_lock — exactly the ordering the PR is trying to prevent (Consumed lock paired with un-updated durable rows).
Both issues share the same root cause: the guard is placed too broadly. Gating only the bijection insert (or splitting into is_removal / non-removal branches) preserves the guard's intent — never clobber another pairing — while restoring correct emission of removals and making the persisted=true path only claim persistence when there was genuinely nothing to write.
source: ['claude']
| // Drop the entry outright when its derivation index is | ||
| // already paired with a DIFFERENT address — committing it | ||
| // would either evict that pairing (`BiBTreeMap::insert` | ||
| // drops conflicting pairs, orphaning the other address's | ||
| // `found` entry) or persist an address `current_balances` | ||
| // can't round-trip back out of the committed seed. | ||
| if state.addresses.get_by_right(&entry.address).is_none() | ||
| && state.addresses.contains_left(&entry.address_index) | ||
| { | ||
| tracing::error!( | ||
| account_index = entry.account_index, | ||
| address_index = entry.address_index, | ||
| address = %entry.address, | ||
| "commit_reconciliation: derivation index already \ | ||
| maps to a different address — state drift; \ | ||
| dropping the reconciliation entry" | ||
| ); | ||
| continue; | ||
| } | ||
| if is_removal { | ||
| state.found.remove(&entry.address); | ||
| } else { | ||
| state.found.insert(entry.address, entry.funds); | ||
| } | ||
| // Merge pool-resolved addresses into the bijection so | ||
| // `current_balances` can pair the fresh funds with a | ||
| // derivation index. Never overwrite an existing pairing — | ||
| // `BiBTreeMap::insert` evicts conflicting pairs, which | ||
| // would orphan another address's `found` entry. | ||
| // derivation index. The conflict guard above already | ||
| // ensured this never overwrites an existing pairing. | ||
| if state.addresses.get_by_right(&entry.address).is_none() { | ||
| if state.addresses.contains_left(&entry.address_index) { | ||
| tracing::error!( | ||
| account_index = entry.account_index, | ||
| address_index = entry.address_index, | ||
| address = %entry.address, | ||
| "commit_reconciliation: derivation index already \ | ||
| maps to a different address — state drift; \ | ||
| leaving the bijection untouched" | ||
| ); | ||
| } else { | ||
| state.addresses.insert(entry.address_index, entry.address); | ||
| } | ||
| state.addresses.insert(entry.address_index, entry.address); | ||
| } | ||
| outcome.entries.push(entry); |
There was a problem hiding this comment.
🟡 Suggestion: No regression test for the new index-conflict guard branch
The existing commit_reconciliation_* tests do not construct the state-drift precondition the new guard targets (an address_index in the bijection paired with a different address than the entry's). A targeted test — especially one combining that precondition with a zero-balance removal entry — would both pin the guard's intent and would have caught the removal-branch behavior flagged above. Worth adding one credit-side and one removal-side case.
source: ['claude']
Split the `commit_reconciliation` index-conflict guard so a zero-funds removal still zeros the in-memory `found` row and is emitted to the durable persister; only credit entries are dropped when their pool-resolved derivation index is already paired with a different address. Without this split a stale persisted balance for the removed address could resurrect on restart because the reconciliation swallowed the zero-out. Adds regression coverage for both branches — the removal case (must survive, must not touch the bijection) and the credit case (must be dropped so the pre-existing pairing isn't evicted). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vailable The shielded spend paths currently flatten `PlatformWalletError::ShieldedMerkleWitnessUnavailable` into `ErrorWalletOperation`, so hosts can't tell it apart from a hard wallet-operation error and lose the retryable / non-broadcast semantics (the error fires *before* broadcast, notes are released — safe to retry after the transient store failure clears). Map it to a new `ErrorShieldedMerkleWitnessUnavailable = 20` result code in both the blanket `From<PlatformWalletError>` impl and each dedicated `shielded_send.rs` match, mirroring `ErrorShieldedNoRecordedAnchor`. Mirror the code and typed variant in the Swift `PlatformWalletResultCode` / `PlatformWalletError` enums so throw-side callers can branch on it. Adds test coverage in both the blanket-From and `map_spend_result` paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Incremental review of head cd9bf65 on top of prior SHA 2f6d9bc. Carried-forward prior findings: both are FIXED at head — the commit_reconciliation index-conflict guard in provider.rs now splits on is_removal (credits still dropped, removals zero state.found and still push to outcome.entries; only the bijection insert is gated on !index_conflict), and two targeted regression tests (commit_reconciliation_index_conflict_still_emits_removal, commit_reconciliation_index_conflict_drops_credit_entry) pin both branches. New in the latest delta: a dedicated FFI code PlatformWalletFFIResultCode::ErrorShieldedMerkleWitnessUnavailable = 20 mirrors the existing ErrorShieldedNoRecordedAnchor pattern end-to-end (Rust #[repr(C)] discriminant, blanket From, both dedicated shielded_send.rs match arms, Swift PlatformWalletResultCode / PlatformWalletError enums, unit tests on both paths). All 8 agent passes converged on zero new findings; verified provider.rs:1022-1059 in the worktree at head. No CodeRabbit inline comments provided.
Note: GitHub does not allow me to approve my own PR, so this is posted as a COMMENT while preserving the verified clean review.
Source: reviewers: opus (general), claude-sonnet-5 (general), gpt-5.5[high] (general, failed_or_unparseable), opus (security-auditor), claude-sonnet-5 (security-auditor), gpt-5.5[high] (security-auditor, failed_or_unparseable), opus (rust-quality), claude-sonnet-5 (rust-quality), gpt-5.5[high] (rust-quality, failed_or_unparseable), opus (ffi-engineer), claude-sonnet-5 (ffi-engineer), gpt-5.5[high] (ffi-engineer, failed_or_unparseable); verifier: opus; specialists: security-auditor, rust-quality, ffi-engineer
Follow-up fixes for CodeRabbit feedback on #3990.\n\nThis branch addresses the platform wallet sync/reconciliation, shielded pending spend cleanup, Swift clear/sync race, and best-effort identity persistence review comments.\n\nValidation:\n- cargo fmt --manifest-path packages/rs-platform-wallet/Cargo.toml --check\n- cargo check -p platform-wallet --features shielded\n\nNote: I could not push directly to dashpay/platform:chore/sync-v4.1-dev-with-v4.0-dev (403/permission denied), so this PR targets that branch.
Summary by CodeRabbit
Bug Fixes
New Features