Files
AutistMask/src/popup/views/deleteAddress.js
2026-08-17 08:38:10 +02:00

177 lines
6.5 KiB
JavaScript

// Confirmation screen for removing one address from a wallet that derives
// its addresses from an extended key.
//
// No password is asked for, unlike delete-wallet. A password gates the
// disclosure or destruction of a secret, and this does neither: the address
// is derived from key material the wallet still holds, so removing it only
// stops the wallet tracking it. An explicit confirmation screen is the
// proportionate treatment.
const {
$,
showView,
showFlash,
goBack,
renderAddressHtml,
attachCopyHandlers,
addressHoldsFunds,
balanceLinesForAddress,
} = require("./helpers");
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
const { state, saveState } = require("../../shared/state");
const {
canRemoveAddress,
removeAddressFromState,
broadcastActiveChanged,
} = require("../../shared/walletDelete");
// The wallet and address indices this screen is confirming, or null when it
// is not confirming anything.
let target = null;
let ctx = null;
function setFlash(msg) {
const el = $("delete-address-flash");
el.textContent = msg;
el.style.visibility = msg ? "visible" : "hidden";
}
// What it actually takes to get the address back, which is not what the
// screen used to claim.
//
// Neither obvious route works: "+" derives the next unused index, because
// wallet.nextIndex is a high-water mark and is deliberately not rewound; and
// re-importing this wallet's key material is refused as a duplicate by
// findWalletByXpub() for as long as the wallet is here. What remains is to
// delete the whole wallet in Settings — which asks for the password and
// destroys the stored secret — and import again, after which
// scanForAddresses() rediscovers the address only if it has on-chain
// activity. An address that was never used is not found by that scan, and
// the copy must not imply otherwise.
//
// The noun follows the wallet: an xprv wallet holds no recovery phrase, and
// this screen is offered on xprv wallets too.
function recoveryPathText(wallet) {
const secret = walletHasRecoveryPhrase(wallet)
? "recovery phrase"
: "extended private key";
return (
"Getting the address back into this list is not easy, so be sure. " +
"Adding an address derives the next unused one, not this one, and " +
"importing this " +
secret +
" again is refused while this wallet is still here. The way back is " +
"to delete the whole wallet in Settings, which asks for your " +
"password and destroys the stored " +
secret +
", and then import that " +
secret +
" again. The scan that follows only finds addresses that have " +
"on-chain activity, so an address that has never been used is not " +
"found by it."
);
}
// The balance warning, or a blank line when the address holds nothing.
//
// A balance is a reason to be careful, not a reason to refuse: the funds are
// at the address, not in this list, and stay there either way.
//
// "Holds" means ETH or any ERC-20 the wallet knows about — an address with no
// ETH and a five-figure stablecoin position must not get the blank line on
// the one screen whose job is to warn. The sentence names no figure of its
// own: the rendered lines round to four decimals, so a sentence built from a
// rounded number would report "0.0000 ETH" for an address holding real money.
// The lines below it carry the amounts, in the same format as Home and
// AddressDetail, followed by the USD total when there is one to give — no
// total line at all on testnet or before the first price fetch, and no figure
// when every holding here is one with no price, since "$0.00" directly under
// "This address holds a balance." is a contradiction.
function balanceWarningHtml(addr) {
if (!addressHoldsFunds(addr)) return " ";
const line = formatAddressTotal(getAddressValue(addr));
const total = line
? `<div class="text-xs text-muted mt-1">${line}</div>`
: "";
return (
`<p class="mb-1">This address holds a balance. Removing it does not ` +
`move or spend anything; the balance stays at the address.</p>` +
balanceLinesForAddress(addr, state.trackedTokens, false) +
total
);
}
function show(walletIdx, addrIdx) {
const wallet = state.wallets[walletIdx];
const addr = wallet && wallet.addresses[addrIdx];
if (!addr) return;
target = { walletIdx, addrIdx };
$("delete-address-label").textContent = "Address " + (addrIdx + 1);
$("delete-address-wallet-name").textContent =
wallet.name || "Wallet " + (walletIdx + 1);
const value = $("delete-address-value");
value.innerHTML = renderAddressHtml(addr.address, {
ensName: addr.ensName,
});
attachCopyHandlers(value);
$("delete-address-recovery").textContent = recoveryPathText(wallet);
$("delete-address-balance").innerHTML = balanceWarningHtml(addr);
setFlash("");
showView("delete-address-confirm");
}
function init(_ctx) {
ctx = _ctx;
$("btn-delete-address-back").addEventListener("click", () => {
target = null;
goBack();
});
$("btn-delete-address-confirm").addEventListener("click", async () => {
if (target === null) {
setFlash("No address is selected for removal.");
return;
}
const { walletIdx, addrIdx } = target;
if (!canRemoveAddress(state.wallets[walletIdx])) {
setFlash(
"This address cannot be removed, because a wallet always " +
"keeps at least one address.",
);
return;
}
const { removed, activeAddressChanged } = removeAddressFromState(
state,
walletIdx,
addrIdx,
);
if (!removed) {
setFlash("This address could not be removed.");
return;
}
target = null;
// Save before broadcasting: the background reads the active address
// back out of storage to build accountsChanged.
await saveState();
if (activeAddressChanged) broadcastActiveChanged();
ctx.renderWalletList();
goBack();
showFlash("Address removed.");
});
}
// recoveryPathText and balanceWarningHtml are exported so the two pieces of
// copy that carry the screen's substance can be tested without a DOM; show()
// is a one-line assignment for each.
module.exports = { init, show, recoveryPathText, balanceWarningHtml };