diff --git a/package.json b/package.json index 9ade42e9..d50c20e0 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "dependencies": { "@ledgerhq/hw-app-btc": "^4.12.0", "@ledgerhq/hw-app-eth": "^4.12.0", + "@ledgerhq/hw-app-xrp": "^4.12.0", "@ledgerhq/hw-transport": "^4.12.0", "@ledgerhq/hw-transport-node-hid": "^4.12.0", "@ledgerhq/ledger-core": "^1.2.0", @@ -83,6 +84,10 @@ "redux-actions": "^2.3.0", "redux-thunk": "^2.2.0", "reselect": "^3.0.1", + "ripple-binary-codec": "^0.1.13", + "ripple-bs58check": "^2.0.2", + "ripple-hashes": "^0.3.1", + "ripple-lib": "^1.0.0-beta.0", "rxjs": "^6.2.0", "rxjs-compat": "^6.1.0", "smooth-scrollbar": "^8.2.7", diff --git a/src/api/Ethereum.js b/src/api/Ethereum.js index d88ce0cd..9d6e260d 100644 --- a/src/api/Ethereum.js +++ b/src/api/Ethereum.js @@ -17,7 +17,7 @@ export type Tx = { to: string, input: string, index: number, - block: { + block?: { hash: string, height: number, time: string, diff --git a/src/api/Ripple.js b/src/api/Ripple.js new file mode 100644 index 00000000..8b9686fc --- /dev/null +++ b/src/api/Ripple.js @@ -0,0 +1,48 @@ +// @flow +import { RippleAPI } from 'ripple-lib' +import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' +import { + parseCurrencyUnit, + getCryptoCurrencyById, + formatCurrencyUnit, +} from '@ledgerhq/live-common/lib/helpers/currencies' + +const rippleUnit = getCryptoCurrencyById('ripple').units[0] + +const apiEndpoint = { + ripple: 'wss://s1.ripple.com', +} + +export const apiForCurrency = (currency: CryptoCurrency) => { + const api = new RippleAPI({ + server: apiEndpoint[currency.id], + }) + api.on('error', (errorCode, errorMessage) => { + console.warn(`Ripple API error: ${errorCode}: ${errorMessage}`) + }) + return api +} + +export const parseAPIValue = (value: string) => parseCurrencyUnit(rippleUnit, value) + +export const parseAPICurrencyObject = ({ + currency, + value, +}: { + currency: string, + value: string, +}) => { + if (currency !== 'XRP') { + console.warn(`RippleJS: attempt to parse unknown currency ${currency}`) + return 0 + } + return parseAPIValue(value) +} + +export const formatAPICurrencyXRP = (amount: number) => { + const value = formatCurrencyUnit(rippleUnit, amount, { + showAllDigits: true, + disableRounding: true, + }) + return { currency: 'XRP', value } +} diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index 68c16971..f902cf0f 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -1,6 +1,7 @@ // @flow import React from 'react' import EthereumKind from 'components/FeesField/EthereumKind' +import throttle from 'lodash/throttle' import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import { apiForCurrency } from 'api/Ethereum' import type { Tx } from 'api/Ethereum' @@ -30,8 +31,8 @@ const toAccountOperation = (account: Account) => (tx: Tx): Operation => { hash: tx.hash, address: sending ? tx.to : tx.from, amount: (sending ? -1 : 1) * tx.value, - blockHeight: tx.block && tx.block.height, - blockHash: tx.block && tx.block.hash, + blockHeight: (tx.block && tx.block.height) || 0, // FIXME will be optional field + blockHash: (tx.block && tx.block.hash) || '', // FIXME will be optional field accountId: account.id, senders: [tx.from], recipients: [tx.to], @@ -62,6 +63,21 @@ const paginateMoreTransactions = async ( return mergeOps(acc, txs.map(toAccountOperation(account))) } +const fetchCurrentBlock = (perCurrencyId => currency => { + if (perCurrencyId[currency.id]) return perCurrencyId[currency.id] + const api = apiForCurrency(currency) + const f = throttle( + () => + api.getCurrentBlock().catch(e => { + f.cancel() + throw e + }), + 5000, + ) + perCurrencyId[currency.id] = f + return f +})({}) + const EthereumBridge: WalletBridge = { scanAccountsOnDevice(currency, deviceId, { next, complete, error }) { let finished = false @@ -73,15 +89,7 @@ const EthereumBridge: WalletBridge = { // in future ideally what we want is: // return mergeMap(addressesObservable, address => fetchAccount(address)) - let balanceZerosCount = 0 - - let currentBlockPromise - function lazyCurrentBlock() { - if (!currentBlockPromise) { - currentBlockPromise = api.getCurrentBlock() - } - return currentBlockPromise - } + let newAccountCount = 0 async function stepAddress( index, @@ -89,12 +97,18 @@ const EthereumBridge: WalletBridge = { isStandard, ): { account?: Account, complete?: boolean } { const balance = await api.getAccountBalance(address) - if (finished) return {} - if (balance === 0) { + if (finished) return { complete: true } + const currentBlock = await fetchCurrentBlock(currency) + if (finished) return { complete: true } + const { txs } = await api.getTransactions(address) + if (finished) return { complete: true } + + if (txs.length === 0) { + // this is an empty account if (isStandard) { - if (balanceZerosCount === 0) { + if (newAccountCount === 0) { // first zero account will emit one account as opportunity to create a new account.. - const currentBlock = await lazyCurrentBlock() + const currentBlock = await fetchCurrentBlock(currency) const accountId = `${currency.id}_${address}` const account: Account = { id: accountId, @@ -104,7 +118,7 @@ const EthereumBridge: WalletBridge = { name: 'New Account', isSegwit: false, address, - addresses: [{ str: address, path, }], + addresses: [{ str: address, path }], balance, blockHeight: currentBlock.height, archived: true, @@ -116,26 +130,22 @@ const EthereumBridge: WalletBridge = { } return { account, complete: true } } - balanceZerosCount++ + newAccountCount++ } - // NB for legacy addresses we might not want to stop at first zero but continue forever + // NB for legacy addresses maybe we will continue at least for the first 10 addresses return { complete: true } } - const currentBlock = await lazyCurrentBlock() - if (finished) return {} - const { txs } = await api.getTransactions(address) - if (finished) return {} const accountId = `${currency.id}_${address}` const account: Account = { id: accountId, xpub: '', - path, // FIXME we probably not want the address path in the account.path + path, // FIXME we probably not want the address path in the account.path walletPath: String(index), name: address.slice(32), isSegwit: false, address, - addresses: [{ str: address, path, }], + addresses: [{ str: address, path }], balance, blockHeight: currentBlock.height, archived: true, @@ -157,12 +167,10 @@ const EthereumBridge: WalletBridge = { const isStandard = last === derivation for (let index = 0; index < 255; index++) { const path = derivation({ currency, x: index, segwit: false }) - console.log(path) const res = await getAddressCommand .send({ currencyId: currency.id, devicePath: deviceId, path }) .toPromise() const r = await stepAddress(index, res, isStandard) - console.log('=>', r.account) if (r.account) next(r.account) if (r.complete) { break @@ -185,7 +193,7 @@ const EthereumBridge: WalletBridge = { const api = apiForCurrency(currency) async function main() { try { - const block = await api.getCurrentBlock() + const block = await fetchCurrentBlock(currency) if (unsubscribed) return if (block.height === blockHeight) { complete() @@ -266,7 +274,7 @@ const EthereumBridge: WalletBridge = { getMaxAmount: (a, t) => Promise.resolve(a.balance - t.gasPrice), - signAndBroadcast: async ({ account: a, transaction: t, deviceId }) => { + signAndBroadcast: async (a, t, deviceId) => { const api = apiForCurrency(a.currency) const nonce = await api.getAccountNonce(a.address) diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index 9b9776cd..b6eb3de3 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -154,7 +154,7 @@ const LibcoreBridge: WalletBridge = { getMaxAmount: (a, t) => Promise.resolve(a.balance - t.feePerByte), - signAndBroadcast: ({ account, transaction, deviceId }) => { + signAndBroadcast: (account, transaction, deviceId) => { const rawAccount = encodeAccount(account) return runJob({ channel: 'accounts', diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js new file mode 100644 index 00000000..d238aff7 --- /dev/null +++ b/src/bridge/RippleJSBridge.js @@ -0,0 +1,402 @@ +// @flow +import React from 'react' +import bs58check from 'ripple-bs58check' +import { computeBinaryTransactionHash } from 'ripple-hashes' +import type { Account, Operation } from '@ledgerhq/live-common/lib/types' +import { getDerivations } from 'helpers/derivations' +import getAddress from 'commands/getAddress' +import signTransaction from 'commands/signTransaction' +import { + apiForCurrency, + parseAPIValue, + parseAPICurrencyObject, + formatAPICurrencyXRP, +} from 'api/Ripple' +import FeesRippleKind from 'components/FeesField/RippleKind' +import AdvancedOptionsRippleKind from 'components/AdvancedOptions/RippleKind' +import type { WalletBridge, EditProps } from './types' + +type Transaction = { + amount: number, + recipient: string, + fee: number, + tag: ?number, +} + +const EditFees = ({ account, onChange, value }: EditProps) => ( + { + onChange({ ...value, fee }) + }} + fee={value.fee} + account={account} + /> +) + +const EditAdvancedOptions = ({ onChange, value }: EditProps) => ( + { + onChange({ ...value, tag }) + }} + /> +) + +function isRecipientValid(currency, recipient) { + try { + bs58check.decode(recipient) + return true + } catch (e) { + return false + } +} + +function mergeOps(existing: Operation[], newFetched: Operation[]) { + const ids = existing.map(o => o.id) + const all = existing.concat(newFetched.filter(o => !ids.includes(o.id))) + return all.sort((a, b) => a.date - b.date) +} + +type Tx = { + type: string, + address: string, + sequence: number, + id: string, + specification: { + source: { + address: string, + maxAmount: { + currency: string, + value: string, + }, + }, + destination: { + address: string, + amount: { + currency: string, + value: string, + }, + }, + paths: string, + }, + outcome: { + result: string, + fee: string, + timestamp: string, + deliveredAmount?: { + currency: string, + value: string, + counterparty: string, + }, + balanceChanges: { + [addr: string]: Array<{ + counterparty: string, + currency: string, + value: string, + }>, + }, + orderbookChanges: { + [addr: string]: Array<{ + direction: string, + quantity: { + currency: string, + value: string, + }, + totalPrice: { + currency: string, + counterparty: string, + value: string, + }, + makeExchangeRate: string, + sequence: number, + status: string, + }>, + }, + ledgerVersion: number, + indexInLedger: number, + }, +} + +const txToOperation = (account: Account) => ({ + id, + outcome: { deliveredAmount, ledgerVersion, timestamp }, + specification: { source, destination }, +}: Tx): Operation => { + const sending = source.address === account.address + const amount = + (sending ? -1 : 1) * (deliveredAmount ? parseAPICurrencyObject(deliveredAmount) : 0) + const op: Operation = { + id, + hash: id, + accountId: account.id, + blockHash: '', + address: sending ? destination.address : source.address, + amount, + blockHeight: ledgerVersion, + senders: [sending ? destination.address : source.address], + recipients: [!sending ? destination.address : source.address], + date: new Date(timestamp), + } + return op +} + +const RippleJSBridge: WalletBridge = { + scanAccountsOnDevice(currency, deviceId, { next, complete, error }) { + let finished = false + const unsubscribe = () => { + finished = true + } + + async function main() { + const api = apiForCurrency(currency) + try { + await api.connect() + const serverInfo = await api.getServerInfo() + const ledgers = serverInfo.completeLedgers.split('-') + const minLedgerVersion = Number(ledgers[0]) + const maxLedgerVersion = Number(ledgers[1]) + + const derivations = getDerivations(currency) + for (const derivation of derivations) { + for (let index = 0; index < 255; index++) { + const path = derivation({ currency, x: index, segwit: false }) + const { address } = await await getAddress + .send({ currencyId: currency.id, devicePath: deviceId, path }) + .toPromise() + if (finished) return + + const accountId = `${currency.id}_${address}` + + let info + try { + info = await api.getAccountInfo(address) + } catch (e) { + if (e.message !== 'actNotFound') { + throw e + } + } + + if (!info) { + // account does not exist in Ripple server + // we are generating a new account locally + next({ + id: accountId, + xpub: '', + path, + walletPath: '', + name: 'New Account', + isSegwit: false, + address, + addresses: [address], + balance: 0, + blockHeight: maxLedgerVersion, + index, + currency, + operations: [], + unit: currency.units[0], + archived: true, + lastSyncDate: new Date(), + }) + break + } + + if (finished) return + const balance = parseAPIValue(info.xrpBalance) + if (isNaN(balance) || !isFinite(balance)) { + throw new Error(`Ripple: invalid balance=${balance} for address ${address}`) + } + + const transactions = await api.getTransactions(address, { + minLedgerVersion, + maxLedgerVersion, + }) + if (finished) return + + const account: Account = { + id: accountId, + xpub: '', + path, + walletPath: '', + name: address.slice(0, 8), + isSegwit: false, + address, + addresses: [address], + balance, + blockHeight: maxLedgerVersion, + index, + currency, + operations: [], + unit: currency.units[0], + archived: true, + lastSyncDate: new Date(), + } + account.operations = transactions.map(txToOperation(account)) + next(account) + } + } + complete() + } catch (e) { + error(e) + } finally { + api.disconnect() + } + } + + main() + + return { unsubscribe } + }, + + synchronize({ currency, address, blockHeight }, { next, error, complete }) { + let finished = false + const unsubscribe = () => { + finished = true + } + + async function main() { + const api = apiForCurrency(currency) + try { + await api.connect() + if (finished) return + const serverInfo = await api.getServerInfo() + if (finished) return + const ledgers = serverInfo.completeLedgers.split('-') + const minLedgerVersion = Number(ledgers[0]) + const maxLedgerVersion = Number(ledgers[1]) + + let info + try { + info = await api.getAccountInfo(address) + } catch (e) { + if (e.message !== 'actNotFound') { + throw e + } + } + if (finished) return + + if (!info) { + // account does not exist, we have nothing to sync + complete() + return + } + + const balance = parseAPIValue(info.xrpBalance) + if (isNaN(balance) || !isFinite(balance)) { + throw new Error(`Ripple: invalid balance=${balance} for address ${address}`) + } + + next(a => ({ ...a, balance })) + + const transactions = await api.getTransactions(address, { + minLedgerVersion: Math.max(blockHeight, minLedgerVersion), + maxLedgerVersion, + }) + + if (finished) return + + next(a => { + const newOps = transactions.map(txToOperation(a)) + const operations = mergeOps(a.operations, newOps) + return { + ...a, + operations, + blockHeight: maxLedgerVersion, + lastSyncDate: new Date(), + } + }) + + complete() + } catch (e) { + error(e) + } finally { + api.disconnect() + } + } + + main() + + return { unsubscribe } + }, + + pullMoreOperations: () => Promise.resolve(a => a), // FIXME not implemented + + isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(currency, recipient)), + + createTransaction: () => ({ + amount: 0, + recipient: '', + fee: 0, + tag: undefined, + }), + + editTransactionAmount: (account, t, amount) => ({ + ...t, + amount, + }), + + getTransactionAmount: (a, t) => t.amount, + + editTransactionRecipient: (account, t, recipient) => ({ + ...t, + recipient, + }), + + // $FlowFixMe + EditFees, + + // $FlowFixMe + EditAdvancedOptions, + + getTransactionRecipient: (a, t) => t.recipient, + + isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, + + getTotalSpent: (a, t) => Promise.resolve(t.amount + t.fee), + + getMaxAmount: (a, t) => Promise.resolve(a.balance - t.fee), + + signAndBroadcast: async (a, t, deviceId) => { + const api = apiForCurrency(a.currency) + try { + await api.connect() + const amount = formatAPICurrencyXRP(t.amount) + const payment = { + source: { + address: a.address, + amount, + }, + destination: { + address: t.recipient, + minAmount: amount, + tag: t.tag, + }, + } + const instruction = { + fee: formatAPICurrencyXRP(t.fee).value, + } + + const prepared = await api.preparePayment(a.address, payment, instruction) + + const transaction = await signTransaction + .send({ + currencyId: a.currency.id, + devicePath: deviceId, + path: a.path, + transaction: JSON.parse(prepared.txJSON), + }) + .toPromise() + + const submittedPayment = await api.submit(transaction) + + if (submittedPayment.resultCode !== 'tesSUCCESS') { + throw new Error(submittedPayment.resultMessage) + } + + return computeBinaryTransactionHash(transaction) + } finally { + api.disconnect() + } + }, +} + +export default RippleJSBridge diff --git a/src/bridge/index.js b/src/bridge/index.js index ce777268..3ea7fe37 100644 --- a/src/bridge/index.js +++ b/src/bridge/index.js @@ -1,11 +1,9 @@ // @flow import type { Currency } from '@ledgerhq/live-common/lib/types' import { WalletBridge } from './types' -import UnsupportedBridge from './UnsupportedBridge' import LibcoreBridge from './LibcoreBridge' import EthereumJSBridge from './EthereumJSBridge' - -const RippleJSBridge = UnsupportedBridge +import RippleJSBridge from './RippleJSBridge' export const getBridgeForCurrency = (currency: Currency): WalletBridge => { if (currency.id.indexOf('ethereum') === 0) { diff --git a/src/bridge/makeMockBridge.js b/src/bridge/makeMockBridge.js index 21032f3d..1bc2eef9 100644 --- a/src/bridge/makeMockBridge.js +++ b/src/bridge/makeMockBridge.js @@ -146,7 +146,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> { getMaxAmount, - signAndBroadcast: async ({ account, transaction: t }) => { + signAndBroadcast: async (account, t) => { const rng = new Prando() const op = genOperation(account, account.operations, account.currency, rng) op.amount = -t.amount diff --git a/src/bridge/types.js b/src/bridge/types.js index d85d3fe0..716ba35d 100644 --- a/src/bridge/types.js +++ b/src/bridge/types.js @@ -98,9 +98,5 @@ export interface WalletBridge { * NOTE: in future, when transaction balance is close to account.balance, we could wipe it all at this level... * to implement that, we might want to have special logic `account.balance-transaction.amount < dust` but not sure where this should leave (i would say on UI side because we need to inform user visually). */ - signAndBroadcast({ - account: Account, - transaction: Transaction, - deviceId: DeviceId, - }): Promise; + signAndBroadcast(account: Account, transaction: Transaction, deviceId: DeviceId): Promise; } diff --git a/src/components/AdvancedOptions/RippleKind.js b/src/components/AdvancedOptions/RippleKind.js new file mode 100644 index 00000000..0e4f5499 --- /dev/null +++ b/src/components/AdvancedOptions/RippleKind.js @@ -0,0 +1,36 @@ +// @flow +import React from 'react' +import { translate } from 'react-i18next' + +import Box from 'components/base/Box' +import Input from 'components/base/Input' +import Label from 'components/base/Label' +import Spoiler from 'components/base/Spoiler' + +type Props = { + tag: ?number, + onChangeTag: (?number) => void, + t: *, +} + +export default translate()(({ tag, onChangeTag, t }: Props) => ( + + + + + + + { + const tag = parseInt(str, 10) + if (!isNaN(tag) && isFinite(tag)) onChangeTag(tag) + else onChangeTag(undefined) + }} + /> + + + +)) diff --git a/src/components/DeviceSignTransaction.js b/src/components/DeviceSignTransaction.js index ff9c3865..31801aac 100644 --- a/src/components/DeviceSignTransaction.js +++ b/src/components/DeviceSignTransaction.js @@ -34,9 +34,10 @@ class DeviceSignTransaction extends PureComponent { sign = async () => { const { device, account, transaction, bridge, onSuccess } = this.props try { - const txid = await bridge.signAndBroadcast({ account, transaction, deviceId: device.path }) + const txid = await bridge.signAndBroadcast(account, transaction, device.path) onSuccess(txid) } catch (error) { + console.warn(error) this.setState({ error }) } } diff --git a/src/components/FeesField/GenericContainer.js b/src/components/FeesField/GenericContainer.js index ccd8e405..f0e76a8a 100644 --- a/src/components/FeesField/GenericContainer.js +++ b/src/components/FeesField/GenericContainer.js @@ -12,7 +12,7 @@ export default translate()( {children} diff --git a/src/components/FeesField/RippleKind.js b/src/components/FeesField/RippleKind.js new file mode 100644 index 00000000..2677ea45 --- /dev/null +++ b/src/components/FeesField/RippleKind.js @@ -0,0 +1,59 @@ +// @flow + +import React, { Component } from 'react' +import type { Account } from '@ledgerhq/live-common/lib/types' +import { apiForCurrency, parseAPIValue } from 'api/Ripple' +import InputCurrency from 'components/base/InputCurrency' +import GenericContainer from './GenericContainer' + +type Props = { + account: Account, + fee: number, + onChange: number => void, +} + +type State = { + error: ?Error, +} + +class FeesField extends Component { + state = { + error: null, + } + componentDidMount() { + this.sync() + } + async sync() { + const api = apiForCurrency(this.props.account.currency) + try { + await api.connect() + const info = await api.getServerInfo() + const serverFee = parseAPIValue(info.validatedLedger.baseFeeXRP) + if (!this.props.fee) { + this.props.onChange(serverFee) + } + } catch (error) { + this.setState({ error }) + } finally { + api.disconnect() + } + } + render() { + const { account, fee, onChange } = this.props + const { error } = this.state + const { units } = account.currency + return ( + + + + ) + } +} + +export default FeesField diff --git a/src/components/modals/Send/03-step-verification.js b/src/components/modals/Send/03-step-verification.js index 850ad404..12d8f9e8 100644 --- a/src/components/modals/Send/03-step-verification.js +++ b/src/components/modals/Send/03-step-verification.js @@ -52,7 +52,10 @@ export default ({ account, device, bridge, transaction, onValidate, t }: Props) transaction={transaction} bridge={bridge} onSuccess={onValidate} - render={({ error }) => } + render={({ error }) => ( + // FIXME we really really REALLY should use error for the display. otherwise we are completely blind on error cases.. + + )} /> )} diff --git a/src/helpers/getAddressForCurrency/index.js b/src/helpers/getAddressForCurrency/index.js index b96813e3..3a859b48 100644 --- a/src/helpers/getAddressForCurrency/index.js +++ b/src/helpers/getAddressForCurrency/index.js @@ -3,6 +3,7 @@ import type Transport from '@ledgerhq/hw-transport' import btc from './btc' import ethereum from './ethereum' +import ripple from './ripple' type Resolver = ( transport: Transport<*>, @@ -24,6 +25,8 @@ const all = { ethereum_testnet: ethereum, ethereum_classic: ethereum, ethereum_classic_testnet: ethereum, + + ripple, } const getAddressForCurrency: Module = (currencyId: string) => diff --git a/src/helpers/getAddressForCurrency/ripple.js b/src/helpers/getAddressForCurrency/ripple.js new file mode 100644 index 00000000..61398a58 --- /dev/null +++ b/src/helpers/getAddressForCurrency/ripple.js @@ -0,0 +1,15 @@ +// @flow + +import Xrp from '@ledgerhq/hw-app-xrp' +import type Transport from '@ledgerhq/hw-transport' + +export default async ( + transport: Transport<*>, + currencyId: string, + path: string, + { verify = false }: { verify: boolean }, +) => { + const xrp = new Xrp(transport) + const { address, publicKey } = await xrp.getAddress(path, verify) + return { path, address, publicKey } +} diff --git a/src/helpers/signTransactionForCurrency/index.js b/src/helpers/signTransactionForCurrency/index.js index c40d534f..1ea3d0fc 100644 --- a/src/helpers/signTransactionForCurrency/index.js +++ b/src/helpers/signTransactionForCurrency/index.js @@ -1,7 +1,9 @@ // @flow import type Transport from '@ledgerhq/hw-transport' + import ethereum from './ethereum' +import ripple from './ripple' type Resolver = ( transport: Transport<*>, @@ -20,6 +22,8 @@ const all = { ethereum_testnet: ethereum, ethereum_classic: ethereum, ethereum_classic_testnet: ethereum, + + ripple, } const m: Module = (currencyId: string) => all[currencyId] || fallback(currencyId) diff --git a/src/helpers/signTransactionForCurrency/ripple.js b/src/helpers/signTransactionForCurrency/ripple.js new file mode 100644 index 00000000..d4d1845a --- /dev/null +++ b/src/helpers/signTransactionForCurrency/ripple.js @@ -0,0 +1,14 @@ +// @flow +import Xrp from '@ledgerhq/hw-app-xrp' +import type Transport from '@ledgerhq/hw-transport' +import BinaryCodec from 'ripple-binary-codec' + +export default async (transport: Transport<*>, currencyId: string, path: string, tx: Object) => { + tx = { ...tx } + const xrp = new Xrp(transport) + const { publicKey } = await xrp.getAddress(path) + tx.SigningPubKey = publicKey.toUpperCase() + const rawTxHex = BinaryCodec.encode(tx).toUpperCase() + tx.TxnSignature = (await xrp.signTransaction(path, rawTxHex)).toUpperCase() + return BinaryCodec.encode(tx).toUpperCase() +} diff --git a/static/i18n/en/send.yml b/static/i18n/en/send.yml index 4d2ee7c8..825618a7 100644 --- a/static/i18n/en/send.yml +++ b/static/i18n/en/send.yml @@ -11,6 +11,7 @@ steps: advancedOptions: Advanced options useRBF: Use the RBF transaction message: Leave a message (140) + rippleTag: Tag connectDevice: title: Connect device verification: @@ -23,7 +24,7 @@ steps: title: Confirmation success: title: Transaction successfully completed - text: You may have to wait few confirmations unitl the transaction appear + text: You may have to wait few confirmations unitl the transaction appear cta: View operation details error: title: Transaction aborted diff --git a/yarn.lock b/yarn.lock index cb97a175..07b88e4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1447,6 +1447,13 @@ dependencies: "@ledgerhq/hw-transport" "^4.12.0" +"@ledgerhq/hw-app-xrp@^4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-xrp/-/hw-app-xrp-4.12.0.tgz#aeba3b5b2447e185811974312a3ed1b3eeb7a0f1" + dependencies: + "@ledgerhq/hw-transport" "^4.12.0" + bip32-path "0.4.2" + "@ledgerhq/hw-transport-node-hid@^4.12.0", "@ledgerhq/hw-transport-node-hid@^4.7.6": version "4.12.0" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-4.12.0.tgz#acd913904d0e600c681375c7bf5e0f9d5165a075" @@ -1729,6 +1736,14 @@ react-split-pane "^0.1.77" react-treebeard "^2.1.0" +"@types/lodash@^4.14.85": + version "4.14.109" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.109.tgz#b1c4442239730bf35cabaf493c772b18c045886d" + +"@types/node@*": + version "10.1.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.1.2.tgz#1b928a0baa408fc8ae3ac012cc81375addc147c6" + "@types/node@^8.0.24": version "8.10.17" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.17.tgz#d48cf10f0dc6dcf59f827f5a3fc7a4a6004318d3" @@ -1737,6 +1752,12 @@ version "1.13.6" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.6.tgz#128d1685a7c34d31ed17010fc87d6a12c1de6976" +"@types/ws@^3.2.0": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-3.2.1.tgz#b0c1579e58e686f83ce0a97bb9463d29705827fb" + dependencies: + "@types/node" "*" + "@webassemblyjs/ast@1.4.3": version "1.4.3" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.4.3.tgz#3b3f6fced944d8660273347533e6d4d315b5934a" @@ -3279,13 +3300,19 @@ babel-register@^6.26.0, babel-register@^6.9.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.5.0, babel-runtime@^6.9.2: +babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.5.0, babel-runtime@^6.6.1, babel-runtime@^6.9.2: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: core-js "^2.4.0" regenerator-runtime "^0.11.0" +babel-runtime@^5.8.20: + version "5.8.38" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-5.8.38.tgz#1c0b02eb63312f5f087ff20450827b425c9d4c19" + dependencies: + core-js "^1.0.0" + babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" @@ -3347,6 +3374,10 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base-x@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-1.1.0.tgz#42d3d717474f9ea02207f6d1aa1f426913eeb7ac" + base-x@^3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.4.tgz#94c1788736da065edb1d68808869e357c977fa77" @@ -3411,6 +3442,10 @@ bigi@^1.1.0, bigi@^1.4.0: version "1.4.2" resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825" +bignumber.js@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.1.0.tgz#db6f14067c140bd46624815a7916c92d9b6c24b1" + bin-links@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-1.1.2.tgz#fb74bd54bae6b7befc6c6221f25322ac830d9757" @@ -3440,6 +3475,10 @@ bindings@^1.2.1, bindings@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" +bip32-path@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/bip32-path/-/bip32-path-0.4.2.tgz#5db0416ad6822712f077836e2557b8697c0c7c99" + bip66@^1.1.0, bip66@^1.1.3: version "1.1.5" resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" @@ -3497,6 +3536,10 @@ bluebird@~3.4.1: version "3.4.7" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" +bn.js@^3.1.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-3.3.0.tgz#1138e577889fdc97bbdab51844f2190dfc0ae3d7" + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.4.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -3581,7 +3624,7 @@ brcast@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/brcast/-/brcast-3.0.1.tgz#6256a8349b20de9eed44257a9b24d71493cd48dd" -brorand@^1.0.1: +brorand@^1.0.1, brorand@^1.0.5: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -5080,6 +5123,10 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +decimal.js@^5.0.8: + version "5.0.8" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-5.0.8.tgz#b48c3fb7d73a2d4d4940e0b38f1cd21db5b367ce" + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -5689,6 +5736,15 @@ elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" +elliptic@^5.1.0: + version "5.2.1" + resolved "http://registry.npmjs.org/elliptic/-/elliptic-5.2.1.tgz#fa294b6563c6ddbc9ba3dc8594687ae840858f10" + dependencies: + bn.js "^3.1.1" + brorand "^1.0.1" + hash.js "^1.0.0" + inherits "^2.0.1" + elliptic@^6.0.0, elliptic@^6.2.3: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" @@ -7405,7 +7461,7 @@ https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" -https-proxy-agent@^2.1.0, https-proxy-agent@^2.2.0: +https-proxy-agent@2.2.1, https-proxy-agent@^2.1.0, https-proxy-agent@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" dependencies: @@ -8541,6 +8597,10 @@ jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" +jsonschema@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.2.tgz#83ab9c63d65bf4d596f91d81195e78772f6452bc" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -8907,7 +8967,7 @@ lodash@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.1, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1: +lodash@^4.0.1, lodash@^4.12.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -12007,6 +12067,82 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +ripple-address-codec@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripple-address-codec/-/ripple-address-codec-2.0.1.tgz#eddbe3a7960d2e02c5c1c74fb9a9fa0d2dfb6571" + dependencies: + hash.js "^1.0.3" + x-address-codec "^0.7.0" + +ripple-binary-codec@^0.1.0, ripple-binary-codec@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-0.1.13.tgz#c68951405a17a71695551e789966ff376da552e4" + dependencies: + babel-runtime "^6.6.1" + bn.js "^4.11.3" + create-hash "^1.1.2" + decimal.js "^5.0.8" + inherits "^2.0.1" + lodash "^4.12.0" + ripple-address-codec "^2.0.1" + +ripple-bs58@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/ripple-bs58/-/ripple-bs58-4.0.1.tgz#b94d7acdc07cfd66906477cb4df39f07583f86a3" + dependencies: + base-x "^3.0.2" + +ripple-bs58check@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripple-bs58check/-/ripple-bs58check-2.0.2.tgz#f270dbcd81630b26a21901c3ce27b7d62a4e9c91" + dependencies: + create-hash "^1.1.0" + ripple-bs58 "^4.0.0" + +ripple-hashes@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/ripple-hashes/-/ripple-hashes-0.3.1.tgz#f2f46f1ff05e6487500a99839019114cd2482411" + dependencies: + bignumber.js "^4.1.0" + create-hash "^1.1.2" + ripple-address-codec "^2.0.1" + ripple-binary-codec "^0.1.0" + +ripple-keypairs@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/ripple-keypairs/-/ripple-keypairs-0.10.1.tgz#ef796b519bb202682515e7cdd6063762636943e6" + dependencies: + babel-runtime "^5.8.20" + bn.js "^3.1.1" + brorand "^1.0.5" + elliptic "^5.1.0" + hash.js "^1.0.3" + ripple-address-codec "^2.0.1" + +ripple-lib-transactionparser@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/ripple-lib-transactionparser/-/ripple-lib-transactionparser-0.6.2.tgz#eb117834816cab3398445a74ec3cacec95b6b5fa" + dependencies: + bignumber.js "^4.1.0" + lodash "^4.17.4" + +ripple-lib@^1.0.0-beta.0: + version "1.0.0-beta.0" + resolved "https://registry.yarnpkg.com/ripple-lib/-/ripple-lib-1.0.0-beta.0.tgz#18d6284f9248044d04c39a17f02eb234a3e8a70f" + dependencies: + "@types/lodash" "^4.14.85" + "@types/ws" "^3.2.0" + bignumber.js "^4.1.0" + https-proxy-agent "2.2.1" + jsonschema "1.2.2" + lodash "^4.17.4" + ripple-address-codec "^2.0.1" + ripple-binary-codec "^0.1.13" + ripple-hashes "^0.3.1" + ripple-keypairs "^0.10.1" + ripple-lib-transactionparser "^0.6.2" + ws "^3.3.1" + rlp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.0.0.tgz#9db384ff4b89a8f61563d92395d8625b18f3afb0" @@ -13284,6 +13420,10 @@ uid-number@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" +ultron@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" + umask@^1.1.0, umask@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" @@ -14043,6 +14183,14 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" +ws@^3.3.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" + dependencies: + async-limiter "~1.0.0" + safe-buffer "~5.1.0" + ultron "~1.1.0" + ws@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ws/-/ws-4.1.0.tgz#a979b5d7d4da68bf54efe0408967c324869a7289" @@ -14056,6 +14204,12 @@ ws@^5.1.1: dependencies: async-limiter "~1.0.0" +x-address-codec@^0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/x-address-codec/-/x-address-codec-0.7.2.tgz#2a2f7bb00278520bd13733a7959a05443d6802e0" + dependencies: + base-x "^1.0.1" + xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"