Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c5bd73f30 |
@@ -311,10 +311,25 @@ function populateOnChainDetails(txData, chainId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Whether TransactionDetail is on screen for the transaction with `txHash` on
|
||||
// `chainId`.
|
||||
function isOnScreen(txHash, chainId) {
|
||||
const tx = state.viewData.tx;
|
||||
return (
|
||||
state.currentView === "transaction" &&
|
||||
tx?.hash === txHash &&
|
||||
tx?.chainId === chainId
|
||||
);
|
||||
}
|
||||
|
||||
// Reads the block, network details and calldata from Blockscout, and decodes
|
||||
// the calldata. While the stored "Use Blockscout" is off, Blockscout is not
|
||||
// asked. An answer that lands after the setting went off is dropped
|
||||
// (src/shared/storedSettings.js). Either way none of them is shown.
|
||||
//
|
||||
// Blockscout can answer after the user has left this screen or opened another
|
||||
// transaction. That answer is dropped. If that request fails, its error is not
|
||||
// logged.
|
||||
async function loadFullTxDetails(txHash, chainId) {
|
||||
if (!(await storedSettingIsOn("useBlockscout"))) return;
|
||||
const section = $("tx-detail-calldata-section");
|
||||
@@ -333,6 +348,7 @@ async function loadFullTxDetails(txHash, chainId) {
|
||||
if (!resp.ok) return;
|
||||
const txData = await resp.json();
|
||||
if (!(await storedSettingIsOn("useBlockscout"))) return;
|
||||
if (!isOnScreen(txHash, chainId)) return;
|
||||
|
||||
// Populate on-chain detail fields (block, nonce, gas, fee)
|
||||
populateOnChainDetails(txData, chainId);
|
||||
@@ -399,7 +415,9 @@ async function loadFullTxDetails(txHash, chainId) {
|
||||
attachCopyHandlers(container);
|
||||
}
|
||||
} catch (e) {
|
||||
log.errorf("loadCalldata failed:", e.message);
|
||||
if (isOnScreen(txHash, chainId)) {
|
||||
log.errorf("loadCalldata failed:", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
// TransactionDetail shows only details that Blockscout gave for the
|
||||
// transaction on screen (https://git.eeqj.de/sneak/AutistMask/issues/675).
|
||||
//
|
||||
// The screen asks Blockscout for its transaction and does not wait for the
|
||||
// answer. So the user can press Back and open a second transaction before
|
||||
// Blockscout answers for the first. The first answer must then fill nothing on
|
||||
// the second transaction's screen. If the first request fails instead, its
|
||||
// error must not be logged.
|
||||
//
|
||||
// Driven through the real popup over a stored profile
|
||||
// (tests/support/popupBoot.js). Each transaction is opened as a history screen
|
||||
// opens it: the current screen is pushed, then transactionDetail.show() draws
|
||||
// the transaction.
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
bootPopup,
|
||||
cleanupPopup,
|
||||
unversionedValidProfile,
|
||||
ADDRESS,
|
||||
} = require("./support/popupBoot");
|
||||
|
||||
const BLOCKSCOUT = "https://eth.blockscout.com/api/v2";
|
||||
const RECIPIENT = "0x2222222222222222222222222222222222222222";
|
||||
|
||||
// A history entry for a send of 1 ETH from ADDRESS to RECIPIENT on mainnet.
|
||||
function historyEntry(hash) {
|
||||
return {
|
||||
hash,
|
||||
from: ADDRESS,
|
||||
to: RECIPIENT,
|
||||
value: "1.0000",
|
||||
exactValue: "1.0",
|
||||
symbol: "ETH",
|
||||
timestamp: 1790000000,
|
||||
isError: false,
|
||||
direction: "sent",
|
||||
directionLabel: "Sent",
|
||||
contractAddress: null,
|
||||
chainId: "0x1",
|
||||
};
|
||||
}
|
||||
|
||||
const FIRST = historyEntry("0x" + "aa".repeat(32));
|
||||
const SECOND = historyEntry("0x" + "bb".repeat(32));
|
||||
|
||||
// What Blockscout reports for each transaction.
|
||||
const ANSWERS = {
|
||||
[FIRST.hash]: {
|
||||
block_number: 21000001,
|
||||
nonce: 4441,
|
||||
fee: { value: "21000000000000" },
|
||||
raw_input: "0x11111111",
|
||||
method: "firstMethod",
|
||||
},
|
||||
[SECOND.hash]: {
|
||||
block_number: 21000002,
|
||||
nonce: 4442,
|
||||
fee: { value: "42000000000000" },
|
||||
raw_input: "0x22222222",
|
||||
method: "secondMethod",
|
||||
},
|
||||
};
|
||||
|
||||
// The text each answer puts on screen: its block, nonce, fee, method name and
|
||||
// raw data. No text from one answer appears in the other.
|
||||
const FIRST_SHOWN = [
|
||||
"21000001",
|
||||
"4441",
|
||||
"0.000021 ETH",
|
||||
"firstMethod",
|
||||
"0x11111111",
|
||||
];
|
||||
const SECOND_SHOWN = [
|
||||
"21000002",
|
||||
"4442",
|
||||
"0.000042 ETH",
|
||||
"secondMethod",
|
||||
"0x22222222",
|
||||
];
|
||||
|
||||
// The error the first request fails with in the second test.
|
||||
const FIRST_FAILURE = "Blockscout did not answer the first request";
|
||||
|
||||
// Everything drawn in the fields that Blockscout's answer fills.
|
||||
function drawnDetails(page) {
|
||||
return [
|
||||
"tx-detail-block",
|
||||
"tx-detail-nonce",
|
||||
"tx-detail-fee",
|
||||
"tx-detail-calldata-action",
|
||||
"tx-detail-rawdata",
|
||||
]
|
||||
.map((id) => page.node(id).innerHTML + page.node(id).textContent)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
// Opens `entry` on TransactionDetail, as a history screen does.
|
||||
function open(entry) {
|
||||
require("../src/popup/views/helpers").pushCurrentView();
|
||||
require("../src/popup/views/transactionDetail").show(entry);
|
||||
}
|
||||
|
||||
// Boots the popup on Home with "Use Blockscout" on. The user opens FIRST,
|
||||
// whose request to Blockscout is held. Then the user presses Back and opens
|
||||
// SECOND, whose request is answered at once. Returns the page and the held
|
||||
// request's two outcomes: answer() lets FIRST's answer land, and fail() makes
|
||||
// the request fail.
|
||||
async function secondOpenedWhileFirstIsOut() {
|
||||
const page = await bootPopup(
|
||||
unversionedValidProfile({ useBlockscout: true }),
|
||||
);
|
||||
let answer;
|
||||
let fail;
|
||||
const firstHeld = new Promise((resolve, reject) => {
|
||||
answer = resolve;
|
||||
fail = reject;
|
||||
});
|
||||
globalThis.fetch = jest.fn(async (url) => {
|
||||
const hash = String(url).slice((BLOCKSCOUT + "/transactions/").length);
|
||||
if (hash === FIRST.hash) await firstHeld;
|
||||
return { ok: true, status: 200, json: async () => ANSWERS[hash] };
|
||||
});
|
||||
|
||||
open(FIRST);
|
||||
await page.settle();
|
||||
await page.click("btn-tx-back");
|
||||
expect(page.visibleViews()).toEqual(["main"]);
|
||||
open(SECOND);
|
||||
await page.settle();
|
||||
expect(globalThis.fetch.mock.calls.map(([url]) => url)).toEqual([
|
||||
BLOCKSCOUT + "/transactions/" + FIRST.hash,
|
||||
BLOCKSCOUT + "/transactions/" + SECOND.hash,
|
||||
]);
|
||||
return {
|
||||
page,
|
||||
answer: () => answer(),
|
||||
fail: () => fail(new Error(FIRST_FAILURE)),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupPopup();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("an answer for the first transaction that lands on the second one's screen fills nothing", async () => {
|
||||
const { page, answer } = await secondOpenedWhileFirstIsOut();
|
||||
answer();
|
||||
await page.settle();
|
||||
|
||||
expect(page.visibleViews()).toEqual(["transaction"]);
|
||||
const drawn = drawnDetails(page);
|
||||
for (const text of SECOND_SHOWN) expect(drawn).toContain(text);
|
||||
for (const text of FIRST_SHOWN) expect(drawn).not.toContain(text);
|
||||
});
|
||||
|
||||
test("a request for the first transaction that fails on the second one's screen logs nothing", async () => {
|
||||
const { page, fail } = await secondOpenedWhileFirstIsOut();
|
||||
fail();
|
||||
await page.settle();
|
||||
|
||||
expect(page.visibleViews()).toEqual(["transaction"]);
|
||||
const drawn = drawnDetails(page);
|
||||
for (const text of SECOND_SHOWN) expect(drawn).toContain(text);
|
||||
expect(console.error.mock.calls.flat()).not.toContain(FIRST_FAILURE);
|
||||
});
|
||||
Reference in New Issue
Block a user