import type { Browser, BrowserContext } from '@playwright/test'; import { Wallet } from 'ethers'; export interface WalletProviderOptions { privateKey: string; rpcUrl?: string; chainId?: number; chainName?: string; walletName?: string; walletUuid?: string; } const DEFAULT_CHAIN_ID = 31337; const DEFAULT_RPC_URL = 'http://127.0.0.1:8545'; const DEFAULT_WALLET_NAME = 'Playwright Wallet'; const DEFAULT_WALLET_UUID = '11111111-2222-3333-4444-555555555555'; export async function createWalletContext( browser: Browser, options: WalletProviderOptions, ): Promise { const chainId = options.chainId ?? DEFAULT_CHAIN_ID; const rpcUrl = options.rpcUrl ?? DEFAULT_RPC_URL; const chainName = options.chainName ?? 'Kraiken Local Fork'; const walletName = options.walletName ?? DEFAULT_WALLET_NAME; const walletUuid = options.walletUuid ?? DEFAULT_WALLET_UUID; const wallet = new Wallet(options.privateKey); const address = wallet.address; const chainIdHex = `0x${chainId.toString(16)}`; const context = await browser.newContext({ viewport: { width: 1280, height: 720 }, screen: { width: 1280, height: 720 }, }); await context.addInitScript(() => { window.localStorage.setItem('authentificated', 'true'); }); await context.addInitScript( ({ account, chainIdHex: cidHex, chainId: cid, rpcEndpoint, chainLabel, walletLabel, walletId, }) => { const listeners = new Map void>>(); let rpcRequestId = 0; let connected = false; const emit = (event: string, payload: any): void => { const handlers = listeners.get(event); if (!handlers) { return; } for (const handler of Array.from(handlers)) { try { handler(payload); } catch (error) { console.error(`[wallet-provider] listener for ${event} failed`, error); } } }; const sendRpc = async (method: string, params: unknown[]): Promise => { rpcRequestId += 1; const response = await fetch(rpcEndpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: rpcRequestId, method, params }), }); if (!response.ok) { throw new Error(`RPC call to ${method} failed with status ${response.status}`); } const payload = await response.json(); if (payload?.error) { const message = payload.error?.message ?? 'RPC error'; throw new Error(message); } return payload.result; }; const provider: any = { isMetaMask: true, isPlaywrightWallet: true, isConnected: () => connected, selectedAddress: account, chainId: cidHex, networkVersion: String(cid), request: async ({ method, params = [] }: { method: string; params?: unknown[] }) => { const args = Array.isArray(params) ? params.slice() : []; switch (method) { case 'eth_requestAccounts': connected = true; provider.selectedAddress = account; provider.chainId = cidHex; provider.networkVersion = String(cid); emit('connect', { chainId: cidHex }); emit('chainChanged', cidHex); emit('accountsChanged', [account]); return [account]; case 'eth_accounts': return [account]; case 'eth_chainId': return cidHex; case 'net_version': return String(cid); case 'wallet_switchEthereumChain': { const requested = (args[0] as { chainId?: string } | undefined)?.chainId; if (requested && requested.toLowerCase() !== cidHex.toLowerCase()) { throw new Error(`Unsupported chain ${requested}`); } return null; } case 'wallet_addEthereumChain': return null; case 'eth_sendTransaction': { const [tx = {}] = args as Record[]; const enrichedTx = { ...tx, from: account }; return sendRpc(method, [enrichedTx]); } case 'eth_sign': case 'personal_sign': case 'eth_signTypedData': case 'eth_signTypedData_v3': case 'eth_signTypedData_v4': { if (args.length > 0) { args[0] = account; } return sendRpc(method, args); } default: return sendRpc(method, args); } }, on: (event: string, listener: (...args: any[]) => void) => { const handlers = listeners.get(event) ?? new Set(); handlers.add(listener); listeners.set(event, handlers); if (event === 'connect' && connected) { listener({ chainId: cidHex }); } return provider; }, removeListener: (event: string, listener: (...args: any[]) => void) => { const handlers = listeners.get(event); if (handlers) { handlers.delete(listener); } return provider; }, enable: () => provider.request({ method: 'eth_requestAccounts' }), requestPermissions: () => Promise.resolve([{ parentCapability: 'eth_accounts' }]), getProviderState: async () => ({ accounts: [account], chainId: cidHex, isConnected: connected, }), }; const announce = () => { const detail = Object.freeze({ info: Object.freeze({ uuid: walletId, name: walletLabel, icon: 'data:image/svg+xml,', rdns: 'org.playwright.wallet', }), provider, }); window.dispatchEvent(new CustomEvent('eip6963:announceProvider', { detail })); }; const requestListener = () => announce(); window.addEventListener('eip6963:requestProvider', requestListener); Object.defineProperty(window, 'ethereum', { configurable: true, enumerable: true, value: provider, writable: false, }); provider.providers = [provider]; announce(); window.dispatchEvent(new Event('ethereum#initialized')); console.info('[wallet-provider] Injected test provider for', chainLabel); }, { account: address, chainIdHex, chainId, rpcEndpoint: rpcUrl, chainLabel: chainName, walletLabel: walletName, walletId: walletUuid, }, ); return context; }