fix: TransactionDetail reads a transaction from the Blockscout of the network it was sent on (closes #623)
TransactionDetail read the block, network details and calldata from the active network's Blockscout and decoded the calldata for the active network, so after a network switch a transaction sent on the other network showed none of them. It now reads them from the Blockscout Settings has for the network the transaction was sent on, and decodes the calldata for that network. endpointsByChainId() in the shared view helpers gives a network's RPC and Blockscout URLs, picked as reviewedNetworkProvider() picked its node; reviewedNetworkProvider() now uses it. Restore refuses a stored transaction whose chain id names no network, as it does for WaitTx. Model: opus-5-5
This commit was merged in pull request #627.
This commit is contained in:
@@ -154,7 +154,14 @@ describe("Back onto a view the reopened popup never rendered", () => {
|
||||
|
||||
test("Back onto the transaction detail renders it", () => {
|
||||
reopenedOn("settings", ["main", "transaction"], {
|
||||
viewData: { tx: { hash: "0xdead", from: ADDRESS, to: ADDRESS } },
|
||||
viewData: {
|
||||
tx: {
|
||||
hash: "0xdead",
|
||||
from: ADDRESS,
|
||||
to: ADDRESS,
|
||||
chainId: "0x1",
|
||||
},
|
||||
},
|
||||
});
|
||||
goBack();
|
||||
expect(calls).toEqual(["transactionDetail"]);
|
||||
|
||||
@@ -465,12 +465,19 @@ const CONTRACT = [
|
||||
from: ADDRESS,
|
||||
to: ADDRESS,
|
||||
contractAddress: 42,
|
||||
chainId: "0x1",
|
||||
},
|
||||
},
|
||||
views: ["transaction"],
|
||||
restored: false,
|
||||
},
|
||||
// A chain id naming no network has no explorer to link to.
|
||||
// A transaction whose chain id is missing or names no network has
|
||||
// no explorer to link to and no Blockscout to be read from.
|
||||
{
|
||||
value: { tx: { hash: "0x1", from: ADDRESS, to: ADDRESS } },
|
||||
views: ["transaction"],
|
||||
restored: false,
|
||||
},
|
||||
{
|
||||
value: {
|
||||
tx: {
|
||||
@@ -836,7 +843,13 @@ const WELL_FORMED_DATA = {
|
||||
message: "boom",
|
||||
to: ADDRESS,
|
||||
decoded: { details: [{ address: TOKEN_ADDRESS }] },
|
||||
tx: { hash: "0x1", from: ADDRESS, to: ADDRESS, contractAddress: null },
|
||||
tx: {
|
||||
hash: "0x1",
|
||||
from: ADDRESS,
|
||||
to: ADDRESS,
|
||||
contractAddress: null,
|
||||
chainId: "0x1",
|
||||
},
|
||||
pendingTx: {
|
||||
token: "ETH",
|
||||
from: ADDRESS,
|
||||
|
||||
@@ -58,7 +58,7 @@ function restoringOnto(view, viewData) {
|
||||
test("the transaction screen's Blockscout request reaches the stub", async () => {
|
||||
const page = await bootPopup(
|
||||
restoringOnto("transaction", {
|
||||
tx: { hash: "0x1", from: ADDRESS, to: ADDRESS },
|
||||
tx: { hash: "0x1", from: ADDRESS, to: ADDRESS, chainId: "0x1" },
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
//
|
||||
// Its links open the explorer of the network the transaction was sent on,
|
||||
// whichever network is active
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/584).
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/584), and the transaction is
|
||||
// read from that network's Blockscout, the one Settings has for it
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/623).
|
||||
//
|
||||
// The row is the one the real history parser makes from the explorer's answer,
|
||||
// opened through transactionDetail.show(), which is what a click on the row
|
||||
@@ -26,10 +28,10 @@ jest.mock("../src/shared/log", () => ({
|
||||
warnf: () => {},
|
||||
errorf: () => {},
|
||||
},
|
||||
debugFetch: async (url) => ({
|
||||
ok: true,
|
||||
json: async () => mockExplorerAnswer(url),
|
||||
}),
|
||||
debugFetch: async (url) => {
|
||||
mockAsked.push(url);
|
||||
return { ok: true, json: async () => mockExplorerAnswer(url) };
|
||||
},
|
||||
setRuntimeDebug: () => {},
|
||||
isDebug: () => false,
|
||||
}));
|
||||
@@ -44,6 +46,8 @@ const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||
|
||||
const EXPLORER = "https://explorer.example/api/v2";
|
||||
// A Blockscout that knows nothing of the transaction.
|
||||
const OTHER_EXPLORER = "https://other-explorer.example/api/v2";
|
||||
const HOLDER = "0x0000000000000000000000000000000000000a11";
|
||||
const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD";
|
||||
// A token the wallet tracks and the bundled token list does not name.
|
||||
@@ -56,6 +60,9 @@ const TRANSFER_DATA = new Interface([
|
||||
"function transfer(address to, uint256 amount)",
|
||||
]).encodeFunctionData("transfer", [RECIPIENT, 1234500000n]);
|
||||
|
||||
// Every URL asked of a Blockscout, in order.
|
||||
const mockAsked = [];
|
||||
|
||||
// The explorer's answers: the transfer in HOLDER's token transfers, and the
|
||||
// transaction itself, sent to the token contract.
|
||||
function mockExplorerAnswer(url) {
|
||||
@@ -149,6 +156,7 @@ beforeEach(() => {
|
||||
globalThis.document = makeDocument();
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.blockscoutUrl = EXPLORER;
|
||||
state.networkEndpoints = {};
|
||||
state.wallets = [
|
||||
{
|
||||
name: "Main",
|
||||
@@ -188,11 +196,13 @@ test("a token transfer's decoded details name the token contract and the amount
|
||||
);
|
||||
});
|
||||
|
||||
// Mainnet is active and the row was read from Sepolia's explorer. The stub
|
||||
// explorer answers whichever network asks, so every link on the screen is
|
||||
// drawn.
|
||||
test("with mainnet active, a Sepolia transaction's links open the Sepolia explorer", async () => {
|
||||
// Mainnet is active, and its Blockscout knows nothing of the transaction. The
|
||||
// row was read from the Blockscout Settings remembers for Sepolia, and the
|
||||
// screen reads the transaction from there too, so every link on it is drawn.
|
||||
test("with mainnet active, a Sepolia transaction is read from Sepolia's Blockscout and its links open the Sepolia explorer", async () => {
|
||||
state.networkId = "mainnet";
|
||||
state.blockscoutUrl = OTHER_EXPLORER;
|
||||
state.networkEndpoints = { sepolia: { blockscoutUrl: EXPLORER } };
|
||||
const [row] = await fetchRecentTransactions(
|
||||
HOLDER,
|
||||
EXPLORER,
|
||||
@@ -217,3 +227,21 @@ test("with mainnet active, a Sepolia transaction's links open the Sepolia explor
|
||||
`${sepolia}/address/${RECIPIENT}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("with mainnet active and no Blockscout remembered for Sepolia, a Sepolia transaction is asked of Sepolia's default Blockscout", async () => {
|
||||
state.networkId = "mainnet";
|
||||
state.blockscoutUrl = OTHER_EXPLORER;
|
||||
const [row] = await fetchRecentTransactions(
|
||||
HOLDER,
|
||||
EXPLORER,
|
||||
NETWORKS.sepolia.chainId,
|
||||
);
|
||||
mockAsked.length = 0;
|
||||
|
||||
transactionDetail.show(row);
|
||||
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
expect(mockAsked).toEqual([
|
||||
NETWORKS.sepolia.defaultBlockscoutUrl + "/transactions/" + TX_HASH,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -186,14 +186,14 @@ function node(id) {
|
||||
return globalThis.document.getElementById(id);
|
||||
}
|
||||
|
||||
// Open the transaction detail screen with Sepolia active, for the
|
||||
// transaction the explorer reports sent to `sentTo`, and wait for its
|
||||
// Open the transaction detail screen with `activeNetworkId` active, for the
|
||||
// Sepolia transaction the explorer reports sent to `sentTo`, and wait for its
|
||||
// decoded details.
|
||||
async function showDetailSentTo(sentTo) {
|
||||
async function showDetailSentTo(sentTo, activeNetworkId) {
|
||||
mockSentTo = sentTo;
|
||||
globalThis.document = makeDocument();
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.networkId = "sepolia";
|
||||
state.networkId = activeNetworkId;
|
||||
state.blockscoutUrl = "https://explorer.example/api/v2";
|
||||
state.viewData = {};
|
||||
state.viewStack = [];
|
||||
@@ -216,21 +216,29 @@ async function showDetailSentTo(sentTo) {
|
||||
|
||||
describe("execute() calldata on the transaction detail screen", () => {
|
||||
test("sent to a router on the active network, it is a swap", async () => {
|
||||
await showDetailSentTo(SEPOLIA_ROUTER);
|
||||
await showDetailSentTo(SEPOLIA_ROUTER, "sepolia");
|
||||
expect(node("tx-detail-calldata-details").innerHTML).toContain(
|
||||
"Swap via Uniswap Universal Router",
|
||||
);
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/623
|
||||
test("sent to a router on its network, with the other network active, it is a swap", async () => {
|
||||
await showDetailSentTo(SEPOLIA_ROUTER, "mainnet");
|
||||
expect(node("tx-detail-calldata-details").innerHTML).toContain(
|
||||
"Swap via Uniswap Universal Router",
|
||||
);
|
||||
});
|
||||
|
||||
test("sent to an ordinary address, it is not decoded", async () => {
|
||||
await showDetailSentTo(ORDINARY);
|
||||
await showDetailSentTo(ORDINARY, "sepolia");
|
||||
expect(node("tx-detail-calldata-action").textContent).toBe("execute");
|
||||
expect(node("tx-detail-calldata-details").innerHTML).toBe("");
|
||||
expect(node("tx-detail-rawdata").innerHTML).toContain(SWAP_DATA);
|
||||
});
|
||||
|
||||
test("sent to a router of the other network only, it is not decoded", async () => {
|
||||
await showDetailSentTo(MAINNET_ROUTER);
|
||||
await showDetailSentTo(MAINNET_ROUTER, "sepolia");
|
||||
expect(node("tx-detail-calldata-action").textContent).toBe("execute");
|
||||
expect(node("tx-detail-calldata-details").innerHTML).toBe("");
|
||||
expect(node("tx-detail-rawdata").innerHTML).toContain(SWAP_DATA);
|
||||
|
||||
Reference in New Issue
Block a user