Skip to content

fix(dashpay): keep contacts hidden and edits safe when saved contact details cannot be read - #1139

Draft
Claudius-Maginificent wants to merge 10 commits into
fix/dashpay-contact-info-keysfrom
fix/dashpay-contacts-read-failure
Draft

Claudius-Maginificent wants to merge 10 commits into
fix/dashpay-contact-info-keysfrom
fix/dashpay-contacts-read-failure

Conversation

@Claudius-Maginificent

@Claudius-Maginificent Claudius-Maginificent commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked atop #1102 (fix/dashpay-contact-info-keys). Review only this PR's own commits until #1102 merges.

TL;DR

When DET cannot read your saved contact details at all, for example because the wallet is locked or you cancelled the password prompt, the Contacts tab now says so. Before this PR, hidden contacts showed up as active with no explanation. They now stay hidden. Saving notes and unhiding are paused until a read succeeds, so a note typed over an empty guess can no longer replace the note you actually saved. Existing data needs no migration.

What changed

  • The read failure is kept. LoadContacts used to log a failed read of the published DIP-0015 documents or of the legacy-key documents and carry on as if there were no details. Either failure now produces a typed error, with the original error as its #[source], and the cause is logged:

    • TaskError::DashPayContactDetailsLocked when the wallet is locked or its password prompt was cancelled.
    • TaskError::DashPayContactDetailsUnread for any other cause.

    The error travels on DashPayContactsWithInfo { details_error }.

  • Hidden contacts stay hidden. A contact whose details would have come from the failed read gets the new ContactData.details_unknown and is listed hidden. Details this device already holds from platform-wallet still decide where a contact goes. The rule is details_unknown(); it reuses legacy_details_shown(), so it follows the same precedence as the shown details.

  • The list explains itself. The Contacts tab shows the error's message under its header. It asks the user to unlock only when the wallet is locked:

    • Locked: "Your wallet is locked, so your saved contact details could not be read. Some contacts stay hidden and editing is paused until they are. Unlock your wallet, then open the Contacts tab again."
    • Any other cause: "Your saved contact details could not be read, so some contacts stay hidden and editing is paused. Open the Contacts tab again to retry."

    Under each such contact, the hidden section says "This contact's saved details have not been read, so it stays hidden for now. Open the Contacts tab again to retry." Switching to another hub tab and back reloads the list.

  • Edits wait for a read. The note editor, Save note and Unhide are disabled while the error is set, with a tooltip that says why. dispatch_note_save and dispatch_unhide also refuse. A list that reads the details clears the error.

  • The contact details screen. Its editor used to open after its own details read failed, starting from this device's copy. It now opens only after the read succeeds; until then Edit is disabled and its tooltip says why. It also no longer takes the nickname, note or hidden flag from a contacts list whose saved details were not read, so a read it already made stays the baseline for Edit.

  • The backend enforces it too. ContactInfoUpdate carries opened, the details the user saw. save_contact_details refuses with the new TaskError::DashPayContactDetailsChanged and writes nothing when a field the update replaces holds another value by the time of the save. Message: "This contact's details changed since you opened them, so nothing was saved. Check the latest details, then make your change again." Fields the update keeps may change freely.

    • Every caller passes what it showed: the hub's note save and unhide pass the listed details, and the contact details screen passes what its editor opened from. No MCP tool writes contact details.
    • Order of the checks: a failed read of the current details refuses first, then this check. fix(dashpay): save contact details through platform-wallet (DIP-15) #1102's refusal of a save that would keep a text field over unreadable details (TaskError::DashPayContactDetailsUnreadable) comes after it. A stale view therefore always gets the reload, even over unreadable details. Each refusal writes nothing.
    • Unhide sends a visibility-only update over the listed details. If the details became readable since the list loaded, unhide keeps them while they are still hidden, and is refused as changed when they are not.
    • After a refusal, the hub reloads the contacts list and the details screen reads the contact's details again, so the next edit starts from the latest values. This also stops a save from overwriting a newer value saved from another device.

Why the save path needed this

Every save already re-reads its baseline, and a save fails if that read fails. Fields the user does not touch are preserved, so the hidden flag survives a note save. The gap was the edited field:

  1. The list showed a note-less contact.
  2. The user typed a note and saved.
  3. The save asked for the password again and its read succeeded.
  4. The save then replaced the real note, which the user never saw.

Blocking the save in the UI closes that gap at its source. The backend check closes it for any caller.

Follow-ups

  1. Contacts whose contactInfo document cannot be tied to them stay outside this rule. That happens when the document's key cannot be derived or its encrypted contact id does not open, so nothing tells whose document it is. Those documents are skipped as before.
  2. Unlocking the wallet does not reload the list on its own; the user reopens the Contacts tab.
  3. When only the legacy read fails, a contact that never had saved details is also kept hidden. Which contacts the unread legacy documents belong to cannot be known without reading them.

Testing

  • New tests, run red before the implementation:
    • details_are_unknown_only_when_they_rest_on_a_failed_read
    • nothing_is_saved_while_the_saved_details_are_unread
    • a_failed_profile_read_keeps_the_editor_closed_until_the_details_read_ends, updated: a failed details read now keeps the editor closed, and a read that succeeds opens it.
    • a_details_read_error_names_the_action_that_fits_its_cause
    • an_unread_contacts_list_keeps_the_details_this_screen_read
  • a_save_over_details_that_changed_writes_nothing (run red first), a_save_over_changed_details_reads_them_again, a_save_over_changed_details_reloads_the_contacts.
  • a_stale_view_of_unreadable_details_is_refused_as_changed pins the check order; swapping the two checks makes it fail. an_unhide_over_details_that_became_readable_keeps_them covers both outcomes of a stale unhide.
  • Hub wiring: unread_details_pause_saving_until_a_newer_list_reads_them covers record, clear, and a stale load that must not pause saving again.
  • New kittest unread_saved_details_are_explained_and_block_the_unhide. It checks the notice, the label and the disabled Unhide, in both states. Removing the notice and the Unhide gate makes it fail. New kittest unread_saved_details_disable_the_note_editor: a note typed while the details are unread is not saved.
  • cargo test --all-features --lib -- dashpay contact (360 passed), cargo test --all-features --test kittest (523 passed) and cargo clippy --all-features --all-targets -- -D warnings pass. cargo fmt --all is clean.

