Browse Source

Merge pull request #614 from gre/ripple-node-config

add Ripple node config
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
65d59c198a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      package.json
  2. 12
      src/api/Ripple.js
  3. 33
      src/bridge/RippleJSBridge.js
  4. 3
      src/bridge/types.js
  5. 4
      src/components/FeesField/RippleKind.js
  6. 81
      src/components/modals/AccountSettingRenderBody.js
  7. 4
      static/i18n/en/app.yml
  8. 6
      yarn.lock

2
package.json

@ -42,7 +42,7 @@
"@ledgerhq/hw-transport": "^4.13.0", "@ledgerhq/hw-transport": "^4.13.0",
"@ledgerhq/hw-transport-node-hid": "^4.13.0", "@ledgerhq/hw-transport-node-hid": "^4.13.0",
"@ledgerhq/ledger-core": "2.0.0-rc.1", "@ledgerhq/ledger-core": "2.0.0-rc.1",
"@ledgerhq/live-common": "2.30.0", "@ledgerhq/live-common": "2.31.0",
"async": "^2.6.1", "async": "^2.6.1",
"axios": "^0.18.0", "axios": "^0.18.0",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",

12
src/api/Ripple.js

@ -1,7 +1,6 @@
// @flow // @flow
import logger from 'logger' import logger from 'logger'
import { RippleAPI } from 'ripple-lib' import { RippleAPI } from 'ripple-lib'
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { import {
parseCurrencyUnit, parseCurrencyUnit,
getCryptoCurrencyById, getCryptoCurrencyById,
@ -10,14 +9,11 @@ import {
const rippleUnit = getCryptoCurrencyById('ripple').units[0] const rippleUnit = getCryptoCurrencyById('ripple').units[0]
const apiEndpoint = { export const defaultEndpoint = 'wss://s2.ripple.com'
ripple: 'wss://s1.ripple.com',
}
export const apiForCurrency = (currency: CryptoCurrency) => { export const apiForEndpointConfig = (endpointConfig: ?string = null) => {
const api = new RippleAPI({ const server = endpointConfig || defaultEndpoint
server: apiEndpoint[currency.id], const api = new RippleAPI({ server })
})
api.on('error', (errorCode, errorMessage) => { api.on('error', (errorCode, errorMessage) => {
logger.warn(`Ripple API error: ${errorCode}: ${errorMessage}`) logger.warn(`Ripple API error: ${errorCode}: ${errorMessage}`)
}) })

33
src/bridge/RippleJSBridge.js

@ -10,7 +10,8 @@ import { getDerivations } from 'helpers/derivations'
import getAddress from 'commands/getAddress' import getAddress from 'commands/getAddress'
import signTransaction from 'commands/signTransaction' import signTransaction from 'commands/signTransaction'
import { import {
apiForCurrency, apiForEndpointConfig,
defaultEndpoint,
parseAPIValue, parseAPIValue,
parseAPICurrencyObject, parseAPICurrencyObject,
formatAPICurrencyXRP, formatAPICurrencyXRP,
@ -47,7 +48,7 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps<Transaction>) => (
) )
async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) { async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) {
const api = apiForCurrency(a.currency) const api = apiForEndpointConfig(a.endpointConfig)
try { try {
await api.connect() await api.connect()
const amount = formatAPICurrencyXRP(t.amount) const amount = formatAPICurrencyXRP(t.amount)
@ -217,10 +218,11 @@ const txToOperation = (account: Account) => ({
return op return op
} }
const getServerInfo = (perCurrencyId => currency => { const getServerInfo = (map => endpointConfig => {
if (perCurrencyId[currency.id]) return perCurrencyId[currency.id]() if (!endpointConfig) endpointConfig = ''
if (map[endpointConfig]) return map[endpointConfig]()
const f = throttle(async () => { const f = throttle(async () => {
const api = apiForCurrency(currency) const api = apiForEndpointConfig(endpointConfig)
try { try {
await api.connect() await api.connect()
const res = await api.getServerInfo() const res = await api.getServerInfo()
@ -232,7 +234,7 @@ const getServerInfo = (perCurrencyId => currency => {
api.disconnect() api.disconnect()
} }
}, 60000) }, 60000)
perCurrencyId[currency.id] = f map[endpointConfig] = f
return f() return f()
})({}) })({})
@ -244,10 +246,10 @@ const RippleJSBridge: WalletBridge<Transaction> = {
} }
async function main() { async function main() {
const api = apiForCurrency(currency) const api = apiForEndpointConfig()
try { try {
await api.connect() await api.connect()
const serverInfo = await getServerInfo(currency) const serverInfo = await getServerInfo()
const ledgers = serverInfo.completeLedgers.split('-') const ledgers = serverInfo.completeLedgers.split('-')
const minLedgerVersion = Number(ledgers[0]) const minLedgerVersion = Number(ledgers[0])
const maxLedgerVersion = Number(ledgers[1]) const maxLedgerVersion = Number(ledgers[1])
@ -342,7 +344,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
return { unsubscribe } return { unsubscribe }
}, },
synchronize: ({ currency, freshAddress, blockHeight }) => synchronize: ({ endpointConfig, freshAddress, blockHeight }) =>
Observable.create(o => { Observable.create(o => {
let finished = false let finished = false
const unsubscribe = () => { const unsubscribe = () => {
@ -350,11 +352,11 @@ const RippleJSBridge: WalletBridge<Transaction> = {
} }
async function main() { async function main() {
const api = apiForCurrency(currency) const api = apiForEndpointConfig(endpointConfig)
try { try {
await api.connect() await api.connect()
if (finished) return if (finished) return
const serverInfo = await getServerInfo(currency) const serverInfo = await getServerInfo(endpointConfig)
if (finished) return if (finished) return
const ledgers = serverInfo.completeLedgers.split('-') const ledgers = serverInfo.completeLedgers.split('-')
const minLedgerVersion = Number(ledgers[0]) const minLedgerVersion = Number(ledgers[0])
@ -456,7 +458,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false,
canBeSpent: async (a, t) => { 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 return t.amount + t.fee + parseAPIValue(r.validatedLedger.reserveBaseXRP) <= a.balance
}, },
@ -495,6 +497,13 @@ const RippleJSBridge: WalletBridge<Transaction> = {
), ),
), ),
}), }),
getDefaultEndpointConfig: () => defaultEndpoint,
validateEndpointConfig: async endpointConfig => {
const api = apiForEndpointConfig(endpointConfig)
await api.connect()
},
} }
export default RippleJSBridge export default RippleJSBridge

3
src/bridge/types.js

@ -111,4 +111,7 @@ export interface WalletBridge<Transaction> {
// Implement an optimistic response for signAndBroadcast. // 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?) // 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; addPendingOperation?: (account: Account, optimisticOperation: Operation) => Account;
getDefaultEndpointConfig?: () => string;
validateEndpointConfig?: (endpointConfig: string) => Promise<void>;
} }

4
src/components/FeesField/RippleKind.js

@ -2,7 +2,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types' 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 InputCurrency from 'components/base/InputCurrency'
import GenericContainer from './GenericContainer' import GenericContainer from './GenericContainer'
@ -24,7 +24,7 @@ class FeesField extends Component<Props, State> {
this.sync() this.sync()
} }
async sync() { async sync() {
const api = apiForCurrency(this.props.account.currency) const api = apiForEndpointConfig(this.props.account.endpointConfig)
try { try {
await api.connect() await api.connect()
const info = await api.getServerInfo() const info = await api.getServerInfo()

81
src/components/modals/AccountSettingRenderBody.js

@ -14,6 +14,8 @@ import { MODAL_SETTINGS_ACCOUNT } from 'config/constants'
import { updateAccount, removeAccount } from 'actions/accounts' import { updateAccount, removeAccount } from 'actions/accounts'
import { setDataModal } from 'reducers/modals' import { setDataModal } from 'reducers/modals'
import { getBridgeForCurrency } from 'bridge'
import Spoiler from 'components/base/Spoiler' import Spoiler from 'components/base/Spoiler'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon' import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import Box from 'components/base/Box' import Box from 'components/base/Box'
@ -30,9 +32,11 @@ import {
} from 'components/base/Modal' } from 'components/base/Modal'
type State = { type State = {
accountName: string | null, accountName: ?string,
accountUnit: Unit | null, accountUnit: ?Unit,
endpointConfig: ?string,
accountNameError: boolean, accountNameError: boolean,
endpointConfigError: ?Error,
isRemoveAccountModalOpen: boolean, isRemoveAccountModalOpen: boolean,
} }
@ -45,6 +49,8 @@ type Props = {
data: any, data: any,
} }
const canConfigureEndpointConfig = account => account.currency.id === 'ripple'
const unitGetOptionValue = unit => unit.magnitude const unitGetOptionValue = unit => unit.magnitude
const renderUnitItemCode = item => item.data.code const renderUnitItemCode = item => item.data.code
@ -57,7 +63,9 @@ const mapDispatchToProps = {
const defaultState = { const defaultState = {
accountName: null, accountName: null,
accountUnit: null, accountUnit: null,
endpointConfig: null,
accountNameError: false, accountNameError: false,
endpointConfigError: null,
isRemoveAccountModalOpen: false, isRemoveAccountModalOpen: false,
} }
@ -66,7 +74,12 @@ class HelperComp extends PureComponent<Props, State> {
...defaultState, ...defaultState,
} }
componentWillUnmount() {
this.handleChangeEndpointConfig_id++
}
getAccount(data: Object): Account { getAccount(data: Object): Account {
// FIXME this should be a selector
const { accountName } = this.state const { accountName } = this.state
const account = get(data, 'account', {}) const account = get(data, 'account', {})
@ -80,6 +93,31 @@ class HelperComp extends PureComponent<Props, State> {
} }
} }
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) => handleChangeName = (value: string) =>
this.setState({ this.setState({
accountName: value, accountName: value,
@ -91,7 +129,7 @@ class HelperComp extends PureComponent<Props, State> {
e.preventDefault() e.preventDefault()
const { updateAccount, setDataModal } = this.props 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 const sanitizedAccountName = accountName ? accountName.replace(/\s+/g, ' ').trim() : null
if (account.name || sanitizedAccountName) { if (account.name || sanitizedAccountName) {
@ -100,6 +138,9 @@ class HelperComp extends PureComponent<Props, State> {
unit: accountUnit || account.unit, unit: accountUnit || account.unit,
name: sanitizedAccountName || account.name, name: sanitizedAccountName || account.name,
} }
if (endpointConfig && !endpointConfigError) {
account.endpointConfig = endpointConfig
}
updateAccount(account) updateAccount(account)
setDataModal(MODAL_SETTINGS_ACCOUNT, { account }) setDataModal(MODAL_SETTINGS_ACCOUNT, { account })
onClose() onClose()
@ -123,7 +164,9 @@ class HelperComp extends PureComponent<Props, State> {
handleChangeUnit = (value: Unit) => { handleChangeUnit = (value: Unit) => {
this.setState({ accountUnit: value }) this.setState({ accountUnit: value })
} }
handleOpenRemoveAccountModal = () => this.setState({ isRemoveAccountModalOpen: true }) handleOpenRemoveAccountModal = () => this.setState({ isRemoveAccountModalOpen: true })
handleCloseRemoveAccountModal = () => this.setState({ isRemoveAccountModalOpen: false }) handleCloseRemoveAccountModal = () => this.setState({ isRemoveAccountModalOpen: false })
handleRemoveAccount = (account: Account) => { handleRemoveAccount = (account: Account) => {
@ -134,10 +177,17 @@ class HelperComp extends PureComponent<Props, State> {
} }
render() { render() {
const { accountUnit, accountNameError, isRemoveAccountModalOpen } = this.state const {
accountUnit,
endpointConfig,
accountNameError,
isRemoveAccountModalOpen,
endpointConfigError,
} = this.state
const { t, onClose, data } = this.props const { t, onClose, data } = this.props
const account = this.getAccount(data) const account = this.getAccount(data)
const bridge = getBridgeForCurrency(account.currency)
const usefulData = { const usefulData = {
xpub: account.xpub || undefined, xpub: account.xpub || undefined,
@ -184,6 +234,29 @@ class HelperComp extends PureComponent<Props, State> {
/> />
</Box> </Box>
</Container> </Container>
{canConfigureEndpointConfig(account) ? (
<Container>
<Box>
<OptionRowTitle>{t('app:account.settings.endpointConfig.title')}</OptionRowTitle>
<OptionRowDesc>{t('app:account.settings.endpointConfig.desc')}</OptionRowDesc>
</Box>
<Box>
<Input
value={
endpointConfig ||
account.endpointConfig ||
(bridge.getDefaultEndpointConfig && bridge.getDefaultEndpointConfig()) ||
''
}
onChange={this.handleChangeEndpointConfig}
onFocus={e => this.handleFocus(e, 'endpointConfig')}
error={
endpointConfigError ? t('app:account.settings.endpointConfig.error') : false
}
/>
</Box>
</Container>
) : null}
<Spoiler title={t('app:account.settings.advancedLogs')}> <Spoiler title={t('app:account.settings.advancedLogs')}>
<textarea <textarea
readOnly readOnly

4
static/i18n/en/app.yml

@ -78,6 +78,10 @@ account:
unit: unit:
title: Unit title: Unit
desc: Lorem ipsum dolort amet desc: Lorem ipsum dolort amet
endpointConfig:
title: Node
desc: The API node to use
error: Invalid endpoint
dashboard: dashboard:
title: Dashboard title: Dashboard
accounts: accounts:

6
yarn.lock

@ -1515,9 +1515,9 @@
npm "^5.7.1" npm "^5.7.1"
prebuild-install "^2.2.2" prebuild-install "^2.2.2"
"@ledgerhq/live-common@2.30.0": "@ledgerhq/live-common@2.31.0":
version "2.30.0" version "2.31.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.30.0.tgz#c46fbb1fef3347b6ae9a693bfc4f792c20c9ee9b" resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.31.0.tgz#0f599a1e23b64d9ed74a845d3a9c82f0696f1df3"
dependencies: dependencies:
axios "^0.18.0" axios "^0.18.0"
invariant "^2.2.2" invariant "^2.2.2"

Loading…
Cancel
Save