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",