Skip to content

feat(addressbook): opt-in save dialog for new external recipients#238

Merged
BitHighlander merged 3 commits into
developfrom
feat/addressbook-save-recipient-dialog
Jun 11, 2026
Merged

feat(addressbook): opt-in save dialog for new external recipients#238
BitHighlander merged 3 commits into
developfrom
feat/addressbook-save-recipient-dialog

Conversation

@BitHighlander

Copy link
Copy Markdown
Collaborator

Summary

When a manual send goes to an address that isn't already a saved contact, a dialog now pops up after broadcast:

  • Headline "New external address" + a "Funds are leaving your wallet" warning
  • Shows the recipient address (identicon + chain icon)
  • Asks "Save this address to your Address Book?" with an optional label field
  • Save records the contact and stamps the date added; Not now leaves it out of the book

This makes saving opt-in. Previously every external recipient was silently auto-saved (shown as "Unlabeled" in Saved recipients) and a subtle inline label box was the only prompt.

How it works

  • recordOutbound still anchors an external row on send to carry outbound tx-history, but now leaves it saved_at = NULL (history-only, hidden from the book/picker) and reports whether the recipient is unsaved.
  • The book view and send-screen picker (listAddressBook) only surface explicitly-saved contacts (savedOnly).
  • Saving (popup, or the manual Add address dialog) stamps saved_at. The Address Book row shows "Added <date>".
  • Additive saved_at column via guarded ALTER TABLE; a one-time backfill marks existing labeled or manually-added rows as saved so nothing visible disappears on upgrade. Unlabeled auto-recorded recipients become history-only.

Files

  • bun/db.tssaved_at column + migration/backfill, recordOutbound opt-in semantics, addExternalEntry stamps saved_at, getAddressBookList savedOnly filter, row mapping.
  • bun/index.tsbroadcastTx returns recipientUnsaved; listAddressBook external = saved-only.
  • shared/types.tssavedAt, savedOnly, recipientUnsaved.
  • mainview/components/SaveRecipientDialog.tsx — new modal.
  • mainview/components/SendForm.tsx — trigger dialog post-broadcast (replaces inline prompt).
  • mainview/components/AddressBookView.tsx — "Added <date>" line.
  • i18n/locales/en/addressbook.json — new source strings.

Test plan

  • Send to a brand-new address → dialog appears; Save with a label → contact appears in Saved recipients with the date; verify it shows in the send picker next time.
  • Send to a new address → Not now → address does not appear in the book (but the send still completes).
  • Send again to a previously-saved contact → no dialog.
  • Existing users: previously-labeled contacts still visible after upgrade; previously-auto-saved unlabeled recipients no longer clutter the list.
  • Passphrase/hidden wallets still skip address-book writes.

Sending to an address not yet in the Address Book now pops a dialog after
broadcast: warns that funds are leaving the wallet, shows the external
address, and offers to save + label it. Saving is explicit (R4 opt-in) —
"Not now" leaves the address out of the book.

- recordOutbound now leaves auto-recorded external rows with saved_at=null
  (history-only) and reports `unsaved`; the book + send picker only show
  explicitly-saved contacts (listAddressBook savedOnly).
- addExternalEntry stamps saved_at on explicit save; migration backfills
  existing labeled / manually-added rows so they stay visible.
- Address Book rows display the date a contact was added.
- Replaces the prior subtle inline label prompt with the SaveRecipientDialog.
…en wallets)

The Address Book is public, non-secret data — your watch-only wallet
addresses + saved contacts — so it should not be tied to which wallet is
active. Previously every address-book RPC blanket-returned empty in a
passphrase/hidden session, so the book showed nothing (no device wallets,
no contacts) and sends couldn't be saved.

- listAddressBook no longer gates on passphrase; own (cross-device) and
  external (cross-wallet) entries load in any session. Only seedOwnFromCache
  (a write) stays standard-only.
- add/update/delete/history RPCs de-gated; mutations act globally by id
  (wallet_id is retained as provenance/dedup, no longer a read/edit scope).
- broadcastTx: split api_log (kept standard-only for tx deniability) from the
  address-book capture, so recipients can be saved from hidden-wallet sends.

Note: a contact saved while in a hidden wallet is therefore visible from the
standard wallet — the address book is intentionally outside hidden-wallet
deniability (api_log/balances writes remain gated).
@BitHighlander

Copy link
Copy Markdown
Collaborator Author

Follow-up commit: Address Book is now wallet-agnostic (00ce3dca)

During testing in a hidden (passphrase) wallet, the entire Address Book came back empty — every address-book RPC blanket-returned [] for passphrase sessions, so no device wallets or contacts loaded and sends couldn't be saved.

Per product decision, the Address Book is treated as public, non-secret data (watch-only wallet addresses + saved contacts) that is independent of which wallet is active:

  • Reads (listAddressBook) de-gated; own (cross-device) + external (cross-wallet) load in any session. Only seedOwnFromCache (a write) stays standard-only.
  • add/update/delete/history de-gated; mutations act globally by id (wallet_id retained as provenance/dedup only).
  • broadcastTx: api_log stays standard-only (tx deniability), but the address-book capture now runs in hidden sessions too, so recipients can be saved from a hidden-wallet send.

Security note for reviewers: this intentionally moves the Address Book outside hidden-wallet deniability — a contact saved while in a hidden wallet is visible from the standard wallet. api_log and balances writes remain gated, so tx-activity logging is unaffected.

Move address-book detection to where it's useful — while entering the
recipient — instead of after the funds have already left.

- New matchAddress RPC + matchAddressBook() (exact networkId equality, R5):
  returns the matching own wallet / saved contact, or null for a new address.
- SendForm runs a debounced matchAddress as the recipient is entered:
  - known → show the contact's identicon + label (gold chip)
  - new valid address → rose "New address" chip + auto-open the save dialog
    (once per address; also reachable via the chip's Save link)
- Saving now happens at form-fill (before sending); the post-broadcast save
  dialog + its confirmation are removed. recordOutbound still records
  outbound history on broadcast.

Dialog warning reworded for the pre-send context (verify the address).
@BitHighlander
BitHighlander merged commit 59e330f into develop Jun 11, 2026
@BitHighlander BitHighlander mentioned this pull request Jun 13, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant