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>
|
<span id="approve-tx-hostname" class="font-bold"></span>
|
||||||
wants to send a transaction.
|
wants to send a transaction.
|
||||||
</p>
|
</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 class="text-xs text-muted mb-1">From</div>
|
||||||
<div id="approve-tx-from" class="text-xs break-all"></div>
|
<div id="approve-tx-from" class="text-xs break-all"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-3">
|
||||||
<div class="text-xs text-muted mb-1">To</div>
|
<div class="text-xs text-muted mb-1">Contract</div>
|
||||||
<div id="approve-tx-to" class="text-xs break-all"></div>
|
<div id="approve-tx-to" class="text-xs break-all"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-3">
|
||||||
<div class="text-xs text-muted mb-1">Value</div>
|
<div class="text-xs text-muted mb-1">Value</div>
|
||||||
<div id="approve-tx-value" class="text-xs font-bold"></div>
|
<div id="approve-tx-value" class="text-xs font-bold"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="approve-tx-data-section" class="mb-2 hidden">
|
<div id="approve-tx-data-section" class="mb-3 hidden">
|
||||||
<div class="text-xs text-muted mb-1">Data</div>
|
<div class="text-xs text-muted mb-1">Raw data</div>
|
||||||
<div id="approve-tx-data" class="text-xs break-all"></div>
|
<div
|
||||||
|
id="approve-tx-data"
|
||||||
|
class="text-xs break-all"
|
||||||
|
style="max-height: 4rem; overflow-y: auto"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="block mb-1 text-xs">Password</label>
|
<label class="block mb-1 text-xs">Password</label>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const { $, addressDotHtml, escapeHtml, showView } = require("./helpers");
|
const { $, addressDotHtml, escapeHtml, showView } = require("./helpers");
|
||||||
const { state, saveState } = require("../../shared/state");
|
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 =
|
const runtime =
|
||||||
typeof browser !== "undefined" ? browser.runtime : chrome.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"/>` +
|
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
|
||||||
`</svg></span>`;
|
`</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) {
|
function approvalAddressHtml(address) {
|
||||||
const dot = addressDotHtml(address);
|
const dot = addressDotHtml(address);
|
||||||
const link = `https://etherscan.io/address/${address}`;
|
const link = `https://etherscan.io/address/${address}`;
|
||||||
@@ -26,28 +36,161 @@ function formatTxValue(val) {
|
|||||||
return parts[0] + "." + dec;
|
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) {
|
function showTxApproval(details) {
|
||||||
$("approve-tx-hostname").textContent = details.hostname;
|
$("approve-tx-hostname").textContent = details.hostname;
|
||||||
$("approve-tx-from").innerHTML = approvalAddressHtml(state.activeAddress);
|
$("approve-tx-from").innerHTML = approvalAddressHtml(state.activeAddress);
|
||||||
const toAddr = details.txParams.to;
|
const toAddr = details.txParams.to;
|
||||||
$("approve-tx-to").innerHTML = toAddr
|
|
||||||
? approvalAddressHtml(toAddr)
|
// Show token symbol next to contract address if known
|
||||||
: escapeHtml("(contract creation)");
|
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 =
|
$("approve-tx-value").textContent =
|
||||||
formatTxValue(formatEther(details.txParams.value || "0")) + " ETH";
|
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") {
|
if (details.txParams.data && details.txParams.data !== "0x") {
|
||||||
$("approve-tx-data").textContent = details.txParams.data;
|
$("approve-tx-data").textContent = details.txParams.data;
|
||||||
$("approve-tx-data-section").classList.remove("hidden");
|
$("approve-tx-data-section").classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
$("approve-tx-data-section").classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
showView("approve-tx");
|
showView("approve-tx");
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(id) {
|
function show(id) {
|
||||||
approvalId = 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.connect({ name: "approval:" + id });
|
||||||
runtime.sendMessage({ type: "AUTISTMASK_GET_APPROVAL", id }, (details) => {
|
runtime.sendMessage({ type: "AUTISTMASK_GET_APPROVAL", id }, (details) => {
|
||||||
if (!details) {
|
if (!details) {
|
||||||
@@ -66,6 +209,8 @@ function show(id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let approvalId = null;
|
||||||
|
|
||||||
function init(ctx) {
|
function init(ctx) {
|
||||||
$("approve-remember").addEventListener("change", async () => {
|
$("approve-remember").addEventListener("change", async () => {
|
||||||
state.rememberSiteChoice = $("approve-remember").checked;
|
state.rememberSiteChoice = $("approve-remember").checked;
|
||||||
|
|||||||
Reference in New Issue
Block a user