Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions DashWallet/Sources/UI/Home/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -607,11 +607,16 @@ struct HomeViewContent<Content: View>: View {
.padding(.leading, 15)

Spacer()

Text(DWDateFormatter.sharedInstance.dayOfWeek(from: date))
.font(.footnote)
.foregroundStyle(Color.dash.tertiaryText)
.padding(.trailing, 15)

// The unknown-date group (restored shielded history with no
// recoverable date) carries the `.distantPast` sentinel — a
// weekday for it would be fabricated.
if date != .distantPast {
Text(DWDateFormatter.sharedInstance.dayOfWeek(from: date))
.font(.footnote)
.foregroundStyle(Color.dash.tertiaryText)
.padding(.trailing, 15)
}
}
.padding(.bottom, 6)
}
Expand Down
13 changes: 12 additions & 1 deletion DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -548,9 +548,20 @@ class HomeViewModel: ObservableObject {
self.txByHash[self.coinJoinWithdrawalSet.id] = item
}

// Restored shielded entries with no recoverable date (`hasKnownDate
// == false`, date == .distantPast) collect under one dedicated
// trailing group instead of a spurious epoch-day header; the
// distantPast sentinel makes both sorts place them last.
let unknownDateKey = NSLocalizedString(
"Date unknown",
comment: "History group header for restored shielded operations whose original date is not recoverable")
let groupedItems = Dictionary(
grouping: items.sorted(by: { $0.date > $1.date }),
by: { DWDateFormatter.sharedInstance.dateOnly(from: $0.date) }
by: {
$0.hasKnownDate
? DWDateFormatter.sharedInstance.dateOnly(from: $0.date)
: unknownDateKey
}
)

let array = groupedItems.compactMap { key, items -> TransactionGroup? in
Expand Down
20 changes: 17 additions & 3 deletions DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,15 @@ struct ShieldedActivityItem: Identifiable {
/// Exact fee in duffs, when the entry recorded one.
let feeDuffs: UInt64?
let blockHeight: UInt64?
/// Sort/grouping key. `Date.distantPast` when `hasKnownDate == false`
/// so unknown-age entries sink to the oldest end of the history —
/// only ever rendered through the `hasKnownDate` gate.
let date: Date
/// False for SDK rows with `createdAtMs == 0` — the sentinel the
/// scan-derived (restored) entries carry because chain data holds no
/// per-note block time and the scan clock must not masquerade as
/// one. Render the date as unknown, never as the epoch.
let hasKnownDate: Bool
/// Decoded UTF-8 text memo, when the 36-byte Dash memo is kind-1 text.
let memoText: String?
/// Created identity id (hex) for `identityCreate` entries.
Expand Down Expand Up @@ -123,7 +131,10 @@ struct ShieldedActivityItem: Identifiable {
amountDuffs = (amountCreditsOverride ?? row.amount) / 1000
feeDuffs = row.hasFee ? row.fee / 1000 : nil
blockHeight = row.hasBlockHeight ? row.blockHeight : nil
date = Date(timeIntervalSince1970: Double(row.createdAtMs) / 1000.0)
hasKnownDate = row.createdAtMs > 0
date = hasKnownDate
? Date(timeIntervalSince1970: Double(row.createdAtMs) / 1000.0)
: .distantPast
Comment on lines +134 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.swift

Repository: 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 500

Repository: 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}")
PY

Repository: 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.

memoText = Self.decodeTextMemo(row.memo)
createdIdentityIdHex = effectiveKind == .identityCreate && row.identityId.count == 32
? row.identityId.map { String(format: "%02x", $0) }.joined()
Expand Down Expand Up @@ -275,7 +286,8 @@ struct ShieldedActivityItem: Identifiable {
}

var shortTimeString: String {
DWDateFormatter.sharedInstance.timeOnly(from: date)
guard hasKnownDate else { return "" }
return DWDateFormatter.sharedInstance.timeOnly(from: date)
}

/// Decode the 36-byte Dash memo when it is kind-1 UTF-8 text:
Expand Down Expand Up @@ -378,7 +390,9 @@ struct ShieldedActivityDetailsView: View {
}
infoRow(
NSLocalizedString("Date", comment: ""),
DWDateFormatter.sharedInstance.longString(from: item.date))
item.hasKnownDate
? DWDateFormatter.sharedInstance.longString(from: item.date)
: NSLocalizedString("Unknown", comment: "Restored shielded operation whose original date is not recoverable"))
}
.padding(.horizontal, 16)
.padding(.vertical, 6)
Expand Down
13 changes: 13 additions & 0 deletions DashWallet/Sources/UI/Home/Views/TransactionListDataItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,17 @@ extension TransactionListDataItem: Identifiable {
return item.date
}
}

/// False only for restored shielded entries whose original date is
/// not recoverable (SDK `createdAtMs == 0`); their `date` is the
/// `.distantPast` sort sentinel and must not be rendered or used as
/// a day-group key.
var hasKnownDate: Bool {
switch self {
case .shieldedActivity(let item):
return item.hasKnownDate
default:
return true
}
}
}
3 changes: 3 additions & 0 deletions DashWallet/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,9 @@
/* No comment provided by engineer. */
"Date" = "Date";

/* History group header for restored shielded operations whose original date is not recoverable */
"Date unknown" = "Date unknown";

/* Voting */
"Date: New to old" = "Date: New to old";

Expand Down
Loading