UNPKG

burner-connector

Version:
290 lines 13.8 kB
import { createConnector } from "wagmi"; import { http, BaseError, RpcRequestError, SwitchChainError, createWalletClient, custom, fromHex, getAddress, createPublicClient, slice, concat, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { getHttpRpcClient, hexToBigInt, hexToNumber, numberToHex } from "viem/utils"; import { burnerWalletId, burnerWalletName, loadBurnerPK } from "../utils/index.js"; const GAS_MULTIPLIER = 110n; // 10% more gas // Magic identifier for burner wallet const BURNER_MAGIC_IDENTIFIER = "0x424E524E52424E52424E52424E52424E52424E52424E52424E52424E52424E52"; // "BNRNRBNRNRBNRNRBNRNRBNRNRBNRNRBNRNRBNRNRBNRNRBNRNRBNRNRNR" export class ConnectorNotConnectedError extends BaseError { constructor() { super("Connector not connected."); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: "ConnectorNotConnectedError" }); } } export class ChainNotConfiguredError extends BaseError { constructor() { super("Chain not configured."); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: "ChainNotConfiguredError" }); } } export const burner = ({ useSessionStorage = false, rpcUrls = {} } = {}) => { let connected = true; let connectedChainId; return createConnector((config) => ({ id: burnerWalletId, name: burnerWalletName, type: burnerWalletId, async connect({ chainId, withCapabilities } = {}) { const provider = await this.getProvider(); const accounts = await provider.request({ method: "eth_accounts", }); let currentChainId = await this.getChainId(); if (chainId && currentChainId !== chainId && this.switchChain) { const chain = await this.switchChain({ chainId }); currentChainId = chain.id; } connected = true; return { accounts: (withCapabilities ? accounts.map((address) => ({ address, capabilities: {} })) : accounts), chainId: currentChainId, }; }, async getProvider({ chainId } = {}) { const targetChainId = chainId || connectedChainId; const chain = config.chains.find((x) => x.id === targetChainId) ?? config.chains[0]; // Use custom RPC URL if provided, otherwise fallback to default const url = rpcUrls[chain.id] || chain.rpcUrls.default.http[0]; if (!url) throw new Error("No rpc url found for chain"); const burnerAccount = privateKeyToAccount(loadBurnerPK({ useSessionStorage })); const client = createWalletClient({ chain: chain, account: burnerAccount, transport: http(url), }); const publicClient = createPublicClient({ chain: chain, transport: http(url), }); const request = async ({ method, params }) => { if (method === "eth_sendTransaction") { const actualParams = params[0]; const hash = await client.sendTransaction({ account: burnerAccount, data: actualParams?.data, to: actualParams?.to, value: actualParams?.value ? hexToBigInt(actualParams.value) : undefined, gas: actualParams?.gas ? hexToBigInt(actualParams.gas) : undefined, nonce: actualParams?.nonce ? hexToNumber(actualParams.nonce) : undefined, maxPriorityFeePerGas: actualParams?.maxPriorityFeePerGas ? hexToBigInt(actualParams.maxPriorityFeePerGas) : undefined, maxFeePerGas: actualParams?.maxFeePerGas ? hexToBigInt(actualParams.maxFeePerGas) : undefined, gasPrice: (actualParams?.gasPrice ? hexToBigInt(actualParams.gasPrice) : undefined), }); return hash; } if (method === "personal_sign") { // first param is Hex data representation of message, // second param is address of the signer const rawMessage = params[0]; const signature = await client.signMessage({ account: burnerAccount, message: { raw: rawMessage }, }); return signature; } if (method === "eth_signTypedData_v4") { // first param is address of the signer // second param is stringified typed data const stringifiedData = params[1]; const signature = await client.signTypedData(JSON.parse(stringifiedData)); return signature; } if (method === "eth_accounts") { return [burnerAccount.address]; } if (method === "wallet_switchEthereumChain") { connectedChainId = fromHex(params[0].chainId, "number"); this.onChainChanged(connectedChainId.toString()); return; } if (method === "wallet_getCapabilities") { // Accept params as [address, chainIds?] const chainIdsRaw = params[1]; // Get supported chains from config const supportedChains = config.chains.map((chain) => numberToHex(chain.id)); // If no chainIds provided, use all supported const chainIds = chainIdsRaw && chainIdsRaw.length > 0 ? chainIdsRaw : supportedChains; const capabilities = {}; for (const chainId of chainIds) { capabilities[chainId] = { atomic: { status: "unsupported" }, }; } return capabilities; } if (method === "wallet_sendCalls") { const sendCallsParams = params[0]; if (sendCallsParams.atomicRequired) { throw new Error("Atomic execution not supported"); } // Execute calls sequentially for now const hashes = []; for (const call of sendCallsParams.calls) { const request = await client.prepareTransactionRequest({ account: burnerAccount, data: call.data, to: call.to, value: call.value ? hexToBigInt(call.value) : undefined, }); const hash = await client.sendTransaction({ ...request, gas: (request.gas * GAS_MULTIPLIER) / 100n, }); hashes.push(hash); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") { break; } } // Create a robust ID by concatenating transaction hashes, chain ID, and magic identifier const id = concat([ ...hashes, numberToHex(Number(sendCallsParams.chainId), { size: 32 }), BURNER_MAGIC_IDENTIFIER, ]); return { id, capabilities: { atomic: sendCallsParams.calls.length === 1, }, }; } if (method === "wallet_getCallsStatus") { const [id] = params; // Check if the ID ends with our magic identifier const isTransactions = id.endsWith(BURNER_MAGIC_IDENTIFIER.slice(2)); if (!isTransactions) { throw new Error("Invalid calls ID: missing or incorrect magic identifier"); } // Extract chainId and hashes const chainId = fromHex(slice(id, -64, -32), "number"); const hashesHex = slice(id, 0, -64).slice(2).match(/.{1,64}/g) || []; const hashes = hashesHex.map((hash) => `0x${hash}`); // Get receipts for all transactions const receipts = await Promise.all(hashes.map(async (hash) => { try { const receipt = await publicClient.getTransactionReceipt({ hash }); return { logs: receipt.logs, status: receipt.status === "success" ? "0x1" : "0x0", blockHash: receipt.blockHash, blockNumber: numberToHex(receipt.blockNumber), gasUsed: numberToHex(receipt.gasUsed), transactionHash: receipt.transactionHash, }; } catch (error) { // If we can't get the receipt, the transaction is still pending return null; } })); // Determine status based on receipts const status = receipts.every((r) => r === null) ? 100 // All pending : receipts.some((r) => r === null) ? 600 // Some pending, some complete : receipts.every((r) => r?.status === "0x1") ? 200 // All successful : receipts.every((r) => r?.status === "0x0") ? 500 // All failed : 600; // Mixed results const result = { version: "1.0", chainId: numberToHex(chainId), id, status, atomic: false, // We always execute non-atomically receipts: receipts.filter((r) => r !== null), capabilities: { atomic: { status: "unsupported", }, }, }; return result; } if (method === "wallet_showCallsStatus") { // This is a UI method, we'll just return success return true; } const body = { method, params }; const httpClient = getHttpRpcClient(url); const { error, result } = await httpClient.request({ body }); if (error) throw new RpcRequestError({ body, error, url }); return result; }; return custom({ request })({ retryCount: 0 }); }, onChainChanged(chain) { const chainId = Number(chain); config.emitter.emit("change", { chainId }); }, async getAccounts() { if (!connected) throw new ConnectorNotConnectedError(); const provider = await this.getProvider(); const accounts = await provider.request({ method: "eth_accounts" }); const burnerAddress = accounts.map((x) => getAddress(x))[0]; return [burnerAddress]; }, async onDisconnect() { config.emitter.emit("disconnect"); connected = false; }, async getChainId() { const provider = await this.getProvider(); const hexChainId = await provider.request({ method: "eth_chainId" }); return fromHex(hexChainId, "number"); }, async isAuthorized() { if (!connected) return false; const accounts = await this.getAccounts(); return !!accounts.length; }, onAccountsChanged(accounts) { if (accounts.length === 0) this.onDisconnect(); else config.emitter.emit("change", { accounts: accounts.map((x) => getAddress(x)), }); }, async switchChain({ chainId }) { const provider = await this.getProvider(); const chain = config.chains.find((x) => x.id === chainId); if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()); await provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: numberToHex(chainId) }], }); return chain; }, disconnect() { console.log("disconnect from burnerwallet"); connected = false; return Promise.resolve(); }, })); }; //# sourceMappingURL=burner.js.map