feat: remove an address from an HD wallet, behind a confirmation (closes #162)
All checks were successful
check / check (push) Successful in 25s

Address rows on Home now carry an [x] control on wallets that derive their
addresses from an extended key and hold more than one; it opens a confirmation
screen before anything is removed.

Removing an address destroys nothing: it stays derivable from key material the
wallet still holds, and any funds at it stay where they are. That is also why
the screen is not password-gated, unlike delete-wallet — a password gates the
disclosure or destruction of a secret, and this does neither.

Getting the address back into the list is another matter, and the copy states
it exactly rather than promising a route the app refuses. "+" derives the next
unused index, because the derivation counter is a high-water mark and is not
rewound, and re-importing the wallet's key material is rejected as a duplicate
for as long as the wallet is present — which it always is here, since a wallet
never gives up its last address. What works is deleting the whole wallet in
Settings, which asks for the password and destroys the stored secret, then
importing again: the scan that follows rediscovers the address only if it has
on-chain activity, and an address that was never used does not come back at
all. The text is built by recoveryPathText() rather than sitting in index.html
so it can name the wallet's own kind of key material, an xprv wallet having no
recovery phrase to re-import.

A balance is surfaced as a warning, never a refusal, and holding something
means any ERC-20 as well as ETH, at any size: an address with no ETH and a
stablecoin position must not get a blank line on the screen whose job is to
warn. The warning names no figure of its own, because the balance lines round
to four decimals and a sentence built from a rounded number would report
0.0000 ETH for an address holding real money; the amounts come from the same
balanceLinesForAddress() and getAddressValueUsd() every other screen uses.

The state transition lives next to the wallet one in
src/shared/walletDelete.js and shares its address comparison, site-permission
cleanup and broadcast, so the rules match one level down: the last address of
a wallet is never removable, the selection moves only when it was the address
removed, an index after the splice is decremented, a selection in another
wallet is untouched, and AUTISTMASK_ACTIVE_CHANGED is broadcast when the
active address moves so a connected site stops being told about an address the
user removed.
This commit is contained in:
2026-08-11 13:37:36 +00:00
parent ce4a0d7b8d
commit 414de0419b
11 changed files with 905 additions and 19 deletions

View File

@@ -1142,6 +1142,62 @@
</button>
</div>
<!-- ============ DELETE ADDRESS CONFIRM ============ -->
<div id="view-delete-address-confirm" class="view hidden">
<button
id="btn-delete-address-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
>
&lt; Back
</button>
<h2 class="font-bold mb-3">Remove Address</h2>
<p class="text-xs mb-2">
You are about to remove
<strong id="delete-address-label"></strong> from
<strong id="delete-address-wallet-name"></strong>.
</p>
<div
id="delete-address-value"
class="text-xs mb-2 break-all min-h-[1rem]"
></div>
<div
class="text-xs mb-2 border border-border border-dashed p-2"
>
This only stops this wallet from tracking the address.
Nothing is destroyed and no key is deleted. Any funds at the
address stay exactly where they are, and the address remains
yours. Any site permissions granted to this address are
forgotten.
</div>
<!-- Filled by src/popup/views/deleteAddress.js: the route
back names the wallet's own kind of key material. -->
<div
id="delete-address-recovery"
class="text-xs mb-2 border border-border border-dashed p-2"
></div>
<div
id="delete-address-balance"
class="text-xs mb-2 min-h-[1.25rem] pointer-events-none"
>
&nbsp;
</div>
<p class="text-xs text-muted mb-3">
A wallet always keeps at least one address. To remove the
last one, delete the whole wallet from Settings instead.
</p>
<div
id="delete-address-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
></div>
<button
id="btn-delete-address-confirm"
class="border border-border text-red-500 px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Remove Address
</button>
</div>
<!-- ============ SHOW RECOVERY PHRASE ============ -->
<div id="view-show-phrase" class="view hidden">
<button