Browse Source

Merge pull request #1247 from gre/hotfixes/1.0.4

prepare for 1.0.4
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
56fe4a252d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      package.json
  2. 1
      src/api/Ripple.js
  3. 6
      src/bridge/BridgeSyncContext.js
  4. 21
      src/bridge/EthereumJSBridge.js
  5. 88
      src/bridge/LibcoreBridge.js
  6. 2
      src/bridge/RippleJSBridge.js
  7. 2
      src/components/Onboarding/OnboardingBreadcrumb.js
  8. 4
      src/components/base/Chart/handleMouseEvents.js
  9. 8
      src/components/modals/Receive/steps/03-step-confirm-address.js
  10. 6
      src/components/modals/Receive/steps/04-step-receive-funds.js
  11. 12
      src/components/modals/Send/index.js
  12. 3
      src/config/constants.js
  13. 8
      src/config/errors.js
  14. 20
      src/helpers/anonymizer.js
  15. 18
      src/helpers/derivations.js
  16. 4
      src/helpers/deviceAccess.js
  17. 3
      src/helpers/devices/getDeviceInfo.js
  18. 2
      src/logger/logger-storybook.js
  19. 8
      src/logger/logger.js
  20. 1
      src/styles/reset.js
  21. 12
      yarn.lock

4
package.json

@ -40,8 +40,8 @@
"@ledgerhq/hw-app-xrp": "^4.13.0",
"@ledgerhq/hw-transport": "^4.13.0",
"@ledgerhq/hw-transport-node-hid": "^4.13.0",
"@ledgerhq/ledger-core": "2.0.0-rc.4",
"@ledgerhq/live-common": "^2.32.0",
"@ledgerhq/ledger-core": "2.0.0-rc.5",
"@ledgerhq/live-common": "^2.35.0",
"animated": "^0.2.2",
"async": "^2.6.1",
"axios": "^0.18.0",

1
src/api/Ripple.js

@ -40,6 +40,7 @@ export const formatAPICurrencyXRP = (amount: number) => {
const value = formatCurrencyUnit(rippleUnit, amount, {
showAllDigits: true,
disableRounding: true,
useGrouping: false,
})
return { currency: 'XRP', value }
}

6
src/bridge/BridgeSyncContext.js

@ -3,7 +3,6 @@
// it handles automatically re-calling synchronize
// this is an even high abstraction than the bridge
import invariant from 'invariant'
import logger from 'logger'
import shuffle from 'lodash/shuffle'
import { timeout } from 'rxjs/operators/timeout'
@ -67,7 +66,10 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
return
}
const account = this.props.accounts.find(a => a.id === accountId)
invariant(account, 'account not found')
if (!account) {
next()
return
}
const bridge = getBridgeForCurrency(account.currency)

21
src/bridge/EthereumJSBridge.js

