diff --git a/src/background/index.js b/src/background/index.js index fcb22f3..887f2a2 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -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; diff --git a/src/popup/index.html b/src/popup/index.html index 396d16c..b2a535d 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -694,6 +694,54 @@ + + +