test: a failing e2e test leaves no fixture switch or send screen behind (closes #318)
A test that turns a fixture switch on for itself alone turns it off in a finally, so a failure no longer reddens the tests after it. The two tests that drive the popup's own send also return it to the address screen, reopening the popup to leave a wait for a receipt. The lying-decimals() test asserts that nothing was broadcast as soon as the send ends, before waiting for the failure screen. ethCallResult() answers an override of 0 instead of falling back to the explorer's scale. Model: opus-5-5
This commit is contained in:
@@ -256,14 +256,15 @@ const SELECTOR_DECIMALS = "0x313ce567";
|
||||
// Blockscout, which is exactly the disagreement the wallet must refuse to
|
||||
// sign over. It is read at request time, so a test flips it on the options
|
||||
// object the route was registered with — after the confirmation screen has
|
||||
// been built — without re-registering anything.
|
||||
// been built — without re-registering anything. Only null or undefined means
|
||||
// no override: 0 is a token with no decimal places, and is answered as one.
|
||||
function ethCallResult(req, opts) {
|
||||
const call = Array.isArray(req.params) ? req.params[0] : null;
|
||||
if (!call || typeof call !== "object") return ZERO_WORD;
|
||||
const data = String(call.data || call.input || "").toLowerCase();
|
||||
const to = String(call.to || "").toLowerCase();
|
||||
if (data.startsWith(SELECTOR_DECIMALS) && to === STUB_TOKEN.address) {
|
||||
return word(opts.tokenDecimalsOverride || STUB_TOKEN.decimals);
|
||||
return word(opts.tokenDecimalsOverride ?? STUB_TOKEN.decimals);
|
||||
}
|
||||
return ZERO_WORD;
|
||||
}
|
||||
@@ -597,9 +598,10 @@ function traceEnabled(raw) {
|
||||
* eth_estimateGas until this is cleared again.
|
||||
* @param {string[]} [opts.broadcastTransactions] every raw signed
|
||||
* transaction handed to eth_sendRawTransaction, appended in order.
|
||||
* @param {string} [opts.tokenDecimalsOverride] what decimals() answers for
|
||||
* the stub token, in place of the value Blockscout reports for it. This is
|
||||
* the token that lies about its scale; read at request time.
|
||||
* @param {number|string|null} [opts.tokenDecimalsOverride] the scale
|
||||
* decimals() answers for the stub token, in place of the value Blockscout
|
||||
* reports for it; null for none, while 0 is a scale like any other. This
|
||||
* is the token that lies about its scale; read at request time.
|
||||
* @param {string} [opts.tokenSymbolOverride] what the explorer reports as
|
||||
* the stub token's symbol, in place of "E2E". This is the token whose
|
||||
* symbol is markup; read at request time.
|
||||
|
||||
+404
-337
@@ -366,54 +366,59 @@ test("the native ETH transaction detail still renders (#151)", async (env) => {
|
||||
// the assertions below are as much about that row staying hidden as
|
||||
// about the screen coming up.
|
||||
env.routeOpts.seedNativeTransfer = true;
|
||||
await env.page.reload();
|
||||
await openAddressDetail(env.page);
|
||||
try {
|
||||
await env.page.reload();
|
||||
await openAddressDetail(env.page);
|
||||
|
||||
const row = env.page
|
||||
.locator("#tx-list .tx-row")
|
||||
.filter({ hasText: NATIVE_ROW_TEXT });
|
||||
await row.waitFor({ state: "visible", timeout: 30000 });
|
||||
await row.click();
|
||||
await visible(env.page, "#view-transaction");
|
||||
const row = env.page
|
||||
.locator("#tx-list .tx-row")
|
||||
.filter({ hasText: NATIVE_ROW_TEXT });
|
||||
await row.waitFor({ state: "visible", timeout: 30000 });
|
||||
await row.click();
|
||||
await visible(env.page, "#view-transaction");
|
||||
|
||||
const hash = await env.page.locator("#tx-detail-hash").innerText();
|
||||
assert(
|
||||
hash.includes(STUB_NATIVE_TX_HASH),
|
||||
"the native transaction detail shows the wrong hash: " + hash,
|
||||
);
|
||||
const hash = await env.page.locator("#tx-detail-hash").innerText();
|
||||
assert(
|
||||
hash.includes(STUB_NATIVE_TX_HASH),
|
||||
"the native transaction detail shows the wrong hash: " + hash,
|
||||
);
|
||||
|
||||
const type = (await env.page.locator("#tx-detail-type").innerText()).trim();
|
||||
assert(
|
||||
type === "Native ETH Transfer",
|
||||
"the native transaction was classified " + JSON.stringify(type),
|
||||
);
|
||||
const type = (
|
||||
await env.page.locator("#tx-detail-type").innerText()
|
||||
).trim();
|
||||
assert(
|
||||
type === "Native ETH Transfer",
|
||||
"the native transaction was classified " + JSON.stringify(type),
|
||||
);
|
||||
|
||||
const value = await env.page.locator("#tx-detail-value").innerText();
|
||||
assert(
|
||||
value.includes(NATIVE_DETAIL_TEXT),
|
||||
"the native transaction detail shows " +
|
||||
JSON.stringify(value) +
|
||||
", expected it to contain " +
|
||||
NATIVE_DETAIL_TEXT,
|
||||
);
|
||||
const value = await env.page.locator("#tx-detail-value").innerText();
|
||||
assert(
|
||||
value.includes(NATIVE_DETAIL_TEXT),
|
||||
"the native transaction detail shows " +
|
||||
JSON.stringify(value) +
|
||||
", expected it to contain " +
|
||||
NATIVE_DETAIL_TEXT,
|
||||
);
|
||||
|
||||
const native = await env.page.locator("#tx-detail-native").innerText();
|
||||
assert(
|
||||
native.includes(STUB_NATIVE_VALUE_WEI + " wei"),
|
||||
"the raw quantity row shows " +
|
||||
JSON.stringify(native) +
|
||||
", expected the value in wei",
|
||||
);
|
||||
const native = await env.page.locator("#tx-detail-native").innerText();
|
||||
assert(
|
||||
native.includes(STUB_NATIVE_VALUE_WEI + " wei"),
|
||||
"the raw quantity row shows " +
|
||||
JSON.stringify(native) +
|
||||
", expected the value in wei",
|
||||
);
|
||||
|
||||
assert(
|
||||
!(await env.page.isVisible("#tx-detail-token-contract-section")),
|
||||
"the token contract row is showing on a transfer that has no token " +
|
||||
"contract",
|
||||
);
|
||||
|
||||
// Back to one seeded transaction for everything after this: the tests
|
||||
// below were written against a list holding the token transfer alone.
|
||||
env.routeOpts.seedNativeTransfer = false;
|
||||
assert(
|
||||
!(await env.page.isVisible("#tx-detail-token-contract-section")),
|
||||
"the token contract row is showing on a transfer that has no " +
|
||||
"token contract",
|
||||
);
|
||||
} finally {
|
||||
// Back to one seeded transaction for everything after this: the
|
||||
// tests below were written against a list holding the token transfer
|
||||
// alone.
|
||||
env.routeOpts.seedNativeTransfer = false;
|
||||
}
|
||||
});
|
||||
|
||||
test("tap-to-copy on the transaction detail screen copies the address (#151)", async (env) => {
|
||||
@@ -1785,6 +1790,7 @@ test("ConfirmTx blocks sending while the fee estimate is pending (#238)", async
|
||||
expectToken: true,
|
||||
});
|
||||
|
||||
// Released by the next test, which watches the estimate land.
|
||||
env.routeOpts.holdGasEstimate = true;
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
@@ -1947,102 +1953,117 @@ test("ConfirmTx refuses to send when the fee estimate fails, with its own messag
|
||||
);
|
||||
env.routeOpts.failGasEstimate = true;
|
||||
env.routeOpts.holdGasEstimate = true;
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: COMFORTABLE_AMOUNT,
|
||||
});
|
||||
try {
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: COMFORTABLE_AMOUNT,
|
||||
});
|
||||
|
||||
const pending = await confirmState(env.page);
|
||||
assert(
|
||||
pending.fee === "Estimating..." && pending.sendDisabled,
|
||||
"the screen is not in the pending state before the estimate fails",
|
||||
);
|
||||
const pending = await confirmState(env.page);
|
||||
assert(
|
||||
pending.fee === "Estimating..." && pending.sendDisabled,
|
||||
"the screen is not in the pending state before the estimate fails",
|
||||
);
|
||||
|
||||
env.routeOpts.holdGasEstimate = false;
|
||||
await waitForEstimate(env.page);
|
||||
env.routeOpts.holdGasEstimate = false;
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
env.routeOpts.failGasEstimate = false;
|
||||
assert(
|
||||
st.fee === "Unable to estimate",
|
||||
"the fee line does not report the failure: " + JSON.stringify(st.fee),
|
||||
);
|
||||
assert(
|
||||
!st.reserveShown,
|
||||
"the reserve line is shown after a failed estimate",
|
||||
);
|
||||
assert(
|
||||
st.feeUnknownError,
|
||||
"the estimate-failed message is not shown after a failed estimate",
|
||||
);
|
||||
assert(
|
||||
!st.amountFeeError && !st.gasError && st.errors === "",
|
||||
"a balance message is shown for an estimate that simply failed",
|
||||
);
|
||||
assert(
|
||||
st.sendDisabled,
|
||||
"Send is enabled with no usable fee estimate — an unknown fee is being treated as zero",
|
||||
);
|
||||
assert(
|
||||
st.height === pending.height,
|
||||
"the view changed height when the estimate failed: " +
|
||||
pending.height +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
const st = await confirmState(env.page);
|
||||
assert(
|
||||
st.fee === "Unable to estimate",
|
||||
"the fee line does not report the failure: " +
|
||||
JSON.stringify(st.fee),
|
||||
);
|
||||
assert(
|
||||
!st.reserveShown,
|
||||
"the reserve line is shown after a failed estimate",
|
||||
);
|
||||
assert(
|
||||
st.feeUnknownError,
|
||||
"the estimate-failed message is not shown after a failed estimate",
|
||||
);
|
||||
assert(
|
||||
!st.amountFeeError && !st.gasError && st.errors === "",
|
||||
"a balance message is shown for an estimate that simply failed",
|
||||
);
|
||||
assert(
|
||||
st.sendDisabled,
|
||||
"Send is enabled with no usable fee estimate — an unknown fee is being treated as zero",
|
||||
);
|
||||
assert(
|
||||
st.height === pending.height,
|
||||
"the view changed height when the estimate failed: " +
|
||||
pending.height +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
} finally {
|
||||
env.routeOpts.failGasEstimate = false;
|
||||
env.routeOpts.holdGasEstimate = false;
|
||||
}
|
||||
});
|
||||
|
||||
test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (env) => {
|
||||
env.routeOpts.holdGasEstimate = true;
|
||||
await goToConfirm(env.page, {
|
||||
token: STUB_TOKEN.address,
|
||||
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
amount: TOKEN_AMOUNT,
|
||||
});
|
||||
try {
|
||||
await goToConfirm(env.page, {
|
||||
token: STUB_TOKEN.address,
|
||||
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
amount: TOKEN_AMOUNT,
|
||||
});
|
||||
|
||||
const pending = await confirmState(env.page);
|
||||
env.erc20PendingHeight = pending.height;
|
||||
console.log("# confirm-tx ERC-20 view height: " + pending.height + "px");
|
||||
assert(
|
||||
pending.type === "ERC-20 token transfer (" + STUB_TOKEN.symbol + ")",
|
||||
"unexpected transaction type: " + JSON.stringify(pending.type),
|
||||
);
|
||||
assert(
|
||||
pending.balance === TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
"the ERC-20 screen shows the wrong balance: " +
|
||||
JSON.stringify(pending.balance),
|
||||
);
|
||||
assert(
|
||||
pending.fee === "Estimating..." && pending.sendDisabled,
|
||||
"the ERC-20 screen does not block sending while its estimate is pending",
|
||||
);
|
||||
const pending = await confirmState(env.page);
|
||||
env.erc20PendingHeight = pending.height;
|
||||
console.log(
|
||||
"# confirm-tx ERC-20 view height: " + pending.height + "px",
|
||||
);
|
||||
assert(
|
||||
pending.type ===
|
||||
"ERC-20 token transfer (" + STUB_TOKEN.symbol + ")",
|
||||
"unexpected transaction type: " + JSON.stringify(pending.type),
|
||||
);
|
||||
assert(
|
||||
pending.balance === TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
"the ERC-20 screen shows the wrong balance: " +
|
||||
JSON.stringify(pending.balance),
|
||||
);
|
||||
assert(
|
||||
pending.fee === "Estimating..." && pending.sendDisabled,
|
||||
"the ERC-20 screen does not block sending while its estimate is pending",
|
||||
);
|
||||
|
||||
env.routeOpts.holdGasEstimate = false;
|
||||
await waitForEstimate(env.page);
|
||||
env.routeOpts.holdGasEstimate = false;
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
assert(
|
||||
st.fee === "~" + feeEth(FEE_ESTIMATE_WEI),
|
||||
"the ERC-20 fee line does not quote the estimate: " +
|
||||
JSON.stringify(st.fee),
|
||||
);
|
||||
assert(
|
||||
st.reserveShown &&
|
||||
st.reserve === "up to " + feeEth(FEE_RESERVE_WEI) + " reserved",
|
||||
"the ERC-20 fee block does not quote the reserve: " +
|
||||
JSON.stringify(st.reserve),
|
||||
);
|
||||
assert(!st.sendDisabled, "Send is disabled for a funded ERC-20 transfer");
|
||||
assert(
|
||||
st.height === pending.height,
|
||||
"the ERC-20 view changed height when the estimate landed: " +
|
||||
pending.height +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
const st = await confirmState(env.page);
|
||||
assert(
|
||||
st.fee === "~" + feeEth(FEE_ESTIMATE_WEI),
|
||||
"the ERC-20 fee line does not quote the estimate: " +
|
||||
JSON.stringify(st.fee),
|
||||
);
|
||||
assert(
|
||||
st.reserveShown &&
|
||||
st.reserve === "up to " + feeEth(FEE_RESERVE_WEI) + " reserved",
|
||||
"the ERC-20 fee block does not quote the reserve: " +
|
||||
JSON.stringify(st.reserve),
|
||||
);
|
||||
assert(
|
||||
!st.sendDisabled,
|
||||
"Send is disabled for a funded ERC-20 transfer",
|
||||
);
|
||||
assert(
|
||||
st.height === pending.height,
|
||||
"the ERC-20 view changed height when the estimate landed: " +
|
||||
pending.height +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
} finally {
|
||||
env.routeOpts.holdGasEstimate = false;
|
||||
}
|
||||
});
|
||||
|
||||
test("ConfirmTx refuses an ERC-20 send that exceeds the token balance (#238)", async (env) => {
|
||||
@@ -2144,40 +2165,43 @@ test("ConfirmTx reports a failed ERC-20 estimate as unknown, not as a fee proble
|
||||
/gas estimation failed/,
|
||||
);
|
||||
env.routeOpts.failGasEstimate = true;
|
||||
await goToConfirm(env.page, {
|
||||
token: STUB_TOKEN.address,
|
||||
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
amount: TOKEN_AMOUNT,
|
||||
});
|
||||
await waitForEstimate(env.page);
|
||||
try {
|
||||
await goToConfirm(env.page, {
|
||||
token: STUB_TOKEN.address,
|
||||
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
amount: TOKEN_AMOUNT,
|
||||
});
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
env.routeOpts.failGasEstimate = false;
|
||||
assert(
|
||||
st.fee === "Unable to estimate",
|
||||
"the ERC-20 fee line does not report the failure: " +
|
||||
JSON.stringify(st.fee),
|
||||
);
|
||||
assert(
|
||||
st.feeUnknownError,
|
||||
"the estimate-failed message is not shown on the ERC-20 path",
|
||||
);
|
||||
assert(
|
||||
!st.gasError,
|
||||
"the ERC-20 network-fee message is shown for a fee that is unknown rather than unaffordable",
|
||||
);
|
||||
assert(
|
||||
st.sendDisabled,
|
||||
"Send is enabled on the ERC-20 path with no usable fee estimate",
|
||||
);
|
||||
assert(
|
||||
st.height === env.erc20PendingHeight,
|
||||
"the ERC-20 estimate-failed state is a different height than its pending state: " +
|
||||
env.erc20PendingHeight +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
const st = await confirmState(env.page);
|
||||
assert(
|
||||
st.fee === "Unable to estimate",
|
||||
"the ERC-20 fee line does not report the failure: " +
|
||||
JSON.stringify(st.fee),
|
||||
);
|
||||
assert(
|
||||
st.feeUnknownError,
|
||||
"the estimate-failed message is not shown on the ERC-20 path",
|
||||
);
|
||||
assert(
|
||||
!st.gasError,
|
||||
"the ERC-20 network-fee message is shown for a fee that is unknown rather than unaffordable",
|
||||
);
|
||||
assert(
|
||||
st.sendDisabled,
|
||||
"Send is enabled on the ERC-20 path with no usable fee estimate",
|
||||
);
|
||||
assert(
|
||||
st.height === env.erc20PendingHeight,
|
||||
"the ERC-20 estimate-failed state is a different height than its pending state: " +
|
||||
env.erc20PendingHeight +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
} finally {
|
||||
env.routeOpts.failGasEstimate = false;
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------------- the popup's own send, end to end (#305)
|
||||
@@ -2234,6 +2258,34 @@ async function fillPasswordAndSend(page) {
|
||||
await page.click("#btn-confirm-send");
|
||||
}
|
||||
|
||||
// Back to the address screen from wherever a send stopped, which is where a
|
||||
// passing send test leaves the popup for the next one. The success and failure
|
||||
// screens are left by their Done button. The wait for a receipt has no button
|
||||
// and asks only every ten seconds; a reopened popup resumes it and asks at
|
||||
// once, and seedReceipt has the stub node confirm the transaction.
|
||||
async function backToAddressAfterSend(env) {
|
||||
if (await env.page.isVisible("#view-wait-tx")) {
|
||||
env.routeOpts.seedReceipt = true;
|
||||
await waitForPersisted(
|
||||
env.page,
|
||||
"currentView",
|
||||
"wait-tx",
|
||||
"before closing the popup",
|
||||
);
|
||||
await env.page.close();
|
||||
env.page = await openPopup(env.ctx, env.popupUrl);
|
||||
await visible(env.page, "#view-success-tx");
|
||||
env.routeOpts.seedReceipt = false;
|
||||
}
|
||||
for (const done of ["#btn-success-tx-done", "#btn-error-tx-done"]) {
|
||||
if (await env.page.isVisible(done)) {
|
||||
await env.page.click(done);
|
||||
await visible(env.page, "#view-address");
|
||||
}
|
||||
}
|
||||
await backToAddress(env.page);
|
||||
}
|
||||
|
||||
async function goToTokenConfirm(env) {
|
||||
await goToConfirm(env.page, {
|
||||
token: STUB_TOKEN.address,
|
||||
@@ -2254,132 +2306,143 @@ test("the popup's own ERC-20 send broadcasts the amount it displayed (#305)", as
|
||||
// The previous test left the ETH balance at the fee-only fixture, which
|
||||
// blocks sending outright; this one has to be able to press Send.
|
||||
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true });
|
||||
const shown = await goToTokenConfirm(env);
|
||||
|
||||
const before = env.routeOpts.broadcastTransactions.length;
|
||||
// Confirm the transaction once it is broadcast, so the wait screen
|
||||
// resolves to the success view instead of polling for the rest of the run.
|
||||
env.routeOpts.seedReceipt = true;
|
||||
await fillPasswordAndSend(env.page);
|
||||
await visible(env.page, "#view-wait-tx", 60000);
|
||||
try {
|
||||
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true });
|
||||
const shown = await goToTokenConfirm(env);
|
||||
|
||||
const broadcast = env.routeOpts.broadcastTransactions;
|
||||
assert(
|
||||
broadcast.length === before + 1,
|
||||
"expected exactly one raw transaction to reach the RPC, got " +
|
||||
(broadcast.length - before),
|
||||
);
|
||||
const { signed, recipient, rawAmount } = decodeTransfer(
|
||||
broadcast[broadcast.length - 1],
|
||||
);
|
||||
const before = env.routeOpts.broadcastTransactions.length;
|
||||
await fillPasswordAndSend(env.page);
|
||||
await visible(env.page, "#view-wait-tx", 60000);
|
||||
|
||||
// The measurement, printed on every run: the amount the user read, and
|
||||
// what the signed bytes mean at each of the two candidate scales. Under
|
||||
// the defect these three lines disagree.
|
||||
console.log(
|
||||
"# erc-20 send artifact: displayed=" +
|
||||
JSON.stringify(shown) +
|
||||
" rawAmount=" +
|
||||
rawAmount +
|
||||
" asIf" +
|
||||
TOKEN_DECIMALS +
|
||||
"Decimals=" +
|
||||
formatUnits(rawAmount, TOKEN_DECIMALS) +
|
||||
" asIf" +
|
||||
LYING_DECIMALS +
|
||||
"Decimals=" +
|
||||
formatUnits(rawAmount, Number(LYING_DECIMALS)),
|
||||
);
|
||||
const broadcast = env.routeOpts.broadcastTransactions;
|
||||
assert(
|
||||
broadcast.length === before + 1,
|
||||
"expected exactly one raw transaction to reach the RPC, got " +
|
||||
(broadcast.length - before),
|
||||
);
|
||||
const { signed, recipient, rawAmount } = decodeTransfer(
|
||||
broadcast[broadcast.length - 1],
|
||||
);
|
||||
|
||||
assert(
|
||||
getAddress(signed.to) === getAddress(STUB_TOKEN.address),
|
||||
"the broadcast transaction does not call the token contract: " +
|
||||
signed.to,
|
||||
);
|
||||
assert(
|
||||
recipient === getAddress(STUB_COUNTERPARTY),
|
||||
"the broadcast transfer goes to " + recipient,
|
||||
);
|
||||
// What the whole issue turns on: the signed amount, read back at the
|
||||
// scale the SCREEN rendered with, is the number the screen rendered.
|
||||
const wanted = parseUnits(shown.split(" ")[0], TOKEN_DECIMALS);
|
||||
assert(
|
||||
rawAmount === wanted,
|
||||
"the broadcast transfer moves " +
|
||||
rawAmount +
|
||||
" base units, which is " +
|
||||
formatUnits(rawAmount, TOKEN_DECIMALS) +
|
||||
" " +
|
||||
STUB_TOKEN.symbol +
|
||||
" at the scale the confirmation screen displayed — but the screen" +
|
||||
" displayed " +
|
||||
JSON.stringify(shown) +
|
||||
", i.e. " +
|
||||
wanted +
|
||||
" base units (#305)",
|
||||
);
|
||||
// The measurement, printed on every run: the amount the user read,
|
||||
// and what the signed bytes mean at each of the two candidate scales.
|
||||
// Under the defect these three lines disagree.
|
||||
console.log(
|
||||
"# erc-20 send artifact: displayed=" +
|
||||
JSON.stringify(shown) +
|
||||
" rawAmount=" +
|
||||
rawAmount +
|
||||
" asIf" +
|
||||
TOKEN_DECIMALS +
|
||||
"Decimals=" +
|
||||
formatUnits(rawAmount, TOKEN_DECIMALS) +
|
||||
" asIf" +
|
||||
LYING_DECIMALS +
|
||||
"Decimals=" +
|
||||
formatUnits(rawAmount, Number(LYING_DECIMALS)),
|
||||
);
|
||||
|
||||
const summary = (
|
||||
await env.page.locator("#wait-tx-summary").innerText()
|
||||
).trim();
|
||||
assert(
|
||||
summary === shown,
|
||||
"the wait screen summarises the send as " +
|
||||
JSON.stringify(summary) +
|
||||
", not as the approved " +
|
||||
JSON.stringify(shown),
|
||||
);
|
||||
assert(
|
||||
getAddress(signed.to) === getAddress(STUB_TOKEN.address),
|
||||
"the broadcast transaction does not call the token contract: " +
|
||||
signed.to,
|
||||
);
|
||||
assert(
|
||||
recipient === getAddress(STUB_COUNTERPARTY),
|
||||
"the broadcast transfer goes to " + recipient,
|
||||
);
|
||||
// What the whole issue turns on: the signed amount, read back at the
|
||||
// scale the SCREEN rendered with, is the number the screen rendered.
|
||||
const wanted = parseUnits(shown.split(" ")[0], TOKEN_DECIMALS);
|
||||
assert(
|
||||
rawAmount === wanted,
|
||||
"the broadcast transfer moves " +
|
||||
rawAmount +
|
||||
" base units, which is " +
|
||||
formatUnits(rawAmount, TOKEN_DECIMALS) +
|
||||
" " +
|
||||
STUB_TOKEN.symbol +
|
||||
" at the scale the confirmation screen displayed — but the screen" +
|
||||
" displayed " +
|
||||
JSON.stringify(shown) +
|
||||
", i.e. " +
|
||||
wanted +
|
||||
" base units (#305)",
|
||||
);
|
||||
|
||||
await visible(env.page, "#view-success-tx", 60000);
|
||||
await env.page.click("#btn-success-tx-done");
|
||||
await visible(env.page, "#view-address");
|
||||
env.routeOpts.seedReceipt = false;
|
||||
const summary = (
|
||||
await env.page.locator("#wait-tx-summary").innerText()
|
||||
).trim();
|
||||
assert(
|
||||
summary === shown,
|
||||
"the wait screen summarises the send as " +
|
||||
JSON.stringify(summary) +
|
||||
", not as the approved " +
|
||||
JSON.stringify(shown),
|
||||
);
|
||||
|
||||
await visible(env.page, "#view-success-tx", 60000);
|
||||
} finally {
|
||||
env.routeOpts.seedReceipt = false;
|
||||
await backToAddressAfterSend(env);
|
||||
}
|
||||
});
|
||||
|
||||
test("a token that lies about decimals() at signing time broadcasts nothing (#305)", async (env) => {
|
||||
const shown = await goToTokenConfirm(env);
|
||||
try {
|
||||
const shown = await goToTokenConfirm(env);
|
||||
|
||||
// Only now, with the screen already built and its estimate already taken
|
||||
// at the explorer's scale, does the contract start answering differently.
|
||||
// This is the whole shape of the defect: a value read at signing time that
|
||||
// nothing on screen was ever derived from.
|
||||
env.routeOpts.tokenDecimalsOverride = LYING_DECIMALS;
|
||||
const before = env.routeOpts.broadcastTransactions.length;
|
||||
await fillPasswordAndSend(env.page);
|
||||
await visible(env.page, "#view-error-tx", 60000);
|
||||
env.routeOpts.tokenDecimalsOverride = null;
|
||||
// Only now, with the screen already built and its estimate already
|
||||
// taken at the explorer's scale, does the contract start answering
|
||||
// differently. This is the whole shape of the defect: a value read at
|
||||
// signing time that nothing on screen was ever derived from.
|
||||
env.routeOpts.tokenDecimalsOverride = LYING_DECIMALS;
|
||||
const before = env.routeOpts.broadcastTransactions.length;
|
||||
await fillPasswordAndSend(env.page);
|
||||
|
||||
assert(
|
||||
env.routeOpts.broadcastTransactions.length === before,
|
||||
"a transfer encoded against a contract that contradicts the " +
|
||||
"confirmation screen still reached the RPC (#305)",
|
||||
);
|
||||
// The count is asserted as soon as the send has ended either way, and
|
||||
// the screen only after it: a transfer that got through shows as the
|
||||
// wait for its receipt, never as the failure screen expected here.
|
||||
await visible(
|
||||
env.page,
|
||||
"#view-wait-tx:not(.hidden), #view-error-tx:not(.hidden)",
|
||||
60000,
|
||||
);
|
||||
assert(
|
||||
env.routeOpts.broadcastTransactions.length === before,
|
||||
"a transfer encoded against a contract that contradicts the " +
|
||||
"confirmation screen still reached the RPC (#305)",
|
||||
);
|
||||
await visible(env.page, "#view-error-tx");
|
||||
|
||||
const message = (
|
||||
await env.page.locator("#error-tx-message").innerText()
|
||||
).trim();
|
||||
console.log(
|
||||
"# erc-20 decimals refusal: displayed=" +
|
||||
JSON.stringify(shown) +
|
||||
" contract=" +
|
||||
LYING_DECIMALS +
|
||||
" message=" +
|
||||
JSON.stringify(message),
|
||||
);
|
||||
assert(
|
||||
message.includes("reports " + LYING_DECIMALS + " decimal places") &&
|
||||
message.includes("displayed using " + STUB_TOKEN.decimals),
|
||||
"the refusal does not name both scales it is refusing over: " +
|
||||
JSON.stringify(message),
|
||||
);
|
||||
assert(
|
||||
/^[A-Z].*\.$/s.test(message),
|
||||
"the refusal is not a full sentence: " + JSON.stringify(message),
|
||||
);
|
||||
|
||||
await env.page.click("#btn-error-tx-done");
|
||||
await visible(env.page, "#view-address");
|
||||
const message = (
|
||||
await env.page.locator("#error-tx-message").innerText()
|
||||
).trim();
|
||||
console.log(
|
||||
"# erc-20 decimals refusal: displayed=" +
|
||||
JSON.stringify(shown) +
|
||||
" contract=" +
|
||||
LYING_DECIMALS +
|
||||
" message=" +
|
||||
JSON.stringify(message),
|
||||
);
|
||||
assert(
|
||||
message.includes("reports " + LYING_DECIMALS + " decimal places") &&
|
||||
message.includes("displayed using " + STUB_TOKEN.decimals),
|
||||
"the refusal does not name both scales it is refusing over: " +
|
||||
JSON.stringify(message),
|
||||
);
|
||||
assert(
|
||||
/^[A-Z].*\.$/s.test(message),
|
||||
"the refusal is not a full sentence: " + JSON.stringify(message),
|
||||
);
|
||||
} finally {
|
||||
env.routeOpts.tokenDecimalsOverride = null;
|
||||
await backToAddressAfterSend(env);
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------------------------------- hostile token symbol (#307)
|
||||
@@ -2451,74 +2514,78 @@ test("a token whose symbol() returns markup renders as text (#307)", async (env)
|
||||
"# stub token symbol() now returns: " + JSON.stringify(HOSTILE_SYMBOL),
|
||||
);
|
||||
|
||||
// Close and reopen so the refresh that runs on open fetches balances
|
||||
// with the hostile symbol in them.
|
||||
await reopenPopup(env, "address");
|
||||
await env.page.waitForFunction(
|
||||
(addr) =>
|
||||
!!document.querySelector(
|
||||
'#address-balances [data-token="' + addr + '"]',
|
||||
),
|
||||
STUB_TOKEN.address,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
const onAddress = await env.page.evaluate(() => ({
|
||||
iframes: document.querySelectorAll("iframe").length,
|
||||
pwnPresent: !!document.getElementById("pwn"),
|
||||
}));
|
||||
console.log("# address-detail iframes = " + onAddress.iframes);
|
||||
assert(
|
||||
onAddress.iframes === 0 && !onAddress.pwnPresent,
|
||||
"the address screen contains " +
|
||||
onAddress.iframes +
|
||||
" iframe(s) after a hostile symbol rendered (#307)",
|
||||
);
|
||||
try {
|
||||
// Close and reopen so the refresh that runs on open fetches balances
|
||||
// with the hostile symbol in them.
|
||||
await reopenPopup(env, "address");
|
||||
await env.page.waitForFunction(
|
||||
(addr) =>
|
||||
!!document.querySelector(
|
||||
'#address-balances [data-token="' + addr + '"]',
|
||||
),
|
||||
STUB_TOKEN.address,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
const onAddress = await env.page.evaluate(() => ({
|
||||
iframes: document.querySelectorAll("iframe").length,
|
||||
pwnPresent: !!document.getElementById("pwn"),
|
||||
}));
|
||||
console.log("# address-detail iframes = " + onAddress.iframes);
|
||||
assert(
|
||||
onAddress.iframes === 0 && !onAddress.pwnPresent,
|
||||
"the address screen contains " +
|
||||
onAddress.iframes +
|
||||
" iframe(s) after a hostile symbol rendered (#307)",
|
||||
);
|
||||
|
||||
await env.page.click("#btn-address-back");
|
||||
await visible(env.page, "#view-main");
|
||||
await visible(
|
||||
env.page,
|
||||
'#wallet-list [data-token="' + STUB_TOKEN.address + '"]',
|
||||
60000,
|
||||
);
|
||||
await env.page.click("#btn-address-back");
|
||||
await visible(env.page, "#view-main");
|
||||
await visible(
|
||||
env.page,
|
||||
'#wallet-list [data-token="' + STUB_TOKEN.address + '"]',
|
||||
60000,
|
||||
);
|
||||
|
||||
const st = await hostileSymbolState(env.page, STUB_TOKEN.address);
|
||||
console.log(
|
||||
"# iframes in the popup DOM = " +
|
||||
st.iframes +
|
||||
" | #pwn present = " +
|
||||
st.pwnPresent +
|
||||
" | symbol = " +
|
||||
JSON.stringify(st.symbolText),
|
||||
);
|
||||
const st = await hostileSymbolState(env.page, STUB_TOKEN.address);
|
||||
console.log(
|
||||
"# iframes in the popup DOM = " +
|
||||
st.iframes +
|
||||
" | #pwn present = " +
|
||||
st.pwnPresent +
|
||||
" | symbol = " +
|
||||
JSON.stringify(st.symbolText),
|
||||
);
|
||||
|
||||
assert(st.rowFound, "the hostile token never rendered a row at all");
|
||||
assert(
|
||||
st.iframes === 0,
|
||||
"the popup DOM contains " + st.iframes + " iframe(s) (#307)",
|
||||
);
|
||||
assert(!st.pwnPresent, "the injected #pwn element is in the popup DOM");
|
||||
assert(
|
||||
st.symbolElementChildren === 0,
|
||||
"the symbol span grew " +
|
||||
st.symbolElementChildren +
|
||||
" element children out of a token symbol (#307)",
|
||||
);
|
||||
assert(
|
||||
st.symbolText === HOSTILE_SYMBOL_DISPLAYED,
|
||||
"the symbol did not render as the literal capped text " +
|
||||
JSON.stringify(HOSTILE_SYMBOL_DISPLAYED) +
|
||||
": " +
|
||||
JSON.stringify(st.symbolText),
|
||||
);
|
||||
assert(
|
||||
!st.rowText.includes("z-index"),
|
||||
"the uncapped symbol reached the screen: " + JSON.stringify(st.rowText),
|
||||
);
|
||||
assert(st.rowFound, "the hostile token never rendered a row at all");
|
||||
assert(
|
||||
st.iframes === 0,
|
||||
"the popup DOM contains " + st.iframes + " iframe(s) (#307)",
|
||||
);
|
||||
assert(!st.pwnPresent, "the injected #pwn element is in the popup DOM");
|
||||
assert(
|
||||
st.symbolElementChildren === 0,
|
||||
"the symbol span grew " +
|
||||
st.symbolElementChildren +
|
||||
" element children out of a token symbol (#307)",
|
||||
);
|
||||
assert(
|
||||
st.symbolText === HOSTILE_SYMBOL_DISPLAYED,
|
||||
"the symbol did not render as the literal capped text " +
|
||||
JSON.stringify(HOSTILE_SYMBOL_DISPLAYED) +
|
||||
": " +
|
||||
JSON.stringify(st.symbolText),
|
||||
);
|
||||
assert(
|
||||
!st.rowText.includes("z-index"),
|
||||
"the uncapped symbol reached the screen: " +
|
||||
JSON.stringify(st.rowText),
|
||||
);
|
||||
} finally {
|
||||
// Put the fixture back before the next test reads it.
|
||||
env.routeOpts.tokenSymbolOverride = null;
|
||||
}
|
||||
|
||||
// Put the fixture back before the next test reads it, and let the
|
||||
// stored balances be rewritten with the honest symbol.
|
||||
env.routeOpts.tokenSymbolOverride = null;
|
||||
// Let the stored balances be rewritten with the honest symbol.
|
||||
await reopenPopup(env, "main");
|
||||
await env.page.waitForFunction(
|
||||
(addr) => {
|
||||
|
||||
Reference in New Issue
Block a user