fix(shielded): render restored entries' unknown dates honestly - #940
Conversation
The SDK's restore-scan reconstruction now writes createdAtMs = 0 for shielded activity entries whose original date is not recoverable from chain data (platform#4342) — previously it stamped the scan wall clock, which grouped weeks-old restored transfers under 'Today' and dated the same entry differently on two devices restoring the same wallet. Render the sentinel as what it is: - ShieldedActivityItem gains hasKnownDate (createdAtMs > 0); its date becomes the .distantPast sort sentinel when unknown, so unknown-age entries sink to the oldest end of the history instead of reading as newest (or as January 1970). - The home list groups all unknown-date entries under one trailing 'Date unknown' section instead of a spurious epoch-day header, and the section header suppresses its fabricated weekday. - The details sheet's Date row shows 'Unknown'; the row's time label is empty rather than '12:00 AM'. No-op until the app consumes an SDK build with platform#4342 — no current rows carry createdAtMs == 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe transaction history now identifies shielded entries with unrecoverable timestamps. These entries sort using ChangesUnknown date handling
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
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 `@DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift`:
- Around line 134-137: Update the Core receipt matching flow in
matchingCoreReceipt to skip the time-window date filter when the pending row’s
hasKnownDate is false, rather than using the .distantPast/Unix-epoch value.
Preserve the existing time-window filtering for rows with known createdAtMs
values so ambiguous zero-amount matches can still resolve correctly.
🪄 Autofix
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 Plus
Run ID: 9c9fe0ae-4fd8-4adf-8ccf-d4b6ec8480f2
📒 Files selected for processing (5)
DashWallet/Sources/UI/Home/Views/HomeView.swiftDashWallet/Sources/UI/Home/Views/HomeViewModel.swiftDashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swiftDashWallet/Sources/UI/Home/Views/TransactionListDataItem.swiftDashWallet/en.lproj/Localizable.strings
| hasKnownDate = row.createdAtMs > 0 | ||
| date = hasKnownDate | ||
| ? Date(timeIntervalSince1970: Double(row.createdAtMs) / 1000.0) | ||
| : .distantPast |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 18 'func matchingCoreReceipt|matchingCoreReceipt' DashWallet/Sources
rg -n -C 10 'createdAtMs|hasKnownDate|isAwaitingTransparentReceipt' \
DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift \
DashWallet/Sources/UI/Home/Views/HomeViewModel.swiftRepository: dashpay/dashwallet-ios
Length of output: 39979
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 30 'CoreWithdrawalReceiptMatchPolicy|selectedIndex|activityDate|isWithinProjectionMatchWindow' DashWallet/Sources DashWallet/Tests DashWallet 2>/dev/null | head -n 500Repository: dashpay/dashwallet-ios
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from dataclasses import dataclass
from datetime import datetime, timezone
`@dataclass`
class Candidate:
amount: int
timestamp: float
def is_within_match_window(timestamp: float, anchor: float) -> bool:
delta = timestamp - anchor
return -3600 <= delta <= 86400
def selected_index(expected_amount: int, activity_date: float, candidates: list[Candidate]):
if not candidates:
return None
if len(candidates) == 1:
return 0
in_window = [
i for i, candidate in enumerate(candidates)
if is_within_match_window(candidate.timestamp, activity_date)
]
if expected_amount > 0:
exact_in_window = [
i for i in in_window
if candidates[i].amount == expected_amount
]
if len(exact_in_window) == 1:
return exact_in_window[0]
exact = [
i for i, candidate in enumerate(candidates)
if candidate.amount == expected_amount
]
if len(exact) == 1:
return exact[0]
return in_window[0] if len(in_window) == 1 else None
epoch = 0.0
current = datetime.now(timezone.utc).timestamp()
cases = {
"single candidate": (100, [Candidate(100, current)]),
"multiple candidates with unique amount": (
100,
[Candidate(100, current), Candidate(200, current + 60)],
),
"multiple candidates with ambiguous amount": (
100,
[Candidate(100, current), Candidate(100, current + 60)],
),
"multiple candidates with zero expected amount": (
0,
[Candidate(0, current), Candidate(200, current + 60)],
),
}
for name, (amount, candidates) in cases.items():
result = selected_index(amount, epoch, candidates)
print(f"{name}: selectedIndex={result}")
PYRepository: dashpay/dashwallet-ios
Length of output: 378
Handle unknown dates in Core receipt matching.
When row.createdAtMs == 0, HomeViewModel.swift:1554 passes the Unix epoch to matchingCoreReceipt. If the destination has multiple candidates and the amount is zero or ambiguous, CoreWithdrawalReceiptMatchPolicy.selectedIndex returns nil, so the pending row remains alongside the Core transaction. Skip the time-window filter when the date is unknown.
🤖 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 `@DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift` around lines
134 - 137, Update the Core receipt matching flow in matchingCoreReceipt to skip
the time-window date filter when the pending row’s hasKnownDate is false, rather
than using the .distantPast/Unix-epoch value. Preserve the existing time-window
filtering for rows with known createdAtMs values so ambiguous zero-amount
matches can still resolve correctly.
…sset-lock reconstruction (#943) * feat(shielded): chain-order the Date-unknown band; trust SDK asset-lock reconstruction Consumes platform #4342 (SDK-side tracked-asset-lock reconstruction + minNotePosition on scan-derived shielded entries): - The trailing "Date unknown" history band (PR #940) now orders by exact on-chain sequence: ShieldedActivityItem carries the SDK's minNotePosition (smallest note commitment-tree position, nil when the row predates the field), TransactionListDataItem exposes it as chainOrderKey, and the pre-grouping sort tiebreaks equal dates by it descending — identical order on every device that restores the seed. - The app-side restore fallback (PR #939's addReconstructedLocks raw-tx reparse) is reduced to a coverage diagnostic: SDK reconstruction now writes store-backed PersistentAssetLock rows for every historical funding tx (verified 9/9 on a restored testnet wallet — identity, platform, and shielded types all classify), and store rows always won over the fallback anyway. Any asset-lock tx with no row is logged as a reconstruction gap. The reconstructed* sentinels and Transaction.reconstructedLockAmountDuffs go with it. - statusRaw 5 (RecoveredFromChain) is mapped explicitly: dedup prefers consumed (4) over recovered (5), and the tx detail sheet shows "Restored — completion unknown" instead of falling through. Note the SDK currently delivers reconstructed rows at 1/3, not 5 — the false- pending consequence and the enrichment gap are SDK-side, tracked for a platform follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dashpay): registration recovery includes RecoveredFromChain locks With platform #4347, a registration asset lock stranded by an app kill whose block chain-locks before relaunch surfaces as statusRaw 5 (RecoveredFromChain) instead of staying in the 0…3 window the recovery lookup filtered on — the stranded payment would have stopped being offered for recovery. Treat everything but the Consumed (4) tombstone as unfinished: a 5-lock with no identity is a genuinely incomplete registration, and the SDK resume path explicitly supports consuming it. Completed registrations never resume from this lock — both call sites probe the identity (local row, then the Platform slot, reconciling the lock to Consumed) first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
App-side companion to platform#4342 (restored-history reconstruction fidelity).
What
The SDK's restore-scan reconstruction now writes
createdAtMs = 0for shielded activity entries whose original date is not recoverable from chain data — previously it stamped the scan wall clock, which grouped weeks-old restored transfers under Today and dated the identical entry (same entryId) differently on two devices restoring the same wallet (observed: "Aug 7 07:20 / block 411495" vs "Aug 8 11:13 / block 412108").This renders the sentinel as what it is:
ShieldedActivityItemgainshasKnownDate(createdAtMs > 0); itsdatebecomes the.distantPastsort sentinel when unknown, so unknown-age entries sink to the oldest end of the history instead of reading as newest (or as January 1970).blockHeightabsent); the details sheet already renders an absent height by omitting the Block row — no app change needed there.Behavior before the SDK lands
No-op: no current
PersistentShieldedActivityrows carrycreatedAtMs == 0, so nothing renders differently until the app consumes an SDK build with platform#4342.Verification
Clean
dashpayscheme build (ARCHS=arm64, iphonesimulator). Unit-test target remains broken repo-wide (pre-existing).🤖 Generated with Claude Code
Summary by CodeRabbit
Follow-up (not in this PR)
Within the trailing Date unknown section, entries currently render in the arbitrary-but-deterministic order the store returns. platform#4342 now also ships
PersistentShieldedActivity.minNotePosition(commitment-tree position = exact chain order); once the app's platform checkout carries that SDK, a follow-up wires the section's ordering to it so restored history reads newest-first in its true on-chain sequence. Kept out of this PR because referencing the new SDK field would break the build against the current checkout.