🤖 Co-authored by Claudius the Magnificent AI Agent

🤖 Generated with Claude Code

https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q

lklimek and others added 6 commits October 27, 2026 00:09
… cannot be read

When the contacts list could not read the saved contact details as a
whole (the wallet is locked, or its password prompt was cancelled), it
treated every contact as having none. Hidden contacts showed as active,
nothing said why, and a note typed over the empty guess replaced the real
note once a save's own read succeeded.

The list now carries the failure as TaskError::DashPayContactDetailsUnread.
- A contact whose details would come from the failed read is listed
  hidden and labelled. Details this device holds still place a contact.
- The Contacts tab shows the error's message: what happened and to unlock
  the wallet, then open Contacts again.
- Save note and Unhide are refused until a read succeeds.
- The contact details screen opens its editor only after its details
  read succeeds, not after it fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…ts-read-failure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…hen contacts cannot be read

- The contact details screen no longer takes nickname, note and hidden
  flag from a contacts list whose saved details were not read. Those are
  guesses, and Edit starts from these fields.
- A locked wallet or a cancelled password prompt reports
  DashPayContactDetailsLocked, which asks the user to unlock. Any other
  cause reports DashPayContactDetailsUnread, which asks them to open the
  Contacts tab again. The cause is logged.
- The per-contact note and the disabled-save tooltip now say what to do.
- Doc comments that ended up on the wrong items are moved back.
- New tests cover the hub's record-and-clear wiring, including a stale
  load, and the disabled note editor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…ts-read-failure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…r saw

The UI gate kept the hub from saving over unread details, but the
backend, the authoritative layer, accepted any update. ContactInfoUpdate
now carries the details the user saw (opened). save_contact_details
refuses with TaskError::DashPayContactDetailsChanged, writing nothing,
when a field the update replaces holds another value by the time of the
save. Fields the update keeps may change freely.

- The hub's note save and unhide pass the listed details. The contact
  details screen passes the details its editor opened from.
- The backend-e2e step reads the details before it edits them.
- A refused save reloads what the user edits from: the hub reloads the
  contacts list, and the details screen reads the contact's details
  again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…ed from

- The hub's note save now carries the note its draft started from. It
  used to carry the listed note, which a reload can change under an
  unsaved edit, so a save over a note changed on another device passed
  the check.
- A refused save restarts the hub's note draft from the reloaded note.
  It closes the contact details screen's editor while the details are
  read again. Before, every later save from the old baseline was refused.
- changed_since_opened compares texts as they are stored: surrounding
  whitespace dropped, and an empty text the same as none.
- Tests assert the real baselines and cover the hidden flag, a nickname
  replace, blank versus no text, and kept fields that moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
@coderabbitai

coderabbitai Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

🕓 Review not started yet because this PR is a draft.

  • Request normal review — click when the PR is ready for review.
  • Request priority review — click to move this review to the front of the queue.

Commit defd206. Normal review starts when eligible; priority review starts as soon as a slot is available.

lklimek and others added 4 commits October 27, 2026 10:14
…ts-read-failure

Conflicts resolved:
- save_contact_details: both checks are kept. The opened-details check
  runs first, then the unreadable-baseline check.
- TaskError: DashPayContactDetailsUnread, DashPayContactDetailsLocked and
  DashPayContactDetailsChanged from this branch, and
  DashPayContactDetailsUnreadable from #1102.
- unhide_task: #1102's replacement for unreadable details, built with
  this branch's opened-details API. replace_all is no longer test-only,
  since unhide uses it.
- Tests from both sides are kept and adapted to the opened-details API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…ts-read-failure

Conflicts resolved:
- ContactInfoUpdate: changed_since_opened from this branch, and
  keeps_text and unhides_only from #1102.
- unhide_task: #1102's single visibility update (the backend decides what
  unreadable details it replaces), built with this branch's opened API.
- Tests from both sides are kept. #1102's failed-publish and
  unreadable-baseline tests are adapted to the opened API. The
  unreadable-baseline test's ContactData gains details_unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
- A stale view of unreadable details is refused as changed, and nothing
  is written. Swapping the two checks fails this test.
- An unhide sent while the list showed unreadable details, after they
  became readable, keeps them if they are still hidden. If they aren't
  hidden, it is refused as changed.
- save_contact_details' # Errors names both refusals and their order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…fix/dashpay-contacts-read-failure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
@Claudius-Maginificent Claudius-Maginificent added the blocked Blocked by something external to this issue label Oct 27, 2026
@Claudius-Maginificent

Copy link
Copy Markdown
Collaborator Author

Blocked by the stack: this PR is based on #1102, which waits on dashpay/platform#5546 (DashPay contact details signed with HASH160 keys, DET's default identity key type). It can land only after that upstream PR merges, DET bumps its Platform pin past it, and #1102 merges; then retarget this branch to v1.0-dev.

🤖 Co-authored by Claudius the Magnificent AI Agent

@Claudius-Maginificent Claudius-Maginificent added priority: low Low priority — nice to have priority: medium Medium priority and removed priority: low Low priority — nice to have labels Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked Blocked by something external to this issue priority: medium Medium priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants