// 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. // // 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() { const runtime = typeof browser !== "undefined" ? browser.runtime : chrome.runtime; runtime.sendMessage({ type: "AUTISTMASK_ACTIVE_CHANGED" }); } module.exports = { canRemoveAddress, removeAddressFromState, removeWalletFromState, broadcastActiveChanged, };