831 lines
29 KiB
JavaScript
831 lines
29 KiB
JavaScript
const {
|
|
$,
|
|
addressTitle,
|
|
escapeHtml,
|
|
showView,
|
|
showError,
|
|
hideError,
|
|
renderAddressHtml,
|
|
attachCopyHandlers,
|
|
onViewLeave,
|
|
} = require("./helpers");
|
|
const { state, saveState } = 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 { notify, runtimeApi, sendMessage } = require("../../shared/browserApi");
|
|
|
|
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,
|
|
);
|
|
}
|
|
|
|
// Awaited by nobody: the popup entry point calls this and moves on. It
|
|
// therefore has to absorb its own failure, and a background that cannot
|
|
// describe the approval is the same outcome as an approval that is gone.
|
|
async function show(id) {
|
|
approvalId = id;
|
|
approvalPort = runtimeApi().connect({ name: "approval:" + id });
|
|
|
|
let details = null;
|
|
try {
|
|
details = await sendMessage({ type: "AUTISTMASK_GET_APPROVAL", id });
|
|
} catch {
|
|
details = null;
|
|
}
|
|
|
|
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;
|
|
// The port this approval was opened on. Closing this window disconnects it,
|
|
// and the background treats that disconnect as "closed without deciding" for a
|
|
// site connection — so the decision goes out on this same port and not as a
|
|
// one-off message. One channel is ordered: a message posted on it is delivered
|
|
// before its own disconnect, however immediately the close follows. Two
|
|
// channels were not, and the close won, reporting a user who approved as
|
|
// having refused.
|
|
let approvalPort = 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");
|
|
}
|
|
|
|
// Answer a site-connection approval and close. The decision goes out on the
|
|
// approval port — see approvalPort above for why — and carries no approval id,
|
|
// because the port name already names the approval the background will settle.
|
|
// The post is guarded because a throw must not cost the close: posting on a
|
|
// port whose background worker has been torn down throws, and the approval it
|
|
// would have settled died with that worker, so the only thing left to do is
|
|
// what the user asked for — go away.
|
|
function decideSite(approved) {
|
|
if (approvalPort) {
|
|
try {
|
|
approvalPort.postMessage({
|
|
type: "AUTISTMASK_APPROVAL_DECISION",
|
|
approved,
|
|
remember: $("approve-remember").checked,
|
|
});
|
|
} catch {
|
|
// Nothing to report it to; the window closes either way.
|
|
}
|
|
}
|
|
window.close();
|
|
}
|
|
|
|
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", () => {
|
|
decideSite(true);
|
|
});
|
|
|
|
$("btn-reject").addEventListener("click", () => {
|
|
decideSite(false);
|
|
});
|
|
|
|
$("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;
|
|
}
|
|
|
|
// A send that never reaches the background is reported to the user
|
|
// the same way a background that refused it is: describeSigningFailure
|
|
// turns a null response into the generic message below.
|
|
let response = null;
|
|
try {
|
|
response = await sendMessage(payload);
|
|
} catch {
|
|
response = null;
|
|
}
|
|
|
|
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", () => {
|
|
notify({
|
|
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;
|
|
}
|
|
|
|
let response = null;
|
|
try {
|
|
response = await sendMessage(payload);
|
|
} catch {
|
|
response = null;
|
|
}
|
|
|
|
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", () => {
|
|
notify({
|
|
type: "AUTISTMASK_SIGN_RESPONSE",
|
|
id: approvalId,
|
|
approved: false,
|
|
});
|
|
window.close();
|
|
});
|
|
}
|
|
|
|
module.exports = { init, show, decodeCalldata };
|