diff --git a/src/renderer/components/settings/AppSettings.stories.tsx b/src/renderer/components/settings/AppSettings.stories.tsx index 87eb4972c..5ac618e69 100644 --- a/src/renderer/components/settings/AppSettings.stories.tsx +++ b/src/renderer/components/settings/AppSettings.stories.tsx @@ -16,6 +16,8 @@ type StoryArgs = { goToReleasePage: (version: string) => void changeLocale: (locale: Locale) => void onChangeMidgardUrl: (url: string) => void + onChangeThornodeNodeUrl: (url: string) => void + onChangeThornodeRpcUrl: (url: string) => void changeNetwork: ChangeNetworkHandler collapsed: boolean } @@ -26,6 +28,8 @@ const Template = ({ checkForUpdates, goToReleasePage, onChangeMidgardUrl, + onChangeThornodeRpcUrl, + onChangeThornodeNodeUrl, changeLocale, collapsed }: StoryArgs) => { @@ -47,8 +51,14 @@ const Template = ({ collapsed={collapsed} toggleCollapse={() => console.log('toggle')} midgardUrl={RD.pending} + thornodeNodeUrl="thornode-node-url" + thornodeRpcUrl="thornode-rpc-url" onChangeMidgardUrl={onChangeMidgardUrl} + onChangeThornodeRpcUrl={onChangeThornodeRpcUrl} + onChangeThornodeNodeUrl={onChangeThornodeNodeUrl} checkMidgardUrl$={(url, _) => Rx.of(RD.success(url))} + checkThornodeNodeUrl$={(url, _) => Rx.of(RD.success(url))} + checkThornodeRpcUrl$={(url, _) => Rx.of(RD.success(url))} /> ) } @@ -76,6 +86,12 @@ const meta: ComponentMeta = { }, onChangeMidgardUrl: { action: 'onChangeMidgardUrl' + }, + onChangeThornodeNodeUrl: { + action: 'onChangeThornodeNodeUrl' + }, + onChangeThornodeRpcUrl: { + action: 'onChangeThornodeRpcUrl' } }, args: { onlineStatus: OnlineStatus.ON, updateDataRD: 'initial', collapsed: false } diff --git a/src/renderer/components/settings/AppSettings.tsx b/src/renderer/components/settings/AppSettings.tsx index 787965e29..8fb0b3318 100644 --- a/src/renderer/components/settings/AppSettings.tsx +++ b/src/renderer/components/settings/AppSettings.tsx @@ -12,6 +12,7 @@ import { Locale } from '../../../shared/i18n/types' import { LOCALES } from '../../i18n' import { AVAILABLE_NETWORKS } from '../../services/const' import { CheckMidgardUrlHandler, MidgardUrlRD } from '../../services/midgard/types' +import { CheckThornodeNodeUrlHandler, CheckThornodeRpcUrlHandler } from '../../services/thorchain/types' import { DownIcon } from '../icons' import { Menu } from '../shared/menu' import { BorderButton, TextButton } from '../uielements/button' @@ -34,6 +35,12 @@ export type Props = { midgardUrl: MidgardUrlRD onChangeMidgardUrl: (url: string) => void checkMidgardUrl$: CheckMidgardUrlHandler + checkThornodeNodeUrl$: CheckThornodeNodeUrlHandler + onChangeThornodeNodeUrl: (url: string) => void + checkThornodeRpcUrl$: CheckThornodeRpcUrlHandler + thornodeRpcUrl: string + thornodeNodeUrl: string + onChangeThornodeRpcUrl: (url: string) => void } type SectionProps = { @@ -63,7 +70,13 @@ export const AppSettings: React.FC = (props): JSX.Element => { toggleCollapse, midgardUrl: midgardUrlRD, onChangeMidgardUrl, - checkMidgardUrl$ + checkMidgardUrl$, + onChangeThornodeNodeUrl, + checkThornodeNodeUrl$, + checkThornodeRpcUrl$, + onChangeThornodeRpcUrl, + thornodeRpcUrl, + thornodeNodeUrl } = props const intl = useIntl() @@ -244,15 +257,36 @@ export const AppSettings: React.FC = (props): JSX.Element => { {advancedActive && ( -
- -
+ <> +
+ +
+
+ +
+
+ +
+ )} diff --git a/src/renderer/components/settings/EditableUrl.tsx b/src/renderer/components/settings/EditableUrl.tsx index 42bf3be3e..daf9e1ccf 100644 --- a/src/renderer/components/settings/EditableUrl.tsx +++ b/src/renderer/components/settings/EditableUrl.tsx @@ -23,13 +23,14 @@ type TestUrlHandler = (url: string, intl: IntlShape) => TestUrlLD type Props = { url: string checkUrl$: TestUrlHandler + successMsg: string loading?: boolean onChange?: (url: string) => void className?: string } const EditableUrl: React.FC = (props): JSX.Element => { - const { url: initialUrl, className, loading = false, onChange = FP.constVoid, checkUrl$ } = props + const { url: initialUrl, className, successMsg, loading = false, onChange = FP.constVoid, checkUrl$ } = props const [editableUrl, setEditableUrl] = useState>(O.none) const [url, setUrl] = useState(initialUrl) @@ -86,14 +87,10 @@ const EditableUrl: React.FC = (props): JSX.Element => { {error?.message ?? error.toString()}

), - (_) => ( -

- {intl.formatMessage({ id: 'midgard.url.valid' })} -

- ) + (_) =>

{successMsg}

) ), - [intl, testUrlState] + [successMsg, testUrlState] ) const renderUrl = useCallback(() => { diff --git a/src/renderer/hooks/useThorchainClientUrl.ts b/src/renderer/hooks/useThorchainClientUrl.ts index 73b7c10db..4dc7b0fdc 100644 --- a/src/renderer/hooks/useThorchainClientUrl.ts +++ b/src/renderer/hooks/useThorchainClientUrl.ts @@ -1,19 +1,24 @@ import { useEffect } from 'react' +import * as RD from '@devexperts/remote-data-ts' import { NodeUrl } from '@xchainjs/xchain-thorchain' import * as FP from 'fp-ts/lib/function' import { useObservableState } from 'observable-hooks' +import { useIntl } from 'react-intl' import * as Rx from 'rxjs' +import * as RxAjax from 'rxjs/ajax' import * as RxOp from 'rxjs/operators' import { Network } from '../../shared/api/types' import { useThorchainContext } from '../contexts/ThorchainContext' import { INITIAL_CLIENT_URL } from '../services/thorchain/const' +import { Configuration, HealthApi } from '../types/generated/thornode' import { useNetwork } from './useNetwork' export const useThorchainClientUrl = () => { const { clientUrl$, setClientUrl } = useThorchainContext() const { network } = useNetwork() + const intl = useIntl() const [nodeUrl, networkUpdated] = useObservableState( (network$) => @@ -32,5 +37,42 @@ export const useThorchainClientUrl = () => { const setRpc = (url: string) => setClientUrl({ url, network, type: 'rpc' }) const setNode = (url: string) => setClientUrl({ url, network, type: 'node' }) - return { rpc: nodeUrl.rpc, node: nodeUrl.node, setRpc, setNode } + const checkNode$ = (url: string) => + FP.pipe( + // Check `ping` endpoint + new HealthApi(new Configuration({ basePath: url })).ping(), + RxOp.map((result) => { + const { ping } = result + if (ping) return RD.success(url) + + return RD.failure( + Error(intl.formatMessage({ id: 'setting.thornode.node.error.unhealthy' }, { endpoint: '/ping' })) + ) + }), + RxOp.catchError((_: Error) => + Rx.of(RD.failure(Error(`${intl.formatMessage({ id: 'setting.thornode.node.error.url' })}`))) + ) + ) + + const checkRpc$ = (url: string) => + FP.pipe( + // Check `health` endpoint + // https://docs.tendermint.com/v0.34/rpc/#/Info/health + RxAjax.ajax(`${url}/health`), + RxOp.map(({ response }) => { + // Empty result object means no error + if (response.result && typeof response.result === 'object' && Object.keys(response.result).length === 0) + return RD.success(url) + + return RD.failure( + Error(intl.formatMessage({ id: 'setting.thornode.rpc.error.unhealthy' }, { endpoint: '/ping' })) + ) + }), + + RxOp.catchError((_: Error) => + Rx.of(RD.failure(Error(`${intl.formatMessage({ id: 'thornode.url.error.invalid' })}`))) + ) + ) + + return { rpc: nodeUrl.rpc, node: nodeUrl.node, setRpc, setNode, checkNode$, checkRpc$ } } diff --git a/src/renderer/i18n/de/settings.ts b/src/renderer/i18n/de/settings.ts index 245be6f38..55c072942 100644 --- a/src/renderer/i18n/de/settings.ts +++ b/src/renderer/i18n/de/settings.ts @@ -18,7 +18,14 @@ const settings: SettingMessages = { 'setting.wallet.index': 'Index', 'setting.wallet.index.info': 'Trage die Index Nummer der Ledger Addresse ein, die Du verwenden möchtest', 'setting.wallet.hdpath.legacy.info': 'Veralteter Derivation Pfad {path}', - 'setting.wallet.hdpath.ledgerlive.info': 'Ledger Live Derivation Pfad {path}' + 'setting.wallet.hdpath.ledgerlive.info': 'Ledger Live Derivation Pfad {path}', + 'setting.thornode.node.error.unhealthy': + 'THORNode API URL scheint "unhealthy" zu sein beim Überprüfen von "{endpoint}"', + 'setting.thornode.node.error.url': 'Ungültige THORNode API URL. Bitte überprüfe diese und versuche es erneut.', + 'setting.thornode.rpc.error.url': 'Ungültige THORNode RPC URL. Bitte überprüfe diese und versuche es erneut.', + 'setting.thornode.rpc.error.unhealthy': 'THORNode RPC scheint "unhealthy" zu sein beim Überprüfen von "{endpoint"', + 'setting.thornode.node.valid': 'Gültige THORNode API URL', + 'setting.thornode.rpc.valid': 'Gültige THORNode RPC URL' } export default settings diff --git a/src/renderer/i18n/en/settings.ts b/src/renderer/i18n/en/settings.ts index 9dd32314e..fe86a757e 100644 --- a/src/renderer/i18n/en/settings.ts +++ b/src/renderer/i18n/en/settings.ts @@ -18,7 +18,13 @@ const settings: SettingMessages = { 'setting.wallet.index': 'Index', 'setting.wallet.index.info': 'Enter the index number of the Ledger address you want to use', 'setting.wallet.hdpath.legacy.info': 'Legacy derivation path {path}', - 'setting.wallet.hdpath.ledgerlive.info': 'Ledger Live derivation path {path}' + 'setting.wallet.hdpath.ledgerlive.info': 'Ledger Live derivation path {path}', + 'setting.thornode.node.error.unhealthy': 'THORNode API seems to be unhealthy by checking "{endpoint}"', + 'setting.thornode.node.error.url': 'Invalid THORNode API URL. Please double check and try again', + 'setting.thornode.rpc.error.url': 'Invalid THORNode RPC URL. Please double check and try again', + 'setting.thornode.rpc.error.unhealthy': 'THORNode RPC seems to be unhealthy by checking "{endpoint}"', + 'setting.thornode.node.valid': 'Valid THORNode API URL', + 'setting.thornode.rpc.valid': 'Valid THORNode RPC URL' } export default settings diff --git a/src/renderer/i18n/fr/settings.ts b/src/renderer/i18n/fr/settings.ts index 0ab16e222..739318e9d 100644 --- a/src/renderer/i18n/fr/settings.ts +++ b/src/renderer/i18n/fr/settings.ts @@ -18,7 +18,13 @@ const settings: SettingMessages = { 'setting.wallet.index': 'Index', 'setting.wallet.index.info': "Entrez le numéro d'index de l'adresse Ledger que vous souhaitez utiliser", 'setting.wallet.hdpath.legacy.info': 'Legacy derivation path {path} - FR', - 'setting.wallet.hdpath.ledgerlive.info': 'Ledger Live derivation path {path} - FR' + 'setting.wallet.hdpath.ledgerlive.info': 'Ledger Live derivation path {path} - FR', + 'setting.thornode.node.error.unhealthy': 'THORNode API seems to be unhealthy by checking "{endpoint} - FR"', + 'setting.thornode.node.error.url': 'Invalid THORNode API URL. Please double check and try again - FR', + 'setting.thornode.rpc.error.url': 'Invalid THORNode RPC URL. Please double check and try again - FR', + 'setting.thornode.rpc.error.unhealthy': 'THORNode RPC seems to be unhealthy by checking "{endpoint}" - FR', + 'setting.thornode.node.valid': 'Valid THORNode API URL - FR', + 'setting.thornode.rpc.valid': 'Valid THORNode RPC URL - FR' } export default settings diff --git a/src/renderer/i18n/ru/settings.ts b/src/renderer/i18n/ru/settings.ts index e3956febb..7417f775f 100644 --- a/src/renderer/i18n/ru/settings.ts +++ b/src/renderer/i18n/ru/settings.ts @@ -18,7 +18,13 @@ const settings: SettingMessages = { 'setting.wallet.index': 'Индекс', 'setting.wallet.index.info': 'Введите индекс Ledger адреса, который вы хотите использовать', 'setting.wallet.hdpath.legacy.info': 'Устаревший путь деривации {path}', - 'setting.wallet.hdpath.ledgerlive.info': 'Путь деривации Ledger Live {path}' + 'setting.wallet.hdpath.ledgerlive.info': 'Путь деривации Ledger Live {path}', + 'setting.thornode.node.error.unhealthy': 'THORNode API seems to be unhealthy by checking "{endpoint} - RU"', + 'setting.thornode.node.error.url': 'Invalid THORNode API URL. Please double check and try again - RU', + 'setting.thornode.rpc.error.url': 'Invalid THORNode RPC URL. Please double check and try again - RU', + 'setting.thornode.rpc.error.unhealthy': 'THORNode RPC seems to be unhealthy by checking "{endpoint}" - RU', + 'setting.thornode.node.valid': 'Valid THORNode API URL - RU', + 'setting.thornode.rpc.valid': 'Valid THORNode RPC URL - RU' } export default settings diff --git a/src/renderer/i18n/types.ts b/src/renderer/i18n/types.ts index c37ab2ccf..de47aa064 100644 --- a/src/renderer/i18n/types.ts +++ b/src/renderer/i18n/types.ts @@ -328,6 +328,12 @@ type SettingMessageKey = | 'setting.wallet.index.info' | 'setting.wallet.hdpath.legacy.info' | 'setting.wallet.hdpath.ledgerlive.info' + | 'setting.thornode.node.error.url' + | 'setting.thornode.node.error.unhealthy' + | 'setting.thornode.rpc.error.url' + | 'setting.thornode.rpc.error.unhealthy' + | 'setting.thornode.node.valid' + | 'setting.thornode.rpc.valid' export type SettingMessages = { [key in SettingMessageKey]: string } diff --git a/src/renderer/services/thorchain/common.ts b/src/renderer/services/thorchain/common.ts index 52a7bb533..10b533dbd 100644 --- a/src/renderer/services/thorchain/common.ts +++ b/src/renderer/services/thorchain/common.ts @@ -23,7 +23,7 @@ const setClientUrl = ({ url, network, type }: { url: string; network: Network; t // TODO(@veado) Store data persistent on disc const current = getClientUrl() const cNetwork = toClientNetwork(network) - _setClientUrl({ ...current, [cNetwork]: { ...[cNetwork], [type]: url } }) + _setClientUrl({ ...current, [cNetwork]: { ...current[cNetwork], [type]: url } }) } /** diff --git a/src/renderer/services/thorchain/types.ts b/src/renderer/services/thorchain/types.ts index d5859fffb..458084b98 100644 --- a/src/renderer/services/thorchain/types.ts +++ b/src/renderer/services/thorchain/types.ts @@ -5,6 +5,7 @@ import { Asset, BaseAmount } from '@xchainjs/xchain-util' import * as O from 'fp-ts/Option' import * as t from 'io-ts' import { optionFromNullable } from 'io-ts-types/lib/optionFromNullable' +import { IntlShape } from 'react-intl' import * as Rx from 'rxjs' import { assetIO } from '../../../shared/api/io' @@ -29,9 +30,16 @@ export type NodeUrl$ = Rx.Observable export type NodeUrlLD = LiveData export type NodeUrlRD = RD.RemoteData +export type CheckThornodeNodeRpcHandler = (url: string, intl: IntlShape) => LiveData + type UrlRD = RD.RemoteData +type CheckUrlHandler = (url: string, intl: IntlShape) => LiveData + export type ThornodeNodeUrlRD = UrlRD +export type CheckThornodeNodeUrlHandler = CheckUrlHandler + export type ThornodeRpcUrlRD = UrlRD +export type CheckThornodeRpcUrlHandler = CheckUrlHandler export type FeesService = C.FeesService diff --git a/src/renderer/views/app/AppSettingsView.tsx b/src/renderer/views/app/AppSettingsView.tsx index 62f883438..b199ae303 100644 --- a/src/renderer/views/app/AppSettingsView.tsx +++ b/src/renderer/views/app/AppSettingsView.tsx @@ -12,6 +12,7 @@ import { useMidgardContext } from '../../contexts/MidgardContext' import { useAppUpdate } from '../../hooks/useAppUpdate' import { useCollapsedSetting } from '../../hooks/useCollapsedSetting' import { useNetwork } from '../../hooks/useNetwork' +import { useThorchainClientUrl } from '../../hooks/useThorchainClientUrl' export const AppSettingsView: React.FC = (): JSX.Element => { const { network, changeNetwork } = useNetwork() @@ -23,6 +24,15 @@ export const AppSettingsView: React.FC = (): JSX.Element => { const { collapsed, toggle: toggleCollapse } = useCollapsedSetting('app') + const { + node: thornodeNodeUrl, + rpc: thornodeRpcUrl, + setRpc: setThornodeRpcUrl, + setNode: setThornodeNodeUrl, + checkRpc$: checkThornodeRpcUrl$, + checkNode$: checkThornodeNodeUrl$ + } = useThorchainClientUrl() + const { changeLocale, locale$ } = useI18nContext() const currentLocale = useObservableState(locale$, DEFAULT_LOCALE) @@ -52,7 +62,13 @@ export const AppSettingsView: React.FC = (): JSX.Element => { toggleCollapse={toggleCollapse} midgardUrl={midgardUrl} onChangeMidgardUrl={updateMidgardUrlHandler} + onChangeThornodeNodeUrl={setThornodeNodeUrl} + onChangeThornodeRpcUrl={setThornodeRpcUrl} checkMidgardUrl$={checkMidgardUrl$} + thornodeRpcUrl={thornodeRpcUrl} + thornodeNodeUrl={thornodeNodeUrl} + checkThornodeRpcUrl$={checkThornodeRpcUrl$} + checkThornodeNodeUrl$={checkThornodeNodeUrl$} /> ) }