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

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, and the copy says so: the address stays
derivable from key material the wallet still holds, any funds at it stay
where they are, and importing the recovery phrase brings it back. 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. A
balance is surfaced as a warning line, never as a refusal.

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.

The wallet's derivation counter is a high-water mark and is not rewound, so
"+" derives a fresh index rather than handing back the address just removed.
This commit is contained in:
clawbot
2026-08-11 13:37:36 +00:00
parent 6f6bc2e7b5
commit 125566d256
10 changed files with 632 additions and 18 deletions

View File

@@ -376,6 +376,87 @@ test("reopening the popup never lands on the phrase screen (#161)", async (env)
assertWiped(st, env.phrase, "after reopening the popup");
});
// -------------------------------------------- address removal (#162)
// Number of address rows across every wallet in the list, counted in the DOM
// whether or not Home is the screen on top.
function addressRowCount(page) {
return page.locator("#wallet-list .btn-addr-info").count();
}
function waitForAddressRows(page, n) {
return page.waitForFunction(
(want) =>
document.querySelectorAll("#wallet-list .btn-addr-info").length ===
want,
n,
{ timeout: 60000 },
);
}
// The suite arrives here with two wallets, an HD one and a key one, holding
// one address each.
test("only a wallet that can spare an address offers to remove one (#162)", async (env) => {
await visible(env.page, "#view-main");
const rows = await addressRowCount(env.page);
assert(rows === 2, "expected two address rows, got " + rows);
const offered = await env.page
.locator("#wallet-list .btn-remove-address")
.count();
assert(
offered === 0,
"a wallet holding its last address offered to remove it",
);
await env.page.click("#wallet-list .btn-add-address");
await waitForAddressRows(env.page, 3);
// Only the HD wallet's two rows; the key wallet still holds one address.
const nowOffered = await env.page
.locator("#wallet-list .btn-remove-address")
.count();
assert(
nowOffered === 2,
"expected the HD wallet's two rows to offer removal, got " + nowOffered,
);
});
// The gate itself: the control opens a confirmation, and leaving that
// confirmation by "Back" removes nothing.
test("leaving the removal confirmation removes nothing (#162)", async (env) => {
await env.page.locator("#wallet-list .btn-remove-address").nth(1).click();
await visible(env.page, "#view-delete-address-confirm");
const label = await env.page.locator("#delete-address-label").innerText();
assert(
label === "Address 2",
"the confirmation names the wrong address: " + JSON.stringify(label),
);
// "Back" re-renders Home, so a count taken after it is a real
// measurement of the wallet rather than a stale screen.
await env.page.click("#btn-delete-address-back");
await visible(env.page, "#view-main");
const rows = await addressRowCount(env.page);
assert(rows === 3, "the address was removed without a confirmation");
});
test("confirming removes the address and returns Home (#162)", async (env) => {
await env.page.locator("#wallet-list .btn-remove-address").nth(1).click();
await visible(env.page, "#view-delete-address-confirm");
await env.page.click("#btn-delete-address-confirm");
await visible(env.page, "#view-main");
await waitForAddressRows(env.page, 2);
const offered = await env.page
.locator("#wallet-list .btn-remove-address")
.count();
assert(
offered === 0,
"the HD wallet still offers to remove its last address",
);
});
// ---------------------------------------------------------------- runner
async function main() {