From 660888f53329bd182d0a8c3c62399d8141227675 Mon Sep 17 00:00:00 2001 From: Anton Sauchyk Date: Mon, 25 May 2026 11:59:12 +0200 Subject: [PATCH 1/8] SEC-359: Replace compare app with server-rendered Grafana API overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Home page rewritten as server-rendered RPC provider overview via Grafana Cloud Prometheus API; ISR revalidate=60 - Streaming SSR: per-ChainCard + Headline + ChainTOC boundaries so cards stream as their PromQL resolves - Bullet bars: p50/p95/p99 layered horizontal bars per provider, leader emerald, others slate - Inline-SVG 24h sparkline per provider row, no chart library - Regional heatmap per chain: provider × region p95 grid, HSL-tinted from chain-wide min/max - Headline strip: fastest-right-now, most-p95-wins, tail-latency leader across all chains - Mini-sparkline TOC: 4×2 chain grid with leader trend + anchor jumps - lib/grafana.js: runPromQuery + runPromRangeQuery - lib/chain-data.js: React.cache-wrapped per-chain fetcher, parallel queries, partial-data tolerant; TOC/Headline/Cards share results - lib/queries.js: providerByRegionQuery(chain, q) for any quantile + providerTrendQuery for range data - Route deletions: compare-single/, compare-double/, injection-start/, injection-result-double/ — all hit the backend's /scenarios/* SSRF surface; legacy URLs 301 to / in next.config.js - next.config.js: dropped wide-open /api/* CORS; CSP with frame-ancestors 'none', HSTS, nosniff, Referrer-Policy - store/store.js emptied; .env.sample drops NEXT_PUBLIC_BACKEND_APP_URL and adds GRAFANA_API_TOKEN - Dead components removed: Bento, Faq, Performance, ProtocolIcon (+ SVGs), ResultCard, orphaned icons --- .env.sample | 11 +- next.config.js | 43 +- package-lock.json | 7 + package.json | 1 + src/app/compare-double/page.js | 401 ------------------- src/app/compare-single/page.js | 183 --------- src/app/injection-result-double/page.js | 284 ------------- src/app/injection-start/page.js | 95 ----- src/app/page.js | 287 +++---------- src/app/store/store.js | 178 +------- src/components/Bento/Bento.js | 114 ------ src/components/Chain/BulletBar.js | 22 + src/components/Chain/ChainCard.js | 104 +++++ src/components/Chain/ChainCardSkeleton.js | 25 ++ src/components/Chain/RegionHeatmap.js | 68 ++++ src/components/Chain/Sparkline.js | 43 ++ src/components/ChainTOC.js | 54 +++ src/components/ChainTOCSkeleton.js | 19 + src/components/Faq/FaqAccordion.js | 70 ---- src/components/Faq/FaqBasic.js | 31 -- src/components/Faq/post.mdx | 11 - src/components/Header/Header.js | 6 - src/components/Headline.js | 73 ++++ src/components/HeadlineSkeleton.js | 7 + src/components/Icons/BarChartIcon.js | 22 - src/components/Icons/Customization.js | 22 - src/components/Icons/ExplainResultsIcon.js | 21 - src/components/Icons/GrafanaIcon.js | 21 - src/components/Icons/Profiling.js | 22 - src/components/Performance/Compare.js | 54 --- src/components/Performance/Preformance.js | 50 --- src/components/ProtocolIcon/ProtocolIcon.js | 129 ------ src/components/ProtocolIcon/aptos.svg | 1 - src/components/ProtocolIcon/arbitrum.svg | 1 - src/components/ProtocolIcon/aurora.svg | 1 - src/components/ProtocolIcon/avalanche.svg | 1 - src/components/ProtocolIcon/base.svg | 1 - src/components/ProtocolIcon/bitcoin.svg | 1 - src/components/ProtocolIcon/bnb.svg | 1 - src/components/ProtocolIcon/cronos.svg | 1 - src/components/ProtocolIcon/ethereum.svg | 1 - src/components/ProtocolIcon/fantom.svg | 1 - src/components/ProtocolIcon/filecoin.svg | 1 - src/components/ProtocolIcon/fuse.svg | 1 - src/components/ProtocolIcon/gnosis.svg | 1 - src/components/ProtocolIcon/harmony.svg | 1 - src/components/ProtocolIcon/near.svg | 1 - src/components/ProtocolIcon/optimism.svg | 1 - src/components/ProtocolIcon/polygonPOS.svg | 1 - src/components/ProtocolIcon/polygonZkEvm.svg | 1 - src/components/ProtocolIcon/ronin.svg | 1 - src/components/ProtocolIcon/scroll.svg | 1 - src/components/ProtocolIcon/solana.svg | 1 - src/components/ProtocolIcon/starknet.svg | 1 - src/components/ProtocolIcon/tezos.svg | 1 - src/components/ProtocolIcon/zkSync.svg | 1 - src/components/ResultCard/ResultCard.js | 161 -------- src/lib/chain-data.js | 126 ++++++ src/lib/format.js | 15 + src/lib/grafana.js | 45 +++ src/lib/queries.js | 50 +++ 61 files changed, 745 insertions(+), 2154 deletions(-) delete mode 100644 src/app/compare-double/page.js delete mode 100644 src/app/compare-single/page.js delete mode 100644 src/app/injection-result-double/page.js delete mode 100644 src/app/injection-start/page.js delete mode 100644 src/components/Bento/Bento.js create mode 100644 src/components/Chain/BulletBar.js create mode 100644 src/components/Chain/ChainCard.js create mode 100644 src/components/Chain/ChainCardSkeleton.js create mode 100644 src/components/Chain/RegionHeatmap.js create mode 100644 src/components/Chain/Sparkline.js create mode 100644 src/components/ChainTOC.js create mode 100644 src/components/ChainTOCSkeleton.js delete mode 100644 src/components/Faq/FaqAccordion.js delete mode 100644 src/components/Faq/FaqBasic.js delete mode 100644 src/components/Faq/post.mdx create mode 100644 src/components/Headline.js create mode 100644 src/components/HeadlineSkeleton.js delete mode 100644 src/components/Icons/BarChartIcon.js delete mode 100644 src/components/Icons/Customization.js delete mode 100644 src/components/Icons/ExplainResultsIcon.js delete mode 100644 src/components/Icons/GrafanaIcon.js delete mode 100644 src/components/Icons/Profiling.js delete mode 100644 src/components/Performance/Compare.js delete mode 100644 src/components/Performance/Preformance.js delete mode 100644 src/components/ProtocolIcon/ProtocolIcon.js delete mode 100644 src/components/ProtocolIcon/aptos.svg delete mode 100644 src/components/ProtocolIcon/arbitrum.svg delete mode 100644 src/components/ProtocolIcon/aurora.svg delete mode 100644 src/components/ProtocolIcon/avalanche.svg delete mode 100644 src/components/ProtocolIcon/base.svg delete mode 100644 src/components/ProtocolIcon/bitcoin.svg delete mode 100644 src/components/ProtocolIcon/bnb.svg delete mode 100644 src/components/ProtocolIcon/cronos.svg delete mode 100644 src/components/ProtocolIcon/ethereum.svg delete mode 100644 src/components/ProtocolIcon/fantom.svg delete mode 100644 src/components/ProtocolIcon/filecoin.svg delete mode 100644 src/components/ProtocolIcon/fuse.svg delete mode 100644 src/components/ProtocolIcon/gnosis.svg delete mode 100644 src/components/ProtocolIcon/harmony.svg delete mode 100644 src/components/ProtocolIcon/near.svg delete mode 100644 src/components/ProtocolIcon/optimism.svg delete mode 100644 src/components/ProtocolIcon/polygonPOS.svg delete mode 100644 src/components/ProtocolIcon/polygonZkEvm.svg delete mode 100644 src/components/ProtocolIcon/ronin.svg delete mode 100644 src/components/ProtocolIcon/scroll.svg delete mode 100644 src/components/ProtocolIcon/solana.svg delete mode 100644 src/components/ProtocolIcon/starknet.svg delete mode 100644 src/components/ProtocolIcon/tezos.svg delete mode 100644 src/components/ProtocolIcon/zkSync.svg delete mode 100644 src/components/ResultCard/ResultCard.js create mode 100644 src/lib/chain-data.js create mode 100644 src/lib/format.js create mode 100644 src/lib/grafana.js create mode 100644 src/lib/queries.js diff --git a/.env.sample b/.env.sample index 28af456..1828737 100644 --- a/.env.sample +++ b/.env.sample @@ -1,5 +1,8 @@ -# requires full url -NEXT_PUBLIC_BACKEND_APP_URL = 'http://0.0.0.0:8000/api/...' +# client app public URL (used in og:image and og:url metadata) +NEXT_PUBLIC_CLIENT_DOMAIN = -# client app public URL -NEXT_PUBLIC_CLIENT_DOMAIN = \ No newline at end of file +# Grafana Cloud service account token, Viewer role on chainstack.grafana.net. +# Server-side only — never exposed to the client bundle. Create at: +# chainstack.grafana.net → Administration → Service accounts → Add token. +# Set this in .env.local for local dev and in Vercel project env vars for deploys. +GRAFANA_API_TOKEN = diff --git a/next.config.js b/next.config.js index 9099da9..af67520 100644 --- a/next.config.js +++ b/next.config.js @@ -1,32 +1,39 @@ /** @type {import('next').NextConfig} */ +const GRAFANA_DASHBOARD_URL = + 'https://chainstack.grafana.net/public-dashboards/65c0fcb02f994faf845d4ec095771bd0?orgId=1'; + +const cspDirectives = [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.segment.com https://www.googletagmanager.com https://www.google-analytics.com", + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", + "font-src 'self' https://fonts.gstatic.com data:", + "img-src 'self' data: https:", + "frame-src https://chainstack.grafana.net", + "connect-src 'self' https://api.segment.io https://*.segment.io https://www.google-analytics.com", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", +].join('; '); + const nextConfig = { async headers() { return [ { - source: '/api/:path*', + source: '/(.*)', headers: [ - { key: 'Access-Control-Allow-Credentials', value: 'true' }, - { key: 'Access-Control-Allow-Origin', value: '*' }, - { - key: 'Access-Control-Allow-Methods', - value: 'GET,DELETE,PATCH,POST,PUT', - }, - { - key: 'Access-Control-Allow-Headers', - value: - 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version', - }, + { key: 'Content-Security-Policy', value: cspDirectives }, + { key: 'X-Content-Type-Options', value: 'nosniff' }, + { key: 'Referrer-Policy', value: 'no-referrer-when-downgrade' }, + { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, ], }, ]; }, async redirects() { return [ - { - source: '/dashboard', - destination: 'https://chainstack.grafana.net/public-dashboards/65c0fcb02f994faf845d4ec095771bd0?orgId=1', - permanent: true - } + { source: '/compare-single', destination: '/', permanent: true }, + { source: '/compare-double', destination: '/', permanent: true }, + { source: '/dashboard', destination: GRAFANA_DASHBOARD_URL, permanent: true }, ]; }, reactStrictMode: false, @@ -39,4 +46,4 @@ const withMDX = require('@next/mdx')({ }, }); -module.exports = withMDX(nextConfig); \ No newline at end of file +module.exports = withMDX(nextConfig); diff --git a/package-lock.json b/package-lock.json index c1e6d77..b315b0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "react-dom": "^18", "react-google-charts": "^4.0.1", "react-type-animation": "^3.2.0", + "server-only": "^0.0.1", "simpler-state": "^1.2.1" }, "devDependencies": { @@ -6741,6 +6742,12 @@ "randombytes": "^2.1.0" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", diff --git a/package.json b/package.json index f1d4879..a4eed98 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "react-dom": "^18", "react-google-charts": "^4.0.1", "react-type-animation": "^3.2.0", + "server-only": "^0.0.1", "simpler-state": "^1.2.1" }, "devDependencies": { diff --git a/src/app/compare-double/page.js b/src/app/compare-double/page.js deleted file mode 100644 index 64efc4a..0000000 --- a/src/app/compare-double/page.js +++ /dev/null @@ -1,401 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -import Header from '@/components/Header/Header'; -import ResultCard from '@/components/ResultCard/ResultCard'; -import ExplainResultsIcon from '../../components/Icons/ExplainResultsIcon'; - -import { CodeIcon, ShareIcon } from '@iconicicons/react'; -import { Button, Loading, Badge } from '@lemonsqueezy/wedges'; -import { ClipboardIcon, CheckIcon, PlusIcon } from '@iconicicons/react'; -import { Chart } from 'react-google-charts'; -import Link from 'next/link'; - -import { - NODE_ENDPOINT, - NODE_ENDPOINT_2, - SET_NODE_ENDPOINT, - SET_NODE_ENDPOINT_2, - METHODS, - METHODS_2, - SET_METHOD_RESPONSE_DATA, - SET_METHOD_RESPONSE_DATA_2, - GET_METHODS_NAMES, -} from '../store/store'; - -import { useSearchParams } from 'next/navigation'; - -const Result = () => { - const searchParams = useSearchParams(); - let url1 = searchParams.get('url1'); - let url2 = searchParams.get('url2'); - - const nodeEndpoint = url1 ? url1 : NODE_ENDPOINT.use(); - const nodeEndpoint2 = url2 ? url2 : NODE_ENDPOINT_2.use(); - const methods = METHODS.use(); - const methods2 = METHODS_2.use(); - const methodsNames = GET_METHODS_NAMES.use(); - - const [copiedToClipboard, setCopiedToClipboard] = useState(false); - const [copiedToClipboard2, setCopiedToClipboard2] = useState(false); - const [compareLinkCopiedToClipboard, setCompareLinkCopiedToClipboard] = - useState(false); - const [chartData, setChartData] = useState(null); - const [chartData2, setChartData2] = useState(null); - const [explainIsDisabled, setExplainIsDisabled] = useState(false); - - const downloadJson = () => { - const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( - JSON.stringify([ - { - endpoint: nodeEndpoint, - results: methods.map((method) => { - return { - method: method.method_used, - results: method.data, - }; - }), - }, - { - endpoint: nodeEndpoint2, - results: methods2.map((method) => { - return { - method: method.method_used, - results: method.data, - }; - }), - }, - ]) - )}`; - const link = document.createElement('a'); - link.href = jsonString; - link.download = 'data.json'; - - link.click(); - }; - - useEffect(() => { - if ( - methods.every((item) => Object.keys(item.data).length != 0) === true && - methods2.every((item) => Object.keys(item.data).length != 0) === true - ) { - let chart = [methods[1], methods2[1]]; - 2; - let chart2 = [methods[0], methods2[0]]; - - setChartData([ - [ - '', - ...chart.map((item, index) => { - return `Endpoint ${index + 1}`; - }), - ], - [ - '', - ...chart.map((item) => { - if ( - Object.keys(item.data).length != 0 && - item.data.hasOwnProperty('error') === false - ) { - return +item.data.blocks_per_seconds.toFixed(2); - } else { - return 0; - } - }), - ], - ]); - - setChartData2([ - [ - '', - ...chart2.map((item, index) => { - return `Endpoint ${index + 1}`; - }), - ], - [ - '', - ...chart2.map((item) => { - if ( - Object.keys(item.data).length != 0 && - item.data.hasOwnProperty('error') === false - ) { - return +item.data.blocks_per_seconds.toFixed(2); - } else { - return 0; - } - }), - ], - ]); - } - }, [methods, methods2]); - - let grid = 'grid grid-cols-2 gap-10'; - - return ( -
-
- - {/* URLS */} -
- {[ - { - endpoint: nodeEndpoint, - copied: copiedToClipboard, - clip(value) { - setCopiedToClipboard(value); - }, - }, - { - endpoint: nodeEndpoint2, - copied: copiedToClipboard2, - clip(value) { - setCopiedToClipboard2(value); - }, - }, - ].map((item, index) => { - return ( -
-
[{index + 1}]
-
- {item.endpoint} -
-
- ); - })} -
- {/* URLS */} - - {/* eth_getBlockByNumber */} -
- {[ - { - config: methods[0], - endpoint: nodeEndpoint, - setResponse: SET_METHOD_RESPONSE_DATA, - }, - { - config: methods2[0], - endpoint: nodeEndpoint2, - setResponse: SET_METHOD_RESPONSE_DATA_2, - }, - ].map((item, index) => { - return ( - - ); - })} -
- {/* eth_getBlockByNumber */} - - {/* eth_call */} -
- {[ - { - config: { ...methods[1] }, - endpoint: nodeEndpoint, - setResponse: SET_METHOD_RESPONSE_DATA, - }, - { - config: { ...methods2[1] }, - endpoint: nodeEndpoint2, - setResponse: SET_METHOD_RESPONSE_DATA_2, - }, - ].map((item, index) => { - return ( - - ); - })} -
- {/* eth_call */} - -
-

