@finn_gal/patchright-wallet-mock-ts
Version:
Mock Web3 Browser wallets, like Metamask, in Patchright tests.
130 lines (115 loc) • 4.59 kB
text/typescript
import { createWallet } from './createWallet';
import cors from 'cors';
import { randomUUID } from 'crypto';
import express from 'express';
import net from 'net';
import type { Page } from 'patchright';
import { Chain, LocalAccount, Transport } from 'viem';
import { Wallet } from './createWallet';
declare global {
interface Window {
ethereum?: EIP1193Provider;
eip1193Request?: (params: eip1193RequestParams) => Promise<any>;
}
}
let wallets: Map<string, Wallet> = new Map();
export async function installMockWallet({ debug, ...params }: & { debug?: boolean; } & { page: Page } & (| {
account: LocalAccount;
transports?: Record<number, Transport>;
defaultChain?: Chain;
} | { wallet: Wallet; })) {
const freePort = await new Promise(res => {
const srv = net.createServer();
srv.listen(0, () => {
const port = (srv.address() as any).port;
srv.close((err) => res(port));
});
});
const app = express();
app.use(cors({ origin: '*', methods: [ 'POST' ], allowedHeaders: [ 'Content-Type' ] }));
app.use(express.json());
app.post('/eip1193', async (req: any, res: any) => {
const { method, params, uuid, debug } = req.body;
const wallet = wallets.get(uuid);
if ( !wallet ) return res.status(400).json({ error: 'Wallet not found' });
try {
const result = await wallet.request({ method, params });
if ( debug ) console.log('WALLET', uuid.substring(0, 8), 'REQUEST', method, params, 'RESULT', result);
res.json(result);
} catch ( error: any ) {
if ( debug ) console.log('WALLET', uuid.substring(0, 8), 'REQUEST', method, params, 'ERROR', error);
res.status(500).json({ error: error.message });
}
});
app.listen(freePort);
const browserOrPage = params.page;
const wallet = 'wallet' in params
? params.wallet
: createWallet(params.account, params.transports, params.defaultChain);
const uuid = randomUUID();
wallets.set(uuid, wallet);
await browserOrPage.addInitScript(({ uuid, debug, freePort }) => {
window.eip1193Request = async ({ method, params }) => {
try {
const response = await fetch(`http://localhost:${freePort}/eip1193`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method, params, uuid, debug })
});
const result = await response.json();
if ( !response.ok ) throw new Error(result.error || 'Request failed');
return result;
} catch ( e ) {
if ( debug ) console.error('EIP-1193 Error:', e);
throw e;
}
};
function announceMockWallet() {
const provider = {
request: async (request: any) => {
return await window.eip1193Request!({ ...request, uuid, debug });
},
on: () => {},
removeListener: () => {}
};
const info = {
uuid,
name: 'MetaMask',
icon: '',
rdns: 'io.metamask'
};
const detail = { info, provider };
const announceEvent = new CustomEvent('eip6963:announceProvider', {
detail: Object.freeze(detail)
});
window.dispatchEvent(announceEvent);
window.ethereum = provider;
}
announceMockWallet();
window.addEventListener('eip6963:requestProvider', () => announceMockWallet());
window.addEventListener('DOMContentLoaded', () => announceMockWallet());
},
{ uuid, debug, freePort }
);
}
interface EIP6963ProviderInfo {
uuid: string;
name: string;
icon: string;
rdns: string;
}
interface EIP1193Provider {
request: (request: { method: string; params?: Array<unknown>; }) => Promise<unknown>; // Standard method for sending requests per EIP-1193
on: () => void;
removeListener: () => void;
}
export interface EIP6963ProviderDetail {
info: EIP6963ProviderInfo;
provider: EIP1193Provider;
}
export interface eip1193RequestParams {
method: string;
params?: Array<unknown>;
uuid: string;
debug?: boolean;
}