158 lines
5.8 KiB
JavaScript
158 lines
5.8 KiB
JavaScript
// Wallet and address deletion state transitions, kept out of the views so the
|
|
// selection and broadcast rules are testable without a DOM.
|
|
|
|
const { notify } = require("./browserApi");
|
|
|
|
// 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.
|
|
//
|
|
// Rules:
|
|
// - `hasWallet` tracks whether any wallet remains.
|
|
// - Site permissions are dropped for every address of the deleted wallet.
|
|
// - `selectedWallet` follows the splice: it is decremented when a wallet
|
|
// before it was removed, and falls back to the first remaining wallet's
|
|
// first address only when the selection itself was deleted.
|
|
// - `activeAddress` is only moved when it belonged to the deleted wallet;
|
|
// the fallback is the first remaining wallet's first address, or null
|
|
// when no wallet remains.
|
|
//
|
|
// Returns whether `activeAddress` changed, so the caller can broadcast it.
|
|
function removeWalletFromState(state, walletIdx) {
|
|
const wallet = state.wallets[walletIdx];
|
|
const addresses = (wallet.addresses || []).map((a) => a.address);
|
|
const previousActive = state.activeAddress;
|
|
const activeWasDeleted = addresses.some((a) =>
|
|
sameAddress(a, previousActive),
|
|
);
|
|
|
|
state.wallets.splice(walletIdx, 1);
|
|
|
|
dropSitePermissions(state, addresses);
|
|
|
|
state.hasWallet = state.wallets.length > 0;
|
|
|
|
const fallbackAddress = state.hasWallet
|
|
? state.wallets[0].addresses[0]?.address || null
|
|
: null;
|
|
|
|
if (!state.hasWallet) {
|
|
state.selectedWallet = null;
|
|
state.selectedAddress = null;
|
|
} else if (state.selectedWallet === walletIdx) {
|
|
state.selectedWallet = 0;
|
|
state.selectedAddress = 0;
|
|
} else if (
|
|
typeof state.selectedWallet === "number" &&
|
|
state.selectedWallet > walletIdx
|
|
) {
|
|
state.selectedWallet -= 1;
|
|
}
|
|
|
|
if (activeWasDeleted || !state.hasWallet) {
|
|
state.activeAddress = fallbackAddress;
|
|
}
|
|
|
|
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.
|
|
function broadcastActiveChanged() {
|
|
notify({ type: "AUTISTMASK_ACTIVE_CHANGED" });
|
|
}
|
|
|
|
module.exports = {
|
|
canRemoveAddress,
|
|
removeAddressFromState,
|
|
removeWalletFromState,
|
|
broadcastActiveChanged,
|
|
};
|