Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe1e84b071 |
@@ -1013,10 +1013,12 @@ rendered differently on two screens.
|
|||||||
|
|
||||||
The native token's label is a network's `nativeCurrency` in
|
The native token's label is a network's `nativeCurrency` in
|
||||||
`src/shared/networks.js`: `ETH` on mainnet, `SepoliaETH` on Sepolia. The
|
`src/shared/networks.js`: `ETH` on mainnet, `SepoliaETH` on Sepolia. The
|
||||||
wallet's balances and the Send and confirmation screens, which send on the
|
wallet's balances and the Send screen, which sends on the active network, use
|
||||||
active network, use the active network's. A transaction's figures use the one of
|
the active network's. The confirmation screen uses the one of the network its
|
||||||
the network its chain id names, whichever network is active: the value and fee
|
transaction was reviewed on, the network active when Send showed it, even after
|
||||||
on the approval screen, the amount on the wait, success and error screens, the
|
the network is switched in Settings. A transaction's figures use the one of the
|
||||||
|
network its chain id names, whichever network is active: the value and fee on
|
||||||
|
the approval screen, the amount on the wait, success and error screens, the
|
||||||
transaction history and the transaction detail screen, and the refusal of a fee
|
transaction history and the transaction detail screen, and the refusal of a fee
|
||||||
above 1 ETH. A chain id that names no network reads `ETH`. Wherever this
|
above 1 ETH. A chain id that names no network reads `ETH`. Wherever this
|
||||||
document shows ETH as the label of a native balance, value or fee, in a "Native
|
document shows ETH as the label of a native balance, value or fee, in a "Native
|
||||||
@@ -1781,12 +1783,16 @@ view would leave a wallet one click from deletion.
|
|||||||
the contract's own answer is read at signing time only to be compared with
|
the contract's own answer is read at signing time only to be compared with
|
||||||
it, and a disagreement is a refusal rather than a preference for either
|
it, and a disagreement is a refusal rather than a preference for either
|
||||||
value (`src/shared/transferAmount.js`)
|
value (`src/shared/transferAmount.js`)
|
||||||
- "Sign & Send" after the active network was switched in Settings while this
|
- "Sign & Send" after the active network was switched in Settings since this
|
||||||
screen was open → nothing is signed; "The active network changed after
|
transaction was reviewed, with this screen open or before the popup
|
||||||
|
reopened onto it → nothing is signed; "The active network changed after
|
||||||
this transaction was reviewed, so it was not sent. Please go back and
|
this transaction was reviewed, so it was not sent. Please go back and
|
||||||
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 the screen was shown for, so it cannot
|
carries the chain id of the network active when Send showed this screen,
|
||||||
be signed for another
|
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:
|
||||||
|
its amounts are in that network's native currency, and the fee estimate
|
||||||
|
and the recipient checks ask that network's RPC endpoint
|
||||||
- "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
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const {
|
|||||||
addressTitle,
|
addressTitle,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
displaySymbol,
|
displaySymbol,
|
||||||
nativeCurrency,
|
|
||||||
renderAddressHtml,
|
renderAddressHtml,
|
||||||
blockieHtml,
|
blockieHtml,
|
||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
@@ -20,6 +19,10 @@ const {
|
|||||||
formatFee,
|
formatFee,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, currentNetwork } = require("../../shared/state");
|
const { state, currentNetwork } = require("../../shared/state");
|
||||||
|
const {
|
||||||
|
networkByChainId,
|
||||||
|
nativeCurrencyByChainId,
|
||||||
|
} = require("../../shared/networks");
|
||||||
const { getSignerForAddress } = require("../../shared/wallet");
|
const { getSignerForAddress } = require("../../shared/wallet");
|
||||||
const { walletDefect } = require("../../shared/walletDefects");
|
const { walletDefect } = require("../../shared/walletDefects");
|
||||||
const { decryptWithPassword } = require("../../shared/vault");
|
const { decryptWithPassword } = require("../../shared/vault");
|
||||||
@@ -87,7 +90,9 @@ function valueWithUsd(text, usdAmount) {
|
|||||||
// once its amount is re-derived from the fee estimate.
|
// once its amount is re-derived from the fee estimate.
|
||||||
function renderAmount(txInfo) {
|
function renderAmount(txInfo) {
|
||||||
const isErc20 = txInfo.token !== "ETH";
|
const isErc20 = txInfo.token !== "ETH";
|
||||||
const rawSymbol = isErc20 ? txInfo.tokenSymbol || "?" : nativeCurrency();
|
const rawSymbol = isErc20
|
||||||
|
? txInfo.tokenSymbol || "?"
|
||||||
|
: nativeCurrencyByChainId(txInfo.chainId);
|
||||||
const price = isErc20 ? getPrice(rawSymbol) : getPrice("ETH");
|
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(
|
||||||
@@ -101,16 +106,21 @@ function show(txInfo) {
|
|||||||
feeStatus = FEE_PENDING;
|
feeStatus = FEE_PENDING;
|
||||||
feeWei = null;
|
feeWei = null;
|
||||||
maxSendFees = null;
|
maxSendFees = null;
|
||||||
// The network this screen is drawn for, and the only one it sends on:
|
// The network this screen is drawn for, and the only one it sends on: the
|
||||||
// Settings can switch the active network while the screen is open. The
|
// one active when Send handed the transaction over. Drawn again from the
|
||||||
// wait, success and error screens label the amount by it too.
|
// saved transaction, as when the popup reopens onto this screen, it keeps
|
||||||
txInfo.chainId = currentNetwork().chainId;
|
// that network, so Sign & Send refuses once Settings has switched to
|
||||||
|
// another. This screen and the wait, success and error screens label
|
||||||
|
// amounts by it.
|
||||||
|
if (!txInfo.chainId) txInfo.chainId = currentNetwork().chainId;
|
||||||
|
|
||||||
const isErc20 = txInfo.token !== "ETH";
|
const isErc20 = txInfo.token !== "ETH";
|
||||||
// The raw symbol is the price-table key; the capped one is what the
|
// The raw symbol is the price-table key; the capped one is what the
|
||||||
// screen says. Truncating before the lookup would silently drop the
|
// screen says. Truncating before the lookup would silently drop the
|
||||||
// price of any token whose symbol is long enough to be capped.
|
// price of any token whose symbol is long enough to be capped.
|
||||||
const rawSymbol = isErc20 ? txInfo.tokenSymbol || "?" : nativeCurrency();
|
const rawSymbol = isErc20
|
||||||
|
? txInfo.tokenSymbol || "?"
|
||||||
|
: nativeCurrencyByChainId(txInfo.chainId);
|
||||||
const symbol = displaySymbol(rawSymbol);
|
const symbol = displaySymbol(rawSymbol);
|
||||||
|
|
||||||
// Transaction type
|
// Transaction type
|
||||||
@@ -212,9 +222,9 @@ function show(txInfo) {
|
|||||||
$("confirm-gas-error").classList.toggle("hidden", !isErc20);
|
$("confirm-gas-error").classList.toggle("hidden", !isErc20);
|
||||||
$("confirm-gas-error").textContent =
|
$("confirm-gas-error").textContent =
|
||||||
"You do not have enough " +
|
"You do not have enough " +
|
||||||
nativeCurrency() +
|
nativeCurrencyByChainId(txInfo.chainId) +
|
||||||
" to pay the network fee for this transfer. Please add " +
|
" to pay the network fee for this transfer. Please add " +
|
||||||
nativeCurrency() +
|
nativeCurrencyByChainId(txInfo.chainId) +
|
||||||
" to this address and try again.";
|
" to this address and try again.";
|
||||||
|
|
||||||
// Both shown later, by checkRecipientHistory(). The first sentence says
|
// Both shown later, by checkRecipientHistory(). The first sentence says
|
||||||
@@ -222,12 +232,12 @@ function show(txInfo) {
|
|||||||
$("confirm-recipient-warning").textContent =
|
$("confirm-recipient-warning").textContent =
|
||||||
"WARNING: The recipient address has never sent a transaction and" +
|
"WARNING: The recipient address has never sent a transaction and" +
|
||||||
" holds no " +
|
" holds no " +
|
||||||
nativeCurrency() +
|
nativeCurrencyByChainId(txInfo.chainId) +
|
||||||
". This may indicate a fresh or unused address. Double-check the" +
|
". This may indicate a fresh or unused address. Double-check the" +
|
||||||
" address before sending.";
|
" address before sending.";
|
||||||
$("confirm-contract-warning").textContent =
|
$("confirm-contract-warning").textContent =
|
||||||
"WARNING: The recipient is a smart contract. Sending " +
|
"WARNING: The recipient is a smart contract. Sending " +
|
||||||
nativeCurrency() +
|
nativeCurrencyByChainId(txInfo.chainId) +
|
||||||
" or tokens directly to a contract may result in permanent loss of" +
|
" or tokens directly to a contract may result in permanent loss of" +
|
||||||
" funds.";
|
" funds.";
|
||||||
|
|
||||||
@@ -280,7 +290,7 @@ function renderValidation(txInfo) {
|
|||||||
const isErc20 = txInfo.token !== "ETH";
|
const isErc20 = txInfo.token !== "ETH";
|
||||||
const symbol = isErc20
|
const symbol = isErc20
|
||||||
? displaySymbol(txInfo.tokenSymbol || "?")
|
? displaySymbol(txInfo.tokenSymbol || "?")
|
||||||
: nativeCurrency();
|
: nativeCurrencyByChainId(txInfo.chainId);
|
||||||
|
|
||||||
const { canSend, codes } = validateTransfer({
|
const { canSend, codes } = validateTransfer({
|
||||||
isErc20,
|
isErc20,
|
||||||
@@ -369,9 +379,23 @@ function setVisible(id, visible) {
|
|||||||
$(id).style.visibility = visible ? "visible" : "hidden";
|
$(id).style.visibility = visible ? "visible" : "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The node of the network this transaction was reviewed for, which need not be
|
||||||
|
// the active one: the popup can reopen onto this screen after Settings switched
|
||||||
|
// the network. Its RPC URL is the one Settings has for that network: the
|
||||||
|
// active network's is state.rpcUrl, any other's the one remembered for it (see
|
||||||
|
// applyChainSwitchFields()).
|
||||||
|
function reviewedNetworkProvider(txInfo) {
|
||||||
|
const net = networkByChainId(txInfo.chainId);
|
||||||
|
if (net.id === state.networkId) {
|
||||||
|
return getProvider(state.rpcUrl, net.id);
|
||||||
|
}
|
||||||
|
const remembered = state.networkEndpoints[net.id] || {};
|
||||||
|
return getProvider(remembered.rpcUrl || net.defaultRpcUrl, net.id);
|
||||||
|
}
|
||||||
|
|
||||||
async function estimateGas(txInfo) {
|
async function estimateGas(txInfo) {
|
||||||
try {
|
try {
|
||||||
const provider = getProvider(state.rpcUrl, state.networkId);
|
const provider = reviewedNetworkProvider(txInfo);
|
||||||
const feeData = await provider.getFeeData();
|
const feeData = await provider.getFeeData();
|
||||||
let gasLimit;
|
let gasLimit;
|
||||||
|
|
||||||
@@ -419,12 +443,13 @@ async function estimateGas(txInfo) {
|
|||||||
// does, so the same fee reads the same on both.
|
// does, so the same fee reads the same on both.
|
||||||
if (estimateWei !== null && estimateWei < gasCostWei) {
|
if (estimateWei !== null && estimateWei < gasCostWei) {
|
||||||
$("confirm-fee-amount").textContent =
|
$("confirm-fee-amount").textContent =
|
||||||
"~" + formatFee(estimateWei, nativeCurrency());
|
"~" +
|
||||||
|
formatFee(estimateWei, nativeCurrencyByChainId(txInfo.chainId));
|
||||||
$("confirm-fee-reserve").textContent =
|
$("confirm-fee-reserve").textContent =
|
||||||
"up to " +
|
"up to " +
|
||||||
truncateAmountNeverZero(formatEther(gasCostWei)) +
|
truncateAmountNeverZero(formatEther(gasCostWei)) +
|
||||||
" " +
|
" " +
|
||||||
nativeCurrency() +
|
nativeCurrencyByChainId(txInfo.chainId) +
|
||||||
" reserved";
|
" reserved";
|
||||||
setVisible("confirm-fee-reserve", true);
|
setVisible("confirm-fee-reserve", true);
|
||||||
} else {
|
} else {
|
||||||
@@ -433,7 +458,7 @@ async function estimateGas(txInfo) {
|
|||||||
// cost is not below the reserve. Show the reserve alone.
|
// cost is not below the reserve. Show the reserve alone.
|
||||||
$("confirm-fee-amount").textContent = formatFee(
|
$("confirm-fee-amount").textContent = formatFee(
|
||||||
gasCostWei,
|
gasCostWei,
|
||||||
nativeCurrency(),
|
nativeCurrencyByChainId(txInfo.chainId),
|
||||||
);
|
);
|
||||||
setVisible("confirm-fee-reserve", false);
|
setVisible("confirm-fee-reserve", false);
|
||||||
}
|
}
|
||||||
@@ -523,7 +548,7 @@ function showSendError(message) {
|
|||||||
|
|
||||||
async function checkRecipientHistory(txInfo) {
|
async function checkRecipientHistory(txInfo) {
|
||||||
try {
|
try {
|
||||||
const provider = getProvider(state.rpcUrl, state.networkId);
|
const provider = reviewedNetworkProvider(txInfo);
|
||||||
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
|
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
|
||||||
fromAddress: txInfo.from,
|
fromAddress: txInfo.from,
|
||||||
});
|
});
|
||||||
@@ -564,8 +589,10 @@ function init(_ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back from Settings only unhides this screen, still drawn for the
|
// The screen describes the network the transaction was reviewed for,
|
||||||
// network that was active when it was shown.
|
// which Settings may have switched away from since: with this screen
|
||||||
|
// open, where Back only unhides it, or before the popup reopened onto
|
||||||
|
// it.
|
||||||
if (currentNetwork().chainId !== pendingTx.chainId) {
|
if (currentNetwork().chainId !== pendingTx.chainId) {
|
||||||
showSendError(
|
showSendError(
|
||||||
"The active network changed after this transaction was" +
|
"The active network changed after this transaction was" +
|
||||||
|
|||||||
@@ -7,25 +7,36 @@
|
|||||||
// reviewed on Sepolia went out as 1 ETH on mainnet once mainnet was chosen in
|
// reviewed on Sepolia went out as 1 ETH on mainnet once mainnet was chosen in
|
||||||
// Settings in between.
|
// Settings in between.
|
||||||
//
|
//
|
||||||
|
// The same holds when the popup was closed after the switch and reopened, and
|
||||||
|
// Back drew the screen again from the transaction it saved
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/586): the screen was drawn for
|
||||||
|
// the new network, and Sign & Send sent the reviewed transfer there.
|
||||||
|
//
|
||||||
// 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 and the DOM stubbed. The stub node is on the network its
|
// only the node and the DOM stubbed. The stub node is on the network 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 signed transactions the stub node was handed, with its network's chain id.
|
// The nodes asked for the fee, by network name and RPC URL, in order. And the
|
||||||
const mockNode = { broadcast: [] };
|
// signed transactions the stub node was handed, with its network's chain id.
|
||||||
|
const mockNode = { feeAsked: [], broadcast: [] };
|
||||||
|
|
||||||
jest.mock("ethers", () => {
|
jest.mock("ethers", () => {
|
||||||
const actual = jest.requireActual("ethers");
|
const actual = jest.requireActual("ethers");
|
||||||
class StubProvider {
|
class StubProvider {
|
||||||
constructor(url, network) {
|
constructor(url, network) {
|
||||||
|
this.url = url;
|
||||||
this.network = network;
|
this.network = network;
|
||||||
}
|
}
|
||||||
async getNetwork() {
|
async getNetwork() {
|
||||||
return this.network;
|
return this.network;
|
||||||
}
|
}
|
||||||
async getFeeData() {
|
async getFeeData() {
|
||||||
|
mockNode.feeAsked.push({
|
||||||
|
network: this.network.name,
|
||||||
|
url: this.url,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
maxFeePerGas: 20000000000n,
|
maxFeePerGas: 20000000000n,
|
||||||
maxPriorityFeePerGas: 1000000000n,
|
maxPriorityFeePerGas: 1000000000n,
|
||||||
@@ -128,6 +139,16 @@ 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 {
|
||||||
|
goBack,
|
||||||
|
pushCurrentView,
|
||||||
|
setBackRenderer,
|
||||||
|
showView,
|
||||||
|
} = require("../src/popup/views/helpers");
|
||||||
|
const {
|
||||||
|
makeBackRenderer,
|
||||||
|
resetRenderedViews,
|
||||||
|
} = require("../src/popup/viewRouter");
|
||||||
const txStatus = require("../src/popup/views/txStatus");
|
const txStatus = require("../src/popup/views/txStatus");
|
||||||
const confirmTx = require("../src/popup/views/confirmTx");
|
const confirmTx = require("../src/popup/views/confirmTx");
|
||||||
|
|
||||||
@@ -150,6 +171,7 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
elements.clear();
|
elements.clear();
|
||||||
|
mockNode.feeAsked = [];
|
||||||
mockNode.broadcast = [];
|
mockNode.broadcast = [];
|
||||||
txStatus.showWait.mockClear();
|
txStatus.showWait.mockClear();
|
||||||
txStatus.showError.mockClear();
|
txStatus.showError.mockClear();
|
||||||
@@ -187,6 +209,12 @@ async function review(networkId) {
|
|||||||
tokenDecimals: null,
|
tokenDecimals: null,
|
||||||
max: false,
|
max: false,
|
||||||
});
|
});
|
||||||
|
await settle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// show() estimates the fee and checks the recipient without awaiting either;
|
||||||
|
// this lets both finish.
|
||||||
|
async function settle() {
|
||||||
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +266,60 @@ describe.each([
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
["sepolia", "mainnet", "SepoliaETH"],
|
||||||
|
["mainnet", "sepolia", "ETH"],
|
||||||
|
])(
|
||||||
|
"a transfer reviewed on %s, with %s chosen in Settings, the popup reopened and Back pressed",
|
||||||
|
(reviewedOn, switchedTo, symbol) => {
|
||||||
|
test("is described on the network it was reviewed for, and neither signed nor sent", async () => {
|
||||||
|
await review(reviewedOn);
|
||||||
|
// 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(el("confirm-type").textContent).toBe(
|
||||||
|
"Native " + symbol + " transfer",
|
||||||
|
);
|
||||||
|
expect(el("confirm-balance").textContent).toBe("5.0000 " + symbol);
|
||||||
|
expect(el("confirm-fee-amount").textContent).toBe(
|
||||||
|
"~0.0002 " + symbol,
|
||||||
|
);
|
||||||
|
expect(mockNode.feeAsked).toEqual([
|
||||||
|
{ network: reviewedOn, url: ownNode },
|
||||||
|
]);
|
||||||
|
expect(el("btn-confirm-send").disabled).toBe(false);
|
||||||
|
|
||||||
|
await signAndSend();
|
||||||
|
|
||||||
|
expect(signTransaction).not.toHaveBeenCalled();
|
||||||
|
expect(mockNode.broadcast).toEqual([]);
|
||||||
|
expect(txStatus.showWait).not.toHaveBeenCalled();
|
||||||
|
expect(txStatus.showError).not.toHaveBeenCalled();
|
||||||
|
expect(el("confirm-errors").innerHTML).toContain(
|
||||||
|
"The active network changed after this transaction was" +
|
||||||
|
" reviewed, so it was not sent.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// The check in the click handler tells the user; the send itself carries the
|
// The check in the click handler tells the user; the send itself carries the
|
||||||
// chain id the screen was drawn for, so a signer on another network's node
|
// chain id the screen was drawn for, so a signer on another network's node
|
||||||
// cannot sign it even if the network changes after that check.
|
// cannot sign it even if the network changes after that check.
|
||||||
|
|||||||
Reference in New Issue
Block a user