Files
AutistMask/src/popup/views/txStatus.js
clawbot e53420f2e2
All checks were successful
check / check (push) Successful in 9s
feat: add Sepolia testnet support (#137)
## Summary

Adds Sepolia testnet support to AutistMask.

### Changes

- **New `src/shared/networks.js`** — centralized network definitions (mainnet + Sepolia) with chain IDs, default RPC/Blockscout endpoints, and block explorer URLs
- **State management** — `networkId` added to persisted state; defaults to mainnet for backward compatibility
- **Settings UI** — network selector dropdown lets users switch between Ethereum Mainnet and Sepolia Testnet
- **Dynamic explorer links** — all hardcoded `etherscan.io` URLs replaced with dynamic links from the current network config (`sepolia.etherscan.io` for Sepolia)
- **Background service** — `wallet_switchEthereumChain` now accepts both mainnet (0x1) and Sepolia (0xaa36a7); broadcasts `chainChanged` to connected dApps
- **Inpage provider** — fetches chain ID on init and updates dynamically via `chainChanged` events (no more hardcoded `0x1`)
- **Blockscout API** — uses `eth-sepolia.blockscout.com/api/v2` for Sepolia
- **Etherscan labels** — phishing/scam checks use the correct explorer per network
- **Price fetching** — skipped on testnets (testnet tokens have no real market value)
- **RPC validation** — checks against the selected network's chain ID, not hardcoded mainnet
- **ethers provider** — `getProvider()` uses the correct ethers `Network` for Sepolia

### API Endpoints Verified

| Service | Mainnet | Sepolia |
|---------|---------|--------|
| Etherscan | etherscan.io | sepolia.etherscan.io |
| Blockscout | eth.blockscout.com/api/v2 | eth-sepolia.blockscout.com/api/v2 |
| RPC | ethereum-rpc.publicnode.com | ethereum-sepolia-rpc.publicnode.com |
| CoinDesk (prices) |  | N/A (skipped on testnet) |

closes #110

Reviewed-on: #137

THIS WAS ONESHOTTED USING OPUS 4.  WTAF
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 20:11:22 +01:00

276 lines
8.9 KiB
JavaScript

// Post-broadcast transaction status views: wait, success, error.
const {
$,
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
} = require("./helpers");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { getProvider } = require("../../shared/balances");
const { log } = require("../../shared/log");
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
let ctx;
let elapsedTimer = null;
let pollTimer = null;
function clearTimers() {
if (elapsedTimer) {
clearInterval(elapsedTimer);
elapsedTimer = null;
}
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function toAddressHtml(address) {
const dot = addressDotHtml(address);
const link = `${currentNetwork().explorerUrl}/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(address, state.wallets);
if (title) {
return (
`<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>` +
`<div class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(address)}">${escapeHtml(address)}</div>` +
extLink
);
}
return `<div class="flex items-center">${dot}<span class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(address)}">${escapeHtml(address)}</span>${extLink}</div>`;
}
function txHashHtml(hash) {
const link = `${currentNetwork().explorerUrl}/tx/${hash}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return (
`<span class="underline decoration-dashed cursor-pointer break-all" data-copy="${escapeHtml(hash)}">${escapeHtml(hash)}</span>` +
extLink
);
}
function blockNumberHtml(blockNumber) {
const num = String(blockNumber);
const link = `${currentNetwork().explorerUrl}/block/${num}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return (
`<span class="underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(num)}">${escapeHtml(num)}</span>` +
extLink
);
}
function attachCopyHandlers(viewId) {
document
.getElementById(viewId)
.querySelectorAll("[data-copy]")
.forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}
function showWait(txInfo, txHash) {
clearTimers();
const symbol = txInfo.token === "ETH" ? "ETH" : txInfo.tokenSymbol || "?";
$("wait-tx-summary").textContent = txInfo.amount + " " + symbol;
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to);
$("wait-tx-hash").innerHTML = txHashHtml(txHash);
attachCopyHandlers("view-wait-tx");
const broadcastTime = Date.now();
$("wait-tx-status").textContent = "Waiting for confirmation... 0s";
elapsedTimer = setInterval(() => {
const elapsed = Math.floor((Date.now() - broadcastTime) / 1000);
$("wait-tx-status").textContent =
"Waiting for confirmation... " + elapsed + "s";
}, 1000);
const provider = getProvider(state.rpcUrl);
pollTimer = setInterval(async () => {
try {
const receipt = await provider.getTransactionReceipt(txHash);
if (receipt) {
showSuccess(txInfo, txHash, receipt.blockNumber);
}
} catch (e) {
log.errorf("poll receipt failed:", e.message);
}
const elapsed = Math.floor((Date.now() - broadcastTime) / 1000);
if (elapsed >= 60) {
showError(
txInfo,
txHash,
"Transaction was not confirmed within 60 seconds. It may still confirm later \u2014 check Etherscan.",
);
}
}, 10000);
showView("wait-tx");
}
function showSuccess(txInfo, txHash, blockNumber) {
clearTimers();
const symbol = txInfo.token === "ETH" ? "ETH" : txInfo.tokenSymbol || "?";
state.viewData = {
amount: txInfo.amount,
symbol: symbol,
to: txInfo.to,
hash: txHash,
blockNumber: blockNumber,
decoded: txInfo.decoded || null,
};
renderSuccess();
ctx.doRefreshAndRender();
}
function tokenLabel(address) {
const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
return t ? t.symbol : null;
}
function etherscanTokenLink(address) {
return `${currentNetwork().explorerUrl}/token/${address}`;
}
function decodedDetailsHtml(decoded) {
if (!decoded || !decoded.details) return "";
let html = `<div class="border border-border border-dashed p-2 mb-3">`;
if (decoded.name) {
html += `<div class="mb-2"><div class="text-xs text-muted mb-1">Action</div>`;
html += `<div class="font-bold">${escapeHtml(decoded.name)}</div></div>`;
}
if (decoded.description) {
html += `<div class="mb-2"><div class="text-xs text-muted mb-1">Description</div>`;
html += `<div>${escapeHtml(decoded.description)}</div></div>`;
}
for (const d of decoded.details) {
html += `<div class="mb-2">`;
html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`;
if (d.address) {
if (d.isToken) {
const sym = tokenLabel(d.address) || "Unknown token";
html += `<div class="font-bold">${escapeHtml(sym)}</div>`;
html += toAddressHtml(d.address);
} else {
html += toAddressHtml(d.address);
}
} else {
html += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
}
html += `</div>`;
}
html += `</div>`;
return html;
}
function renderSuccess() {
const d = state.viewData;
if (!d || !d.hash) return;
const hasDecoded = d.decoded && d.decoded.details;
// When decoded details are present, the Amount and To are already
// shown inside the decoded well — hide the top-level duplicates.
const summarySection = $("success-tx-summary").parentElement;
const toSection = $("success-tx-to").parentElement;
if (hasDecoded) {
summarySection.classList.add("hidden");
toSection.classList.add("hidden");
} else {
summarySection.classList.remove("hidden");
toSection.classList.remove("hidden");
$("success-tx-summary").textContent = d.amount + " " + d.symbol;
$("success-tx-to").innerHTML = toAddressHtml(d.to);
}
$("success-tx-block").innerHTML = blockNumberHtml(d.blockNumber);
$("success-tx-hash").innerHTML = txHashHtml(d.hash);
// Show decoded calldata details if present
const decodedEl = $("success-tx-decoded");
if (decodedEl && hasDecoded) {
decodedEl.innerHTML = decodedDetailsHtml(d.decoded);
decodedEl.classList.remove("hidden");
} else if (decodedEl) {
decodedEl.classList.add("hidden");
}
attachCopyHandlers("view-success-tx");
showView("success-tx");
}
function showError(txInfo, txHash, message) {
clearTimers();
const symbol = txInfo.token === "ETH" ? "ETH" : txInfo.tokenSymbol || "?";
state.viewData = {
amount: txInfo.amount,
symbol: symbol,
to: txInfo.to,
hash: txHash || null,
message: message,
};
renderError();
}
function renderError() {
const d = state.viewData;
if (!d || !d.message) return;
$("error-tx-summary").textContent = d.amount + " " + d.symbol;
$("error-tx-to").innerHTML = toAddressHtml(d.to);
$("error-tx-message").textContent = d.message;
if (d.hash) {
$("error-tx-hash").innerHTML = txHashHtml(d.hash);
$("error-tx-hash-section").classList.remove("hidden");
attachCopyHandlers("view-error-tx");
} else {
$("error-tx-hash-section").classList.add("hidden");
}
showView("error-tx");
}
function isApprovalPopup() {
return new URLSearchParams(window.location.search).has("approval");
}
function navigateBack() {
if (isApprovalPopup()) {
window.close();
return;
}
if (state.selectedToken) {
ctx.showAddressToken();
} else {
ctx.showAddressDetail();
}
}
function init(_ctx) {
ctx = _ctx;
$("btn-success-tx-done").addEventListener("click", navigateBack);
$("btn-error-tx-done").addEventListener("click", navigateBack);
}
module.exports = { init, showWait, showError, renderSuccess, renderError };