feat(wallet): restore DashPay contact payment history after wallet restore - #918
Open
romchornyi wants to merge 3 commits into
Open
feat(wallet): restore DashPay contact payment history after wallet restore#918romchornyi wants to merge 3 commits into
romchornyi wants to merge 3 commits into
Conversation
…store
A restored wallet showed "No payments with this contact yet" on every
contact card, and DashPay rows in the transaction list rendered their
avatar as "?".
Drain the deferred contact crypto. `loadFromPersistor` rehydrates a
wallet external-signable — xpubs only, no key material — so the DashPay
contact sweep cannot ECDH each contact's encrypted xpub into a
`DashpayExternalAccount` and enqueues the build instead ("Deferred
DashPay account build"). Nothing drained that queue: `send_payment` does
it with its own signer, which is why the gap only showed on wallets that
had not paid the contact since restoring, and the app never called
`unlockWalletFromKeychain`. Without those accounts the wallet has no
derived contact addresses, so sent-payment history cannot be
reconstructed at all.
One unlock at load is not enough — it schedules the drain only when the
queue is already non-empty, and at load it is empty because the contact
sweep has not run yet (measured: 0 pending at unlock, 4 pending 45s
later). So watch the SDK's published pending count and unlock again when
work appears, bounded to a few attempts and skipped while a drain is in
flight. Re-unlocking is cheap: the seed-binding verify is marker-cached.
Fall back to the DPNS label for a payment row's title.
`ContactItem.displayTitle` resolves alias → profile display name → DPNS
label → truncated id, but the payment projection stopped after the
profile display name. Most contacts have no `dashpay.profile.displayName`,
so the contact card showed the username while the transaction row had no
name at all. The DPNS hints now resolve in one batch through a static
helper: reaching the contacts service singleton from here would re-enter
its own initialization (`init` → `refresh` → the payment lookup) and trap
on the `static let shared` line.
The truncated-identity last resort is deliberately not mirrored — a
transaction row falls back to its own generic title, which reads better
than "Sent to 89fd6ddb…".
…uilt Restoring a wallet left the contact card empty on first open; the payments only appeared when it was opened again later. Measured end to end: 32s from launch to the reconstructed rows, roughly half of it spent idling until the next scheduled DashPay pass after the deferred contact accounts had already been registered. Watch the pending-account-build count down to zero and run a DashPay sync right then, instead of waiting out the interval.
|
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 Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Every payment on a restored wallet's contact card showed today's date, while the transaction-detail screen behind it showed the real one. The list read `PersistentDashpayPayment.createdAt`, which is row bookkeeping — the model says so in as many words — and only passed for a payment date because a live send writes its row as the payment happens. A payment reconstructed after a restore is written today, so the whole recovered history collapsed onto one date. Take the date from the transaction's `blockTimestamp` instead, matched by txid (reversed: payment rows carry display-order hex, the transaction table is keyed by wire-order bytes). An unconfirmed or not-yet-synced transaction has no block time and keeps the row's timestamp, which for a live send is the correct answer anyway. Sorting moves onto the same date. Reconstructed rows are all written within the same second, so insert order carried no information about which payment came first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue being fixed or feature implemented
BUG-28. After restoring a wallet from seed, every contact card showed "No payments with this contact yet", and DashPay rows in the transaction list rendered their avatar as "?". The counterparty wallet still listed the full history, so nothing was lost on chain.
Companion to dashpay/platform#4300, which does the actual reconstruction. This PR is what makes it possible to run: without it the restored wallet has no contact accounts to match against, so the upstream pass has nothing to scan.
What was done?
Drain the deferred contact crypto.
loadFromPersistorrehydrates a wallet external-signable — xpubs only, no key material — so the DashPay contact sweep cannot ECDH each contact's encrypted xpub into aDashpayExternalAccount. It enqueues the build instead and logsDeferred DashPay account build. Nothing ever drained that queue:send_paymentdoes it with its own signer (which is why the gap only showed on wallets that had not paid the contact since restoring), and the app never calledunlockWalletFromKeychain— there were zero references to it in the app.Unlock again when work appears. One unlock at load is not enough: the SDK schedules the drain only when the queue is already non-empty, and at load it is empty because the contact sweep has not run yet. Measured on device: 0 pending at unlock, 4 pending 45s later, nothing draining them. So the host watches the SDK's published pending count and unlocks again once work shows up — bounded to a few attempts, skipped while a drain is in flight, and cheap because the seed-binding verify is marker-cached.
Sweep immediately once the accounts exist. Waiting for the next scheduled DashPay pass was roughly half of the 32s between launch and the history appearing — long enough that the first open of the contact card looked empty and only a later one showed anything.
Fall back to the DPNS label for a payment row's title.
ContactItem.displayTitleresolves alias → profile display name → DPNS label → truncated id, but the payment projection stopped after the profile display name. Most contacts have nodashpay.profile.displayName, so the contact card showed the username while the transaction row had no name at all and rendered "?". The truncated-identity last resort is deliberately not mirrored — a transaction row falls back to its own generic title, which reads better than "Sent to 89fd6ddb…".The DPNS hints resolve in one batch through a
statichelper. Reaching the contacts service singleton from the payment lookup re-enters that singleton's own initialization (init→refresh→ the payment lookup) and traps withEXC_BREAKPOINTon thestatic let sharedline; the batch also stops the identity snapshot being re-read once per payment row.How Has This Been Tested?
Testnet, on device, wallet restored from seed with two established contacts and outgoing payments predating the restore.
Before:
Deferred DashPay account buildrepeating every sweep (64 occurrences in one session), no external accounts, empty contact card.After: the queue drains, both accounts register, and the upstream pass reconstructs six
Sententries with amounts and dates matching the counterparty wallet. They survive relaunch, and later passes report no eligible contacts — no repeated scanning. Transaction rows show the contact name instead of "?".Also checked in the same runs:
tookMs=1…4on the main thread, so it is not a source of UI stalls.Clean
dashpaybuild. The unit-test target remains broken (pre-existing).Breaking Changes
None.
Checklist:
For repository code-owners and collaborators only