Decode ERC-20 calldata in transaction approval popup
All checks were successful
check / check (push) Successful in 15s
All checks were successful
check / check (push) Successful in 15s
The tx approval screen now decodes known ERC-20 function calls (approve, transfer) and shows them in plain language instead of raw hex. For the Uniswap approve example, the user now sees: Action: Token Approval Approve spending of your USDT Token: USDT (with full contract address + etherscan link) Spender: (full address + etherscan link) Amount: Unlimited Known tokens from the built-in list show their symbol. Unknown tokens show the contract address. Max uint256 approvals are labeled "Unlimited". The raw data is still shown below in a scrollable area for verification. Also labels the "To" field as "Contract" since dApp transactions are always contract calls, and shows the token symbol above the contract address when recognized.
This commit is contained in:
@@ -858,21 +858,39 @@
|
||||
<span id="approve-tx-hostname" class="font-bold"></span>
|
||||
wants to send a transaction.
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
|
||||
<!-- decoded action (shown when calldata is recognized) -->
|
||||
<div
|
||||
id="approve-tx-decoded"
|
||||
class="mb-3 border border-border border-dashed p-2 hidden"
|
||||
>
|
||||
<div class="text-xs text-muted mb-1">Action</div>
|
||||
<div
|
||||
id="approve-tx-action"
|
||||
class="text-xs font-bold mb-2"
|
||||
></div>
|
||||
<div id="approve-tx-decoded-details" class="text-xs"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">From</div>
|
||||
<div id="approve-tx-from" class="text-xs break-all"></div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Contract</div>
|
||||
<div id="approve-tx-to" class="text-xs break-all"></div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Value</div>
|
||||
<div id="approve-tx-value" class="text-xs font-bold"></div>
|
||||
</div>
|
||||
<div id="approve-tx-data-section" class="mb-2 hidden">
|
||||
<div class="text-xs text-muted mb-1">Data</div>
|
||||
<div id="approve-tx-data" class="text-xs break-all"></div>
|
||||
<div id="approve-tx-data-section" class="mb-3 hidden">
|
||||
<div class="text-xs text-muted mb-1">Raw data</div>
|
||||
<div
|
||||
id="approve-tx-data"
|
||||
class="text-xs break-all"
|
||||
style="max-height: 4rem; overflow-y: auto"
|
||||
></div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="block mb-1 text-xs">Password</label>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const { $, addressDotHtml, escapeHtml, showView } = require("./helpers");
|
||||
const { state, saveState } = require("../../shared/state");
|
||||
const { formatEther } = require("ethers");
|
||||
const { formatEther, formatUnits, Interface } = require("ethers");
|
||||
const { ERC20_ABI } = require("../../shared/constants");
|
||||
const { TOKENS } = require("../../shared/tokens");
|
||||
|
||||
const runtime =
|
||||
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||
@@ -12,6 +14,14 @@ const EXT_ICON =
|
||||
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
|
||||
`</svg></span>`;
|
||||
|
||||
const erc20Iface = new Interface(ERC20_ABI);
|
||||
|
||||
// Build address→token lookup from known token list
|
||||
const TOKEN_BY_ADDRESS = new Map();
|
||||
for (const t of TOKENS) {
|
||||
TOKEN_BY_ADDRESS.set(t.address.toLowerCase(), t);
|
||||
}
|
||||
|
||||
function approvalAddressHtml(address) {
|
||||
const dot = addressDotHtml(address);
|
||||
const link = `https://etherscan.io/address/${address}`;
|
||||
@@ -26,28 +36,161 @@ function formatTxValue(val) {
|
||||
return parts[0] + "." + dec;
|
||||
}
|
||||
|
||||
let approvalId = null;
|
||||
function tokenLabel(address) {
|
||||
const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
|
||||
return t ? t.symbol : null;
|
||||
}
|
||||
|
||||
function etherscanTokenLink(address) {
|
||||
return `https://etherscan.io/token/${address}`;
|
||||
}
|
||||
|
||||
// Try to decode calldata using the ERC-20 ABI.
|
||||
// Returns { name, description, details } or null.
|
||||
function decodeCalldata(data, toAddress) {
|
||||
if (!data || data === "0x" || data.length < 10) return null;
|
||||
|
||||
try {
|
||||
const parsed = erc20Iface.parseTransaction({ data });
|
||||
if (!parsed) return null;
|
||||
|
||||
const token = TOKEN_BY_ADDRESS.get(toAddress.toLowerCase());
|
||||
const tokenSymbol = token ? token.symbol : null;
|
||||
const tokenDecimals = token ? token.decimals : 18;
|
||||
const contractLabel = tokenSymbol
|
||||
? tokenSymbol + " (" + toAddress + ")"
|
||||
: toAddress;
|
||||
|
||||
if (parsed.name === "approve") {
|
||||
const spender = parsed.args[0];
|
||||
const rawAmount = parsed.args[1];
|
||||
const maxUint = BigInt(
|
||||
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
);
|
||||
const isUnlimited = rawAmount === maxUint;
|
||||
const amountStr = isUnlimited
|
||||
? "Unlimited"
|
||||
: formatTxValue(formatUnits(rawAmount, tokenDecimals)) +
|
||||
(tokenSymbol ? " " + tokenSymbol : "");
|
||||
|
||||
return {
|
||||
name: "Token Approval",
|
||||
description: tokenSymbol
|
||||
? "Approve spending of your " + tokenSymbol
|
||||
: "Approve spending of an ERC-20 token",
|
||||
details: [
|
||||
{
|
||||
label: "Token",
|
||||
value: contractLabel,
|
||||
address: toAddress,
|
||||
isToken: true,
|
||||
},
|
||||
{ label: "Spender", value: spender, address: spender },
|
||||
{ label: "Amount", value: amountStr },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (parsed.name === "transfer") {
|
||||
const to = parsed.args[0];
|
||||
const rawAmount = parsed.args[1];
|
||||
const amountStr =
|
||||
formatTxValue(formatUnits(rawAmount, tokenDecimals)) +
|
||||
(tokenSymbol ? " " + tokenSymbol : "");
|
||||
|
||||
return {
|
||||
name: "Token Transfer",
|
||||
description: tokenSymbol
|
||||
? "Transfer " + tokenSymbol
|
||||
: "Transfer ERC-20 token",
|
||||
details: [
|
||||
{
|
||||
label: "Token",
|
||||
value: contractLabel,
|
||||
address: toAddress,
|
||||
isToken: true,
|
||||
},
|
||||
{ label: "Recipient", value: to, address: to },
|
||||
{ label: "Amount", value: amountStr },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function showTxApproval(details) {
|
||||
$("approve-tx-hostname").textContent = details.hostname;
|
||||
$("approve-tx-from").innerHTML = approvalAddressHtml(state.activeAddress);
|
||||
const toAddr = details.txParams.to;
|
||||
$("approve-tx-to").innerHTML = toAddr
|
||||
? approvalAddressHtml(toAddr)
|
||||
: escapeHtml("(contract creation)");
|
||||
|
||||
// Show token symbol next to contract address if known
|
||||
const symbol = toAddr ? tokenLabel(toAddr) : null;
|
||||
if (toAddr) {
|
||||
let toHtml = "";
|
||||
if (symbol) {
|
||||
toHtml += `<div class="font-bold mb-1">${escapeHtml(symbol)}</div>`;
|
||||
}
|
||||
toHtml += approvalAddressHtml(toAddr);
|
||||
if (symbol) {
|
||||
const link = etherscanTokenLink(toAddr);
|
||||
toHtml = toHtml.replace("</div>", "") + ""; // approvalAddressHtml already has etherscan link
|
||||
}
|
||||
$("approve-tx-to").innerHTML = toHtml;
|
||||
} else {
|
||||
$("approve-tx-to").innerHTML = escapeHtml("(contract creation)");
|
||||
}
|
||||
|
||||
$("approve-tx-value").textContent =
|
||||
formatTxValue(formatEther(details.txParams.value || "0")) + " ETH";
|
||||
|
||||
// Decode calldata
|
||||
const decoded = decodeCalldata(details.txParams.data, toAddr || "");
|
||||
const decodedEl = $("approve-tx-decoded");
|
||||
if (decoded) {
|
||||
$("approve-tx-action").textContent = decoded.name;
|
||||
let detailsHtml = "";
|
||||
if (decoded.description) {
|
||||
detailsHtml += `<div class="mb-2">${escapeHtml(decoded.description)}</div>`;
|
||||
}
|
||||
for (const d of decoded.details) {
|
||||
detailsHtml += `<div class="mb-2">`;
|
||||
detailsHtml += `<div class="text-muted">${escapeHtml(d.label)}</div>`;
|
||||
if (d.address) {
|
||||
if (d.isToken) {
|
||||
const tLink = etherscanTokenLink(d.address);
|
||||
detailsHtml += `<div class="font-bold">${escapeHtml(tokenLabel(d.address) || "Unknown token")}</div>`;
|
||||
detailsHtml += approvalAddressHtml(d.address);
|
||||
} else {
|
||||
detailsHtml += approvalAddressHtml(d.address);
|
||||
}
|
||||
} else {
|
||||
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
|
||||
}
|
||||
detailsHtml += `</div>`;
|
||||
}
|
||||
$("approve-tx-decoded-details").innerHTML = detailsHtml;
|
||||
decodedEl.classList.remove("hidden");
|
||||
} else {
|
||||
decodedEl.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Always show raw data when present
|
||||
if (details.txParams.data && details.txParams.data !== "0x") {
|
||||
$("approve-tx-data").textContent = details.txParams.data;
|
||||
$("approve-tx-data-section").classList.remove("hidden");
|
||||
} else {
|
||||
$("approve-tx-data-section").classList.add("hidden");
|
||||
}
|
||||
|
||||
showView("approve-tx");
|
||||
}
|
||||
|
||||
function show(id) {
|
||||
approvalId = id;
|
||||
// Connect a port so the background detects if the popup closes
|
||||
// without an explicit response (e.g. user clicks away).
|
||||
runtime.connect({ name: "approval:" + id });
|
||||
runtime.sendMessage({ type: "AUTISTMASK_GET_APPROVAL", id }, (details) => {
|
||||
if (!details) {
|
||||
@@ -66,6 +209,8 @@ function show(id) {
|
||||
});
|
||||
}
|
||||
|
||||
let approvalId = null;
|
||||
|
||||
function init(ctx) {
|
||||
$("approve-remember").addEventListener("change", async () => {
|
||||
state.rememberSiteChoice = $("approve-remember").checked;
|
||||
|
||||
Reference in New Issue
Block a user