Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8101b559e |
@@ -1972,8 +1972,10 @@ view would leave a wallet one click from deletion.
|
|||||||
#### TransactionDetail (`transaction`)
|
#### TransactionDetail (`transaction`)
|
||||||
|
|
||||||
- **When**: User tapped a transaction row on Home, AddressDetail, or
|
- **When**: User tapped a transaction row on Home, AddressDetail, or
|
||||||
AddressToken. Its etherscan links open the explorer of the network the
|
AddressToken. The screen describes the transaction on the network it was sent
|
||||||
transaction was sent on, whichever network is active.
|
on, whichever network is active: its block, network details, decoded details
|
||||||
|
and raw data are read from that network's Blockscout, the one Settings has for
|
||||||
|
it, and its etherscan links open that network's explorer.
|
||||||
- **Elements** (grouped into logical blocks using light well containers; field
|
- **Elements** (grouped into logical blocks using light well containers; field
|
||||||
labels are self-explanatory so groups have no headings):
|
labels are self-explanatory so groups have no headings):
|
||||||
- "Transaction" heading, "Back" button
|
- "Transaction" heading, "Back" button
|
||||||
|
|||||||
@@ -608,18 +608,28 @@ const EXT_ICON =
|
|||||||
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
|
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
|
||||||
`</svg></span>`;
|
`</svg></span>`;
|
||||||
|
|
||||||
// The node of the network a transaction was reviewed for, the one its chain id
|
// The RPC and Blockscout URLs Settings has for the network a transaction was
|
||||||
// names, which need not be the active one: the popup can reopen onto ConfirmTx
|
// reviewed for or sent on, the one its chain id names, which need not be the
|
||||||
// or WaitTx after Settings switched the network. Its RPC URL is the one
|
// active one: the popup can reopen onto ConfirmTx, WaitTx or TransactionDetail
|
||||||
// Settings has for that network: the active network's is state.rpcUrl, any
|
// after Settings switched the network. The active network's are state.rpcUrl
|
||||||
// other's the one remembered for it (see applyChainSwitchFields()).
|
// and state.blockscoutUrl, any other's the ones remembered for it, or its
|
||||||
function reviewedNetworkProvider(txInfo) {
|
// defaults (see applyChainSwitchFields()).
|
||||||
|
function reviewedNetworkEndpoints(txInfo) {
|
||||||
const net = networkByChainId(txInfo.chainId);
|
const net = networkByChainId(txInfo.chainId);
|
||||||
if (net.id === state.networkId) {
|
if (net.id === state.networkId) {
|
||||||
return getProvider(state.rpcUrl, net.id);
|
return { rpcUrl: state.rpcUrl, blockscoutUrl: state.blockscoutUrl };
|
||||||
}
|
}
|
||||||
const remembered = state.networkEndpoints[net.id] || {};
|
const remembered = state.networkEndpoints[net.id] || {};
|
||||||
return getProvider(remembered.rpcUrl || net.defaultRpcUrl, net.id);
|
return {
|
||||||
|
rpcUrl: remembered.rpcUrl || net.defaultRpcUrl,
|
||||||
|
blockscoutUrl: remembered.blockscoutUrl || net.defaultBlockscoutUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The node of the network a transaction was reviewed for.
|
||||||
|
function reviewedNetworkProvider(txInfo) {
|
||||||
|
const net = networkByChainId(txInfo.chainId);
|
||||||
|
return getProvider(reviewedNetworkEndpoints(txInfo).rpcUrl, net.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block-explorer URLs. The origin is a per-network constant from
|
// Block-explorer URLs. The origin is a per-network constant from
|
||||||
@@ -777,6 +787,7 @@ module.exports = {
|
|||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
etherscanLinkHtml,
|
etherscanLinkHtml,
|
||||||
explorerUrl,
|
explorerUrl,
|
||||||
|
reviewedNetworkEndpoints,
|
||||||
reviewedNetworkProvider,
|
reviewedNetworkProvider,
|
||||||
EXT_ICON,
|
EXT_ICON,
|
||||||
truncateMiddle,
|
truncateMiddle,
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ const {
|
|||||||
copyableHtml,
|
copyableHtml,
|
||||||
etherscanLinkHtml,
|
etherscanLinkHtml,
|
||||||
explorerUrl,
|
explorerUrl,
|
||||||
|
reviewedNetworkEndpoints,
|
||||||
displaySymbol,
|
displaySymbol,
|
||||||
goBack,
|
goBack,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, currentNetwork } = require("../../shared/state");
|
const { state } = require("../../shared/state");
|
||||||
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
||||||
const { formatEther, formatUnits } = require("ethers");
|
const { formatEther, formatUnits } = require("ethers");
|
||||||
const { log, debugFetch } = require("../../shared/log");
|
const { log, debugFetch } = require("../../shared/log");
|
||||||
@@ -84,9 +85,10 @@ function show(tx) {
|
|||||||
contractAddress: tx.contractAddress || null,
|
contractAddress: tx.contractAddress || null,
|
||||||
// The network the history entry was read from. The type line and
|
// The network the history entry was read from. The type line and
|
||||||
// the fee are in its native currency, not the active network's,
|
// the fee are in its native currency, not the active network's,
|
||||||
// and the links open its explorer: the active network can change,
|
// the links open its explorer, and the block, network details and
|
||||||
// in Settings or when the user approves a site's request, before
|
// calldata are read from its Blockscout: the active network can
|
||||||
// a later popup shows this screen again.
|
// change, in Settings or when the user approves a site's request,
|
||||||
|
// before a later popup shows this screen again.
|
||||||
chainId: tx.chainId,
|
chainId: tx.chainId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -189,7 +191,7 @@ function render() {
|
|||||||
if (el) el.classList.add("hidden");
|
if (el) el.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFullTxDetails(tx.hash, tx.chainId);
|
loadFullTxDetails(tx);
|
||||||
|
|
||||||
const isoStr = isoDate(tx.timestamp);
|
const isoStr = isoDate(tx.timestamp);
|
||||||
$("tx-detail-time").innerHTML =
|
$("tx-detail-time").innerHTML =
|
||||||
@@ -301,7 +303,8 @@ function populateOnChainDetails(txData, chainId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFullTxDetails(txHash, chainId) {
|
async function loadFullTxDetails(tx) {
|
||||||
|
const chainId = tx.chainId;
|
||||||
const section = $("tx-detail-calldata-section");
|
const section = $("tx-detail-calldata-section");
|
||||||
const actionEl = $("tx-detail-calldata-action");
|
const actionEl = $("tx-detail-calldata-action");
|
||||||
const detailsEl = $("tx-detail-calldata-details");
|
const detailsEl = $("tx-detail-calldata-details");
|
||||||
@@ -311,8 +314,9 @@ async function loadFullTxDetails(txHash, chainId) {
|
|||||||
if (!section || !actionEl || !detailsEl) return;
|
if (!section || !actionEl || !detailsEl) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const { blockscoutUrl } = reviewedNetworkEndpoints(tx);
|
||||||
const resp = await debugFetch(
|
const resp = await debugFetch(
|
||||||
state.blockscoutUrl + "/transactions/" + txHash,
|
blockscoutUrl + "/transactions/" + tx.hash,
|
||||||
);
|
);
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
const txData = await resp.json();
|
const txData = await resp.json();
|
||||||
@@ -326,12 +330,12 @@ async function loadFullTxDetails(txHash, chainId) {
|
|||||||
// Decoded against the address the transaction was sent to, as the
|
// Decoded against the address the transaction was sent to, as the
|
||||||
// explorer reports it, not the history entry's `to`: for a token
|
// explorer reports it, not the history entry's `to`: for a token
|
||||||
// transfer that is the transfer's recipient, not the token contract.
|
// transfer that is the transfer's recipient, not the token contract.
|
||||||
// On the active network, the one whose explorer was just asked, and
|
// On the network the transaction was sent on, whose Blockscout was
|
||||||
// from the sender it reports.
|
// just asked, and from the sender it reports.
|
||||||
const decoded = decodeCalldata(
|
const decoded = decodeCalldata(
|
||||||
inputData,
|
inputData,
|
||||||
txData.to?.hash || "",
|
txData.to?.hash || "",
|
||||||
currentNetwork().chainId,
|
chainId,
|
||||||
txData.from?.hash || "",
|
txData.from?.hash || "",
|
||||||
);
|
);
|
||||||
if (decoded) {
|
if (decoded) {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function restoringOnto(view, viewData) {
|
|||||||
test("the transaction screen's Blockscout request reaches the stub", async () => {
|
test("the transaction screen's Blockscout request reaches the stub", async () => {
|
||||||
const page = await bootPopup(
|
const page = await bootPopup(
|
||||||
restoringOnto("transaction", {
|
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,
|
// Its links open the explorer of the network the transaction was sent on,
|
||||||
// whichever network is active
|
// 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,
|
// 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
|
// opened through transactionDetail.show(), which is what a click on the row
|
||||||
@@ -26,10 +28,10 @@ jest.mock("../src/shared/log", () => ({
|
|||||||
warnf: () => {},
|
warnf: () => {},
|
||||||
errorf: () => {},
|
errorf: () => {},
|
||||||
},
|
},
|
||||||
debugFetch: async (url) => ({
|
debugFetch: async (url) => {
|
||||||
ok: true,
|
mockAsked.push(url);
|
||||||
json: async () => mockExplorerAnswer(url),
|
return { ok: true, json: async () => mockExplorerAnswer(url) };
|
||||||
}),
|
},
|
||||||
setRuntimeDebug: () => {},
|
setRuntimeDebug: () => {},
|
||||||
isDebug: () => false,
|
isDebug: () => false,
|
||||||
}));
|
}));
|
||||||
@@ -44,6 +46,8 @@ const { fetchRecentTransactions } = require("../src/shared/transactions");
|
|||||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||||
|
|
||||||
const EXPLORER = "https://explorer.example/api/v2";
|
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 HOLDER = "0x0000000000000000000000000000000000000a11";
|
||||||
const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD";
|
const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD";
|
||||||
// A token the wallet tracks and the bundled token list does not name.
|
// 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)",
|
"function transfer(address to, uint256 amount)",
|
||||||
]).encodeFunctionData("transfer", [RECIPIENT, 1234500000n]);
|
]).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
|
// The explorer's answers: the transfer in HOLDER's token transfers, and the
|
||||||
// transaction itself, sent to the token contract.
|
// transaction itself, sent to the token contract.
|
||||||
function mockExplorerAnswer(url) {
|
function mockExplorerAnswer(url) {
|
||||||
@@ -149,6 +156,7 @@ beforeEach(() => {
|
|||||||
globalThis.document = makeDocument();
|
globalThis.document = makeDocument();
|
||||||
globalThis.window = { location: { search: "" } };
|
globalThis.window = { location: { search: "" } };
|
||||||
state.blockscoutUrl = EXPLORER;
|
state.blockscoutUrl = EXPLORER;
|
||||||
|
state.networkEndpoints = {};
|
||||||
state.wallets = [
|
state.wallets = [
|
||||||
{
|
{
|
||||||
name: "Main",
|
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
|
// Mainnet is active, and its Blockscout knows nothing of the transaction. The
|
||||||
// explorer answers whichever network asks, so every link on the screen is
|
// row was read from the Blockscout Settings remembers for Sepolia, and the
|
||||||
// drawn.
|
// screen reads the transaction from there too, so every link on it is drawn.
|
||||||
test("with mainnet active, a Sepolia transaction's links open the Sepolia explorer", async () => {
|
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.networkId = "mainnet";
|
||||||
|
state.blockscoutUrl = OTHER_EXPLORER;
|
||||||
|
state.networkEndpoints = { sepolia: { blockscoutUrl: EXPLORER } };
|
||||||
const [row] = await fetchRecentTransactions(
|
const [row] = await fetchRecentTransactions(
|
||||||
HOLDER,
|
HOLDER,
|
||||||
EXPLORER,
|
EXPLORER,
|
||||||
@@ -217,3 +227,21 @@ test("with mainnet active, a Sepolia transaction's links open the Sepolia explor
|
|||||||
`${sepolia}/address/${RECIPIENT}`,
|
`${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);
|
return globalThis.document.getElementById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the transaction detail screen with Sepolia active, for the
|
// Open the transaction detail screen with `activeNetworkId` active, for the
|
||||||
// transaction the explorer reports sent to `sentTo`, and wait for its
|
// Sepolia transaction the explorer reports sent to `sentTo`, and wait for its
|
||||||
// decoded details.
|
// decoded details.
|
||||||
async function showDetailSentTo(sentTo) {
|
async function showDetailSentTo(sentTo, activeNetworkId) {
|
||||||
mockSentTo = sentTo;
|
mockSentTo = sentTo;
|
||||||
globalThis.document = makeDocument();
|
globalThis.document = makeDocument();
|
||||||
globalThis.window = { location: { search: "" } };
|
globalThis.window = { location: { search: "" } };
|
||||||
state.networkId = "sepolia";
|
state.networkId = activeNetworkId;
|
||||||
state.blockscoutUrl = "https://explorer.example/api/v2";
|
state.blockscoutUrl = "https://explorer.example/api/v2";
|
||||||
state.viewData = {};
|
state.viewData = {};
|
||||||
state.viewStack = [];
|
state.viewStack = [];
|
||||||
@@ -216,21 +216,29 @@ async function showDetailSentTo(sentTo) {
|
|||||||
|
|
||||||
describe("execute() calldata on the transaction detail screen", () => {
|
describe("execute() calldata on the transaction detail screen", () => {
|
||||||
test("sent to a router on the active network, it is a swap", async () => {
|
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(
|
expect(node("tx-detail-calldata-details").innerHTML).toContain(
|
||||||
"Swap via Uniswap Universal Router",
|
"Swap via Uniswap Universal Router",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sent to an ordinary address, it is not decoded", async () => {
|
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-action").textContent).toBe("execute");
|
||||||
expect(node("tx-detail-calldata-details").innerHTML).toBe("");
|
expect(node("tx-detail-calldata-details").innerHTML).toBe("");
|
||||||
expect(node("tx-detail-rawdata").innerHTML).toContain(SWAP_DATA);
|
expect(node("tx-detail-rawdata").innerHTML).toContain(SWAP_DATA);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sent to a router of the other network only, it is not decoded", async () => {
|
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-action").textContent).toBe("execute");
|
||||||
expect(node("tx-detail-calldata-details").innerHTML).toBe("");
|
expect(node("tx-detail-calldata-details").innerHTML).toBe("");
|
||||||
expect(node("tx-detail-rawdata").innerHTML).toContain(SWAP_DATA);
|
expect(node("tx-detail-rawdata").innerHTML).toContain(SWAP_DATA);
|
||||||
|
|||||||
Reference in New Issue
Block a user