@ -1,5 +1,6 @@
// @flow
import { Observable } from 'rxjs'
import logger from 'logger'
import React from 'react'
import FeesField from 'components/FeesField/EthereumKind'
import AdvancedOptions from 'components/AdvancedOptions/EthereumKind'
@ -176,7 +177,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
index,
{ address, path: freshAddressPath, publicKey },
isStandard,
mandatoryCount,
shouldSkipEmpty,
): { account?: Account, complete?: boolean } {
const balance = await api.getAccountBalance(address)
if (finished) return { complete: true }
@ -213,7 +214,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
newAccountCount++
}
if (index < mandatoryCount) {
if (shouldSkipEmpty) {
return {}
}
// NB for legacy addresses maybe we will continue at least for the first 10 addresses
@ -254,6 +255,8 @@ const EthereumBridge: WalletBridge<Transaction> = {
const last = derivations[derivations.length - 1]
for (const derivation of derivations) {
const isStandard = last === derivation
let emptyCount = 0
const mandatoryEmptyAccountSkip = derivation.mandatoryEmptyAccountSkip || 0
for (let index = 0; index < 255; index++) {
const freshAddressPath = derivation({ currency, x: index, segwit: false })
const res = await getAddressCommand
@ -263,10 +266,18 @@ const EthereumBridge: WalletBridge<Transaction> = {
index,
res,
isStandard,
// $FlowFixMe i know i know, not part of function
derivation.mandatoryCount || 0,
emptyCount < mandatoryEmptyAccountSkip,
)
if (r.account) o.next(r.account)
logger.log(
`scanning ${currency.id} at ${freshAddressPath}: ${res.address} resulted of ${
r.account ? `Account with ${r.account.operations.length} txs` : 'no account'
}. ${r.complete ? 'ALL SCANNED' : ''}`,
)
if (r.account) {
o.next(r.account)
} else {
emptyCount++
}
if (r.complete) {
break
}

88
src/bridge/LibcoreBridge.js

@ -1,8 +1,7 @@
// @flow
import React from 'react'
import { Observable } from 'rxjs'
import LRU from 'lru-cache'
import { map } from 'rxjs/operators'
import LRU from 'lru-cache'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { decodeAccount, encodeAccount } from 'reducers/accounts'
import FeesBitcoinKind from 'components/FeesField/BitcoinKind'
@ -110,58 +109,41 @@ const LibcoreBridge: WalletBridge<Transaction> = {
},
synchronize: account =>
Observable.create(o => {
// FIXME TODO:
// - when you implement addPendingOperation you also here need to:
// - if there were pendingOperations that are now in operations, remove them as well.
// - if there are pendingOperations that is older than a threshold (that depends on blockchain speed typically)
// then we probably should trash them out? it's a complex question for UI
;(async () => {
try {
const rawAccount = encodeAccount(account)
const rawSyncedAccount = await libcoreSyncAccount.send({ rawAccount }).toPromise()
const syncedAccount = decodeAccount(rawSyncedAccount)
o.next(account => {
const accountOps = account.operations
const syncedOps = syncedAccount.operations
const patch: $Shape<Account> = {
id: syncedAccount.id,
freshAddress: syncedAccount.freshAddress,
freshAddressPath: syncedAccount.freshAddressPath,
balance: syncedAccount.balance,
blockHeight: syncedAccount.blockHeight,
lastSyncDate: new Date(),
}
const hasChanged =
accountOps.length !== syncedOps.length || // size change, we do a full refresh for now...
(accountOps.length > 0 &&
syncedOps.length > 0 &&
(accountOps[0].accountId !== syncedOps[0].accountId ||
accountOps[0].id !== syncedOps[0].id || // if same size, only check if the last item has changed.
accountOps[0].blockHeight !== syncedOps[0].blockHeight))
if (hasChanged) {
patch.operations = syncedAccount.operations
patch.pendingOperations = [] // For now, we assume a change will clean the pendings.
}
return {
...account,
...patch,
}
})
o.complete()
} catch (e) {
o.error(e)
libcoreSyncAccount.send({ rawAccount: encodeAccount(account) }).pipe(
map(rawSyncedAccount => {
const syncedAccount = decodeAccount(rawSyncedAccount)
return account => {
const accountOps = account.operations
const syncedOps = syncedAccount.operations
const patch: $Shape<Account> = {
id: syncedAccount.id,
freshAddress: syncedAccount.freshAddress,
freshAddressPath: syncedAccount.freshAddressPath,
balance: syncedAccount.balance,
blockHeight: syncedAccount.blockHeight,
lastSyncDate: new Date(),
}
const hasChanged =
accountOps.length !== syncedOps.length || // size change, we do a full refresh for now...
(accountOps.length > 0 &&
syncedOps.length > 0 &&
(accountOps[0].accountId !== syncedOps[0].accountId ||
accountOps[0].id !== syncedOps[0].id || // if same size, only check if the last item has changed.
accountOps[0].blockHeight !== syncedOps[0].blockHeight))
if (hasChanged) {
patch.operations = syncedAccount.operations
patch.pendingOperations = [] // For now, we assume a change will clean the pendings.
}
return {
...account,
...patch,
}
}
})()
return {
unsubscribe() {
// LibcoreBridge: unsub sync not currently implemented
},
}
}),
}),
),
pullMoreOperations: () => Promise.reject(notImplemented),

2
src/bridge/RippleJSBridge.js

@ -68,6 +68,7 @@ async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOpera
}
const instruction = {
fee: formatAPICurrencyXRP(t.fee).value,
maxLedgerVersionOffset: 12,
}
const prepared = await api.preparePayment(a.freshAddress, payment, instruction)
@ -416,6 +417,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const [last] = operations
const pendingOperations = a.pendingOperations.filter(
o =>
!operations.some(op => o.hash === op.hash) &&
last &&
last.transactionSequenceNumber &&
o.transactionSequenceNumber &&

2
src/components/Onboarding/OnboardingBreadcrumb.js

@ -51,7 +51,7 @@ function OnboardingBreadcrumb(props: Props) {
return (
<Breadcrumb
stepsErrors={genuine.isGenuineFail ? [genuineStepIndex] : undefined}
stepsErrors={genuine.displayErrorScreen ? [genuineStepIndex] : undefined}
currentStep={stepIndex}
items={filteredSteps}
/>

4
src/components/base/Chart/handleMouseEvents.js

@ -65,7 +65,7 @@ export default function handleMouseEvents({
NODES.tooltip
.style('transition', '100ms cubic-bezier(.61,1,.53,1) opacity')
.style('opacity', 1)
.style('transform', `translate3d(${MARGINS.left + x(d.parsedDate)}px, 0, 0)`)
.style('left', `${Math.floor(MARGINS.left + x(d.parsedDate))}px`)
NODES.focus.style('opacity', 1)
NODES.xBar.style('opacity', 1)
}
@ -102,7 +102,7 @@ export default function handleMouseEvents({
</Provider>,
),
)
.style('transform', `translate3d(${MARGINS.left + x(d.parsedDate)}px, 0, 0)`)
.style('left', `${Math.floor(MARGINS.left + x(d.parsedDate))}px`)
NODES.xBar
.attr('x1', x(d.parsedDate))
.attr('x2', x(d.parsedDate))

8
src/components/modals/Receive/steps/03-step-confirm-address.js

@ -1,6 +1,5 @@
// @flow
import invariant from 'invariant'
import styled from 'styled-components'
import React, { Fragment, PureComponent } from 'react'
@ -13,9 +12,7 @@ import TranslatedError from '../../../TranslatedError'
export default class StepConfirmAddress extends PureComponent<StepProps> {
render() {
const { t, device, account, isAddressVerified, verifyAddressError, transitionTo } = this.props
invariant(account, 'No account given')
invariant(device, 'No device given')
const { t, account, isAddressVerified, verifyAddressError, transitionTo } = this.props
return (
<Container>
<TrackPage category="Receive Flow" name="Step 3" />
@ -34,7 +31,8 @@ export default class StepConfirmAddress extends PureComponent<StepProps> {
<Fragment>
<Title>{t('app:receive.steps.confirmAddress.action')}</Title>
<Text>
{t('app:receive.steps.confirmAddress.text', { currencyName: account.currency.name })}
{account &&
t('app:receive.steps.confirmAddress.text', { currencyName: account.currency.name })}
</Text>
<Button mt={4} mb={2} primary onClick={() => transitionTo('receive')}>
{t('app:buttons.displayAddressOnDevice')}

6
src/components/modals/Receive/steps/04-step-receive-funds.js

@ -9,6 +9,7 @@ import { isSegwitAccount } from 'helpers/bip32'
import Box from 'components/base/Box'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import { WrongDeviceForAccount } from 'components/EnsureDeviceApp'
import { DisconnectedDevice } from 'config/errors'
import type { StepProps } from '..'
@ -21,9 +22,10 @@ export default class StepReceiveFunds extends PureComponent<StepProps> {
confirmAddress = async () => {
const { account, device, onChangeAddressVerified, transitionTo } = this.props
invariant(account, 'No account given')
invariant(device, 'No device given')
try {
if (!device || !account) {
throw new DisconnectedDevice()
}
const params = {
currencyId: account.currency.id,
devicePath: device.path,

12
src/components/modals/Send/index.js

@ -8,7 +8,6 @@ import { translate } from 'react-i18next'
import { createStructuredSelector } from 'reselect'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import { createCustomErrorClass } from 'helpers/errors'
import Track from 'analytics/Track'
import { updateAccountWithUpdater } from 'actions/accounts'
import { MODAL_SEND } from 'config/constants'
@ -21,6 +20,7 @@ import type { StepProps as DefaultStepProps } from 'components/base/Stepper'
import { getCurrentDevice } from 'reducers/devices'
import { accountsSelector } from 'reducers/accounts'
import { closeModal, openModal } from 'reducers/modals'
import { DisconnectedDevice, UserRefusedOnDevice } from 'config/errors'
import Modal from 'components/base/Modal'
import Stepper from 'components/base/Stepper'
@ -31,8 +31,6 @@ import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-conn
import StepVerification from './steps/03-step-verification'
import StepConfirmation, { StepConfirmationFooter } from './steps/04-step-confirmation'
const UserRefusedOnDevice = createCustomErrorClass('UserRefusedOnDevice')
type Props = {
t: T,
device: ?Device,
@ -204,7 +202,13 @@ class SendModal extends PureComponent<Props, State<*>> {
const { device } = this.props
const { account, transaction, bridge } = this.state
invariant(device && account && transaction && bridge, 'signTransaction invalid conditions')
if (!device) {
this.handleTransactionError(new DisconnectedDevice())
transitionTo('confirmation')
return
}
invariant(account && transaction && bridge, 'signTransaction invalid conditions')
this._signTransactionSub = bridge
.signAndBroadcast(account, transaction, device.path)

3
src/config/constants.js

@ -56,6 +56,9 @@ export const MANAGER_API_BASE = stringFromEnv(
export const BASE_SOCKET_URL = stringFromEnv('BASE_SOCKET_URL', 'wss://api.ledgerwallet.com/update')
// Provider
export const FORCE_PROVIDER = intFromEnv('FORCE_PROVIDER', 0)
// Flags
export const DEBUG_ANALYTICS = boolFromEnv('DEBUG_ANALYTICS')

8
src/config/errors.js

@ -0,0 +1,8 @@
// @flow
// TODO we need to start porting all custom errors here.
import { createCustomErrorClass } from 'helpers/errors'
export const DisconnectedDevice = createCustomErrorClass('DisconnectedDevice')
export const UserRefusedOnDevice = createCustomErrorClass('UserRefusedOnDevice') // TODO rename because it's just for transaction refusal

20
src/helpers/anonymizer.js

@ -11,9 +11,25 @@ const configDir = (() => {
const cwd = typeof process === 'object' ? process.cwd() || '.' : '__NOTHING_TO_REPLACE__'
// all the paths the app will use. we replace them to anonymize
const basePaths = {
$USER_DATA: configDir,
'.': cwd,
}
function filepathReplace(path: string) {
if (!cwd) return path
return path.replace(cwd, '.').replace(configDir, '$USER_DATA')
if (!path) return path
const replaced = Object.keys(basePaths).reduce((path, name) => {
const p = basePaths[name]
return replaced
.replace(p, name) // normal replace of the path
.replace(encodeURI(p.replace(/\\/g, '/')), name) // replace of the URI version of the path (that are in file:///)
}, path)
if (replaced.length !== path.length) {
// we need to continue until there is no more occurences
return filepathReplace(replaced)
}
return replaced
}
function filepathRecursiveReplacer(obj: mixed, seen: Array<*>) {

18
src/helpers/derivations.js

@ -1,17 +1,21 @@
// @flow
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
type Derivation = ({
currency: CryptoCurrency,
segwit: boolean,
x: number,
}) => string
type Derivation = {
({
currency: CryptoCurrency,
segwit: boolean,
x: number,
}): string,
mandatoryEmptyAccountSkip?: number,
}
const ethLegacyMEW: Derivation = ({ x }) => `44'/60'/0'/${x}`
ethLegacyMEW.mandatoryCount = 5
ethLegacyMEW.mandatoryEmptyAccountSkip = 10
const etcLegacyMEW: Derivation = ({ x }) => `44'/60'/160720'/${x}'/0`
etcLegacyMEW.mandatoryCount = 5
etcLegacyMEW.mandatoryEmptyAccountSkip = 10
const rippleLegacy: Derivation = ({ x }) => `44'/144'/0'/${x}'`

4
src/helpers/deviceAccess.js

@ -3,8 +3,8 @@ import logger from 'logger'
import throttle from 'lodash/throttle'
import type Transport from '@ledgerhq/hw-transport'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import { DisconnectedDevice } from 'config/errors'
import { retry } from './promise'
import { createCustomErrorClass } from './errors'
// all open to device must use openDevice so we can prevent race conditions
// and guarantee we do one device access at a time. It also will handle the .close()
@ -12,8 +12,6 @@ import { createCustomErrorClass } from './errors'
type WithDevice = (devicePath: string) => <T>(job: (Transport<*>) => Promise<*>) => Promise<T>
const DisconnectedDevice = createCustomErrorClass('DisconnectedDevice')
const mapError = e => {
if (e && e.message && e.message.indexOf('HID') >= 0) {
throw new DisconnectedDevice(e.message)

3
src/helpers/devices/getDeviceInfo.js

@ -3,6 +3,7 @@
import type Transport from '@ledgerhq/hw-transport'
import { getFirmwareInfo } from 'helpers/common'
import { FORCE_PROVIDER } from 'config/constants'
export type DeviceInfo = {
targetId: string | number,
@ -31,7 +32,7 @@ export default async (transport: Transport<*>): Promise<DeviceInfo> => {
seVersion.match(/([0-9]+.[0-9])+(.[0-9]+)?((?!-osu)-([a-z]+))?(-osu)?/) || []
const isOSU = typeof parsedVersion[5] !== 'undefined'
const providerName = parsedVersion[4] || ''
const providerId = PROVIDERS[providerName]
const providerId = FORCE_PROVIDER || PROVIDERS[providerName]
const isBootloader = targetId === 0x01000001
const majMin = parsedVersion[1]
const patch = parsedVersion[2] || '.0'

2
src/logger/logger-storybook.js

@ -16,6 +16,8 @@ module.exports = {
analyticsTrack: noop,
analyticsPage: noop,
log: noop,
info: noop,
debug: noop,
warn: noop,
error: noop,
critical: noop,

8
src/logger/logger.js

@ -350,6 +350,14 @@ export default {
// General functions in case the hooks don't apply
debug: (...args: any) => {
logger.log('debug', ...args)
},
info: (...args: any) => {
logger.log('info', ...args)
},
log: (...args: any) => {
logger.log('info', ...args)
},

1
src/styles/reset.js

@ -1,5 +1,6 @@
module.exports = `* {
-webkit-font-smoothing: antialiased;
backface-visibility: hidden;
box-sizing: border-box;
margin: 0;
padding: 0;

12
yarn.lock

@ -1521,9 +1521,9 @@
dependencies:
events "^2.0.0"
"@ledgerhq/ledger-core@2.0.0-rc.4":
version "2.0.0-rc.4"
resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-2.0.0-rc.4.tgz#0ec80a763c666658bea94bd38b86aa90d5a24906"
"@ledgerhq/ledger-core@2.0.0-rc.5":
version "2.0.0-rc.5"
resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-2.0.0-rc.5.tgz#ec42f6c3cc265fc5ca82e01d27df38357642d3ed"
dependencies:
"@ledgerhq/hw-app-btc" "^4.7.3"
"@ledgerhq/hw-transport-node-hid" "^4.7.6"
@ -1534,9 +1534,9 @@
npm "^5.7.1"
prebuild-install "^2.2.2"
"@ledgerhq/live-common@^2.32.0":
version "2.34.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.34.0.tgz#436ec0816c37f6aabcbca807090f152f1bd3b785"
"@ledgerhq/live-common@^2.35.0":
version "2.35.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.35.0.tgz#526bfb94f1a2f1449674c7854cf2c7a162385018"
dependencies:
axios "^0.18.0"
invariant "^2.2.2"

Loading…
Cancel
Save