From ce5603240518d84749c47cecade902aec5f6326c Mon Sep 17 00:00:00 2001 From: Scotty <66335769+ScottyPoi@users.noreply.github.com> Date: Wed, 18 Oct 2023 20:06:12 -0600 Subject: [PATCH] UI: Implement Procedures and Interface (#481) * define procedure for talkResp subscription * define procudure for ContentAdded subscription * import subscription procudures from file * define procedure for NodeAdded subscription * add sub for utp events * add new methods to router * Fix Ping Button * build trpc schema for portal_history rpc endpoints * define trpc procedures for standard rpc methods * externalize procudures code into separate files * rpc: debug ts-errors * comment out buggy nodeAddr code * portalnetwork: add option to enable tickertape events * portalnetwork: enable tickertape event log for talkReq and talkResp * util: create utility function to format content to JSON * implement basic rpc methods for WS client * cleanup subscription procedures * use message interfaces from discv5 * small fixes * edit ws procedures * add decodeENR trpc endpoint * ui: create initial app state for both clients * ui: update mock router * cleanup app.tsx * cleanup tabs.tsx * delete unused * clean up NodeInfo.tsx * cleanup FunctionTabs.tsx * update ping tab * create message logs component * create peer message logs component * create content store tab * create RPC call interface * create enr helper * small fixes/updates to rpc input * update dependencies * fix lint error * commit lockfile * enable RPC method send and logging * move method definitions to rpcstate * set methods in client state * fix websocket error * App improvements * update http client functions * debug routingtable and bootnodes calls * debug client components * edit react components * include the rest of http client methods * add README for UI * edit README * edit README * cleanup * rename option to eventLog * edit readme * move ui interface cli files to cli/ui * remove fake test peer * remove FakeKey tester * fix server script * cleanup port component * add icon to button * fix list key error * fix websocket close error * Nits * enable event log in portal constructor * log messages to browser console * only update node info at component load * fix RPC action * quiet liveness check log * debug rpc menu * debug messagelog and bootnodes * default to only http * disable log tab for http * relocate UI server to UI package * copy utils.ts over to server directory * update package.json files for CLI and UI * Start WS Client via procedure * update README.md --------- Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com> --- package-lock.json | 25 +- packages/cli/package.json | 12 +- packages/cli/scripts/sendOffer.ts | 55 +- packages/cli/src/rpc/modules/portal.ts | 52 +- packages/cli/src/server.ts | 381 ------------- packages/cli/src/util.ts | 62 +- packages/portalnetwork/src/client/client.ts | 12 +- packages/portalnetwork/src/client/types.ts | 3 + .../src/subprotocols/history/history.ts | 7 +- .../src/subprotocols/protocol.ts | 2 +- packages/ui/README.md | 88 +++ packages/ui/package.json | 14 +- packages/ui/src/App.tsx | 32 +- packages/ui/src/Clients/HTTPClient.tsx | 178 ++---- packages/ui/src/Clients/WSSClient.tsx | 92 +-- packages/ui/src/Components/BootNodes.tsx | 81 ++- packages/ui/src/Components/Client.tsx | 145 ++++- packages/ui/src/Components/ContentStore.tsx | 358 ++++++++++++ packages/ui/src/Components/FunctionTabs.tsx | 143 +++-- packages/ui/src/Components/LookupContent.tsx | 4 +- packages/ui/src/Components/MessageLogs.tsx | 303 ++++++++++ packages/ui/src/Components/NodeInfo.tsx | 201 ++++++- .../ui/src/Components/PeerMessageLogs.tsx | 127 +++++ packages/ui/src/Components/Ping.tsx | 131 ++++- packages/ui/src/Components/Port.tsx | 93 +++ packages/ui/src/Components/RPC.tsx | 351 ++++++++++++ packages/ui/src/Components/RPCInput.tsx | 531 +++++++++++++++++ packages/ui/src/Components/RPCParams.tsx | 141 +++++ packages/ui/src/Components/Start.tsx | 37 ++ packages/ui/src/Components/Tabs.tsx | 59 +- .../ui/src/Contexts/AllClientsContext.tsx | 26 +- packages/ui/src/Contexts/ClientContext.tsx | 192 ++++++- packages/ui/src/Contexts/RPCContext.tsx | 205 +++++++ packages/ui/src/server/procedures.ts | 409 +++++++++++++ packages/ui/src/server/rpc/procedures.ts | 348 ++++++++++++ packages/ui/src/server/rpc/trpcTypes.ts | 146 +++++ packages/ui/src/server/server.ts | 222 ++++++++ packages/ui/src/server/subTypes.ts | 9 + packages/ui/src/server/subscriptions.ts | 253 +++++++++ packages/ui/src/util.ts | 114 ++++ packages/ui/src/utils/enr.ts | 56 ++ packages/ui/src/utils/router.ts | 535 +++++++++++++++++- 42 files changed, 5357 insertions(+), 878 deletions(-) delete mode 100644 packages/cli/src/server.ts create mode 100644 packages/ui/README.md create mode 100644 packages/ui/src/Components/ContentStore.tsx create mode 100644 packages/ui/src/Components/MessageLogs.tsx create mode 100644 packages/ui/src/Components/PeerMessageLogs.tsx create mode 100644 packages/ui/src/Components/Port.tsx create mode 100644 packages/ui/src/Components/RPC.tsx create mode 100644 packages/ui/src/Components/RPCInput.tsx create mode 100644 packages/ui/src/Components/RPCParams.tsx create mode 100644 packages/ui/src/Components/Start.tsx create mode 100644 packages/ui/src/Contexts/RPCContext.tsx create mode 100644 packages/ui/src/server/procedures.ts create mode 100644 packages/ui/src/server/rpc/procedures.ts create mode 100644 packages/ui/src/server/rpc/trpcTypes.ts create mode 100644 packages/ui/src/server/server.ts create mode 100644 packages/ui/src/server/subTypes.ts create mode 100644 packages/ui/src/server/subscriptions.ts create mode 100644 packages/ui/src/util.ts create mode 100644 packages/ui/src/utils/enr.ts diff --git a/package-lock.json b/package-lock.json index d7a7e681f..e98b4d932 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7390,7 +7390,8 @@ "integrity": "sha512-9s8/kwo2IDB5hwB2SKZZrfevRhdb1f9fdXtIYd3lbQuf2jQaC/LyQuHaIQjDQoUx9updBfsHXcFFPiCP1DL6pg==", "funding": [ "https://trpc.io/sponsor" - ] + ], + "peer": true }, "node_modules/@tsconfig/node10": { "version": "1.0.9", @@ -20292,9 +20293,9 @@ } }, "node_modules/zod": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", - "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -20427,20 +20428,14 @@ "@lodestar/params": "^1.9.2", "@lodestar/types": "^1.9.2", "@multiformats/multiaddr": "^11.0.0", - "@trpc/server": "10.38.3", - "cors": "2.8.5", "debug": "^4.3.3", "jayson": "^4.0.0", - "json-bigint": "^1.0.0", "level": "^8.0.0", "portalnetwork": "^0.0.1", "prom-client": "^14.0.1", - "ws": "^8.14.1", - "yargs": "^17.3.0", - "zod": "^3.0.0" + "yargs": "^17.3.0" }, "devDependencies": { - "@types/cors": "2.8.14", "@types/debug": "^4.1.7", "@types/node": "18.11.18", "@types/tape": "^4.13.2", @@ -20478,6 +20473,8 @@ "version": "10.38.2", "extraneous": true, "dependencies": { + "@ethereumjs/block": "^5.0.0", + "@ethereumjs/util": "^9.0.0", "@tanstack/react-query": "^4.18.0", "@trpc/client": "^10.38.2", "@trpc/react-query": "^10.38.2", @@ -20623,6 +20620,8 @@ "@trpc/react-query": "^10.38.2", "@trpc/server": "^10.38.2", "ajv-formats": "^2.1.1", + "base64url": "^3.0.1", + "cors": "2.8.5", "fuzzysort": "^2.0.4", "json-bigint": "^1.0.0", "path": "^0.12.7", @@ -20633,6 +20632,8 @@ "superjson": "^1.13.1", "trpc-panel": "^1.3.4", "url": "^0.11.0", + "ws": "^8.14.1", + "zod": "^3.0.0", "zod-to-json-schema": "^3.20.0", "zustand": "^4.4.1" }, @@ -20642,11 +20643,13 @@ "@hookform/resolvers": "^2.9.10", "@mui/icons-material": "^5.10.16", "@mui/material": "^5.10.16", + "@types/cors": "2.8.14", "@types/json-bigint": "^1.0.2", "@types/react": "^18.2.8", "@types/react-dom": "^18.2.4", "@vitejs/plugin-react": "^3.1.0", "eslint": "^8.40.0", + "tsx": "^3.12.7", "typescript": "^5.1.3", "vite": "^4.1.2" } diff --git a/packages/cli/package.json b/packages/cli/package.json index cbac62be1..50a30852d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -8,7 +8,6 @@ "node": "^18" }, "devDependencies": { - "@types/cors": "2.8.14", "@types/debug": "^4.1.7", "@types/node": "18.11.18", "@types/tape": "^4.13.2", @@ -36,7 +35,6 @@ "@multiformats/multiaddr": "^11.0.0", "debug": "^4.3.3", "jayson": "^4.0.0", - "json-bigint": "^1.0.0", "level": "^8.0.0", "portalnetwork": "^0.0.1", "prom-client": "^14.0.1", @@ -44,16 +42,12 @@ "@lodestar/config": "^1.9.2", "@lodestar/light-client": "^1.11.0", "@lodestar/params": "^1.9.2", - "@lodestar/types": "^1.9.2", - "@trpc/server": "10.38.3", - "cors":"2.8.5", - "zod": "^3.0.0", - "ws":"^8.14.1" + "@lodestar/types": "^1.9.2" }, "scripts": { "start": "ts-node --esm src/index.ts", - "server:dev": "tsx watch src/server.ts", - "server:start": "node dist/server.js", + "server:dev": "tsx watch src/server/server.ts", + "server:start": "node dist/server/server.js", "devnet": "npx ts-node --esm scripts/devnet.ts ", "dev": "npx nodemon --esm src/index.ts -- --bindAddress=127.0.0.1:9000 --pk=0x0a27002508021221031947fd30ff7c87d8c7ff2c0ad1515624d247970f946efda872e884a432abb634122508021221031947fd30ff7c87d8c7ff2c0ad1515624d247970f946efda872e884a432abb6341a2408021220456aad29a26c39bf438813d30bb3f0730b8b776ebc4cb0721a3d9a5b3955380e --dataDir='./dist/data'", "build": "tsc && cp bootnodes.txt ./dist", diff --git a/packages/cli/scripts/sendOffer.ts b/packages/cli/scripts/sendOffer.ts index 0a74ad74b..65afbed5c 100644 --- a/packages/cli/scripts/sendOffer.ts +++ b/packages/cli/scripts/sendOffer.ts @@ -1,5 +1,5 @@ import jayson from 'jayson/promise/index.js' -import { ProtocolId, fromHexString, getContentKey, toHexString } from 'portalnetwork' +import { ENR, ProtocolId, fromHexString, getContentKey, toHexString } from 'portalnetwork' import { Block, BlockData, BlockHeader } from '@ethereumjs/block' const blocks = { @@ -45,23 +45,46 @@ const blocks = { }, } -const main = async () => { - const nodeA = jayson.Client.http({ host: '127.0.0.1', port: 8545 }) - const nodeAEnr = await nodeA.request('discv5_nodeInfo', []) - console.log(nodeAEnr) +// Block number: 1 +const blockHeaderContent_key = + '0x0088e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6' +const blockHeaderContent_value = + '0x080000001c020000f90211a0d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479405a56e2d52c817161883f50c441c3228cfe54d9fa0d67e4d450343046425ae4271474353857ab860dbc0a1dde64b41b5cd3a532bf3a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008503ff80000001821388808455ba422499476574682f76312e302e302f6c696e75782f676f312e342e32a0969b900de27b6ac6a67742365dd65f55a0526c41fd18e1b16f1a1215c2e66f5988539bd4979fef1ec401000080ff0700000000000000000000000000000000000000000000000000000023d6398abe4eba641e97a075b30780c12ebe18b24e83a9a9c7bdd94a910cf749bb6bb61aeab6bc5786067f7432bad790642b578881460279ad773a8191596c3087811c70634dbf2ea3abb7199cb5638713844db315d63467f40b5d38eeb884ddcb57866840a050f634417365e9515cd5e6826038ceb45659d85365cfcfceb7a6e9886aaff50b16b6af2bc3bde8b7e701b2cb5022ba49cac9d6c456834e692772b12acf7af78a8375b80ef177c9ad743a14ff0d4935f9ac105444fd57f802fed32495bab257b9585a149a7de4ac53eda7b6df7b9dac7f92325ba05eb1e6b588202048719c250620f4bfa71307470d6c835156db527294c6e6004f9de0c3595a7f1df43427c770506e7e3ca5d021f065544c6ba191d8ffc5fc0805b805d301c926c183ed9ec7e467b962e2304fa7945b6b18042dc2a53cb62b27b28af50fc06db5da2f83bd479f3719b9972fc723c69e4cd13877dcf7cc2a919a95cdf5d7805d9bd9a9f1fbf7a880d82ba9d7af9ed554ce01ea778db5d93d0665ca4fee11f4f873b0b1b58ff1337769b6ee458316030aeac65a5aab68d60fbf214bd44455f892260020000000000000000000000000000000000000000000000000000000000000' +const blockBodyContent_key = '0x0188e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6' +const blockBodyContent_value = '0x0800000008000000c0' - const nodeB = jayson.Client.http({ host: '127.0.0.1', port: 8545 }) - const nodeBEnr = await nodeB.request('discv5_nodeInfo', []) - console.log(nodeBEnr) +const main = async () => { + const nodeA = jayson.Client.http({ host: '192.168.86.29', port: 8545 }) + // const nodeAEnr = await nodeA.request('discv5_nodeInfo', []) + // console.log(nodeAEnr) - for (const [hash, block] of Object.entries(blocks)) { - await nodeA.request('ultralight_addBlockToHistory', [hash, block.rlp]) + // const nodeB = jayson.Client.http({ host: '127.0.0.1', port: 8545 }) + const nodeB = 'enr:-I24QLH4ZiTtmYzqPve_jx0_yNoEJWRLqe6Ds0a0e253TO5BIkLk0XZT0KN2obLDEh2vWAVLkerfOJP32hSvuFjsgbcEY4d1IDAuMC4xgmlkgnY0gmlwhMCoVh2Jc2VjcDI1NmsxoQLrLhwfPIrdoMeH9KPXEhmFoog3wGVbWhuXV33d0tnZl4N1ZHCCIWI' + const nodeBEnr = { + result: { + enr: nodeB, + nodeId: ENR.decodeTxt(nodeB).nodeId, + }, } - const blockHashes = Object.keys(blocks) + // console.log(nodeBEnr) - blockHashes.push(...blockHashes) + // for (const [hash, block] of Object.entries(blocks)) { + // await nodeA.request('ultralight_addBlockToHistory', [hash, block.rlp]) + // } + // const blockHashes = Object.keys(blocks) - const types = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + // blockHashes.push(...blockHashes) + + // const types = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + + await nodeA.request('portal_historyStore', [ + blockHeaderContent_key, + blockHeaderContent_value, + ]) + await nodeA.request('portal_historyStore', [ + blockBodyContent_key, + blockBodyContent_value, + ]) const ping = await nodeA.request('portal_historyPing', [ nodeBEnr.result.enr, @@ -71,9 +94,9 @@ const main = async () => { console.log(ping) const offer = await nodeA.request('portal_historyOffer', [ - nodeBEnr.result.nodeId.slice(2), - blockHashes.map((hash, idx) => getContentKey(idx < 10 ? 0 : 1, fromHexString(hash))), - types, + nodeBEnr.result.enr, + blockBodyContent_key, + blockBodyContent_value ]) console.log(offer) diff --git a/packages/cli/src/rpc/modules/portal.ts b/packages/cli/src/rpc/modules/portal.ts index 72846154d..db95fd83d 100644 --- a/packages/cli/src/rpc/modules/portal.ts +++ b/packages/cli/src/rpc/modules/portal.ts @@ -26,6 +26,7 @@ import { import { GetEnrResult } from '../schema/types.js' import { isValidId } from '../util.js' import { middleware, validators } from '../validators.js' +import { Multiaddr } from '@multiformats/multiaddr' const methods = [ 'portal_historyRoutingTableInfo', @@ -162,6 +163,23 @@ export class portal { [validators.hex], ]) } + + async sendPortalNetworkResponse( + nodeId: string, + socketAddr: Multiaddr, + requestId: bigint, + payload: Uint8Array, + ) { + this._client.sendPortalNetworkResponse( + { + nodeId, + socketAddr, + }, + BigInt(requestId), + payload, + ) + } + async methods() { return methods } @@ -307,10 +325,11 @@ export class portal { }) this.logger.extend('PONG')(`Sent to ${shortId(enr.nodeId)}`) try { - await this._client.sendPortalNetworkResponse( - { nodeId: enr.nodeId, socketAddr: enr.getLocationMultiaddr('udp')! }, + await this.sendPortalNetworkResponse( + enr.nodeId, + enr.getLocationMultiaddr('udp')!, BigInt(requestId), - Buffer.from(pongMsg), + pongMsg, ) } catch { return false @@ -364,13 +383,11 @@ export class portal { selector: MessageCodes.NODES, value: nodesPayload, }) - this._client.sendPortalNetworkResponse( - { - nodeId: dstId, - socketAddr: enr.getLocationMultiaddr('udp')!, - }, + this.sendPortalNetworkResponse( + dstId, + enr.getLocationMultiaddr('udp')!, BigInt(requestId), - Uint8Array.from(encodedPayload), + encodedPayload, ) return enrs.length > 0 ? 1 : 0 @@ -453,10 +470,11 @@ export class portal { value: fromHexString(content), }) const enr = this._history.routingTable.getWithPending(nodeId)?.value - this._client.sendPortalNetworkResponse( - { nodeId, socketAddr: enr?.getLocationMultiaddr('udp')! }, + this.sendPortalNetworkResponse( + nodeId, + enr?.getLocationMultiaddr('udp')!, enr!.seq, - Buffer.concat([Buffer.from([MessageCodes.CONTENT]), Buffer.from(payload)]), + Uint8Array.from(Buffer.concat([Buffer.from([MessageCodes.CONTENT]), Buffer.from(payload)])), ) return '0x' + enr!.seq.toString(16) } @@ -532,13 +550,11 @@ export class portal { selector: MessageCodes.ACCEPT, value: payload, }) - this._client.sendPortalNetworkResponse( - { - nodeId: _enr.nodeId, - socketAddr: _enr.getLocationMultiaddr('udp')!, - }, + this.sendPortalNetworkResponse( + _enr.nodeId, + _enr.getLocationMultiaddr('udp')!, myEnr.seq, - Buffer.from(encodedPayload), + encodedPayload, ) return '0x' + myEnr.seq.toString(16) diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts deleted file mode 100644 index 946a0b953..000000000 --- a/packages/cli/src/server.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { initTRPC } from '@trpc/server' -// eslint-disable-next-line node/file-extension-in-import -import { createHTTPServer } from '@trpc/server/adapters/standalone' -import cors from 'cors' -import { z } from 'zod' -import jayson from 'jayson/promise/index.js' -import { Discv5EventEmitter, ENR, SignableENR } from '@chainsafe/discv5' -import { createSecp256k1PeerId } from '@libp2p/peer-id-factory' -import { multiaddr } from '@multiformats/multiaddr' -import { execSync } from 'child_process' -import { HistoryProtocol, PortalNetwork, ProtocolId, toHexString } from 'portalnetwork' -// eslint-disable-next-line node/file-extension-in-import -import { observable } from '@trpc/server/observable' -import EventEmitter from 'events' -// export only the type definition of the API -// None of the actual implementation is exposed to the client -// export type AppRouter = typeof appRouter -import ws from 'ws' - -// eslint-disable-next-line node/file-extension-in-import -import { applyWSSHandler } from '@trpc/server/adapters/ws' -const bootnodes = [ - 'enr:-I24QDy_atpK3KlPjl6X5yIrK7FosdHI1cW0I0MeiaIVuYg3AEEH9tRSTyFb2k6lpUiFsqxt8uTW3jVMUzoSlQf5OXYBY4d0IDAuMS4wgmlkgnY0gmlwhKEjVaWJc2VjcDI1NmsxoQOSGugH1jSdiE_fRK1FIBe9oLxaWH8D_7xXSnaOVBe-SYN1ZHCCIyg', - 'enr:-I24QIdQtNSyUNcoyR4R7pWLfGj0YuX550Qld0HuInYo_b7JE9CIzmi2TF9hPg-OFL3kebYgLjnPkRu17niXB6xKQugBY4d0IDAuMS4wgmlkgnY0gmlwhJO2oc6Jc2VjcDI1NmsxoQJal-rNlNBoOMikJ7PcGk1h6Mlt_XtTWihHwOKmFVE-GoN1ZHCCIyg', - 'enr:-I24QI_QC3IsdxHUX_jk8udbQ4U2bv-Gncsdg9GzgaPU95ayHdAwnH7mY22A6ggd_aZegFiBBOAPamkP2pyHbjNH61sBY4d0IDAuMS4wgmlkgnY0gmlwhJ31OTWJc2VjcDI1NmsxoQMo_DLYhV1nqAVC1ayEIwrhoFCcHvWuhC_J-w-n_4aHP4N1ZHCCIyg', - 'enr:-IS4QGUtAA29qeT3cWVr8lmJfySmkceR2wp6oFQtvO_uMe7KWaK_qd1UQvd93MJKXhMnubSsTQPJ6KkbIu0ywjvNdNEBgmlkgnY0gmlwhMIhKO6Jc2VjcDI1NmsxoQJ508pIqRqsjsvmUQfYGvaUFTxfsELPso_62FKDqlxI24N1ZHCCI40', - 'enr:-IS4QNaaoQuHGReAMJKoDd6DbQKMbQ4Mked3Gi3GRatwgRVVPXynPlO_-gJKRF_ZSuJr3wyHfwMHyJDbd6q1xZQVZ2kBgmlkgnY0gmlwhMIhKO6Jc2VjcDI1NmsxoQM2kBHT5s_Uh4gsNiOclQDvLK4kPpoQucge3mtbuLuUGYN1ZHCCI44', - 'enr:-IS4QBdIjs6S1ZkvlahSkuYNq5QW3DbD-UDcrm1l81f2PPjnNjb_NDa4B5x4olHCXtx0d2ZeZBHQyoHyNnuVZ-P1GVkBgmlkgnY0gmlwhMIhKO-Jc2VjcDI1NmsxoQOO3gFuaCAyQKscaiNLC9HfLbVzFdIerESFlOGcEuKWH4N1ZHCCI40', - 'enr:-IS4QM731tV0CvQXLTDcZNvgFyhhpAjYDKU5XLbM7sZ1WEzIRq4zsakgrv3KO3qyOYZ8jFBK-VzENF8o-vnykuQ99iABgmlkgnY0gmlwhMIhKO-Jc2VjcDI1NmsxoQMTq6Cdx3HmL3Q9sitavcPHPbYKyEibKPKvyVyOlNF8J4N1ZHCCI44', - 'enr:-IS4QFV_wTNknw7qiCGAbHf6LxB-xPQCktyrCEZX-b-7PikMOIKkBg-frHRBkfwhI3XaYo_T-HxBYmOOQGNwThkBBHYDgmlkgnY0gmlwhKRc9_OJc2VjcDI1NmsxoQKHPt5CQ0D66ueTtSUqwGjfhscU_LiwS28QvJ0GgJFd-YN1ZHCCE4k', - 'enr:-IS4QDpUz2hQBNt0DECFm8Zy58Hi59PF_7sw780X3qA0vzJEB2IEd5RtVdPUYZUbeg4f0LMradgwpyIhYUeSxz2Tfa8DgmlkgnY0gmlwhKRc9_OJc2VjcDI1NmsxoQJd4NAVKOXfbdxyjSOUJzmA4rjtg43EDeEJu1f8YRhb_4N1ZHCCE4o', - 'enr:-IS4QGG6moBhLW1oXz84NaKEHaRcim64qzFn1hAG80yQyVGNLoKqzJe887kEjthr7rJCNlt6vdVMKMNoUC9OCeNK-EMDgmlkgnY0gmlwhKRc9-KJc2VjcDI1NmsxoQLJhXByb3LmxHQaqgLDtIGUmpANXaBbFw3ybZWzGqb9-IN1ZHCCE4k', - 'enr:-IS4QA5hpJikeDFf1DD1_Le6_ylgrLGpdwn3SRaneGu9hY2HUI7peHep0f28UUMzbC0PvlWjN8zSfnqMG07WVcCyBhADgmlkgnY0gmlwhKRc9-KJc2VjcDI1NmsxoQJMpHmGj1xSP1O-Mffk_jYIHVcg6tY5_CjmWVg1gJEsPIN1ZHCCE4o', -] -const main = async () => { - const t = initTRPC - .context() - .meta<{ - description: string - }>() - .create() - const publicProcedure = t.procedure - const cmd = 'hostname -I' - const pubIp = execSync(cmd).toString().split(' ')[0] - console.log('pubIp', pubIp) - const id = await createSecp256k1PeerId() - const enr = SignableENR.createFromPeerId(id) - const initMa: any = multiaddr(`/ip4/${pubIp}/udp/8546`) - enr.setLocationMultiaddr(initMa) - const config = { - enr: enr, - peerId: id, - config: { - enrUpdate: true, - addrVotesToUpdateEnr: 5, - allowUnverifiedSessions: true, - }, - bindAddrs: { - ip4: initMa, - }, - } as any - const portal = await PortalNetwork.create({ - config: config, - radius: 2n ** 256n - 1n, - supportedProtocols: [ProtocolId.HistoryNetwork], - }) - portal.discv5.enableLogs() - - portal.enableLog('*ultralight*, *LightClient*, *Portal*') - - const history = portal.protocols.get(ProtocolId.HistoryNetwork) as HistoryProtocol - const router = t.router - - const clientA = jayson.Client.http({ - host: '192.168.86.29', - port: 8545, - }) - const clients: Record = { - 8545: clientA, - } - const ee = new EventEmitter() - ;(portal.discv5 as Discv5EventEmitter).on('talkReqReceived', (msg: any) => { - ee.emit('talkReqReceived', msg) - }) - - // WSS Client Methods - - const onTalkReq = publicProcedure - .meta({ - description: 'Subscribe to Discv5 TalkReq listener', - }) - .subscription(() => { - return observable((emit) => { - const talkReq = (msg: any) => { - console.log(msg) - emit.next(msg) - } - ee.on('talkReqReceived', talkReq) - return () => { - ee.off('talkReqReceived', talkReq) - } - }) - }) - - /** - * {@link discv5_nodeInfo} - */ - const self = publicProcedure - .meta({ - description: 'Get ENR, NodeId, Client Tag, and MultiAddress', - }) - .mutation(() => { - return { - enr: portal.discv5.enr.encodeTxt(), - nodeId: portal.discv5.enr.nodeId, - client: 'ultralight', - multiAddr: portal.discv5.enr.getLocationMultiaddr('udp')?.toString(), - } - }) - - /** - * {@link portal_historyRoutingTableInfo} - */ - const local_routingTable = publicProcedure - .meta({ - description: 'Get Local Routing Table', - }) - .mutation(() => { - return [...history.routingTable.buckets.entries()] - .filter(([_, bucket]) => bucket.values().length > 0) - .map(([idx, bucket]) => { - return bucket - .values() - .map((enr) => [ - enr.kvs.get('c')?.toString() ?? '', - enr.encodeTxt(), - enr.nodeId, - enr.getLocationMultiaddr('udp')!.toString(), - idx, - ]) - }) - .flat() - }) - - /** - * {@link portal_historyPing} - */ - const ping = publicProcedure - .meta({ - description: 'Send Ping to ENR', - }) - .input( - z.object({ - enr: z.string(), - }), - ) - .mutation(async ({ input }) => { - const _pong = await history.sendPing(input.enr) - const pong = _pong - ? { customPayload: toHexString(_pong.customPayload), enrSeq: Number(_pong.enrSeq) } - : undefined - return pong - }) - - const pingBootNodes = publicProcedure - .meta({ - description: 'Ping all BootNodes', - }) - .mutation(async () => { - const pongs = [] - for await (const [idx, enr] of bootnodes.entries()) { - const _pong = await history.sendPing(enr) - console.log({ - enr: `${idx < 3 ? 'trin' : idx < 7 ? 'fluffy' : 'ultralight'}: ${enr.slice(0, 12)}`, - _pong, - }) - const pong = _pong - ? { - tag: `${idx < 3 ? 'trin' : idx < 7 ? 'fluffy' : 'ultralight'}`, - enr: `${enr.slice(0, 12)}`, - customPayload: BigInt(toHexString(_pong.customPayload)).toString(2).length, - enrSeq: Number(_pong.enrSeq), - } - : { - tag: ``, - enr: ``, - customPayload: '', - enrSeq: -1, - } - pongs.push(pong) - } - return pongs - }) - - /** - * HTTP Client Methods - */ - - /** - * {@link portal_historyRoutingTableInfo} - */ - const portal_historyRoutingTableInfo = publicProcedure - .meta({ - description: 'Get Local Routing Table Info', - }) - .mutation(async () => { - const res = await clients[8545].request('portal_historyRoutingTableInfo', []) - const routingTable = res.result - return { - routingTable: routingTable.buckets, - } - }) - const discv5_nodeInfo = publicProcedure - .meta({ - description: 'Get ENR, NodeId, Client Tag, and MultiAddress', - }) - .input( - z.object({ - port: z.number(), - }), - ) - .mutation(async ({ input }) => { - let client - if (clients[input.port]) { - client = clients[input.port] - } else { - client = jayson.Client.http({ - host: '192.168.86.29', - port: input.port, - }) - } - const info = await client.request('discv5_nodeInfo', []) - const enr = ENR.decodeTxt(info.result.enr) - return { - client: enr.kvs.get('c')?.toString(), - enr: info.result.enr, - nodeId: info.result.nodeId, - multiAddr: (await enr.getFullMultiaddr('udp'))?.toString(), - } - }) - - const pingBootNodeHTTP = publicProcedure - .meta({ - description: 'Ping all BootNodes', - }) - .mutation(async () => { - const pongs = [] - for await (const [idx, enr] of bootnodes.entries()) { - const p = await clients[8545].request('portal_historyPing', [enr]) - const _pong = p.result - console.log({ - enr: `${idx < 3 ? 'trin' : idx < 7 ? 'fluffy' : 'ultralight'}: ${enr.slice(0, 12)}`, - _pong, - }) - const pong = _pong - ? { - tag: `${idx < 3 ? 'trin' : idx < 7 ? 'fluffy' : 'ultralight'}`, - enr: `${enr.slice(0, 12)}`, - customPayload: toHexString(_pong.dataRadius), - enrSeq: Number(_pong.enrSeq), - } - : { - tag: '', - enr: ``, - customPayload: '', - enrSeq: -1, - } - pongs.push(pong) - } - return pongs - }) - - /** - * {@link portal_historyPing} - */ - const portal_historyPing = publicProcedure - .meta({ - description: 'Send Ping to ENR', - }) - .input( - z.object({ - port: z.number(), - enr: z.string(), - }), - ) - .mutation(async ({ input }) => { - const client = clients[input.port] - if (!client) { - throw new Error('no client') - } - const p = await client.request('portal_historyPing', [input.enr]) - const _pong = p.result - const pong = _pong - ? { - dataRadius: toHexString(_pong.dataRadius), - enrSeq: Number(_pong.enrSeq), - } - : { - dataRadius: 'undefined', - enrSeq: -1, - } - return pong - }) - - // Create tRpc Router - - const appRouter = router({ - onTalkReq, - self, - local_routingTable, - ping, - pingBootNodes, - discv5_nodeInfo, - portal_historyRoutingTableInfo, - portal_historyPing, - pingBootNodeHTTP, - }) - - const wss = new ws.Server({ - port: 3001, - }) - const handler = applyWSSHandler({ - wss, - router: appRouter, - createContext() { - console.log('context 4') - return {} - }, - }) - wss.on('connection', (ws) => { - console.log(`➕➕ Connection (${wss.clients.size})`) - ws.once('close', () => { - console.log(`➖➖ Connection (${wss.clients.size})`) - }) - }) - console.log('✅ WebSocket Server listening on ws://localhost:3001') - process.on('SIGTERM', () => { - console.log('SIGTERM') - handler.broadcastReconnectNotification() - wss.close() - }) - portal.discv5.enableLogs() - - portal.enableLog('*ultralight*, *LightClient*, -OFFER, -ACCEPT, *ultralight:RPC*') - - await portal.start() - - // const manager = new RPCManager(portal) - // const methods = manager.getMethods() - // const server = new jayson.Server(methods, { - // router: function (method, params) { - // // `_methods` is not part of the jayson.Server interface but exists on the object - // // but the docs recommend this pattern for custom routing - // // https://github.com/tedeh/jayson/blob/HEAD/examples/method_routing/server.js - // { - // console.log(`Received ${method} with params: ${JSON.stringify(params)}`) - // return this.getMethod(method) - // } - // }, - // }) - // server.http().listen(8546) - - // console.log(`Started JSON RPC Server address=http://${ip}:${8546}`) - - console.log('nodeId', portal.discv5.enr.encodeTxt()) - console.log('nodeId', portal.discv5.enr.nodeId) - - console.log('self', JSON.stringify(appRouter.self._def)) - console.log('self', JSON.stringify(appRouter.ping._def)) - - // create server - createHTTPServer({ - middleware: cors(), - router: appRouter, - createContext() { - console.log('context 3') - return {} - }, - }).listen(8546) -} -main() diff --git a/packages/cli/src/util.ts b/packages/cli/src/util.ts index 5daef213d..8d6441378 100644 --- a/packages/cli/src/util.ts +++ b/packages/cli/src/util.ts @@ -1,5 +1,17 @@ +import { BlockHeader } from '@ethereumjs/block' +import { RLP } from '@ethereumjs/rlp' +import { TransactionFactory } from '@ethereumjs/tx' import { Enr } from './rpc/schema/types.js' -import { BaseProtocol, ProtocolId } from 'portalnetwork' +import { + BaseProtocol, + BlockBodyContentType, + BlockHeaderWithProof, + EpochAccumulator, + ProtocolId, + sszReceiptType, + sszUnclesType, + toHexString, +} from 'portalnetwork' export const hasValidEnrPrefix = (enr: Enr) => { return enr.startsWith('enr:') @@ -51,3 +63,51 @@ export const addBootNode = async (protocolId: ProtocolId, baseProtocol: BaseProt ${protocolId}: ${error.message ?? error}`) } } + +export const toJSON = (contentKey: Uint8Array, res: Uint8Array) => { + const contentType = contentKey[0] + let content = {} + switch (contentType) { + case 0: { + const blockHeaderWithProof = BlockHeaderWithProof.deserialize(res) + const header = BlockHeader.fromRLPSerializedHeader(blockHeaderWithProof.header, { + setHardfork: true, + }).toJSON() + const proof = + blockHeaderWithProof.proof.selector === 0 + ? [] + : blockHeaderWithProof.proof.value?.map((p) => toHexString(p)) + content = { header, proof } + break + } + case 1: { + const blockBody = BlockBodyContentType.deserialize(res) + const transactions = blockBody.allTransactions.map((tx) => + TransactionFactory.fromSerializedData(tx).toJSON(), + ) + const unclesRlp = toHexString(sszUnclesType.deserialize(blockBody.sszUncles)) + content = { + transactions, + uncles: { + rlp: unclesRlp, + count: RLP.decode(unclesRlp).length.toString(), + }, + } + break + } + case 2: { + const receipt = sszReceiptType.deserialize(res) + content = receipt + break + } + case 3: { + const epochAccumulator = EpochAccumulator.deserialize(res) + content = epochAccumulator + break + } + default: { + content = {} + } + } + return JSON.stringify(content) +} diff --git a/packages/portalnetwork/src/client/client.ts b/packages/portalnetwork/src/client/client.ts index db9564a7e..7e4fb0366 100644 --- a/packages/portalnetwork/src/client/client.ts +++ b/packages/portalnetwork/src/client/client.ts @@ -39,6 +39,7 @@ import { peerIdFromKeys } from '@libp2p/peer-id' import { hexToBytes } from '@ethereumjs/util' export class PortalNetwork extends (EventEmitter as { new (): PortalNetworkEventEmitter }) { + eventLog: boolean discv5: Discv5 protocols: Map uTP: PortalNetworkUTP @@ -149,6 +150,7 @@ export class PortalNetwork extends (EventEmitter as { new (): PortalNetworkEvent dbSize: dbSize as () => Promise, metrics: opts.metrics, trustedBlockRoot: opts.trustedBlockRoot, + eventLog: opts.eventLog, }) return portal @@ -162,7 +164,7 @@ export class PortalNetwork extends (EventEmitter as { new (): PortalNetworkEvent constructor(opts: PortalNetworkOpts) { // eslint-disable-next-line constructor-super super() - + this.eventLog = opts.eventLog ?? false this.discv5 = Discv5.create(opts.config as IDiscv5CreateOptions) // cache signature to ensure ENR can be encoded on startup this.discv5.enr.encode() @@ -380,7 +382,7 @@ export class PortalNetwork extends (EventEmitter as { new (): PortalNetworkEvent const messageProtocol = utpMessage ? ProtocolId.UTPNetwork : protocolId try { this.metrics?.totalBytesSent.inc(payload.length) - let nodeAddr + let nodeAddr: ENR | undefined if (typeof enr === 'string') { // If ENR is not provided, look up ENR in protocol routing table by nodeId const protocol = this.protocols.get(protocolId) @@ -388,7 +390,7 @@ export class PortalNetwork extends (EventEmitter as { new (): PortalNetworkEvent nodeAddr = protocol.routingTable.getWithPending(enr)?.value if (!nodeAddr) { // Check in unverified sessions cache if no ENR found in routing table - nodeAddr = this.unverifiedSessionCache.get(enr) + // nodeAddr = this.unverifiedSessionCache.get(enr) } } } else { @@ -404,6 +406,8 @@ export class PortalNetwork extends (EventEmitter as { new (): PortalNetworkEvent Buffer.from(payload), hexToBytes(messageProtocol), ) + this.eventLog && + this.emit('SendTalkReq', nodeAddr.nodeId, toHexString(res), toHexString(payload)) return res } catch (err: any) { if (protocolId === ProtocolId.UTPNetwork) { @@ -419,6 +423,8 @@ export class PortalNetwork extends (EventEmitter as { new (): PortalNetworkEvent requestId: bigint, payload: Uint8Array, ) => { + this.eventLog && + this.emit('SendTalkResp', src.nodeId, requestId.toString(16), toHexString(payload)) this.discv5.sendTalkResp(src, requestId, payload) } } diff --git a/packages/portalnetwork/src/client/types.ts b/packages/portalnetwork/src/client/types.ts index 9ea46784f..bebd0f2ff 100644 --- a/packages/portalnetwork/src/client/types.ts +++ b/packages/portalnetwork/src/client/types.ts @@ -10,6 +10,8 @@ export interface IPortalNetworkEvents { NodeRemoved: (nodeId: NodeId, protocolId: ProtocolId) => void ContentAdded: (key: string, contentType: number, content: string) => void Verified: (key: string, verified: boolean) => void + SendTalkReq: (nodeId: string, requestId: string, payload: string) => void + SendTalkResp: (nodeId: string, requestId: string, payload: string) => void } export enum TransportLayer { @@ -32,6 +34,7 @@ export interface PortalNetworkOpts { dataDir?: string dbSize(): Promise trustedBlockRoot?: string + eventLog?: boolean } export type PortalNetworkEventEmitter = StrictEventEmitter diff --git a/packages/portalnetwork/src/subprotocols/history/history.ts b/packages/portalnetwork/src/subprotocols/history/history.ts index 89253caa5..a4cbbfef5 100644 --- a/packages/portalnetwork/src/subprotocols/history/history.ts +++ b/packages/portalnetwork/src/subprotocols/history/history.ts @@ -14,6 +14,7 @@ import { PortalNetwork, FoundContent, toHexString, + ENR, } from '../../index.js' import { ProtocolId } from '../types.js' import { ETH } from './eth_module.js' @@ -100,7 +101,11 @@ export class HistoryProtocol extends BaseProtocol { * @returns the value of the FOUNDCONTENT response or undefined */ public sendFindContent = async (dstId: string, key: Uint8Array) => { - const enr = this.routingTable.getWithPending(dstId)?.value + const enr = dstId.startsWith('enr:') + ? ENR.decodeTxt(dstId) + : this.routingTable.getWithPending(dstId)?.value + ? this.routingTable.getWithPending(dstId)?.value + : this.routingTable.getWithPending(dstId.slice(2))?.value if (!enr) { this.logger(`No ENR found for ${shortId(dstId)}. FINDCONTENT aborted.`) return diff --git a/packages/portalnetwork/src/subprotocols/protocol.ts b/packages/portalnetwork/src/subprotocols/protocol.ts index e80ba321b..ea138db3a 100644 --- a/packages/portalnetwork/src/subprotocols/protocol.ts +++ b/packages/portalnetwork/src/subprotocols/protocol.ts @@ -649,7 +649,7 @@ export abstract class BaseProtocol extends EventEmitter { } if (flagged.length > 0) { this.logger.extend('livenessCheck')(`Flagged ${flagged.length} peers from routing table`) - } else { + } else if (peers.length > 0) { this.logger.extend('livenessCheck')(`${peers.length} peers passed liveness check`) } this.routingTable.clearIgnored() diff --git a/packages/ui/README.md b/packages/ui/README.md new file mode 100644 index 000000000..4af2e3c72 --- /dev/null +++ b/packages/ui/README.md @@ -0,0 +1,88 @@ +# Portal Client UI + +This package contains a React web-app that is used to render a client-agnostic UI for Portal Clients. +This is separate from the `Browser-Client` package, which runs Portal Client in the Browser. +Th UI differs from the `Browser-Client` in that the UI package has no portal-network or discv5 dependencies, and can be used to interact with any locally running Portal Client, not just Ultralight. + +The React App, server, and the process by which they are started and run, are currently WIP. + +The App utilizes **tRpc**, a typescript based RPC library, to communicate with the Portal Client. The App is currently configured to communicate an Ultralight Client via a websocket connection, but this is not a requirement of the App. Any Portal Client running on a local RPC server should be accessible in the `HTTP Client` tab. A `WebSocket` based connection to Ultralight enables the App to receive real-time events from the Ultralight Client, allowing for additional functions not available using the JSON-RPC API alone, such as subscribing to client events. + +## Running the App (Development mode) + +- Install dependencies: + - *from ultralight/* + - run `npm i` + +- Run one of the following Portal Network client options: + - Start RPC-based portal client + - Default app configuration looks for http client on udp port 8545 + - for Ultralight: + - *from ultralight/packages/ultralight/cli/* + - run `npm run devnet --numNodes=1` + - for Fluffy: + - *see Fluffy README.md* + - for Trin: + - *see Trin README.md* + + - Start the **tRpc Ultralight server**: + - *from ultralight/packages/UI/:* + - run `npm run server:dev` + - At time of writing, this server is prone to crashing if an error occurs in the client, and will need to be restarted manually. + +- Start the UI server: + - *from ultralight/packages/ui/* + - run `npm run dev` + - serves the app on http://localhost:3000 + - starts websocket server on http://ws.localhost:3001 + +- Open Browser to http://localhost:3000 + - WS Client tab interacts with websocket client + - Start client with Start Button + - Then connect to bootnodes, or manually ping a peer and view the routing table + - By default all available event subscriptions are enabled. + - HTTP Client tab interacts with http client + - Manually change IP/PORT in Browser to connect to different clients + - TESTS tab + - not yet implemented + +## App Features + +### Routing Table + +The Routing Table tab is a visual representation of the client's routing table, and uses a combination of RPC-methods to access detailed info about each peer. The table is updated in real-time as the client's routing table changes, and is sortable by client type, enr, nodeId, address, and distance. + +### Bootnode Responses + +The Bootnodes tab displays the reponses from the Clients' initial attempt to reach the public bootnodes. + +### PING/PONG + +The Ping/Pong tab provides an AutoComplete input field to select a peer from the client's routing table, and then sends a PING request to that peer. The response is displayed in the UI. The PING/PONG is the most basic exchange that peers can use to establish a connection, or to test the status of an existing connection. + +### STATEROOT + +The StateRoot tab follows the tip of the chain using the Beacon Light Protocol + +### PEER_LOGS (WebSocket only) + +Peer Logs displays the history of incoming and outgoing messages for each Peer organized by message type. Individual peer histories can be viewed by clicking on the peer's enr in the Routing Table. Current implementation simply tallies the number of messages of each type sent to and from each peer, though more detailed information could be collected in this manner. + +### CONTENT_STORE + +Content Store provides an interface to store, retrieve, and view content from the Client's local DataBase. Serialized content is parsed into visual components for BlockHeader / Proof/ Block Body, etc. Newly stored keys are automatically added to the dropdown menu for easy retrieval. + +### RPC + +The RPC Tab provides access to the rest of the Portal JSON-RPC methods. Users can select a method, and be provided with a form to enter the correct parameters, and send an RPC request with validated parameters. Both the outgoing request and the incoming response are displayed in the UI as well as the browser console. + +## Contributions + +Contributions are welcome. Please open an issue to discuss any changes you would like to make. + +Desired contributions include: + - Error handling / Server stability + - Implement tRpc procedures for remaining RPC methods + - Testing / Debugging / User Feedback + - Visualizations / Data Analysis components + - UI / UX / Interactivity improvements \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index d29b09c92..f85d4daa1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -7,13 +7,16 @@ "dev": "vite", "build-ui": "tsc && vite build", "lint": "eslint --cache --ext \".js,.ts,.tsx\" --report-unused-disable-directives src", - "start": "vite preview" + "start": "vite preview", + "server:dev": "tsx watch src/server/server.ts", + "server:start": "node dist/server/server.js" }, "dependencies": { "@tanstack/react-query": "^4.18.0", "@trpc/client": "^10.38.2", "@trpc/react-query": "^10.38.2", "@trpc/server": "^10.38.2", + "base64url": "^3.0.1", "react": "^18.2.0", "json-bigint": "^1.0.0", "react-dom": "^18.2.0", @@ -24,11 +27,15 @@ "react-hot-toast": "^2.4.1", "react-hotkeys-hook": "4.4.1", "url": "^0.11.0", - "zod-to-json-schema": "^3.20.0", + "zod-to-json-schema": "^3.20.0", "ajv-formats": "^2.1.1", - "superjson": "^1.13.1" + "superjson": "^1.13.1", + "cors": "2.8.5", + "zod": "^3.0.0", + "ws": "^8.14.1" }, "devDependencies": { + "@types/cors": "2.8.14", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@hookform/resolvers": "^2.9.10", @@ -40,6 +47,7 @@ "@vitejs/plugin-react": "^3.1.0", "eslint": "^8.40.0", "typescript": "^5.1.3", + "tsx": "^3.12.7", "vite": "^4.1.2" } } diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 2516a9f89..cc5a82e0f 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -1,21 +1,33 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createWSClient, wsLink } from '@trpc/client' -import { useEffect, useReducer, useState } from 'react' +import { useState } from 'react' import { trpc } from './utils/trpc' import ClientTabs from './Components/Tabs' +import { AppRouter } from './utils/router' export function App() { - const wsClient = createWSClient({ - url: `ws://localhost:3001`, - }) - const [queryClient] = useState(() => new QueryClient()) - const [trpcClient] = useState(() => + const [wsClient] = useState( + createWSClient({ + url: `ws://localhost:3001`, + retryDelayMs(attemptIndex) { + console.log('ws retrying', attemptIndex) + return Math.min(1000 * 2 ** attemptIndex, 30_000) + }, + onClose(cause) { + clearInterval('update') + clearInterval('updated') + console.log('ws closed', cause) + }, + onOpen() { + console.log('ws opened: ws://localhost:3001') + }, + }), + ) + const [queryClient] = useState(new QueryClient()) + const [trpcClient] = useState( trpc.createClient({ links: [ - // httpBatchLink({ - // url: 'http://localhost:8546', - // }), - wsLink({ + wsLink({ client: wsClient, }), ], diff --git a/packages/ui/src/Clients/HTTPClient.tsx b/packages/ui/src/Clients/HTTPClient.tsx index 89ae2a730..3d4356fcc 100644 --- a/packages/ui/src/Clients/HTTPClient.tsx +++ b/packages/ui/src/Clients/HTTPClient.tsx @@ -1,6 +1,5 @@ -import { Box, Stack } from '@mui/material' -import { trpc } from '../utils/trpc' -import { useEffect, useReducer, useState } from 'react' +import { Box } from '@mui/material' +import { useEffect, useReducer } from 'react' import Client from '../Components/Client' import { ClientContext, @@ -8,142 +7,75 @@ import { ClientInitialState, ClientReducer, } from '../Contexts/ClientContext' +import { + RPCContext, + RPCDispatchContext, + RPCInitialState, + RPCReducer, + httpMethods, +} from '../Contexts/RPCContext' export default function HTTPClient() { const [state, dispatch] = useReducer(ClientReducer, ClientInitialState) + const [rpcState, rpcDispatch] = useReducer(RPCReducer, RPCInitialState) + const routingTable = ClientInitialState.RPC.http.local_routingTable.useMutation() + const pingBootNodes = ClientInitialState.RPC.http.pingBootNodes.useMutation() + const getEnr = ClientInitialState.RPC.http.portal_historyGetEnr.useMutation() + async function updateRT() { + const rt = await routingTable.mutateAsync({ port: rpcState.PORT, ip: rpcState.IP }) + console.groupCollapsed('update routing table', rpcState.PORT) + console.dir(rt) + const rtEnrs = await Promise.allSettled( + rt.map(async ([nodeId, bucket]) => { + const res = await getEnr.mutateAsync({ + port: rpcState.PORT, + ip: rpcState.IP, + nodeId, + }) + const enr = res + return enr instanceof Object + ? [nodeId, [enr.c, enr.enr, enr.nodeId, enr.multiaddr, bucket]] + : [nodeId, ['N/A', 'N/A', nodeId, 'N/A', bucket]] + }), + ) - const routingTable = trpc.portal_historyRoutingTableInfo.useMutation({ - onMutate(variables) { - console.log('routingtable mutate:', { variables }) - }, - onSettled(data, error, variables, context) { - console.log('routingtable settled:', { - data, - error, - variables, - context, - }) - }, - }) - const nodeInfo = trpc.discv5_nodeInfo.useMutation({ - onMutate(variables) { - console.log('nodeinfo mutate:', { variables }) - }, - onSettled(data, error, variables, context) { - console.log('nodeinfo settled:', { - data, - error, - variables, - context, - }) - }, - onError(error, variables, context) { - console.log('nodeinfo error:', { - error, - variables, - context, - }) - }, - }) - const getNodeInfo = async (port: number = 8545) => { - const info = await nodeInfo.mutateAsync({ port }) + console.dir({ rtEnrs: rtEnrs.map((r) => r.status === 'fulfilled' && r.value) }) + console.groupEnd() dispatch({ - type: 'NODE_INFO', - ...info, + type: 'ROUTING_TABLE', + routingTable: Object.fromEntries( + rtEnrs.map((r) => (r.status === 'fulfilled' ? r.value : ['', ['', '', '', '', -1]])), + ), }) } - const sendPing = trpc.portal_historyPing.useMutation({ - onMutate(variables) { - // A mutation is about to happen! - // You can do something here like show a loading indicator - // onMutate returns a context object that will be passed to the other methods - console.log({ variables }) - }, - onSettled(data, error, variables, context) { - console.log({ - data, - error, - variables, - context, - }) - }, - }) - const [pong, setPong] = useState() - - const ping = async (enr: string) => { - const pong = await sendPing.mutateAsync({ enr }) - setPong(pong) - getRoutingTable() - } - - const getRoutingTable = async () => { - const table = await routingTable.mutateAsync() - const allBucketsEntries = [...table.routingTable.reverse().entries()] - const buckets = allBucketsEntries.filter(([_, bucket]) => bucket.length > 0) - const p = buckets - .map(([idx, bucket]) => { - return bucket.map((peer: string) => { - return ['', '', '0x' + peer, '', idx] - }) - }) - .flat(1) - const r = Object.fromEntries(p.entries()) - dispatch({ type: 'ROUTING_TABLE', routingTable: r }) - } - const bootHTTP = trpc.pingBootNodeHTTP.useMutation({ - onMutate(variables) { - console.log('bootnode mutate:', { variables }) - }, - onSettled(data, error, variables, context) { - console.log('bootnode settled:', { - data, - error, - variables, - context, - }) - }, - onError(error, variables, context) { - console.log('bootnode error:', { - error, - variables, - context, - }) - }, - }) - const pingBootNodesHTTP = async () => { - const res = await bootHTTP.mutateAsync() - const bootnoderes = res.map((r) => { - return r - ? { - tag: r.tag, - enr: r.enr, - connected: 'true', - } - : { - tag: 'client0.0.1', - enr: 'enr:xxxx....', - connected: 'false', - } - }) + async function bootUP() { + const bootnodes = await pingBootNodes.mutateAsync({ port: rpcState.PORT, ip: rpcState.IP }) + console.log({ bootnodes }) dispatch({ type: 'BOOTNODES', - bootnodeResponses: bootnoderes, + bootnodes, }) - getRoutingTable() } - + useEffect(() => { - getNodeInfo() - pingBootNodesHTTP() + updateRT() + clearInterval(setInterval(() => updateRT(), 10000)) + // bootUP() + const updated = setInterval(() => updateRT(), 10000) + console.log(updated) }, []) - + return ( - + - - - + + + + + + + ) diff --git a/packages/ui/src/Clients/WSSClient.tsx b/packages/ui/src/Clients/WSSClient.tsx index b9f01e821..00386678c 100644 --- a/packages/ui/src/Clients/WSSClient.tsx +++ b/packages/ui/src/Clients/WSSClient.tsx @@ -6,65 +6,27 @@ import { ClientContext, ClientDispatchContext, ClientInitialState, - ClientProvider, ClientReducer, } from '../Contexts/ClientContext' +import { + RPCContext, + RPCDispatchContext, + RPCInitialState, + RPCReducer, + wsMethods, +} from '../Contexts/RPCContext' export function WSSClient() { const [state, dispatch] = useReducer(ClientReducer, ClientInitialState) - const boot = trpc.pingBootNodes.useMutation() + const [rpcState, rpcDispatch] = useReducer(RPCReducer, RPCInitialState) - const pingBootNodes = async () => { - const res = await boot.mutateAsync() - const bootnoderes = res.map((r) => { - return r - ? { - tag: r.tag, - enr: r.enr, - connected: 'true', - } - : { - tag: 'client0.0.1', - enr: 'enr:xxxx....', - connected: 'false', - } - }) - dispatch({ - type: 'BOOTNODES', - bootnodeResponses: bootnoderes, - }) - getLocalRoutingTable() - } - const localRoutingTable = trpc.local_routingTable.useMutation() - trpc.onTalkReq.useSubscription(undefined, { - onData(data) { - console.log('onTalkReq data:', data) - }, - onError(error) { - console.log('onTalkReq error:', error) - }, - onStarted() { - console.log('onTalkReq started') - }, - }) + useEffect(() => { + bootUP() + }, []) + + const localRoutingTable = ClientInitialState.RPC.ws.portal_historyRoutingTableInfo.useMutation() - const sendPing = trpc.ping.useMutation({ - onMutate(variables) { - console.log({ variables }) - }, - onSettled(data) { - console.log({ - data, - }) - }, - }) - const [pong, setPong] = useState() - const ping = async (enr: string) => { - const pong = await sendPing.mutateAsync({ enr }) - setPong(pong) - getLocalRoutingTable() - } const getLocalRoutingTable = async () => { const _peers = await localRoutingTable.mutateAsync() @@ -73,26 +35,34 @@ export function WSSClient() { routingTable: _peers, }) } - const node = trpc.self.useMutation() - const getSelf = async () => { - const nodeInfo = await node.mutateAsync() + const node = trpc.browser_nodeInfo.useMutation() + const getSelf = async () => { + const nodeInfo = await node.mutateAsync({}) dispatch({ type: 'NODE_INFO', ...nodeInfo, }) } - useEffect(() => { + + const bootUP = () => { + clearInterval('update') getSelf() getLocalRoutingTable() - pingBootNodes() - }, []) + const update = setInterval(() => { + getLocalRoutingTable() + }, 10000) + } return ( - + - - - + + + + + + + ) diff --git a/packages/ui/src/Components/BootNodes.tsx b/packages/ui/src/Components/BootNodes.tsx index 9972fdf56..b2710677e 100644 --- a/packages/ui/src/Components/BootNodes.tsx +++ b/packages/ui/src/Components/BootNodes.tsx @@ -1,18 +1,38 @@ -import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material"; -import { CheckmarkIcon, ErrorIcon } from "react-hot-toast"; -import { ClientContext } from "../Contexts/ClientContext"; -import React from "react"; +import { + Container, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, +} from '@mui/material' +import { CheckmarkIcon, ErrorIcon } from 'react-hot-toast' +import { ClientContext } from '../Contexts/ClientContext' +import React from 'react' -function createRow(tag: string, enr: string, nodeId: string, multiAddr: string, bucket: number) { - return { tag, enr, nodeId, multiAddr, bucket } - } +function createRow(idx: number, client: string, enr: string, nodeId: string, connected: boolean) { + return { idx, client, enr, nodeId, connected } +} export default function BootNodeResponses() { - const state = React.useContext(ClientContext) - const rows = Object.values(state.BOOTNODES).map(({ tag, enr, connected }) => createRow(tag, enr, connected, '', 0)) - return ( - - + const state = React.useContext(ClientContext) + const rows = state.BOOTNODES + ? Object.entries(state.BOOTNODES).map(([nodeId, { idx, client, enr, connected }]) => + createRow(idx, client, enr, nodeId, connected), + ) + : [] + return ( + + +
@@ -23,22 +43,33 @@ export default function BootNodeResponses() { # Client ENR - Ping + NodeId + Connected - {rows.map((row, idx) => ( - - {idx} - {row.tag} - {row.enr} - - {row.nodeId === 'true' ? : } - - - ))} + {rows.length > 0 ? ( + rows.map((row, idx) => ( + + {idx} + {row.client} + + {row.enr.slice(0, 24)}... + + + 0x{row.nodeId.slice(0, 16)}... + + + {row.connected ? : } + + + )) + ) : ( + + )}
- ) - } \ No newline at end of file + + ) +} diff --git a/packages/ui/src/Components/Client.tsx b/packages/ui/src/Components/Client.tsx index d22a3ab63..20788c060 100644 --- a/packages/ui/src/Components/Client.tsx +++ b/packages/ui/src/Components/Client.tsx @@ -1,21 +1,136 @@ -import { Stack } from '@mui/material' +import { Container, ListItemText, Stack } from '@mui/material' import { SelfNodeInfo } from './NodeInfo' import FunctionTabs from './FunctionTabs' +import PortMenu from './Port' +import { ClientContext, ClientDispatchContext } from '../Contexts/ClientContext' +import React from 'react' +import { trpc } from '../utils/trpc' +import { RPCContext, RPCDispatchContext } from '../Contexts/RPCContext' +import Start from './Start' -export default function Client(props: { - name: string - ping: any - pong: any -}) { - const { name, ping, pong } = props +export default function Client(props: { name: string }) { + const dispatch = React.useContext(ClientDispatchContext) + const state = React.useContext(ClientContext) + + if (state.CONNECTION === 'ws') { + trpc.onTalkReq.useSubscription(undefined, { + onData(data: any) { + console.groupCollapsed(`Talk Request Received: ${data.topic} ${data.nodeId.slice(0, 6)}...`) + console.dir(data) + console.groupEnd() + dispatch({ + type: 'LOG_RECEIVED', + topic: data.topic, + nodeId: data.nodeId, + log: data.message, + }) + }, + onStarted() { + console.info('WS: onTalkReq subscription started') + }, + }) + trpc.onTalkResp.useSubscription(undefined, { + onData(data: any) { + console.groupCollapsed( + `Talk Response Received: ${data.topic} ${data.nodeId.slice(0, 6)}...`, + ) + console.dir(data) + console.groupEnd() + dispatch({ + type: 'LOG_RECEIVED', + topic: data.topic, + nodeId: data.nodeId, + log: data.message, + }) + }, + onStarted() { + console.info('onTalkResp subscription started') + }, + }) + trpc.onSendTalkReq.useSubscription(undefined, { + onData(data: any) { + console.groupCollapsed( + 'Talk Request Sent:' + data.topic + ' ' + data.nodeId.slice(0, 6) + '...', + ) + console.dir(data) + console.groupEnd() + dispatch({ + type: 'LOG_SENT', + topic: data.topic, + nodeId: data.nodeId, + log: data.payload, + }) + }, + onStarted() { + console.info('onSendTalkReq subscription started') + }, + }) + trpc.onSendTalkResp.useSubscription(undefined, { + onData(data: any) { + console.groupCollapsed( + 'Talk Response Sent:' + data.topic + ' ' + data.nodeId.slice(0, 6) + '...', + ) + console.dir(data) + console.groupEnd() + + dispatch({ + type: 'LOG_SENT', + topic: data.topic, + nodeId: data.nodeId, + log: data.payload, + }) + }, + onStarted() { + console.info('onSendTalkResp subscription started') + }, + }) + trpc.onContentAdded.useSubscription(undefined, { + onData(data: any) { + const type = + data.contentType === 0 + ? 'BlockHeader' + : data.contentType === 1 + ? 'BlockBody' + : data.contentType === 2 + ? 'BlockReceipts' + : data.contentType === 3 + ? 'EpochAccumulator' + : 'unknown' + dispatch({ + type: 'CONTENT_STORE', + contentKey: '0x0' + data.contentType + data.key.slice(2), + content: { type, added: new Date().toString().split(' ').slice(1, 5).join(' ') }, + }) + }, + onStarted() { + console.info('onContentAdded subscription started') + }, + }) + trpc.onNodeAdded.useSubscription(undefined, { + onData({ nodeId, protocolId }) { + console.info('node added: ', nodeId) + }, + onStarted() { + console.info('onNodeAdded subscription started') + }, + }) + } return ( - -

{name}

- - -
+ + {!state.CONNECTED ? ( + + ) : ( + + + {props.name === 'HTTP Client' && } + + + + + + + + )} + ) } diff --git a/packages/ui/src/Components/ContentStore.tsx b/packages/ui/src/Components/ContentStore.tsx new file mode 100644 index 000000000..11f71072a --- /dev/null +++ b/packages/ui/src/Components/ContentStore.tsx @@ -0,0 +1,358 @@ +import { + Box, + Button, + Container, + FormControl, + FormControlLabel, + FormHelperText, + Input, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + TextField, + TextareaAutosize, + Tooltip, +} from '@mui/material' +import SaveAs from '@mui/icons-material/SaveAs' +import React, { useEffect } from 'react' +import { trpc } from '../utils/trpc' + +import { ClientContext, ClientDispatchContext } from '../Contexts/ClientContext' +import { JSONObject } from 'superjson/dist/types' +import { RPCContext } from '../Contexts/RPCContext' + +const blockHeaderContent_key = + '0x0088e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6' +const blockHeaderContent_value = + '0x080000001c020000f90211a0d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479405a56e2d52c817161883f50c441c3228cfe54d9fa0d67e4d450343046425ae4271474353857ab860dbc0a1dde64b41b5cd3a532bf3a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008503ff80000001821388808455ba422499476574682f76312e302e302f6c696e75782f676f312e342e32a0969b900de27b6ac6a67742365dd65f55a0526c41fd18e1b16f1a1215c2e66f5988539bd4979fef1ec401000080ff0700000000000000000000000000000000000000000000000000000023d6398abe4eba641e97a075b30780c12ebe18b24e83a9a9c7bdd94a910cf749bb6bb61aeab6bc5786067f7432bad790642b578881460279ad773a8191596c3087811c70634dbf2ea3abb7199cb5638713844db315d63467f40b5d38eeb884ddcb57866840a050f634417365e9515cd5e6826038ceb45659d85365cfcfceb7a6e9886aaff50b16b6af2bc3bde8b7e701b2cb5022ba49cac9d6c456834e692772b12acf7af78a8375b80ef177c9ad743a14ff0d4935f9ac105444fd57f802fed32495bab257b9585a149a7de4ac53eda7b6df7b9dac7f92325ba05eb1e6b588202048719c250620f4bfa71307470d6c835156db527294c6e6004f9de0c3595a7f1df43427c770506e7e3ca5d021f065544c6ba191d8ffc5fc0805b805d301c926c183ed9ec7e467b962e2304fa7945b6b18042dc2a53cb62b27b28af50fc06db5da2f83bd479f3719b9972fc723c69e4cd13877dcf7cc2a919a95cdf5d7805d9bd9a9f1fbf7a880d82ba9d7af9ed554ce01ea778db5d93d0665ca4fee11f4f873b0b1b58ff1337769b6ee458316030aeac65a5aab68d60fbf214bd44455f892260020000000000000000000000000000000000000000000000000000000000000' +const blockBodyContent_key = '0x0188e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6' +const blockBodyContent_value = '0x0800000008000000c0' + +const typeFromKey = (key: string) => { + if (key.length < 2) { + return '' + } + if (key.startsWith('0x')) { + key = key.slice(2) + } + if (key[0] !== '0') { + return 'unknown' + } + switch (key[1]) { + case '0': + return 'BlockHeaderWithProof' + case '1': + return 'BlockBody' + case '2': + return 'BlockReceipts' + case '3': + return 'EpochAccumulator' + default: + return 'unknown' + } +} + +export default function ContentStore(props: any) { + const state = React.useContext(ClientContext) + const dispatch = React.useContext(ClientDispatchContext) + const [contentKey, setContentKey] = React.useState( + '0x0088e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6', + ) + const [content, setContent] = React.useState( + '0x080000001c020000f90211a0d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479405a56e2d52c817161883f50c441c3228cfe54d9fa0d67e4d450343046425ae4271474353857ab860dbc0a1dde64b41b5cd3a532bf3a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008503ff80000001821388808455ba422499476574682f76312e302e302f6c696e75782f676f312e342e32a0969b900de27b6ac6a67742365dd65f55a0526c41fd18e1b16f1a1215c2e66f5988539bd4979fef1ec401000080ff0700000000000000000000000000000000000000000000000000000023d6398abe4eba641e97a075b30780c12ebe18b24e83a9a9c7bdd94a910cf749bb6bb61aeab6bc5786067f7432bad790642b578881460279ad773a8191596c3087811c70634dbf2ea3abb7199cb5638713844db315d63467f40b5d38eeb884ddcb57866840a050f634417365e9515cd5e6826038ceb45659d85365cfcfceb7a6e9886aaff50b16b6af2bc3bde8b7e701b2cb5022ba49cac9d6c456834e692772b12acf7af78a8375b80ef177c9ad743a14ff0d4935f9ac105444fd57f802fed32495bab257b9585a149a7de4ac53eda7b6df7b9dac7f92325ba05eb1e6b588202048719c250620f4bfa71307470d6c835156db527294c6e6004f9de0c3595a7f1df43427c770506e7e3ca5d021f065544c6ba191d8ffc5fc0805b805d301c926c183ed9ec7e467b962e2304fa7945b6b18042dc2a53cb62b27b28af50fc06db5da2f83bd479f3719b9972fc723c69e4cd13877dcf7cc2a919a95cdf5d7805d9bd9a9f1fbf7a880d82ba9d7af9ed554ce01ea778db5d93d0665ca4fee11f4f873b0b1b58ff1337769b6ee458316030aeac65a5aab68d60fbf214bd44455f892260020000000000000000000000000000000000000000000000000000000000000', + ) + const [hashKey, setHashKey] = React.useState('') + const [curType, setCurType] = React.useState('') + const [stored, setStored] = React.useState([]) + const [displayKey, setDisplayKey] = React.useState('') + const [display, setDisplay] = React.useState() + const { REQUEST, PORT, IP } = React.useContext(RPCContext) + const historyStore = REQUEST.portal_historyStore.useMutation() + const historyRetrieve = REQUEST.portal_historyLocalContent.useMutation() + const [sortBy, setSortBy] = React.useState<{ + key: 'added' | 'key' + asc: boolean + }>({ + key: 'added', + asc: true, + }) + + async function retrieve() { + let params: any = { + contentKey: displayKey!, + } + if (state.CONNECTION === 'http') { + params = { + ...params, + port: PORT, + // ip: IP, + } + } + const res = await historyRetrieve.mutateAsync(params) + console.groupCollapsed('portal_historyLocalContent') + console.dir(params) + console.dir(res) + console.groupEnd() + setDisplay(JSON.parse(res)) + } + + useEffect(() => { + displayKey ? retrieve() : setDisplay(undefined) + }, [displayKey]) + + useEffect(() => { + if (contentKey.length < 2) { + setCurType('') + return + } + const key = contentKey.startsWith('0x') ? contentKey.slice(2) : contentKey + setCurType(typeFromKey(key)) + if (key.length !== 66) { + setHashKey('') + return + } + try { + const h = parseInt(key, 16) + setHashKey(h.toString(16)) + } catch { + setHashKey('') + } + }, [contentKey]) + + const onClick = async () => { + const key = contentKey.startsWith('0x') ? contentKey : '0x' + contentKey + const value = content.startsWith('0x') ? content : '0x' + content + let params: any = { + contentKey: key, + content: value, + } + if (state.CONNECTION === 'http') { + params = { + ...params, + port: PORT, + // ip: IP, + } + } + const res = await historyStore.mutateAsync(params) + console.groupCollapsed('portal_historyStore') + console.dir(params) + console.dir(res) + console.groupEnd() + setStored([...stored, key]) + setDisplayKey(key) + } + + return ( + + + + + + + + ContentKey + + setContentKey(e.target.value)} + /> + Content + setContent(e.target.value)} + /> + + + + + + + + + + {stored.map((key) => { + return ( + + + setDisplayKey(key)}> + + + + + ) + })} + + + + + + + + setSortBy({ + key: 'key', + asc: !sortBy.asc, + }) + } + direction={sortBy.asc ? 'asc' : 'desc'} + > + Key + + + Value + + + setSortBy({ + key: 'added', + asc: !sortBy.asc, + }) + } + > + Added + + + + + + {Object.entries(state.CONTENT_STORE) + .sort(([ka, a], [kb, b]) => { + if (sortBy.asc) { + if (sortBy.key === 'key') { + return ka > kb ? 1 : -1 + } + return a[sortBy.key] > b[sortBy.key] ? 1 : -1 + } else { + if (sortBy.key === 'key') { + return ka < kb ? 1 : -1 + } + return a[sortBy.key] < b[sortBy.key] ? 1 : -1 + } + }) + .map(([key, value]) => { + return ( + + + + setDisplayKey(key)}> + + + + + {value.type} + {value.added} + + ) + })} + +
+
+
+
+ + {display && ( + + + + + + + + + {Object.keys(display).map((key) => { + return {key} + })} + + + + {Array.from({ + length: Math.max( + ...Object.values(display).map((value) => { + return value ? Object.keys(value).length : 0 + }), + ), + }).map((_, i) => { + return ( + + {Object.values(display).map((val, idx) => { + const data = val ? Object.entries(val)[i] : undefined + return ( + { + console.log('copy', data && data[1]) + }} + > + + + + + ) + })} + + ) + })} + +
+ )} +
+
+ ) +} diff --git a/packages/ui/src/Components/FunctionTabs.tsx b/packages/ui/src/Components/FunctionTabs.tsx index 12357f5c7..6330e4b20 100644 --- a/packages/ui/src/Components/FunctionTabs.tsx +++ b/packages/ui/src/Components/FunctionTabs.tsx @@ -1,15 +1,17 @@ import * as React from 'react' import Tabs from '@mui/material/Tabs' import Tab from '@mui/material/Tab' -import Typography from '@mui/material/Typography' import Box from '@mui/material/Box' -import LookupContent from './LookupContent' -import GetBlockBy from './getBlockBy' import GetBeacon from './getChainTip' import Ping from './Ping' import NodeInfo from './NodeInfo' import BootNodeResponses from './BootNodes' -import { ClientContext } from '../Contexts/ClientContext' +import { ClientContext, ClientDispatchContext } from '../Contexts/ClientContext' +import { Button, LinearProgress, Stack } from '@mui/material' +import ContentStore from './ContentStore' +import MessageLogs from './MessageLogs' +import RPC from './RPC' +import { RPCContext, RPCDispatchContext } from '../Contexts/RPCContext' interface TabPanelProps { children?: React.ReactNode @@ -17,23 +19,20 @@ interface TabPanelProps { value: number } -function TabPanel(props: TabPanelProps) { +export function TabPanel(props: TabPanelProps) { const { children, value, index, ...other } = props return ( - + {value === index && {children}} + ) } @@ -44,52 +43,92 @@ function a11yProps(index: number) { } } -export default function FunctionTabs(props: { ping: any; pong: any }) { +export default function FunctionTabs() { const state = React.useContext(ClientContext) - const { ping, pong } = props + const { REQUEST, PORT, IP } = React.useContext(RPCContext) + const dispatch = React.useContext(ClientDispatchContext) + const rpcDispatch = React.useContext(RPCDispatchContext) const [value, setValue] = React.useState(0) - const handleChange = (event: React.SyntheticEvent, newValue: number) => { + const [bootup, setBootup] = React.useState(true) + + const pingBooTnodes = REQUEST.pingBootNodes.useMutation() + + const handleChange = (_: any, newValue: number) => { setValue(newValue) } + const bootNodes = async () => { + setBootup(undefined) + setTimeout(() => { + setBootup(false) + }, 10000) + const res = + state.CONNECTION === 'http' + ? await pingBooTnodes.mutateAsync({ + port: PORT, + ip: IP, + }) + : await pingBooTnodes.mutateAsync({}) + res && + dispatch({ + type: 'BOOTNODES', + bootnodes: res, + }) + setBootup(false) + } + return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + {bootup === true ? ( + + ) : bootup === undefined ? ( + + + + ) : ( + <> + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/packages/ui/src/Components/LookupContent.tsx b/packages/ui/src/Components/LookupContent.tsx index 0ea159b83..c4ef38bbe 100644 --- a/packages/ui/src/Components/LookupContent.tsx +++ b/packages/ui/src/Components/LookupContent.tsx @@ -115,7 +115,7 @@ export default function LookupContent() { - + {/* - + */} ) } diff --git a/packages/ui/src/Components/MessageLogs.tsx b/packages/ui/src/Components/MessageLogs.tsx new file mode 100644 index 000000000..94563bd77 --- /dev/null +++ b/packages/ui/src/Components/MessageLogs.tsx @@ -0,0 +1,303 @@ +import { + TableContainer, + Paper, + Table, + TableHead, + TableCell, + TableBody, + TableRow, + Tooltip, + ListItemText, + Button, + Tabs, + Tab, + Stack, + Container, +} from '@mui/material' +import React, { Fragment } from 'react' +import { ClientContext, ClientDispatchContext } from '../Contexts/ClientContext' +import { TabPanel } from './FunctionTabs' +import PeerMessageLogs from './PeerMessageLogs' + +export default function MessageLogs() { + const state = React.useContext(ClientContext) + const [currentReceivedLogs, setCurrentReceivedLogs] = React.useState< + Record> + >(state.RECEIVED_LOGS) + const [currentSentLogs, setCurrentSentLogs] = React.useState< + Record> + >(state.SENT_LOGS) + const [msgAlerts, setMsgAlerts] = React.useState({ + ping: false, + pong: false, + findNodes: false, + nodes: false, + findContent: false, + content: false, + offer: false, + accept: false, + }) + const [peerAlerts, setPeerAlerts] = React.useState>({ + '0xabcd': false, + }) + const [selected, setSelected] = React.useState('') + const [hover, setHover] = React.useState('') + const altertMsg = (type: keyof typeof msgAlerts) => { + switch (type) { + case 'ping': + clearTimeout('ping') + const ping = setTimeout(() => { + normalizeMsg(type) + }, 500) + break + case 'pong': + clearTimeout('pong') + const pong = setTimeout(() => { + normalizeMsg(type) + }, 500) + break + case 'findNodes': + clearTimeout('findNodes') + const findNodes = setTimeout(() => { + normalizeMsg(type) + }, 500) + break + case 'nodes': + clearTimeout('nodes') + const nodes = setTimeout(() => { + normalizeMsg(type) + }, 500) + break + case 'findContent': + clearTimeout('findContent') + const findContent = setTimeout(() => { + normalizeMsg(type) + }, 500) + break + case 'content': + clearTimeout('content') + const content = setTimeout(() => { + normalizeMsg(type) + }, 500) + break + case 'offer': + clearTimeout('offer') + const offer = setTimeout(() => { + normalizeMsg(type) + }, 500) + break + case 'accept': + clearTimeout('accept') + const accept = setTimeout(() => { + normalizeMsg(type) + }, 500) + break + default: + return + } + setMsgAlerts((prev) => ({ ...prev, [type]: true })) + } + const normalizeMsg = (type: keyof typeof msgAlerts) => { + setMsgAlerts((prev) => ({ ...prev, [type]: false })) + } + + const alertPeer = (peer: keyof typeof peerAlerts) => { + clearTimeout('peer') + const peerTimeout = setTimeout(() => { + normalizePeer(peer) + }, 500) + setPeerAlerts((prev) => ({ ...prev, [peer]: true })) + } + const normalizePeer = (peer: keyof typeof peerAlerts) => { + setPeerAlerts((prev) => ({ ...prev, [peer]: false })) + } + + const msgCellStyle = (type: keyof typeof msgAlerts) => ({ + backgroundColor: msgAlerts[type] ? 'orange' : 'white', + }) + + const peerCellStyle = (peer: keyof typeof peerAlerts) => ({ + border: selected === peer ? 'solid black 1px' : 'none', + backgroundColor: peerAlerts[peer] ? 'orange' : hover === peer ? 'grey' : 'white', + }) + + React.useEffect(() => { + for (const [peer, logs] of Object.entries(state.RECEIVED_LOGS)) { + if (currentReceivedLogs[peer] === undefined) { + alertPeer(peer) + } else { + for (const [type, msgs] of Object.entries(logs as Record)) { + const msgAlterType = type.toLowerCase() as keyof typeof msgAlerts + if (!Object.keys(currentReceivedLogs[peer]).includes(type)) { + if (msgs.length > 0) { + alertPeer(peer) + altertMsg(msgAlterType) + } + } else { + if (msgs.length > currentReceivedLogs[peer][type].length) { + alertPeer(peer) + altertMsg(msgAlterType) + } + } + } + } + } + for (const [peer, logs] of Object.entries(state.SENT_LOGS)) { + if (currentSentLogs[peer] === undefined) { + alertPeer(peer) + } else { + for (const [type, msgs] of Object.entries(logs as Record)) { + const msgAlterType = type.toLowerCase() as keyof typeof msgAlerts + if (!Object.keys(currentSentLogs[peer]).includes(type)) { + if (msgs.length > 0) { + alertPeer(peer) + altertMsg(msgAlterType) + } + } else { + if (msgs.length > currentSentLogs[peer][type].length) { + alertPeer(peer) + altertMsg(msgAlterType) + } + } + } + } + } + setCurrentSentLogs(state.SENT_LOGS) + setCurrentReceivedLogs(state.RECEIVED_LOGS) + }, [state.RECEIVED_LOGS, state.SENT_LOGS]) + + const handleSelect = (peer: string) => { + setSelected(peer) + } + + return ( + + + + + + + + Peer + + + + + + Pong + + + FindNodes + + + Nodes + + + + + + Content + + + Offer + + + Accept + + + uTP + + + + + {Object.entries(state.RECEIVED_LOGS).map(([peer, logs]: any) => { + const sentLogs = (state.SENT_LOGS as any)[peer] ?? {} + return ( + + + setHover(peer)} + onMouseLeave={() => setHover('')} + onClick={() => handleSelect(peer)} + rowSpan={2} + style={peerCellStyle(peer)} + colSpan={1} + > + + + + + + SENT + + + {sentLogs['PING'] ? sentLogs['PING'].length : 0} + + + {sentLogs['PONG'] ? sentLogs['PONG'].length : 0} + + + {sentLogs['FINDNODES'] ? sentLogs['FINDNODES'].length : 0} + + + {sentLogs['NODES'] ? sentLogs['NODES'].length : 0} + + + {sentLogs['FINDCONTENT'] ? sentLogs['FINDCONTENT'].length : 0} + + + {sentLogs['CONTENT'] ? sentLogs['CONTENT'].length : 0} + + + {sentLogs['OFFER'] ? sentLogs['OFFER'].length : 0} + + + {sentLogs['ACCEPT'] ? sentLogs['ACCEPT'].length : 0} + + + {sentLogs['UTP'] ? sentLogs['UTP'].length : 0} + + + + + RECV + + + {logs['PING'] ? logs['PING'].length : 0} + + + {logs['PONG'] ? logs['PONG'].length : 0} + + + {logs['FINDNODES'] ? logs['FINDNODES'].length : 0} + + + {logs['NODES'] ? logs['NODES'].length : 0} + + + {logs['FINDCONTENT'] ? logs['FINDCONTENT'].length : 0} + + + {logs['CONTENT'] ? logs['CONTENT'].length : 0} + + + {logs['OFFER'] ? logs['OFFER'].length : 0} + + + {logs['ACCEPT'] ? logs['ACCEPT'].length : 0} + + + {logs['UTP'] ? logs['UTP'].length : 0} + + + + ) + })} + +
+
+ +
+
+ ) +} diff --git a/packages/ui/src/Components/NodeInfo.tsx b/packages/ui/src/Components/NodeInfo.tsx index 688836e01..ee26f1ddf 100644 --- a/packages/ui/src/Components/NodeInfo.tsx +++ b/packages/ui/src/Components/NodeInfo.tsx @@ -6,8 +6,9 @@ import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' -import { Box } from '@mui/material' -import { ClientContext } from '../Contexts/ClientContext' +import { Box, TableSortLabel } from '@mui/material' +import { ClientContext, ClientDispatchContext } from '../Contexts/ClientContext' +import { RPCContext, RPCDispatchContext } from '../Contexts/RPCContext' function createRow(tag: string, enr: string, nodeId: string, multiAddr: string, bucket: number) { return { tag, enr, nodeId, multiAddr, bucket } @@ -15,16 +16,34 @@ function createRow(tag: string, enr: string, nodeId: string, multiAddr: string, export function SelfNodeInfo() { const state = React.useContext(ClientContext) + const dispatch = React.useContext(ClientDispatchContext) + const rpc = React.useContext(RPCContext) + const rpcDispatch = React.useContext(RPCDispatchContext) const { tag, enr, nodeId, multiAddr } = state.NODE_INFO + const row = createRow(tag, enr, nodeId, multiAddr, 0) + const nodeInfo = rpc.REQUEST.discv5_nodeInfo.useMutation() + const getNodeInfo = async () => { + const info = + state.CONNECTION === 'http' + ? await nodeInfo.mutateAsync({ port: rpc.PORT }) + : await nodeInfo.mutateAsync({}) + dispatch({ + type: 'NODE_INFO', + ...info, + }) + } + React.useEffect(() => { + getNodeInfo() + }, [rpc.PORT, rpc.IP]) return ( - - + +
{row.tag} - {row.enr.slice(0, 16)}... - {row.nodeId.slice(0, 12)}... + {row.enr} + {row.nodeId} {row.multiAddr} @@ -35,21 +54,120 @@ export function SelfNodeInfo() { } export default function NodeInfo() { const state = React.useContext(ClientContext) - const rt = Object.values(state.ROUTING_TABLE) as [string, string, string, string, number][] - const rows = rt.map(([tag, enr, nodeId, multiAddr, bucket]) => - createRow(tag, enr, nodeId, multiAddr, bucket), + const [sort, setSort] = React.useState('') + const [order, setOrder] = React.useState<'asc' | 'desc'>('asc') + const [rows, setRows] = React.useState( + (Object.values(state.ROUTING_TABLE) as [string, string, string, string, number][]).map( + ([tag, enr, nodeId, multiAddr, bucket]) => createRow(tag, enr, nodeId, multiAddr, bucket), + ), ) + function handleSort(sortBy: string) { + if (sortBy === sort) { + setOrder(order === 'asc' ? 'desc' : 'asc') + } else { + setSort(sortBy) + setOrder('asc') + } + const sorted = rt().sort( + ( + [clientA, enrA, nodeIdA, multiAddrA, bucketA], + [clientB, enrB, nodeIdB, multiAddrB, bucketB], + ) => { + switch (sortBy) { + case 'kBucket': + return order === 'asc' ? bucketA - bucketB : bucketB - bucketA + case 'NodeId': + return order === 'asc' ? nodeIdA.localeCompare(nodeIdB) : nodeIdB.localeCompare(nodeIdA) + case 'Client': + return order === 'asc' ? clientA.localeCompare(clientB) : clientB.localeCompare(clientA) + case 'MultiAddr': + return order === 'asc' + ? multiAddrA.localeCompare(multiAddrB) + : multiAddrB.localeCompare(multiAddrA) + case 'ENR': + return order === 'asc' ? enrA.localeCompare(enrB) : enrB.localeCompare(enrA) + default: + return order === 'asc' ? bucketA - bucketB : bucketB - bucketA + } + }, + ) + + setRows( + sorted.map(([tag, enr, nodeId, multiAddr, bucket]) => + createRow(tag, enr, nodeId, multiAddr, bucket), + ), + ) + } + + function rt() { + return Object.values(state.ROUTING_TABLE) as [string, string, string, string, number][] + } + + // switch (state.CONNECTION) { + // case 'ws': { + React.useEffect(() => { + setRows( + rt().map(([tag, enr, nodeId, multiAddr, bucket]) => + createRow(tag, enr, nodeId, multiAddr, bucket), + ), + ) + }, [state.ROUTING_TABLE]) return ( - - -
+ + +
- Client - ENR - NodeId - MultiAddr - Dist + + handleSort('Client')} + hideSortIcon + > + Client + + + + handleSort('ENR')} + hideSortIcon + > + ENR + + + + handleSort('NodeId')} + hideSortIcon + > + NodeId + + + + handleSort('MultiAddr')} + hideSortIcon + > + MultiAddr + + + + handleSort('kBucket')} + hideSortIcon + > + kBucket + + @@ -67,4 +185,53 @@ export default function NodeInfo() { ) + // } + // case 'http': { + // const rt = Object.values(state.ROUTING_TABLE) + // return ( + // + // + //
+ // + // + // + // handleSort('kBucket')}> + // kBucket + // + // + // + // handleSort('NodeId')}> + // NodeId + // + // + // + // + // + // {rt + // .sort(([nodeA, bucketA], [nodeB, bucketB]) => + // sort === 'kBucket' + // ? order === 'asc' + // ? parseInt(bucketA as string) - parseInt(bucketB as string) + // : parseInt(bucketB as string) - parseInt(bucketA as string) + // : nodeA > nodeB + // ? order === 'asc' + // ? 1 + // : -1 + // : order === 'asc' + // ? -1 + // : 1, + // ) + // .map(([nodeId, bucket], idx) => ( + // + // {256 - parseInt(bucket as string)} + // 0x{nodeId} + // + // ))} + // + //
+ //
+ // + // ) + // } + // } } diff --git a/packages/ui/src/Components/PeerMessageLogs.tsx b/packages/ui/src/Components/PeerMessageLogs.tsx new file mode 100644 index 000000000..4530be49a --- /dev/null +++ b/packages/ui/src/Components/PeerMessageLogs.tsx @@ -0,0 +1,127 @@ +import { + TableContainer, + Paper, + Table, + TableHead, + TableCell, + TableBody, + TableRow, + Tabs, + Tab, + Stack, + ListItemText, + Button, + FormLabel, + Container, +} from '@mui/material' +import React from 'react' +import { ClientContext, ClientDispatchContext } from '../Contexts/ClientContext' +import { TabPanel } from './FunctionTabs' + +const messageTypes = [ + 'PING', + 'PONG', + 'FINDNODES', + 'NODES', + 'FINDCONTENT', + 'CONTENT', + 'OFFER', + 'ACCEPT', + 'UTP', +] + +export default function PeerMessageLogs(props: { selected: string }) { + const { selected } = props + const state = React.useContext(ClientContext) + const dispatch = React.useContext(ClientDispatchContext) + const [value, setValue] = React.useState(0) + const sentLogs = React.useMemo(() => state.SENT_LOGS[selected], [state.SENT_LOGS]) + const receivedLogs = React.useMemo(() => state.RECEIVED_LOGS[selected], [state.RECEIVED_LOGS]) + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValue(newValue) + } + function msgCellStyle(msgType: string) { + return {} + } + return ( + + + + + + + + + MessageType + + + SENT + + + RECV + + + + + {messageTypes.map((msgType, idx) => { + return ( + + + + + + {(selected in state.SENT_LOGS && + state.SENT_LOGS[selected][msgType]?.length) ?? + 0} + + + {(selected in state.RECEIVED_LOGS && + state.RECEIVED_LOGS[selected][msgType]?.length) ?? + 0} + + + ) + })} + +
+
+ {messageTypes.map((msgType, idx) => { + return ( + + + + + + + + SENT + RECV + + + + {sentLogs && + sentLogs[msgType] && + Array.from({ + length: Math.max( + sentLogs[msgType].length ?? 0, + sentLogs[msgType].length ?? 0, + ), + }).map((_, _idx) => { + return ( + + {sentLogs[msgType][_idx] ?? ''} + + {(receivedLogs[msgType] && receivedLogs[msgType][_idx]) ?? ''} + + + ) + })} + +
+
+ ) + })} +
+
+ ) +} diff --git a/packages/ui/src/Components/Ping.tsx b/packages/ui/src/Components/Ping.tsx index b4f4d6470..0084db108 100644 --- a/packages/ui/src/Components/Ping.tsx +++ b/packages/ui/src/Components/Ping.tsx @@ -12,23 +12,40 @@ import { MenuItem, Alert, AlertTitle, + Box, + Button, + CircularProgress, + Fade, + Typography, } from '@mui/material' import SendIcon from '@mui/icons-material/Send' import { CheckmarkIcon } from 'react-hot-toast' -import React from 'react' -import { ClientContext } from '../Contexts/ClientContext'; +import React, { useEffect } from 'react' +import { ClientContext } from '../Contexts/ClientContext' +import { RPCContext } from '../Contexts/RPCContext' -export default function Ping(props: { ping: any; pong: any; }) { +export default function Ping() { const state = React.useContext(ClientContext) - const { ping, pong } = props + const rpc = React.useContext(RPCContext) + const [pong, setPong] = React.useState(null) + const ping = rpc.REQUEST.portal_historyPing.useMutation() const [open, setOpen] = React.useState(false) const [alert, setAlert] = React.useState<'closed' | 'open' | 'success' | 'fail'>('closed') const [toPing, setToPing] = React.useState('') const [peer, setPeer] = React.useState('') - const handleClick = () => { - ping(toPing) + const [pinging, setPinging] = React.useState('') + const handleClick = async () => { + setPinging(toPing) + const pong = await ping.mutateAsync({ enr: toPing, port: rpc.PORT }) + setPong(pong) setAlert('open') - setOpen(true) + } + const setEnr = (enr: string) => { + setToPing(enr) + } + + useEffect(() => { + if (!open) return setTimeout(() => { if (pong) { setAlert('success') @@ -40,41 +57,91 @@ export default function Ping(props: { ping: any; pong: any; }) { setAlert('closed') }, 2000) }, 1000) - } - const setEnr = (enr: string) => { - if (enr.startsWith('enr:'.slice(0, enr.length))) { - setToPing(enr) - } - } + }, [open]) const handleChangePeer = (event: SelectChangeEvent) => { setPeer(event.target.value as string) + setToPing(event.target.value as string) } + const [query, setQuery] = React.useState('idle') + const timerRef = React.useRef() + + React.useEffect( + () => () => { + clearTimeout(timerRef.current) + }, + [], + ) + + const handleClickQuery = () => { + handleClick() + if (timerRef.current) { + clearTimeout(timerRef.current) + } + + if (query !== 'idle') { + setQuery('idle') + return + } + + setQuery('progress') + timerRef.current = window.setTimeout(() => { + if (pong) { + setQuery('success') + } else { + setQuery('fail') + } + }, 2000) + } + + useEffect(() => { + if (query === 'success' || query === 'fail') { + timerRef.current = window.setTimeout(() => { + setQuery('idle') + }, 2000) + } + }, [query]) return ( {open && alert === 'fail' ? ( Fail - Ping Pong Failed + Ping Pong Failed{toPing.slice(0, 16)}... ) : open && alert === 'success' ? ( Pong - Ping Pong Successcheck it out! + Ping Pong Success{toPing.slice(0, 16)}... ) : ( open && ( Pinging - Pinging -- + Pinging -- * {toPing.slice(0, 16)}... ) )} - - {pong ? : } - - + + + {query === 'success' ? ( + Pong Received! + ) : ( + + + + )} + + + - {Object.values(state.ROUTING_TABLE).map(([tag, enr, nodeid, ma, b]) => ( + + {' '} + + {Object.values(state.ROUTING_TABLE).map(([tag, enr, nodeId, ma, b]) => ( - {enr} + 0x{nodeId} ))} {pong && ( - - customPayload - - {pong.customPayload.slice(0, 5)}...{pong.customPayload.slice(-5)} - - enrSeq: - {pong.enrSeq} - + <> + + + + )} ) diff --git a/packages/ui/src/Components/Port.tsx b/packages/ui/src/Components/Port.tsx new file mode 100644 index 000000000..088181ea5 --- /dev/null +++ b/packages/ui/src/Components/Port.tsx @@ -0,0 +1,93 @@ +import * as React from 'react' +import Button from '@mui/material/Button' +import { RPCContext, RPCDispatchContext } from '../Contexts/RPCContext' +import { Dialog, DialogActions, DialogContent, DialogTitle, List, ListItemText, Popover, TextField } from '@mui/material' +import { ClientContext } from '../Contexts/ClientContext' +import { trpc } from '../utils/trpc' +import { set, z } from 'zod' +import DropDown from '@mui/icons-material/ArrowDropDown' + +export default function PortMenu() { + const rpc = React.useContext(RPCContext) + const dipsatch = React.useContext(RPCDispatchContext) + const address = trpc.getPubIp.useQuery() + const [curIP, setCurIp] = React.useState(address.data ?? '') + const [validIp, setValidIp] = React.useState(true) + const [curPort, setCurPort] = React.useState(8545) + const state = React.useContext(ClientContext) + const [anchorEl, setAnchorEl] = React.useState(null) + const open = Boolean(anchorEl) + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + const handleClose = () => { + setAnchorEl(null) + } + + + function handleIpInput(e: React.ChangeEvent) { + try { + z.string().ip().parse(e.target.value) + setValidIp(true) + } catch { + setValidIp(false) + } + setCurIp(e.target.value) + } + function handlePortInput(e: React.ChangeEvent) { + setCurPort(parseInt(e.target.value)) + } + function setAddr() { + console.log('setting addr', curIP, curPort) + dipsatch({ + type: 'PORT', + port: curPort, + }) + dipsatch({ + type: 'IP', + port: curIP, + }) + setAnchorEl(null) + } + + const id = open ? 'simple-popover' : undefined + return ( +
+ + + Set RPC Address + + + + + + + + + +
+ ) +} diff --git a/packages/ui/src/Components/RPC.tsx b/packages/ui/src/Components/RPC.tsx new file mode 100644 index 000000000..a5d842989 --- /dev/null +++ b/packages/ui/src/Components/RPC.tsx @@ -0,0 +1,351 @@ +import { + Box, + Button, + Collapse, + Container, + FormControl, + InputLabel, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + ListSubheader, + MenuItem, + Paper, + Select, + SelectChangeEvent, + Stack, +} from '@mui/material' +import React from 'react' +import RPCInput from './RPCInput' +import RPCParams from './RPCParams' +import { RPCContext, RPCDispatchContext, TMethods, WSMethods } from '../Contexts/RPCContext' +import { ClientContext } from '../Contexts/ClientContext' +import { trpc } from '../utils/trpc' +import ExpandLess from '@mui/icons-material/ExpandLess' +import ExpandMore from '@mui/icons-material/ExpandMore' + +export const methodNames = [ + 'discv5_nodeInfo', + 'portal_historyRoutingTableInfo', + 'portal_historyPing', + 'portal_historyFindNodes', + 'portal_historyFindContent', + 'portal_historyRecursiveFindContent', + 'portal_historyOffer', + 'portal_historySendOffer', + 'portal_historyGossip', + 'eth_getBlockByHash', + 'eth_getBlockByNumber', +] as const + +export type RPCMethod = (typeof methodNames)[number] + +export default function RPC() { + const [open, setOpen] = React.useState(true) + const handleOpen = () => { + setOpen(!open) + } + + const { CONNECTION } = React.useContext(ClientContext) + const rpcState = React.useContext(RPCContext) + const rpcDispatch = React.useContext(RPCDispatchContext) + const [meta, setMeta] = React.useState({}) + const [method, setMethod] = React.useState('discv5_nodeInfo') + const rpcMethods: Record = { + pingBootNodes: rpcState.REQUEST.pingBootNodes.useMutation(), + discv5_nodeInfo: rpcState.REQUEST.discv5_nodeInfo.useMutation(), + portal_historyPing: rpcState.REQUEST.portal_historyPing.useMutation(), + portal_historyRoutingTableInfo: rpcState.REQUEST.portal_historyRoutingTableInfo.useMutation(), + portal_historyFindNodes: rpcState.REQUEST.portal_historyFindNodes.useMutation(), + portal_historyFindContent: rpcState.REQUEST.portal_historyFindContent.useMutation(), + portal_historyRecursiveFindContent: + rpcState.REQUEST.portal_historyRecursiveFindContent.useMutation(), + portal_historyOffer: rpcState.REQUEST.portal_historyOffer.useMutation(), + portal_historySendOffer: rpcState.REQUEST.portal_historySendOffer.useMutation(), + portal_historyGossip: rpcState.REQUEST.portal_historyGossip.useMutation(), + portal_historyStore: rpcState.REQUEST.portal_historyStore.useMutation(), + portal_historyLocalContent: rpcState.REQUEST.portal_historyLocalContent.useMutation(), + eth_getBlockByHash: rpcState.REQUEST.eth_getBlockByHash.useMutation(), + eth_getBlockByNumber: rpcState.REQUEST.eth_getBlockByNumber.useMutation(), + } + function handleChangeMethod(event: SelectChangeEvent) { + setMethod(event.target.value as RPCMethod) + } + + async function handleClick() { + rpcDispatch({ + type: 'CURRENT_RESPONSE', + response: '', + }) + const req = await request() + console.log(`method: ${method}`) + console.log(`params: ${JSON.stringify(req.params)}`) + rpcDispatch({ + type: 'CURRENT_REQUEST', + request: JSON.stringify({ + method: req.type, + params: req.params, + }), + }) + const res = req.response + console.log(`response: ${JSON.stringify(res)}`) + rpcDispatch({ + type: 'CURRENT_RESPONSE', + response: res, + }) + } + + function methodArgs() { + let params = {} + switch (method) { + case 'discv5_nodeInfo': + params = {} + break + case 'portal_historyPing': + params = { + enr: rpcState.ENR, + } + break + case 'portal_historyRoutingTableInfo': + params = {} + break + case 'portal_historyFindNodes': + params = { + nodeId: rpcState.NODEID, + distances: rpcState.DISTANCES, + } + break + case 'portal_historyFindContent': + params = { + nodeId: rpcState.NODEID, + contentKey: rpcState.CONTENT_KEY, + } + break + case 'portal_historyRecursiveFindContent': + params = { + contentKey: rpcState.CONTENT_KEY, + } + break + case 'portal_historyOffer': + params = { + nodeId: rpcState.NODEID, + contentKey: rpcState.CONTENT_KEY, + content: rpcState.CONTENT, + } + break + case 'portal_historySendOffer': + params = { + nodeId: rpcState.NODEID, + contentKeys: rpcState.CONTENT_KEY_ARRAY, + } + break + case 'portal_historyGossip': + params = { + contentKey: rpcState.CONTENT_KEY, + content: rpcState.CONTENT, + } + break + default: + params = {} + break + } + if (CONNECTION === 'http') { + params = { ...params, port: rpcState.PORT, ip: rpcState.IP } + } + return params + } + + async function request() { + console.log(`Preparing request for ${method} (${CONNECTION})`) + const params = methodArgs() + console.log(`params: ${JSON.stringify(params)}`) + + switch (method) { + case 'discv5_nodeInfo': { + const res = await rpcMethods.discv5_nodeInfo.mutateAsync(params) + return { + type: 'discv5_nodeInfo', + params, + response: res, + } + } + case 'portal_historyPing': { + const res = await rpcMethods.portal_historyPing.mutateAsync(params) + return { + type: 'portal_historyPing', + params, + response: res, + } + } + case 'portal_historyRoutingTableInfo': { + const res = await rpcMethods.portal_historyRoutingTableInfo.mutateAsync() + return { + type: 'portal_historyRoutingTableInfo', + params, + response: res, + } + } + case 'portal_historyFindNodes': { + const res = await rpcMethods.portal_historyFindNodes.mutateAsync(params) + return { + type: 'portal_historyFindNodes', + params, + response: res, + } + } + case 'portal_historyFindContent': { + const res = await rpcMethods.portal_historyFindContent.mutateAsync(params) + return { + type: 'portal_historyFindContent', + params, + response: res, + } + } + case 'portal_historyRecursiveFindContent': { + const res = await rpcMethods.portal_historyRecursiveFindContent.mutateAsync(params) + return { + type: 'portal_historyRecursiveFindContent', + params, + response: res, + } + } + case 'portal_historyOffer': { + const res = await rpcMethods.portal_historyOffer.mutateAsync(params) + return { + type: 'portal_historyOffer', + params, + response: res, + } + } + case 'portal_historySendOffer': { + const res = await rpcMethods.portal_historySendOffer.mutateAsync(params) + return { + type: 'portal_historySendOffer', + params, + response: res, + } + } + case 'portal_historyGossip': { + const res = await rpcMethods.portal_historyGossip.mutateAsync(params) + return { + type: 'portal_historyGossip', + params, + response: res, + } + } + default: + return { + type: 'UNKNOWN', + params: [], + response: {}, + } + } + } + + return ( + + + + + + + + + + + + + + Method + + + + + + + + {/* {meta?.description} */} + + + + + + + + + + Request: + {rpcState.CURRENT_LOG.request} + + + Response: + {rpcState.CURRENT_LOG.response === undefined ? ( + + ) : typeof rpcState.CURRENT_LOG.response === 'string' ? ( + + ) : typeof rpcState.CURRENT_LOG.response === 'number' ? ( + + ) : 'asJSON' in rpcState.CURRENT_LOG.response ? ( + + + {/* */} + + {open ? : } + + + + {Object.entries(JSON.parse(rpcState.CURRENT_LOG.response.asJSON as string)).map( + ([jsonKey, jsonVal]) => { + return ( + + ) + }, + )} + + + + ) : ( + Object.entries(rpcState.CURRENT_LOG.response).map(([key, value]) => { + return + }) + )} + + + + + ) +} diff --git a/packages/ui/src/Components/RPCInput.tsx b/packages/ui/src/Components/RPCInput.tsx new file mode 100644 index 000000000..67cf51c89 --- /dev/null +++ b/packages/ui/src/Components/RPCInput.tsx @@ -0,0 +1,531 @@ +import { + FormControl, + FormHelperText, + Alert, + Snackbar, + TextField, + Autocomplete, + Stack, + ListItemText, +} from '@mui/material' +import React, { ChangeEvent, useEffect } from 'react' +import z from 'zod' +import { RPCContext, RPCDispatchContext } from '../Contexts/RPCContext' +import { decodeTxt } from '../utils/enr' +import { ClientContext } from '../Contexts/ClientContext' +import { RPCMethod } from './RPC' + +const nodeIdParser = z + .string() + .transform((val) => (val.startsWith('0x') ? val : '0x' + val)) + .refine((val) => val.length === 66) +const keyParser = z + .string() + .transform((val) => (val.startsWith('0x') ? val : '0x' + val)) + .refine((val) => val.length === 68) + +export default function RPCInput(props: { method: RPCMethod }) { + switch (props.method) { + case 'portal_historyPing': { + return + } + case 'portal_historyFindNodes': { + return ( + + + + + ) + } + case 'portal_historyFindContent': { + return ( + + + + + ) + } + case 'portal_historyRecursiveFindContent': { + return ( + + + + ) + } + case 'portal_historyOffer': { + return ( + + + + + + ) + } + case 'portal_historySendOffer': { + return ( + + + + + ) + } + case 'portal_historyGossip': { + return ( + + + + + + ) + } + case 'eth_getBlockByHash': { + return + } + case 'eth_getBlockByNumber': { + return + } + default: { + return <> + } + } +} + +export function InputBlockHash() { + const dispatch = React.useContext(RPCDispatchContext) + const [valid, setValid] = React.useState() + const [cur, setCur] = React.useState('') + const setBlockHash = (e: ChangeEvent) => { + const blockHash = e.target.value + if (blockHash.length === 0) { + setValid(undefined) + } + setCur(blockHash) + try { + keyParser.refine((val) => val.length === 66).parse(blockHash) + setValid(true) + dispatch({ + type: 'BLOCK_HASH', + blockHash: blockHash, + }) + } catch { + setValid(false) + } + } + return ( + + BlockHash + + + ) +} + +export function InputBlockNumber() { + const dispatch = React.useContext(RPCDispatchContext) + const [valid, setValid] = React.useState() + const [cur, setCur] = React.useState('') + const setBlockNumber = (e: ChangeEvent) => { + try { + const value = BigInt(e.target.value) + const blockNumber = z.bigint().min(0n).parse(value).toString() + setCur(blockNumber) + dispatch({ + type: 'BLOCK_NUMBER', + blockNumber: blockNumber, + }) + setValid(true) + } catch { + setValid(false) + } + } + return ( + + BlockNumber (int or hex) + + + ) +} + +export function SelectContentKey() { + const { CONTENT_STORE } = React.useContext(ClientContext) + const dispatch = React.useContext(RPCDispatchContext) + const [value, setValue] = React.useState(null) + + useEffect(() => { + dispatch({ + type: 'CONTENT_KEY', + contentKey: value, + }) + }, [value]) + + return ( + { + setValue(newInputValue) + }} + onChange={(event, newValue) => { + setValue(newValue) + }} + options={Object.keys(CONTENT_STORE)} + getOptionLabel={(option) => option} + defaultValue={Object.keys(CONTENT_STORE)[0] ?? 'ContentKeys'} + filterSelectedOptions + renderInput={(params) => ( + + )} + /> + ) +} +export function SelectContentKeyArray() { + const { CONTENT_STORE } = React.useContext(ClientContext) + const dispatch = React.useContext(RPCDispatchContext) + const [value, setValue] = React.useState([]) + + useEffect(() => { + dispatch({ + type: 'CONTENT_KEY_ARRAY', + contentKeyArray: value, + }) + }, [value]) + + return ( + { + setValue(newValue) + }} + options={Object.keys(CONTENT_STORE)} + getOptionLabel={(option) => option} + filterSelectedOptions + renderInput={(params) => ( + + )} + /> + ) +} + +export function InputContentKey() { + const dispatch = React.useContext(RPCDispatchContext) + const [cur, setCur] = React.useState( + '', + ) + const [valid, setValid] = React.useState(undefined) + const [keyErr, setKeyErr] = React.useState(undefined) + const setContentKey = (e: ChangeEvent) => { + const contentKey = e.target.value + if (contentKey.length === 0) { + setValid(undefined) + } + setCur(contentKey) + try { + keyParser.parse(contentKey) + setValid(true) + dispatch({ + type: 'CONTENT_KEY', + contentKey: contentKey, + }) + setKeyErr(undefined) + } catch (err: any) { + setKeyErr(err.message) + setValid(false) + } + } + return ( + + {keyErr && {keyErr}} + ContentKey + + + ) +} + +export function InputContent() { + const [cur, setCur] = React.useState('') + const { CONTENT } = React.useContext(RPCContext) + const dispatch = React.useContext(RPCDispatchContext) + + function setContent() { + dispatch({ + type: 'CONTENT', + content: cur, + }) + } + + useEffect(() => { + if (cur.length === 0) { + return + } + setContent() + }, [cur]) + + return ( + + Content + + setCur(e.target.value) + } + /> + + ) +} + +export function InputEnr() { + const [cur, setCur] = React.useState('') + const [valid, setValid] = React.useState(false) + const { ROUTING_TABLE } = React.useContext(ClientContext) + const dispatch = React.useContext(RPCDispatchContext) + + useEffect(() => { + setEnr(cur) + }, [cur]) + + function setEnr(e: string) { + setCur(e) + if (cur.length === 0) { + setValid(undefined) + return + } + try { + z.string().startsWith('enr:').parse(cur) + dispatch({ + type: 'ENR', + enr: cur, + }) + try { + const decoded = decodeTxt(cur) + console.log('DECODED', decoded) + setValid(true) + } catch { + setValid(false) + } + } catch { + setValid(false) + } + } + + return ( + + + { + setCur(newInputValue) + }} + onChange={(_, newValue) => { + if (!newValue) return + setCur(newValue) + }} + freeSolo + fullWidth + selectOnFocus + clearOnBlur + handleHomeEndKeys + id="select-enr" + options={Object.values(ROUTING_TABLE).map(([, enr]) => enr)} + getOptionLabel={(option) => option} + filterSelectedOptions + renderInput={(params) => } + /> + + + ) +} + +export function InputNodeId() { + const { ROUTING_TABLE } = React.useContext(ClientContext) + const dispatch = React.useContext(RPCDispatchContext) + const [valid, setValid] = React.useState() + const [error, setError] = React.useState('') + const [cur, setCur] = React.useState('') + + const onChangeInput = (nodeId: string) => { + if (nodeId.length === 0) { + setValid(undefined) + return + } + try { + setCur(nodeIdParser.parse(nodeId)) + } catch { + setValid(false) + setError('NodeId must be valid hex string') + // setCur('') + // setNodeId('') + } + } + + const setNodeId = (nodeId: string) => { + dispatch({ + type: 'NODEID', + nodeId: nodeId, + }) + } + + useEffect(() => { + if (cur.length === 66) { + setValid(true) + setNodeId(cur) + } else { + setValid(false) + setError('NodeId must be 32 bytes') + } + }, [cur]) + + return ( + nodeId)} + getOptionLabel={(option) => option} + placeholder={'Node ID'} + onChange={(_, newValue) => { + if (!newValue) return + onChangeInput(newValue) + // setNodeId(newValue) + }} + onInputChange={(_, newInputValue) => { + onChangeInput(newInputValue) + }} + filterSelectedOptions + renderInput={(params) => ( + {}} {...params} label="NodeId" /> + )} + /> + ) +} + +export function InputDistances() { + const dispatch = React.useContext(RPCDispatchContext) + + function setDistances(distances: number[]) { + dispatch({ + type: 'DISTANCES', + distances, + }) + } + + return ( + + + i).reverse()} + getOptionLabel={(option) => option.toString()} + defaultValue={[]} + onChange={(event, newValue) => { + setDistances(newValue) + }} + selectOnFocus + clearOnBlur + handleHomeEndKeys + renderInput={(params) => } + /> + + + ) +} + +export function InputContentKeyArray() { + const { CONTENT_STORE } = React.useContext(ClientContext) + const { CONTENT_KEY_ARRAY } = React.useContext(RPCContext) + const dispatch = React.useContext(RPCDispatchContext) + const [newKey, setNewKey] = React.useState('') + const [open, setOpen] = React.useState(false) + const [errorMsg, setErrorMsg] = React.useState('Invalid ContentKey') + const [contentKeyArray, setContentKeyArray] = React.useState([]) + const keyArrParser = z.array(keyParser) + + const handleClose = () => { + setOpen(false) + } + + const handleNewVlaue = (key: string | null) => { + if (!key || CONTENT_KEY_ARRAY.includes(key)) { + return + } + setInputArray([...contentKeyArray, key]) + } + + const setInputArray = (array: string[]) => { + // setArrayStr(JSON.stringify(array)) + try { + keyArrParser.parse(array) + setContentKeyArray(array) + inputKeyArray() + } catch (err) { + console.log('contentkeyparser error', err) + // + } + } + + const inputKeyArray = () => { + try { + // const keyArr = keyArrParser.parse(contentKeyArray) + console.log('content_key_array', contentKeyArray) + console.log('CONTENT_KEY_ARRAY pre', CONTENT_KEY_ARRAY) + dispatch({ + type: 'CONTENT_KEY_ARRAY', + contentKeyArray: contentKeyArray, + }) + console.log('CONTENT_KEY_ARRAY post', CONTENT_KEY_ARRAY) + } catch (err: any) { + setErrorMsg(`Invalid ContentKey Array: err.message`) + setOpen(true) + } + } + + return ( + + Add ContentKey + { + // setArrayStr([...contentKeyArray, newValue]) + handleNewVlaue(newValue) + inputKeyArray() + }} + onInputChange={(_, newInputValue) => { + handleNewVlaue(newInputValue) + }} + selectOnFocus + clearOnBlur + handleHomeEndKeys + id="select-multiple-keys" + onKeyDown={(e) => { + if (e.key === 'Enter') { + inputKeyArray() + } + }} + options={[newKey, ...Object.keys(CONTENT_STORE)]} + getOptionLabel={(option) => option} + // defaultValue={'ContentKey'} + filterSelectedOptions + renderInput={(params) => ( + + )} + /> + + + {errorMsg} + + + + ) +} diff --git a/packages/ui/src/Components/RPCParams.tsx b/packages/ui/src/Components/RPCParams.tsx new file mode 100644 index 000000000..6b422f1a6 --- /dev/null +++ b/packages/ui/src/Components/RPCParams.tsx @@ -0,0 +1,141 @@ +import { + ListItemText, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, +} from '@mui/material' +import { RPCMethod } from './RPC' +import React from 'react' +import { RPCContext } from '../Contexts/RPCContext' + +export default function RPCParams(props: { method: RPCMethod }) { + const state = React.useContext(RPCContext) + + const params = (method: RPCMethod) => { + switch (method) { + case 'discv5_nodeInfo': { + return { + none: [], + } + } + case 'portal_historyRoutingTableInfo': { + return { + none: [], + } + } + case 'portal_historyPing': { + return { + enr: state.ENR, + } + } + case 'portal_historyFindNodes': { + return { + nodeId: state.NODEID, + distances: state.DISTANCES, + } + } + case 'portal_historyFindContent': { + return { + nodeId: state.NODEID, + contentKey: state.CONTENT_KEY, + } + } + case 'portal_historyRecursiveFindContent': { + return { + contentKey: state.CONTENT_KEY, + } + } + case 'portal_historyOffer': { + return { + nodeId: state.NODEID, + contentKey: state.CONTENT_KEY, + content: state.CONTENT, + } + } + case 'portal_historySendOffer': { + return { + nodeId: state.NODEID, + contentKeyArray: state.CONTENT_KEY_ARRAY, + } + } + case 'portal_historyGossip': { + return { + contentKey: state.CONTENT_KEY, + content: state.CONTENT, + } + } + case 'eth_getBlockByHash': { + return { + blockHash: state.BLOCK_HASH, + } + } + case 'eth_getBlockByNumber': { + return { + blockNumber: state.BLOCK_NUMBER, + } + } + default: + return {} + } + } + + return ( + + + + + + + + + + + + {Object.entries(params(props.method)).map(([key, entry]) => { + if (typeof entry === 'string') { + let val = entry + if (val.length === 0) { + val = `` + } + return ( + + {key} + + {val.length > 68 ? ( + + + + ) : ( + + )} + + + ) + } + if (entry instanceof Array) { + return ( + + {key} + + {entry.map((e, i) => { + return + })} + + + ) + } + })} + +
+
+
+ ) +} diff --git a/packages/ui/src/Components/Start.tsx b/packages/ui/src/Components/Start.tsx new file mode 100644 index 000000000..f17c6ee3d --- /dev/null +++ b/packages/ui/src/Components/Start.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { trpc } from '../utils/trpc' +import { Button, Container, ListItemText } from '@mui/material' +import { RPCDispatchContext } from '../Contexts/RPCContext' +import { ClientDispatchContext } from '../Contexts/ClientContext' + +export default function Start() { + const dispatch = React.useContext(ClientDispatchContext) + const [started, setStarted] = React.useState(false) + const [nodeId, setNodeId] = React.useState(null) + const [error, setError] = React.useState(null) + const start = trpc.start.useMutation() + + const startUP = async () => { + const client = await start.mutateAsync() + if (client.startsWith('enr')) { + setStarted(true) + setNodeId(client) + dispatch({ + type: 'CONNECTED', + }) + } else { + setError(client) + } + } + + return ( + + + + {nodeId && } + {error && } + + ) +} diff --git a/packages/ui/src/Components/Tabs.tsx b/packages/ui/src/Components/Tabs.tsx index 7a9205271..4321ee2ec 100644 --- a/packages/ui/src/Components/Tabs.tsx +++ b/packages/ui/src/Components/Tabs.tsx @@ -12,6 +12,7 @@ import { AllClientsInitialState, AllClientsReducer, } from '../Contexts/AllClientsContext' +import { Container } from '@mui/material' interface TabPanelProps { children?: React.ReactNode @@ -22,19 +23,16 @@ interface TabPanelProps { function TabPanel(props: TabPanelProps) { const { children, value, index, ...other } = props return ( - + {value === index && {children}} + ) } @@ -48,44 +46,23 @@ function a11yProps(index: number) { export default function ClientTabs() { const [clients, dispatch] = React.useReducer(AllClientsReducer, AllClientsInitialState) - const [value, setValue] = React.useState(0) + const [value, setValue] = React.useState(1) const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValue(newValue) } - const wssClient = trpc.self.useMutation() - const getWSSClient = async () => { - const wssClientInfo = await wssClient.mutateAsync() - dispatch({ - type: 'WSS_INFO', - ...wssClientInfo, - }) - } - - const httpClient = trpc.discv5_nodeInfo.useMutation() - const getNodeInfo = async (port: number = 8545) => { - const nodeInfo = await httpClient.mutateAsync({ port }) - dispatch({ - type: 'HTTP_INFO', - port, - ...nodeInfo, - }) - } - React.useEffect(() => { - getWSSClient() - getNodeInfo() - }, []) - return ( - + @@ -101,18 +78,6 @@ export default function ClientTabs() { TESTS - - Item Four - - - Item Five - - - Item Six - - - Item Seven - diff --git a/packages/ui/src/Contexts/AllClientsContext.tsx b/packages/ui/src/Contexts/AllClientsContext.tsx index 97ad464ce..18be4018d 100644 --- a/packages/ui/src/Contexts/AllClientsContext.tsx +++ b/packages/ui/src/Contexts/AllClientsContext.tsx @@ -1,13 +1,24 @@ +import { QueryClient } from '@tanstack/react-query' +import { createWSClient, wsLink } from '@trpc/client' import { Dispatch, createContext, useContext, useReducer } from 'react' +import { trpc } from '../utils/trpc' + +const trpcClient = trpc.createClient({ + links: [ + wsLink({ + client: createWSClient({ + url: `ws://localhost:3001`, + }), + }), + ], +}) -export const AllClientsContext = createContext(null) -export const AllClientsDispatchContext = createContext(null) export const AllClientsInitialState = { WSS_CLIENT: { client: 'ultralight', enr: 'enr:xxxx...', nodeId: '0x...', - multiAddr: '/ip4/xxx.xxx.xx.xx/udp/xxxx' + multiAddr: '/ip4/xxx.xxx.xx.xx/udp/xxxx', }, HTTP_CLIENTS: { 8545: { @@ -17,7 +28,11 @@ export const AllClientsInitialState = { multiAddr: '/ip4/xxx.xxx.xx.xx/udp/xxxx', }, }, + queryClient: new QueryClient(), + trpcClient, } +export const AllClientsContext = createContext(AllClientsInitialState) +export const AllClientsDispatchContext = createContext(null) export function AllClientsReducer(clients: any, action: any) { switch (action.type) { @@ -63,11 +78,10 @@ export function ALLClientsProvider({ children }: { children: React.ReactNode }) ) } -export function useClients() { +export function useAllClients() { return useContext(AllClientsContext) } -export function useClientsDispatch() { +export function useAllClientsDispatch() { return useContext(AllClientsDispatchContext) } - diff --git a/packages/ui/src/Contexts/ClientContext.tsx b/packages/ui/src/Contexts/ClientContext.tsx index 03fd346c5..efaac1282 100644 --- a/packages/ui/src/Contexts/ClientContext.tsx +++ b/packages/ui/src/Contexts/ClientContext.tsx @@ -1,29 +1,109 @@ import { createContext, useContext, useReducer } from 'react' +import { trpc } from '../utils/trpc' export const ClientDispatchContext = createContext(null) -export const ClientInitialState = { - NODE_INFO: { - tag: 'ultralight', - enr: 'enr:xxxx...', - nodeId: '0x...', - multiAddr: '/ip4/xxx.xxx.xx.xx/udp/xxxx', +export const mutations = { + ws: { + pingBootNodes: trpc.pingBootNodes, + discv5_nodeInfo: trpc.browser_nodeInfo, + portal_historyPing: trpc.ping, + portal_historyRoutingTableInfo: trpc.browser_localRoutingTable, + portal_historyFindNodes: trpc.browser_historyFindNodes, + portal_historyFindContent: trpc.browser_historyFindContent, + portal_historyRecursiveFindContent: trpc.browser_historyRecursiveFindContent, + portal_historyOffer: trpc.browser_historyOffer, + portal_historySendOffer: trpc.browser_historySendOffer, + portal_historyGossip: trpc.browser_historyGossip, + eth_getBlockByHash: trpc.browser_ethGetBlockByHash, + eth_getBlockByNumber: trpc.browser_ethGetBlockByNumber, }, - CONNECTED: false, + http: { + pingBootNodes: trpc.pingBootNodeHTTP, + local_routingTable: trpc.local_routingTable, + discv5_nodeInfo: trpc.discv5_nodeInfo, + portal_historyGetEnr: trpc.portal_historyGetEnr, + portal_historyRoutingTableInfo: trpc.portal_historyRoutingTableInfo, + portal_historyPing: trpc.portal_historyPing, + portal_historyFindNodes: trpc.portal_historyFindNodes, + portal_historyFindContent: trpc.portal_historyFindContent, + portal_historyRecursiveFindContent: trpc.portal_historyRecursiveFindContent, + portal_historyOffer: trpc.portal_historyOffer, + portal_historySendOffer: trpc.portal_historySendOffer, + portal_historyGossip: trpc.portal_historyGossip, + eth_getBlockByHash: trpc.eth_getBlockByHash, + eth_getBlockByNumber: trpc.eth_getBlockByNumber, + }, +} + +export type TMutations = typeof mutations + +interface IClientInitialState { + NODE_INFO: { + tag: string + enr: string + nodeId: string + multiAddr: string + } + CONNECTED: boolean + CONNECTION: 'http' | 'ws' ROUTING_TABLE: { - 0: ['ultralight', 'enr:xxxx...', '0x...', '/ip4/xxx.xxx.xx.xx/udp/xxxx', 0], + [key: number]: [string, string, string, string, number] + } + SELECTED_PEER: string + OUTGOING_ENR: string + BOOTNODES: Record< + string, + { + idx: number + client: string + enr: string + connected: boolean + } + > + SUBSCRIPTION_LOGS: { + [key: string]: { + [key: string]: string[] + } + } + RECEIVED_LOGS: { + [key: string]: { + [key: string]: string[] + } + } + SENT_LOGS: { + [key: string]: { + [key: string]: string[] + } + } + CONTENT_STORE: { + [key: string]: { + type: string + added: string + } + } + + RPC: TMutations +} + +export const ClientInitialState: IClientInitialState = { + NODE_INFO: { + tag: '', + enr: '', + nodeId: '', + multiAddr: '', }, - SELECTED_PEER: {}, + CONNECTION: 'ws', + CONNECTED: false, + ROUTING_TABLE: {}, + SELECTED_PEER: '', OUTGOING_ENR: '', - BOOTNODES: { - ['0x0000']: { - tag: 'ultralight', - enr: 'enr:xxxx...', - connected: 'false', - }, - }, - + BOOTNODES: {}, SUBSCRIPTION_LOGS: {}, + RECEIVED_LOGS: {}, + SENT_LOGS: {}, + CONTENT_STORE: {}, + RPC: mutations, } export const ClientContext = createContext(ClientInitialState) @@ -40,6 +120,12 @@ export function ClientReducer(state: any, action: any) { }, } } + case 'SET_CONNECTION': { + return { + ...state, + CONNECTION: action.connection, + } + } case 'CONNECTED': { return { ...state, @@ -59,14 +145,19 @@ export function ClientReducer(state: any, action: any) { } } case 'BOOTNODES': { + return { + ...state, + BOOTNODES: action.bootnodes, + } + } + case 'CONNECT_BOOTNODE': { return { ...state, BOOTNODES: { ...state.BOOTNODES, [action.nodeId]: { - tag: action.client, - enr: action.enr, - connected: action.response, + ...state.BOOTNODES[action.nodeId], + connected: true, }, }, } @@ -108,13 +199,67 @@ export function ClientReducer(state: any, action: any) { } } case 'LOG_SUBSCRIPTION': { + if (!state.SUBSCRIPTION_LOGS[action.nodeId]) { + state.SUBSCRIPTION_LOGS[action.nodeId] = {} + } + if (!state.SUBSCRIPTION_LOGS[action.nodeId][action.topic]) { + state.SUBSCRIPTION_LOGS[action.nodeId][action.topic] = [] + } return { ...state, SUBSCRIPTION_LOGS: { ...state.SUBSCRIPTION_LOGS, - [action.topic]: { - ...state.SUBSCRIPTION_LOGS[action.topic], - [action.nodeId]: [...state.SUBSCRIPTION_LOGS[action.topic][action.nodeId], action.log], + [action.nodeId]: { + ...state.SUBSCRIPTION_LOGS[action.nodeId], + [action.topic]: [...state.SUBSCRIPTION_LOGS[action.nodeId][action.topic], action.log], + }, + }, + } + } + case 'LOG_RECEIVED': { + if (!state.RECEIVED_LOGS[action.nodeId]) { + state.RECEIVED_LOGS[action.nodeId] = {} + } + if (!state.RECEIVED_LOGS[action.nodeId][action.topic]) { + state.RECEIVED_LOGS[action.nodeId][action.topic] = [] + } + return { + ...state, + RECEIVED_LOGS: { + ...state.RECEIVED_LOGS, + [action.nodeId]: { + ...state.RECEIVED_LOGS[action.nodeId], + [action.topic]: [...state.RECEIVED_LOGS[action.nodeId][action.topic], action.log], + }, + }, + } + } + case 'LOG_SENT': { + if (!state.SENT_LOGS[action.nodeId]) { + state.SENT_LOGS[action.nodeId] = {} + } + if (!state.SENT_LOGS[action.nodeId][action.topic]) { + state.SENT_LOGS[action.nodeId][action.topic] = [] + } + return { + ...state, + SENT_LOGS: { + ...state.SENT_LOGS, + [action.nodeId]: { + ...state.SENT_LOGS[action.nodeId], + [action.topic]: [...state.SENT_LOGS[action.nodeId][action.topic], action.log], + }, + }, + } + } + case 'CONTENT_STORE': { + return { + ...state, + CONTENT_STORE: { + ...state.CONTENT_STORE, + [action.contentKey]: { + type: action.content.type, + added: action.content.added, }, }, } @@ -127,6 +272,7 @@ export function ClientReducer(state: any, action: any) { export function ClientProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(ClientReducer, ClientInitialState) + return ( {children} diff --git a/packages/ui/src/Contexts/RPCContext.tsx b/packages/ui/src/Contexts/RPCContext.tsx new file mode 100644 index 000000000..e24f2033d --- /dev/null +++ b/packages/ui/src/Contexts/RPCContext.tsx @@ -0,0 +1,205 @@ +import { createContext, useContext, useReducer } from 'react' +import { TMutations } from './ClientContext' +import { trpc } from '../utils/trpc' +import { BuildProcedure } from '@trpc/server' + +export const RPCDispatchContext = createContext(null) + +interface IMethods { +pingBootNodes: typeof trpc.pingBootNodes +discv5_nodeInfo: typeof trpc.browser_nodeInfo +portal_historyPing: typeof trpc.ping +portal_historyRoutingTableInfo: typeof trpc.browser_localRoutingTable +portal_historyFindNodes: typeof trpc.browser_historyFindNodes +portal_historyFindContent: typeof trpc.browser_historyFindContent +portal_historyLocalContent: typeof trpc.browser_historyLocalContent +portal_historyRecursiveFindContent: typeof trpc.browser_historyRecursiveFindContent +portal_historyOffer: typeof trpc.browser_historyOffer +portal_historySendOffer: typeof trpc.browser_historySendOffer +portal_historyStore: typeof trpc.browser_historyStore +portal_historyGossip: typeof trpc.browser_historyGossip +eth_getBlockByHash: typeof trpc.browser_ethGetBlockByHash +eth_getBlockByNumber: typeof trpc.browser_ethGetBlockByNumber +} + +export const wsMethods = { + pingBootNodes: trpc.pingBootNodes, + discv5_nodeInfo: trpc.browser_nodeInfo, + portal_historyPing: trpc.ping, + portal_historyRoutingTableInfo: trpc.browser_localRoutingTable, + portal_historyFindNodes: trpc.browser_historyFindNodes, + portal_historyFindContent: trpc.browser_historyFindContent, + portal_historyLocalContent: trpc.browser_historyLocalContent, + portal_historyRecursiveFindContent: trpc.browser_historyRecursiveFindContent, + portal_historyOffer: trpc.browser_historyOffer, + portal_historySendOffer: trpc.browser_historySendOffer, + portal_historyGossip: trpc.browser_historyGossip, + portal_historyStore: trpc.browser_historyStore, + eth_getBlockByHash: trpc.browser_ethGetBlockByHash, + eth_getBlockByNumber: trpc.browser_ethGetBlockByNumber, +} + +export const httpMethods = { + pingBootNodes: trpc.pingBootNodeHTTP, + local_routingTable: trpc.local_routingTable, + discv5_nodeInfo: trpc.discv5_nodeInfo, + portal_historyPing: trpc.portal_historyPing, + portal_historyRoutingTableInfo: trpc.portal_historyRoutingTableInfo, + portal_historyFindNodes: trpc.portal_historyFindNodes, + portal_historyFindContent: trpc.portal_historyFindContent, + portal_historyLocalContent: trpc.portal_historyLocalContent, + portal_historyRecursiveFindContent: trpc.portal_historyRecursiveFindContent, + portal_historyOffer: trpc.portal_historyOffer, + portal_historySendOffer: trpc.portal_historySendOffer, + portal_historyGossip: trpc.portal_historyGossip, + portal_historyStore: trpc.portal_historyStore, + eth_getBlockByHash: trpc.eth_getBlockByHash, + eth_getBlockByNumber: trpc.eth_getBlockByNumber, + +} +export type WSMethods = typeof wsMethods +export type HttpMethods = typeof httpMethods +export type TMethods = WSMethods | HttpMethods + +interface IRPCInitialState { + PORT: number + IP?: string + CONTENT_KEY: string + CONTENT: string + CONTENT_KEY_ARRAY: string[] + ENR: string + NODEID: string + DISTANCES: number[] + BLOCK_HASH: string + BLOCK_NUMBER: string + CURRENT_LOG: { + request: string | undefined + response: string | object | undefined + } + REQUEST: TMethods +} + +export const RPCInitialState: IRPCInitialState = { + PORT: 8545, + CONTENT_KEY: '', + CONTENT: '', + CONTENT_KEY_ARRAY: [""], + ENR: '', + NODEID: '', + DISTANCES: [], + BLOCK_HASH: '', + BLOCK_NUMBER: '', + CURRENT_LOG: { + request: undefined, + response: undefined, + }, + REQUEST: wsMethods, +} +export const RPCContext = createContext(RPCInitialState) + +export function RPCReducer(state: any, action: any) { + switch (action.type) { + case 'PORT': { + return { + ...state, + PORT: action.port, + } + } + case 'IP': { + return { + ...state, + IP: action.ip, + } + } + case 'RPC_ADDR': { + return { + ...state, + PORT: action.port, + IP: action.ip, + } + } + case 'CONTENT_KEY': { + return { + ...state, + CONTENT_KEY: action.contentKey, + } + } + case 'CONTENT': { + return { + ...state, + CONTENT: action.content, + } + } + case 'CONTENT_KEY_ARRAY': { + return { + ...state, + CONTENT_KEY_ARRAY: action.contentKeyArray, + } + } + case 'ENR': { + return { + ...state, + ENR: action.enr, + } + } + case 'NODEID': { + return { + ...state, + NODEID: action.nodeId, + } + } + case 'DISTANCES': { + return { + ...state, + DISTANCES: action.distances, + } + } + case 'BLOCK_HASH': { + return { + ...state, + BLOCK_HASH: action.blockHash, + } + } + case 'BLOCK_NUMBER': { + return { + ...state, + BLOCK_NUMBER: action.blockNumber, + } + } + case 'CURRENT_REQUEST': { + return { + ...state, + CURRENT_LOG: { + ...state.CURRENT_LOG, + request: action.request, + }, + } + } + case 'CURRENT_RESPONSE': { + return { + ...state, + CURRENT_LOG: { + ...state.CURRENT_LOG, + response: action.response, + }, + } + } + default: { + throw Error('Unknown action: ' + action.type) + } + } +} + +export function RPCProvider({ children }: { children: React.ReactNode }) { + const [state, dispatch] = useReducer(RPCReducer, RPCInitialState) + return ( + + {children} + + ) +} + +export function useRPC() { + const context = useContext(RPCContext) + return context +} diff --git a/packages/ui/src/server/procedures.ts b/packages/ui/src/server/procedures.ts new file mode 100644 index 000000000..007fbedd5 --- /dev/null +++ b/packages/ui/src/server/procedures.ts @@ -0,0 +1,409 @@ +import { + ENR, + ContentLookup, + HistoryProtocol, + PortalNetwork, + ProtocolId, + fromHexString, + toHexString, +} from 'portalnetwork' +import { PublicProcudure } from './subscriptions.js' +import { z } from 'zod' +import { + z_historyFindContentParams, + z_historyFindContentResult, + z_historyGossipParams, + z_historyGossipResult, + z_historyLocalContentParams, + z_historyLocalContentResult, + z_historyOfferParams, + z_historyOfferResult, + z_historyPingParams, + z_historyPingResult, + z_historyRecursiveFindContentParams, + z_historyRecursiveFindContentResult, + z_historySendOfferParams, + z_historySendOfferResult, + z_historyStoreParams, + z_historyStoreResult, +} from './rpc/trpcTypes.js' +import { toJSON } from '../util.js' +import { BitArray } from '@chainsafe/ssz' +const bootnodeENRs = [ + 'enr:-I24QDy_atpK3KlPjl6X5yIrK7FosdHI1cW0I0MeiaIVuYg3AEEH9tRSTyFb2k6lpUiFsqxt8uTW3jVMUzoSlQf5OXYBY4d0IDAuMS4wgmlkgnY0gmlwhKEjVaWJc2VjcDI1NmsxoQOSGugH1jSdiE_fRK1FIBe9oLxaWH8D_7xXSnaOVBe-SYN1ZHCCIyg', + 'enr:-I24QIdQtNSyUNcoyR4R7pWLfGj0YuX550Qld0HuInYo_b7JE9CIzmi2TF9hPg-OFL3kebYgLjnPkRu17niXB6xKQugBY4d0IDAuMS4wgmlkgnY0gmlwhJO2oc6Jc2VjcDI1NmsxoQJal-rNlNBoOMikJ7PcGk1h6Mlt_XtTWihHwOKmFVE-GoN1ZHCCIyg', + 'enr:-I24QI_QC3IsdxHUX_jk8udbQ4U2bv-Gncsdg9GzgaPU95ayHdAwnH7mY22A6ggd_aZegFiBBOAPamkP2pyHbjNH61sBY4d0IDAuMS4wgmlkgnY0gmlwhJ31OTWJc2VjcDI1NmsxoQMo_DLYhV1nqAVC1ayEIwrhoFCcHvWuhC_J-w-n_4aHP4N1ZHCCIyg', + 'enr:-IS4QGUtAA29qeT3cWVr8lmJfySmkceR2wp6oFQtvO_uMe7KWaK_qd1UQvd93MJKXhMnubSsTQPJ6KkbIu0ywjvNdNEBgmlkgnY0gmlwhMIhKO6Jc2VjcDI1NmsxoQJ508pIqRqsjsvmUQfYGvaUFTxfsELPso_62FKDqlxI24N1ZHCCI40', + 'enr:-IS4QNaaoQuHGReAMJKoDd6DbQKMbQ4Mked3Gi3GRatwgRVVPXynPlO_-gJKRF_ZSuJr3wyHfwMHyJDbd6q1xZQVZ2kBgmlkgnY0gmlwhMIhKO6Jc2VjcDI1NmsxoQM2kBHT5s_Uh4gsNiOclQDvLK4kPpoQucge3mtbuLuUGYN1ZHCCI44', + 'enr:-IS4QBdIjs6S1ZkvlahSkuYNq5QW3DbD-UDcrm1l81f2PPjnNjb_NDa4B5x4olHCXtx0d2ZeZBHQyoHyNnuVZ-P1GVkBgmlkgnY0gmlwhMIhKO-Jc2VjcDI1NmsxoQOO3gFuaCAyQKscaiNLC9HfLbVzFdIerESFlOGcEuKWH4N1ZHCCI40', + 'enr:-IS4QM731tV0CvQXLTDcZNvgFyhhpAjYDKU5XLbM7sZ1WEzIRq4zsakgrv3KO3qyOYZ8jFBK-VzENF8o-vnykuQ99iABgmlkgnY0gmlwhMIhKO-Jc2VjcDI1NmsxoQMTq6Cdx3HmL3Q9sitavcPHPbYKyEibKPKvyVyOlNF8J4N1ZHCCI44', + 'enr:-IS4QFV_wTNknw7qiCGAbHf6LxB-xPQCktyrCEZX-b-7PikMOIKkBg-frHRBkfwhI3XaYo_T-HxBYmOOQGNwThkBBHYDgmlkgnY0gmlwhKRc9_OJc2VjcDI1NmsxoQKHPt5CQ0D66ueTtSUqwGjfhscU_LiwS28QvJ0GgJFd-YN1ZHCCE4k', + 'enr:-IS4QDpUz2hQBNt0DECFm8Zy58Hi59PF_7sw780X3qA0vzJEB2IEd5RtVdPUYZUbeg4f0LMradgwpyIhYUeSxz2Tfa8DgmlkgnY0gmlwhKRc9_OJc2VjcDI1NmsxoQJd4NAVKOXfbdxyjSOUJzmA4rjtg43EDeEJu1f8YRhb_4N1ZHCCE4o', + 'enr:-IS4QGG6moBhLW1oXz84NaKEHaRcim64qzFn1hAG80yQyVGNLoKqzJe887kEjthr7rJCNlt6vdVMKMNoUC9OCeNK-EMDgmlkgnY0gmlwhKRc9-KJc2VjcDI1NmsxoQLJhXByb3LmxHQaqgLDtIGUmpANXaBbFw3ybZWzGqb9-IN1ZHCCE4k', + 'enr:-IS4QA5hpJikeDFf1DD1_Le6_ylgrLGpdwn3SRaneGu9hY2HUI7peHep0f28UUMzbC0PvlWjN8zSfnqMG07WVcCyBhADgmlkgnY0gmlwhKRc9-KJc2VjcDI1NmsxoQJMpHmGj1xSP1O-Mffk_jYIHVcg6tY5_CjmWVg1gJEsPIN1ZHCCE4o', +] +export const bootnodes = bootnodeENRs.map((b) => { + const enr = ENR.decodeTxt(b) + const tag = enr.kvs.get('c') + const c = tag ? tag.toString() : '' + const nodeId = enr.nodeId + return { + enr: b, + nodeId, + c, + } +}) +export const websocketProcedures = (portal: PortalNetwork, publicProcedure: PublicProcudure) => { + const history = portal.protocols.get(ProtocolId.HistoryNetwork) as HistoryProtocol + + const start = publicProcedure + .meta({ + description: 'Start Portal Network', + }) + .mutation(async () => { + if (portal.discv5.isStarted()) return 'Already started' + try { + await portal.start() + portal.discv5.isStarted() + ? console.log('Discv5 started') + : console.log('Discv5 not started') + return portal.discv5.enr.encodeTxt() + } catch (err: any) { + console.log('PORTAL_START_ERROR', err.message) + return err.message + } + }) + + const browser_nodeInfo = publicProcedure + .meta({ + description: 'Get ENR, NodeId, Client Tag, and MultiAddress', + }) + .mutation(() => { + return { + enr: portal.discv5.enr.encodeTxt(), + nodeId: portal.discv5.enr.nodeId, + client: 'ultralight', + multiAddr: portal.discv5.enr.getLocationMultiaddr('udp')?.toString(), + } + }) + + const browser_localRoutingTable = publicProcedure + .meta({ + description: 'Get Local Routing Table', + }) + .mutation(() => { + return [...history.routingTable.buckets.entries()] + .filter(([_, bucket]) => bucket.values().length > 0) + .map(([idx, bucket]) => { + return bucket + .values() + .map((enr) => [ + enr.kvs.get('c')?.toString() ?? '', + enr.encodeTxt(), + enr.nodeId, + enr.getLocationMultiaddr('udp')!.toString(), + idx, + ]) + }) + .flat() + }) + + const ping = publicProcedure + .meta({ + description: 'Send Ping to ENR', + }) + .input(z_historyPingParams) + .output(z_historyPingResult) + .mutation(async ({ input }) => { + const res = await history.sendPing(input.enr) + const pong = res + ? { + enrSeq: Number(res.enrSeq), + dataRadius: toHexString(res.customPayload), + } + : undefined + console.log('ping', { input, pong }) + return pong + }) + + const pingBootNodes = publicProcedure + .meta({ + description: 'Ping all BootNodes', + }) + .output( + z.record( + z.string(), + z.object({ + idx: z.number(), + client: z.string(), + nodeId: z.string(), + enr: z.string(), + connected: z.boolean(), + }), + ), + ) + .mutation(async () => { + const pongs = [] + for await (const [idx, enr] of bootnodes.entries()) { + const pong = await history.sendPing(enr.enr) + pongs.push([ + enr.nodeId, + { + idx, + client: idx < 3 ? 'trin' : idx < 7 ? 'fluffy' : 'ultralight', + nodeId: enr.nodeId, + enr: enr.enr, + connected: pong ? true : false, + }, + ]) + } + return Object.fromEntries(pongs) + }) + + const browser_historyStore = publicProcedure + .meta({ + description: 'Store Content', + }) + .input(z_historyStoreParams) + .output(z_historyStoreResult) + .mutation(async ({ input }) => { + const key = fromHexString(input.contentKey) + try { + await history.store(key[0], toHexString(key.slice(1)), fromHexString(input.content)) + } catch { + return false + } + const stored = await history.findContentLocally(key) + return stored.length > 0 + }) + + const browser_historyLocalContent = publicProcedure + .meta({ + description: 'Get Local Content', + }) + .input(z_historyLocalContentParams) + .output(z_historyLocalContentResult) + .mutation(async ({ input }) => { + const contentKey = fromHexString(input.contentKey) + const res = await history.findContentLocally(contentKey) + return toJSON(contentKey, res) + }) + + const browser_historyFindContent = publicProcedure + .meta({ + description: 'Find Content', + }) + .input(z_historyFindContentParams) + .output(z_historyFindContentResult) + .mutation(async ({ input }) => { + const contentKey = fromHexString(input.contentKey) + const res = await history.sendFindContent(input.nodeId, contentKey) + console.log('browser_historyFindContent', { input, contentKey, res }) + if (!res) return undefined + + switch (res?.selector) { + case 0: { + return { + content: toHexString(res.value as Uint8Array), + utpTransfer: true, + asJSON: toJSON(contentKey, res.value), + } + } + case 1: { + return { + content: toHexString(res.value as Uint8Array), + asJSON: toJSON(contentKey, res.value), + } + } + case 2: { + return { enrs: (res.value).map(toHexString) } + } + default: { + return undefined + } + } + }) + + const browser_historyRecursiveFindContent = publicProcedure + .meta({ + description: 'Recursive Find Content', + }) + .input(z_historyRecursiveFindContentParams) + .output(z_historyRecursiveFindContentResult) + .mutation(async ({ input }) => { + const contentKey = fromHexString(input.contentKey) + const lookup = new ContentLookup(history, contentKey) + const res = await lookup.startLookup() + return !res + ? undefined + : 'content' in res + ? { content: toJSON(contentKey, res.content), utpTransfer: res.utp } + : { enrs: res.enrs.map(toHexString) } + }) + + const browser_historyOffer = publicProcedure + .meta({ + description: 'Offer Content', + }) + .input(z_historyOfferParams) + .output(z_historyOfferResult) + .mutation(async ({ input }) => { + const contentKey = fromHexString(input.contentKey) + await history.store( + contentKey[0], + toHexString(contentKey.slice(1)), + fromHexString(input.content), + ) + const res = await history.sendOffer(input.enr, [contentKey]) + if (!res) return undefined + if (res instanceof BitArray) { + return res.toBoolArray() + } else { + return [] + } + }) + + const browser_historySendOffer = publicProcedure + .meta({ + description: 'Send Offer', + }) + .input(z_historySendOfferParams) + .output(z_historySendOfferResult) + .mutation(async ({ input }) => { + const res = await history.sendOffer(input.nodeId, input.contentKeys.map(fromHexString)) + if (!res) { + return { + result: undefined, + response: undefined, + } + } else { + const enr = history.routingTable.getWithPending(input.nodeId)?.value + const result = enr ? '0x' + enr.seq.toString(16) : undefined + if (res instanceof BitArray) { + return { result, response: res.toBoolArray() } + } else { + return { result, response: [] } + } + } + }) + + const browser_historyGossip = publicProcedure + .meta({ + description: 'Gossip Content', + }) + .input(z_historyGossipParams) + .output(z_historyGossipResult) + .mutation(async ({ input }) => { + const res = await history.gossipContent( + fromHexString(input.contentKey), + fromHexString(input.content), + ) + return res + }) + + const browser_ethGetBlockByHash = publicProcedure + .meta({ + description: 'Get Block By Hash', + }) + .input( + z.object({ + blockHash: z.string(), + includeTransactions: z.boolean(), + }), + ) + .output( + z.union([ + z.undefined(), + z.object({ + number: z.string(), + hash: z.string(), + parentHash: z.string(), + nonce: z.string(), + sha3Uncles: z.string(), + logsBloom: z.string(), + transactionsRoot: z.string(), + stateRoot: z.string(), + receiptsRoot: z.string(), + miner: z.string(), + difficulty: z.string(), + totalDifficulty: z.string(), + extraData: z.string(), + size: z.string(), + gasLimit: z.string(), + gasUsed: z.string(), + timestamp: z.string(), + transactions: z.array(z.string()), + uncles: z.array(z.string()), + }), + z.string(), + ]), + ) + .mutation(async ({ input }) => { + const block = await history.ETH.getBlockByHash(input.blockHash, input.includeTransactions) + if (!block) return undefined + return JSON.stringify(block.toJSON()) + }) + + const browser_ethGetBlockByNumber = publicProcedure + .meta({ + description: 'Get Block By Number', + }) + .input( + z.object({ + blockNumber: z.string(), + includeTransactions: z.boolean(), + }), + ) + .output( + z.union([ + z.undefined(), + z.object({ + number: z.string(), + hash: z.string(), + parentHash: z.string(), + nonce: z.string(), + sha3Uncles: z.string(), + logsBloom: z.string(), + transactionsRoot: z.string(), + stateRoot: z.string(), + receiptsRoot: z.string(), + miner: z.string(), + difficulty: z.string(), + totalDifficulty: z.string(), + extraData: z.string(), + size: z.string(), + gasLimit: z.string(), + gasUsed: z.string(), + timestamp: z.string(), + transactions: z.array(z.string()), + uncles: z.array(z.string()), + }), + z.string(), + ]), + ) + .mutation(async ({ input }) => { + const block = await history.ETH.getBlockByNumber( + BigInt(input.blockNumber), + input.includeTransactions, + ) + if (!block) return undefined + return JSON.stringify(block.toJSON()) + }) + + return { + start, + browser_nodeInfo, + browser_localRoutingTable, + ping, + pingBootNodes, + browser_historyStore, + browser_historyLocalContent, + browser_historyFindContent, + browser_historyRecursiveFindContent, + browser_historyOffer, + browser_historySendOffer, + browser_historyGossip, + browser_ethGetBlockByHash, + browser_ethGetBlockByNumber, + } +} diff --git a/packages/ui/src/server/rpc/procedures.ts b/packages/ui/src/server/rpc/procedures.ts new file mode 100644 index 000000000..3ecc26f25 --- /dev/null +++ b/packages/ui/src/server/rpc/procedures.ts @@ -0,0 +1,348 @@ +import { ENR, fromHexString, toHexString } from 'portalnetwork' +import { PublicProcudure } from '../subscriptions.js' +import { z } from 'zod' +import jayson from 'jayson/promise/index.js' +import { + z_RoutingTableInfoResult, + z_historyFindContentParams, + z_historyFindContentResult, + z_historyFindNodesParams, + z_historyFindNodesResult, + z_historyGossipParams, + z_historyGossipResult, + z_historyLocalContentParams, + z_historyLocalContentResult, + z_historyOfferParams, + z_historyOfferResult, + z_historyPingParams, + z_historyPingResult, + z_historyRecursiveFindContentParams, + z_historyRecursiveFindContentResult, + z_historySendOfferParams, + z_historySendOfferResult, + z_historyStoreParams, + z_historyStoreResult, + z_nodeId, + z_ui, +} from './trpcTypes.js' +import { bootnodes } from '../procedures.js' +import { toJSON } from '../../util.js' + +export const httpProcedures = (publicProcedure: PublicProcudure, ipAddr: string) => { + const httpClient = (port: number = 8545, ip: string = ipAddr) => { + return jayson.Client.http({ + host: ip, + port: port, + }) + } + /** + * HTTP Client Methods + */ + const getPubIp = publicProcedure.input(z.undefined()).query(() => { + console.log(ipAddr) + return ipAddr + }) + + const portal_historyGetEnr = publicProcedure + .meta({ + description: 'Get ENR', + }) + .input( + z.object({ port: z.number(), ip: z.union([z.undefined(), z.string()]), nodeId: z_nodeId }), + ) + .output( + z.union([ + z.string(), + z.object({ + enr: z.string(), + nodeId: z.string(), + multiaddr: z.string(), + c: z.string(), + }), + ]), + ) + .mutation(async ({ input }) => { + const client = httpClient(input.port, input.ip) + const res = await client.request('portal_historyGetEnr', [input.nodeId]) + if (res.result && res.result.startsWith('enr')) { + const enr = ENR.decodeTxt(res.result) + return { + enr: res.result, + nodeId: enr.nodeId, + multiaddr: enr.getLocationMultiaddr('udp')?.toString() ?? '', + c: enr.kvs.get('c')?.toString() ?? '', + } + } + return '' + }) + + /** + * {@link portal_historyRoutingTableInfo} + */ + const portal_historyRoutingTableInfo = publicProcedure + .meta({ + description: 'Get Local Routing Table Info', + }) + .input(z.object({ port: z.number(), ip: z.union([z.undefined(), z.string()]) })) + .output(z_RoutingTableInfoResult) + .mutation(async ({ input }) => { + const client = httpClient(input.port, input.ip) + const res = await client.request('portal_historyRoutingTableInfo', []) + const routingTable = res.result ?? { localNodeId: '', buckets: [] } + return routingTable + }) + + const local_routingTable = publicProcedure + .meta({ + description: 'Get Local Routing Table Info', + }) + .input(z.object({ port: z.number(), ip: z.union([z.undefined(), z.string()]) })) + .output(z.array(z.array(z.union([z.number(), z.string()])))) + .mutation(async ({ input }) => { + const client = httpClient(input.port, input.ip) + const res = await client.request('portal_historyRoutingTableInfo', []) + const routingTable = res.result ?? { localNodeId: 'err', buckets: [] } + const buckets: [string, string, string, string, number][] = [ + ...routingTable.buckets.entries(), + ] + .filter(([_, bucket]) => bucket.length > 0) + .map(([idx, bucket]) => { + return bucket.map((nodeId: string) => [nodeId, 256 - idx]) + }) + .flat() + + return buckets + }) + + const discv5_nodeInfo = publicProcedure + .meta({ + description: 'Get ENR, NodeId, Client Tag, and MultiAddress', + }) + .input( + z.object({ + port: z.number(), + ip: z.union([z.undefined(), z.string()]), + }), + ) + .mutation(async ({ input }) => { + console.log('discv5_nodeInfo request:', input) + const client = httpClient(input.port, input.ip) + const info = await client.request('discv5_nodeInfo', []) + console.log('discv5_nodeInfo result:', info.result) + if (!info.result) { + return { + client: '', + enr: '', + nodeId: '', + multiAddr: '', + } + } + const enr = ENR.decodeTxt(info.result.enr) + return { + client: enr.kvs.get('c')?.toString(), + enr: info.result.enr, + nodeId: info.result.nodeId, + multiAddr: (await enr.getFullMultiaddr('udp'))?.toString(), + } + }) + + const portal_historyPing = publicProcedure + .meta({ + description: 'Send Ping to ENR', + }) + .input(z_ui(z_historyPingParams)) + .output(z.any()) + .mutation(async ({ input }) => { + const client = httpClient(input.port, input.ip) + const res = await client.request('portal_historyPing', [input.enr]) + const _pong = res.result + const pong = _pong + ? { + enrSeq: Number(_pong.enrSeq), + dataRadius: toHexString(_pong.dataRadius), + } + : { + enrSeq: 0, + dataRadius: '', + } + console.log('portal_historyPing', { input, res, pong }) + return pong + }) + + const pingBootNodeHTTP = publicProcedure + .meta({ + description: 'Ping all BootNodes', + }) + .input( + z.object({ + port: z.number(), + ip: z.union([z.undefined(), z.string()]), + }), + ) + .output( + z.record( + z.string(), + z.object({ + idx: z.number(), + client: z.string(), + nodeId: z.string(), + enr: z.string(), + connected: z.boolean(), + }), + ), + ) + .mutation(async ({ input }) => { + const client = httpClient(input.port, input.ip) + const pongs = [] + for await (const [idx, enr] of bootnodes.entries()) { + const p = await client.request('portal_historyPing', [enr.enr]) + const pongRes = p.result + const pong = { + idx, + client: `${idx < 3 ? 'trin' : idx < 7 ? 'fluffy' : 'ultralight'}`, + enr: enr.enr, + nodeId: enr.nodeId, + connected: pongRes ? true : false, + } + + pongs.push([enr.nodeId, pong]) + } + return Object.fromEntries(pongs) + }) + + const portal_historyStore = publicProcedure + .meta({ + description: 'Store Content', + }) + .input(z_ui(z_historyStoreParams)) + .output(z_historyStoreResult) + .mutation(async ({ input }) => { + const res = await httpClient(input.port, input.ip).request('portal_historyStore', [ + input.contentKey, + input.content, + ]) + console.log('portal_historyStore', { input, res }) + return res.result + }) + + const portal_historyLocalContent = publicProcedure + .meta({ + description: 'Get Content from local DB', + }) + .input(z_ui(z_historyLocalContentParams)) + .output(z_historyLocalContentResult) + .mutation(async ({ input }) => { + console.log('portal_historyLocalContent') + const res = await httpClient(input.port, input.ip).request('portal_historyLocalContent', [ + input.contentKey, + ]) + try { + return toJSON(fromHexString(input.contentKey), fromHexString(res.result)) + } catch { + return res.result + } + }) + + const portal_historyFindNodes = publicProcedure + .meta({ + description: 'Find Nodes', + }) + .input(z_ui(z_historyFindNodesParams)) + .output(z_historyFindNodesResult) + .mutation(async ({ input }) => { + const res = await httpClient(input.port, input.ip).request('portal_historyFindNodes', [ + input.enr, + input.distances, + ]) + return res.result + }) + + const portal_historyFindContent = publicProcedure + .meta({ + description: 'Find Content', + }) + .input(z_ui(z_historyFindContentParams)) + .output(z_historyFindContentResult) + .mutation(async ({ input }) => { + const res = await httpClient(input.port, input.ip).request('portal_historyFindContent', [ + input.enr, + input.contentKey, + ]) + return res.result + }) + + const portal_historyRecursiveFindContent = publicProcedure + .meta({ + description: 'Recursive Find Content', + }) + .input(z_ui(z_historyRecursiveFindContentParams)) + .output(z_historyRecursiveFindContentResult) + .mutation(async ({ input }) => { + const res = await httpClient(input.port, input.ip).request( + 'portal_historyRecursiveFindContent', + [input.contentKey], + ) + return res.result + }) + + const portal_historyOffer = publicProcedure + .meta({ + description: 'Offer Content', + }) + .input(z_ui(z_historyOfferParams)) + .output(z_historyOfferResult) + .mutation(async ({ input }) => { + const res = await httpClient(input.port, input.ip).request('portal_historyOffer', [ + input.enr, + input.contentKey, + input.content, + ]) + return res.result + }) + + const portal_historySendOffer = publicProcedure + .meta({ + description: 'Send Offer', + }) + .input(z_ui(z_historySendOfferParams)) + .output(z_historySendOfferResult) + .mutation(async ({ input }) => { + const res = await httpClient(input.port, input.ip).request('portal_historySendOffer', [ + input.nodeId, + input.contentKeys, + ]) + return res.result + }) + + const portal_historyGossip = publicProcedure + .meta({ + description: 'Gossip Content', + }) + .input(z_ui(z_historyGossipParams)) + .output(z_historyGossipResult) + .mutation(async ({ input }) => { + const res = await httpClient(input.port, input.ip).request('portal_historyGossip', [ + input.contentKey, + input.content, + ]) + return res.result + }) + + return { + getPubIp, + portal_historyRoutingTableInfo, + local_routingTable, + discv5_nodeInfo, + pingBootNodeHTTP, + portal_historyGetEnr, + portal_historyStore, + portal_historyLocalContent, + portal_historyPing, + portal_historyFindNodes, + portal_historyFindContent, + portal_historyRecursiveFindContent, + portal_historyOffer, + portal_historySendOffer, + portal_historyGossip, + } +} diff --git a/packages/ui/src/server/rpc/trpcTypes.ts b/packages/ui/src/server/rpc/trpcTypes.ts new file mode 100644 index 000000000..4452598f4 --- /dev/null +++ b/packages/ui/src/server/rpc/trpcTypes.ts @@ -0,0 +1,146 @@ +import z from 'zod' + +const bit = 2 +const byte = 256 + +export const z_uint = (length: number) => z.number().min(bit ** length) +export const z_bytes = (length: number) => z.number().min(byte ** length) +export const z_hexString = (bytes?: number) => { + return z.string().refine((x) => !bytes || x.length === bytes * 2 + (x.startsWith('0x') ? 2 : 0)) +} +export const z_prefixedHexString = (bytes?: number) => { + return z + .string() + .transform((x) => (x.startsWith('0x') ? x : '0x' + x)) + .refine((x) => !bytes || x.length === bytes * 2) +} +export const z_unprefixedHexString = (bytes?: number) => { + return z + .string() + .transform((x) => (x.startsWith('0x') ? x.slice(2) : x)) + .refine((x) => !bytes || x.length === bytes * 2) +} + +export const z_kBucket = z.array(z_bytes(32)) +export const z_kBucketArray = z.array(z_kBucket) +export const z_DataRadius = z_hexString(32) +export const z_nodeId = z_unprefixedHexString() +export const z_Enr = z.string().startsWith('enr:') +export const z_EnrSeq = z.number() +export const z_ipAddr = z.string() +export const z_socketAddr = z.string() +export const z_udpPort = z_uint(16) +export const z_isTcp = z.boolean() +export const z_distance = z.number() +export const z_RequestId = z_bytes(8) +export const z_ProtocolId = z.number().gte(0).lte(4) +export const z_Discv5Payload = z_hexString() +export const z_ContentKey = z_hexString() +export const z_Content = z.string() + +export const z_ContentMessage = z.object({ + content: z_hexString(), + utpTransfer: z.boolean().optional(), +}) +export const z_ContentNodes = z.object({ + enrs: z.array(z_Enr), +}) + +export const z_NodesMessage = z.array(z_Enr) +export const z_PongMessage = z.object({ + enrSeq: z_EnrSeq, + dataRadius: z_DataRadius, +}) + +export const z_AcceptResult = z.boolean() +export const z_SendAcceptResult = z_RequestId +export const z_AddEnrResult = z.boolean() +export const z_ContentResult = z.boolean() +export const z_SendContentResult = z_RequestId +export const z_DeleteEnrResult = z.boolean() +export const z_FindContentResult = z_ContentMessage +export const z_FindNodeResult = z_NodesMessage +export const z_LookupEnrResult = z_Enr.optional() +export const z_OfferResult = z.union([z.array(z.boolean()), z.undefined()]) +export const z_SendOfferResult = z_hexString() +export const z_RecursiveFindContentResult = z_hexString() +export const z_RoutingTableInfoResult = z.object({ + localNodeId: z_bytes(32), + buckets: z_kBucketArray, +}) + +export const z_NodeInfoResult = z.object({ + nodeId: z_hexString(32), + enr: z_Enr, +}) +export const z_historyRoutingTableInfoParams = z.array(z.never()).length(0) + +export const z_historyAddEnrParams = z.array(z_Enr).length(1) +export const z_historyAddEnrsParams = z.array(z_Enr) +export const z_historyAddBootnodeParams = z.array(z_Enr).length(1) +export const z_historyLookupEnrParams = z.array(z_nodeId).length(1) +export const z_historyGetEnrParams = z.array(z_nodeId).length(1) +export const z_historyDeleteEnrParams = z.array(z_nodeId).length(1) + +export const z_GetEnrResult = z_Enr + +export const z_historyStoreParams = z.object({ + contentKey: z_ContentKey, + content: z_Content, +}) +export const z_historyLocalContentParams = z.object({ + contentKey: z_ContentKey, +}) +export const z_historyPingParams = z.object({ enr: z_Enr }) +export const z_historyFindNodesParams = z.object({ + enr: z_Enr, + distances: z.array(z_distance), +}) +export const z_historyFindContentParams = z.object({ + nodeId: z_nodeId, + contentKey: z_ContentKey, +}) +export const z_historyRecursiveFindContentParams = z.object({ + contentKey: z_ContentKey, +}) +export const z_historyOfferParams = z.object({ + enr: z_Enr, + contentKey: z_ContentKey, + content: z_Content, +}) +export const z_historySendOfferParams = z.object({ + nodeId: z_nodeId, + contentKeys: z.array(z_ContentKey), +}) +export const z_historyGossipParams = z.object({ + contentKey: z_ContentKey, + content: z_Content, +}) + +export const ui_port = z.object({ port: z.number() }) + +export const z_ui = (params: z.AnyZodObject) => { + return ui_port.merge(params) +} + +export const z_historyJSONContent = z.object({ + content: z.string(), +}) + +export const z_historyStoreResult = z.boolean() +export const z_historyLocalContentResult = z_Content +export const z_historyPingResult = z.union([z_PongMessage, z.undefined()]) +export const z_historyFindNodesResult = z_NodesMessage +export const z_historyFindContentResult = z.union([ + z_ContentMessage, + z_ContentNodes, + z_historyJSONContent, + z.undefined(), +]) +export const z_historyRecursiveFindContentResult = z_historyFindContentResult +export const z_historyOfferResult = z_OfferResult +export const z_historySendOfferResult = z.object({ + result: z.union([z_SendOfferResult, z.undefined()]), + response: z.union([z_OfferResult, z.undefined()]), +}) +export const z_historyGossipResult = z.number() diff --git a/packages/ui/src/server/server.ts b/packages/ui/src/server/server.ts new file mode 100644 index 000000000..d3e7948e7 --- /dev/null +++ b/packages/ui/src/server/server.ts @@ -0,0 +1,222 @@ +import { initTRPC } from '@trpc/server' +// eslint-disable-next-line node/file-extension-in-import +import { createHTTPServer } from '@trpc/server/adapters/standalone' +import cors from 'cors' +import { ENR, SignableENR } from '@chainsafe/discv5' +import { createSecp256k1PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { execSync } from 'child_process' +import { HistoryProtocol, PortalNetwork, ProtocolId } from 'portalnetwork' + +import ws from 'ws' + +// eslint-disable-next-line node/file-extension-in-import +import { applyWSSHandler } from '@trpc/server/adapters/ws' +import { subscriptions } from './subscriptions.js' +import { websocketProcedures } from './procedures.js' +import { httpProcedures } from './rpc/procedures.js' +import { z_Enr } from './rpc/trpcTypes.js' +import { z } from 'zod' +import debug from 'debug' + +const main = async () => { + const t = initTRPC + .context() + .meta<{ + description: string + }>() + .create() + const publicProcedure = t.procedure + const cmd = 'hostname -I' + const pubIp = execSync(cmd).toString().split(' ')[0] + console.log('pubIp', pubIp) + const id = await createSecp256k1PeerId() + const enr = SignableENR.createFromPeerId(id) + const initMa: any = multiaddr(`/ip4/${pubIp}/udp/8546`) + enr.setLocationMultiaddr(initMa) + const config = { + enr: enr, + peerId: id, + config: { + enrUpdate: true, + addrVotesToUpdateEnr: 5, + allowUnverifiedSessions: true, + }, + bindAddrs: { + ip4: initMa, + }, + } as any + const portal = await PortalNetwork.create({ + config: config, + radius: 2n ** 256n - 1n, + supportedProtocols: [ProtocolId.HistoryNetwork], + eventLog: true, + }) + portal.discv5.enableLogs() + portal.enableLog('*') + + const history = portal.protocols.get(ProtocolId.HistoryNetwork) as HistoryProtocol + const router = t.router + + // WSS Client Methods + + const decodeENR = publicProcedure + .input(z_Enr) + .output( + z.object({ + nodeId: z.string(), + multiaddr: z.string(), + c: z.string(), + }), + ) + .mutation(({ input }) => { + const enr = ENR.decodeTxt(input) + const tag = enr.kvs.get('c') + const c = tag ? tag.toString() : '' + const nodeId = enr.nodeId + const multiaddr = enr.getLocationMultiaddr('udp')?.toString() ?? '' + return { + nodeId, + multiaddr, + c, + } + }) + + const { onTalkReq, onTalkResp, onContentAdded, onNodeAdded, onSendTalkReq, onSendTalkResp } = + await subscriptions(portal, history, publicProcedure) + + const { + start, + browser_nodeInfo, + browser_localRoutingTable, + ping, + pingBootNodes, + browser_historyStore, + browser_historyLocalContent, + browser_ethGetBlockByHash, + browser_ethGetBlockByNumber, + browser_historyFindContent, + browser_historyGossip, + browser_historyOffer, + browser_historyRecursiveFindContent, + browser_historySendOffer, + } = websocketProcedures(portal, publicProcedure) + + /** + * HTTP Client Methods + */ + + const { + portal_historyRoutingTableInfo, + local_routingTable, + portal_historyPing, + discv5_nodeInfo, + pingBootNodeHTTP, + portal_historyStore, + getPubIp, + portal_historyGetEnr, + portal_historyFindContent, + portal_historyFindNodes, + portal_historyRecursiveFindContent, + portal_historyOffer, + portal_historySendOffer, + portal_historyGossip, + portal_historyLocalContent, + } = httpProcedures(publicProcedure, pubIp) + + // Create tRpc Router + + const appRouter = router({ + start, + getPubIp, + decodeENR, + onTalkReq, + onTalkResp, + onSendTalkReq, + onSendTalkResp, + onContentAdded, + onNodeAdded, + browser_nodeInfo, + local_routingTable, + browser_localRoutingTable, + ping, + pingBootNodes, + discv5_nodeInfo, + portal_historyRoutingTableInfo, + portal_historyGetEnr, + portal_historyPing, + portal_historyStore, + portal_historyFindContent, + portal_historyFindNodes, + portal_historyRecursiveFindContent, + portal_historyOffer, + portal_historySendOffer, + portal_historyGossip, + portal_historyLocalContent, + browser_historyStore, + browser_historyLocalContent, + browser_ethGetBlockByHash, + browser_ethGetBlockByNumber, + browser_historyFindContent, + browser_historyGossip, + browser_historyOffer, + browser_historyRecursiveFindContent, + browser_historySendOffer, + pingBootNodeHTTP, + }) + + // export only the type definition of the API + // None of the actual implementation is exposed to the client + + const wss = new ws.Server({ + port: 3001, + }) + const handler = applyWSSHandler({ + wss, + router: appRouter, + createContext() { + console.log('context 4') + return {} + }, + onError: (err: any) => { + console.debug(`❌ WebSocket Server Handler error: ${err.message}`) + }, + }) + wss.on('connection', (ws) => { + console.info(`➕➕ Connection (${wss.clients.size})`) + ws.once('close', () => { + console.info(`➖➖ Connection (${wss.clients.size})`) + }) + }) + wss.on('error', (err: any) => { + console.debug(`❌ WebSocket Server error: ${err.message}`) + }) + console.info('✅ WebSocket Server listening on ws://localhost:3001') + process.on('SIGTERM', () => { + clearInterval('update') + clearInterval('updated') + console.warn('SIGTERM') + handler.broadcastReconnectNotification() + wss.close() + }) + + portal.discv5.enableLogs() + portal.enableLog(`*${portal.discv5.enr.nodeId.slice(0, 5)}*`) + debug.enable(`*${portal.discv5.enr.nodeId.slice(0, 5)}*`) + // await portal.start() + // console.log({ enr: portal.discv5.enr.encodeTxt(), nodeId: portal.discv5.enr.nodeId }) + + // create server + createHTTPServer({ + middleware: cors(), + router: appRouter, + createContext() { + console.log('context 3') + return {} + }, + }).listen(8546) +} +main().catch((err) => { + console.error(err) + console.log(`SERVER ERROR: ${err.message}`) +}) diff --git a/packages/ui/src/server/subTypes.ts b/packages/ui/src/server/subTypes.ts new file mode 100644 index 000000000..cea2e21fc --- /dev/null +++ b/packages/ui/src/server/subTypes.ts @@ -0,0 +1,9 @@ +import { MessageCodes } from 'portalnetwork' + +export type Topic = keyof typeof MessageCodes | 'UTP' + +export type TalkRequestResult = { + nodeId: string + topic: Topic + message: string +} diff --git a/packages/ui/src/server/subscriptions.ts b/packages/ui/src/server/subscriptions.ts new file mode 100644 index 000000000..a65651809 --- /dev/null +++ b/packages/ui/src/server/subscriptions.ts @@ -0,0 +1,253 @@ +import { Discv5EventEmitter, ENR, NodeId } from '@chainsafe/discv5' +import { Multiaddr } from '@multiformats/multiaddr' + +import { initTRPC } from '@trpc/server' +// eslint-disable-next-line node/file-extension-in-import +import { observable } from '@trpc/server/observable' +import debug from 'debug' +import { + HistoryProtocol, + MessageCodes, + PortalNetwork, + PortalNetworkEventEmitter, + PortalWireMessageType, + ProtocolId, + fromHexString, + toHexString, +} from 'portalnetwork' + +/** A representation of an unsigned contactable node. */ +export interface INodeAddress { + /** The destination socket address. */ + socketAddr: Multiaddr + /** The destination Node Id. */ + nodeId: NodeId +} + +export interface ITalkReqMessage { + type: 5 + id: bigint + protocol: Buffer + request: Buffer +} +export interface ITalkRespMessage { + type: 6 + id: bigint + response: Buffer +} + +const _portalEvents = [ + 'onTalkReq', + 'onTalkResp', + 'onSendTalkReq', + 'onSendTalkResp', + 'NodeAdded', + 'ContentAdded', + 'utp.send', + 'utp.socket.send', + 'utp.socket.done', + 'utp.', + 'utp.cc.write', + 'utp.writer.send', + 'utp.writer.sent', + 'transport.packet', + 'transport.decodeError', + 'transport.multiaddr', +] + +const t = initTRPC + .context() + .meta<{ + description: string + }>() + .create() +const pubProcedure = t.procedure +export type PublicProcudure = typeof pubProcedure + +export const subscriptions = async ( + portal: PortalNetwork, + history: HistoryProtocol, + publicProcedure: PublicProcudure, +) => { + const log = debug('ui:subscription') + const discv5 = portal.discv5 as Discv5EventEmitter + const client = portal as PortalNetworkEventEmitter + + // WSS Client Methods + + const onTalkReq = publicProcedure + .meta({ + description: 'Subscribe to Discv5 TalkReq listener', + }) + .subscription(() => { + return observable((emit) => { + const talkReq = (src: INodeAddress, sourceId: ENR | null, message: ITalkReqMessage) => { + if (toHexString(message.protocol) === ProtocolId.UTPNetwork) { + emit.next({ + nodeId: '0x' + src.nodeId, + topic: 'UTP', + message: toHexString(message.request), + }) + } else { + const deserialized = PortalWireMessageType.deserialize(message.request) + const messageType = deserialized.selector + emit.next({ + nodeId: '0x' + src.nodeId, + topic: MessageCodes[messageType], + message: deserialized.value.toString(), + }) + } + } + discv5.on('talkReqReceived', talkReq) + return () => { + discv5.off('talkReqReceived', talkReq) + } + }) + }) + const onTalkResp = publicProcedure + .meta({ + description: 'Subscribe to Discv5 TalkResp listener', + }) + .subscription(() => { + return observable((emit) => { + const talkResp = (src: INodeAddress, sourceId: ENR | null, msg: ITalkRespMessage) => { + const source = { + addr: src.socketAddr.toString(), + nodeId: '0x' + src.nodeId, + } + const message = { + id: msg.id.toString(), + response: toHexString(msg.response), + } + try { + const deserialized = PortalWireMessageType.deserialize(fromHexString(message.response)) + const messageType = deserialized.selector + emit.next({ + nodeId: source.nodeId, + topic: MessageCodes[messageType], + message: deserialized.value.toString(), + }) + } catch (err: any) { + log.extend('TalkResp ERROR')(`${{ src, msg, error: err.message }}`) + } + } + discv5.on('talkRespReceived', talkResp) + return () => { + discv5.off('talkRespReceived', talkResp) + } + }) + }) + const onContentAdded = publicProcedure + .meta({ + description: 'Subscribe to ContentAdded listener', + }) + .subscription(() => { + return observable((emit) => { + const contentAdded = (key: string, contentType: number, content: string) => { + console.groupCollapsed('onContentAdded') + console.dir({ key, contentType, content }) + console.groupEnd() + emit.next({ key, contentType, content }) + } + history.on('ContentAdded', contentAdded) + return () => { + history.off('ContentAdded', contentAdded) + } + }) + }) + const onNodeAdded = publicProcedure + .meta({ + description: 'Subscribe to NodeAdded listener', + }) + .subscription(() => { + return observable((emit) => { + const nodeAdded = (...args: any) => { + emit.next(args) + } + client.on('NodeAdded', nodeAdded) + return () => { + client.off('NodeAdded', nodeAdded) + } + }) + }) + const onUtp = publicProcedure + .meta({ + description: 'Subscribe to uTP event listener', + }) + .subscription(() => { + return observable((emit) => { + const utpEvent = (peerId: string, msg: Buffer, protocolId: ProtocolId) => { + emit.next({ + peerId, + protocolId, + msg: toHexString(msg), + }) + } + portal.uTP.on('Send', utpEvent) + return () => { + portal.uTP.off('Send', utpEvent) + } + }) + }) + const onSendTalkReq = publicProcedure + .meta({ + description: 'Subscribe to send talk req listener', + }) + .subscription(() => { + return observable((emit) => { + const sendReq = (nodeId: string, _res: string, payload: string) => { + console.log('sendTalkRequest') + const deserialized = PortalWireMessageType.deserialize(fromHexString(payload)) + const messageType = deserialized.selector + emit.next({ + nodeId: '0x' + nodeId, + topic: MessageCodes[messageType], + message: deserialized.value.toString(), + }) + } + client.on('SendTalkReq', sendReq) + return () => { + client.off('SendTalkReq', sendReq) + } + }) + }) + const onSendTalkResp = publicProcedure + .meta({ + description: 'Subscribe to send talk resp listener', + }) + .subscription(() => { + return observable((emit) => { + const sendResp = (...args: any) => { + try { + const [nodeId, requestId, payload] = args + const deserialized = PortalWireMessageType.deserialize(fromHexString(payload)) + const messageType = deserialized.selector + emit.next({ + nodeId: '0x' + nodeId, + requestId, + topic: MessageCodes[messageType], + message: deserialized.value.toString(), + }) + } catch (err: any) { + console.log('SendTalkResp ERROR') + console.dir({ args, error: err.message }) + console.groupEnd() + } + } + client.on('SendTalkResp', sendResp) + return () => { + client.off('SendTalkResp', sendResp) + } + }) + }) + + return { + onTalkReq, + onTalkResp, + onSendTalkReq, + onSendTalkResp, + onContentAdded, + onNodeAdded, + onUtp, + } +} diff --git a/packages/ui/src/util.ts b/packages/ui/src/util.ts new file mode 100644 index 000000000..d337fbf64 --- /dev/null +++ b/packages/ui/src/util.ts @@ -0,0 +1,114 @@ +import { BlockHeader } from '@ethereumjs/block' +import { RLP } from '@ethereumjs/rlp' +import { TransactionFactory } from '@ethereumjs/tx' +import { + BaseProtocol, + BlockBodyContentType, + BlockHeaderWithProof, + EpochAccumulator, + ProtocolId, + sszReceiptType, + sszUnclesType, + toHexString, +} from 'portalnetwork' + +type Enr = string + +export const hasValidEnrPrefix = (enr: Enr) => { + return enr.startsWith('enr:') +} + +// remove ENR prefix 'enr:' +export const extractBase64URLCharsFromEnr = (enr: Enr) => { + return enr.substr(4, enr.length) +} + +export const hasValidBase64URLChars = (enr: Enr) => { + const str = extractBase64URLCharsFromEnr(enr) + const validBase64Chars = /^[A-Za-z0-9_-]+$/i + if (str && validBase64Chars.test(str)) { + return true + } + return false +} + +/** + * Check if an Ethereum Node Record (ENR) is valid as specified in EIP-778 + * https://eips.ethereum.org/EIPS/eip-778 by starting with `enr:` and encoded + * using valid characters of the Base64URL encoding scheme + * https://base64.guru/standards/base64url + * @param enr a base64 encoded string containing an Ethereum Node Record (ENR) + */ +export const isValidEnr = (enr: Enr) => { + if (enr && hasValidEnrPrefix(enr) && hasValidBase64URLChars(enr)) { + return true + } + return false +} + +/** + * Add a valid Ethereum Node Record (ENR) that is compliant with EIP-778 + * https://eips.ethereum.org/EIPS/eip-778 as a bootnode to an instance of a Portal Network + * protocol + * @param protocolId the protocolId associated with an instance of a Portal Network protocol + * @param baseProtocol the methods of the protocolId instance of a Portal Network protocol + * @param enr a base64 encoded string containing an Ethereum Node Record (ENR) + * @throws {Error} + */ +export const addBootNode = async (protocolId: ProtocolId, baseProtocol: BaseProtocol, enr: Enr) => { + try { + await baseProtocol!.addBootNode(enr) + baseProtocol!.logger(`Added bootnode ${enr} to ${protocolId}`) + } catch (error: any) { + throw new Error(`Error adding bootnode ${enr} to protocol \ + ${protocolId}: ${error.message ?? error}`) + } +} + +export const toJSON = (contentKey: Uint8Array, res: Uint8Array) => { + const contentType = contentKey[0] + let content = {} + switch (contentType) { + case 0: { + const blockHeaderWithProof = BlockHeaderWithProof.deserialize(res) + const header = BlockHeader.fromRLPSerializedHeader(blockHeaderWithProof.header, { + setHardfork: true, + }).toJSON() + const proof = + blockHeaderWithProof.proof.selector === 0 + ? [] + : blockHeaderWithProof.proof.value?.map((p) => toHexString(p)) + content = { header, proof } + break + } + case 1: { + const blockBody = BlockBodyContentType.deserialize(res) + const transactions = blockBody.allTransactions.map((tx) => + TransactionFactory.fromSerializedData(tx).toJSON(), + ) + const unclesRlp = toHexString(sszUnclesType.deserialize(blockBody.sszUncles)) + content = { + transactions, + uncles: { + rlp: unclesRlp, + count: RLP.decode(unclesRlp).length.toString(), + }, + } + break + } + case 2: { + const receipt = sszReceiptType.deserialize(res) + content = receipt + break + } + case 3: { + const epochAccumulator = EpochAccumulator.deserialize(res) + content = epochAccumulator + break + } + default: { + content = {} + } + } + return JSON.stringify(content) +} diff --git a/packages/ui/src/utils/enr.ts b/packages/ui/src/utils/enr.ts new file mode 100644 index 000000000..65f39ca7f --- /dev/null +++ b/packages/ui/src/utils/enr.ts @@ -0,0 +1,56 @@ +import * as RLP from '@ethereumjs/rlp' +import { bytesToHex, bytesToUtf8 } from '@ethereumjs/util' +import base64url from 'base64url' +import SuperJSON from 'superjson' + +type Decoded = Uint8Array | RLP.NestedUint8Array + +export function decodeFromValues(decoded: Decoded) { + if (!Array.isArray(decoded)) { + throw new Error('Decoded ENR must be an array') + } + if (decoded.length % 2 !== 0) { + throw new Error('Decoded ENR must have an even number of elements') + } + const [signature, seq] = decoded + if (!signature || Array.isArray(signature)) { + throw new Error('Decoded ENR invalid signature: must be a byte array') + } + if (!seq || Array.isArray(seq)) { + throw new Error('Decoded ENR invalid sequence number: must be a byte array') + } + const kvs = new Map() + const signed = [seq] + for (let i = 2; i < decoded.length; i += 2) { + const k = decoded[i] + const v = decoded[i + 1] + kvs.set(bytesToUtf8(k), v) + signed.push(k, v) + } + + const values = { + kvs: Object.fromEntries([...kvs.entries()]), + seq: bytesToHex(seq), + signature: bytesToHex(signature), + } + console.log(values) + return values +} +export function decode(encoded: Uint8Array) { + console.log('encoded', encoded) + const decoded = RLP.decode(encoded) + console.log('decoded', decoded) + return decodeFromValues(decoded) +} +export function txtToBuf(encoded: string) { + if (!encoded.startsWith('enr:')) { + throw new Error("string encoded ENR must start with 'enr:'") + } + const binString = atob( + encoded.slice(4).replace(/\s+/g, '').replace(/\-/g, '+').replace(/\_/g, '/'), + ) + return Uint8Array.from(binString, (m) => m.codePointAt(0)!) +} +export function decodeTxt(encoded: string) { + return decode(txtToBuf(encoded)) +} diff --git a/packages/ui/src/utils/router.ts b/packages/ui/src/utils/router.ts index bd27a4d61..1914fb08c 100644 --- a/packages/ui/src/utils/router.ts +++ b/packages/ui/src/utils/router.ts @@ -30,7 +30,127 @@ const onTalkReq = publicProcedure.subscription(({ input }) => { }) }) -const self = publicProcedure.mutation(() => { +const onTalkResp = publicProcedure.subscription(({ input }) => { + const ee = new EventEmitter() + return observable((emit) => { + const talkResp = (msg: any) => { + console.log('router', 'onTalkResp') + emit.next(msg) + } + ee.on('talkRespReceived', (msg: any) => { + console.log('talkResponseReceived') + talkResp(msg) + }) + return () => { + ee.off('talkRespReceived', () => { + console.log('off talkResponse') + talkResp + }) + } + }) +}) +const onSendTalkReq = publicProcedure.subscription(({ input }) => { + const ee = new EventEmitter() + return observable((emit) => { + const sendTalkReq = (msg: any) => { + console.log('router', 'onSendTalkReq') + emit.next(msg) + } + ee.on('sendTalkReq', (msg: any) => { + sendTalkReq(msg) + }) + return () => { + ee.off('sendTalkReq', (msg: any) => { + sendTalkReq + }) + } + }) +}) +const onSendTalkResp = publicProcedure.subscription(({ input }) => { + const ee = new EventEmitter() + return observable((emit) => { + const sendTalkResp = (msg: any) => { + emit.next(msg) + } + ee.on('sendTalkResp', (msg: any) => { + sendTalkResp(msg) + }) + return () => { + ee.off('sendTalkReq', (msg: any) => { + sendTalkResp + }) + } + }) +}) + +const onContentAdded = publicProcedure.subscription(({ input }) => { + const ee = new EventEmitter() + return observable((emit) => { + const contentAdded = (msg: any) => { + console.log(msg) + emit.next(msg) + } + ee.on('ContentAdded', (msg: any) => { + console.log('contentAdded') + contentAdded(msg) + }) + return () => { + ee.off('ContentAdded', () => { + console.log('off ContentAdded') + contentAdded + }) + } + }) +}) +const onNodeAdded = publicProcedure.subscription(() => { + const ee = new EventEmitter() + return observable((emit) => { + const nodeAdded = (nodeId: string, protocolId: number) => { + console.log('nodeAdded', { nodeId, protocolId }) + emit.next({ + nodeId, + protocolId, + }) + } + ee.on('NodeAdded', (nodeId: string, protocolId: number) => { + nodeAdded(nodeId, protocolId) + }) + return () => { + ee.off('NodeAdded', () => { + nodeAdded + }) + } + }) +}) +const onUtp = publicProcedure.subscription(({ input }) => { + const ee = new EventEmitter() + return observable((emit) => { + const utp = (msg: any) => { + console.log(msg) + emit.next(msg) + } + ee.on('utpEvent', (msg: any) => { + console.log('utpEvent', msg) + utp(msg) + }) + return () => { + ee.off('utpEvent', () => { + console.log('off utpEvent') + utp + }) + } + }) +}) + +const start = publicProcedure + .meta({ + description: 'Start Portal Network', + }) + .mutation(async () => { + return '' + }) + +const browser_nodeInfo = publicProcedure.input(z.any()).mutation(() => { return { enr: '', nodeId: '', @@ -39,28 +159,116 @@ const self = publicProcedure.mutation(() => { } }) -const local_routingTable = publicProcedure +const discv5_nodeInfo = publicProcedure.input(z.any()).mutation(async ({ input }) => { + return { + client: 'ultralight', + enr: '', + nodeId: '', + multiAddr: '', + } +}) + +const browser_localRoutingTable = publicProcedure + .input(z.union([z.undefined(), z.object({ port: z.number(), ip: z.string() })])) .output(z.array(z.tuple([z.string(), z.string(), z.string(), z.string(), z.number()]))) .mutation(() => { return demorows }) -const portal_historyRoutingTableInfo = publicProcedure.mutation(async () => { - return { - routingTable: [['']], - } -}) -const discv5_nodeInfo = publicProcedure +const local_routingTable = publicProcedure + .input(z.union([z.undefined(), z.object({ port: z.number(), ip: z.string() })])) + .output(z.array(z.tuple([z.string(), z.number()]))) + .mutation(() => { + return [] + }) + +const browser_historyFindNodes = publicProcedure + .input( + z.object({ + nodeId: z.string(), + }), + ) + .mutation(async () => { + return demorows + }) + +const browser_historyFindContent = publicProcedure + .input( + z.object({ + nodeId: z.string(), + contentKey: z.string(), + }), + ) + .output( + z.union([ + z.undefined(), + z.object({ + content: z.array(z.string()), + }), + z.object({ + enrs: z.array(z.string()), + }), + ]), + ) + .mutation(async () => { + return undefined + }) +const browser_historyRecursiveFindContent = publicProcedure + .input( + z.object({ + contentKey: z.string(), + }), + ) + .mutation(async () => { + return JSON.stringify({ key: 'value' }) + }) +const browser_historyOffer = publicProcedure + .input( + z.object({ + nodeId: z.string(), + contentKey: z.string(), + content: z.string(), + }), + ) + .mutation(async () => { + return true + }) + +const browser_historySendOffer = publicProcedure + .input( + z.object({ + nodeId: z.string(), + contentKeys: z.array(z.string()), + }), + ) + .mutation(async () => { + return { + result: '', + response: [], + } + }) + +const browser_historyGossip = publicProcedure + .input( + z.object({ + contentKey: z.string(), + content: z.string(), + }), + ) + .mutation(async () => { + return 0 + }) + +const portal_historyRoutingTableInfo = publicProcedure .input( z.object({ port: z.number(), + ip: z.string(), }), ) - .mutation(async ({ input }) => { + .mutation(async () => { return { - client: 'ultralight', - enr: '', - nodeId: '', - multiAddr: '', + localNodeId: '', + buckets: [''], } }) @@ -75,36 +283,317 @@ const ping = publicProcedure const pong = x ? undefined : { customPayload: '', enrSeq: '' } return pong }) -const pingBootNodes = publicProcedure.mutation(async () => { - const x = Math.random() >= 0.5 - return [{ tag: '', enr: '', customPayload: '', enrSeq: -1 }, null] -}) +const pingBootNodes = publicProcedure + .input(z.any()) + .output( + z.record( + z.string(), + z.object({ + idx: z.number(), + client: z.string(), + enr: z.string(), + nodeId: z.string(), + connected: z.boolean(), + }), + ), + ) + .mutation(async () => { + return {} + }) const portal_historyPing = publicProcedure .input( z.object({ + port: z.union([z.undefined(), z.number()]), enr: z.string(), }), ) + .output(z.any()) .mutation(async () => { - const x = Math.random() >= 0.5 - return [{ dataRadius: '', enrSeq: 1 }] + return { dataRadius: '', enrSeq: 1 } + }) + +const browser_historyStore = publicProcedure + .input( + z.object({ + contentKey: z.string(), + content: z.string(), + }), + ) + .mutation(async () => { + return true + }) +const portal_historyStore = publicProcedure + .input( + z.object({ + port: z.number(), + ip: z.union([z.undefined(), z.string()]), + contentKey: z.string(), + content: z.string(), + }), + ) + .mutation(async () => { + return true + }) +const browser_historyLocalContent = publicProcedure.input(z.any()).mutation(async () => { + return '' +}) +const portal_historyLocalContent = publicProcedure + .input( + z.object({ + contentKey: z.string(), + port: z.number(), + ip: z.union([z.undefined(), z.string()]), + }), + ) + .mutation(async () => { + return '' + }) + +const pingBootNodeHTTP = publicProcedure + .input( + z.object({ + port: z.union([z.undefined(), z.number()]), + ip: z.union([z.undefined(), z.string()]), + }), + ) + .output( + z.record( + z.string(), + z.object({ + idx: z.number(), + client: z.string(), + enr: z.string(), + nodeId: z.string(), + connected: z.boolean(), + }), + ), + ) + .mutation(async () => { + return {} + }) + +const decodeENR = publicProcedure + .input(z.string()) + .output( + z.object({ + nodeId: z.string(), + c: z.string(), + multiaddr: z.string(), + }), + ) + .mutation(async () => { + return { + nodeId: '', + c: '', + multiaddr: '', + } + }) + +const browser_ethGetBlockByHash = publicProcedure + .input( + z.object({ + hash: z.string(), + includeTransactions: z.boolean(), + }), + ) + .output(z.union([z.undefined(), z.string(), z.record(z.string(), z.string())])) + .mutation(({ input }) => { + return undefined + }) + +const eth_getBlockByNumber = publicProcedure + .input( + z.object({ + blockNumber: z.string(), + includeTransactions: z.boolean(), + }), + ) + .output(z.union([z.undefined(), z.string(), z.record(z.string(), z.string())])) + .mutation(({ input }) => { + return undefined + }) +const eth_getBlockByHash = publicProcedure + .input( + z.object({ + hash: z.string(), + includeTransactions: z.boolean(), + }), + ) + .output(z.union([z.undefined(), z.string(), z.record(z.string(), z.string())])) + .mutation(({ input }) => { + return undefined + }) + +const browser_ethGetBlockByNumber = publicProcedure + .input( + z.object({ + blockNumber: z.string(), + includeTransactions: z.boolean(), + }), + ) + .output(z.union([z.undefined(), z.string(), z.record(z.string(), z.string())])) + .mutation(({ input }) => { + return undefined }) -const pingBootNodeHTTP = publicProcedure.mutation(async () => { - const x = Math.random() >= 0.5 - return [{ tag: '', enr: '', dataRadius: '', enrSeq: -1 }] +const getPubIp = publicProcedure.query(() => { + return '' }) +const portal_historyFindNodes = publicProcedure + .input( + z.object({ + port: z.number(), + ip: z.string(), + nodeId: z.string(), + }), + ) + .output(z.object({ enrs: z.array(z.string()) })) + .mutation(async () => { + return { + enrs: [], + } + }) + +const portal_historyFindContent = publicProcedure + .input( + z.object({ + port: z.number(), + ip: z.string(), + enr: z.string(), + contentKey: z.string(), + }), + ) + .output(z.object({ content: z.string() })) + .mutation(async () => { + return { + content: '', + utpTransfer: true, + } + }) + +const portal_historyRecursiveFindContent = publicProcedure + .input( + z.object({ + port: z.number(), + ip: z.string(), + contentKey: z.string(), + }), + ) + .output(z.object({ content: z.string(), utpTransfer: z.boolean() })) + .mutation(async () => { + return { + content: '', + utpTransfer: true, + } + }) + +const portal_historyOffer = publicProcedure + .input( + z.object({ + port: z.number(), + ip: z.string(), + enr: z.string(), + contentKey: z.string(), + content: z.string(), + }), + ) + .output(z.union([z.undefined(), z.array(z.boolean()), z.array(z.never())])) + .mutation(async () => { + return [true] + }) + +const portal_historySendOffer = publicProcedure + .input( + z.object({ + port: z.number(), + ip: z.string(), + nodeId: z.string(), + contentKeys: z.array(z.string()), + }), + ) + .output(z.union([z.string(), z.undefined()])) + .mutation(async () => { + return '' + }) + +const portal_historyGossip = publicProcedure + .input( + z.object({ + port: z.number(), + ip: z.string(), + contentKey: z.string(), + content: z.string(), + }), + ) + .output(z.number()) + .mutation(async () => { + return 0 + }) + +const portal_historyGetEnr = publicProcedure + .input( + z.object({ + port: z.number(), + ip: z.string(), + nodeId: z.string(), + }), + ) + .output( + z.union([ + z.string(), + z.object({ + enr: z.string(), + nodeId: z.string(), + multiaddr: z.string(), + c: z.string(), + }), + ]), + ) + .mutation(async () => { + return '' + }) export const appRouter = router({ + getPubIp, + decodeENR, onTalkReq, - self, + onTalkResp, + onSendTalkReq, + onSendTalkResp, + onContentAdded, + onNodeAdded, + onUtp, + start, + browser_nodeInfo, local_routingTable, ping, pingBootNodes, discv5_nodeInfo, portal_historyRoutingTableInfo, portal_historyPing, + browser_localRoutingTable, + browser_historyLocalContent, + browser_historyStore, pingBootNodeHTTP, + browser_historyFindNodes, + browser_historyFindContent, + browser_historyRecursiveFindContent, + browser_historyOffer, + browser_historySendOffer, + browser_historyGossip, + browser_ethGetBlockByHash, + browser_ethGetBlockByNumber, + eth_getBlockByHash, + eth_getBlockByNumber, + portal_historyFindNodes, + portal_historyFindContent, + portal_historyRecursiveFindContent, + portal_historyOffer, + portal_historySendOffer, + portal_historyGossip, + portal_historyStore, + portal_historyLocalContent, + portal_historyGetEnr, }) export type AppRouter = typeof appRouter