The balance lists and Home's ETH line rounded with toFixed(4), so 0.99996 ETH read 1.0000 there and 0.9999 on Send. Send's current balance and ConfirmTx's balance line kept the nonzero floor, so 0.00004 USDC read 0.0000 in the lists and 0.00004 on Send. All four now cut the stored decimal string with truncateAmount(), as the history rows do. balanceLine() takes the stored balance string instead of a parsed number, and escapes the quantity cell. The amounts, fees and allowances a screen asks the user to approve keep the floor of truncateAmountNeverZero(). Disclosure: for a holding below 0.0001, ConfirmTx shows the amount sent beside a balance that reads 0.0000. Model: opus-5-5
189 lines
6.9 KiB
JavaScript
189 lines
6.9 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,
|
|
escapeHtml,
|
|
goBack,
|
|
renderAddressHtml,
|
|
attachCopyHandlers,
|
|
addressHoldsFunds,
|
|
balanceLinesForAddress,
|
|
keyMaterialName,
|
|
} = require("./helpers");
|
|
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
|
|
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 destroys the stored secret,
|
|
// with or without the password — 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 = keyMaterialName(wallet);
|
|
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 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 cut to four decimals, so a sentence built from their
|
|
// figure 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, before the first price fetch or while no
|
|
// refresh has read the ETH balance, 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. A token holding below 0.000001 does not count,
|
|
// as the lines below leave it out.
|
|
//
|
|
// While no refresh has read the ETH balance the address may be empty, so the
|
|
// sentence says "may hold".
|
|
function balanceWarningHtml(addr) {
|
|
if (!addressHoldsFunds(addr)) return " ";
|
|
const line = formatAddressTotal(getAddressValue(addr));
|
|
const total = line
|
|
? `<div class="text-xs text-muted mt-1">${escapeHtml(line)}</div>`
|
|
: "";
|
|
const sentence =
|
|
addr.balance == null
|
|
? "This address may hold a balance."
|
|
: "This address holds a balance.";
|
|
return (
|
|
`<p class="mb-1">${sentence} 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);
|
|
// Only Blockscout reports the tokens the user has not tracked.
|
|
$("delete-address-tokens-note").classList.toggle(
|
|
"hidden",
|
|
!!state.useBlockscout,
|
|
);
|
|
|
|
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 };
|