Implement eth_sendTransaction for dApp-initiated transactions
All checks were successful
check / check (push) Successful in 17s
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:
@@ -7,8 +7,10 @@ const {
|
|||||||
DEFAULT_RPC_URL,
|
DEFAULT_RPC_URL,
|
||||||
} = require("../shared/constants");
|
} = require("../shared/constants");
|
||||||
const { state, loadState, saveState } = require("../shared/state");
|
const { state, loadState, saveState } = require("../shared/state");
|
||||||
const { refreshBalances } = require("../shared/balances");
|
const { refreshBalances, getProvider } = require("../shared/balances");
|
||||||
const { debugFetch } = require("../shared/log");
|
const { debugFetch } = require("../shared/log");
|
||||||
|
const { decryptWithPassword } = require("../shared/vault");
|
||||||
|
const { getSignerForAddress } = require("../shared/wallet");
|
||||||
|
|
||||||
const storageApi =
|
const storageApi =
|
||||||
typeof browser !== "undefined"
|
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
|
// Detect when an approval popup (browser-action) closes without a response
|
||||||
runtime.onConnect.addListener((port) => {
|
runtime.onConnect.addListener((port) => {
|
||||||
if (port.name.startsWith("approval:")) {
|
if (port.name.startsWith("approval:")) {
|
||||||
@@ -152,7 +184,16 @@ runtime.onConnect.addListener((port) => {
|
|||||||
port.onDisconnect.addListener(() => {
|
port.onDisconnect.addListener(() => {
|
||||||
const approval = pendingApprovals[id];
|
const approval = pendingApprovals[id];
|
||||||
if (approval) {
|
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];
|
delete pendingApprovals[id];
|
||||||
}
|
}
|
||||||
resetPopupUrl();
|
resetPopupUrl();
|
||||||
@@ -364,12 +405,24 @@ async function handleRpc(method, params, origin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (method === "eth_sendTransaction") {
|
if (method === "eth_sendTransaction") {
|
||||||
return {
|
const s = await getState();
|
||||||
error: {
|
const activeAddress = await getActiveAddress();
|
||||||
message:
|
if (!activeAddress)
|
||||||
"Transaction signing via dApps not yet implemented. Use the AutistMask popup to send transactions.",
|
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
|
// Proxy safe read-only methods to the RPC node
|
||||||
@@ -456,7 +509,16 @@ if (windowsApi && windowsApi.onRemoved) {
|
|||||||
windowsApi.onRemoved.addListener((windowId) => {
|
windowsApi.onRemoved.addListener((windowId) => {
|
||||||
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
||||||
if (approval.windowId === windowId) {
|
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];
|
delete pendingApprovals[id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -475,10 +537,15 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
||||||
const approval = pendingApprovals[msg.id];
|
const approval = pendingApprovals[msg.id];
|
||||||
if (approval) {
|
if (approval) {
|
||||||
sendResponse({
|
const resp = {
|
||||||
hostname: approval.hostname,
|
hostname: approval.hostname,
|
||||||
origin: approval.origin,
|
origin: approval.origin,
|
||||||
});
|
};
|
||||||
|
if (approval.type === "tx") {
|
||||||
|
resp.type = "tx";
|
||||||
|
resp.txParams = approval.txParams;
|
||||||
|
}
|
||||||
|
sendResponse(resp);
|
||||||
} else {
|
} else {
|
||||||
sendResponse(null);
|
sendResponse(null);
|
||||||
}
|
}
|
||||||
@@ -498,6 +565,57 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
return false;
|
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") {
|
if (msg.type === "AUTISTMASK_ACTIVE_CHANGED") {
|
||||||
broadcastAccountsChanged();
|
broadcastAccountsChanged();
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -694,6 +694,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ TRANSACTION APPROVAL ============ -->
|
||||||
|
<div id="view-approve-tx" class="view hidden">
|
||||||
|
<h2 class="font-bold mb-2">Transaction Request</h2>
|
||||||
|
<p class="mb-2">
|
||||||
|
<span id="approve-tx-hostname" class="font-bold"></span>
|
||||||
|
wants to send a transaction.
|
||||||
|
</p>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="text-xs text-muted mb-1">From</div>
|
||||||
|
<div id="approve-tx-from" class="text-xs break-all"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="text-xs text-muted mb-1">To</div>
|
||||||
|
<div id="approve-tx-to" class="text-xs break-all"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="text-xs text-muted mb-1">Value</div>
|
||||||
|
<div id="approve-tx-value" class="text-xs font-bold"></div>
|
||||||
|
</div>
|
||||||
|
<div id="approve-tx-data-section" class="mb-2 hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Data</div>
|
||||||
|
<div id="approve-tx-data" class="text-xs break-all"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="block mb-1 text-xs">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="approve-tx-password"
|
||||||
|
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="approve-tx-error" class="text-xs hidden mb-2"></div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<button
|
||||||
|
id="btn-approve-tx"
|
||||||
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="btn-reject-tx"
|
||||||
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ============ SITE APPROVAL ============ -->
|
<!-- ============ SITE APPROVAL ============ -->
|
||||||
<div id="view-approve-site" class="view hidden">
|
<div id="view-approve-site" class="view hidden">
|
||||||
<h2 class="font-bold mb-2">Connection Request</h2>
|
<h2 class="font-bold mb-2">Connection Request</h2>
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
const { $, formatAddressHtml } = require("./helpers");
|
const { $, formatAddressHtml, showView } = require("./helpers");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
|
const { formatEther } = require("ethers");
|
||||||
|
|
||||||
const runtime =
|
const runtime =
|
||||||
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||||
|
|
||||||
let approvalId = null;
|
let approvalId = null;
|
||||||
|
|
||||||
|
function showTxApproval(details) {
|
||||||
|
$("approve-tx-hostname").textContent = details.hostname;
|
||||||
|
$("approve-tx-from").textContent = state.activeAddress;
|
||||||
|
$("approve-tx-to").textContent =
|
||||||
|
details.txParams.to || "(contract creation)";
|
||||||
|
$("approve-tx-value").textContent =
|
||||||
|
formatEther(details.txParams.value || "0") + " ETH";
|
||||||
|
if (details.txParams.data && details.txParams.data !== "0x") {
|
||||||
|
$("approve-tx-data").textContent = details.txParams.data;
|
||||||
|
$("approve-tx-data-section").classList.remove("hidden");
|
||||||
|
}
|
||||||
|
showView("approve-tx");
|
||||||
|
}
|
||||||
|
|
||||||
function show(id) {
|
function show(id) {
|
||||||
approvalId = id;
|
approvalId = id;
|
||||||
// Connect a port so the background detects if the popup closes
|
// Connect a port so the background detects if the popup closes
|
||||||
@@ -16,6 +31,10 @@ function show(id) {
|
|||||||
window.close();
|
window.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (details.type === "tx") {
|
||||||
|
showTxApproval(details);
|
||||||
|
return;
|
||||||
|
}
|
||||||
$("approve-hostname").textContent = details.hostname;
|
$("approve-hostname").textContent = details.hostname;
|
||||||
$("approve-address").innerHTML = formatAddressHtml(
|
$("approve-address").innerHTML = formatAddressHtml(
|
||||||
state.activeAddress,
|
state.activeAddress,
|
||||||
@@ -53,6 +72,25 @@ function init(ctx) {
|
|||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("btn-approve-tx").addEventListener("click", () => {
|
||||||
|
runtime.sendMessage({
|
||||||
|
type: "AUTISTMASK_TX_RESPONSE",
|
||||||
|
id: approvalId,
|
||||||
|
approved: true,
|
||||||
|
password: $("approve-tx-password").value,
|
||||||
|
});
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("btn-reject-tx").addEventListener("click", () => {
|
||||||
|
runtime.sendMessage({
|
||||||
|
type: "AUTISTMASK_TX_RESPONSE",
|
||||||
|
id: approvalId,
|
||||||
|
approved: false,
|
||||||
|
});
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init, show };
|
module.exports = { init, show };
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const VIEWS = [
|
|||||||
"settings",
|
"settings",
|
||||||
"transaction",
|
"transaction",
|
||||||
"approve-site",
|
"approve-site",
|
||||||
|
"approve-tx",
|
||||||
];
|
];
|
||||||
|
|
||||||
function $(id) {
|
function $(id) {
|
||||||
|
|||||||
Reference in New Issue
Block a user