From 07b707452fd016c3bf7ac0ac474d9c2dd6d4dc22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Tue, 19 Jun 2018 12:19:08 +0200 Subject: [PATCH] add Ripple node config --- package.json | 2 +- src/api/Ripple.js | 12 +-- src/bridge/RippleJSBridge.js | 33 +++++--- src/bridge/types.js | 3 + src/components/FeesField/RippleKind.js | 4 +- .../modals/AccountSettingRenderBody.js | 81 ++++++++++++++++++- static/i18n/en/app.yml | 4 + yarn.lock | 6 +- 8 files changed, 115 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 4361abff..c13ba64b 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@ledgerhq/hw-transport": "^4.13.0", "@ledgerhq/hw-transport-node-hid": "^4.13.0", "@ledgerhq/ledger-core": "2.0.0-rc.1", - "@ledgerhq/live-common": "2.30.0", + "@ledgerhq/live-common": "2.31.0", "async": "^2.6.1", "axios": "^0.18.0", "babel-runtime": "^6.26.0", diff --git a/src/api/Ripple.js b/src/api/Ripple.js index 52a7fe2f..8f0d5fd4 100644 --- a/src/api/Ripple.js +++ b/src/api/Ripple.js @@ -1,7 +1,6 @@ // @flow import logger from 'logger' import { RippleAPI } from 'ripple-lib' -import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' import { parseCurrencyUnit, getCryptoCurrencyById, @@ -10,14 +9,11 @@ import { const rippleUnit = getCryptoCurrencyById('ripple').units[0] -const apiEndpoint = { - ripple: 'wss://s1.ripple.com', -} +export const defaultEndpoint = 'wss://s2.ripple.com' -export const apiForCurrency = (currency: CryptoCurrency) => { - const api = new RippleAPI({ - server: apiEndpoint[currency.id], - }) +export const apiForEndpointConfig = (endpointConfig: ?string = null) => { + const server = endpointConfig || defaultEndpoint + const api = new RippleAPI({ server }) api.on('error', (errorCode, errorMessage) => { logger.warn(`Ripple API error: ${errorCode}: ${errorMessage}`) }) diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js index 27353db0..a8fb4621 100644 --- a/src/bridge/RippleJSBridge.js +++ b/src/bridge/RippleJSBridge.js @@ -10,7 +10,8 @@ import { getDerivations } from 'helpers/derivations' import getAddress from 'commands/getAddress' import signTransaction from 'commands/signTransaction' import { - apiForCurrency, + apiForEndpointConfig, + defaultEndpoint, parseAPIValue, parseAPICurrencyObject, formatAPICurrencyXRP, @@ -47,7 +48,7 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps) => ( ) async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) { - const api = apiForCurrency(a.currency) + const api = apiForEndpointConfig(a.endpointConfig) try { await api.connect() const amount = formatAPICurrencyXRP(t.amount) @@ -217,10 +218,11 @@ const txToOperation = (account: Account) => ({ return op } -const getServerInfo = (perCurrencyId => currency => { - if (perCurrencyId[currency.id]) return perCurrencyId[currency.id]() +const getServerInfo = (map => endpointConfig => { + if (!endpointConfig) endpointConfig = '' + if (map[endpointConfig]) return map[endpointConfig]() const f = throttle(async () => { - const api = apiForCurrency(currency) + const api = apiForEndpointConfig(endpointConfig) try { await api.connect() const res = await api.getServerInfo() @@ -232,7 +234,7 @@ const getServerInfo = (perCurrencyId => currency => { api.disconnect() } }, 60000) - perCurrencyId[currency.id] = f + map[endpointConfig] = f return f() })({}) @@ -244,10 +246,10 @@ const RippleJSBridge: WalletBridge = { } async function main() { - const api = apiForCurrency(currency) + const api = apiForEndpointConfig() try { await api.connect() - const serverInfo = await getServerInfo(currency) + const serverInfo = await getServerInfo() const ledgers = serverInfo.completeLedgers.split('-') const minLedgerVersion = Number(ledgers[0]) const maxLedgerVersion = Number(ledgers[1]) @@ -342,7 +344,7 @@ const RippleJSBridge: WalletBridge = { return { unsubscribe } }, - synchronize: ({ currency, freshAddress, blockHeight }) => + synchronize: ({ endpointConfig, freshAddress, blockHeight }) => Observable.create(o => { let finished = false const unsubscribe = () => { @@ -350,11 +352,11 @@ const RippleJSBridge: WalletBridge = { } async function main() { - const api = apiForCurrency(currency) + const api = apiForEndpointConfig(endpointConfig) try { await api.connect() if (finished) return - const serverInfo = await getServerInfo(currency) + const serverInfo = await getServerInfo(endpointConfig) if (finished) return const ledgers = serverInfo.completeLedgers.split('-') const minLedgerVersion = Number(ledgers[0]) @@ -456,7 +458,7 @@ const RippleJSBridge: WalletBridge = { isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, canBeSpent: async (a, t) => { - const r = await getServerInfo(a.currency) + const r = await getServerInfo(a.endpointConfig) return t.amount + t.fee + parseAPIValue(r.validatedLedger.reserveBaseXRP) <= a.balance }, @@ -495,6 +497,13 @@ const RippleJSBridge: WalletBridge = { ), ), }), + + getDefaultEndpointConfig: () => defaultEndpoint, + + validateEndpointConfig: async endpointConfig => { + const api = apiForEndpointConfig(endpointConfig) + await api.connect() + }, } export default RippleJSBridge diff --git a/src/bridge/types.js b/src/bridge/types.js index 75bbe777..530befe4 100644 --- a/src/bridge/types.js +++ b/src/bridge/types.js @@ -111,4 +111,7 @@ export interface WalletBridge { // Implement an optimistic response for signAndBroadcast. // you likely should add the operation in account.pendingOperations but maybe you want to clean it (because maybe some are replaced / cancelled by this one?) addPendingOperation?: (account: Account, optimisticOperation: Operation) => Account; + + getDefaultEndpointConfig?: () => string; + validateEndpointConfig?: (endpointConfig: string) => Promise; } diff --git a/src/components/FeesField/RippleKind.js b/src/components/FeesField/RippleKind.js index 2677ea45..f40a22e5 100644 --- a/src/components/FeesField/RippleKind.js +++ b/src/components/FeesField/RippleKind.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import type { Account } from '@ledgerhq/live-common/lib/types' -import { apiForCurrency, parseAPIValue } from 'api/Ripple' +import { apiForEndpointConfig, parseAPIValue } from 'api/Ripple' import InputCurrency from 'components/base/InputCurrency' import GenericContainer from './GenericContainer' @@ -24,7 +24,7 @@ class FeesField extends Component { this.sync() } async sync() { - const api = apiForCurrency(this.props.account.currency) + const api = apiForEndpointConfig(this.props.account.endpointConfig) try { await api.connect() const info = await api.getServerInfo() diff --git a/src/components/modals/AccountSettingRenderBody.js b/src/components/modals/AccountSettingRenderBody.js index 510613d2..ee8594e8 100644 --- a/src/components/modals/AccountSettingRenderBody.js +++ b/src/components/modals/AccountSettingRenderBody.js @@ -14,6 +14,8 @@ import { MODAL_SETTINGS_ACCOUNT } from 'config/constants' import { updateAccount, removeAccount } from 'actions/accounts' import { setDataModal } from 'reducers/modals' +import { getBridgeForCurrency } from 'bridge' + import Spoiler from 'components/base/Spoiler' import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon' import Box from 'components/base/Box' @@ -30,9 +32,11 @@ import { } from 'components/base/Modal' type State = { - accountName: string | null, - accountUnit: Unit | null, + accountName: ?string, + accountUnit: ?Unit, + endpointConfig: ?string, accountNameError: boolean, + endpointConfigError: ?Error, isRemoveAccountModalOpen: boolean, } @@ -45,6 +49,8 @@ type Props = { data: any, } +const canConfigureEndpointConfig = account => account.currency.id === 'ripple' + const unitGetOptionValue = unit => unit.magnitude const renderUnitItemCode = item => item.data.code @@ -57,7 +63,9 @@ const mapDispatchToProps = { const defaultState = { accountName: null, accountUnit: null, + endpointConfig: null, accountNameError: false, + endpointConfigError: null, isRemoveAccountModalOpen: false, } @@ -66,7 +74,12 @@ class HelperComp extends PureComponent { ...defaultState, } + componentWillUnmount() { + this.handleChangeEndpointConfig_id++ + } + getAccount(data: Object): Account { + // FIXME this should be a selector const { accountName } = this.state const account = get(data, 'account', {}) @@ -80,6 +93,31 @@ class HelperComp extends PureComponent { } } + handleChangeEndpointConfig_id = 0 + handleChangeEndpointConfig = async (endpointConfig: string) => { + const bridge = getBridgeForCurrency(this.getAccount(this.props.data).currency) + this.handleChangeEndpointConfig_id++ + const { handleChangeEndpointConfig_id } = this + this.setState({ + endpointConfig, + endpointConfigError: null, + }) + try { + if (bridge.validateEndpointConfig) { + await bridge.validateEndpointConfig(endpointConfig) + } + if (handleChangeEndpointConfig_id === this.handleChangeEndpointConfig_id) { + this.setState({ + endpointConfigError: null, + }) + } + } catch (endpointConfigError) { + if (handleChangeEndpointConfig_id === this.handleChangeEndpointConfig_id) { + this.setState({ endpointConfigError }) + } + } + } + handleChangeName = (value: string) => this.setState({ accountName: value, @@ -91,7 +129,7 @@ class HelperComp extends PureComponent { e.preventDefault() const { updateAccount, setDataModal } = this.props - const { accountName, accountUnit } = this.state + const { accountName, accountUnit, endpointConfig, endpointConfigError } = this.state const sanitizedAccountName = accountName ? accountName.replace(/\s+/g, ' ').trim() : null if (account.name || sanitizedAccountName) { @@ -100,6 +138,9 @@ class HelperComp extends PureComponent { unit: accountUnit || account.unit, name: sanitizedAccountName || account.name, } + if (endpointConfig && !endpointConfigError) { + account.endpointConfig = endpointConfig + } updateAccount(account) setDataModal(MODAL_SETTINGS_ACCOUNT, { account }) onClose() @@ -123,7 +164,9 @@ class HelperComp extends PureComponent { handleChangeUnit = (value: Unit) => { this.setState({ accountUnit: value }) } + handleOpenRemoveAccountModal = () => this.setState({ isRemoveAccountModalOpen: true }) + handleCloseRemoveAccountModal = () => this.setState({ isRemoveAccountModalOpen: false }) handleRemoveAccount = (account: Account) => { @@ -134,10 +177,17 @@ class HelperComp extends PureComponent { } render() { - const { accountUnit, accountNameError, isRemoveAccountModalOpen } = this.state + const { + accountUnit, + endpointConfig, + accountNameError, + isRemoveAccountModalOpen, + endpointConfigError, + } = this.state const { t, onClose, data } = this.props const account = this.getAccount(data) + const bridge = getBridgeForCurrency(account.currency) const usefulData = { xpub: account.xpub || undefined, @@ -184,6 +234,29 @@ class HelperComp extends PureComponent { /> + {canConfigureEndpointConfig(account) ? ( + + + {t('app:account.settings.endpointConfig.title')} + {t('app:account.settings.endpointConfig.desc')} + + + this.handleFocus(e, 'endpointConfig')} + error={ + endpointConfigError ? t('app:account.settings.endpointConfig.error') : false + } + /> + + + ) : null}