feat: remove an address from an HD wallet, behind a confirmation (closes #162)
All checks were successful
check / check (push) Successful in 36s
All checks were successful
check / check (push) Successful in 36s
Address rows on Home gain an [x] control, on wallets that derive addresses from an extended key and hold more than one, opening a DeleteAddress confirmation screen. Removal cannot destroy anything: the key material stays. Derivation indices are not renumbered, so the next "+" derives the next unused index rather than resurrecting the removed one. The confirmation states the real route back -- delete the whole wallet in Settings, which asks for the password and destroys the stored recovery phrase, then import it again -- and notes that the scan which follows only finds addresses with on-chain activity. The copy varies by wallet type, since an xprv wallet has no recovery phrase. Removing an address that holds a balance is allowed, with a warning naming no figure; the funds are at the address on-chain and stay there either way. Selection and active address move only when the removed address was the one selected, and site permissions are dropped for it alone. The state transition shares its address comparison, permission cleanup and active-changed broadcast with the wallet-level removal.
This commit was merged in pull request #240.
This commit is contained in:
@@ -1,5 +1,22 @@
|
||||
// Wallet deletion state transition, kept out of the view so the selection
|
||||
// and broadcast rules are testable without a DOM.
|
||||
// Wallet and address deletion state transitions, kept out of the views so the
|
||||
// selection and broadcast rules are testable without a DOM.
|
||||
|
||||
// Two records of the same address can be stored in different cases, so
|
||||
// address equality is never a literal string comparison.
|
||||
function sameAddress(a, b) {
|
||||
if (a === null || a === undefined || b === null || b === undefined) {
|
||||
return false;
|
||||
}
|
||||
return String(a).toLowerCase() === String(b).toLowerCase();
|
||||
}
|
||||
|
||||
// Forget every site permission held against the given addresses.
|
||||
function dropSitePermissions(state, addresses) {
|
||||
for (const addr of addresses) {
|
||||
delete state.allowedSites[addr];
|
||||
delete state.deniedSites[addr];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove wallet `walletIdx` from `state` and repair the derived state.
|
||||
//
|
||||
@@ -18,19 +35,13 @@ function removeWalletFromState(state, walletIdx) {
|
||||
const wallet = state.wallets[walletIdx];
|
||||
const addresses = (wallet.addresses || []).map((a) => a.address);
|
||||
const previousActive = state.activeAddress;
|
||||
const activeWasDeleted =
|
||||
previousActive !== null &&
|
||||
previousActive !== undefined &&
|
||||
addresses.some(
|
||||
(a) => a.toLowerCase() === String(previousActive).toLowerCase(),
|
||||
);
|
||||
const activeWasDeleted = addresses.some((a) =>
|
||||
sameAddress(a, previousActive),
|
||||
);
|
||||
|
||||
state.wallets.splice(walletIdx, 1);
|
||||
|
||||
for (const addr of addresses) {
|
||||
delete state.allowedSites[addr];
|
||||
delete state.deniedSites[addr];
|
||||
}
|
||||
dropSitePermissions(state, addresses);
|
||||
|
||||
state.hasWallet = state.wallets.length > 0;
|
||||
|
||||
@@ -58,6 +69,77 @@ function removeWalletFromState(state, walletIdx) {
|
||||
return { activeAddressChanged: state.activeAddress !== previousActive };
|
||||
}
|
||||
|
||||
// Whether a wallet may be offered a per-address remove control, and the same
|
||||
// gate the removal itself is held behind.
|
||||
//
|
||||
// Only a wallet that derives its addresses from an extended key can hold more
|
||||
// than one, so only those get the control — a key wallet has exactly one
|
||||
// address and no "+" button either. The last address of any wallet is never
|
||||
// removable: a wallet with no addresses is what delete-wallet is for.
|
||||
function canRemoveAddress(wallet) {
|
||||
if (!wallet) return false;
|
||||
if (wallet.type !== "hd" && wallet.type !== "xprv") return false;
|
||||
return (wallet.addresses || []).length > 1;
|
||||
}
|
||||
|
||||
// Remove address `addrIdx` of wallet `walletIdx` and repair the derived state.
|
||||
//
|
||||
// Nothing is destroyed here. The address stays derivable from the wallet's own
|
||||
// key material and any funds at it are untouched; this only stops the wallet
|
||||
// tracking it. `nextIndex` is deliberately left alone — it is a derivation
|
||||
// high-water mark, so "+" derives a fresh index rather than handing back the
|
||||
// address just removed, and the gap it leaves is within what
|
||||
// `scanForAddresses()` re-discovers on a later import.
|
||||
//
|
||||
// The rules mirror removeWalletFromState() one level down:
|
||||
// - The call is refused unless canRemoveAddress() allows it, so the last
|
||||
// address of a wallet always survives.
|
||||
// - Site permissions are dropped for the removed address.
|
||||
// - `selectedAddress` follows the splice, but only within the wallet that
|
||||
// lost the address: it is decremented when an earlier address was
|
||||
// removed, and falls back to that wallet's first address when the
|
||||
// selection itself was removed. `selectedWallet` never moves, because the
|
||||
// wallet list does not.
|
||||
// - `activeAddress` moves only when it was the removed address, and then to
|
||||
// the wallet's first remaining address.
|
||||
//
|
||||
// Returns whether the address was removed and whether `activeAddress`
|
||||
// changed, so the caller can broadcast it.
|
||||
function removeAddressFromState(state, walletIdx, addrIdx) {
|
||||
const wallet = state.wallets[walletIdx];
|
||||
const refused = { removed: false, activeAddressChanged: false };
|
||||
if (!canRemoveAddress(wallet)) return refused;
|
||||
if (!wallet.addresses[addrIdx]) return refused;
|
||||
|
||||
const address = wallet.addresses[addrIdx].address;
|
||||
const previousActive = state.activeAddress;
|
||||
const activeWasRemoved = sameAddress(address, previousActive);
|
||||
|
||||
wallet.addresses.splice(addrIdx, 1);
|
||||
|
||||
dropSitePermissions(state, [address]);
|
||||
|
||||
if (state.selectedWallet === walletIdx) {
|
||||
if (state.selectedAddress === addrIdx) {
|
||||
state.selectedAddress = 0;
|
||||
} else if (
|
||||
typeof state.selectedAddress === "number" &&
|
||||
state.selectedAddress > addrIdx
|
||||
) {
|
||||
state.selectedAddress -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (activeWasRemoved) {
|
||||
state.activeAddress = wallet.addresses[0].address;
|
||||
}
|
||||
|
||||
return {
|
||||
removed: true,
|
||||
activeAddressChanged: state.activeAddress !== previousActive,
|
||||
};
|
||||
}
|
||||
|
||||
// Tell the background the active address changed, so it re-emits
|
||||
// accountsChanged to connected sites. Same call shape as the address
|
||||
// switch in the home view.
|
||||
@@ -67,4 +149,9 @@ function broadcastActiveChanged() {
|
||||
runtime.sendMessage({ type: "AUTISTMASK_ACTIVE_CHANGED" });
|
||||
}
|
||||
|
||||
module.exports = { removeWalletFromState, broadcastActiveChanged };
|
||||
module.exports = {
|
||||
canRemoveAddress,
|
||||
removeAddressFromState,
|
||||
removeWalletFromState,
|
||||
broadcastActiveChanged,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user