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
|
// Reads the block, network details and calldata from Blockscout, and decodes
|
||||||
// the calldata. While the stored "Use Blockscout" is off, Blockscout is not
|
// the calldata. While the stored "Use Blockscout" is off, Blockscout is not
|
||||||
// asked. An answer that lands after the setting went off is dropped
|
// asked. An answer that lands after the setting went off is dropped
|
||||||
// (src/shared/storedSettings.js). Either way none of them is shown.
|
// (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) {
|
async function loadFullTxDetails(txHash, chainId) {
|
||||||
if (!(await storedSettingIsOn("useBlockscout"))) return;
|
if (!(await storedSettingIsOn("useBlockscout"))) return;
|
||||||
const section = $("tx-detail-calldata-section");
|
const section = $("tx-detail-calldata-section");
|
||||||
@@ -333,6 +348,7 @@ async function loadFullTxDetails(txHash, chainId) {
|
|||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
const txData = await resp.json();
|
const txData = await resp.json();
|
||||||
if (!(await storedSettingIsOn("useBlockscout"))) return;
|
if (!(await storedSettingIsOn("useBlockscout"))) return;
|
||||||
|
if (!isOnScreen(txHash, chainId)) return;
|
||||||
|
|
||||||
// Populate on-chain detail fields (block, nonce, gas, fee)
|
// Populate on-chain detail fields (block, nonce, gas, fee)
|
||||||
populateOnChainDetails(txData, chainId);
|
populateOnChainDetails(txData, chainId);
|
||||||
@@ -399,7 +415,9 @@ async function loadFullTxDetails(txHash, chainId) {
|
|||||||
attachCopyHandlers(container);
|
attachCopyHandlers(container);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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