Files
AutistMask/src/popup/views/approval.js
clawbot 50078b3566
All checks were successful
check / check (push) Successful in 34s
e2e / e2e-chrome (push) Successful in 1m13s
e2e / e2e-firefox (push) Successful in 28s
fix: resolve approval-screen token decimals, and refuse to format an unknown scale (closes #306)
decodeCalldata consulted only the 512-entry bundled list and defaulted to 18
decimals, so a transfer of 5,000 units of a 6-decimal token rendered
"Amount 0.0000" and the user confirmed a drain reading zero. The same
understatement applied to approve, where an unbounded allowance also rendered
0.0000.

Decimals now resolve from the bundled list, then trackedTokens, then the
address's explorer-reported entry, with uint8 validation and a refusal when
sources for one contract disagree. When no source knows the scale, no
formatUnits call is reached at all: the line renders raw base units with an
explicit "decimals unknown" warning, and the same string reaches
pendingTxDetails.amount so the status screens carry no formatted figure either.

Verified failing first two independent ways: restoring the old
`token ? token.decimals : 18` fails 6 of 15 new tests with the unknown case
reporting "0.0000"; making the resolver return 18 rather than null on the
unknown path fails a different 6, spanning resolver and render levels.
2026-08-20 12:57:38 +02:00

859 lines
30 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 {
resolveTokenDecimals,
unknownDecimalsAmount,
} = require("../../shared/approvalAmount");
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;
}
// The amount line for a decoded ERC-20 call. With a known scale it is the
// token quantity; with `decimals` null it is the base-unit integer with the
// unknown scale stated, because formatting it with an assumed scale is what
// showed a 5,000-token transfer as `0.0000`. `raw` is what the status screens
// carry, `display` is what the approval screen shows.
function tokenAmountText(rawAmount, decimals, symbol) {
if (decimals === null) {
const unknown = unknownDecimalsAmount(rawAmount);
return { raw: unknown, display: unknown };
}
const formatted = formatTxValue(formatUnits(rawAmount, decimals));
return {
raw: formatted,
display: formatted + (symbol ? " " + symbol : ""),
};
}
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;
// null when no source knows this token's scale. It is not
// defaulted to 18: an amount formatted with a guessed scale is
// the wrong number, and for a token with fewer decimals than the
// guess it is the wrong number in the direction that reads as
// zero. See tokenAmountText().
const tokenDecimals = resolveTokenDecimals(toAddress, {
trackedTokens: state.trackedTokens,
wallets: state.wallets,
});
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;
// An unbounded allowance needs no scale to describe, so it is
// still named rather than refused.
const amount = isUnlimited
? { raw: "Unlimited", display: "Unlimited" }
: tokenAmountText(rawAmount, tokenDecimals, 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: amount.display,
rawValue: amount.raw,
},
],
};
}
if (parsed.name === "transfer") {
const to = parsed.args[0];
const rawAmount = parsed.args[1];
const amount = tokenAmountText(
rawAmount,
tokenDecimals,
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: amount.display,
rawValue: amount.raw,
},
],
};
}
}
} 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 };