Implement personal_sign and eth_signTypedData_v4 message signing
All checks were successful
check / check (push) Successful in 4s
All checks were successful
check / check (push) Successful in 4s
Replace stub error handlers with full approval flow for personal_sign, eth_sign, eth_signTypedData_v4, and eth_signTypedData. Uses toolbar popup only (no fallback window) and keeps sign approvals pending across popup close/reopen cycles so the user can respond via the toolbar icon.
This commit is contained in:
@@ -6,6 +6,7 @@ const {
|
||||
ETHEREUM_MAINNET_CHAIN_ID,
|
||||
DEFAULT_RPC_URL,
|
||||
} = require("../shared/constants");
|
||||
const { getBytes } = require("ethers");
|
||||
const { state, loadState, saveState } = require("../shared/state");
|
||||
const { refreshBalances, getProvider } = require("../shared/balances");
|
||||
const { debugFetch } = require("../shared/log");
|
||||
@@ -148,6 +149,7 @@ function requestApproval(origin, hostname) {
|
||||
}
|
||||
|
||||
// Open a tx-approval popup and return a promise that resolves with txHash or error.
|
||||
// Uses the toolbar popup only — no fallback window.
|
||||
function requestTxApproval(origin, hostname, txParams) {
|
||||
return new Promise((resolve) => {
|
||||
const id = nextApprovalId++;
|
||||
@@ -159,41 +161,70 @@ function requestTxApproval(origin, hostname, txParams) {
|
||||
type: "tx",
|
||||
};
|
||||
|
||||
if (actionApi && typeof actionApi.openPopup === "function") {
|
||||
if (actionApi && typeof actionApi.setPopup === "function") {
|
||||
actionApi.setPopup({
|
||||
popup: "src/popup/index.html?approval=" + id,
|
||||
});
|
||||
}
|
||||
if (actionApi && typeof actionApi.openPopup === "function") {
|
||||
try {
|
||||
const result = actionApi.openPopup();
|
||||
if (result && typeof result.catch === "function") {
|
||||
result.catch(() => openApprovalWindow(id));
|
||||
result.catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
openApprovalWindow(id);
|
||||
// openPopup unsupported — user clicks toolbar icon
|
||||
}
|
||||
} else {
|
||||
openApprovalWindow(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Detect when an approval popup (browser-action) closes without a response
|
||||
// Open a sign-approval popup and return a promise that resolves with { signature } or { error }.
|
||||
// Uses the toolbar popup only — no fallback window. If openPopup() fails the
|
||||
// popup URL is still set, so the user can click the toolbar icon to respond.
|
||||
function requestSignApproval(origin, hostname, signParams) {
|
||||
return new Promise((resolve) => {
|
||||
const id = nextApprovalId++;
|
||||
pendingApprovals[id] = {
|
||||
origin,
|
||||
hostname,
|
||||
signParams,
|
||||
resolve,
|
||||
type: "sign",
|
||||
};
|
||||
|
||||
if (actionApi && typeof actionApi.setPopup === "function") {
|
||||
actionApi.setPopup({
|
||||
popup: "src/popup/index.html?approval=" + id,
|
||||
});
|
||||
}
|
||||
if (actionApi && typeof actionApi.openPopup === "function") {
|
||||
try {
|
||||
const result = actionApi.openPopup();
|
||||
if (result && typeof result.catch === "function") {
|
||||
result.catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
// openPopup unsupported — user clicks toolbar icon
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Detect when an approval popup (browser-action) closes without a response.
|
||||
// TX and sign approvals are NOT auto-rejected on disconnect because toolbar
|
||||
// popups naturally close on focus loss and the user can reopen them.
|
||||
runtime.onConnect.addListener((port) => {
|
||||
if (port.name.startsWith("approval:")) {
|
||||
const id = parseInt(port.name.split(":")[1], 10);
|
||||
port.onDisconnect.addListener(() => {
|
||||
const approval = pendingApprovals[id];
|
||||
if (approval) {
|
||||
if (approval.type === "tx") {
|
||||
approval.resolve({
|
||||
error: {
|
||||
code: 4001,
|
||||
message: "User rejected the request.",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
approval.resolve({ approved: false, remember: false });
|
||||
if (approval.type === "tx" || approval.type === "sign") {
|
||||
// Keep pending — user can reopen the toolbar popup
|
||||
return;
|
||||
}
|
||||
approval.resolve({ approved: false, remember: false });
|
||||
delete pendingApprovals[id];
|
||||
}
|
||||
resetPopupUrl();
|
||||
@@ -390,18 +421,59 @@ async function handleRpc(method, params, origin) {
|
||||
}
|
||||
|
||||
if (method === "personal_sign" || method === "eth_sign") {
|
||||
return {
|
||||
error: { message: "Signing not yet implemented in AutistMask." },
|
||||
};
|
||||
const s = await getState();
|
||||
const activeAddress = await getActiveAddress();
|
||||
if (!activeAddress)
|
||||
return { error: { message: "No accounts available" } };
|
||||
|
||||
const hostname = extractHostname(origin);
|
||||
const allowed = s.allowedSites[activeAddress] || [];
|
||||
if (
|
||||
!allowed.includes(hostname) &&
|
||||
!connectedSites[origin + ":" + activeAddress]
|
||||
) {
|
||||
return { error: { code: 4100, message: "Unauthorized" } };
|
||||
}
|
||||
|
||||
// personal_sign: params[0]=message, params[1]=address
|
||||
// eth_sign: params[0]=address, params[1]=message
|
||||
const signParams =
|
||||
method === "personal_sign"
|
||||
? { method, message: params[0], from: params[1] }
|
||||
: { method, message: params[1], from: params[0] };
|
||||
|
||||
const decision = await requestSignApproval(
|
||||
origin,
|
||||
hostname,
|
||||
signParams,
|
||||
);
|
||||
if (decision.error) return { error: decision.error };
|
||||
return { result: decision.signature };
|
||||
}
|
||||
|
||||
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData") {
|
||||
return {
|
||||
error: {
|
||||
message:
|
||||
"Typed data signing not yet implemented in AutistMask.",
|
||||
},
|
||||
};
|
||||
const s = await getState();
|
||||
const activeAddress = await getActiveAddress();
|
||||
if (!activeAddress)
|
||||
return { error: { message: "No accounts available" } };
|
||||
|
||||
const hostname = extractHostname(origin);
|
||||
const allowed = s.allowedSites[activeAddress] || [];
|
||||
if (
|
||||
!allowed.includes(hostname) &&
|
||||
!connectedSites[origin + ":" + activeAddress]
|
||||
) {
|
||||
return { error: { code: 4100, message: "Unauthorized" } };
|
||||
}
|
||||
|
||||
const signParams = { method, typedData: params[1], from: params[0] };
|
||||
const decision = await requestSignApproval(
|
||||
origin,
|
||||
hostname,
|
||||
signParams,
|
||||
);
|
||||
if (decision.error) return { error: decision.error };
|
||||
return { result: decision.signature };
|
||||
}
|
||||
|
||||
if (method === "eth_sendTransaction") {
|
||||
@@ -446,7 +518,13 @@ async function broadcastAccountsChanged() {
|
||||
}
|
||||
// Reject and close any pending approval popups so they don't hang
|
||||
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
||||
approval.resolve({ approved: false, remember: false });
|
||||
if (approval.type === "tx" || approval.type === "sign") {
|
||||
approval.resolve({
|
||||
error: { code: 4001, message: "User rejected the request." },
|
||||
});
|
||||
} else {
|
||||
approval.resolve({ approved: false, remember: false });
|
||||
}
|
||||
if (approval.windowId) {
|
||||
windowsApi.remove(approval.windowId, () => {
|
||||
if (runtime.lastError) {
|
||||
@@ -514,7 +592,7 @@ if (windowsApi && windowsApi.onRemoved) {
|
||||
windowsApi.onRemoved.addListener((windowId) => {
|
||||
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
||||
if (approval.windowId === windowId) {
|
||||
if (approval.type === "tx") {
|
||||
if (approval.type === "tx" || approval.type === "sign") {
|
||||
approval.resolve({
|
||||
error: {
|
||||
code: 4001,
|
||||
@@ -550,6 +628,10 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
resp.type = "tx";
|
||||
resp.txParams = approval.txParams;
|
||||
}
|
||||
if (approval.type === "sign") {
|
||||
resp.type = "sign";
|
||||
resp.signParams = approval.signParams;
|
||||
}
|
||||
sendResponse(resp);
|
||||
} else {
|
||||
sendResponse(null);
|
||||
@@ -624,6 +706,76 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg.type === "AUTISTMASK_SIGN_RESPONSE") {
|
||||
const approval = pendingApprovals[msg.id];
|
||||
if (!approval) return false;
|
||||
delete pendingApprovals[msg.id];
|
||||
resetPopupUrl();
|
||||
|
||||
if (!msg.approved) {
|
||||
approval.resolve({
|
||||
error: { code: 4001, message: "User rejected the request." },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await loadState();
|
||||
const activeAddress = await getActiveAddress();
|
||||
let wallet, addrIndex;
|
||||
for (const w of state.wallets) {
|
||||
for (let i = 0; i < w.addresses.length; i++) {
|
||||
if (w.addresses[i].address === activeAddress) {
|
||||
wallet = w;
|
||||
addrIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (wallet) break;
|
||||
}
|
||||
if (!wallet) throw new Error("Wallet not found");
|
||||
const decrypted = await decryptWithPassword(
|
||||
wallet.encryptedSecret,
|
||||
msg.password,
|
||||
);
|
||||
const signer = getSignerForAddress(
|
||||
wallet,
|
||||
addrIndex,
|
||||
decrypted,
|
||||
);
|
||||
|
||||
const sp = approval.signParams;
|
||||
let signature;
|
||||
|
||||
if (sp.method === "personal_sign" || sp.method === "eth_sign") {
|
||||
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;
|
||||
signature = await signer.signTypedData(
|
||||
domain,
|
||||
types,
|
||||
message,
|
||||
);
|
||||
}
|
||||
|
||||
approval.resolve({ signature });
|
||||
sendResponse({ signature });
|
||||
} catch (e) {
|
||||
const errMsg = e.shortMessage || e.message;
|
||||
approval.resolve({
|
||||
error: { message: errMsg },
|
||||
});
|
||||
sendResponse({ error: errMsg });
|
||||
}
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg.type === "AUTISTMASK_ACTIVE_CHANGED") {
|
||||
broadcastAccountsChanged();
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user