Implement eth_sendTransaction for dApp-initiated transactions
All checks were successful
check / check (push) Successful in 17s

Show a confirmation popup with tx details (from, to, value, data) and
password prompt when a dApp calls eth_sendTransaction. Sign and broadcast
the transaction in the background, returning the tx hash to the dApp.
This commit is contained in:
2026-02-26 18:39:09 +07:00
parent c131b89732
commit a5b2470dba
4 changed files with 217 additions and 12 deletions

View File

@@ -7,8 +7,10 @@ const {
DEFAULT_RPC_URL,
} = require("../shared/constants");
const { state, loadState, saveState } = require("../shared/state");
const { refreshBalances } = require("../shared/balances");
const { refreshBalances, getProvider } = require("../shared/balances");
const { debugFetch } = require("../shared/log");
const { decryptWithPassword } = require("../shared/vault");
const { getSignerForAddress } = require("../shared/wallet");
const storageApi =
typeof browser !== "undefined"
@@ -145,6 +147,36 @@ function requestApproval(origin, hostname) {
});
}
// Open a tx-approval popup and return a promise that resolves with txHash or error.
function requestTxApproval(origin, hostname, txParams) {
return new Promise((resolve) => {
const id = nextApprovalId++;
pendingApprovals[id] = {
origin,
hostname,
txParams,
resolve,
type: "tx",
};
if (actionApi && typeof actionApi.openPopup === "function") {
actionApi.setPopup({
popup: "src/popup/index.html?approval=" + id,
});
try {
const result = actionApi.openPopup();
if (result && typeof result.catch === "function") {
result.catch(() => openApprovalWindow(id));
}
} catch {
openApprovalWindow(id);
}
} else {
openApprovalWindow(id);
}
});
}
// Detect when an approval popup (browser-action) closes without a response
runtime.onConnect.addListener((port) => {
if (port.name.startsWith("approval:")) {
@@ -152,7 +184,16 @@ runtime.onConnect.addListener((port) => {
port.onDisconnect.addListener(() => {
const approval = pendingApprovals[id];
if (approval) {
approval.resolve({ approved: false, remember: false });
if (approval.type === "tx") {
approval.resolve({
error: {
code: 4001,
message: "User rejected the request.",
},
});
} else {
approval.resolve({ approved: false, remember: false });
}
delete pendingApprovals[id];
}
resetPopupUrl();
@@ -364,12 +405,24 @@ async function handleRpc(method, params, origin) {
}
if (method === "eth_sendTransaction") {
return {
error: {
message:
"Transaction signing via dApps not yet implemented. Use the AutistMask popup to send transactions.",
},
};
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 txParams = params?.[0] || {};
const decision = await requestTxApproval(origin, hostname, txParams);
if (decision.error) return { error: decision.error };
return { result: decision.txHash };
}
// Proxy safe read-only methods to the RPC node
@@ -456,7 +509,16 @@ if (windowsApi && windowsApi.onRemoved) {
windowsApi.onRemoved.addListener((windowId) => {
for (const [id, approval] of Object.entries(pendingApprovals)) {
if (approval.windowId === windowId) {
approval.resolve({ approved: false, remember: false });
if (approval.type === "tx") {
approval.resolve({
error: {
code: 4001,
message: "User rejected the request.",
},
});
} else {
approval.resolve({ approved: false, remember: false });
}
delete pendingApprovals[id];
}
}
@@ -475,10 +537,15 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
const approval = pendingApprovals[msg.id];
if (approval) {
sendResponse({
const resp = {
hostname: approval.hostname,
origin: approval.origin,
});
};
if (approval.type === "tx") {
resp.type = "tx";
resp.txParams = approval.txParams;
}
sendResponse(resp);
} else {
sendResponse(null);
}
@@ -498,6 +565,57 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
return false;
}
if (msg.type === "AUTISTMASK_TX_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 provider = getProvider(state.rpcUrl);
const connected = signer.connect(provider);
const tx = await connected.sendTransaction(approval.txParams);
approval.resolve({ txHash: tx.hash });
} catch (e) {
approval.resolve({
error: { message: e.shortMessage || e.message },
});
}
})();
return true;
}
if (msg.type === "AUTISTMASK_ACTIVE_CHANGED") {
broadcastAccountsChanged();
return false;