diff --git a/package-lock.json b/package-lock.json index aafc0e1..4d67344 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "copy-to-clipboard": "^3.3.3", "ethers": "^6.9.0", "fhevmjs": "^0.2.0", + "isomorphic-ws": "^5.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.0" @@ -2635,6 +2636,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/javascript-natural-sort": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", @@ -5303,6 +5312,12 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "requires": {} + }, "javascript-natural-sort": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", diff --git a/package.json b/package.json index e5dc2cb..8de65cd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "copy-to-clipboard": "^3.3.3", "ethers": "^6.9.0", "fhevmjs": "^0.2.0", + "isomorphic-ws": "^5.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.0" diff --git a/src/fhevmjs.ts b/src/fhevmjs.ts index 7a2c984..ccb759f 100644 --- a/src/fhevmjs.ts +++ b/src/fhevmjs.ts @@ -17,11 +17,9 @@ export const createFhevmInstance = async (account: string) => { to: '0x000000000000000000000000000000000000005d', data: '0xd9d47bb001', }); - const strKP = getStorage(account); const keypairs: ExportedContractKeypairs | undefined = strKP ? JSON.parse(strKP) : undefined; instances[account] = await createInstance({ chainId, publicKey, keypairs }); - console.log(publicKey); }; const getStorageKey = (account: string) => { diff --git a/src/modules/game/components/Game/Game.tsx b/src/modules/game/components/Game/Game.tsx index 8c431bd..82ab310 100644 --- a/src/modules/game/components/Game/Game.tsx +++ b/src/modules/game/components/Game/Game.tsx @@ -117,23 +117,26 @@ export const Game = ({ account, provider }: GameProps) => { console.log('on'); void refreshInformations(); void refreshPlayers(); - const gameContract = getEventContract(contract); - void gameContract.on(gameContract.filters.GameOpen, gameHasBeenOpen); - void gameContract.on(gameContract.filters.GameStart, gameHasStarted); - void gameContract.on(gameContract.filters.PlayerNameChanged, onPlayerNameChanged); - void gameContract.on(gameContract.filters.PlayerJoined, onPlayerJoined); - void gameContract.on(gameContract.filters.PlayerKicked, onPlayerLeave); - void gameContract.on(gameContract.filters.GoodGuysWin, onGoodGuysWin); - void gameContract.on(gameContract.filters.BadGuysWin, onBadGuysWin); + void getEventContract(contract).then((gameContract) => { + void gameContract.on(gameContract.filters.GameOpen, gameHasBeenOpen); + void gameContract.on(gameContract.filters.GameStart, gameHasStarted); + void gameContract.on(gameContract.filters.PlayerNameChanged, onPlayerNameChanged); + void gameContract.on(gameContract.filters.PlayerJoined, onPlayerJoined); + void gameContract.on(gameContract.filters.PlayerKicked, onPlayerLeave); + void gameContract.on(gameContract.filters.GoodGuysWin, onGoodGuysWin); + void gameContract.on(gameContract.filters.BadGuysWin, onBadGuysWin); + }); return () => { console.log('off'); - void gameContract.off(gameContract.filters.GameOpen, gameHasBeenOpen); - void gameContract.off(gameContract.filters.GameStart, gameHasStarted); - void gameContract.off(gameContract.filters.PlayerNameChanged, onPlayerNameChanged); - void gameContract.off(gameContract.filters.PlayerJoined, onPlayerJoined); - void gameContract.off(gameContract.filters.PlayerKicked, onPlayerLeave); - void gameContract.off(gameContract.filters.GoodGuysWin, onGoodGuysWin); - void gameContract.off(gameContract.filters.BadGuysWin, onBadGuysWin); + void getEventContract(contract).then((gameContract) => { + void gameContract.off(gameContract.filters.GameOpen, gameHasBeenOpen); + void gameContract.off(gameContract.filters.GameStart, gameHasStarted); + void gameContract.off(gameContract.filters.PlayerNameChanged, onPlayerNameChanged); + void gameContract.off(gameContract.filters.PlayerJoined, onPlayerJoined); + void gameContract.off(gameContract.filters.PlayerKicked, onPlayerLeave); + void gameContract.off(gameContract.filters.GoodGuysWin, onGoodGuysWin); + void gameContract.off(gameContract.filters.BadGuysWin, onBadGuysWin); + }); }; } }, [contract]); diff --git a/src/modules/game/components/Table/Table.tsx b/src/modules/game/components/Table/Table.tsx index d85f694..2d34f0b 100644 --- a/src/modules/game/components/Table/Table.tsx +++ b/src/modules/game/components/Table/Table.tsx @@ -69,12 +69,15 @@ export const Table = ({ contract, account, players }: TableProps) => { }; if (contract) { - const gameContract = getEventContract(contract); - void gameContract.on(gameContract.filters.CardPicked, onCardPicked); - void gameContract.on(gameContract.filters.GoodDeal, onGoodDeal); - return () => { - void gameContract.off(gameContract.filters.CardPicked, onCardPicked); + void getEventContract(contract).then((gameContract) => { + void gameContract.on(gameContract.filters.CardPicked, onCardPicked); void gameContract.on(gameContract.filters.GoodDeal, onGoodDeal); + }); + return () => { + void getEventContract(contract).then((gameContract) => { + void gameContract.off(gameContract.filters.CardPicked, onCardPicked); + void gameContract.on(gameContract.filters.GoodDeal, onGoodDeal); + }); }; } }, [contract, refresh]); diff --git a/src/utils/rpc.ts b/src/utils/rpc.ts index 55b762b..f794091 100644 --- a/src/utils/rpc.ts +++ b/src/utils/rpc.ts @@ -1,4 +1,91 @@ -import { Contract, JsonRpcProvider, WebSocketProvider } from 'ethers'; +import { Contract, JsonRpcProvider, SocketBlockSubscriber, WebSocketProvider } from 'ethers'; +import WebSocket from 'isomorphic-ws'; + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +// Testing WebSocket on connection / reconnection before initiating new provider to prevent deadlock +const testWS = async (url: string, reconnectDelay = 100): Promise<{ chainId: number; block: { number: number } }> => { + const MAX_RETRY = 5; + let retry = 0; + let errorObject; + + while (retry < MAX_RETRY + 1) { + try { + return await new Promise((resolve, reject) => { + const socket = new WebSocket(url); + + socket.onopen = () => { + socket.send( + JSON.stringify([ + { + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + id: 1, + }, + { + jsonrpc: '2.0', + method: 'eth_getBlockByNumber', + params: ['latest', false], + id: 2, + }, + ]), + ); + }; + + socket.onmessage = (event: any) => { + const data = JSON.parse(event.data); + + resolve({ + chainId: Number(data[0]?.result), + block: data[1]?.result, + }); + }; + + socket.onerror = (e: Error) => { + reject(e); + }; + }); + } catch (e) { + console.log(`Connection to ${url} failed, attempt: (${retry} of ${MAX_RETRY})`); + await sleep(reconnectDelay); + errorObject = e; + retry++; + } + } + + throw errorObject; +}; + +const connectWS = async (url: string, reconnectDelay = 100) => { + // Test websocket connection to prevent WebSocketProvider deadlock caused by await this._start(); + const { chainId, block } = await testWS(url, reconnectDelay); + console.log(`WebSocket ${url} connected: Chain ${chainId} Block ${Number(block?.number)}`); + + const provider = new WebSocketProvider(url); + const blockSub = new SocketBlockSubscriber(provider); + + (provider.websocket as any).onclose = () => { + console.log(`Socket ${url} is closed, reconnecting in ${reconnectDelay} ms`); + setTimeout(() => connectWS(url, reconnectDelay), reconnectDelay); + }; + + provider.websocket.onerror = (e: Error) => { + console.error(`Socket ${url} encountered error, reconnecting it:\n${e.stack || e.message}`); + blockSub.stop(); + void provider.destroy(); + }; + + blockSub._handleMessage = (result) => { + console.log((provider as any)._wrapBlock({ ...result, transactions: [] })); + }; + blockSub.start(); + + void provider.on('pending', (tx: string) => { + console.log(`New pending tx: ${tx}`); + }); + return provider; +}; // const JSONRPC_URL = 'http://localhost:8545/'; // const WEBSOCKET_URL = 'ws://localhost:8546'; @@ -6,22 +93,22 @@ const JSONRPC_URL = 'https://devnet.zama.ai/'; const WEBSOCKET_URL = 'wss://devnet.ws.zama.ai/'; const jsonProvider = new JsonRpcProvider(JSONRPC_URL); -const wsProvider = new WebSocketProvider(WEBSOCKET_URL); +const wsProvider = connectWS(WEBSOCKET_URL); const getJsonProvider = () => { return jsonProvider; }; -const getWsProvider = () => { - return wsProvider; +const getWsProvider = async () => { + return await wsProvider; }; export const getReadContract = (contract: Contract): Contract => { return contract.connect(getJsonProvider()) as Contract; }; -export const getEventContract = (contract: Contract): Contract => { - return contract.connect(getWsProvider()) as Contract; +export const getEventContract = async (contract: Contract): Promise => { + return contract.connect(await getWsProvider()) as Contract; }; export const onNextBlock = (fn: () => void | Promise) => {