From 90436effbc0fbe55e5faa18a853bd70f2dd82261 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 31 May 2024 12:54:14 +0200 Subject: [PATCH] Set RPC node HTTP endpoint --- packages/ui/.env.example | 2 + packages/ui/src/app/config/network.ts | 10 ++ .../app/pages/Settings/SettingsNetworkTab.tsx | 98 ++++++++++++++----- .../components/NetworkInfo/NetworkInfo.tsx | 32 ++++-- .../providers/network-endpoints/provider.tsx | 17 ++-- packages/ui/src/common/types/index.ts | 2 + .../src/services/i18n/dict/en/settings.json | 11 ++- 7 files changed, 130 insertions(+), 42 deletions(-) diff --git a/packages/ui/.env.example b/packages/ui/.env.example index 838d188bc9..bfc537c415 100644 --- a/packages/ui/.env.example +++ b/packages/ui/.env.example @@ -1,5 +1,6 @@ # TESTNET Endpoints REACT_APP_TESTNET_NODE_SOCKET=wss://rpc.joystream.org:9944 +REACT_APP_TESTNET_NODE_HTTP_RPC=https://rpc.joystream.org REACT_APP_TESTNET_QUERY_NODE=https://query.joystream.org/graphql REACT_APP_TESTNET_QUERY_NODE_SOCKET=wss://query.joystream.org/graphql REACT_APP_TESTNET_MEMBERSHIP_FAUCET_URL=https://faucet.joystream.org/member-faucet/register @@ -7,6 +8,7 @@ REACT_APP_TESTNET_BACKEND=http://localhost:3000 # MAINNET Endpoints REACT_APP_MAINNET_NODE_SOCKET=wss://rpc.joystream.org:9944 +REACT_APP_MAINNET_NODE_HTTP_RPC=https://rpc.joystream.org REACT_APP_MAINNET_QUERY_NODE=https://query.joystream.org/graphql REACT_APP_MAINNET_QUERY_NODE_SOCKET=wss://query.joystream.org/graphql REACT_APP_MAINNET_MEMBERSHIP_FAUCET_URL=https://faucet.joystream.org/member-faucet/register diff --git a/packages/ui/src/app/config/network.ts b/packages/ui/src/app/config/network.ts index 67fd385b3c..232b25cbcb 100644 --- a/packages/ui/src/app/config/network.ts +++ b/packages/ui/src/app/config/network.ts @@ -11,6 +11,7 @@ export interface NetworkEndpoints { } const TESTNET_NODE_SOCKET = process.env.REACT_APP_TESTNET_NODE_SOCKET +const TESTNET_NODE_HTTP_RPC = process.env.REACT_APP_TESTNET_NODE_HTTP_RPC const TESTNET_QUERY_NODE = process.env.REACT_APP_TESTNET_QUERY_NODE const TESTNET_QUERY_NODE_SOCKET = process.env.REACT_APP_TESTNET_QUERY_NODE_SOCKET const TESTNET_MEMBERSHIP_FAUCET_URL = process.env.REACT_APP_TESTNET_MEMBERSHIP_FAUCET_URL @@ -20,6 +21,7 @@ export const IS_TESTNET_DEFINED = TESTNET_NODE_SOCKET && TESTNET_QUERY_NODE && TESTNET_QUERY_NODE_SOCKET && TESTNET_MEMBERSHIP_FAUCET_URL const MAINNET_NODE_SOCKET = process.env.REACT_APP_MAINNET_NODE_SOCKET +const MAINNET_NODE_HTTP_RPC = process.env.REACT_APP_MAINNET_NODE_HTTP_RPC const MAINNET_QUERY_NODE = process.env.REACT_APP_MAINNET_QUERY_NODE const MAINNET_QUERY_NODE_SOCKET = process.env.REACT_APP_MAINNET_QUERY_NODE_SOCKET const MAINNET_MEMBERSHIP_FAUCET_URL = process.env.REACT_APP_MAINNET_MEMBERSHIP_FAUCET_URL @@ -58,6 +60,13 @@ const NODE_RPC_ENDPOINT: PredefinedEndpoint = { 'local-mocks': 'ws://127.0.0.1:9944', } +const NODE_HTTP_RPC_ENDPOINT: PredefinedEndpoint = { + mainnet: MAINNET_NODE_HTTP_RPC, + local: 'http://127.0.0.1:9933', // TODO: check + testnet: TESTNET_NODE_HTTP_RPC, + 'local-mocks': 'http://127.0.0.1:9933', // TODO: check +} + const BACKEND_ENDPOINT: PredefinedEndpoint = { mainnet: MAINNET_BACKEND, local: 'http://localhost:3000', @@ -67,6 +76,7 @@ const BACKEND_ENDPOINT: PredefinedEndpoint = { export const pickEndpoints = (network: NetworkType): Partial => ({ nodeRpcEndpoint: NODE_RPC_ENDPOINT[network], + nodeHttpRpcEndpoint: NODE_HTTP_RPC_ENDPOINT[network], queryNodeEndpoint: QUERY_NODE_ENDPOINT[network], queryNodeEndpointSubscription: QUERY_NODE_ENDPOINT_SUBSCRIPTION[network], membershipFaucetEndpoint: MEMBERSHIP_FAUCET_ENDPOINT[network], diff --git a/packages/ui/src/app/pages/Settings/SettingsNetworkTab.tsx b/packages/ui/src/app/pages/Settings/SettingsNetworkTab.tsx index 164a2f929b..2ced0b97cb 100644 --- a/packages/ui/src/app/pages/Settings/SettingsNetworkTab.tsx +++ b/packages/ui/src/app/pages/Settings/SettingsNetworkTab.tsx @@ -1,3 +1,4 @@ +import { random } from 'lodash' import React, { useState, useEffect } from 'react' import { useForm, FormProvider } from 'react-hook-form' import { useTranslation } from 'react-i18next' @@ -35,15 +36,18 @@ export const SettingsNetworkTab = () => { const [endpoints, fetchNetworkEndpoints] = useNetworkEndpoints() const form = useForm() - const [customFaucetEndpoint, customRpcEndpoint, customQueryEndpoint, customBackendEndpoint] = form.watch([ - 'settings.customFaucetEndpoint', - 'settings.customRpcEndpoint', - 'settings.customQueryEndpoint', - 'settings.customBackendEndpoint', - ]) - const [, storeCustomEndpoints] = useLocalStorage>('custom_endpoint') + const [customFaucetEndpoint, customWsRpcEndpoint, customHttpRpcEndpoint, customQueryEndpoint, customBackendEndpoint] = + form.watch([ + 'settings.customFaucetEndpoint', + 'settings.customWsRpcEndpoint', + 'settings.customHttpRpcEndpoint', + 'settings.customQueryEndpoint', + 'settings.customBackendEndpoint', + ]) + const [, storeCustomEndpoints] = useLocalStorage('custom_endpoint') const [isValidFaucetEndpoint, setIsValidFaucetEndpoint] = useState(true) - const [isValidRpcEndpoint, setIsValidRpcEndpoint] = useState(true) + const [isValidWsRpcEndpoint, setIsValidWsRpcEndpoint] = useState(true) + const [isValidHttpRpcEndpoint, setIsValidHttpRpcEndpoint] = useState(true) const [isValidQueryEndpoint, setIsValidQueryEndpoint] = useState(true) const [isValidBackendEndpoint, setIsValidBackendEndpoint] = useState(true) const [customSaveStatus, setCustomSaveStatus] = useState<'Init' | 'Saving' | 'Done'>('Init') @@ -57,7 +61,8 @@ export const SettingsNetworkTab = () => { useEffect(() => { if (network === 'custom') { - form.setValue('settings.customRpcEndpoint', endpoints.nodeRpcEndpoint) + form.setValue('settings.customWsRpcEndpoint', endpoints.nodeRpcEndpoint) + form.setValue('settings.customHttpRpcEndpoint', endpoints.nodeHttpRpcEndpoint) form.setValue('settings.customQueryEndpoint', endpoints.queryNodeEndpoint) form.setValue('settings.customFaucetEndpoint', endpoints.membershipFaucetEndpoint ?? '') form.setValue('settings.customBackendEndpoint', endpoints.backendEndpoint ?? '') @@ -66,14 +71,16 @@ export const SettingsNetworkTab = () => { useEffect(() => { if ( - isValidRpcEndpoint && + isValidWsRpcEndpoint && + isValidHttpRpcEndpoint && isValidQueryEndpoint && isValidFaucetEndpoint && isValidBackendEndpoint && customSaveStatus === 'Done' ) { storeCustomEndpoints({ - nodeRpcEndpoint: customRpcEndpoint, + nodeRpcEndpoint: customWsRpcEndpoint, + nodeHttpRpcEndpoint: customHttpRpcEndpoint, queryNodeEndpoint: customQueryEndpoint, queryNodeEndpointSubscription: customQueryEndpoint.replace(/^http?/, 'ws'), membershipFaucetEndpoint: customFaucetEndpoint || undefined, @@ -82,11 +89,12 @@ export const SettingsNetworkTab = () => { }) window.location.reload() } - }, [isValidFaucetEndpoint, isValidRpcEndpoint, isValidQueryEndpoint, customSaveStatus]) + }, [isValidFaucetEndpoint, isValidWsRpcEndpoint, isValidQueryEndpoint, customSaveStatus]) const saveSettings = async () => { if ( - !isValidRPCUrl(customRpcEndpoint) || + !isValidWsUrl(customWsRpcEndpoint) || + !isValidHttpUrl(customHttpRpcEndpoint) || !isValidQNUrl(customQueryEndpoint) || !isValidFaucetUrl(customFaucetEndpoint) || !isValidBackendUrl(customBackendEndpoint) @@ -97,7 +105,8 @@ export const SettingsNetworkTab = () => { setCustomSaveStatus('Saving') await Promise.all([ - checkEndpoint(customRpcEndpoint, checkRpcEndpoint, setIsValidRpcEndpoint), + checkEndpoint(customWsRpcEndpoint, checkWsRpcEndpoint, setIsValidWsRpcEndpoint), + checkEndpoint(customHttpRpcEndpoint, checkHttpRpcEndpoint, setIsValidHttpRpcEndpoint), checkEndpoint(customQueryEndpoint, checkGQLEndpoint, setIsValidQueryEndpoint), checkEndpoint(customFaucetEndpoint, checkFaucetEndpoint, setIsValidFaucetEndpoint), checkEndpoint(customBackendEndpoint, checkGQLEndpoint, setIsValidBackendEndpoint), @@ -127,17 +136,36 @@ export const SettingsNetworkTab = () => { !isValidRPCUrl(customRpcEndpoint), 'This RPC endpoint must start with ws or wss'], + [() => !isValidWsUrl(customWsRpcEndpoint), 'This WS RPC endpoint must start with ws or wss'], [ - () => !isValidRpcEndpoint, + () => !isValidWsRpcEndpoint, 'Connection Error. Sometimes it fails due to network speed. Please try to check once more', ] )} > - + + + + !isValidHttpUrl(customHttpRpcEndpoint), 'This HTTP RPC endpoint must start with http or https'], + [() => !isValidHttpRpcEndpoint, 'Connection Error'] + )} + > + { )} @@ -226,7 +256,8 @@ type IsValidOptions = { prefix?: 'https?' | 'wss?'; isRequired?: boolean } const isValid = (url: string, { prefix = 'https?', isRequired = true }: IsValidOptions = {}) => (isRequired === false && url === '') || RegExp(String.raw`${prefix}://\w+/?`, 'i').test(url) -const isValidRPCUrl = (url: string) => isValid(url, { prefix: 'wss?' }) +const isValidWsUrl = (url: string) => isValid(url, { prefix: 'wss?' }) +const isValidHttpUrl = (url: string) => isValid(url, { prefix: 'https?' }) const isValidQNUrl = (url: string) => isValid(url) const isValidFaucetUrl = (url: string) => isValid(url, { isRequired: false }) const isValidBackendUrl = (url: string) => isValid(url, { isRequired: false }) @@ -241,8 +272,8 @@ const checkEndpoint = async ( return isValid } -const checkRpcEndpoint = async (endpoint: string) => { - // check RPC endpoint +const checkWsRpcEndpoint = async (endpoint: string) => { + // check WS RPC endpoint try { return await new Promise((resolve) => { const ws = new WebSocket(endpoint) @@ -261,6 +292,25 @@ const checkRpcEndpoint = async (endpoint: string) => { } } +const checkHttpRpcEndpoint = async (endpoint: string) => { + // check HTTP RPC endpoint + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: random(Number.MAX_SAFE_INTEGER), + jsonrpc: '2.0', + method: 'system_name', + params: [], + }), + }) + return response.status < 400 && (await response.json()).result === 'Joystream Node' + } catch { + return false + } +} + const checkGQLEndpoint = async (endpoint: string) => { // check GraphQL endpoint try { diff --git a/packages/ui/src/common/components/NetworkInfo/NetworkInfo.tsx b/packages/ui/src/common/components/NetworkInfo/NetworkInfo.tsx index 9730b6139b..80b67505d6 100644 --- a/packages/ui/src/common/components/NetworkInfo/NetworkInfo.tsx +++ b/packages/ui/src/common/components/NetworkInfo/NetworkInfo.tsx @@ -10,24 +10,42 @@ import { TextMedium } from '@/common/components/typography' export interface NetworkInfoProps { detailsTitle: string - networkAddress: string - queryNodeAddress: string + networkWsAddress: string + networkHttpAddress: string + queryNodeHttpAddress: string + queryNodeWsAddress: string faucetAddress?: string backendAddress?: string } const NetworkInfo: React.FC = React.memo( - ({ detailsTitle, networkAddress, queryNodeAddress, faucetAddress, backendAddress }) => { + ({ + detailsTitle, + networkWsAddress, + networkHttpAddress, + queryNodeHttpAddress, + queryNodeWsAddress, + faucetAddress, + backendAddress, + }) => { const { t } = useTranslation('settings') return ( }> - {t('networkAddress')} - + {t('networkSubscriptionAddress')} + - {t('QueryNodeAddress')} - + {t('networkHTTPAddress')} + + + + {t('QueryNodeHttpAddress')} + + + + {t('QueryNodeSubscriptionAddress')} + {t('faucet')} diff --git a/packages/ui/src/common/providers/network-endpoints/provider.tsx b/packages/ui/src/common/providers/network-endpoints/provider.tsx index f1e334d340..462ea074a0 100644 --- a/packages/ui/src/common/providers/network-endpoints/provider.tsx +++ b/packages/ui/src/common/providers/network-endpoints/provider.tsx @@ -5,6 +5,7 @@ import { DEFAULT_NETWORK, NetworkEndpoints, pickEndpoints } from '@/app/config' import { Loading } from '@/common/components/Loading' import { useLocalStorage } from '@/common/hooks/useLocalStorage' import { useNetwork } from '@/common/hooks/useNetwork' +import { Optional } from '@/common/types' import { objectEquals } from '@/common/utils' import { NetworkEndpointsContext } from './context' @@ -21,17 +22,19 @@ const EndpointsSchema = Yup.object().shape({ backendEndpoint: Yup.string(), }) -// HACK Pioneer should store the actual HTTP-RPC endpoint. -const endpointReducer: Reducer> = ( +// HACK will approximately guess the http endpoint +const endpointReducer: Reducer> = ( _, newEndpoints ): NetworkEndpoints => ({ ...newEndpoints, - nodeHttpRpcEndpoint: newEndpoints.nodeRpcEndpoint - .replace('ws:', 'http:') - .replace('wss:', 'https:') - .replace('ws-rpc', 'http-rpc') - .replace(':9944', ''), + nodeHttpRpcEndpoint: + newEndpoints.nodeHttpRpcEndpoint ?? + newEndpoints.nodeRpcEndpoint + .replace('ws:', 'http:') + .replace('wss:', 'https:') + .replace('ws-rpc', 'http-rpc') + .replace(':9944', ''), }) export const NetworkEndpointsProvider = ({ children }: Props) => { diff --git a/packages/ui/src/common/types/index.ts b/packages/ui/src/common/types/index.ts index 6922688ef2..4f0737242c 100644 --- a/packages/ui/src/common/types/index.ts +++ b/packages/ui/src/common/types/index.ts @@ -9,3 +9,5 @@ export type AnyKeys = { } export type AnyObject = Record + +export type Optional = Omit & Partial> diff --git a/packages/ui/src/services/i18n/dict/en/settings.json b/packages/ui/src/services/i18n/dict/en/settings.json index 73104a6d1c..e115ede2ad 100644 --- a/packages/ui/src/services/i18n/dict/en/settings.json +++ b/packages/ui/src/services/i18n/dict/en/settings.json @@ -4,15 +4,18 @@ "notifications": "Notifications", "selectNetwork": "Select Network", "networkDetails": "Network Details", - "networkAddress": "Network: ", - "QueryNodeAddress": "Query Node: ", + "networkSubscriptionAddress": "Network subscription: ", + "networkHTTPAddress": "Network HTTP endpoint: ", + "QueryNodeHttpAddress": "Query Node: ", + "QueryNodeSubscriptionAddress": "Query Node subscription: ", "faucet": "Faucet: ", "backend": "Backend: ", "chainInfo": "Chain Information", "rpcBlockheight": "RPC Block Height: ", "qnBlockheight": "Query Node Block Height: ", "customFaucet": "FAUCET", - "customRPCNode": "RPC NODE", + "customWsRpcNode": "RPC NODE SUBSCRIPTION", + "customHttpRpcNode": "RPC NODE HTTP ENDPOINT", "customQueryNode": "QUERY NODE", "customBackend": "BACKEND" -} +} \ No newline at end of file