- Compare results -

- -
-
- } color="blue" stroke className=""> - {methodsNames[1]} - -
Blocks per second
-
- {!chartData ? ( -
- -
- ) : ( - - )} -
-
-
- } color="blue" stroke className=""> - {methodsNames[0]} - -
Blocks per second
-
- {!chartData2 ? ( -
- -
- ) : ( - - )} -
- -
- - - - - -
- - -

- Learn how Chainstack Compare works -
under the hood and why we built it ↗. -

-
-
-
- ); -}; - -export default Result; diff --git a/src/app/compare-single/page.js b/src/app/compare-single/page.js deleted file mode 100644 index 1f6b8d8..0000000 --- a/src/app/compare-single/page.js +++ /dev/null @@ -1,183 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -import Header from '@/components/Header/Header'; -import ResultCard from '@/components/ResultCard/ResultCard'; -import ExplainResultsIcon from '../../components/Icons/ExplainResultsIcon'; - -import { Button, Loading } from '@lemonsqueezy/wedges'; -import { ClipboardIcon, CheckIcon, PlusIcon } from '@iconicicons/react'; -import { Chart } from 'react-google-charts'; -import Link from 'next/link'; - -import { - NODE_ENDPOINT, - METHODS, - SET_METHOD_RESPONSE_DATA, -} from '../store/store'; - -const Result = () => { - const nodeEndpoint = NODE_ENDPOINT.use(); - const methods = METHODS.use(); - - const [copiedToClipboard, setCopiedToClipboard] = useState(false); - const [chartData, setChartData] = useState(null); - const [explainIsDisabled, setExplainIsDisabled] = useState(true); - - const downloadJson = () => { - const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( - JSON.stringify( - methods.map((method) => { - return { - method: method.method_used, - results: method.data, - }; - }) - ) - )}`; - const link = document.createElement('a'); - link.href = jsonString; - link.download = 'data.json'; - - link.click(); - }; - - useEffect(() => { - if (methods.every((item) => Object.keys(item.data).length != 0) === true) { - setChartData([ - ['', ...methods.map((item) => item.method_used)], - [ - '', - ...methods.map((item) => { - if ( - Object.keys(item.data).length != 0 && - item.data.hasOwnProperty('error') === false - ) { - return +item.data.blocks_per_seconds.toFixed(2); - } else { - return 0; - } - }), - ], - ]); - setExplainIsDisabled(false); - } - }, [methods]); - - return ( -
-
-
-
-
{nodeEndpoint}
-
- {methods.map((item, index) => { - return ( - - ); - })} - -
- {!chartData ? ( -
- -
- ) : ( - - )} -
- -
- - - - -
- - -

- Learn how Chainstack Compare works -
under the hood and why we built it ↗. -

-
-
-
- ); -}; - -export default Result; diff --git a/src/app/injection-result-double/page.js b/src/app/injection-result-double/page.js deleted file mode 100644 index 8db746d..0000000 --- a/src/app/injection-result-double/page.js +++ /dev/null @@ -1,284 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; -import ResultCard from '@/components/ResultCard/ResultCard'; -import { Button } from '@lemonsqueezy/wedges'; -import { ClipboardIcon, CheckIcon } from '@iconicicons/react'; - -import Link from 'next/link'; -import Script from 'next/script'; - -import { - NODE_ENDPOINT, - NODE_ENDPOINT_2, - METHODS, - METHODS_2, - SET_METHOD_RESPONSE_DATA, - SET_METHOD_RESPONSE_DATA_2, - GET_METHODS_NAMES, -} from '../store/store'; - -const Result = () => { - const nodeEndpoint = NODE_ENDPOINT.use(); - const nodeEndpoint2 = NODE_ENDPOINT_2.use(); - const methods = METHODS.use(); - const methods2 = METHODS_2.use(); - const methodsNames = GET_METHODS_NAMES.use(); - - const [copiedToClipboard, setCopiedToClipboard] = useState(false); - const [copiedToClipboard2, setCopiedToClipboard2] = useState(false); - const [chartData, setChartData] = useState(null); - const [chartData2, setChartData2] = useState(null); - const [explainIsDisabled, setExplainIsDisabled] = useState(false); - - const downloadJson = () => { - const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( - JSON.stringify([ - { - endpoint: nodeEndpoint, - results: methods.map((method) => { - return { - method: method.method_used, - results: method.data, - }; - }), - }, - { - endpoint: nodeEndpoint2, - results: methods2.map((method) => { - return { - method: method.method_used, - results: method.data, - }; - }), - }, - ]) - )}`; - const link = document.createElement('a'); - link.href = jsonString; - link.download = 'data.json'; - - link.click(); - }; - - useEffect(() => { - if ( - methods.every((item) => Object.keys(item.data).length != 0) === true && - methods2.every((item) => Object.keys(item.data).length != 0) === true - ) { - let chart = [methods[1], methods2[1]]; - let chart2 = [methods[0], methods2[0]]; - - setChartData([ - [ - '', - ...chart.map((item, index) => { - return `Endpoint ${index + 1}`; - }), - ], - [ - '', - ...chart.map((item) => { - if ( - Object.keys(item.data).length != 0 && - item.data.hasOwnProperty('error') === false - ) { - return +item.data.blocks_per_seconds.toFixed(2); - } else { - return 0; - } - }), - ], - ]); - - setChartData2([ - [ - '', - ...chart2.map((item, index) => { - return `Endpoint ${index + 1}`; - }), - ], - [ - '', - ...chart2.map((item) => { - if ( - Object.keys(item.data).length != 0 && - item.data.hasOwnProperty('error') === false - ) { - return +item.data.blocks_per_seconds.toFixed(2); - } else { - return 0; - } - }), - ], - ]); - } - }, [methods, methods2]); - - let grid = 'grid grid-cols-2 gap-10'; - - return ( - //
- <> -
- {/* URLS */} -
- {[ - { - endpoint: nodeEndpoint, - copied: copiedToClipboard, - clip(value) { - setCopiedToClipboard(value); - }, - }, - { - endpoint: nodeEndpoint2, - copied: copiedToClipboard2, - clip(value) { - setCopiedToClipboard2(value); - }, - }, - ].map((item, index) => { - return ( -
-
[{index + 1}]
-
- {item.endpoint} -
-
- ); - })} -
- {/* URLS */} - - {/* eth_getBlockByNumber */} -
- {[ - { - config: methods[0], - endpoint: nodeEndpoint, - setResponse: SET_METHOD_RESPONSE_DATA, - }, - { - config: methods2[0], - endpoint: nodeEndpoint2, - setResponse: SET_METHOD_RESPONSE_DATA_2, - }, - ].map((item, index) => { - return ( - - ); - })} -
- {/* eth_getBlockByNumber */} - - {/* eth_call */} -
- {[ - { - config: { ...methods[1] }, - endpoint: nodeEndpoint, - setResponse: SET_METHOD_RESPONSE_DATA, - }, - { - config: { ...methods2[1] }, - endpoint: nodeEndpoint2, - setResponse: SET_METHOD_RESPONSE_DATA_2, - }, - ].map((item, index) => { - return ( - - ); - })} -
- {/* eth_call */} - -
- {/*

- Full results -

*/} - - - - {/* - - */} -
-
-