// 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 { formatUsd, getAddressValueUsd } = 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 prices are known (null on // testnet and before the first price fetch, where the line is left off rather // than printed as $0.00). function balanceWarningHtml(addr) { if (!addressHoldsFunds(addr)) return " "; const usd = getAddressValueUsd(addr); const total = usd === null ? "" : `
Total: ${formatUsd(usd)}
`; return ( `

This address holds a balance. Removing it does not ` + `move or spend anything; the balance stays at the address.

` + 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 };