UNPKG

@msquared/etherbase-client

Version:

React hooks for interacting with Etherbase smart contracts

2,007 lines (1,994 loc) 65.1 kB
// src/EtherbaseProvider.tsx import React, { createContext, useContext, useEffect, useState } from "react"; // src/config.ts var config = null; function initializeApp(userConfig) { if (!userConfig.wsReaderUrl && !userConfig.wsWriterUrl && !userConfig.httpReaderUrl) { throw new Error( "wsReaderUrl or wsWriterUrl or httpReaderUrl must be provided in etherbase config" ); } config = userConfig; } function getConfig() { if (!config) { throw new Error( "Etherbase must be initialized with initializeApp() before use, or use the EtherbaseProvider" ); } return config; } // src/EtherbaseProvider.tsx var EtherbaseContext = createContext({ initialized: false }); function EtherbaseProvider({ config: config2, children }) { const [initialized, setInitialized] = useState(false); useEffect(() => { if (!initialized) { initializeApp(config2); setInitialized(true); } }, [config2, initialized]); return /* @__PURE__ */ React.createElement(EtherbaseContext.Provider, { value: { initialized } }, initialized ? children : null); } function useEtherbaseContext() { const context = useContext(EtherbaseContext); if (context === void 0) { throw new Error( "useEtherbaseContext must be used within an EtherbaseProvider" ); } return context; } // src/hooks/useEtherstore.ts import { useEffect as useEffect3, useMemo as useMemo2, useRef, useState as useState4 } from "react"; // src/hooks/WebSocketManager.ts var WebSocketManager = class _WebSocketManager { constructor() { this.ws = null; this.dead = false; /** A map from localId -> SubscriptionStatus. */ this.subscriptions = /* @__PURE__ */ new Map(); this.writerWs = null; this.readerWs = null; const config2 = getConfig(); this.readerUrl = config2.wsReaderUrl; this.writerUrl = config2.wsWriterUrl; this.privateKey = config2.privateKey; } static get() { if (!_WebSocketManager.instance) { _WebSocketManager.instance = new _WebSocketManager(); } return _WebSocketManager.instance; } /** Connect if not open or connecting */ async connectReader() { if (this.dead) return; if (!this.readerUrl) { console.error("[WebSocketManager] Reader URL not set"); return; } if (this.readerWs && (this.readerWs.readyState === WebSocket.OPEN || this.readerWs.readyState === WebSocket.CONNECTING)) { if (this.readerWs.readyState === WebSocket.CONNECTING) { await new Promise((resolve) => { if (!this.readerWs) return; this.readerWs.addEventListener("open", () => resolve(), { once: true }); }); } return; } console.log("[WebSocketManager] Connecting reader..."); const url = new URL(`${this.readerUrl}/read`); if (this.privateKey) { url.searchParams.set("privateKey", this.privateKey); } this.readerWs = new WebSocket(url.toString()); this.readerWs.onopen = () => { console.log("[WebSocketManager] Reader connected."); for (const status of this.subscriptions.values()) { status.subscriptionId = void 0; status.error = void 0; this.sendSubscribe(status); } }; this.readerWs.onmessage = (event) => this.handleMessage(event); this.readerWs.onerror = (err) => { console.error("[WebSocketManager] Reader WebSocket error:", err); }; this.readerWs.onclose = () => { console.warn("[WebSocketManager] Reader WebSocket closed."); this.readerWs = null; }; await new Promise((resolve) => { if (this.readerWs?.readyState === WebSocket.OPEN) { resolve(); } else { this.readerWs?.addEventListener("open", () => resolve(), { once: true }); } }); console.log("[WebSocketManager] Reader connected."); } async connectWriter() { if (this.dead) return; if (!this.writerUrl) { console.error("[WebSocketManager] Writer URL not set"); return; } if (this.writerWs && (this.writerWs.readyState === WebSocket.OPEN || this.writerWs.readyState === WebSocket.CONNECTING)) { if (this.writerWs.readyState === WebSocket.CONNECTING) { await new Promise((resolve) => { if (!this.writerWs) return; this.writerWs.addEventListener("open", () => resolve(), { once: true }); }); } return; } console.log("[WebSocketManager] Connecting writer..."); const url = new URL(`${this.writerUrl}/write`); if (this.privateKey) { url.searchParams.set("privateKey", this.privateKey); } this.writerWs = new WebSocket(url.toString()); this.writerWs.onopen = () => { console.log("[WebSocketManager] Writer connected."); }; this.writerWs.onmessage = (event) => this.handleWriterMessage(event); this.writerWs.onerror = (err) => { console.error("[WebSocketManager] Writer WebSocket error:", err); }; this.writerWs.onclose = () => { console.warn("[WebSocketManager] Writer WebSocket closed."); this.writerWs = null; }; await new Promise((resolve) => { if (this.writerWs?.readyState === WebSocket.OPEN) { resolve(); } else { this.writerWs?.addEventListener("open", () => resolve(), { once: true }); } }); console.log("[WebSocketManager] Writer connected."); } handleMessage(event) { try { const msg = JSON.parse(event.data); switch (msg.type) { case "error": console.error("[WS] Server error:", msg.error); break; case "subscription_success": { const { pendingId, subscriptionId } = msg.data; const sub = this.subscriptions.get(pendingId); if (sub) { sub.subscriptionId = subscriptionId; sub.error = void 0; console.log( `[WS] Subscription success for localId=${pendingId}, subscriptionId=${subscriptionId}` ); } break; } case "subscription_failed": { const { pendingId, error } = msg.data; const sub = this.subscriptions.get(pendingId); if (sub) { sub.error = error; console.warn( `[WS] Subscription failed for localId=${pendingId}: ${error}` ); } break; } case "updates": { console.log("[WS] Got updates:", msg.data); this.handleUpdates(msg.data); break; } case "initial_state": { this.handleUpdates(msg.data); break; } case "transaction_complete": console.log("[WS] transaction_complete:", msg.data); break; case "transaction_failed": console.warn("[WS] transaction_failed:", msg.data); break; case "event_submitted": console.log("[WS] event_submitted"); break; default: console.log("[WS] unhandled message:", msg); break; } } catch (err) { console.error("[WS] Failed to parse message:", err); } } handleWriterMessage(event) { try { const msg = JSON.parse(event.data); switch (msg.type) { case "error": console.error("[WS Writer] Server error:", msg.error); break; case "transaction_complete": console.log("[WS Writer] transaction_complete:", msg.data); break; case "transaction_failed": console.warn("[WS Writer] transaction_failed:", msg.data); break; case "event_submitted": console.log("[WS Writer] event_submitted"); break; default: console.log("[WS Writer] unhandled message:", msg); break; } } catch (err) { console.error("[WS Writer] Failed to parse message:", err); } } handleUpdates({ block, updates }) { for (const update of updates) { const sub = [...this.subscriptions.values()].find( (s) => s.subscriptionId === update.subscriptionId ); if (!sub) { console.warn( "[WS] Got update for unknown subscriptionId:", update.subscriptionId ); continue; } if (update.type === "event" && "onEvent" in sub.subscription) { sub.subscription.onEvent(update.data); } else if (update.type === "state" && "onStateUpdate" in sub.subscription) { sub.subscription.onStateUpdate(update.data); } } } async addEventSubscription(localId, subscription) { this.subscriptions.set(localId, { localId, subscription }); await this.connectReader(); const status = this.subscriptions.get(localId); if (!status) { console.warn( "[WS] Tried to add event subscription for unknown localId:", localId ); return; } this.sendSubscribe(status); } async addStateSubscription(localId, subscription) { this.subscriptions.set(localId, { localId, subscription }); console.log("Adding state subscription", localId, subscription); await this.connectReader(); const status = this.subscriptions.get(localId); if (!status) { console.warn( "[WS] Tried to add state subscription for unknown localId:", localId ); return; } this.sendSubscribe(status); } removeSubscription(localId) { const status = this.subscriptions.get(localId); if (!status) return; if (status.subscriptionId && this.readerWs?.readyState === WebSocket.OPEN) { this.readerWs.send( JSON.stringify({ type: "unsubscribe", data: { subscriptionId: status.subscriptionId } }) ); } this.subscriptions.delete(localId); } sendSubscribe(status) { if (!this.readerWs || this.readerWs.readyState !== WebSocket.OPEN) return; const { localId, subscription } = status; if ("onEvent" in subscription) { this.readerWs.send( JSON.stringify({ type: "subscribe", data: { pendingId: localId, eventSubscription: { contractAddress: subscription.contractAddress, events: subscription.events } } }) ); } else { console.log("Sending state subscription!!!", localId, subscription); this.readerWs.send( JSON.stringify({ type: "subscribe", data: { pendingId: localId, stateSubscription: { contractAddress: subscription.contractAddress, statePath: subscription.statePath, options: subscription.options } } }) ); } } getSubscriptionError(localId) { return this.subscriptions.get(localId)?.error; } forceClose() { console.log("[WebSocketManager] Force closing..."); this.dead = true; if (this.readerWs) { this.readerWs.close(); this.readerWs = null; } if (this.writerWs) { this.writerWs.close(); this.writerWs = null; } } async setValue({ contractAddress, state }) { if (!this.writerWs || this.writerWs.readyState !== WebSocket.OPEN) { await this.connectWriter(); } this.writerWs?.send( JSON.stringify({ type: "set_value", data: { contractAddress, state } }) ); } async emitEvent({ sourceAddress, name, args }) { console.log( "[WebSocketManager] Emitting event...", this.writerWs?.readyState ); if (!this.writerWs || this.writerWs.readyState !== WebSocket.OPEN) { await this.connectWriter(); } this.writerWs?.send( JSON.stringify({ type: "emit_event", data: { contractAddress: sourceAddress, name, args } }) ); } async executeContractMethod({ contractAddress, methodName, args }) { console.log( "[WebSocketManager] Executing contract method...", this.writerWs?.readyState ); if (!this.writerWs || this.writerWs.readyState !== WebSocket.OPEN) { await this.connectWriter(); } this.writerWs?.send( JSON.stringify({ type: "execute_contract_method", data: { contractAddress, methodName, args } }) ); } }; // src/hooks/useEtherbaseSource.ts import { useCallback as useCallback2, useEffect as useEffect2, useState as useState3 } from "react"; import { encodeAbiParameters, keccak256, pad, parseAbi, stringToBytes, toHex } from "viem"; // src/abi/EtherbaseSource.ts var EtherbaseSourceAbi = [ { "inputs": [ { "internalType": "address", "name": "_owner", "type": "address" }, { "internalType": "address", "name": "validator", "type": "address" } ], "stateMutability": "nonpayable", "type": "constructor" }, { "inputs": [], "name": "EventNameAlreadyRegistered", "type": "error" }, { "inputs": [], "name": "EventNameNotRegistered", "type": "error" }, { "inputs": [], "name": "IdentityAlreadyExists", "type": "error" }, { "inputs": [], "name": "IdentityDoesNotExist", "type": "error" }, { "inputs": [], "name": "IncorrectNumberOfTopics", "type": "error" }, { "inputs": [ { "internalType": "enum EtherDatabaseLib.DataType", "name": "dataType", "type": "uint8" } ], "name": "InvalidDataEncoding", "type": "error" }, { "inputs": [ { "internalType": "enum EtherDatabaseLib.DataType", "name": "expected", "type": "uint8" }, { "internalType": "enum EtherDatabaseLib.DataType", "name": "actual", "type": "uint8" } ], "name": "InvalidDataType", "type": "error" }, { "inputs": [], "name": "InvalidIdentity", "type": "error" }, { "inputs": [], "name": "PathNotFound", "type": "error" }, { "inputs": [], "name": "TooManyIndexedargs", "type": "error" }, { "inputs": [], "name": "TooManyTopics", "type": "error" }, { "inputs": [], "name": "Unauthorized", "type": "error" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "string[]", "name": "path", "type": "string[]" }, { "indexed": false, "internalType": "bytes", "name": "data", "type": "bytes" }, { "indexed": false, "internalType": "uint8", "name": "dataType", "type": "uint8" } ], "name": "EthDBPathUpdate", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "wallet", "type": "address" } ], "name": "IdentityCreated", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "wallet", "type": "address" } ], "name": "IdentityDeleted", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "string", "name": "segment", "type": "string" }, { "indexed": true, "internalType": "uint256", "name": "id", "type": "uint256" } ], "name": "NewSegment", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "wallet", "type": "address" }, { "indexed": true, "internalType": "enum RoleControl.Role", "name": "role", "type": "uint8" } ], "name": "RoleGranted", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "wallet", "type": "address" }, { "indexed": true, "internalType": "enum RoleControl.Role", "name": "role", "type": "uint8" } ], "name": "RoleRevoked", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "string", "name": "name", "type": "string" }, { "indexed": false, "internalType": "string", "name": "id", "type": "string" } ], "name": "SchemaRegistered", "type": "event" }, { "inputs": [ { "internalType": "address", "name": "walletAddress", "type": "address" }, { "internalType": "enum RoleControl.Role[]", "name": "initialRoles", "type": "uint8[]" } ], "name": "createWalletIdentity", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "wallet", "type": "address" } ], "name": "deleteIdentity", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "bytes32[]", "name": "ArgumentTopics", "type": "bytes32[]" }, { "internalType": "bytes", "name": "data", "type": "bytes" } ], "name": "emitEvent", "outputs": [ { "internalType": "bool", "name": "success", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "bytes32[]", "name": "argumentTopics", "type": "bytes32[]" }, { "internalType": "bytes", "name": "data", "type": "bytes" } ], "internalType": "struct EtherbaseSource.BatchEmitEvent[]", "name": "events", "type": "tuple[]" } ], "name": "emitEvents", "outputs": [ { "internalType": "bool", "name": "success", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "", "type": "string" } ], "name": "eventSchemas", "outputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "id", "type": "string" }, { "internalType": "bytes32", "name": "eventTopic", "type": "bytes32" }, { "internalType": "uint8", "name": "numIndexedArgs", "type": "uint8" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getAllIdentities", "outputs": [ { "components": [ { "internalType": "address", "name": "walletAddress", "type": "address" }, { "internalType": "enum RoleControl.Role[]", "name": "roles", "type": "uint8[]" } ], "internalType": "struct RoleControl.IdentityView[]", "name": "", "type": "tuple[]" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getAllSegments", "outputs": [ { "internalType": "string[]", "name": "", "type": "string[]" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getAllWallets", "outputs": [ { "internalType": "address[]", "name": "", "type": "address[]" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getEntries", "outputs": [ { "components": [ { "internalType": "bytes", "name": "path", "type": "bytes" }, { "internalType": "enum EtherDatabaseLib.DataType", "name": "dataType", "type": "uint8" }, { "internalType": "bytes", "name": "data", "type": "bytes" }, { "internalType": "bool", "name": "exists", "type": "bool" } ], "internalType": "struct EtherDatabaseLib.Node[]", "name": "", "type": "tuple[]" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "id", "type": "string" } ], "name": "getEventSchemaFromId", "outputs": [ { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "id", "type": "string" }, { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "argType", "type": "string" }, { "internalType": "bool", "name": "isIndexed", "type": "bool" } ], "internalType": "struct Argument[]", "name": "args", "type": "tuple[]" }, { "internalType": "bytes32", "name": "eventTopic", "type": "bytes32" }, { "internalType": "uint8", "name": "numIndexedArgs", "type": "uint8" } ], "internalType": "struct EventSchema", "name": "", "type": "tuple" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "name", "type": "string" } ], "name": "getEventSchemaFromName", "outputs": [ { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "id", "type": "string" }, { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "argType", "type": "string" }, { "internalType": "bool", "name": "isIndexed", "type": "bool" } ], "internalType": "struct Argument[]", "name": "args", "type": "tuple[]" }, { "internalType": "bytes32", "name": "eventTopic", "type": "bytes32" }, { "internalType": "uint8", "name": "numIndexedArgs", "type": "uint8" } ], "internalType": "struct EventSchema", "name": "", "type": "tuple" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "segment", "type": "string" } ], "name": "getOrCreateSegmentId", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "getRegisteredEventNames", "outputs": [ { "internalType": "string[]", "name": "", "type": "string[]" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "segment", "type": "string" } ], "name": "getSegmentId", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "string[]", "name": "segments", "type": "string[]" } ], "name": "getValue", "outputs": [ { "internalType": "enum EtherDatabaseLib.DataType", "name": "", "type": "uint8" }, { "internalType": "bytes", "name": "", "type": "bytes" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "wallet", "type": "address" }, { "internalType": "enum RoleControl.Role", "name": "role", "type": "uint8" } ], "name": "grantRole", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string[]", "name": "segments", "type": "string[]" } ], "name": "hasEntry", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "", "type": "string" } ], "name": "idToName", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "owner", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "id", "type": "string" }, { "internalType": "bytes32", "name": "eventTopic", "type": "bytes32" }, { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "argType", "type": "string" }, { "internalType": "bool", "name": "isIndexed", "type": "bool" } ], "internalType": "struct Argument[]", "name": "args", "type": "tuple[]" } ], "name": "registerEventSchema", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string[]", "name": "segments", "type": "string[]" } ], "name": "removeValue", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "wallet", "type": "address" }, { "internalType": "enum RoleControl.Role", "name": "role", "type": "uint8" } ], "name": "revokeRole", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string[]", "name": "segments", "type": "string[]" }, { "internalType": "enum EtherDatabaseLib.DataType", "name": "dataType", "type": "uint8" }, { "internalType": "bytes", "name": "data", "type": "bytes" } ], "name": "setValue", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "components": [ { "internalType": "string[]", "name": "segments", "type": "string[]" }, { "internalType": "enum EtherDatabaseLib.DataType", "name": "dataType", "type": "uint8" }, { "internalType": "bytes", "name": "data", "type": "bytes" } ], "internalType": "struct EtherDatabaseLib.BatchSetValue[]", "name": "values", "type": "tuple[]" } ], "name": "setValues", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "validator", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" } ]; // src/etherbaseSource.ts async function fetchEventDefinitionsData(sourceAddress, config2) { const { httpReaderUrl } = config2 || getConfig(); const response = await fetch( `${httpReaderUrl}/event-definitions?sourceAddress=${sourceAddress}`, { cache: "no-store" } ); return response.json(); } // src/hooks/useWebThree.tsx import { useCallback, useMemo, useState as useState2 } from "react"; import { createPublicClient, createWalletClient, custom } from "viem"; function useWebThree() { const [walletClient, setWalletClient] = useState2(null); const { chain, privateKey } = getConfig(); const publicClient = useMemo(() => { if (!window.ethereum) { return null; } const pClient = createPublicClient({ chain, transport: custom(window.ethereum) }); return pClient; }, [chain]); const getWalletClient = useCallback(async () => { if (walletClient) { console.log("Already initialized"); return walletClient; } if (!chain) { console.error( "No chain specified in config but is required for write operations" ); return; } if (!window.ethereum) { console.error("No Ethereum provider found"); return; } const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); const account = accounts[0]; const wClient = createWalletClient({ account, transport: custom(window.ethereum), chain }); setWalletClient(wClient); return wClient; }, [walletClient, chain]); return { publicClient, getWalletClient }; } // src/hooks/useEtherbaseSource.ts function useEtherbaseSource({ sourceAddress }) { useEtherbaseContext(); const { httpReaderUrl, wsReaderUrl, wsWriterUrl, useBackend, privateKey } = getConfig(); const { publicClient, getWalletClient } = useWebThree(); const [eventDefinitions, setEventDefinitions] = useState3([]); const getWalletClientInternal = useCallback2(async () => { const walletClient = await getWalletClient(); if (!walletClient) { throw new Error("Wallet client not found"); } return walletClient; }, [getWalletClient]); const executeWriteBrowser = useCallback2( async (writeFunctionName, args) => { if (!publicClient) { throw new Error("Public client not found"); } const walletClient = await getWalletClientInternal(); 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 = useCallback2((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 = useCallback2((event) => { return keccak256( stringToBytes( `${event.name}(${event.args.map((arg) => arg.argType).join(",")})` ) ); }, []); const parseEvent = useCallback2( (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 = useCallback2( ({ eventTopic, numIndexedArgs }) => { return `${eventTopic}:${numIndexedArgs}`; }, [] ); const registerEventBrowser = useCallback2( 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 = useCallback2( async (eventName, args) => { if (!publicClient) { throw new Error("Public client not found"); } 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 = []; if (typeof args !== "object" || Array.isArray(args)) { throw new Error("Parameter values must be an object"); } const parameterValues = eventSchema.args.map( (arg) => args[arg.name] ); if (parameterValues.length !== eventSchema.args.length) { throw new Error("Event arguments/values length mismatch"); } 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 = useCallback2( async ({ name, args }) => { const { parameterTopics, parameterData } = await encodeEvent(name, args); await executeWriteBrowser("emitEvent", [ name, parameterTopics, parameterData ]); }, [encodeEvent, executeWriteBrowser] ); const emitEventBackend = useCallback2( async ({ name, args }) => { if (!privateKey) { throw new Error("No private key configured"); } 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 = useCallback2( 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; ((DataType2) => { DataType2[DataType2["NONE"] = 0] = "NONE"; DataType2[DataType2["STRING"] = 1] = "STRING"; DataType2[DataType2["BOOL"] = 2] = "BOOL"; DataType2[DataType2["UINT256"] = 3] = "UINT256"; DataType2[DataType2["INT256"] = 4] = "INT256"; DataType2[DataType2["BYTES"] = 5] = "BYTES"; })(DataType || (DataType = {})); const setValueBrowser = useCallback2( 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 === void 0) { batchSetValues.push({ segments: currentPath, dataType: 0 /* NONE */, data: "0x" }); } else if (typeof value === "object") { processState(value, currentPath); } else if (typeof value === "string") { batchSetValues.push({ segments: currentPath, dataType: 1 /* STRING */, data: toHex(value) }); } else if (typeof value === "boolean") { batchSetValues.push({ segments: currentPath, dataType: 2 /* 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 ? 3 /* UINT256 */ : 4 /* 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 = useCallback2( async (state) => { if (!privateKey) return; 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 = useCallback2( async (state) => { if (useBackend) { await setValueBackend(state); } else { await setValueBrowser(state); } }, [useBackend, setValueBackend, setValueBrowser] ); const fetchEventDefinitions = useCallback2(async () => { if (!sourceAddress) return; try { const definitions = await fetchEventDefinitionsData(sourceAddress); setEventDefinitions(definitions); } catch (error) { console.error("Error fetching event definitions:", error); } }, [sourceAddress]); useEffect2(() => { fetchEventDefinitions(); const intervalId = setInterval(() => { fetchEventDefinitions(); }, 1e3); return () => clearInterval(intervalId); }, [fetchEventDefinitions]); return { eventDefinitions, fetchEventDefinitions, registerEvent: registerEventBrowser, emitEvent: useBackend ? emitEventBackend : emitEventBrowser, setValue: useBackend ? setValueBackend : setValueBrowser }; } // src/hooks/useEtherstore.ts function deepMerge(target, source) { if (!target) { return source; } if (!source) { return target || {}; } const output = { ...target }; for (const key in source) { console.log("key", key, source[key], target[key]); if (source[key] === null) { delete output[key]; } else if (source[key] instanceof Object && key in target) { output[key] = deepMerge( target[key], source[key] ); } else { output[key] = source[key]; } } return output; } function useEtherstore({ contractAddress, path, options = {}, onStateChange }) { useEtherbaseContext(); const [state, setState] = useState4({}); const [loading, setLoading] = useState4(true); const [error, setError] = useState4(null); const keyRef = useRef(Math.random().toString(36).substring(2, 9)); if (!contractAddress) { throw new Error("Contract address must be provided in path"); } const statePath = useMemo2(() => path, [JSON.stringify(path)]); const optionsMemo = useMemo2(() => options, [JSON.stringify(options)]); const { useBackend } = getConfig(); const { setValue } = useEtherbaseSource({ sourceAddress: contractAddress }); const update = async (state2, merge = true) => { if (!useBackend) { await setValue(state2); return; } console.log("Updating state", contractAddress, state2); try { await WebSocketManager.get().setValue({ contractAddress, state: state2 }); } catch (err) { setError(err instanceof Error ? err.message : String(err)); throw err; } }; useEffect3(() => { async function createSubscription() { try { await WebSocketManager.get().addStateSubscription(keyRef.current, { contractAddress, statePath, options: optionsMemo, onStateUpdate: (update2) => { setLoading(false); setState((prev) => { const mergedState = deepMerge(prev, update2.state); if (onStateChange) { onStateChange(mergedState); } return mergedState; }); } }); } catch (err) { console.error("Error creating subscription", err); setError(err instanceof Error ? err.message : String(err)); setLoading(false); return; } } console.log("Creating subscription", contractAddress, statePath); createSubscription(); return () => { WebSocketManager.get().removeSubscription(keyRef.current); }; }, [contractAddress, statePath, optionsMemo, onStateChange]); return { state, loading, error, update }; } // src/hooks/useEtherbaseEvents.ts import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef2, useState as useState5 } from "react"; function useEtherbaseEvents({ contractAddress, events, onEvent }) { useEtherbaseContext(); const [error, setError] = useState5(null); const localIdRef = useRef2(Math.random().toString(36).substring(2, 15)); const handleEvent = useCallback3( (event) => { try { onEvent(event); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }, [onEvent] ); useEffect4(() => { if (!contractAddress) { setError("No contract address provided"); return; } const localId = localIdRef.current; WebSocketManager.get().addEventSubscription(localId, { contractAddress, events, onEvent: handleEvent }).catch((err) => { setError(err instanceof Error ? err.message : String(err)); }); return () => { WebSocketManager.get().removeSubscription(localId); }; }, [contractAddress, JSON.stringify(events), handleEvent]); return { error }; } // src/hooks/useEtherbase.ts import { useCallback as useCallback4, useEffect as useEffect5, useState as useState6 } from "react"; // src/abi/Etherbase.ts var EtherbaseAbi = [ { "inputs": [ { "internalType": "address", "name": "_validator", "type": "address" } ], "stateMutability": "nonpayable", "type": "constructor" }, { "inputs": [], "name": "ContractAlreadyExists", "type": "error" }, { "inputs": [], "name": "ContractNotFound", "type": "error" }, { "inputs": [], "name": "EmptyContractABI", "type": "error" }, { "inputs": [], "name": "NotAuthorized", "type": "error" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "contractAddress", "type": "address" } ], "name": "CustomContractAdded", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "contractAddress", "type": "address" } ], "name": "CustomContractDeleted", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "contractAddress", "type": "address" }, { "indexed": false, "internalType": "string", "name": "eventName", "type": "string" } ], "name": "CustomContractSchemaAdded", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "sourceAddress", "type": "address" }, { "indexed": true, "internalType": "address", "name": "owner", "type": "address" } ], "name": "SourceCreated", "type": "event" }, { "inputs": [ { "internalType": "address", "name": "contractAddress", "type": "address" }, { "internalType": "string", "name": "contractABI", "type": "string" } ], "name": "addCustomContract", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "createSource", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "customContractAddresses", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "", "type": "address" } ], "name": "customContracts", "outputs": [ { "internalType": "address", "name": "contractAddress", "type": "address" }, { "internalType": "address", "name": "addedBy", "type": "address" }, { "internalType": "string", "name": "contractABI", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "contractAddress", "type": "address" } ], "name": "deleteCustomContract", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "contractAddress", "type": "address" } ], "name": "getCustomContract", "outputs": [ { "components": [ { "intern