All checks were successful
check / check (push) Successful in 32s
The signed artifact was compared with the dApp's request object, so every field the dApp left out — normally the nonce, the gas limit and every fee field, because populateTransaction() filled them in the popup — was checked by nothing but the absolute ceilings. A bare transfer at the fee ceiling hands the validator 2.1 ETH. The ceilings were never the defect: the thing being verified was not the thing the user approved. The transaction is now populated in the background, before the approval window opens, and that populated object is what is displayed, what the popup signs, and what the artifact is verified against. Every consequential field is compared exactly. - src/shared/approvalTx.js populates the request through a VoidSigner over the configured RPC and serializes the result to the fields its type serializes, as hex quantities that survive the JSON messaging boundary. Fields the wallet does not act on are dropped before ethers sees the page's object. - Population failure raises no approval and opens no window: the error goes back to the requesting page, bounded by a 20-second timeout. A half-initialised approval record would be exactly the state the settle interlock exists to keep out of that record, and the same estimate previously failed after the user had typed their password. - verifySignedTx compares the artifact field by field over SERIALIZED_FIELDS[type], plus the type itself. A quantity the approval does not fix is a refusal rather than a skipped comparison. The ceilings stay as a documented backstop and now also apply at population, where they bound what an RPC node can talk the wallet into displaying. - The approval pins the address it was raised for. Verification uses that address, not getActiveAddress(), and an address switch between approval and signing refuses rather than signing from an account the screen never named — including a switch during population, and on the message-signing path. A request naming an address that is not the active one is refused outright. - The approval screen shows the network, gas limit, fee per gas, maximum fee and nonce it now vouches for, and the popup signs the object it was given with no provider and no population of its own. The settle chokepoint is untouched: one delete of pendingApprovals and one approval.resolve(), both inside settleApproval(), the claim taken synchronously before the first await, and a refused settle still leaving the approval window standing.
796 lines
28 KiB
JavaScript
796 lines
28 KiB
JavaScript
const {
|
|
$,
|
|
addressTitle,
|
|
escapeHtml,
|
|
showView,
|
|
showError,
|
|
hideError,
|
|
renderAddressHtml,
|
|
attachCopyHandlers,
|
|
onViewLeave,
|
|
} = require("./helpers");
|
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
|
const { networkByChainId } = require("../../shared/networks");
|
|
const {
|
|
formatEther,
|
|
formatUnits,
|
|
getBytes,
|
|
Interface,
|
|
toUtf8String,
|
|
} = require("ethers");
|
|
const { getPrice, formatUsd } = require("../../shared/prices");
|
|
const { ERC20_ABI } = require("../../shared/constants");
|
|
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
|
|
const { decryptWithPassword } = require("../../shared/vault");
|
|
const { getSignerForAddress } = require("../../shared/wallet");
|
|
const { walletDefect } = require("../../shared/walletDefects");
|
|
const { describeSigningFailure } = require("../../shared/approvalVerify");
|
|
const txStatus = require("./txStatus");
|
|
const uniswap = require("../../shared/uniswap");
|
|
const runtime =
|
|
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
|
|
|
const erc20Iface = new Interface(ERC20_ABI);
|
|
|
|
function approvalAddressHtml(address) {
|
|
const title = addressTitle(address, state.wallets);
|
|
return renderAddressHtml(address, { title });
|
|
}
|
|
|
|
function formatTxValue(val) {
|
|
const parts = val.split(".");
|
|
if (parts.length === 1) return val + ".0000";
|
|
const dec = (parts[1] + "0000").slice(0, 4);
|
|
return parts[0] + "." + dec;
|
|
}
|
|
|
|
function tokenLabel(address) {
|
|
const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
|
|
return t ? t.symbol : null;
|
|
}
|
|
|
|
// Try to decode calldata using known ABIs.
|
|
// Returns { name, description, details } or null.
|
|
function decodeCalldata(data, toAddress) {
|
|
if (!data || data === "0x" || data.length < 10) return null;
|
|
|
|
// Try ERC-20 (approve / transfer)
|
|
try {
|
|
const parsed = erc20Iface.parseTransaction({ data });
|
|
if (parsed) {
|
|
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 amountRaw = isUnlimited
|
|
? "Unlimited"
|
|
: formatTxValue(formatUnits(rawAmount, tokenDecimals));
|
|
const amountStr = isUnlimited
|
|
? "Unlimited"
|
|
: amountRaw + (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,
|
|
rawValue: amountRaw,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (parsed.name === "transfer") {
|
|
const to = parsed.args[0];
|
|
const rawAmount = parsed.args[1];
|
|
const amountRaw = formatTxValue(
|
|
formatUnits(rawAmount, tokenDecimals),
|
|
);
|
|
const amountStr =
|
|
amountRaw + (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,
|
|
rawValue: amountRaw,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
}
|
|
} catch {
|
|
// Not ERC-20 — fall through
|
|
}
|
|
|
|
// Try Uniswap Universal Router
|
|
const routerResult = uniswap.decode(data, toAddress);
|
|
if (routerResult) return routerResult;
|
|
|
|
return null;
|
|
}
|
|
|
|
function showPhishingWarning(elementId, isPhishing) {
|
|
const el = $(elementId);
|
|
if (!el) return;
|
|
// The background script performs the authoritative phishing domain check
|
|
// and passes the result via the isPhishingDomain flag.
|
|
if (isPhishing) {
|
|
el.classList.remove("hidden");
|
|
} else {
|
|
el.classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
// The fields of the approved transaction the value and recipient lines do not
|
|
// already carry: network, gas limit, fee per gas, the most the fee can come to,
|
|
// and the nonce. The background compares every one of them against the signed
|
|
// artifact, so every one of them has to be on the screen — a number that is
|
|
// verified but never displayed is verified against nothing the user agreed to.
|
|
function showTxFee(approvedTx, ethPrice) {
|
|
const network = networkByChainId(approvedTx.chainId);
|
|
$("approve-tx-network").textContent = network
|
|
? network.name
|
|
: "Unknown network (chain id " + BigInt(approvedTx.chainId) + ")";
|
|
|
|
const gasLimit = BigInt(approvedTx.gasLimit);
|
|
const feePerGas = BigInt(approvedTx.maxFeePerGas || approvedTx.gasPrice);
|
|
const maxFeeEth = formatTxValue(formatEther(gasLimit * feePerGas));
|
|
const usdStr = formatUsd(
|
|
ethPrice ? parseFloat(maxFeeEth) * ethPrice : null,
|
|
);
|
|
$("approve-tx-fee").textContent =
|
|
maxFeeEth + " ETH" + (usdStr ? " (" + usdStr + ")" : "");
|
|
|
|
let detail =
|
|
gasLimit.toString() +
|
|
" gas at up to " +
|
|
formatUnits(feePerGas, 9) +
|
|
" gwei";
|
|
if (approvedTx.maxPriorityFeePerGas) {
|
|
detail +=
|
|
", " +
|
|
formatUnits(approvedTx.maxPriorityFeePerGas, 9) +
|
|
" gwei priority";
|
|
}
|
|
$("approve-tx-fee-detail").textContent = detail;
|
|
$("approve-tx-nonce").textContent = BigInt(approvedTx.nonce).toString();
|
|
}
|
|
|
|
function showTxApproval(details) {
|
|
showPhishingWarning(
|
|
"approve-tx-phishing-warning",
|
|
details.isPhishingDomain,
|
|
);
|
|
|
|
// The transaction the background populated. It is displayed as it stands,
|
|
// signed as it stands, and verified against as it stands — the popup fills
|
|
// nothing in, so there is no number on this screen that the background
|
|
// cannot compare with the artifact it gets back.
|
|
pendingTxParams = details.approvedTx;
|
|
const approvedTx = details.approvedTx;
|
|
|
|
const toAddr = approvedTx.to;
|
|
const token = toAddr ? TOKEN_BY_ADDRESS.get(toAddr.toLowerCase()) : null;
|
|
const ethValue = formatEther(approvedTx.value || "0");
|
|
|
|
// Build txInfo for status screens
|
|
pendingTxDetails = {
|
|
from: details.approvedFrom,
|
|
to: toAddr || "",
|
|
amount: formatTxValue(ethValue),
|
|
token: "ETH",
|
|
tokenSymbol: token ? token.symbol : null,
|
|
};
|
|
|
|
// If this is an ERC-20 call, try to extract the real recipient and amount
|
|
const decoded = decodeCalldata(approvedTx.data, toAddr || "");
|
|
if (decoded && decoded.details) {
|
|
let decodedTokenAddr = null;
|
|
let decodedTokenSymbol = null;
|
|
for (const d of decoded.details) {
|
|
if (d.label === "Recipient" && d.address) {
|
|
pendingTxDetails.to = d.address;
|
|
}
|
|
if (d.label === "Amount") {
|
|
pendingTxDetails.amount = d.rawValue || d.value;
|
|
}
|
|
if (d.label === "Token In" && d.isToken && d.address) {
|
|
const t = TOKEN_BY_ADDRESS.get(d.address.toLowerCase());
|
|
if (t) {
|
|
decodedTokenAddr = d.address;
|
|
decodedTokenSymbol = t.symbol;
|
|
}
|
|
}
|
|
}
|
|
if (token) {
|
|
pendingTxDetails.token = toAddr;
|
|
pendingTxDetails.tokenSymbol = token.symbol;
|
|
} else if (decodedTokenAddr) {
|
|
pendingTxDetails.token = decodedTokenAddr;
|
|
pendingTxDetails.tokenSymbol = decodedTokenSymbol;
|
|
}
|
|
}
|
|
|
|
// Carry decoded calldata info through to success/error views
|
|
if (decoded) {
|
|
pendingTxDetails.decoded = {
|
|
name: decoded.name,
|
|
description: decoded.description,
|
|
details: decoded.details,
|
|
};
|
|
}
|
|
|
|
$("approve-tx-hostname").textContent = details.hostname;
|
|
$("approve-tx-from").innerHTML = approvalAddressHtml(details.approvedFrom);
|
|
|
|
// 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);
|
|
$("approve-tx-to").innerHTML = toHtml;
|
|
} else {
|
|
$("approve-tx-to").innerHTML = escapeHtml("(contract creation)");
|
|
}
|
|
|
|
const ethValueFormatted = formatTxValue(
|
|
formatEther(approvedTx.value || "0"),
|
|
);
|
|
const ethPrice = getPrice("ETH");
|
|
const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null;
|
|
const usdStr = formatUsd(ethUsd);
|
|
$("approve-tx-value").textContent =
|
|
ethValueFormatted + " ETH" + (usdStr ? " (" + usdStr + ")" : "");
|
|
|
|
showTxFee(approvedTx, ethPrice);
|
|
|
|
// Decode calldata (reuse decoded from above)
|
|
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) {
|
|
detailsHtml += `<div class="font-bold">${escapeHtml(tokenLabel(d.address) || "Unknown token")}</div>`;
|
|
}
|
|
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 (approvedTx.data && approvedTx.data !== "0x") {
|
|
$("approve-tx-data").textContent = approvedTx.data;
|
|
$("approve-tx-data-section").classList.remove("hidden");
|
|
} else {
|
|
$("approve-tx-data-section").classList.add("hidden");
|
|
}
|
|
|
|
$("approve-tx-password").value = "";
|
|
hideError("approve-tx-error");
|
|
|
|
showView("approve-tx");
|
|
attachCopyHandlers("view-approve-tx");
|
|
gateOnWalletDefect(
|
|
"approve-tx-error",
|
|
"btn-approve-tx",
|
|
details.approvedFrom,
|
|
);
|
|
}
|
|
|
|
function decodeHexMessage(hex) {
|
|
try {
|
|
const bytes = Uint8Array.from(
|
|
hex
|
|
.slice(2)
|
|
.match(/.{1,2}/g)
|
|
.map((b) => parseInt(b, 16)),
|
|
);
|
|
return toUtf8String(bytes);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function formatTypedDataHtml(jsonStr) {
|
|
try {
|
|
const data = JSON.parse(jsonStr);
|
|
let html = "";
|
|
|
|
if (data.domain) {
|
|
html += `<div class="mb-2"><div class="text-muted">Domain</div>`;
|
|
for (const [key, val] of Object.entries(data.domain)) {
|
|
html += `<div><span class="text-muted">${escapeHtml(key)}:</span> ${escapeHtml(String(val))}</div>`;
|
|
}
|
|
html += `</div>`;
|
|
}
|
|
|
|
if (data.primaryType) {
|
|
html += `<div class="mb-2"><div class="text-muted">Primary type</div>`;
|
|
html += `<div class="font-bold">${escapeHtml(data.primaryType)}</div></div>`;
|
|
}
|
|
|
|
if (data.message) {
|
|
html += `<div class="mb-2"><div class="text-muted">Message</div>`;
|
|
for (const [key, val] of Object.entries(data.message)) {
|
|
const display =
|
|
typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
html += `<div><span class="text-muted">${escapeHtml(key)}:</span> <span class="break-all">${escapeHtml(display)}</span></div>`;
|
|
}
|
|
html += `</div>`;
|
|
}
|
|
|
|
return html;
|
|
} catch {
|
|
return `<div class="break-all">${escapeHtml(jsonStr)}</div>`;
|
|
}
|
|
}
|
|
|
|
function showSignApproval(details) {
|
|
showPhishingWarning(
|
|
"approve-sign-phishing-warning",
|
|
details.isPhishingDomain,
|
|
);
|
|
|
|
const sp = details.signParams;
|
|
pendingSignParams = sp;
|
|
pendingSignFrom = details.approvedFrom;
|
|
|
|
$("approve-sign-hostname").textContent = details.hostname;
|
|
$("approve-sign-from").innerHTML = approvalAddressHtml(
|
|
details.approvedFrom,
|
|
);
|
|
|
|
const isTyped =
|
|
sp.method === "eth_signTypedData_v4" ||
|
|
sp.method === "eth_signTypedData";
|
|
$("approve-sign-type").textContent = isTyped
|
|
? "Typed data (EIP-712)"
|
|
: "Personal message";
|
|
|
|
if (isTyped) {
|
|
$("approve-sign-message").innerHTML = formatTypedDataHtml(sp.typedData);
|
|
} else {
|
|
const decoded = decodeHexMessage(sp.message);
|
|
if (decoded !== null) {
|
|
$("approve-sign-message").textContent = decoded;
|
|
} else {
|
|
$("approve-sign-message").textContent = sp.message;
|
|
}
|
|
}
|
|
|
|
// Display danger warning for eth_sign (raw hash signing)
|
|
const warningEl = $("approve-sign-danger-warning");
|
|
if (warningEl) {
|
|
if (sp.dangerWarning) {
|
|
warningEl.textContent = sp.dangerWarning;
|
|
warningEl.style.visibility = "visible";
|
|
} else {
|
|
warningEl.textContent = "";
|
|
warningEl.style.visibility = "hidden";
|
|
}
|
|
}
|
|
|
|
$("approve-sign-password").value = "";
|
|
hideError("approve-sign-error");
|
|
$("btn-approve-sign").disabled = false;
|
|
$("btn-approve-sign").classList.remove("text-muted");
|
|
|
|
showView("approve-sign");
|
|
attachCopyHandlers("view-approve-sign");
|
|
gateOnWalletDefect(
|
|
"approve-sign-error",
|
|
"btn-approve-sign",
|
|
details.approvedFrom,
|
|
);
|
|
}
|
|
|
|
function show(id) {
|
|
approvalId = id;
|
|
runtime.connect({ name: "approval:" + id });
|
|
runtime.sendMessage({ type: "AUTISTMASK_GET_APPROVAL", id }, (details) => {
|
|
if (!details) {
|
|
window.close();
|
|
return;
|
|
}
|
|
if (details.type === "tx") {
|
|
showTxApproval(details);
|
|
return;
|
|
}
|
|
if (details.type === "sign") {
|
|
showSignApproval(details);
|
|
return;
|
|
}
|
|
// Site connection approval
|
|
showPhishingWarning(
|
|
"approve-site-phishing-warning",
|
|
details.isPhishingDomain,
|
|
);
|
|
$("approve-hostname").textContent = details.hostname;
|
|
$("approve-address").innerHTML = approvalAddressHtml(
|
|
state.activeAddress,
|
|
);
|
|
attachCopyHandlers("view-approve-site");
|
|
$("approve-remember").checked = state.rememberSiteChoice;
|
|
});
|
|
}
|
|
|
|
let approvalId = null;
|
|
let pendingTxDetails = null;
|
|
// The exact objects shown to the user, kept so the popup signs what it
|
|
// displayed rather than re-fetching or re-populating anything at approval
|
|
// time. All are repopulated by show() when the popup is closed and reopened.
|
|
let pendingTxParams = null;
|
|
let pendingSignParams = null;
|
|
// The address the approval was raised for. Signing uses this rather than the
|
|
// active address, so that an address switch since the approval fails here
|
|
// instead of producing a signature from an account the screen never named.
|
|
let pendingSignFrom = null;
|
|
|
|
// Approve buttons stay disabled and muted while the popup derives the key and
|
|
// signs, which is slow enough (Argon2id) that a double click is likely.
|
|
function setTxButtonBusy(busy) {
|
|
$("btn-approve-tx").disabled = busy;
|
|
$("btn-approve-tx").classList.toggle("text-muted", busy);
|
|
}
|
|
|
|
function setSignButtonBusy(busy) {
|
|
$("btn-approve-sign").disabled = busy;
|
|
$("btn-approve-sign").classList.toggle("text-muted", busy);
|
|
}
|
|
|
|
// Say so on the approval screen itself, and disable the approve button, when
|
|
// the address the approval was raised for belongs to a wallet whose keys
|
|
// cannot be derived. Without this the screen would take a password and fail
|
|
// after deriving it. Reject stays available; the wallet is not touched.
|
|
// Returns true when it gated.
|
|
function gateOnWalletDefect(errorId, buttonId, address) {
|
|
const owner = findWalletFor(address);
|
|
const defect = owner ? walletDefect(owner.wallet) : null;
|
|
if (!defect) return false;
|
|
showError(errorId, defect.shortMessage);
|
|
$(buttonId).disabled = true;
|
|
$(buttonId).classList.add("text-muted");
|
|
return true;
|
|
}
|
|
|
|
// Locate the wallet and the address index owning an address. Returns null when
|
|
// no wallet holds it. Approvals look up the address they were raised for, not
|
|
// whichever address is active now: the approval named one account, and signing
|
|
// with another is what verification refuses.
|
|
function findWalletFor(address) {
|
|
for (const wallet of state.wallets) {
|
|
for (let i = 0; i < wallet.addresses.length; i++) {
|
|
if (wallet.addresses[i].address === address) {
|
|
return { wallet, addrIndex: i };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Drop the password from the DOM when either approval screen is left. The
|
|
// approval window navigates on after a signature — approve-tx goes to the
|
|
// wait screen — and the password must not sit in the hidden view for the
|
|
// life of that window.
|
|
function clearTxPassword() {
|
|
$("approve-tx-password").value = "";
|
|
hideError("approve-tx-error");
|
|
}
|
|
|
|
function clearSignPassword() {
|
|
$("approve-sign-password").value = "";
|
|
hideError("approve-sign-error");
|
|
}
|
|
|
|
function init(ctx) {
|
|
onViewLeave("approve-tx", clearTxPassword);
|
|
onViewLeave("approve-sign", clearSignPassword);
|
|
|
|
$("approve-remember").addEventListener("change", async () => {
|
|
state.rememberSiteChoice = $("approve-remember").checked;
|
|
await saveState();
|
|
});
|
|
|
|
$("btn-approve").addEventListener("click", () => {
|
|
const remember = $("approve-remember").checked;
|
|
runtime.sendMessage({
|
|
type: "AUTISTMASK_APPROVAL_RESPONSE",
|
|
id: approvalId,
|
|
approved: true,
|
|
remember,
|
|
});
|
|
window.close();
|
|
});
|
|
|
|
$("btn-reject").addEventListener("click", () => {
|
|
const remember = $("approve-remember").checked;
|
|
runtime.sendMessage({
|
|
type: "AUTISTMASK_APPROVAL_RESPONSE",
|
|
id: approvalId,
|
|
approved: false,
|
|
remember,
|
|
});
|
|
window.close();
|
|
});
|
|
|
|
$("btn-approve-tx").addEventListener("click", async () => {
|
|
let password = $("approve-tx-password").value;
|
|
if (!password) {
|
|
showError("approve-tx-error", "Please enter your password.");
|
|
return;
|
|
}
|
|
hideError("approve-tx-error");
|
|
setTxButtonBusy(true);
|
|
|
|
const active = findWalletFor(pendingTxParams.from);
|
|
if (!active) {
|
|
password = null;
|
|
showError(
|
|
"approve-tx-error",
|
|
"No wallet was found for the address this transaction was approved for.",
|
|
);
|
|
setTxButtonBusy(false);
|
|
return;
|
|
}
|
|
|
|
const defect = walletDefect(active.wallet);
|
|
if (defect) {
|
|
password = null;
|
|
showError("approve-tx-error", defect.shortMessage);
|
|
setTxButtonBusy(false);
|
|
return;
|
|
}
|
|
|
|
// Decrypt here, in the popup. The password must never cross the
|
|
// extension messaging boundary; only the signed transaction does.
|
|
let decryptedSecret;
|
|
try {
|
|
decryptedSecret = await decryptWithPassword(
|
|
active.wallet.encryptedSecret,
|
|
password,
|
|
);
|
|
} catch {
|
|
showError(
|
|
"approve-tx-error",
|
|
"That password is incorrect. Please try again.",
|
|
);
|
|
setTxButtonBusy(false);
|
|
return;
|
|
} finally {
|
|
// Best-effort: drop the password as soon as the key derivation
|
|
// is done. Note that JS strings are immutable; this clears the
|
|
// reference but the original string may persist until GC.
|
|
password = null;
|
|
}
|
|
|
|
const payload = {
|
|
type: "AUTISTMASK_TX_RESPONSE",
|
|
id: approvalId,
|
|
approved: true,
|
|
};
|
|
try {
|
|
const signer = getSignerForAddress(
|
|
active.wallet,
|
|
active.addrIndex,
|
|
decryptedSecret,
|
|
);
|
|
// Sign the approved transaction exactly as it was displayed. The
|
|
// background populated it before this screen was drawn and checks
|
|
// the artifact against it field for field, so there is nothing to
|
|
// fill in here and no provider to fill it in from. The copy is
|
|
// because ethers may strip `from` off what it is handed, and the
|
|
// approval has to survive a retry intact; keeping `from` on it
|
|
// makes ethers refuse a key that is not the approved address.
|
|
payload.rawSignedTx = await signer.signTransaction({
|
|
...pendingTxParams,
|
|
});
|
|
} catch (e) {
|
|
payload.error =
|
|
e.shortMessage || e.message || "Transaction signing failed.";
|
|
} finally {
|
|
// Best-effort: clear the decrypted secret after use, with the
|
|
// same immutability caveat as the password above.
|
|
decryptedSecret = null;
|
|
}
|
|
|
|
runtime.sendMessage(payload, (response) => {
|
|
if (response && response.txHash) {
|
|
txStatus.showWait(pendingTxDetails, response.txHash);
|
|
return;
|
|
}
|
|
// A retryable failure leaves the approval pending in the
|
|
// background, so stay on this screen with a live button rather
|
|
// than sending the user to a dead end.
|
|
const outcome = describeSigningFailure(
|
|
response,
|
|
"The transaction could not be sent.",
|
|
);
|
|
if (outcome.retryable) {
|
|
showError("approve-tx-error", outcome.message);
|
|
setTxButtonBusy(false);
|
|
} else {
|
|
txStatus.showError(pendingTxDetails, null, outcome.message);
|
|
}
|
|
});
|
|
});
|
|
|
|
$("btn-reject-tx").addEventListener("click", () => {
|
|
runtime.sendMessage({
|
|
type: "AUTISTMASK_TX_RESPONSE",
|
|
id: approvalId,
|
|
approved: false,
|
|
});
|
|
window.close();
|
|
});
|
|
|
|
$("btn-approve-sign").addEventListener("click", async () => {
|
|
let password = $("approve-sign-password").value;
|
|
if (!password) {
|
|
showError("approve-sign-error", "Please enter your password.");
|
|
return;
|
|
}
|
|
hideError("approve-sign-error");
|
|
setSignButtonBusy(true);
|
|
|
|
const active = findWalletFor(pendingSignFrom);
|
|
if (!active) {
|
|
password = null;
|
|
showError(
|
|
"approve-sign-error",
|
|
"No wallet was found for the address this request was approved for.",
|
|
);
|
|
setSignButtonBusy(false);
|
|
return;
|
|
}
|
|
|
|
const defect = walletDefect(active.wallet);
|
|
if (defect) {
|
|
password = null;
|
|
showError("approve-sign-error", defect.shortMessage);
|
|
setSignButtonBusy(false);
|
|
return;
|
|
}
|
|
|
|
// Decrypt here, in the popup. The password must never cross the
|
|
// extension messaging boundary; only the signature does.
|
|
let decryptedSecret;
|
|
try {
|
|
decryptedSecret = await decryptWithPassword(
|
|
active.wallet.encryptedSecret,
|
|
password,
|
|
);
|
|
} catch {
|
|
showError(
|
|
"approve-sign-error",
|
|
"That password is incorrect. Please try again.",
|
|
);
|
|
setSignButtonBusy(false);
|
|
return;
|
|
} finally {
|
|
// Best-effort: drop the password as soon as the key derivation
|
|
// is done. Note that JS strings are immutable; this clears the
|
|
// reference but the original string may persist until GC.
|
|
password = null;
|
|
}
|
|
|
|
const payload = {
|
|
type: "AUTISTMASK_SIGN_RESPONSE",
|
|
id: approvalId,
|
|
approved: true,
|
|
};
|
|
try {
|
|
const signer = getSignerForAddress(
|
|
active.wallet,
|
|
active.addrIndex,
|
|
decryptedSecret,
|
|
);
|
|
const sp = pendingSignParams;
|
|
if (sp.method === "personal_sign" || sp.method === "eth_sign") {
|
|
payload.signature = await signer.signMessage(
|
|
getBytes(sp.message),
|
|
);
|
|
} else {
|
|
// eth_signTypedData_v4 / eth_signTypedData
|
|
const typedData = JSON.parse(sp.typedData);
|
|
const { domain, types, message } = typedData;
|
|
// ethers handles EIP712Domain internally
|
|
delete types.EIP712Domain;
|
|
payload.signature = await signer.signTypedData(
|
|
domain,
|
|
types,
|
|
message,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
payload.error = e.shortMessage || e.message || "Signing failed.";
|
|
} finally {
|
|
// Best-effort: clear the decrypted secret after use, with the
|
|
// same immutability caveat as the password above.
|
|
decryptedSecret = null;
|
|
}
|
|
|
|
runtime.sendMessage(payload, (response) => {
|
|
if (response && response.signature) {
|
|
window.close();
|
|
return;
|
|
}
|
|
// The button comes back only when the approval is still pending in
|
|
// the background; otherwise it stays disabled and the message says
|
|
// why, because a control that cannot succeed must not look like it
|
|
// can.
|
|
const outcome = describeSigningFailure(
|
|
response,
|
|
"The message could not be signed.",
|
|
);
|
|
showError("approve-sign-error", outcome.message);
|
|
if (outcome.retryable) setSignButtonBusy(false);
|
|
});
|
|
});
|
|
|
|
$("btn-reject-sign").addEventListener("click", () => {
|
|
runtime.sendMessage({
|
|
type: "AUTISTMASK_SIGN_RESPONSE",
|
|
id: approvalId,
|
|
approved: false,
|
|
});
|
|
window.close();
|
|
});
|
|
}
|
|
|
|
module.exports = { init, show, decodeCalldata };
|