UNPKG

@msquared/etherbase-client

Version:

React hooks for interacting with Etherbase smart contracts

317 lines (316 loc) 12.8 kB
"use client"; import { useCallback, useEffect, useState } from "react"; import { encodeAbiParameters, keccak256, pad, parseAbi, stringToBytes, toHex, } from "viem"; import { useEtherbaseContext } from "../EtherbaseProvider"; import { EtherbaseSourceAbi } from "../abi/EtherbaseSource"; import { getConfig } from "../config"; import { fetchEventDefinitionsData } from "../etherbaseSource"; import { WebSocketManager } from "./WebSocketManager"; import useWebThree from "./useWebThree"; export default function useEtherbaseSource({ sourceAddress, }) { useEtherbaseContext(); const { httpReaderUrl, wsReaderUrl, wsWriterUrl, useBackend, privateKey } = getConfig(); const { publicClient, getWalletClient } = useWebThree(); const [eventDefinitions, setEventDefinitions] = useState([]); const getWalletClientInternal = useCallback(async () => { const walletClient = await getWalletClient(); if (!walletClient) { throw new Error("Wallet client not found"); } return walletClient; }, [getWalletClient]); const executeWriteBrowser = useCallback(async (writeFunctionName, args) => { if (!publicClient) { throw new Error("Public client not found"); } const walletClient = await getWalletClientInternal(); // check the function name is valid if (!EtherbaseSourceAbi.find((f) => f.type === "function" && f.name === writeFunctionName)) { throw new Error(`Invalid function name: ${writeFunctionName}`); } const hash = await walletClient.writeContract({ address: sourceAddress, abi: EtherbaseSourceAbi, // @ts-ignore functionName: writeFunctionName, // @ts-ignore args, // currently gas estimation is broken in somnia so hardcoding a high value gas: 7920027n, }); const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log("Execute write receipt:", receipt); }, [sourceAddress, publicClient, getWalletClientInternal]); const getEventAbi = useCallback((event) => { let abi = `event ${event.name}(`; for (const param of event.args) { abi += `${param.argType}`; if (param.isIndexed) { abi += " indexed"; } abi += ` ${param.name}`; abi += ", "; } if (event.args.length > 0) { abi = abi.slice(0, -2); } abi += ")"; return abi; }, []); const getEventTopic = useCallback((event) => { return keccak256(stringToBytes(`${event.name}(${event.args.map((arg) => arg.argType).join(",")})`)); }, []); const parseEvent = useCallback((event) => { const abi = getEventAbi(event); const eventTopic = getEventTopic(event); const eventAbi = parseAbi([abi]); const eventName = eventAbi[0].name; const args = eventAbi[0].inputs.map((input) => ({ name: input.name ?? "", argType: input.type, isIndexed: !!input?.indexed, })); return { eventName, eventTopic, args }; }, [getEventAbi, getEventTopic]); const getEventId = useCallback(({ eventTopic, numIndexedArgs, }) => { return `${eventTopic}:${numIndexedArgs}`; }, []); const registerEventBrowser = useCallback(async (event) => { const { eventName, eventTopic, args } = parseEvent(event); const id = getEventId({ eventTopic, numIndexedArgs: args.filter((arg) => arg.isIndexed).length, }); console.log("registering event", eventName, id, eventTopic, args); await executeWriteBrowser("registerEventSchema", [ eventName, id, eventTopic, args, ]); }, [executeWriteBrowser, getEventId, parseEvent]); const encodeEvent = useCallback(async (eventName, args) => { if (!publicClient) { throw new Error("Public client not found"); } // fetch schema from the contract const eventSchema = await publicClient.readContract({ address: sourceAddress, abi: EtherbaseSourceAbi, functionName: "getEventSchemaFromName", args: [eventName], }); if (!eventSchema) { throw new Error(`Event schema not found for ${eventName}`); } const topics = []; const dataTypes = []; const dataValues = []; // Validate number of parameters // Ensure parameterValues is an object if (typeof args !== "object" || Array.isArray(args)) { throw new Error("Parameter values must be an object"); } // Convert parameter values object to array const parameterValues = eventSchema.args.map((arg) => args[arg.name]); if (parameterValues.length !== eventSchema.args.length) { throw new Error("Event arguments/values length mismatch"); } // Process each parameter eventSchema.args.forEach((arg, index) => { const value = parameterValues[index]; if (arg.isIndexed) { if (arg.argType === "string") { topics.push(keccak256(stringToBytes(value))); } else if (arg.argType.startsWith("bytes")) { topics.push(keccak256(value)); } else if (arg.argType === "address") { topics.push(pad(value, { size: 32 })); } else if (arg.argType === "uint256" || arg.argType === "int256") { topics.push(pad(toHex(value), { size: 32 })); } else if (arg.argType === "bool") { topics.push(pad(value ? "0x01" : "0x00", { size: 32 })); } else { topics.push(pad(toHex(value), { size: 32 })); } } else { dataTypes.push(arg.argType); dataValues.push(value); } }); const data = encodeAbiParameters(dataTypes.map((type) => ({ type })), dataValues); return { parameterTopics: topics, parameterData: data }; }, [publicClient, sourceAddress]); const emitEventBrowser = useCallback(async ({ name, args }) => { const { parameterTopics, parameterData } = await encodeEvent(name, args); await executeWriteBrowser("emitEvent", [ name, parameterTopics, parameterData, ]); }, [encodeEvent, executeWriteBrowser]); const emitEventBackend = useCallback(async ({ name, args }) => { if (!privateKey) { throw new Error("No private key configured"); } // Use WebSocket if available, fallback to HTTP if (wsWriterUrl) { WebSocketManager.get().emitEvent({ sourceAddress, name, args, }); } else if (httpReaderUrl) { const response = await fetch(`${httpReaderUrl}/emit-event`, { method: "POST", body: JSON.stringify({ sourceAddress, name, args }), headers: { "x-private-key": privateKey, }, }); const data = await response.json(); console.log("Emit event response:", data); } else { throw new Error("No backend URL configured"); } }, [privateKey, httpReaderUrl, wsWriterUrl, sourceAddress]); const emitEvent = useCallback(async ({ name, args }) => { if (useBackend) { console.log("emitting event backend", name, args); await emitEventBackend({ name, args }); } else { console.log("emitting event browser", name, args); await emitEventBrowser({ name, args }); } }, [useBackend, emitEventBackend, emitEventBrowser]); let DataType; (function (DataType) { DataType[DataType["NONE"] = 0] = "NONE"; DataType[DataType["STRING"] = 1] = "STRING"; DataType[DataType["BOOL"] = 2] = "BOOL"; DataType[DataType["UINT256"] = 3] = "UINT256"; DataType[DataType["INT256"] = 4] = "INT256"; DataType[DataType["BYTES"] = 5] = "BYTES"; })(DataType || (DataType = {})); const setValueBrowser = useCallback(async (state) => { const batchSetValues = []; const processState = (obj, path = []) => { for (const [key, value] of Object.entries(obj)) { const currentPath = path.length === 0 ? [key] : [...path, key]; console.log("currentPath", currentPath); if (value === null || value === undefined) { batchSetValues.push({ segments: currentPath, dataType: DataType.NONE, data: "0x", }); } else if (typeof value === "object") { processState(value, currentPath); } else if (typeof value === "string") { batchSetValues.push({ segments: currentPath, dataType: DataType.STRING, data: toHex(value), }); } else if (typeof value === "boolean") { batchSetValues.push({ segments: currentPath, dataType: DataType.BOOL, data: value ? "0x01" : "0x00", }); } else if (typeof value === "number") { if (Number.isInteger(value)) { const hexValue = toHex(BigInt(value), { size: 32 }); batchSetValues.push({ segments: currentPath, dataType: value >= 0 ? DataType.UINT256 : DataType.INT256, data: hexValue, }); } else { throw new Error("Only integer numbers are supported"); } } else { throw new Error(`Unsupported value type: ${typeof value}`); } } }; console.log("state", state); processState(state); console.log("batchSetValues", batchSetValues); await executeWriteBrowser("setValues", [batchSetValues]); }, [executeWriteBrowser]); const setValueBackend = useCallback(async (state) => { if (!privateKey) return; // Use WebSocket if available, fallback to HTTP if (wsWriterUrl) { WebSocketManager.get().setValue({ contractAddress: sourceAddress, state, }); } else if (httpReaderUrl) { const response = await fetch(`${httpReaderUrl}/set-value`, { method: "POST", body: JSON.stringify({ state }), headers: { "x-private-key": privateKey, }, }); const data = await response.json(); console.log("Set value response:", data); } else { throw new Error("No backend URL configured"); } }, [privateKey, httpReaderUrl, wsWriterUrl, sourceAddress]); const setValue = useCallback(async (state) => { if (useBackend) { await setValueBackend(state); } else { await setValueBrowser(state); } }, [useBackend, setValueBackend, setValueBrowser]); const fetchEventDefinitions = useCallback(async () => { if (!sourceAddress) return; try { const definitions = await fetchEventDefinitionsData(sourceAddress); setEventDefinitions(definitions); } catch (error) { console.error("Error fetching event definitions:", error); } }, [sourceAddress]); // Poll for event definitions every second useEffect(() => { fetchEventDefinitions(); const intervalId = setInterval(() => { fetchEventDefinitions(); }, 1000); return () => clearInterval(intervalId); }, [fetchEventDefinitions]); return { eventDefinitions, fetchEventDefinitions, registerEvent: registerEventBrowser, emitEvent: useBackend ? emitEventBackend : emitEventBrowser, setValue: useBackend ? setValueBackend : setValueBrowser, }; }