Implement EIP-1193 provider for dApp connectivity
All checks were successful
check / check (push) Successful in 13s
All checks were successful
check / check (push) Successful in 13s
Three-part architecture: - inpage.js: creates window.ethereum in page context with request(), on(), send(), sendAsync(), enable() methods. Sets isMetaMask=true for compatibility. - content/index.js: bridge between page and extension via postMessage (page<->content) and runtime.sendMessage (content<->background). - background/index.js: handles RPC routing. Proxies read-only methods (eth_call, eth_getBalance, etc.) to configured RPC. Handles eth_requestAccounts (auto-connect for now), wallet_switchEthereumChain (mainnet only), and returns informative errors for unimplemented signing methods. Manifests updated with web_accessible_resources for inpage.js. Build updated to bundle inpage.js as a separate output file.
This commit is contained in:
parent
98b5eef21b
commit
1c9d5a9f2d
11
build.js
11
build.js
@ -62,6 +62,17 @@ async function build() {
|
|||||||
minify: true,
|
minify: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// bundle inpage script (injected into page context, separate file)
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: [path.join(SRC, "content", "inpage.js")],
|
||||||
|
bundle: true,
|
||||||
|
format: "iife",
|
||||||
|
outfile: path.join(distDir, "src", "content", "inpage.js"),
|
||||||
|
platform: "browser",
|
||||||
|
target: ["chrome110", "firefox110"],
|
||||||
|
minify: true,
|
||||||
|
});
|
||||||
|
|
||||||
// copy popup HTML
|
// copy popup HTML
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
path.join(SRC, "popup", "index.html"),
|
path.join(SRC, "popup", "index.html"),
|
||||||
|
|||||||
@ -16,5 +16,11 @@
|
|||||||
"js": ["src/content/index.js"],
|
"js": ["src/content/index.js"],
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["src/content/inpage.js"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"web_accessible_resources": ["src/content/inpage.js"],
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "autistmask@sneak.berlin"
|
"id": "autistmask@sneak.berlin"
|
||||||
|
|||||||
@ -1,2 +1,218 @@
|
|||||||
// AutistMask background service worker
|
// AutistMask background service worker
|
||||||
// TODO: wallet management, message routing, provider implementation
|
// Handles EIP-1193 RPC requests from content scripts and proxies
|
||||||
|
// non-sensitive calls to the configured Ethereum JSON-RPC endpoint.
|
||||||
|
|
||||||
|
const CHAIN_ID = "0x1";
|
||||||
|
const DEFAULT_RPC = "https://eth.llamarpc.com";
|
||||||
|
|
||||||
|
const storageApi =
|
||||||
|
typeof browser !== "undefined"
|
||||||
|
? browser.storage.local
|
||||||
|
: chrome.storage.local;
|
||||||
|
const runtime =
|
||||||
|
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||||
|
|
||||||
|
// Connected sites: { origin: [address, ...] }
|
||||||
|
const connectedSites = {};
|
||||||
|
|
||||||
|
async function getState() {
|
||||||
|
const result = await storageApi.get("autistmask");
|
||||||
|
return result.autistmask || { wallets: [], rpcUrl: DEFAULT_RPC };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAccounts() {
|
||||||
|
const state = await getState();
|
||||||
|
const accounts = [];
|
||||||
|
for (const wallet of state.wallets) {
|
||||||
|
for (const addr of wallet.addresses) {
|
||||||
|
accounts.push(addr.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRpcUrl() {
|
||||||
|
const state = await getState();
|
||||||
|
return state.rpcUrl || DEFAULT_RPC;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy an RPC call to the Ethereum node
|
||||||
|
async function proxyRpc(method, params) {
|
||||||
|
const rpcUrl = await getRpcUrl();
|
||||||
|
const resp = await fetch(rpcUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 1,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
if (json.error) {
|
||||||
|
throw new Error(json.error.message || "RPC error");
|
||||||
|
}
|
||||||
|
return json.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods that are safe to proxy directly to the RPC node
|
||||||
|
const PROXY_METHODS = [
|
||||||
|
"eth_blockNumber",
|
||||||
|
"eth_call",
|
||||||
|
"eth_chainId",
|
||||||
|
"eth_estimateGas",
|
||||||
|
"eth_gasPrice",
|
||||||
|
"eth_getBalance",
|
||||||
|
"eth_getBlockByHash",
|
||||||
|
"eth_getBlockByNumber",
|
||||||
|
"eth_getCode",
|
||||||
|
"eth_getLogs",
|
||||||
|
"eth_getStorageAt",
|
||||||
|
"eth_getTransactionByHash",
|
||||||
|
"eth_getTransactionCount",
|
||||||
|
"eth_getTransactionReceipt",
|
||||||
|
"eth_maxPriorityFeePerGas",
|
||||||
|
"eth_sendRawTransaction",
|
||||||
|
"net_version",
|
||||||
|
"web3_clientVersion",
|
||||||
|
"eth_feeHistory",
|
||||||
|
"eth_getBlockTransactionCountByHash",
|
||||||
|
"eth_getBlockTransactionCountByNumber",
|
||||||
|
];
|
||||||
|
|
||||||
|
async function handleRpc(method, params, origin) {
|
||||||
|
// Methods that need wallet involvement
|
||||||
|
if (method === "eth_requestAccounts" || method === "eth_accounts") {
|
||||||
|
const accounts = await getAccounts();
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
return { error: { message: "No accounts available" } };
|
||||||
|
}
|
||||||
|
// Auto-connect for now (approval flow is a future TODO)
|
||||||
|
connectedSites[origin] = accounts;
|
||||||
|
return { result: accounts };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "eth_chainId") {
|
||||||
|
return { result: CHAIN_ID };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "net_version") {
|
||||||
|
return { result: "1" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "wallet_switchEthereumChain") {
|
||||||
|
const chainId = params?.[0]?.chainId;
|
||||||
|
if (chainId === CHAIN_ID) {
|
||||||
|
return { result: null };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
code: 4902,
|
||||||
|
message: "AutistMask only supports Ethereum mainnet.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "wallet_addEthereumChain") {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
code: 4902,
|
||||||
|
message: "AutistMask only supports Ethereum mainnet.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "wallet_requestPermissions") {
|
||||||
|
const accounts = await getAccounts();
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
return { error: { message: "No accounts available" } };
|
||||||
|
}
|
||||||
|
connectedSites[origin] = accounts;
|
||||||
|
return {
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
parentCapability: "eth_accounts",
|
||||||
|
caveats: [
|
||||||
|
{
|
||||||
|
type: "restrictReturnedAccounts",
|
||||||
|
value: accounts,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "wallet_getPermissions") {
|
||||||
|
const accounts = connectedSites[origin] || [];
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
return { result: [] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
parentCapability: "eth_accounts",
|
||||||
|
caveats: [
|
||||||
|
{
|
||||||
|
type: "restrictReturnedAccounts",
|
||||||
|
value: accounts,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "personal_sign" || method === "eth_sign") {
|
||||||
|
// TODO: implement signature approval flow
|
||||||
|
return {
|
||||||
|
error: { message: "Signing not yet implemented in AutistMask." },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData") {
|
||||||
|
// TODO: implement typed data signing
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message:
|
||||||
|
"Typed data signing not yet implemented in AutistMask.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "eth_sendTransaction") {
|
||||||
|
// TODO: implement transaction signing approval flow
|
||||||
|
// For now, return an error directing the user to use the popup
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message:
|
||||||
|
"Transaction signing via dApps not yet implemented. Use the AutistMask popup to send transactions.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy safe read-only methods to the RPC node
|
||||||
|
if (PROXY_METHODS.includes(method)) {
|
||||||
|
try {
|
||||||
|
const result = await proxyRpc(method, params);
|
||||||
|
return { result };
|
||||||
|
} catch (e) {
|
||||||
|
return { error: { message: e.message } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: { message: "Unsupported method: " + method } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for messages from content scripts
|
||||||
|
runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
|
if (msg.type !== "AUTISTMASK_RPC") return;
|
||||||
|
|
||||||
|
handleRpc(msg.method, msg.params, msg.origin).then((response) => {
|
||||||
|
sendResponse(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return true to indicate async response
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|||||||
@ -1,2 +1,51 @@
|
|||||||
// AutistMask content script
|
// AutistMask content script — bridges between inpage (window.ethereum)
|
||||||
// TODO: inject window.ethereum provider into page context
|
// and the background service worker via extension messaging.
|
||||||
|
|
||||||
|
// Inject the inpage script into the page's JS context
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = (typeof browser !== "undefined" ? browser : chrome).runtime.getURL(
|
||||||
|
"src/content/inpage.js",
|
||||||
|
);
|
||||||
|
script.onload = function () {
|
||||||
|
this.remove();
|
||||||
|
};
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
|
||||||
|
// Relay requests from the page to the background script
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
if (event.source !== window) return;
|
||||||
|
if (event.data?.type !== "AUTISTMASK_REQUEST") return;
|
||||||
|
const { id, method, params } = event.data;
|
||||||
|
|
||||||
|
const runtime =
|
||||||
|
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||||
|
|
||||||
|
runtime.sendMessage(
|
||||||
|
{ type: "AUTISTMASK_RPC", id, method, params, origin: location.origin },
|
||||||
|
(response) => {
|
||||||
|
if (response) {
|
||||||
|
window.postMessage(
|
||||||
|
{ type: "AUTISTMASK_RESPONSE", id, ...response },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for events pushed from the background (e.g. accountsChanged)
|
||||||
|
const runtime =
|
||||||
|
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||||
|
|
||||||
|
runtime.onMessage.addListener((msg) => {
|
||||||
|
if (msg.type === "AUTISTMASK_EVENT") {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: "AUTISTMASK_EVENT",
|
||||||
|
eventName: msg.eventName,
|
||||||
|
data: msg.data,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
128
src/content/inpage.js
Normal file
128
src/content/inpage.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// AutistMask inpage script — injected into the page's JS context.
|
||||||
|
// Creates window.ethereum (EIP-1193 provider).
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
if (typeof window.ethereum !== "undefined") return;
|
||||||
|
|
||||||
|
const CHAIN_ID = "0x1"; // Ethereum mainnet
|
||||||
|
|
||||||
|
const listeners = {};
|
||||||
|
let nextId = 1;
|
||||||
|
const pending = {};
|
||||||
|
|
||||||
|
// Listen for responses from the content script
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
if (event.source !== window) return;
|
||||||
|
if (event.data?.type !== "AUTISTMASK_RESPONSE") return;
|
||||||
|
const { id, result, error } = event.data;
|
||||||
|
const p = pending[id];
|
||||||
|
if (!p) return;
|
||||||
|
delete pending[id];
|
||||||
|
if (error) {
|
||||||
|
p.reject(new Error(error.message || "Request failed"));
|
||||||
|
} else {
|
||||||
|
p.resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for events pushed from the extension
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
if (event.source !== window) return;
|
||||||
|
if (event.data?.type !== "AUTISTMASK_EVENT") return;
|
||||||
|
const { eventName, data } = event.data;
|
||||||
|
emit(eventName, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
function emit(eventName, data) {
|
||||||
|
const cbs = listeners[eventName];
|
||||||
|
if (!cbs) return;
|
||||||
|
for (const cb of cbs) {
|
||||||
|
try {
|
||||||
|
cb(data);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore listener errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function request(args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = nextId++;
|
||||||
|
pending[id] = { resolve, reject };
|
||||||
|
window.postMessage(
|
||||||
|
{ type: "AUTISTMASK_REQUEST", id, ...args },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = {
|
||||||
|
isAutistMask: true,
|
||||||
|
isMetaMask: true, // compatibility — many dApps check this
|
||||||
|
chainId: CHAIN_ID,
|
||||||
|
networkVersion: "1",
|
||||||
|
selectedAddress: null,
|
||||||
|
|
||||||
|
request(args) {
|
||||||
|
return request({ method: args.method, params: args.params || [] });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legacy methods (still used by some dApps)
|
||||||
|
enable() {
|
||||||
|
return this.request({ method: "eth_requestAccounts" });
|
||||||
|
},
|
||||||
|
|
||||||
|
send(methodOrPayload, paramsOrCallback) {
|
||||||
|
// Handle both send(method, params) and send({method, params})
|
||||||
|
if (typeof methodOrPayload === "string") {
|
||||||
|
return this.request({
|
||||||
|
method: methodOrPayload,
|
||||||
|
params: paramsOrCallback || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.request({
|
||||||
|
method: methodOrPayload.method,
|
||||||
|
params: methodOrPayload.params || [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
sendAsync(payload, callback) {
|
||||||
|
this.request({
|
||||||
|
method: payload.method,
|
||||||
|
params: payload.params || [],
|
||||||
|
})
|
||||||
|
.then((result) =>
|
||||||
|
callback(null, { id: payload.id, jsonrpc: "2.0", result }),
|
||||||
|
)
|
||||||
|
.catch((err) => callback(err));
|
||||||
|
},
|
||||||
|
|
||||||
|
on(event, cb) {
|
||||||
|
if (!listeners[event]) listeners[event] = [];
|
||||||
|
listeners[event].push(cb);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeListener(event, cb) {
|
||||||
|
if (!listeners[event]) return this;
|
||||||
|
listeners[event] = listeners[event].filter((c) => c !== cb);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAllListeners(event) {
|
||||||
|
if (event) {
|
||||||
|
delete listeners[event];
|
||||||
|
} else {
|
||||||
|
for (const key of Object.keys(listeners)) {
|
||||||
|
delete listeners[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ethereum = provider;
|
||||||
|
|
||||||
|
// Announce via EIP-6963 (multi-wallet discovery)
|
||||||
|
window.dispatchEvent(new Event("ethereum#initialized"));
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue
Block a user