Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe1e84b071 |
@@ -1790,13 +1790,9 @@ view would leave a wallet one click from deletion.
|
|||||||
review it again." in the errors box, no screen change. The transaction
|
review it again." in the errors box, no screen change. The transaction
|
||||||
carries the chain id of the network active when Send showed this screen,
|
carries the chain id of the network active when Send showed this screen,
|
||||||
and keeps it when the screen is drawn again from the saved transaction, so
|
and keeps it when the screen is drawn again from the saved transaction, so
|
||||||
it cannot be signed for another. The screen describes it on that network,
|
it cannot be signed for another. The screen describes it on that network:
|
||||||
whichever network is active: its amounts are in that network's native
|
its amounts are in that network's native currency, and the fee estimate
|
||||||
currency, its USD values are that network's, so a testnet transaction
|
and the recipient checks ask that network's RPC endpoint
|
||||||
shows none, its From, To and token contract links open that network's
|
|
||||||
explorer, the fee estimate and the contract and never-sent recipient
|
|
||||||
checks ask that network's RPC endpoint, and the Etherscan label check asks
|
|
||||||
that network's explorer
|
|
||||||
- "Sign & Send" (wrong password) → "Wrong password." on the password error
|
- "Sign & Send" (wrong password) → "Wrong password." on the password error
|
||||||
line, no screen change
|
line, no screen change
|
||||||
- "Sign & Send" signs as the From address above, not as whichever address is
|
- "Sign & Send" signs as the From address above, not as whichever address is
|
||||||
@@ -2046,9 +2042,7 @@ view would leave a wallet one click from deletion.
|
|||||||
- **Elements**:
|
- **Elements**:
|
||||||
- "Back" button, "Delete Wallet" heading
|
- "Back" button, "Delete Wallet" heading
|
||||||
- Warning naming the wallet and stating that deletion is permanent and any
|
- Warning naming the wallet and stating that deletion is permanent and any
|
||||||
funds are unrecoverable without the recovery phrase, the private key or
|
funds are unrecoverable without the recovery phrase
|
||||||
the extended private key, according to the wallet's kind
|
|
||||||
(`keyMaterialName()` in `src/popup/views/helpers.js`)
|
|
||||||
- Password input
|
- Password input
|
||||||
- Error line
|
- Error line
|
||||||
- "Confirm Delete" button
|
- "Confirm Delete" button
|
||||||
@@ -2089,10 +2083,9 @@ view would leave a wallet one click from deletion.
|
|||||||
cannot be unlocked again, and that no password is needed to delete it
|
cannot be unlocked again, and that no password is needed to delete it
|
||||||
- What deletion does and does not do: it erases the copy of the key stored
|
- What deletion does and does not do: it erases the copy of the key stored
|
||||||
on this device; nothing on chain changes and no money is moved
|
on this device; nothing on chain changes and no money is moved
|
||||||
- The route back — adding the wallet again, with a new password, from a kept
|
- The route back — adding the wallet again with the recovery phrase and a
|
||||||
copy of its recovery phrase, private key or extended private key (named as
|
new password — and, in bold, that without that phrase written down the
|
||||||
on DeleteWallet) — and, in bold, that without that copy the deletion loses
|
deletion loses everything the wallet holds, forever
|
||||||
everything the wallet holds, forever
|
|
||||||
- That the other wallets are not touched
|
- That the other wallets are not touched
|
||||||
- The wallet's name, and a text input asking for it to be typed back. A name
|
- The wallet's name, and a text input asking for it to be typed back. A name
|
||||||
that shows nothing at all (only spaces, or only characters that paint
|
that shows nothing at all (only spaces, or only characters that paint
|
||||||
@@ -2150,9 +2143,8 @@ view would leave a wallet one click from deletion.
|
|||||||
the address **only if it has on-chain activity**. An address that was
|
the address **only if it has on-chain activity**. An address that was
|
||||||
never used is not found by that scan. The text is written by
|
never used is not found by that scan. The text is written by
|
||||||
`recoveryPathText()` rather than sitting in `index.html`, so it can name
|
`recoveryPathText()` rather than sitting in `index.html`, so it can name
|
||||||
the wallet's own kind of key material through `keyMaterialName()`, as
|
the wallet's own kind of key material: an xprv wallet has no recovery
|
||||||
DeleteWallet and DeleteWalletLostPassword do: an xprv wallet has no
|
phrase to re-import.
|
||||||
recovery phrase to re-import.
|
|
||||||
- A warning when the address holds anything, ETH or any tracked ERC-20,
|
- A warning when the address holds anything, ETH or any tracked ERC-20,
|
||||||
followed by the holdings themselves via `balanceLinesForAddress()` and the
|
followed by the holdings themselves via `balanceLinesForAddress()` and the
|
||||||
USD total via `formatAddressTotal()` (see
|
USD total via `formatAddressTotal()` (see
|
||||||
|
|||||||
+10
-11
@@ -924,9 +924,9 @@
|
|||||||
<div class="bg-well p-3 mx-1 mb-3">
|
<div class="bg-well p-3 mx-1 mb-3">
|
||||||
<h3 class="font-bold mb-1">Network</h3>
|
<h3 class="font-bold mb-1">Network</h3>
|
||||||
<p class="text-xs text-muted mb-1">
|
<p class="text-xs text-muted mb-1">
|
||||||
Select the Ethereum network. Switching restores the RPC
|
Select the Ethereum network. Switching networks will
|
||||||
and Blockscout endpoints last used on that network, or
|
update the RPC and Blockscout endpoints to their
|
||||||
that network's defaults if it has none.
|
defaults.
|
||||||
</p>
|
</p>
|
||||||
<div class="text-xs flex items-center gap-1">
|
<div class="text-xs flex items-center gap-1">
|
||||||
<select
|
<select
|
||||||
@@ -1123,8 +1123,7 @@
|
|||||||
<p class="text-xs mb-3">
|
<p class="text-xs mb-3">
|
||||||
Deleting
|
Deleting
|
||||||
<strong id="delete-wallet-name"></strong> is permanent. Any
|
<strong id="delete-wallet-name"></strong> is permanent. Any
|
||||||
funds will be unrecoverable without your
|
funds will be unrecoverable without your recovery phrase.
|
||||||
<span id="delete-wallet-key-material"></span>.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="block mb-1">Password</label>
|
<label class="block mb-1">Password</label>
|
||||||
@@ -1175,13 +1174,13 @@
|
|||||||
money at its addresses is not moved or destroyed.
|
money at its addresses is not moved or destroyed.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs mb-2">
|
<p class="text-xs mb-2">
|
||||||
If you have kept a copy of this wallet's
|
If you have the recovery phrase for this wallet written
|
||||||
<span id="delete-wallet-lost-key-material"></span>, add the
|
down, add the wallet again afterwards with a new password
|
||||||
wallet again afterwards with a new password and you will
|
and you will have it back.
|
||||||
have it back.
|
|
||||||
<strong
|
<strong
|
||||||
>If you have not, deleting this wallet means losing
|
>If you do not have it written down, deleting this
|
||||||
everything it holds, forever.</strong
|
wallet means losing everything it holds,
|
||||||
|
forever.</strong
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs mb-3">Your other wallets are not touched.</p>
|
<p class="text-xs mb-3">Your other wallets are not touched.</p>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
// cannot be required outside a browser.
|
// cannot be required outside a browser.
|
||||||
|
|
||||||
const { RESTORABLE_VIEWS } = require("../shared/restorableViews");
|
const { RESTORABLE_VIEWS } = require("../shared/restorableViews");
|
||||||
const { SUPPORTED_CHAIN_IDS } = require("../shared/networks");
|
|
||||||
|
|
||||||
// The views this page load has rendered.
|
// The views this page load has rendered.
|
||||||
//
|
//
|
||||||
@@ -126,16 +125,13 @@ function isRenderableDecoded(value) {
|
|||||||
|
|
||||||
// The pending transaction confirmTx.show() renders: `token` reaches
|
// The pending transaction confirmTx.show() renders: `token` reaches
|
||||||
// renderAddressHtml() when it is not "ETH", and `from`/`to` reach
|
// renderAddressHtml() when it is not "ETH", and `from`/`to` reach
|
||||||
// addressTitle(), makeBlockie() and getLocalWarnings(). `chainId`, which
|
// addressTitle(), makeBlockie() and getLocalWarnings().
|
||||||
// show() fills in when it is absent, picks the network whose explorer the
|
|
||||||
// address links open, so it has to name one.
|
|
||||||
function isRenderablePendingTx(value) {
|
function isRenderablePendingTx(value) {
|
||||||
return (
|
return (
|
||||||
isRecord(value) &&
|
isRecord(value) &&
|
||||||
isText(value.token) &&
|
isText(value.token) &&
|
||||||
isAddressText(value.from) &&
|
isAddressText(value.from) &&
|
||||||
isAddressText(value.to) &&
|
isAddressText(value.to)
|
||||||
(!value.chainId || SUPPORTED_CHAIN_IDS.has(value.chainId))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ function restore() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmAddressHtml(address, ensName, title, chainId) {
|
function confirmAddressHtml(address, ensName, title) {
|
||||||
const blockie = blockieHtml(address);
|
const blockie = blockieHtml(address);
|
||||||
return (
|
return (
|
||||||
`<div class="mb-1">${blockie}</div>` +
|
`<div class="mb-1">${blockie}</div>` +
|
||||||
renderAddressHtml(address, { title, ensName, chainId })
|
renderAddressHtml(address, { title, ensName })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ function renderAmount(txInfo) {
|
|||||||
const rawSymbol = isErc20
|
const rawSymbol = isErc20
|
||||||
? txInfo.tokenSymbol || "?"
|
? txInfo.tokenSymbol || "?"
|
||||||
: nativeCurrencyByChainId(txInfo.chainId);
|
: nativeCurrencyByChainId(txInfo.chainId);
|
||||||
const price = getPrice(isErc20 ? rawSymbol : "ETH", txInfo.chainId);
|
const price = isErc20 ? getPrice(rawSymbol) : getPrice("ETH");
|
||||||
const amountUsd = price ? parseFloat(txInfo.amount) * price : null;
|
const amountUsd = price ? parseFloat(txInfo.amount) * price : null;
|
||||||
$("confirm-amount").textContent = valueWithUsd(
|
$("confirm-amount").textContent = valueWithUsd(
|
||||||
txInfo.amount + " " + displaySymbol(rawSymbol),
|
txInfo.amount + " " + displaySymbol(rawSymbol),
|
||||||
@@ -110,9 +110,8 @@ function show(txInfo) {
|
|||||||
// one active when Send handed the transaction over. Drawn again from the
|
// one active when Send handed the transaction over. Drawn again from the
|
||||||
// saved transaction, as when the popup reopens onto this screen, it keeps
|
// saved transaction, as when the popup reopens onto this screen, it keeps
|
||||||
// that network, so Sign & Send refuses once Settings has switched to
|
// that network, so Sign & Send refuses once Settings has switched to
|
||||||
// another. Everything this screen shows is that network's: native
|
// another. This screen and the wait, success and error screens label
|
||||||
// currency, USD values, explorer links, and the node and explorer its
|
// amounts by it.
|
||||||
// checks ask. The wait, success and error screens label amounts by it.
|
|
||||||
if (!txInfo.chainId) txInfo.chainId = currentNetwork().chainId;
|
if (!txInfo.chainId) txInfo.chainId = currentNetwork().chainId;
|
||||||
|
|
||||||
const isErc20 = txInfo.token !== "ETH";
|
const isErc20 = txInfo.token !== "ETH";
|
||||||
@@ -137,7 +136,7 @@ function show(txInfo) {
|
|||||||
if (isErc20) {
|
if (isErc20) {
|
||||||
$("confirm-token-contract").innerHTML = renderAddressHtml(
|
$("confirm-token-contract").innerHTML = renderAddressHtml(
|
||||||
txInfo.token,
|
txInfo.token,
|
||||||
{ chainId: txInfo.chainId },
|
{},
|
||||||
);
|
);
|
||||||
tokenSection.classList.remove("hidden");
|
tokenSection.classList.remove("hidden");
|
||||||
attachCopyHandlers(tokenSection);
|
attachCopyHandlers(tokenSection);
|
||||||
@@ -151,7 +150,6 @@ function show(txInfo) {
|
|||||||
txInfo.from,
|
txInfo.from,
|
||||||
null,
|
null,
|
||||||
fromTitle,
|
fromTitle,
|
||||||
txInfo.chainId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// To (with blockie)
|
// To (with blockie)
|
||||||
@@ -160,15 +158,14 @@ function show(txInfo) {
|
|||||||
txInfo.to,
|
txInfo.to,
|
||||||
txInfo.ensName,
|
txInfo.ensName,
|
||||||
toTitle,
|
toTitle,
|
||||||
txInfo.chainId,
|
|
||||||
);
|
);
|
||||||
$("confirm-to-ens").classList.add("hidden");
|
$("confirm-to-ens").classList.add("hidden");
|
||||||
|
|
||||||
renderAmount(txInfo);
|
renderAmount(txInfo);
|
||||||
|
|
||||||
// Balance (with inline USD)
|
// Balance (with inline USD)
|
||||||
const ethPrice = getPrice("ETH", txInfo.chainId);
|
const ethPrice = getPrice("ETH");
|
||||||
const tokenPrice = getPrice(rawSymbol, txInfo.chainId);
|
const tokenPrice = getPrice(rawSymbol);
|
||||||
if (isErc20) {
|
if (isErc20) {
|
||||||
// null is a balance whose scale nothing knows, not a balance of zero
|
// null is a balance whose scale nothing knows, not a balance of zero
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/349). The send is
|
// (https://git.eeqj.de/sneak/AutistMask/issues/349). The send is
|
||||||
@@ -447,11 +444,7 @@ async function estimateGas(txInfo) {
|
|||||||
if (estimateWei !== null && estimateWei < gasCostWei) {
|
if (estimateWei !== null && estimateWei < gasCostWei) {
|
||||||
$("confirm-fee-amount").textContent =
|
$("confirm-fee-amount").textContent =
|
||||||
"~" +
|
"~" +
|
||||||
formatFee(
|
formatFee(estimateWei, nativeCurrencyByChainId(txInfo.chainId));
|
||||||
estimateWei,
|
|
||||||
nativeCurrencyByChainId(txInfo.chainId),
|
|
||||||
txInfo.chainId,
|
|
||||||
);
|
|
||||||
$("confirm-fee-reserve").textContent =
|
$("confirm-fee-reserve").textContent =
|
||||||
"up to " +
|
"up to " +
|
||||||
truncateAmountNeverZero(formatEther(gasCostWei)) +
|
truncateAmountNeverZero(formatEther(gasCostWei)) +
|
||||||
@@ -466,7 +459,6 @@ async function estimateGas(txInfo) {
|
|||||||
$("confirm-fee-amount").textContent = formatFee(
|
$("confirm-fee-amount").textContent = formatFee(
|
||||||
gasCostWei,
|
gasCostWei,
|
||||||
nativeCurrencyByChainId(txInfo.chainId),
|
nativeCurrencyByChainId(txInfo.chainId),
|
||||||
txInfo.chainId,
|
|
||||||
);
|
);
|
||||||
setVisible("confirm-fee-reserve", false);
|
setVisible("confirm-fee-reserve", false);
|
||||||
}
|
}
|
||||||
@@ -559,7 +551,6 @@ async function checkRecipientHistory(txInfo) {
|
|||||||
const provider = reviewedNetworkProvider(txInfo);
|
const provider = reviewedNetworkProvider(txInfo);
|
||||||
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
|
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
|
||||||
fromAddress: txInfo.from,
|
fromAddress: txInfo.from,
|
||||||
chainId: txInfo.chainId,
|
|
||||||
});
|
});
|
||||||
for (const w of asyncWarnings) {
|
for (const w of asyncWarnings) {
|
||||||
if (w.type === "contract") {
|
if (w.type === "contract") {
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ const {
|
|||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
addressHoldsFunds,
|
addressHoldsFunds,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
keyMaterialName,
|
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
|
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
|
||||||
|
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const {
|
const {
|
||||||
canRemoveAddress,
|
canRemoveAddress,
|
||||||
@@ -54,7 +54,9 @@ function setFlash(msg) {
|
|||||||
// The noun follows the wallet: an xprv wallet holds no recovery phrase, and
|
// The noun follows the wallet: an xprv wallet holds no recovery phrase, and
|
||||||
// this screen is offered on xprv wallets too.
|
// this screen is offered on xprv wallets too.
|
||||||
function recoveryPathText(wallet) {
|
function recoveryPathText(wallet) {
|
||||||
const secret = keyMaterialName(wallet);
|
const secret = walletHasRecoveryPhrase(wallet)
|
||||||
|
? "recovery phrase"
|
||||||
|
: "extended private key";
|
||||||
return (
|
return (
|
||||||
"Getting the address back into this list is not easy, so be sure. " +
|
"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 " +
|
"Adding an address derives the next unused one, not this one, and " +
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const {
|
|||||||
goBack,
|
goBack,
|
||||||
clearViewStack,
|
clearViewStack,
|
||||||
onViewLeave,
|
onViewLeave,
|
||||||
keyMaterialName,
|
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const { decryptWithPassword } = require("../../shared/vault");
|
const { decryptWithPassword } = require("../../shared/vault");
|
||||||
@@ -76,9 +75,6 @@ function show(walletIdx) {
|
|||||||
clear();
|
clear();
|
||||||
deleteWalletIndex = walletIdx;
|
deleteWalletIndex = walletIdx;
|
||||||
$("delete-wallet-name").textContent = displayName(walletIdx);
|
$("delete-wallet-name").textContent = displayName(walletIdx);
|
||||||
$("delete-wallet-key-material").textContent = keyMaterialName(
|
|
||||||
state.wallets[walletIdx],
|
|
||||||
);
|
|
||||||
showView("delete-wallet-confirm");
|
showView("delete-wallet-confirm");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +94,6 @@ function showLostPassword() {
|
|||||||
clearLostPassword();
|
clearLostPassword();
|
||||||
$("delete-wallet-lost-name").textContent = name;
|
$("delete-wallet-lost-name").textContent = name;
|
||||||
$("delete-wallet-lost-name-echo").textContent = name;
|
$("delete-wallet-lost-name-echo").textContent = name;
|
||||||
$("delete-wallet-lost-key-material").textContent = keyMaterialName(
|
|
||||||
state.wallets[walletIdx],
|
|
||||||
);
|
|
||||||
// showView() runs the leave hook of delete-wallet-confirm, which nulls
|
// showView() runs the leave hook of delete-wallet-confirm, which nulls
|
||||||
// deleteWalletIndex, so this screen's own selection is recorded after
|
// deleteWalletIndex, so this screen's own selection is recorded after
|
||||||
// it and not before.
|
// it and not before.
|
||||||
|
|||||||
+15
-33
@@ -22,11 +22,9 @@ const { resolveTokenSymbol } = require("../../shared/approvalAmount");
|
|||||||
const { DEBUG } = require("../../shared/constants");
|
const { DEBUG } = require("../../shared/constants");
|
||||||
const { escapeHtml } = require("../../shared/html");
|
const { escapeHtml } = require("../../shared/html");
|
||||||
const { isDebug } = require("../../shared/log");
|
const { isDebug } = require("../../shared/log");
|
||||||
const { networkByChainId } = require("../../shared/networks");
|
|
||||||
const { formatUsd, getPrice } = require("../../shared/prices");
|
const { formatUsd, getPrice } = require("../../shared/prices");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { displaySymbol } = require("../../shared/symbolDisplay");
|
const { displaySymbol } = require("../../shared/symbolDisplay");
|
||||||
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
|
||||||
const { markViewRendered } = require("../viewRouter");
|
const { markViewRendered } = require("../viewRouter");
|
||||||
|
|
||||||
// When views are added, removed, or transitions between them change,
|
// When views are added, removed, or transitions between them change,
|
||||||
@@ -308,12 +306,11 @@ function tokenLabel(address) {
|
|||||||
// A network fee in wei as the confirmation and approval screens both show it:
|
// A network fee in wei as the confirmation and approval screens both show it:
|
||||||
// the ETH figure through truncateAmountNeverZero() and labelled `symbol`, the
|
// the ETH figure through truncateAmountNeverZero() and labelled `symbol`, the
|
||||||
// native currency of the network the fee is paid on, then its USD value when
|
// native currency of the network the fee is paid on, then its USD value when
|
||||||
// the ETH price is known on the network with `chainId`, or on the active
|
// the ETH price is known. The USD value is of the exact fee, not of the
|
||||||
// network when no chain id is given. The USD value is of the exact fee, not of
|
// truncated figure.
|
||||||
// the truncated figure.
|
function formatFee(wei, symbol) {
|
||||||
function formatFee(wei, symbol, chainId) {
|
|
||||||
const eth = formatEther(wei);
|
const eth = formatEther(wei);
|
||||||
const ethPrice = getPrice("ETH", chainId);
|
const ethPrice = getPrice("ETH");
|
||||||
const usd = ethPrice ? formatUsd(parseFloat(eth) * ethPrice) : "";
|
const usd = ethPrice ? formatUsd(parseFloat(eth) * ethPrice) : "";
|
||||||
return (
|
return (
|
||||||
truncateAmountNeverZero(eth) +
|
truncateAmountNeverZero(eth) +
|
||||||
@@ -421,16 +418,6 @@ function addressHoldsFunds(addr) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The name of a wallet's key material, which is also its only backup: the
|
|
||||||
// recovery phrase of an HD wallet, the private key of a key wallet, the
|
|
||||||
// extended private key of an xprv wallet. DeleteWallet,
|
|
||||||
// DeleteWalletLostPassword and DeleteAddress name the backup with it.
|
|
||||||
function keyMaterialName(wallet) {
|
|
||||||
if (walletHasRecoveryPhrase(wallet)) return "recovery phrase";
|
|
||||||
if (wallet.type === "key") return "private key";
|
|
||||||
return "extended private key";
|
|
||||||
}
|
|
||||||
|
|
||||||
// The fewest characters of an address any caller may ask to display. The
|
// The fewest characters of an address any caller may ask to display. The
|
||||||
// 10-character cap inside truncateMiddle() is the other half of the same
|
// 10-character cap inside truncateMiddle() is the other half of the same
|
||||||
// guarantee; this is the half that used to be spelled out at each call
|
// guarantee; this is the half that used to be spelled out at each call
|
||||||
@@ -606,19 +593,17 @@ const EXT_ICON =
|
|||||||
`</svg></span>`;
|
`</svg></span>`;
|
||||||
|
|
||||||
// Block-explorer URLs. The origin is a per-network constant from
|
// Block-explorer URLs. The origin is a per-network constant from
|
||||||
// src/shared/networks.js, the explorer of the network with `chainId`, or of
|
// src/shared/networks.js; only the path segment is data, and it comes out
|
||||||
// the active network when no chain id is given; only the path segment is
|
// of explorer JSON (a transaction's from/to, a token's address_hash), which
|
||||||
// data, and it comes out of explorer JSON (a transaction's from/to, a token's
|
// nothing upstream validates as hex. percent-encoding it keeps a segment
|
||||||
// address_hash), which nothing upstream validates as hex. percent-encoding it
|
// that contains a slash, a query or a fragment from re-pointing the link
|
||||||
// keeps a segment that contains a slash, a query or a fragment from
|
// somewhere else in the explorer.
|
||||||
// re-pointing the link somewhere else in the explorer.
|
function explorerUrl(kind, value) {
|
||||||
function explorerUrl(kind, value, chainId) {
|
return `${currentNetwork().explorerUrl}/${kind}/${encodeURIComponent(value)}`;
|
||||||
const network = chainId ? networkByChainId(chainId) : currentNetwork();
|
|
||||||
return `${network.explorerUrl}/${kind}/${encodeURIComponent(value)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function etherscanAddressUrl(address, chainId) {
|
function etherscanAddressUrl(address) {
|
||||||
return explorerUrl("address", address, chainId);
|
return explorerUrl("address", address);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The URL still has to be escaped on the way into href="...": encoding
|
// The URL still has to be escaped on the way into href="...": encoding
|
||||||
@@ -684,18 +669,16 @@ function attachCopyHandlers(container) {
|
|||||||
// with them, and the "at least 32 characters" guarantee has
|
// with them, and the "at least 32 characters" guarantee has
|
||||||
// to survive having no current callers to be a guarantee.
|
// to survive having no current callers to be a guarantee.
|
||||||
// noLink — if true, omit etherscan link
|
// noLink — if true, omit etherscan link
|
||||||
// chainId — the network whose explorer the link opens; the active
|
|
||||||
// network's when absent
|
|
||||||
//
|
//
|
||||||
// After inserting the returned HTML into the DOM, call
|
// After inserting the returned HTML into the DOM, call
|
||||||
// attachCopyHandlers() on the parent to wire up click-to-copy.
|
// attachCopyHandlers() on the parent to wire up click-to-copy.
|
||||||
function renderAddressHtml(address, opts) {
|
function renderAddressHtml(address, opts) {
|
||||||
const { title, ensName, maxLen, noLink, chainId } = opts || {};
|
const { title, ensName, maxLen, noLink } = opts || {};
|
||||||
const dot = addressDotHtml(address);
|
const dot = addressDotHtml(address);
|
||||||
const displayAddr = maxLen
|
const displayAddr = maxLen
|
||||||
? truncateMiddle(address, Math.max(ADDRESS_MIN_DISPLAY_LEN, maxLen))
|
? truncateMiddle(address, Math.max(ADDRESS_MIN_DISPLAY_LEN, maxLen))
|
||||||
: address;
|
: address;
|
||||||
const link = etherscanAddressUrl(address, chainId);
|
const link = etherscanAddressUrl(address);
|
||||||
const extLink = noLink ? "" : etherscanLinkHtml(link);
|
const extLink = noLink ? "" : etherscanLinkHtml(link);
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
@@ -742,7 +725,6 @@ module.exports = {
|
|||||||
balanceLine,
|
balanceLine,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
addressHoldsFunds,
|
addressHoldsFunds,
|
||||||
keyMaterialName,
|
|
||||||
unknownableAmount,
|
unknownableAmount,
|
||||||
nativeCurrency,
|
nativeCurrency,
|
||||||
tokenLabel,
|
tokenLabel,
|
||||||
|
|||||||
@@ -58,8 +58,6 @@ function getLocalWarnings(address, options = {}) {
|
|||||||
* @param {object} provider - An ethers.js provider instance.
|
* @param {object} provider - An ethers.js provider instance.
|
||||||
* @param {object} [options] - Optional context.
|
* @param {object} [options] - Optional context.
|
||||||
* @param {string} [options.fromAddress] - Sender address (for self-send check).
|
* @param {string} [options.fromAddress] - Sender address (for self-send check).
|
||||||
* @param {string} [options.chainId] - Chain id of the network whose explorer
|
|
||||||
* the Etherscan label check asks; the active network's when omitted.
|
|
||||||
* @returns {Promise<Array<{type: string, message: string, severity: string}>>}
|
* @returns {Promise<Array<{type: string, message: string, severity: string}>>}
|
||||||
*/
|
*/
|
||||||
async function getFullWarnings(address, provider, options = {}) {
|
async function getFullWarnings(address, provider, options = {}) {
|
||||||
@@ -112,10 +110,7 @@ async function getFullWarnings(address, provider, options = {}) {
|
|||||||
// Runs for ALL addresses including contracts, since many dangerous
|
// Runs for ALL addresses including contracts, since many dangerous
|
||||||
// flagged addresses on Etherscan (drainers, phishing contracts) are contracts.
|
// flagged addresses on Etherscan (drainers, phishing contracts) are contracts.
|
||||||
try {
|
try {
|
||||||
const etherscanWarning = await checkEtherscanLabel(
|
const etherscanWarning = await checkEtherscanLabel(address);
|
||||||
address,
|
|
||||||
options.chainId,
|
|
||||||
);
|
|
||||||
if (etherscanWarning) {
|
if (etherscanWarning) {
|
||||||
warnings.push(etherscanWarning);
|
warnings.push(etherscanWarning);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// Extension users make the requests directly to Etherscan — no proxy needed.
|
// Extension users make the requests directly to Etherscan — no proxy needed.
|
||||||
// This is a best-effort enrichment: network failures return null silently.
|
// This is a best-effort enrichment: network failures return null silently.
|
||||||
|
|
||||||
const { networkByChainId } = require("./networks");
|
|
||||||
|
|
||||||
// Patterns in the page title that indicate a flagged address.
|
// Patterns in the page title that indicate a flagged address.
|
||||||
// Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan"
|
// Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan"
|
||||||
const PHISHING_LABEL_PATTERNS = [/^Fake_Phishing/i, /^Phish:/i, /^Exploiter/i];
|
const PHISHING_LABEL_PATTERNS = [/^Fake_Phishing/i, /^Phish:/i, /^Exploiter/i];
|
||||||
@@ -74,21 +72,18 @@ function parseEtherscanPage(html) {
|
|||||||
* Returns a warning object if the address is flagged, or null.
|
* Returns a warning object if the address is flagged, or null.
|
||||||
* Network failures return null silently (best-effort check).
|
* Network failures return null silently (best-effort check).
|
||||||
*
|
*
|
||||||
* Uses the explorer of the network with `chainId`, or of the active network
|
* Uses the current network's explorer URL so the lookup works on both
|
||||||
* when it is omitted: etherscan.io on mainnet, sepolia.etherscan.io on
|
* mainnet (etherscan.io) and Sepolia (sepolia.etherscan.io).
|
||||||
* Sepolia.
|
|
||||||
*
|
*
|
||||||
* @param {string} address - Ethereum address to check.
|
* @param {string} address - Ethereum address to check.
|
||||||
* @param {string} [chainId] - Chain id of the network to look it up on.
|
|
||||||
* @returns {Promise<{type: string, message: string, severity: string}|null>}
|
* @returns {Promise<{type: string, message: string, severity: string}|null>}
|
||||||
*/
|
*/
|
||||||
async function checkEtherscanLabel(address, chainId) {
|
async function checkEtherscanLabel(address) {
|
||||||
try {
|
try {
|
||||||
// Lazy require to avoid pulling in chrome.storage at module scope
|
// Lazy require to avoid pulling in chrome.storage at module scope
|
||||||
// (which breaks unit tests that only exercise parseEtherscanPage).
|
// (which breaks unit tests that only exercise parseEtherscanPage).
|
||||||
const { currentNetwork } = require("./state");
|
const { currentNetwork } = require("./state");
|
||||||
const network = chainId ? networkByChainId(chainId) : currentNetwork();
|
const etherscanBase = currentNetwork().explorerUrl + "/address/";
|
||||||
const etherscanBase = network.explorerUrl + "/address/";
|
|
||||||
const resp = await fetch(etherscanBase + address, {
|
const resp = await fetch(etherscanBase + address, {
|
||||||
headers: { Accept: "text/html" },
|
headers: { Accept: "text/html" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
const { getTopTokenPrices } = require("./tokenList");
|
const { getTopTokenPrices } = require("./tokenList");
|
||||||
const { isBelowOneMillionth } = require("./amountDisplay");
|
const { isBelowOneMillionth } = require("./amountDisplay");
|
||||||
const { networkByChainId } = require("./networks");
|
|
||||||
|
|
||||||
const PRICE_CACHE_TTL = 300000; // 5 minutes
|
const PRICE_CACHE_TTL = 300000; // 5 minutes
|
||||||
|
|
||||||
@@ -37,12 +36,10 @@ function clearPrices() {
|
|||||||
lastFetchedAt = 0;
|
lastFetchedAt = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the USD price for a symbol on the network with `chainId`, or on the
|
// Return the USD price for a symbol, or null on testnet / unknown.
|
||||||
// active network when no chain id is given; null on testnet / unknown.
|
function getPrice(symbol) {
|
||||||
function getPrice(symbol, chainId) {
|
|
||||||
const { currentNetwork } = require("./state");
|
const { currentNetwork } = require("./state");
|
||||||
const network = chainId ? networkByChainId(chainId) : currentNetwork();
|
if (currentNetwork().isTestnet) return null;
|
||||||
if (network.isTestnet) return null;
|
|
||||||
return prices[symbol] || null;
|
return prices[symbol] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,22 +10,17 @@
|
|||||||
// The same holds when the popup was closed after the switch and reopened, and
|
// The same holds when the popup was closed after the switch and reopened, and
|
||||||
// Back drew the screen again from the transaction it saved
|
// Back drew the screen again from the transaction it saved
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/586): the screen was drawn for
|
// (https://git.eeqj.de/sneak/AutistMask/issues/586): the screen was drawn for
|
||||||
// the new network, and Sign & Send sent the reviewed transfer there. Drawn
|
// the new network, and Sign & Send sent the reviewed transfer there.
|
||||||
// again, the screen describes the transaction entirely on the network it was
|
|
||||||
// reviewed for, because choosing that network again in Settings and pressing
|
|
||||||
// Back only unhides it, and Sign & Send then sends from it.
|
|
||||||
//
|
//
|
||||||
// Driven through the real confirmation screen, signer and chain switch, with
|
// Driven through the real confirmation screen, signer and chain switch, with
|
||||||
// only the node, fetch and the DOM stubbed. The stub node is on the network
|
// only the node and the DOM stubbed. The stub node is on the network its
|
||||||
// its provider was built for, so a broadcast records the network it went to.
|
// provider was built for, so a broadcast records the network it went to.
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// The questions the stub node was asked for the fee and the recipient checks,
|
// The nodes asked for the fee, by network name and RPC URL, in order. And the
|
||||||
// by method, network name and RPC URL, in order. The signed transactions it
|
// signed transactions the stub node was handed, with its network's chain id.
|
||||||
// was handed, with its network's chain id and RPC URL. And whether it quotes
|
const mockNode = { feeAsked: [], broadcast: [] };
|
||||||
// fees as a network with no type-2 pricing does.
|
|
||||||
const mockNode = { asked: [], broadcast: [], legacyFees: false };
|
|
||||||
|
|
||||||
jest.mock("ethers", () => {
|
jest.mock("ethers", () => {
|
||||||
const actual = jest.requireActual("ethers");
|
const actual = jest.requireActual("ethers");
|
||||||
@@ -34,25 +29,14 @@ jest.mock("ethers", () => {
|
|||||||
this.url = url;
|
this.url = url;
|
||||||
this.network = network;
|
this.network = network;
|
||||||
}
|
}
|
||||||
record(method) {
|
|
||||||
mockNode.asked.push({
|
|
||||||
method,
|
|
||||||
network: this.network.name,
|
|
||||||
url: this.url,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async getNetwork() {
|
async getNetwork() {
|
||||||
return this.network;
|
return this.network;
|
||||||
}
|
}
|
||||||
async getFeeData() {
|
async getFeeData() {
|
||||||
this.record("getFeeData");
|
mockNode.feeAsked.push({
|
||||||
if (mockNode.legacyFees) {
|
network: this.network.name,
|
||||||
return {
|
url: this.url,
|
||||||
maxFeePerGas: null,
|
});
|
||||||
maxPriorityFeePerGas: null,
|
|
||||||
gasPrice: 10000000000n,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
maxFeePerGas: 20000000000n,
|
maxFeePerGas: 20000000000n,
|
||||||
maxPriorityFeePerGas: 1000000000n,
|
maxPriorityFeePerGas: 1000000000n,
|
||||||
@@ -63,22 +47,16 @@ jest.mock("ethers", () => {
|
|||||||
return 21000n;
|
return 21000n;
|
||||||
}
|
}
|
||||||
async getCode() {
|
async getCode() {
|
||||||
this.record("getCode");
|
|
||||||
return "0x";
|
return "0x";
|
||||||
}
|
}
|
||||||
async getTransactionCount() {
|
async getTransactionCount() {
|
||||||
this.record("getTransactionCount");
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
async lookupAddress() {
|
async lookupAddress() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
async broadcastTransaction(signed) {
|
async broadcastTransaction(signed) {
|
||||||
mockNode.broadcast.push({
|
mockNode.broadcast.push({ chainId: this.network.chainId, signed });
|
||||||
chainId: this.network.chainId,
|
|
||||||
url: this.url,
|
|
||||||
signed,
|
|
||||||
});
|
|
||||||
return { hash: "0x" + "ab".repeat(32) };
|
return { hash: "0x" + "ab".repeat(32) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,8 +86,6 @@ jest.mock("../src/popup/views/txStatus", () => ({
|
|||||||
showError: jest.fn(),
|
showError: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// The Etherscan label check is its only caller here: the explorer pages it
|
|
||||||
// asks for are recorded, and nothing is fetched.
|
|
||||||
global.fetch = jest.fn(() => {
|
global.fetch = jest.fn(() => {
|
||||||
throw new Error("tests must not perform network requests");
|
throw new Error("tests must not perform network requests");
|
||||||
});
|
});
|
||||||
@@ -163,7 +139,6 @@ const { encryptWithPassword } = require("../src/shared/vault");
|
|||||||
const { state, loadState } = require("../src/shared/state");
|
const { state, loadState } = require("../src/shared/state");
|
||||||
const { onChainSwitch } = require("../src/shared/chainSwitch");
|
const { onChainSwitch } = require("../src/shared/chainSwitch");
|
||||||
const { NETWORKS } = require("../src/shared/networks");
|
const { NETWORKS } = require("../src/shared/networks");
|
||||||
const { prices } = require("../src/shared/prices");
|
|
||||||
const {
|
const {
|
||||||
goBack,
|
goBack,
|
||||||
pushCurrentView,
|
pushCurrentView,
|
||||||
@@ -180,7 +155,6 @@ const confirmTx = require("../src/popup/views/confirmTx");
|
|||||||
const PRIVATE_KEY = "0x" + "11".repeat(32);
|
const PRIVATE_KEY = "0x" + "11".repeat(32);
|
||||||
const HOLDER = new Wallet(PRIVATE_KEY).address;
|
const HOLDER = new Wallet(PRIVATE_KEY).address;
|
||||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||||
const TOKEN = "0x7070707070707070707070707070707070707070";
|
|
||||||
const PASSWORD = "correct horse battery staple";
|
const PASSWORD = "correct horse battery staple";
|
||||||
|
|
||||||
function el(id) {
|
function el(id) {
|
||||||
@@ -197,10 +171,8 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
elements.clear();
|
elements.clear();
|
||||||
mockNode.asked = [];
|
mockNode.feeAsked = [];
|
||||||
mockNode.broadcast = [];
|
mockNode.broadcast = [];
|
||||||
mockNode.legacyFees = false;
|
|
||||||
global.fetch.mockClear();
|
|
||||||
txStatus.showWait.mockClear();
|
txStatus.showWait.mockClear();
|
||||||
txStatus.showError.mockClear();
|
txStatus.showError.mockClear();
|
||||||
signTransaction = jest.spyOn(BaseWallet.prototype, "signTransaction");
|
signTransaction = jest.spyOn(BaseWallet.prototype, "signTransaction");
|
||||||
@@ -210,10 +182,9 @@ afterEach(() => {
|
|||||||
signTransaction.mockRestore();
|
signTransaction.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show the confirmation screen for 1 of the native token on `networkId`, or
|
// Show the confirmation screen for 1 of the native token on `networkId`, as
|
||||||
// for the transfer `changes` makes of it, as Review on the Send screen does,
|
// Review on the Send screen does, with its fee estimate settled.
|
||||||
// with its fee estimate settled.
|
async function review(networkId) {
|
||||||
async function review(networkId, changes = {}) {
|
|
||||||
await onChainSwitch(networkId);
|
await onChainSwitch(networkId);
|
||||||
state.wallets = [
|
state.wallets = [
|
||||||
{
|
{
|
||||||
@@ -237,87 +208,10 @@ async function review(networkId, changes = {}) {
|
|||||||
tokenBalance: null,
|
tokenBalance: null,
|
||||||
tokenDecimals: null,
|
tokenDecimals: null,
|
||||||
max: false,
|
max: false,
|
||||||
...changes,
|
|
||||||
});
|
});
|
||||||
await settle();
|
await settle();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user's own node for a network, set in Settings.
|
|
||||||
function ownNode(networkId) {
|
|
||||||
return "https://" + networkId + ".node.example.invalid";
|
|
||||||
}
|
|
||||||
|
|
||||||
// After review() on `reviewedOn`: the user's own node set for it, the Settings
|
|
||||||
// gear, `switchedTo` chosen there, the popup closed and reopened onto
|
|
||||||
// Settings, and Back, which draws the confirmation screen again from the
|
|
||||||
// transaction it saved. The reopened page holds an ETH price of $2,000 and a
|
|
||||||
// USDC price of $1 whichever network is active, so each USD value shows which
|
|
||||||
// network it was priced for.
|
|
||||||
async function switchReopenAndGoBack(reviewedOn, switchedTo) {
|
|
||||||
state.rpcUrl = ownNode(reviewedOn);
|
|
||||||
pushCurrentView();
|
|
||||||
showView("settings");
|
|
||||||
await onChainSwitch(switchedTo);
|
|
||||||
|
|
||||||
await loadState();
|
|
||||||
elements.clear();
|
|
||||||
resetRenderedViews();
|
|
||||||
setBackRenderer(makeBackRenderer(state, { confirmTx }));
|
|
||||||
confirmTx.init({});
|
|
||||||
prices.ETH = 2000;
|
|
||||||
prices.USDC = 1;
|
|
||||||
mockNode.asked = [];
|
|
||||||
global.fetch.mockClear();
|
|
||||||
goBack();
|
|
||||||
await settle();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The USD value `text` as the screen appends it for `networkId`: nothing on a
|
|
||||||
// testnet, which has no USD values.
|
|
||||||
function usdOn(networkId, text) {
|
|
||||||
return NETWORKS[networkId].isTestnet ? "" : " (" + text + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
// The screen for the transfer review() shows by default, described on
|
|
||||||
// `networkId`: in its native currency, priced by it, linking to its explorer.
|
|
||||||
function expectNativeTransferOn(networkId) {
|
|
||||||
const { nativeCurrency: symbol, explorerUrl } = NETWORKS[networkId];
|
|
||||||
expect(el("confirm-type").textContent).toBe(
|
|
||||||
"Native " + symbol + " transfer",
|
|
||||||
);
|
|
||||||
expect(el("confirm-amount").textContent).toBe(
|
|
||||||
"1 " + symbol + usdOn(networkId, "$2,000.00"),
|
|
||||||
);
|
|
||||||
expect(el("confirm-balance").textContent).toBe(
|
|
||||||
"5.0000 " + symbol + usdOn(networkId, "$10,000.00"),
|
|
||||||
);
|
|
||||||
expect(el("confirm-fee-amount").textContent).toBe(
|
|
||||||
"~0.0002 " + symbol + usdOn(networkId, "$0.42"),
|
|
||||||
);
|
|
||||||
expect(el("confirm-fee-reserve").textContent).toBe(
|
|
||||||
"up to 0.0004 " + symbol + " reserved",
|
|
||||||
);
|
|
||||||
expect(el("confirm-gas-error").textContent).toBe(
|
|
||||||
"You do not have enough " +
|
|
||||||
symbol +
|
|
||||||
" to pay the network fee for this transfer. Please add " +
|
|
||||||
symbol +
|
|
||||||
" to this address and try again.",
|
|
||||||
);
|
|
||||||
expect(el("confirm-recipient-warning").textContent).toContain(
|
|
||||||
" holds no " + symbol + ". ",
|
|
||||||
);
|
|
||||||
expect(el("confirm-contract-warning").textContent).toContain(
|
|
||||||
" Sending " + symbol + " or tokens ",
|
|
||||||
);
|
|
||||||
expect(el("confirm-from").innerHTML).toContain(
|
|
||||||
`href="${explorerUrl}/address/${HOLDER}"`,
|
|
||||||
);
|
|
||||||
expect(el("confirm-to").innerHTML).toContain(
|
|
||||||
`href="${explorerUrl}/address/${RECIPIENT}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// show() estimates the fee and checks the recipient without awaiting either;
|
// show() estimates the fee and checks the recipient without awaiting either;
|
||||||
// this lets both finish.
|
// this lets both finish.
|
||||||
async function settle() {
|
async function settle() {
|
||||||
@@ -373,38 +267,42 @@ describe.each([
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
["sepolia", "mainnet"],
|
["sepolia", "mainnet", "SepoliaETH"],
|
||||||
["mainnet", "sepolia"],
|
["mainnet", "sepolia", "ETH"],
|
||||||
])(
|
])(
|
||||||
"a transfer reviewed on %s, with %s chosen in Settings, the popup reopened and Back pressed",
|
"a transfer reviewed on %s, with %s chosen in Settings, the popup reopened and Back pressed",
|
||||||
(reviewedOn, switchedTo) => {
|
(reviewedOn, switchedTo, symbol) => {
|
||||||
const { nativeCurrency: symbol, explorerUrl } = NETWORKS[reviewedOn];
|
|
||||||
|
|
||||||
test("is described on the network it was reviewed for, and neither signed nor sent", async () => {
|
test("is described on the network it was reviewed for, and neither signed nor sent", async () => {
|
||||||
await review(reviewedOn);
|
await review(reviewedOn);
|
||||||
await switchReopenAndGoBack(reviewedOn, switchedTo);
|
// The user's own node for that network, set in Settings.
|
||||||
|
const ownNode = "https://" + reviewedOn + ".node.example.invalid";
|
||||||
|
state.rpcUrl = ownNode;
|
||||||
|
// The Settings gear, then its network select.
|
||||||
|
pushCurrentView();
|
||||||
|
showView("settings");
|
||||||
|
await onChainSwitch(switchedTo);
|
||||||
|
|
||||||
|
// The reopened popup loads what was saved onto a blank page, and
|
||||||
|
// Back draws the confirmation screen again from it.
|
||||||
|
await loadState();
|
||||||
|
elements.clear();
|
||||||
|
resetRenderedViews();
|
||||||
|
setBackRenderer(makeBackRenderer(state, { confirmTx }));
|
||||||
|
confirmTx.init({});
|
||||||
|
mockNode.feeAsked = [];
|
||||||
|
goBack();
|
||||||
|
await settle();
|
||||||
|
|
||||||
expect(state.currentView).toBe("confirm-tx");
|
expect(state.currentView).toBe("confirm-tx");
|
||||||
expectNativeTransferOn(reviewedOn);
|
expect(el("confirm-type").textContent).toBe(
|
||||||
expect(mockNode.asked).toEqual([
|
"Native " + symbol + " transfer",
|
||||||
{
|
);
|
||||||
method: "getFeeData",
|
expect(el("confirm-balance").textContent).toBe("5.0000 " + symbol);
|
||||||
network: reviewedOn,
|
expect(el("confirm-fee-amount").textContent).toBe(
|
||||||
url: ownNode(reviewedOn),
|
"~0.0002 " + symbol,
|
||||||
},
|
);
|
||||||
{
|
expect(mockNode.feeAsked).toEqual([
|
||||||
method: "getCode",
|
{ network: reviewedOn, url: ownNode },
|
||||||
network: reviewedOn,
|
|
||||||
url: ownNode(reviewedOn),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "getTransactionCount",
|
|
||||||
network: reviewedOn,
|
|
||||||
url: ownNode(reviewedOn),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
expect(global.fetch.mock.calls.map(([url]) => url)).toEqual([
|
|
||||||
explorerUrl + "/address/" + RECIPIENT,
|
|
||||||
]);
|
]);
|
||||||
expect(el("btn-confirm-send").disabled).toBe(false);
|
expect(el("btn-confirm-send").disabled).toBe(false);
|
||||||
|
|
||||||
@@ -419,78 +317,6 @@ describe.each([
|
|||||||
" reviewed, so it was not sent.",
|
" reviewed, so it was not sent.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("is sent on that network, as described, once it is chosen in Settings again and Back pressed", async () => {
|
|
||||||
await review(reviewedOn);
|
|
||||||
await switchReopenAndGoBack(reviewedOn, switchedTo);
|
|
||||||
// The Settings gear, the reviewed network chosen, and Back, which
|
|
||||||
// only unhides the screen as it was drawn.
|
|
||||||
pushCurrentView();
|
|
||||||
showView("settings");
|
|
||||||
await onChainSwitch(reviewedOn);
|
|
||||||
goBack();
|
|
||||||
|
|
||||||
expect(state.currentView).toBe("confirm-tx");
|
|
||||||
expectNativeTransferOn(reviewedOn);
|
|
||||||
|
|
||||||
await signAndSend();
|
|
||||||
|
|
||||||
const chainId = BigInt(NETWORKS[reviewedOn].chainId);
|
|
||||||
expect(mockNode.broadcast).toHaveLength(1);
|
|
||||||
expect(mockNode.broadcast[0].chainId).toBe(chainId);
|
|
||||||
expect(mockNode.broadcast[0].url).toBe(ownNode(reviewedOn));
|
|
||||||
expect(Transaction.from(mockNode.broadcast[0].signed).chainId).toBe(
|
|
||||||
chainId,
|
|
||||||
);
|
|
||||||
expect(txStatus.showWait).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("for more than the balance, says so in that network's native currency", async () => {
|
|
||||||
await review(reviewedOn, { amount: "10" });
|
|
||||||
await switchReopenAndGoBack(reviewedOn, switchedTo);
|
|
||||||
|
|
||||||
expect(el("confirm-errors").innerHTML).toContain(
|
|
||||||
"Insufficient balance. You have 5.0000 " +
|
|
||||||
symbol +
|
|
||||||
" but are trying to send 10 " +
|
|
||||||
symbol +
|
|
||||||
".",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("with a fee quoted by a node with no type-2 pricing, shows the fee in that network's native currency and price", async () => {
|
|
||||||
await review(reviewedOn);
|
|
||||||
mockNode.legacyFees = true;
|
|
||||||
await switchReopenAndGoBack(reviewedOn, switchedTo);
|
|
||||||
|
|
||||||
expect(el("confirm-fee-amount").textContent).toBe(
|
|
||||||
"0.0002 " + symbol + usdOn(reviewedOn, "$0.42"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("of an ERC-20 token, links the token contract to that network's explorer and prices the token by that network", async () => {
|
|
||||||
await review(reviewedOn, {
|
|
||||||
token: TOKEN,
|
|
||||||
tokenSymbol: "USDC",
|
|
||||||
tokenBalance: "50",
|
|
||||||
tokenDecimals: 6,
|
|
||||||
amount: "10",
|
|
||||||
});
|
|
||||||
await switchReopenAndGoBack(reviewedOn, switchedTo);
|
|
||||||
|
|
||||||
expect(el("confirm-type").textContent).toBe(
|
|
||||||
"ERC-20 token transfer (USDC)",
|
|
||||||
);
|
|
||||||
expect(el("confirm-token-contract").innerHTML).toContain(
|
|
||||||
`href="${explorerUrl}/address/${TOKEN}"`,
|
|
||||||
);
|
|
||||||
expect(el("confirm-amount").textContent).toBe(
|
|
||||||
"10 USDC" + usdOn(reviewedOn, "$10.00"),
|
|
||||||
);
|
|
||||||
expect(el("confirm-balance").textContent).toBe(
|
|
||||||
"50.0000 USDC" + usdOn(reviewedOn, "$50.00"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -259,36 +259,6 @@ describe("reaching the screen", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Both screens tell the user what they need to get the wallet back, and a
|
|
||||||
// key or xprv wallet has no recovery phrase: telling its owner to rely on
|
|
||||||
// one sends them looking for words that do not exist
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/529).
|
|
||||||
describe("what the screens say the wallet's backup is", () => {
|
|
||||||
test.each([
|
|
||||||
["hd", "recovery phrase"],
|
|
||||||
["key", "private key"],
|
|
||||||
["xprv", "extended private key"],
|
|
||||||
])(
|
|
||||||
"a wallet of type %s is told about its %s",
|
|
||||||
async (type, keyMaterial) => {
|
|
||||||
const { deleteWallet, state } = load();
|
|
||||||
state.wallets[1].type = type;
|
|
||||||
|
|
||||||
deleteWallet.show(1);
|
|
||||||
expect(state.currentView).toBe("delete-wallet-confirm");
|
|
||||||
expect(node("delete-wallet-key-material").textContent).toBe(
|
|
||||||
keyMaterial,
|
|
||||||
);
|
|
||||||
|
|
||||||
await click("btn-delete-wallet-lost-password");
|
|
||||||
expect(state.currentView).toBe(VIEW);
|
|
||||||
expect(node("delete-wallet-lost-key-material").textContent).toBe(
|
|
||||||
keyMaterial,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("the typed confirmation", () => {
|
describe("the typed confirmation", () => {
|
||||||
test("a name that is not the wallet's deletes nothing", async () => {
|
test("a name that is not the wallet's deletes nothing", async () => {
|
||||||
const { deleteWallet, state, storage } = load();
|
const { deleteWallet, state, storage } = load();
|
||||||
|
|||||||
@@ -453,20 +453,6 @@ const CONTRACT = [
|
|||||||
views: ["confirm-tx"],
|
views: ["confirm-tx"],
|
||||||
restored: false,
|
restored: false,
|
||||||
},
|
},
|
||||||
// A chain id naming no network has no explorer to link to.
|
|
||||||
{
|
|
||||||
value: {
|
|
||||||
pendingTx: {
|
|
||||||
token: "ETH",
|
|
||||||
from: ADDRESS,
|
|
||||||
to: ADDRESS,
|
|
||||||
amount: "1",
|
|
||||||
chainId: "0x5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
views: ["confirm-tx"],
|
|
||||||
restored: false,
|
|
||||||
},
|
|
||||||
// wait-tx passes on `pendingWait.hash`; restoreWait() has checked
|
// wait-tx passes on `pendingWait.hash`; restoreWait() has checked
|
||||||
// the fields below it since it was written, and this is the
|
// the fields below it since it was written, and this is the
|
||||||
// regression guard.
|
// regression guard.
|
||||||
|
|||||||
Reference in New Issue
Block a user