diff --git a/README.md b/README.md index 418bba9..7f6847b 100644 --- a/README.md +++ b/README.md @@ -2395,6 +2395,8 @@ view would leave a wallet one click from deletion. - Raw data: full calldata displayed inline (shown if present) - Password input and an error line - "Confirm" / "Reject" buttons +- **Behavior**: Every etherscan link on the screen opens the explorer of the + network the transaction was populated on, whichever network is active. - **Transitions**: - "Confirm" (correct password) → decrypts and signs the transaction it was shown, exactly as shown, hands the signed transaction to the background to diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js index 6911927..0344652 100644 --- a/src/popup/views/approval.js +++ b/src/popup/views/approval.js @@ -58,9 +58,15 @@ const { notify, runtimeApi, sendMessage } = require("../../shared/browserApi"); const erc20Iface = new Interface(ERC20_ABI); -function approvalAddressHtml(address) { +// The link opens the explorer of the network with `chainId`, or of the active +// network when none is given. A chain id that names no network has no +// explorer, so the address is drawn without a link. +function approvalAddressHtml(address, chainId) { const title = addressTitle(address, state.wallets); - return renderAddressHtml(address, { title }); + if (chainId && !networkByChainId(chainId)) { + return renderAddressHtml(address, { title, noLink: true }); + } + return renderAddressHtml(address, { title, chainId }); } // The amount line for a decoded ERC-20 call. With a known scale it is the @@ -338,8 +344,14 @@ function showTxApproval(details) { }; } + // Every address link on this screen opens the explorer of the network the + // transaction is for, which the Network line names, whichever network is + // active. $("approve-tx-origin").textContent = details.origin; - $("approve-tx-from").innerHTML = approvalAddressHtml(details.approvedFrom); + $("approve-tx-from").innerHTML = approvalAddressHtml( + details.approvedFrom, + approvedTx.chainId, + ); // Show token symbol next to contract address if known on the network the // transaction is for @@ -349,7 +361,7 @@ function showTxApproval(details) { if (symbol) { toHtml += `
${escapeHtml(symbol)}
`; } - toHtml += approvalAddressHtml(toAddr); + toHtml += approvalAddressHtml(toAddr, approvedTx.chainId); $("approve-tx-to").innerHTML = toHtml; } else { $("approve-tx-to").innerHTML = escapeHtml(CONTRACT_CREATION_TEXT); @@ -388,7 +400,10 @@ function showTxApproval(details) { if (d.isToken) { detailsHtml += `
${escapeHtml(tokenLabel(d.address, approvedTx.chainId) || "Unknown token")}
`; } - detailsHtml += approvalAddressHtml(d.address); + detailsHtml += approvalAddressHtml( + d.address, + approvedTx.chainId, + ); } else { detailsHtml += `
${escapeHtml(d.value)}
`; } diff --git a/tests/approvalTokenNetwork.test.js b/tests/approvalTokenNetwork.test.js index 20ed1dc..914433f 100644 --- a/tests/approvalTokenNetwork.test.js +++ b/tests/approvalTokenNetwork.test.js @@ -1,11 +1,16 @@ // The approval screen for a site's Sepolia token transfer while Ethereum -// Mainnet is active (https://git.eeqj.de/sneak/AutistMask/issues/629). +// Mainnet is active. // // The screen named the token, and scaled its amount, from the active network's // tracked tokens and balances, so a token the wallet tracks on Sepolia read as // an unknown token with its amount in base units, and the screens after it -// labelled the amount `?`. It now uses what the wallet knows of the tokens on -// the network the transaction is for. +// labelled the amount `?` (https://git.eeqj.de/sneak/AutistMask/issues/629). +// It now uses what the wallet knows of the tokens on the network the +// transaction is for. +// +// Its address links opened the active network's explorer +// (https://git.eeqj.de/sneak/AutistMask/issues/631). They now open the +// explorer of the network the transaction is for. // // Driven through the real approval screen, password and Confirm included, // against a minimal DOM stub in the shape tests/swapStatusAmount.test.js uses. @@ -107,10 +112,13 @@ function decodedLine(label) { } // Open the approval screen, with Ethereum Mainnet active and FROM holding -// `tokenBalances` there, for the site's Sepolia transaction sending -// TRANSFER_DATA to the token. Confirm answers the signed transaction with a -// failure, which leads to the error screen. -async function showSepoliaTransfer(tokenBalances = []) { +// `tokenBalances` there, for the site's transaction sending TRANSFER_DATA to +// the token, on Sepolia unless `chainId` names another chain. Confirm answers +// the signed transaction with a failure, which leads to the error screen. +async function showTokenTransfer( + tokenBalances = [], + chainId = NETWORKS.sepolia.chainId, +) { globalThis.document = makeDocument(); globalThis.window = { location: { search: "" }, close: () => {} }; globalThis.chrome.runtime = { @@ -131,7 +139,7 @@ async function showSepoliaTransfer(tokenBalances = []) { approvedTx: { type: 2, from: FROM, - chainId: NETWORKS.sepolia.chainId, + chainId, nonce: "0x7", gasLimit: "0x30d40", maxPriorityFeePerGas: "0x3b9aca00", @@ -183,7 +191,7 @@ describe("a token tracked on Sepolia only", () => { }); test("is named on the Contract line and the decoded Token line, with its amount at its scale", async () => { - await showSepoliaTransfer(); + await showTokenTransfer(); expect(node("approve-tx-to").innerHTML).toContain( '
TDOL
', ); @@ -196,7 +204,7 @@ describe("a token tracked on Sepolia only", () => { }); test("labels the amount on the error screen that follows", async () => { - await showSepoliaTransfer(); + await showTokenTransfer(); await confirm(); expect(state.currentView).toBe("error-tx"); expect(node("error-tx-summary").textContent).toBe("1234.5000 TDOL"); @@ -207,7 +215,7 @@ describe("a token tracked on Sepolia only", () => { // tracks and holds there names nothing on Sepolia. test("a token tracked and held on mainnet only is not named, and its amount is in base units", async () => { state.trackedTokens = [{ address: TOKEN, symbol: "MDOL", decimals: 18 }]; - await showSepoliaTransfer([ + await showTokenTransfer([ { address: TOKEN, symbol: "MDOL", decimals: 18, balance: "1.0" }, ]); expect(node("approve-tx-to").innerHTML).not.toContain("MDOL"); @@ -218,3 +226,45 @@ test("a token tracked and held on mainnet only is not named, and its amount is i '
1234500000 base units (decimals unknown)
', ); }); + +// The From and Contract lines, and the decoded Token and Recipient lines. +test("every address links to the Sepolia explorer", async () => { + await showTokenTransfer(); + const sepolia = NETWORKS.sepolia.explorerUrl; + expect(node("approve-tx-from").innerHTML).toContain( + `href="${sepolia}/address/${FROM}"`, + ); + expect(node("approve-tx-to").innerHTML).toContain( + `href="${sepolia}/address/${TOKEN}"`, + ); + expect(decodedLine("Token")).toContain( + `href="${sepolia}/address/${TOKEN}"`, + ); + expect(decodedLine("Recipient")).toContain( + `href="${sepolia}/address/${RECIPIENT}"`, + ); + for (const id of [ + "approve-tx-from", + "approve-tx-to", + "approve-tx-decoded-details", + ]) { + expect(node(id).innerHTML).not.toContain(NETWORKS.mainnet.explorerUrl); + } +}); + +// Chain id 1337 names no network here, so it has no explorer to link to. +test("a transaction on a chain id that names no network is shown with no address links", async () => { + await showTokenTransfer([], "0x539"); + expect(state.currentView).toBe("approve-tx"); + expect(node("approve-tx-network").textContent).toBe( + "Unknown network (chain id 1337)", + ); + expect(node("approve-tx-from").innerHTML).toContain(FROM); + for (const id of [ + "approve-tx-from", + "approve-tx-to", + "approve-tx-decoded-details", + ]) { + expect(node(id).innerHTML).not.toContain("href="); + } +}); diff --git a/tests/walletDefects.test.js b/tests/walletDefects.test.js index d5e9ea5..fe1c389 100644 --- a/tests/walletDefects.test.js +++ b/tests/walletDefects.test.js @@ -475,7 +475,7 @@ describe("every way to a signature or the private key refuses a defective wallet to: RECIPIENT, value: "0x0", data: "0x", - chainId: 1, + chainId: "0x1", nonce: 0, gasLimit: "21000", maxFeePerGas: "1000000000",