Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1ac51d514 | ||
|
|
fc042c3191 |
@@ -1742,16 +1742,18 @@ view would leave a wallet one click from deletion.
|
|||||||
plus four reserved warning boxes made visible by the async checks —
|
plus four reserved warning boxes made visible by the async checks —
|
||||||
recipient with no transaction history, recipient is a contract, burn
|
recipient with no transaction history, recipient is a contract, burn
|
||||||
address, and an Etherscan phishing/scam label
|
address, and an Etherscan phishing/scam label
|
||||||
- Errors (insufficient balance, or a balance that could not be read, which
|
- Errors (an invalid amount), plus five reserved error boxes, so the balance
|
||||||
asks the user to go back and try again), plus three reserved error boxes —
|
and the fee arriving later move nothing — the amount exceeds the balance
|
||||||
the amount plus the fee exceeds the balance (ETH transfers), not enough
|
(for a token whose scale is unknown it says the token's balance is
|
||||||
ETH to pay the fee for the transfer (ERC-20 transfers), and the fee could
|
unknown), the balance could not be read (it asks the user to go back and
|
||||||
not be estimated. The first two are mutually exclusive per transfer type,
|
try again), the amount plus the fee exceeds the balance (ETH transfers),
|
||||||
so only the applicable one holds space. The last names its cause: for a
|
not enough ETH to pay the fee for the transfer (ERC-20 transfers), and the
|
||||||
token whose scale is unknown the fee can never be estimated, and it says
|
fee could not be estimated. The third and fourth are mutually exclusive
|
||||||
the wallet does not know how many decimal places the token uses and that
|
per transfer type, so only the applicable one holds space. The last names
|
||||||
the transaction cannot be sent; for any other failure it asks the user to
|
its cause: for a token whose scale is unknown the fee can never be
|
||||||
go back and try again
|
estimated, and it says the wallet does not know how many decimal places
|
||||||
|
the token uses and that the transaction cannot be sent; for any other
|
||||||
|
failure it asks the user to go back and try again
|
||||||
- Password: an inline field on this screen, not a modal, with its own error
|
- Password: an inline field on this screen, not a modal, with its own error
|
||||||
line
|
line
|
||||||
- "Sign & Send" button (disabled if errors, while the balance is being read,
|
- "Sign & Send" button (disabled if errors, while the balance is being read,
|
||||||
@@ -2186,14 +2188,17 @@ view would leave a wallet one click from deletion.
|
|||||||
code `4100` and opens no prompt. Two such origins are `null`, which the
|
code `4100` and opens no prompt. Two such origins are `null`, which the
|
||||||
browser reports for a page with an opaque origin, such as one its server
|
browser reports for a page with an opaque origin, such as one its server
|
||||||
sandboxes with a Content-Security-Policy, and `file://`, which Chrome reports
|
sandboxes with a Content-Security-Policy, and `file://`, which Chrome reports
|
||||||
for a local file. The background script prefers the toolbar popup
|
for a local file. Nor is such a page told the active address by
|
||||||
(`action.openPopup()`) and falls back to a separate popup window
|
`accountsChanged`, even at a connected site's URL: the background names the
|
||||||
(`src/background/index.js`, `requestApproval()`). Only one exists per site at
|
origin of the tab's URL on the event, and the content script passes the
|
||||||
a time: a further connection request from a site whose prompt is still
|
address on only to a page whose own origin is that one. The background script
|
||||||
unanswered is refused with EIP-1193 code `-32002` and opens no new prompt. If
|
prefers the toolbar popup (`action.openPopup()`) and falls back to a separate
|
||||||
that prompt was in a toolbar popup that closed before it connected, and the
|
popup window (`src/background/index.js`, `requestApproval()`). Only one exists
|
||||||
toolbar popup has since been set to open something else, the refused request
|
per site at a time: a further connection request from a site whose prompt is
|
||||||
shows that prompt again.
|
still unanswered is refused with EIP-1193 code `-32002` and opens no new
|
||||||
|
prompt. If that prompt was in a toolbar popup that closed before it connected,
|
||||||
|
and the toolbar popup has since been set to open something else, the refused
|
||||||
|
request shows that prompt again.
|
||||||
- **Elements**:
|
- **Elements**:
|
||||||
- "Connection Request" heading
|
- "Connection Request" heading
|
||||||
- Phishing warning banner (shown when the hostname is on the phishing
|
- Phishing warning banner (shown when the hostname is on the phishing
|
||||||
|
|||||||
@@ -1308,6 +1308,12 @@ async function broadcastAccountsChanged() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
|
// The tab's URL is all the background knows of the page in it, and a
|
||||||
|
// page at a site's URL may have another origin: a page its server
|
||||||
|
// sandboxes with a Content-Security-Policy has the origin "null". So
|
||||||
|
// the event names the origin it was judged by, and the content script
|
||||||
|
// passes the address on only to a page whose own origin is that one
|
||||||
|
// (src/content/index.js).
|
||||||
const origin = tab.url ? new URL(tab.url).origin : "";
|
const origin = tab.url ? new URL(tab.url).origin : "";
|
||||||
const hasPermission =
|
const hasPermission =
|
||||||
activeAddress &&
|
activeAddress &&
|
||||||
@@ -1319,6 +1325,7 @@ async function broadcastAccountsChanged() {
|
|||||||
type: "AUTISTMASK_EVENT",
|
type: "AUTISTMASK_EVENT",
|
||||||
eventName: "accountsChanged",
|
eventName: "accountsChanged",
|
||||||
data: hasPermission ? [activeAddress] : [],
|
data: hasPermission ? [activeAddress] : [],
|
||||||
|
origin,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-9
@@ -49,14 +49,23 @@ window.addEventListener("message", (event) => {
|
|||||||
|
|
||||||
// Listen for events pushed from the background (e.g. accountsChanged)
|
// Listen for events pushed from the background (e.g. accountsChanged)
|
||||||
runtimeApi().onMessage.addListener((msg) => {
|
runtimeApi().onMessage.addListener((msg) => {
|
||||||
if (msg.type === "AUTISTMASK_EVENT") {
|
if (msg.type !== "AUTISTMASK_EVENT") return;
|
||||||
window.postMessage(
|
// The background chooses the addresses in accountsChanged for the site
|
||||||
{
|
// at the origin the event names, which is the origin of the tab's URL.
|
||||||
type: "AUTISTMASK_EVENT",
|
// This page hears them only when its own origin is that one. A page whose
|
||||||
eventName: msg.eventName,
|
// origin has no host is not a site: one its server sandboxes with a
|
||||||
data: msg.data,
|
// Content-Security-Policy has the origin "null" whatever its URL, and is
|
||||||
},
|
// told no address, as its requests are refused.
|
||||||
"*",
|
let data = msg.data;
|
||||||
);
|
if (msg.eventName === "accountsChanged" && msg.origin !== window.origin) {
|
||||||
|
data = [];
|
||||||
}
|
}
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: "AUTISTMASK_EVENT",
|
||||||
|
eventName: msg.eventName,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -669,6 +669,20 @@
|
|||||||
id="confirm-errors"
|
id="confirm-errors"
|
||||||
class="mb-2 border border-border border-dashed p-2 invisible min-h-[1.25rem]"
|
class="mb-2 border border-border border-dashed p-2 invisible min-h-[1.25rem]"
|
||||||
></div>
|
></div>
|
||||||
|
<!-- Its sentence names the token, or why the token's balance
|
||||||
|
is unknown, so show() in confirmTx.js sets it. -->
|
||||||
|
<div
|
||||||
|
id="confirm-balance-error"
|
||||||
|
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
id="confirm-balance-read-error"
|
||||||
|
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
||||||
|
>
|
||||||
|
Your balance could not be read from the network, so this
|
||||||
|
transaction cannot be checked against it. Please go back and
|
||||||
|
try again.
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
id="confirm-amount-fee-error"
|
id="confirm-amount-fee-error"
|
||||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
||||||
|
|||||||
@@ -263,6 +263,20 @@ function show(txInfo) {
|
|||||||
" cannot be checked against your balance. Please go back and" +
|
" cannot be checked against your balance. Please go back and" +
|
||||||
" try again.";
|
" try again.";
|
||||||
|
|
||||||
|
// So does the message for an amount the balance does not cover. It is
|
||||||
|
// set here, before the first paint, so the balance read returning only
|
||||||
|
// makes it visible and moves nothing, which is why it quotes no balance.
|
||||||
|
// Without the token's scale readBalances() reads no token balance.
|
||||||
|
$("confirm-balance-error").textContent =
|
||||||
|
isErc20 && toDecimals(txInfo.tokenDecimals) === null
|
||||||
|
? "This token's balance is unknown, because nothing this wallet" +
|
||||||
|
" can consult reports how many decimal places it uses, so the" +
|
||||||
|
" amount you are trying to send cannot be checked against it."
|
||||||
|
: "Your " +
|
||||||
|
symbol +
|
||||||
|
" balance does not cover this amount. Please go back and send" +
|
||||||
|
" a smaller amount.";
|
||||||
|
|
||||||
renderValidation(txInfo);
|
renderValidation(txInfo);
|
||||||
|
|
||||||
// Reset password field and error
|
// Reset password field and error
|
||||||
@@ -332,13 +346,11 @@ async function readBalances(txInfo, provider) {
|
|||||||
// re-running it never moves anything.
|
// re-running it never moves anything.
|
||||||
function renderValidation(txInfo) {
|
function renderValidation(txInfo) {
|
||||||
const isErc20 = txInfo.token !== "ETH";
|
const isErc20 = txInfo.token !== "ETH";
|
||||||
const symbol = isErc20
|
|
||||||
? displaySymbol(txInfo.tokenSymbol || "?")
|
|
||||||
: nativeCurrencyByChainId(txInfo.chainId);
|
|
||||||
|
|
||||||
// Null while the balances are being read and after a read that failed.
|
// Null while the balances are being read and after a read that failed.
|
||||||
// validateTransfer() takes a missing balance as nothing to spend from, so
|
// validateTransfer() takes a missing balance as nothing to spend from, so
|
||||||
// sending stays blocked either way, and no message below quotes one.
|
// sending stays blocked either way, and the only message shown for it is
|
||||||
|
// the one saying the read failed.
|
||||||
const ethBalance = balances ? balances.eth : null;
|
const ethBalance = balances ? balances.eth : null;
|
||||||
const tokenBalance = balances ? balances.token : null;
|
const tokenBalance = balances ? balances.token : null;
|
||||||
const balanceRead = ethBalance !== null;
|
const balanceRead = ethBalance !== null;
|
||||||
@@ -353,53 +365,14 @@ function renderValidation(txInfo) {
|
|||||||
feeWei,
|
feeWei,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Messages carrying the user's own numbers are built here; the fixed
|
// An invalid amount is known before the first paint, so its message is
|
||||||
// sentences live in the reserved elements in index.html, except the
|
// written here. Every message a later result brings has a reserved
|
||||||
// gas and fee-unknown ones, which show() sets.
|
// element, its sentence fixed in index.html or set by show(), and is only
|
||||||
|
// made visible below, so nothing moves when the result arrives.
|
||||||
const messages = [];
|
const messages = [];
|
||||||
if (codes.includes(CODES.AMOUNT_INVALID)) {
|
if (codes.includes(CODES.AMOUNT_INVALID)) {
|
||||||
messages.push("Please enter a valid amount to send.");
|
messages.push("Please enter a valid amount to send.");
|
||||||
}
|
}
|
||||||
if (balances !== null && !balanceRead) {
|
|
||||||
messages.push(
|
|
||||||
"Your balance could not be read from the network, so this" +
|
|
||||||
" transaction cannot be checked against it. Please go back" +
|
|
||||||
" and try again.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (balanceRead && codes.includes(CODES.INSUFFICIENT_TOKEN)) {
|
|
||||||
messages.push(
|
|
||||||
tokenBalance == null
|
|
||||||
? "This token's balance is unknown, because nothing this" +
|
|
||||||
" wallet can consult reports how many decimal places it" +
|
|
||||||
" uses, so the amount you are trying to send cannot be" +
|
|
||||||
" checked against it."
|
|
||||||
: "Insufficient " +
|
|
||||||
symbol +
|
|
||||||
" balance. You have " +
|
|
||||||
truncateAmountNeverZero(tokenBalance) +
|
|
||||||
" " +
|
|
||||||
symbol +
|
|
||||||
" but are trying to send " +
|
|
||||||
txInfo.amount +
|
|
||||||
" " +
|
|
||||||
symbol +
|
|
||||||
".",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (balanceRead && codes.includes(CODES.INSUFFICIENT_ETH)) {
|
|
||||||
messages.push(
|
|
||||||
"Insufficient balance. You have " +
|
|
||||||
truncateAmountNeverZero(ethBalance) +
|
|
||||||
" " +
|
|
||||||
symbol +
|
|
||||||
" but are trying to send " +
|
|
||||||
txInfo.amount +
|
|
||||||
" " +
|
|
||||||
symbol +
|
|
||||||
".",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorsEl = $("confirm-errors");
|
const errorsEl = $("confirm-errors");
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
@@ -412,6 +385,13 @@ function renderValidation(txInfo) {
|
|||||||
errorsEl.style.visibility = "hidden";
|
errorsEl.style.visibility = "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setVisible("confirm-balance-read-error", balances !== null && !balanceRead);
|
||||||
|
setVisible(
|
||||||
|
"confirm-balance-error",
|
||||||
|
balanceRead &&
|
||||||
|
(codes.includes(CODES.INSUFFICIENT_TOKEN) ||
|
||||||
|
codes.includes(CODES.INSUFFICIENT_ETH)),
|
||||||
|
);
|
||||||
setVisible(
|
setVisible(
|
||||||
"confirm-amount-fee-error",
|
"confirm-amount-fee-error",
|
||||||
balanceRead && codes.includes(CODES.INSUFFICIENT_ETH_WITH_FEE),
|
balanceRead && codes.includes(CODES.INSUFFICIENT_ETH_WITH_FEE),
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// A page whose origin has no host is never told an address by accountsChanged
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/564).
|
||||||
|
//
|
||||||
|
// When the user switches address, the background tells each open tab the new
|
||||||
|
// active address if the site at the tab's URL is connected. A page at a
|
||||||
|
// connected site's URL can still have another origin: one its server
|
||||||
|
// sandboxes with a Content-Security-Policy has the origin "null", and every
|
||||||
|
// request it makes is refused. Each test here switches the address in the real
|
||||||
|
// background, takes the event it sent to a tab at the site's URL, and hands
|
||||||
|
// that event to the real content script in a page of the given origin.
|
||||||
|
|
||||||
|
const { networkById } = require("../src/shared/networks");
|
||||||
|
const { makeStorageStub } = require("./support/storageStub");
|
||||||
|
|
||||||
|
const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
const SITE = "https://dapp.example";
|
||||||
|
|
||||||
|
const EXT_URL = "chrome-extension://autistmask/";
|
||||||
|
const FROM_POPUP = { url: EXT_URL + "src/popup/index.html" };
|
||||||
|
|
||||||
|
// One address, with SITE remembered as allowed for it.
|
||||||
|
function storedProfile() {
|
||||||
|
const mainnet = networkById("mainnet");
|
||||||
|
return {
|
||||||
|
hasWallet: true,
|
||||||
|
wallets: [
|
||||||
|
{
|
||||||
|
name: "Wallet 1",
|
||||||
|
type: "hd",
|
||||||
|
addresses: [
|
||||||
|
{ address: ADDRESS, balance: "0", tokenBalances: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
activeAddress: ADDRESS,
|
||||||
|
networkId: "mainnet",
|
||||||
|
rpcUrl: mainnet.defaultRpcUrl,
|
||||||
|
blockscoutUrl: mainnet.defaultBlockscoutUrl,
|
||||||
|
allowedSites: { [ADDRESS]: [SITE] },
|
||||||
|
deniedSites: {},
|
||||||
|
trackedTokens: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function settle() {
|
||||||
|
for (let i = 0; i < 50; i++) await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete global.chrome;
|
||||||
|
delete global.window;
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The popup tells the background the active address changed, and the one
|
||||||
|
// open tab, at SITE's URL, is sent an event. That event is returned.
|
||||||
|
async function eventSentToSiteTab() {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.doMock("../src/shared/balances", () => ({
|
||||||
|
getProvider: () => ({}),
|
||||||
|
refreshBalances: jest.fn(async () => {}),
|
||||||
|
}));
|
||||||
|
jest.doMock("../src/shared/phishingDomains", () => ({
|
||||||
|
isPhishingDomain: () => false,
|
||||||
|
}));
|
||||||
|
jest.doMock("../src/shared/alarms", () => ({
|
||||||
|
BALANCE_REFRESH_ALARM: "balance",
|
||||||
|
BALANCE_REFRESH_PERIOD_MINUTES: 1,
|
||||||
|
ensureRecurringAlarms: jest.fn(async () => {}),
|
||||||
|
registerAlarmHandlers: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let messageListener = null;
|
||||||
|
const sentToTabs = [];
|
||||||
|
global.chrome = {
|
||||||
|
storage: makeStorageStub({ autistmask: storedProfile() }),
|
||||||
|
runtime: {
|
||||||
|
getURL: (path) => EXT_URL + path,
|
||||||
|
onMessage: {
|
||||||
|
addListener: (fn) => {
|
||||||
|
messageListener = fn;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onConnect: { addListener: () => {} },
|
||||||
|
getPlatformInfo: (cb) => cb({ os: "linux" }),
|
||||||
|
lastError: null,
|
||||||
|
},
|
||||||
|
windows: {
|
||||||
|
remove: (id, cb) => {
|
||||||
|
if (cb) cb();
|
||||||
|
},
|
||||||
|
onRemoved: { addListener: () => {} },
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
query: (q, cb) => cb([{ id: 1, url: SITE + "/app" }]),
|
||||||
|
sendMessage: (tabId, msg, cb) => {
|
||||||
|
sentToTabs.push(msg);
|
||||||
|
if (cb) cb();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: { setPopup: () => {} },
|
||||||
|
};
|
||||||
|
|
||||||
|
require("../src/background/index");
|
||||||
|
messageListener(
|
||||||
|
{ type: "AUTISTMASK_ACTIVE_CHANGED" },
|
||||||
|
FROM_POPUP,
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(sentToTabs).toHaveLength(1);
|
||||||
|
return sentToTabs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The content script in a page whose origin is `pageOrigin` is handed `event`
|
||||||
|
// from the background. What it posts to the page is returned.
|
||||||
|
function postedToPage(event, pageOrigin) {
|
||||||
|
jest.resetModules();
|
||||||
|
let eventListener = null;
|
||||||
|
const posted = [];
|
||||||
|
global.chrome = {
|
||||||
|
runtime: {
|
||||||
|
onMessage: {
|
||||||
|
addListener: (fn) => {
|
||||||
|
eventListener = fn;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
global.window = {
|
||||||
|
origin: pageOrigin,
|
||||||
|
addEventListener: () => {},
|
||||||
|
postMessage: (data) => posted.push(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
require("../src/content/index");
|
||||||
|
eventListener(event);
|
||||||
|
|
||||||
|
return posted;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("accountsChanged after an address switch, at a connected site's URL", () => {
|
||||||
|
test("the site's own page is told the active address", async () => {
|
||||||
|
const event = await eventSentToSiteTab();
|
||||||
|
|
||||||
|
expect(postedToPage(event, SITE)).toEqual([
|
||||||
|
{
|
||||||
|
type: "AUTISTMASK_EVENT",
|
||||||
|
eventName: "accountsChanged",
|
||||||
|
data: [ADDRESS],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a page whose origin has no host is told no address", async () => {
|
||||||
|
const event = await eventSentToSiteTab();
|
||||||
|
|
||||||
|
expect(postedToPage(event, "null")).toEqual([
|
||||||
|
{
|
||||||
|
type: "AUTISTMASK_EVENT",
|
||||||
|
eventName: "accountsChanged",
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -259,6 +259,10 @@ function errors() {
|
|||||||
return el("confirm-errors").innerHTML;
|
return el("confirm-errors").innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shown(id) {
|
||||||
|
return el(id).style.visibility === "visible";
|
||||||
|
}
|
||||||
|
|
||||||
function sendDisabled() {
|
function sendDisabled() {
|
||||||
return el("btn-confirm-send").disabled;
|
return el("btn-confirm-send").disabled;
|
||||||
}
|
}
|
||||||
@@ -347,6 +351,7 @@ describe("a confirmation screen saved on mainnet and reopened after a switch to
|
|||||||
);
|
);
|
||||||
expect(el("confirm-balance").textContent).toBe("5.0000 SepoliaETH");
|
expect(el("confirm-balance").textContent).toBe("5.0000 SepoliaETH");
|
||||||
expect(errors()).toBe("");
|
expect(errors()).toBe("");
|
||||||
|
expect(shown("confirm-balance-error")).toBe(false);
|
||||||
expect(el("confirm-amount-fee-error").style.visibility).toBe("hidden");
|
expect(el("confirm-amount-fee-error").style.visibility).toBe("hidden");
|
||||||
expect(sendDisabled()).toBe(false);
|
expect(sendDisabled()).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -362,9 +367,10 @@ describe("a confirmation screen saved on mainnet and reopened after a switch to
|
|||||||
|
|
||||||
await reopenOn("sepolia");
|
await reopenOn("sepolia");
|
||||||
expect(el("confirm-balance").textContent).toBe("0.5000 SepoliaETH");
|
expect(el("confirm-balance").textContent).toBe("0.5000 SepoliaETH");
|
||||||
expect(errors()).toContain(
|
expect(shown("confirm-balance-error")).toBe(true);
|
||||||
"Insufficient balance. You have 0.5000 SepoliaETH but are trying" +
|
expect(el("confirm-balance-error").textContent).toBe(
|
||||||
" to send 1 SepoliaETH.",
|
"Your SepoliaETH balance does not cover this amount. Please go" +
|
||||||
|
" back and send a smaller amount.",
|
||||||
);
|
);
|
||||||
expect(sendDisabled()).toBe(true);
|
expect(sendDisabled()).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -384,6 +390,7 @@ describe("a confirmation screen saved on mainnet and reopened after a switch to
|
|||||||
await reopenOn("sepolia");
|
await reopenOn("sepolia");
|
||||||
expect(el("confirm-balance").textContent).toBe("10.0000 TOK");
|
expect(el("confirm-balance").textContent).toBe("10.0000 TOK");
|
||||||
expect(errors()).toBe("");
|
expect(errors()).toBe("");
|
||||||
|
expect(shown("confirm-balance-error")).toBe(false);
|
||||||
expect(el("confirm-gas-error").style.visibility).toBe("hidden");
|
expect(el("confirm-gas-error").style.visibility).toBe("hidden");
|
||||||
expect(sendDisabled()).toBe(false);
|
expect(sendDisabled()).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -396,9 +403,10 @@ describe("a confirmation screen saved on mainnet and reopened after a switch to
|
|||||||
|
|
||||||
await reopenOn("sepolia");
|
await reopenOn("sepolia");
|
||||||
expect(el("confirm-balance").textContent).toBe("0.5000 TOK");
|
expect(el("confirm-balance").textContent).toBe("0.5000 TOK");
|
||||||
expect(errors()).toContain(
|
expect(shown("confirm-balance-error")).toBe(true);
|
||||||
"Insufficient TOK balance. You have 0.5000 TOK but are trying to" +
|
expect(el("confirm-balance-error").textContent).toBe(
|
||||||
" send 1 TOK.",
|
"Your TOK balance does not cover this amount. Please go back and" +
|
||||||
|
" send a smaller amount.",
|
||||||
);
|
);
|
||||||
expect(sendDisabled()).toBe(true);
|
expect(sendDisabled()).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -418,6 +426,8 @@ describe("the balance read", () => {
|
|||||||
expect(el("confirm-balance").textContent).toBe("Loading...");
|
expect(el("confirm-balance").textContent).toBe("Loading...");
|
||||||
expect(el("confirm-fee-amount").textContent).toBe("Estimating...");
|
expect(el("confirm-fee-amount").textContent).toBe("Estimating...");
|
||||||
expect(el("confirm-errors").style.visibility).toBe("hidden");
|
expect(el("confirm-errors").style.visibility).toBe("hidden");
|
||||||
|
expect(shown("confirm-balance-error")).toBe(false);
|
||||||
|
expect(shown("confirm-balance-read-error")).toBe(false);
|
||||||
expect(sendDisabled()).toBe(true);
|
expect(sendDisabled()).toBe(true);
|
||||||
|
|
||||||
answer(parseEther("5"));
|
answer(parseEther("5"));
|
||||||
@@ -426,6 +436,32 @@ describe("the balance read", () => {
|
|||||||
expect(sendDisabled()).toBe(false);
|
expect(sendDisabled()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The Chrome suite measures that nothing moves; this checks the message
|
||||||
|
// is already written, only hidden, before the read returns.
|
||||||
|
test("that finds the amount not covered only makes a message written before it visible", async () => {
|
||||||
|
let answer;
|
||||||
|
const getBalance = jest
|
||||||
|
.spyOn(JsonRpcProvider.prototype, "getBalance")
|
||||||
|
.mockImplementationOnce(() => new Promise((r) => (answer = r)));
|
||||||
|
try {
|
||||||
|
await review("sepolia");
|
||||||
|
} finally {
|
||||||
|
getBalance.mockRestore();
|
||||||
|
}
|
||||||
|
const message =
|
||||||
|
"Your SepoliaETH balance does not cover this amount. Please go" +
|
||||||
|
" back and send a smaller amount.";
|
||||||
|
expect(el("confirm-balance-error").textContent).toBe(message);
|
||||||
|
expect(shown("confirm-balance-error")).toBe(false);
|
||||||
|
|
||||||
|
answer(parseEther("0.5"));
|
||||||
|
await settle();
|
||||||
|
expect(el("confirm-balance-error").textContent).toBe(message);
|
||||||
|
expect(shown("confirm-balance-error")).toBe(true);
|
||||||
|
expect(errors()).toBe("");
|
||||||
|
expect(sendDisabled()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
// The fee is estimated once the read returns, so a node chosen then would
|
// The fee is estimated once the read returns, so a node chosen then would
|
||||||
// be the new network's.
|
// be the new network's.
|
||||||
test("still in flight when Sepolia is chosen in Settings leaves the balance and the fee on mainnet", async () => {
|
test("still in flight when Sepolia is chosen in Settings leaves the balance and the fee on mainnet", async () => {
|
||||||
@@ -463,11 +499,9 @@ describe("the balance read", () => {
|
|||||||
getBalance.mockRestore();
|
getBalance.mockRestore();
|
||||||
}
|
}
|
||||||
expect(el("confirm-balance").textContent).toBe("unknown (SepoliaETH)");
|
expect(el("confirm-balance").textContent).toBe("unknown (SepoliaETH)");
|
||||||
expect(errors()).toBe(
|
expect(shown("confirm-balance-read-error")).toBe(true);
|
||||||
'<div class="text-xs">Your balance could not be read from the' +
|
expect(shown("confirm-balance-error")).toBe(false);
|
||||||
" network, so this transaction cannot be checked against it." +
|
expect(errors()).toBe("");
|
||||||
" Please go back and try again.</div>",
|
|
||||||
);
|
|
||||||
expect(sendDisabled()).toBe(true);
|
expect(sendDisabled()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -428,9 +428,10 @@ const HOLD_MAX_MS = 30000;
|
|||||||
|
|
||||||
// Hold a reply open for as long as the test asks: until opts[name] is false.
|
// Hold a reply open for as long as the test asks: until opts[name] is false.
|
||||||
//
|
//
|
||||||
// The switch (holdGasEstimate, holdTransactionCount or holdBlockscout) is read
|
// The switch (holdBalance, holdGasEstimate, holdTransactionCount or
|
||||||
// here rather than captured, so a test flips it on the same options object the
|
// holdBlockscout) is read here rather than captured, so a test flips it on the
|
||||||
// route was registered with — the same pattern as seedTokenTransfer. This is
|
// same options object the route was registered with — the same pattern as
|
||||||
|
// seedTokenTransfer. This is
|
||||||
// the only way to observe a screen while its request is genuinely in flight;
|
// the only way to observe a screen while its request is genuinely in flight;
|
||||||
// sampling the screen and hoping to win a race against the network would
|
// sampling the screen and hoping to win a race against the network would
|
||||||
// assert nothing on a slow machine.
|
// assert nothing on a slow machine.
|
||||||
@@ -565,6 +566,9 @@ async function handleRpc(route, postData, opts, report) {
|
|||||||
report("unstubbed request: POST " + route.request().url());
|
report("unstubbed request: POST " + route.request().url());
|
||||||
return route.abort();
|
return route.abort();
|
||||||
}
|
}
|
||||||
|
if (batch.some((req) => req.method === "eth_getBalance")) {
|
||||||
|
await awaitRelease(opts, "holdBalance", report);
|
||||||
|
}
|
||||||
if (batch.some((req) => req.method === "eth_estimateGas")) {
|
if (batch.some((req) => req.method === "eth_estimateGas")) {
|
||||||
await awaitRelease(opts, "holdGasEstimate", report);
|
await awaitRelease(opts, "holdGasEstimate", report);
|
||||||
}
|
}
|
||||||
@@ -623,6 +627,8 @@ function traceEnabled(raw) {
|
|||||||
* fixture expects.
|
* fixture expects.
|
||||||
* @param {boolean} [opts.failGasEstimate] answer eth_estimateGas with a
|
* @param {boolean} [opts.failGasEstimate] answer eth_estimateGas with a
|
||||||
* node-side refusal.
|
* node-side refusal.
|
||||||
|
* @param {boolean} [opts.holdBalance] hold every batch containing an
|
||||||
|
* eth_getBalance until this is cleared again.
|
||||||
* @param {boolean} [opts.holdGasEstimate] hold every batch containing an
|
* @param {boolean} [opts.holdGasEstimate] hold every batch containing an
|
||||||
* eth_estimateGas until this is cleared again.
|
* eth_estimateGas until this is cleared again.
|
||||||
* @param {boolean} [opts.holdTransactionCount] hold every batch containing an
|
* @param {boolean} [opts.holdTransactionCount] hold every batch containing an
|
||||||
|
|||||||
+92
-45
@@ -2232,7 +2232,7 @@ function feeEth(wei) {
|
|||||||
|
|
||||||
// What the confirmation screen is showing right now, read out of the DOM in
|
// What the confirmation screen is showing right now, read out of the DOM in
|
||||||
// one pass: whether sending is allowed, which reason it is giving, what the
|
// one pass: whether sending is allowed, which reason it is giving, what the
|
||||||
// fee block says, and how tall the whole view is.
|
// fee block says, how tall the whole view is, and where Sign & Send sits in it.
|
||||||
async function confirmState(page) {
|
async function confirmState(page) {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const el = (id) => document.getElementById(id);
|
const el = (id) => document.getElementById(id);
|
||||||
@@ -2245,6 +2245,9 @@ async function confirmState(page) {
|
|||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
height: el("view-confirm-tx").getBoundingClientRect().height,
|
height: el("view-confirm-tx").getBoundingClientRect().height,
|
||||||
|
sendTop:
|
||||||
|
el("btn-confirm-send").getBoundingClientRect().top -
|
||||||
|
el("view-confirm-tx").getBoundingClientRect().top,
|
||||||
type: el("confirm-type").textContent.trim(),
|
type: el("confirm-type").textContent.trim(),
|
||||||
balance: el("confirm-balance").textContent.trim(),
|
balance: el("confirm-balance").textContent.trim(),
|
||||||
fee: el("confirm-fee-amount").textContent.trim(),
|
fee: el("confirm-fee-amount").textContent.trim(),
|
||||||
@@ -2253,6 +2256,9 @@ async function confirmState(page) {
|
|||||||
errors: shown("confirm-errors")
|
errors: shown("confirm-errors")
|
||||||
? el("confirm-errors").textContent.trim()
|
? el("confirm-errors").textContent.trim()
|
||||||
: "",
|
: "",
|
||||||
|
balanceError: shown("confirm-balance-error")
|
||||||
|
? el("confirm-balance-error").textContent.trim()
|
||||||
|
: "",
|
||||||
amountFeeError: shown("confirm-amount-fee-error"),
|
amountFeeError: shown("confirm-amount-fee-error"),
|
||||||
gasError: shown("confirm-gas-error"),
|
gasError: shown("confirm-gas-error"),
|
||||||
feeUnknownError: shown("confirm-fee-unknown-error"),
|
feeUnknownError: shown("confirm-fee-unknown-error"),
|
||||||
@@ -2271,6 +2277,16 @@ async function waitForEstimate(page) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForBalance(page) {
|
||||||
|
await page.waitForFunction(
|
||||||
|
() =>
|
||||||
|
document.getElementById("confirm-balance").textContent.trim() !==
|
||||||
|
"Loading...",
|
||||||
|
null,
|
||||||
|
{ timeout: 60000 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function backToAddress(page) {
|
async function backToAddress(page) {
|
||||||
if (await page.isVisible("#view-confirm-tx")) {
|
if (await page.isVisible("#view-confirm-tx")) {
|
||||||
await page.click("#btn-confirm-back");
|
await page.click("#btn-confirm-back");
|
||||||
@@ -2282,13 +2298,14 @@ async function backToAddress(page) {
|
|||||||
await openAddressDetail(page);
|
await openAddressDetail(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drive the popup to the confirmation screen for one send.
|
// Drive the popup to the confirmation screen for one send, and return as soon
|
||||||
|
// as the screen is shown, its balance read still in flight.
|
||||||
//
|
//
|
||||||
// It waits for the send screen to be showing `balance`, the fixture's balance
|
// It waits for the send screen to be showing `balance`, the fixture's balance
|
||||||
// as that screen displays it, before filling anything in. Waiting for it —
|
// as that screen displays it, before filling anything in. Waiting for it —
|
||||||
// rather than for a refresh to have probably landed — is what keeps every
|
// rather than for a refresh to have probably landed — is what keeps every
|
||||||
// assertion below deterministic after a fixture change.
|
// assertion below deterministic after a fixture change.
|
||||||
async function goToConfirm(page, { token, balance, amount }) {
|
async function openConfirm(page, { token, balance, amount }) {
|
||||||
await backToAddress(page);
|
await backToAddress(page);
|
||||||
await page.click("#btn-send");
|
await page.click("#btn-send");
|
||||||
await visible(page, "#view-send");
|
await visible(page, "#view-send");
|
||||||
@@ -2303,16 +2320,14 @@ async function goToConfirm(page, { token, balance, amount }) {
|
|||||||
await page.fill("#send-amount", amount);
|
await page.fill("#send-amount", amount);
|
||||||
await page.click("#btn-send-review");
|
await page.click("#btn-send-review");
|
||||||
await visible(page, "#view-confirm-tx");
|
await visible(page, "#view-confirm-tx");
|
||||||
// The screen reads the balance itself before it estimates the fee, so
|
}
|
||||||
// waiting for it here means every state below is read with the balance
|
|
||||||
// in place, the pending fee included.
|
// The same, returning once the screen has read the balance. It reads it
|
||||||
await page.waitForFunction(
|
// before it estimates the fee, so every state below is read with the balance
|
||||||
() =>
|
// in place, the pending fee included.
|
||||||
document.getElementById("confirm-balance").textContent.trim() !==
|
async function goToConfirm(page, send) {
|
||||||
"Loading...",
|
await openConfirm(page, send);
|
||||||
null,
|
await waitForBalance(page);
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
await assertAddressesFit(page, "the confirmation screen");
|
await assertAddressesFit(page, "the confirmation screen");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2388,7 +2403,10 @@ test("ConfirmTx blocks sending while the fee estimate is pending (#238)", async
|
|||||||
"the estimate-failed message is shown for an estimate that is merely pending",
|
"the estimate-failed message is shown for an estimate that is merely pending",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
!st.amountFeeError && !st.gasError && st.errors === "",
|
!st.amountFeeError &&
|
||||||
|
!st.gasError &&
|
||||||
|
st.errors === "" &&
|
||||||
|
st.balanceError === "",
|
||||||
"a balance message is shown before the fee is known",
|
"a balance message is shown before the fee is known",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -2419,6 +2437,7 @@ test("ConfirmTx enables Send once the estimate lands, quoting both numbers (#238
|
|||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
st.errors === "" &&
|
st.errors === "" &&
|
||||||
|
st.balanceError === "" &&
|
||||||
!st.amountFeeError &&
|
!st.amountFeeError &&
|
||||||
!st.gasError &&
|
!st.gasError &&
|
||||||
!st.feeUnknownError,
|
!st.feeUnknownError,
|
||||||
@@ -2485,30 +2504,54 @@ test("ConfirmTx gates on the fee RESERVE, not the displayed estimate (#238)", as
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ConfirmTx refuses a send that exceeds the balance outright (#238)", async (env) => {
|
// The screen is first measured with its balance read held, so the message the
|
||||||
await goToConfirm(env.page, {
|
// read brings must already hold its space when the read returns (#558).
|
||||||
token: "ETH",
|
test("ConfirmTx refuses a send that exceeds the balance outright, and nothing moves when the balance arrives (#238)", async (env) => {
|
||||||
balance: FUNDED_ETH_TEXT + " ETH",
|
env.routeOpts.holdBalance = true;
|
||||||
amount: OVER_BALANCE_AMOUNT,
|
let reading;
|
||||||
});
|
try {
|
||||||
|
await openConfirm(env.page, {
|
||||||
|
token: "ETH",
|
||||||
|
balance: FUNDED_ETH_TEXT + " ETH",
|
||||||
|
amount: OVER_BALANCE_AMOUNT,
|
||||||
|
});
|
||||||
|
reading = await confirmState(env.page);
|
||||||
|
} finally {
|
||||||
|
env.routeOpts.holdBalance = false;
|
||||||
|
}
|
||||||
|
assert(
|
||||||
|
reading.balance === "Loading...",
|
||||||
|
"the balance read was not held: " + JSON.stringify(reading.balance),
|
||||||
|
);
|
||||||
|
await waitForBalance(env.page);
|
||||||
await waitForEstimate(env.page);
|
await waitForEstimate(env.page);
|
||||||
|
|
||||||
const st = await confirmState(env.page);
|
const st = await confirmState(env.page);
|
||||||
const want =
|
|
||||||
"Insufficient balance. You have " +
|
|
||||||
FUNDED_ETH_TEXT +
|
|
||||||
" ETH but are trying to send " +
|
|
||||||
OVER_BALANCE_AMOUNT +
|
|
||||||
" ETH.";
|
|
||||||
assert(
|
assert(
|
||||||
st.errors === want,
|
st.balance === FUNDED_ETH_TEXT + " ETH",
|
||||||
"wrong over-balance message: " + JSON.stringify(st.errors),
|
"the confirmation screen shows the wrong balance: " +
|
||||||
|
JSON.stringify(st.balance),
|
||||||
|
);
|
||||||
|
const want =
|
||||||
|
"Your ETH balance does not cover this amount. Please go back and" +
|
||||||
|
" send a smaller amount.";
|
||||||
|
assert(
|
||||||
|
st.balanceError === want,
|
||||||
|
"wrong over-balance message: " + JSON.stringify(st.balanceError),
|
||||||
);
|
);
|
||||||
assert(st.sendDisabled, "Send is enabled for a send that exceeds balance");
|
assert(st.sendDisabled, "Send is enabled for a send that exceeds balance");
|
||||||
assert(
|
assert(
|
||||||
!st.amountFeeError,
|
!st.amountFeeError,
|
||||||
"the amount-plus-fee message is shown for an amount that alone exceeds the balance",
|
"the amount-plus-fee message is shown for an amount that alone exceeds the balance",
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
st.sendTop === reading.sendTop,
|
||||||
|
"Sign & Send moved when the balance read returned: " +
|
||||||
|
reading.sendTop +
|
||||||
|
"px -> " +
|
||||||
|
st.sendTop +
|
||||||
|
"px",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ConfirmTx refuses to send when the fee estimate fails, with its own message (#238)", async (env) => {
|
test("ConfirmTx refuses to send when the fee estimate fails, with its own message (#238)", async (env) => {
|
||||||
@@ -2552,7 +2595,10 @@ test("ConfirmTx refuses to send when the fee estimate fails, with its own messag
|
|||||||
"the estimate-failed message is not shown after a failed estimate",
|
"the estimate-failed message is not shown after a failed estimate",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
!st.amountFeeError && !st.gasError && st.errors === "",
|
!st.amountFeeError &&
|
||||||
|
!st.gasError &&
|
||||||
|
st.errors === "" &&
|
||||||
|
st.balanceError === "",
|
||||||
"a balance message is shown for an estimate that simply failed",
|
"a balance message is shown for an estimate that simply failed",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
@@ -2643,21 +2689,19 @@ test("ConfirmTx refuses an ERC-20 send that exceeds the token balance (#238)", a
|
|||||||
await waitForEstimate(env.page);
|
await waitForEstimate(env.page);
|
||||||
|
|
||||||
const st = await confirmState(env.page);
|
const st = await confirmState(env.page);
|
||||||
const want =
|
|
||||||
"Insufficient " +
|
|
||||||
STUB_TOKEN.symbol +
|
|
||||||
" balance. You have " +
|
|
||||||
TOKEN_BALANCE_TEXT +
|
|
||||||
" " +
|
|
||||||
STUB_TOKEN.symbol +
|
|
||||||
" but are trying to send " +
|
|
||||||
OVER_TOKEN_AMOUNT +
|
|
||||||
" " +
|
|
||||||
STUB_TOKEN.symbol +
|
|
||||||
".";
|
|
||||||
assert(
|
assert(
|
||||||
st.errors === want,
|
st.balance === TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||||
"wrong over-token-balance message: " + JSON.stringify(st.errors),
|
"the confirmation screen shows the wrong token balance: " +
|
||||||
|
JSON.stringify(st.balance),
|
||||||
|
);
|
||||||
|
const want =
|
||||||
|
"Your " +
|
||||||
|
STUB_TOKEN.symbol +
|
||||||
|
" balance does not cover this amount. Please go back and send a" +
|
||||||
|
" smaller amount.";
|
||||||
|
assert(
|
||||||
|
st.balanceError === want,
|
||||||
|
"wrong over-token-balance message: " + JSON.stringify(st.balanceError),
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
st.sendDisabled,
|
st.sendDisabled,
|
||||||
@@ -2713,9 +2757,9 @@ test("ConfirmTx gates the ERC-20 fee on the RESERVE, with the ERC-20 message (#2
|
|||||||
"the native-ETH over-budget message is shown on an ERC-20 transfer",
|
"the native-ETH over-budget message is shown on an ERC-20 transfer",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
st.errors === "",
|
st.errors === "" && st.balanceError === "",
|
||||||
"a token-balance message is shown for a transfer the token balance covers: " +
|
"a token-balance message is shown for a transfer the token balance covers: " +
|
||||||
JSON.stringify(st.errors),
|
JSON.stringify(st.errors + st.balanceError),
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
st.height === env.erc20PendingHeight,
|
st.height === env.erc20PendingHeight,
|
||||||
@@ -4869,6 +4913,9 @@ async function main() {
|
|||||||
seedTokenBalance: false,
|
seedTokenBalance: false,
|
||||||
ethBalanceWei: null,
|
ethBalanceWei: null,
|
||||||
failGasEstimate: false,
|
failGasEstimate: false,
|
||||||
|
// Whether a JSON-RPC batch asking for a balance is held unanswered:
|
||||||
|
// the confirmation screen's own balance read (#558).
|
||||||
|
holdBalance: false,
|
||||||
holdGasEstimate: false,
|
holdGasEstimate: false,
|
||||||
// Whether a JSON-RPC batch asking for a transaction count is held
|
// Whether a JSON-RPC batch asking for a transaction count is held
|
||||||
// unanswered: the address scan after creating a wallet (#475).
|
// unanswered: the address scan after creating a wallet (#475).
|
||||||
|
|||||||
@@ -336,14 +336,11 @@ describe.each([
|
|||||||
|
|
||||||
test("the insufficient-balance error", async () => {
|
test("the insufficient-balance error", async () => {
|
||||||
await confirmSend("2");
|
await confirmSend("2");
|
||||||
expect(
|
expect(text("confirm-balance-error")).toBe(
|
||||||
global.document.getElementById("confirm-errors").innerHTML,
|
"Your " +
|
||||||
).toContain(
|
|
||||||
"You have 1.5000 " +
|
|
||||||
symbol +
|
symbol +
|
||||||
" but are trying to send 2 " +
|
" balance does not cover this amount. Please go back and send" +
|
||||||
symbol +
|
" a smaller amount.",
|
||||||
".",
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -203,6 +203,12 @@ function errors() {
|
|||||||
return global.document.getElementById("confirm-errors").innerHTML;
|
return global.document.getElementById("confirm-errors").innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The message for an amount the balance does not cover, while it is shown.
|
||||||
|
function balanceError() {
|
||||||
|
const box = global.document.getElementById("confirm-balance-error");
|
||||||
|
return box.style.visibility === "visible" ? box.textContent : "";
|
||||||
|
}
|
||||||
|
|
||||||
// The ETH balance the node reports and the token rows the explorer reports,
|
// The ETH balance the node reports and the token rows the explorer reports,
|
||||||
// fetched and stored exactly where the popup stores them. The node reports the
|
// fetched and stored exactly where the popup stores them. The node reports the
|
||||||
// token holding the explorer does.
|
// token holding the explorer does.
|
||||||
@@ -311,9 +317,10 @@ describe("a tracked token holding below 0.000001 never renders as zero", () => {
|
|||||||
await confirmSend("0.0000005", TOKEN);
|
await confirmSend("0.0000005", TOKEN);
|
||||||
expect(text("confirm-balance")).toBe("0.0000005 TOK");
|
expect(text("confirm-balance")).toBe("0.0000005 TOK");
|
||||||
expect(errors()).toBe("");
|
expect(errors()).toBe("");
|
||||||
|
expect(balanceError()).toBe("");
|
||||||
await confirmSend("0.0000006", TOKEN);
|
await confirmSend("0.0000006", TOKEN);
|
||||||
expect(errors()).toContain(
|
expect(balanceError()).toContain(
|
||||||
"You have 0.0000005 TOK but are trying to send 0.0000006 TOK.",
|
"Your TOK balance does not cover this amount.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -327,6 +334,7 @@ describe("a tracked token holding below 0.000001 never renders as zero", () => {
|
|||||||
await confirmSend("1.5", TOKEN);
|
await confirmSend("1.5", TOKEN);
|
||||||
expect(text("confirm-balance")).toBe("1.5000 TOK");
|
expect(text("confirm-balance")).toBe("1.5000 TOK");
|
||||||
expect(errors()).toBe("");
|
expect(errors()).toBe("");
|
||||||
|
expect(balanceError()).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("with more than 18 decimals and a holding below 10^-18", async () => {
|
test("with more than 18 decimals and a holding below 10^-18", async () => {
|
||||||
@@ -340,9 +348,8 @@ describe("a tracked token holding below 0.000001 never renders as zero", () => {
|
|||||||
);
|
);
|
||||||
await confirmSend("0.000000000000000001", TOKEN);
|
await confirmSend("0.000000000000000001", TOKEN);
|
||||||
expect(text("confirm-balance")).toBe("0.000000000000000000000001 TOK");
|
expect(text("confirm-balance")).toBe("0.000000000000000000000001 TOK");
|
||||||
expect(errors()).toContain(
|
expect(balanceError()).toContain(
|
||||||
"You have 0.000000000000000000000001 TOK but are trying to send" +
|
"Your TOK balance does not cover this amount.",
|
||||||
" 0.000000000000000001 TOK.",
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -367,9 +374,10 @@ describe("an untracked token holding below 0.000001 never renders as zero", () =
|
|||||||
await confirmSend("0.0000005", TOKEN);
|
await confirmSend("0.0000005", TOKEN);
|
||||||
expect(text("confirm-balance")).toBe("0.0000005 TOK");
|
expect(text("confirm-balance")).toBe("0.0000005 TOK");
|
||||||
expect(errors()).toBe("");
|
expect(errors()).toBe("");
|
||||||
|
expect(balanceError()).toBe("");
|
||||||
await confirmSend("0.0000006", TOKEN);
|
await confirmSend("0.0000006", TOKEN);
|
||||||
expect(errors()).toContain(
|
expect(balanceError()).toContain(
|
||||||
"You have 0.0000005 TOK but are trying to send 0.0000006 TOK.",
|
"Your TOK balance does not cover this amount.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -249,6 +249,12 @@ function errors() {
|
|||||||
return el("confirm-errors").innerHTML;
|
return el("confirm-errors").innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The message for an amount the balance does not cover, while it is shown.
|
||||||
|
function balanceError() {
|
||||||
|
const box = el("confirm-balance-error");
|
||||||
|
return box.style.visibility === "visible" ? box.textContent : "";
|
||||||
|
}
|
||||||
|
|
||||||
function sendDisabled() {
|
function sendDisabled() {
|
||||||
return el("btn-confirm-send").disabled;
|
return el("btn-confirm-send").disabled;
|
||||||
}
|
}
|
||||||
@@ -319,7 +325,7 @@ describe("the Send screen resolves the scale rather than reading the stored one"
|
|||||||
confirmTx.show(txInfo);
|
confirmTx.show(txInfo);
|
||||||
await settle();
|
await settle();
|
||||||
expect(text("confirm-balance")).toBe("unknown (NOVEL)");
|
expect(text("confirm-balance")).toBe("unknown (NOVEL)");
|
||||||
expect(errors()).toContain("This token's balance is unknown");
|
expect(balanceError()).toContain("This token's balance is unknown");
|
||||||
expect(sendDisabled()).toBe(true);
|
expect(sendDisabled()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -376,8 +382,7 @@ describe("a scale the explorer's own rows disagree about", () => {
|
|||||||
// confirm-fee-unknown-error — "the network fee could not be
|
// confirm-fee-unknown-error — "the network fee could not be
|
||||||
// estimated... please go back and try again" — as the only explanation
|
// estimated... please go back and try again" — as the only explanation
|
||||||
// for a screen that can never proceed.
|
// for a screen that can never proceed.
|
||||||
expect(errors()).not.toBe("");
|
expect(balanceError()).toContain("This token's balance is unknown");
|
||||||
expect(errors()).toContain("This token's balance is unknown");
|
|
||||||
expect(text("confirm-balance")).toBe("unknown (NOVEL)");
|
expect(text("confirm-balance")).toBe("unknown (NOVEL)");
|
||||||
expect(sendDisabled()).toBe(true);
|
expect(sendDisabled()).toBe(true);
|
||||||
// The fee line still reports the estimate as unavailable, because it
|
// The fee line still reports the estimate as unavailable, because it
|
||||||
@@ -447,6 +452,7 @@ describe("a scale the explorer's own rows disagree about", () => {
|
|||||||
await settle();
|
await settle();
|
||||||
expect(text("confirm-balance")).toBe("5.0000 NOVEL");
|
expect(text("confirm-balance")).toBe("5.0000 NOVEL");
|
||||||
expect(errors()).toBe("");
|
expect(errors()).toBe("");
|
||||||
|
expect(balanceError()).toBe("");
|
||||||
expect(sendDisabled()).toBe(false);
|
expect(sendDisabled()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -476,7 +482,7 @@ describe("the confirmation screen tells an unknown balance from a zero one", ()
|
|||||||
tokenDecimals,
|
tokenDecimals,
|
||||||
});
|
});
|
||||||
await settle();
|
await settle();
|
||||||
return { balance: text("confirm-balance"), errors: errors() };
|
return { balance: text("confirm-balance"), message: balanceError() };
|
||||||
}
|
}
|
||||||
|
|
||||||
test("the balance line states unknown rather than a quantity of zero", async () => {
|
test("the balance line states unknown rather than a quantity of zero", async () => {
|
||||||
@@ -488,16 +494,18 @@ describe("the confirmation screen tells an unknown balance from a zero one", ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Both hit INSUFFICIENT_TOKEN — an unknown balance is treated as nothing to
|
// Both hit INSUFFICIENT_TOKEN — an unknown balance is treated as nothing to
|
||||||
// spend from, which is the fail-closed side — but "you have 0.0" is a claim
|
// spend from, which is the fail-closed side — but "your balance does not
|
||||||
// about the holding, and this one has no established quantity to claim.
|
// cover this amount" is a claim about the holding, and this one has no
|
||||||
test("the insufficient-balance message names the reason, not a figure", async () => {
|
// established quantity to claim.
|
||||||
|
test("the insufficient-balance message names the reason, not a shortfall", async () => {
|
||||||
const unknown = await render(null);
|
const unknown = await render(null);
|
||||||
const zero = await render(18);
|
const zero = await render(18);
|
||||||
expect(unknown.errors).not.toBe(zero.errors);
|
expect(unknown.message).toContain("This token's balance is unknown");
|
||||||
expect(unknown.errors).toContain("This token's balance is unknown");
|
expect(unknown.message).not.toContain("does not cover");
|
||||||
expect(unknown.errors).not.toContain("You have");
|
expect(zero.message).toBe(
|
||||||
expect(zero.errors).toContain("You have 0.0000 NOVEL");
|
"Your NOVEL balance does not cover this amount. Please go back and" +
|
||||||
expect(zero.errors).not.toContain("balance is unknown");
|
" send a smaller amount.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user