Browse Source

Merge pull request #1343 from LedgerHQ/develop

Prepare 1.1.0-rc.0
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
d537273908
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 49
      .circleci/config.yml
  2. 1
      .eslintrc
  3. 4
      .github/ISSUE_TEMPLATE/enhancement.md
  4. 4
      .github/ISSUE_TEMPLATE/feature_request.md
  5. 17
      .gitignore
  6. 11
      README.md
  7. 53
      build/linux/arch/PKGBUILD
  8. 10
      build/linux/arch/ledger-live.desktop
  9. 33
      package.json
  10. 132
      scripts/check-wordings.js
  11. 40
      scripts/compile.sh
  12. 17
      scripts/dist-dir.sh
  13. 31
      scripts/dist.sh
  14. 26
      scripts/hash-utils.sh
  15. 22
      scripts/helpers/display-env.sh
  16. 48
      scripts/helpers/format.sh
  17. 39
      scripts/helpers/hash.sh
  18. 62
      scripts/helpers/run-job.sh
  19. 14
      scripts/install-ci-deps.sh
  20. 135
      scripts/live-cli.js
  21. 63
      scripts/parse-accounts.js
  22. 100
      scripts/postinstall.sh
  23. 42
      scripts/publish-arch-package.sh
  24. 50
      scripts/release.sh
  25. 0
      scripts/reset-files.sh
  26. 3
      scripts/start.sh
  27. 16
      src/actions/accounts.js
  28. 43
      src/actions/general.js
  29. 4
      src/analytics/segment.js
  30. 6
      src/api/Ethereum.js
  31. 5
      src/api/Ripple.js
  32. 12
      src/bridge/BridgeSyncContext.js
  33. 54
      src/bridge/EthereumJSBridge.js
  34. 133
      src/bridge/LibcoreBridge.js
  35. 36
      src/bridge/RippleJSBridge.js
  36. 7
      src/bridge/UnsupportedBridge.js
  37. 9
      src/bridge/index.js
  38. 53
      src/bridge/makeMockBridge.js
  39. 9
      src/bridge/types.js
  40. 28
      src/commands/debugAppInfosForCurrency.js
  41. 5
      src/commands/getAddress.js
  42. 2
      src/commands/index.js
  43. 2
      src/commands/installApp.js
  44. 21
      src/commands/libcoreGetFees.js
  45. 82
      src/commands/libcoreSignAndBroadcast.js
  46. 9
      src/commands/libcoreSyncAccount.js
  47. 56
      src/commands/listenDevices.js
  48. 2
      src/commands/uninstallApp.js
  49. 7
      src/components/AccountPage/AccountBalanceSummaryHeader.js
  50. 3
      src/components/AccountPage/AccountHeaderActions.js
  51. 2
      src/components/AccountPage/EmptyStateAccount.js
  52. 2
      src/components/AccountPage/index.js
  53. 11
      src/components/AdvancedOptions/EthereumKind.js
  54. 11
      src/components/BalanceSummary/BalanceInfos.js
  55. 18
      src/components/BalanceSummary/index.js
  56. 17
      src/components/CalculateBalance.js
  57. 5
      src/components/CounterValue/index.js
  58. 1
      src/components/CurrentAddress/index.js
  59. 103
      src/components/DashboardPage/AccountCard.js
  60. 33
      src/components/DashboardPage/AccountCard/Header.js
  61. 94
      src/components/DashboardPage/AccountCard/index.js
  62. 66
      src/components/DashboardPage/AccountCardList.js
  63. 33
      src/components/DashboardPage/AccountCardListHeader.js
  64. 64
      src/components/DashboardPage/AccountCardPlaceholder.js
  65. 80
      src/components/DashboardPage/AccountsOrder.js
  66. 31
      src/components/DashboardPage/CurrentGreetings.js
  67. 22
      src/components/DashboardPage/SummaryDesc.js
  68. 190
      src/components/DashboardPage/index.js
  69. 51
      src/components/DebugAppInfosForCurrency.js
  70. 13
      src/components/DeltaChange.js
  71. 68
      src/components/DeviceInteraction/components.js
  72. 11
      src/components/EnsureDeviceApp.js
  73. 4
      src/components/ExchangePage/ExchangeCard.js
  74. 2
      src/components/ExchangePage/index.js
  75. 35
      src/components/FeesField/BitcoinKind.js
  76. 9
      src/components/FeesField/EthereumKind.js
  77. 7
      src/components/FeesField/RippleKind.js
  78. 22
      src/components/GenuineCheck.js
  79. 96
      src/components/IsUnlocked.js
  80. 1
      src/components/MainSideBar/AddAccountButton.js
  81. 22
      src/components/ManagerPage/AppSearchBar.js
  82. 10
      src/components/ManagerPage/AppsList.js
  83. 17
      src/components/ManagerPage/Dashboard.js
  84. 2
      src/components/ManagerPage/FirmwareUpdate.js
  85. 4
      src/components/ManagerPage/ManagerGenuineCheck.js
  86. 2
      src/components/ManagerPage/UpdateFirmwareButton.js
  87. 14
      src/components/Onboarding/helperComponents.js
  88. 2
      src/components/Onboarding/index.js
  89. 31
      src/components/Onboarding/steps/Finish.js
  90. 14
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckErrorPage.js
  91. 13
      src/components/Onboarding/steps/GenuineCheck/index.js
  92. 15
      src/components/Onboarding/steps/Init.js
  93. 18
      src/components/Onboarding/steps/NoDevice.js
  94. 2
      src/components/Onboarding/steps/SelectDevice.js
  95. 41
      src/components/Onboarding/steps/SetPassword.js
  96. 6
      src/components/Onboarding/steps/Start.js
  97. 5
      src/components/OperationsList/AccountCell.js
  98. 2
      src/components/OperationsList/AddressCell.js
  99. 2
      src/components/OperationsList/AmountCell.js
  100. 1
      src/components/OperationsList/Operation.js

49
.circleci/config.yml

@ -1,47 +1,26 @@
version: 2
docker_defaults: &docker_defaults
defaults: &defaults
working_directory: ~/ledger-live-desktop
docker:
- image: circleci/node:9.5
- image: circleci/node:8.11.3-stretch-browsers
jobs:
build:
<<: *docker_defaults
branches:
ignore:
- gh-pages
<<: *defaults
steps:
- run: sudo apt-get install -y libudev-dev
- checkout
- restore_cache:
name: Restore Yarn Package Cache
keys:
- v2-yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: bash scripts/install-ci-deps.sh
- v7-yarn-packages-{{ checksum "yarn.lock" }}
- run: yarn install
- save_cache:
name: Save Yarn Package Cache
key: v2-yarn-packages-{{ checksum "yarn.lock" }}
key: v7-yarn-packages-{{ checksum "yarn.lock" }}
paths:
- node_modules/
- run:
name: Lint
command: yarn lint
- run:
name: Prettier
command: ./node_modules/.bin/prettier -l "{src,webpack,.storybook}/**/*.js"
- run:
name: Flow
command: yarn flow --quiet
# - run:
# name: Test
# command: yarn test
# - run:
# name: Build
# command: yarn dist:dir
# - run:
# name: Generate build stats
# command: "du -h dist | sort -h > /tmp/build-stats.txt"
# - store_artifacts:
# path: /tmp/build-stats.txt
# destination: build-stats.txt
- node_modules
- run: yarn lint
- run: ./node_modules/.bin/prettier -l "{src,webpack,.storybook,static/i18n}/**/*.js"
- run: yarn flow --quiet
- run: yarn test
- run: yarn release

1
.eslintrc

@ -20,6 +20,7 @@
"ResizeObserver": false,
"jest": false,
"describe": false,
"beforeEach": false,
"test": false,
"it": false,
"expect": false,

4
.github/ISSUE_TEMPLATE/enhancement.md

@ -1,10 +1,10 @@
---
name: 🗣 Start a Discussion
about: Discuss to propose changes to improve the state of Ledger Live.
about: Discuss changes to improve the state of Ledger Live. Please keep one issue per topic.
---
<!-- DESCRIPTION: Explain precisely what you think should be improved and how you think it should work -->
<!-- One topic at a time, use more issues if needed -->
#### Ledger Live Version

4
.github/ISSUE_TEMPLATE/feature_request.md

@ -5,9 +5,7 @@ about: Any feature you find missing in Ledger Live? Discuss to suggest feature r
- [ ] I have checked this feature was not yet requested.
<!-- DESCRIPTION: Explain precisely what is the feature about -->
<!-- DESCRIPTION: Explain precisely what is the feature about. One topic at a time. use more issues if needed -->
#### Part of the application

17
.gitignore

@ -1,11 +1,20 @@
.DS_Store
*.log
/.env
/dist/
/flow-typed/
/node_modules/
/static/fonts/museosans/
/storybook-static/
thumbs.db
/build/linux/arch/.SRCINFO
/build/linux/arch/pkg
/build/linux/arch/src
/build/linux/arch/*.tar.gz
/build/linux/arch/*.tar.xz
# TODO this should be in devs global gitignore
# it makes no sense to have it here
*.log
.DS_Store
.vscode
jsconfig.json
thumbs.db
jsconfig.json

11
README.md

@ -1,10 +1,12 @@
# Ledger Live (desktop) [![CircleCI](https://circleci.com/gh/LedgerHQ/ledger-live-desktop.svg?style=svg)](https://circleci.com/gh/LedgerHQ/ledger-live-desktop) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/ledger-wallet/localized.svg)](https://crowdin.com/project/ledger-wallet)
> Ledger Live is a new generation wallet desktop application providing a unique interface to maintain multiple cryptocurrencies for your Ledger Nano S / Blue. Manage your device, create accounts, receive and send cryptoassets, [...and many more](https://www.ledgerwallet.com/#LINK_TO_ANNOUNCEMENT).
> Ledger Live is a new generation wallet desktop application providing a unique interface to maintain multiple cryptocurrencies for your Ledger Nano S / Blue. Manage your device, create accounts, receive and send cryptoassets, [...and many more](https://www.ledger.fr/2018/07/09/ledger-launches-ledger-live-the-all-in-one-companion-app-to-your-ledger-device/).
<a href="https://github.com/LedgerHQ/ledger-live-desktop/releases">
<p align="center">
<img src="/docs/screenshot.png" width="550"/>
</p>
</a>
## Architecture
@ -14,6 +16,11 @@ Ledger Live is an hybrid desktop application built with Electron, React, Redux,
<img src="/docs/architecture.png" width="550"/>
</p>
### Coins
- supported by [ledger-core](https://github.com/LedgerHQ/lib-ledger-core) (C++) implementation: BTC BCH LTC DASH QTUM ZEC BTG STRAT DOGE DGB HSR KMD PIVX ZEN VTC PPC VIA XST POSW CLUB
- supported by JavaScript implementation: ETH, ETC, XRP
## Setup
### Requirements
@ -73,7 +80,6 @@ DEBUG_ACTION=1
DEBUG_TAB_KEY=1
DEBUG_LIBCORE=1
DEBUG_WS=1
LEDGER_RESET_ALL=1
LEDGER_DEBUG_ALL_LANGS=1
SKIP_GENUINE=1
SKIP_ONBOARDING=1
@ -89,7 +95,6 @@ SYNC_ALL_INTERVAL=60000
CHECK_APP_INTERVAL_WHEN_INVALID=600
CHECK_APP_INTERVAL_WHEN_VALID=1200
CHECK_UPDATE_DELAY=5000
DEVICE_DISCONNECT_DEBOUNCE=500
```
### Launch storybook

53
build/linux/arch/PKGBUILD

@ -0,0 +1,53 @@
# Maintainer: Meriadec Pillet <meriadec.pillet@gmail.com>
# shellcheck disable=SC2154,SC2034,SC2164
pkgname=ledger-live
pkgver=1.0.7
pkgrel=1
pkgdesc="Open source companion app for your Ledger devices"
arch=('x86_64')
url="https://www.ledgerwallet.com/live"
license=('MIT')
makedepends=(yarn python2)
# TODO generate changelog from release notes
changelog=
source=("https://github.com/LedgerHQ/ledger-live-desktop/archive/v${pkgver}.tar.gz"
"ledger-live.desktop")
md5sums=('d60d772a03c0a1c59df07f93b0268a4c'
'52705147909a0a988907a23a71199092')
# TODO sign with ledger pgp
validpgpkeys=()
extractedFolder=ledger-live-desktop-$pkgver
prepare() {
cd $extractedFolder
export JOBS=max
yarn --ignore-scripts
}
build() {
cd $extractedFolder
export GIT_REVISION=$pkgver
export JOBS=max
yarn dist
}
package() {
install -D -m644 \
"${pkgname}.desktop" \
"${pkgdir}/usr/share/applications/${pkgname}.desktop"
cd $extractedFolder
install -dm755 "${pkgdir}/opt"
cp -r "dist/linux-unpacked" "${pkgdir}/opt/ledger-live"
install -dm755 "${pkgdir}/usr/bin"
ln -s "/opt/${pkgname}/ledger-live-desktop" "${pkgdir}/usr/bin/${pkgname}"
install -D -m644 \
"static/images/browser-window-icon-512x512.png" \
"${pkgdir}/usr/share/icons/hicolor/512x512/apps/ledger-live.png"
}

10
build/linux/arch/ledger-live.desktop

@ -0,0 +1,10 @@
[Desktop Entry]
Name=ledger-live
Comment=Open source companion app for your Ledger devices
Path=/opt/ledger-live
Exec=ledger-live
Icon=ledger-live
Type=Application
StartupNotify=true
Categories=Utility;
StartupWMClass=ledger-live

33
package.json

@ -7,20 +7,20 @@
"author": "Ledger",
"license": "MIT",
"scripts": {
"compile": "bash ./scripts/compile.sh",
"dist:dir": "bash ./scripts/dist-dir.sh",
"dist": "bash ./scripts/dist.sh",
"test": "jest",
"flow": "flow",
"lint": "eslint src webpack .storybook",
"ci": "yarn lint && yarn flow && yarn prettier",
"postinstall": "bash ./scripts/postinstall.sh",
"prettier": "prettier --write \"{src,webpack,.storybook}/**/*.js\"",
"publish-storybook": "bash ./scripts/publish-storybook.sh",
"release": "bash ./scripts/release.sh",
"start": "bash ./scripts/start.sh",
"release": "bash ./scripts/release.sh",
"dist": "bash ./scripts/dist.sh",
"dist:dir": "bash ./scripts/dist-dir.sh",
"compile": "bash ./scripts/compile.sh",
"lint": "eslint src webpack .storybook",
"flow": "flow",
"test": "jest",
"prettier": "prettier --write \"{src,webpack,.storybook}/**/*.{js,json}\"",
"ci": "yarn lint && yarn flow && yarn prettier && yarn test",
"storybook": "NODE_ENV=development STORYBOOK_ENV=1 start-storybook -s ./static -p 4444",
"reset-files": "bash ./scripts/reset-files.sh"
"publish-storybook": "bash ./scripts/legacy/publish-storybook.sh",
"reset-files": "bash ./scripts/legacy/reset-files.sh"
},
"electronWebpack": {
"title": true,
@ -31,22 +31,19 @@
"webpackConfig": "./webpack/main.config.js"
}
},
"resolutions": {
"webpack-sources": "1.0.1"
},
"dependencies": {
"@ledgerhq/hw-app-btc": "4.21.0",
"@ledgerhq/hw-app-eth": "^4.14.0",
"@ledgerhq/hw-app-xrp": "^4.13.0",
"@ledgerhq/hw-transport": "^4.13.0",
"@ledgerhq/hw-transport-node-hid": "^4.13.0",
"@ledgerhq/hw-transport-node-hid": "4.22.0",
"@ledgerhq/ledger-core": "2.0.0-rc.5",
"@ledgerhq/live-common": "^2.35.0",
"@ledgerhq/live-common": "3.0.0-beta.2",
"animated": "^0.2.2",
"async": "^2.6.1",
"axios": "^0.18.0",
"babel-runtime": "^6.26.0",
"bcryptjs": "^2.4.3",
"bignumber.js": "^7.2.1",
"bitcoinjs-lib": "^3.3.2",
"bs58": "^4.0.1",
"color": "^3.0.0",
@ -90,6 +87,7 @@
"redux-actions": "^2.4.0",
"redux-thunk": "^2.3.0",
"reselect": "^3.0.1",
"rimraf": "^2.6.2",
"ripple-binary-codec": "^0.1.13",
"ripple-bs58check": "^2.0.2",
"ripple-hashes": "^0.3.1",
@ -109,6 +107,7 @@
"winston": "^3.0.0",
"winston-daily-rotate-file": "^3.2.3",
"winston-transport": "^4.2.0",
"write-file-atomic": "^2.3.0",
"ws": "^5.1.1",
"zxcvbn": "^4.4.2"
},

132
scripts/check-wordings.js

@ -0,0 +1,132 @@
/* eslint-disable no-console */
const { spawn } = require('child_process')
// those wordings are dynamically created, so they are detected
// as false positive
const WHITELIST = [
'app:operation.type.IN',
'app:operation.type.OUT',
'app:exchange.coinhouse',
'app:exchange.changelly',
'app:exchange.coinmama',
'app:exchange.simplex',
'app:exchange.paybis',
'app:addAccounts.accountToImportSubtitle_plural',
'app:dashboard.summary_plural',
'app:addAccounts.success_plural',
'app:addAccounts.successDescription_plural',
'app:time.since.day',
'app:time.since.week',
'app:time.since.month',
'app:time.since.year',
'app:time.day',
'app:time.week',
'app:time.month',
'app:time.year',
'app:addAccounts.cta.add_plural',
'app:manager.apps.installing',
'app:manager.apps.uninstalling',
'app:manager.apps.installSuccess',
'app:manager.apps.uninstallSuccess',
]
const WORDINGS = {
app: require('../static/i18n/en/app.json'),
onboarding: require('../static/i18n/en/onboarding.json'),
// errors: require('../static/i18n/en/errors.json'),
// language: require('../static/i18n/en/language.json'),
}
async function main() {
console.log(`>> Checking for unused wordings...`)
for (const ns in WORDINGS) {
if (WORDINGS.hasOwnProperty(ns)) {
try {
const root = WORDINGS[ns]
await checkForUsage(root, ns, ':')
} catch (err) {
console.log(err)
}
}
}
console.log(`>> Checking for duplicates...`)
for (const ns in WORDINGS) {
if (WORDINGS.hasOwnProperty(ns)) {
try {
const root = WORDINGS[ns]
checkForDuplicate(root, ns, {}, ':')
} catch (err) {
console.log(err)
}
}
}
}
function checkForDuplicate(v, key, values, delimiter = '.') {
if (typeof v === 'object') {
for (const k in v) {
if (v.hasOwnProperty(k)) {
checkForDuplicate(v[k], `${key}${delimiter}${k}`, values)
}
}
} else if (typeof v === 'string') {
if (values[v]) {
console.log(`duplicate value [${v}] for key ${key} (exists in [${values[v].join(', ')}])`)
values[v].push(key)
} else {
values[v] = [key]
}
} else {
console.log(v)
throw new Error('invalid input')
}
}
async function checkForUsage(v, key, delimiter = '.') {
if (WHITELIST.includes(key)) {
return
}
if (typeof v === 'object') {
for (const k in v) {
if (v.hasOwnProperty(k)) {
await checkForUsage(v[k], `${key}${delimiter}${k}`)
}
}
} else if (typeof v === 'string') {
try {
const hasOccurences = await getHasOccurrences(key)
if (!hasOccurences) {
console.log(key)
}
} catch (err) {
console.log(err)
}
} else {
throw new Error('invalid input')
}
}
function getHasOccurrences(key) {
return new Promise(resolve => {
const childProcess = spawn('rg', [key, 'src'])
let data
childProcess.stdout.on('data', d => {
data = d.toString()
})
childProcess.on('close', () => {
if (!data) return resolve(false)
const rows = data.split('\n').filter(Boolean)
return resolve(rows.length > 0)
})
childProcess.on('error', err => {
if (err.code === 'ENOENT') {
console.log(`You need to install ripgrep first`)
console.log(`see: https://github.com/BurntSushi/ripgrep`)
process.exit(1)
}
})
})
}
main()

40
scripts/compile.sh

@ -2,12 +2,38 @@
set -e
GIT_REVISION=$(git rev-parse HEAD)
# shellcheck disable=SC1091
source scripts/helpers/format.sh
# shellcheck disable=SC1091
source scripts/helpers/run-job.sh
if [ "$GIT_REVISION" == "" ]; then
GIT_REVISION=$(git rev-parse HEAD)
fi
export GIT_REVISION
export SENTRY_URL=https://db8f5b9b021048d4a401f045371701cb@sentry.io/274561
export JOBS=max
rm -rf ./node_modules/.cache dist
yarn
NODE_ENV=production yarn run webpack-cli --mode production --config webpack/internals.config.js
NODE_ENV=production yarn run electron-webpack
export SENTRY_URL='https://db8f5b9b021048d4a401f045371701cb@sentry.io/274561'
export JOBS='max'
echo
formatEnvVar 'GIT_REVISION'
formatEnvVar 'SENTRY_URL'
echo
runJob \
"rm -rf dist" \
"clearing dist..." \
"dist cleared" \
"failed to clear dist"
runJob \
"NODE_ENV=production yarn run webpack-cli --mode production --config webpack/internals.config.js" \
"building internal bundle..." \
"internal bundle built" \
"failed to build internal bundle"
runJob \
"NODE_ENV=production yarn run electron-webpack" \
"building main & renderer bundles..." \
"main & renderer bundles built" \
"failed to build main & renderer bundles"

17
scripts/dist-dir.sh

@ -1,3 +1,18 @@
#!/bin/bash
yarn compile && DEBUG=electron-builder electron-builder --dir -c.compression=store -c.mac.identity=null
set -e
# shellcheck disable=SC1091
source scripts/helpers/run-job.sh
# shellcheck disable=SC1091
source scripts/helpers/display-env.sh
yarn compile
runJob \
"DEBUG=electron-builder electron-builder --dir -c.compression=store -c.mac.identity=null" \
"building app..." \
"app built successfully" \
"failed to build app" \
"verbose"

31
scripts/dist.sh

@ -1,16 +1,41 @@
#!/bin/bash
set -e
export JOBS=max
# shellcheck disable=SC1091
source scripts/helpers/run-job.sh
# shellcheck disable=SC1091
source scripts/helpers/display-env.sh
# hilarious fix: to make linux icon we have to remove icon.png from build folder
# some context:
# - https://github.com/electron-userland/electron-builder/issues/2577
# - https://github.com/electron-userland/electron-builder/issues/2269
if [[ $(uname) == 'Linux' ]]; then
mv build/icon.png /tmp
runJob \
"mv build/icon.png /tmp" \
"dirty fix to handle linux icon..." \
"successfully applied dirty fix to handle linux icon" \
"failed to apply dirty fix to handle linux icon"
fi
yarn compile && DEBUG=electron-builder electron-builder
yarn compile
runJob \
"DEBUG=electron-builder electron-builder" \
"building and packaging app..." \
"app built and packaged successfully" \
"failed to build app" \
"verbose"
# hilarious fix continuation: put back the icon where it was
if [[ $(uname) == 'Linux' ]]; then
mv /tmp/icon.png build
runJob \
"mv /tmp/icon.png build" \
"cleaning dirty fix to handle linux icon..." \
"successfully applied clean dirty fix to handle linux icon" \
"failed to apply clean dirty fix to handle linux icon"
fi

26
scripts/hash-utils.sh

@ -1,26 +0,0 @@
#!/bin/bash
function GET_HASH_PATH {
HASH_NAME=$1
echo "./node_modules/.cache/LEDGER_HASH_$HASH_NAME.hash"
}
function GET_HASH {
HASH_NAME=$1
HASH_PATH=$(GET_HASH_PATH "$HASH_NAME")
if [ ! -e "$HASH_PATH" ]; then
echo ''
else
HASH_CONTENT=$(cat "$HASH_PATH")
echo "$HASH_CONTENT"
fi
}
function SET_HASH {
HASH_NAME=$1
HASH_CONTENT=$2
echo "setting hash $HASH_NAME to $HASH_CONTENT"
HASH_PATH=$(GET_HASH_PATH "$HASH_NAME")
mkdir -p ./node_modules/.cache
echo "$HASH_CONTENT" > "$HASH_PATH"
}

22
scripts/helpers/display-env.sh

@ -0,0 +1,22 @@
#!/bin/bash
# shellcheck disable=SC1091
source scripts/helpers/format.sh
if [ "$GIT_REVISION" == "" ]; then
GIT_REVISION=$(git rev-parse HEAD)
fi
echo
printf " │ \\e[4;1m%s\\e[0;0m\\n" "Ledger Live Desktop - ${GIT_REVISION}"
printf " │ \\e[1;30m%s\\e[1;0m\\n" "$(uname -srmo)"
printf " │ \\e[2;1mcommit \\e[0;33m%s\\e[0;0m\\n" "$(git rev-parse HEAD)"
echo
formatEnvVar "CI"
formatEnvVar "NODE_ENV"
formatEnvVar "JOBS"
echo
formatGeneric "node" "$(node --version)"
echo

48
scripts/helpers/format.sh

@ -0,0 +1,48 @@
#!/bin/bash
colSize=20
function formatJobTitle {
echo "[$1]"
echo
}
function formatEnvVar {
key=$1
value=$(eval echo \$"${key}")
color="32"
if [ "$value" == "" ]; then color="34"; value="unset"
elif [ "$value" == "1" ]; then color="32"
elif [ "$value" == "0" ]; then color="35"
else value="'$value'"
fi
printf " %-${colSize}s\\e[2;${color}m%s\\e[1;0m\\n" "$key" "$value"
}
function formatGeneric {
printf " %-${colSize}s\\e[0;2m%s\\e[0m\\n" "$1" "$2"
}
function formatDiscret {
printf "\\e[2;34m%s\\e[2;0m\\n" "$1"
}
function formatSkip {
printf "\\e[2;34m[-] skipping %s (%s)\\e[0;0m\\n" "$1" "$2"
}
function clearLine {
echo -en "\\r\\e[0K"
}
function formatError {
printf "\\e[0;31m[✘] %s\\e[0;0m\\n" "$1"
}
function formatProgress {
printf "\\e[0;35m[⬇] %s\\e[0;0m" "$1"
}
function formatSuccess {
printf "\\e[0;36m[✔] %s\\e[0;0m\\n" "$1"
}

39
scripts/helpers/hash.sh

@ -0,0 +1,39 @@
#!/bin/bash
# shellcheck disable=SC1091
source scripts/helpers/format.sh
function _getHashPath {
HASH_NAME=$1
echo "./node_modules/.cache/LEDGER_HASH_$HASH_NAME.hash"
}
function getHash {
HASH_NAME=$1
HASH_PATH=$(_getHashPath "$HASH_NAME")
if [ ! -e "$HASH_PATH" ]; then
echo ''
else
HASH_CONTENT=$(cat "$HASH_PATH")
echo "$HASH_CONTENT"
fi
}
function setHash {
HASH_NAME=$1
HASH_CONTENT=$2
formatSuccess "$HASH_NAME hash set to $HASH_CONTENT"
HASH_PATH=$(_getHashPath "$HASH_NAME")
mkdir -p ./node_modules/.cache
echo "$HASH_CONTENT" > "$HASH_PATH"
}
function hashDiffers {
cachedHash=$(getHash "$1")
hash=$2
if [ "$cachedHash" == "$hash" ]; then
return 1
else
return 0
fi
}

62
scripts/helpers/run-job.sh

@ -0,0 +1,62 @@
#!/bin/bash
# shellcheck disable=SC1091
source scripts/helpers/format.sh
operatingSystem=$(uname -s)
if [ "$operatingSystem" != "Linux" ] && [ "$operatingSystem" != "Darwin" ]; then
operatingSystem="Windows"
fi
function runJob {
local job=$1
local progressMsg=$2
local successMsg=$3
local errMsg=$4
local logLevel=$5
local tmpScript
local tmpErrFile
local childPid
local returnCode
# let's absolutely don't take care of this fake os
if [ "$operatingSystem" == "Windows" ]; then
tmpScript=$(mktemp)
echo "$job" > "$tmpScript"
bash "$tmpScript"
rm "$tmpScript"
return $?
fi
tmpErrFile=$(mktemp)
formatProgress "$progressMsg"
if [ "$logLevel" == "verbose" ]; then
echo
echo "$job" | bash &
else
echo "$job" | bash >/dev/null 2>"$tmpErrFile" &
fi
childPid=$!
# prevent set -e to exit if child fail
wait $childPid && returnCode=$? || returnCode=$?
if [ "$logLevel" != "verbose" ]; then
clearLine
fi
if [ $returnCode -eq 0 ]; then
formatSuccess "$successMsg"
else
formatError "$errMsg"
formatError "$(cat "$tmpErrFile")"
fi
rm "$tmpErrFile"
return $returnCode
}

14
scripts/install-ci-deps.sh

@ -1,14 +0,0 @@
#!/bin/bash
# shellcheck disable=SC1091
source scripts/hash-utils.sh
PACKAGE_JSON_HASH=$(md5sum package.json | cut -d ' ' -f 1)
CACHED_PACKAGE_JSON_HASH=$(GET_HASH 'package.json')
if [ "$CACHED_PACKAGE_JSON_HASH" == "$PACKAGE_JSON_HASH" ]; then
echo "> Skipping yarn install"
else
yarn install
SET_HASH 'package.json' "$PACKAGE_JSON_HASH"
fi

135
scripts/live-cli.js

@ -1,135 +0,0 @@
// This is a work in progress
// The goal is to provide a cli which allow interact
// with device & libcore for faster iterations
require('babel-polyfill')
require('babel-register')
const chalk = require('chalk')
const inquirer = require('inquirer')
const path = require('path')
const TransportNodeHid = require('@ledgerhq/hw-transport-node-hid').default
const { serializeAccounts, encodeAccount, decodeAccount } = require('../src/reducers/accounts')
const { doSignAndBroadcast } = require('../src/commands/libcoreSignAndBroadcast')
const coreHelper = require('../src/helpers/libcore')
const withLibcore = require('../src/helpers/withLibcore').default
if (!process.env.LEDGER_LIVE_SQLITE_PATH) {
throw new Error('you must define process.env.LEDGER_LIVE_SQLITE_PATH first')
}
const LOCAL_DIRECTORY_PATH = path.resolve(process.env.LEDGER_LIVE_SQLITE_PATH, '../')
gimmeDeviceAndLibCore(async ({ device, core, njsWalletPool }) => {
const raw = require(path.join(LOCAL_DIRECTORY_PATH, 'accounts.json')) // eslint-disable-line import/no-dynamic-require
const accounts = serializeAccounts(raw.data)
const accountToUse = await chooseAccount('Which account to use?', accounts)
await actionLoop({ account: accountToUse, accounts, core, njsWalletPool, device })
process.exit(0)
})
async function actionLoop(props) {
try {
const { account, accounts, core, njsWalletPool, device } = props
const actionToDo = await chooseAction(`What do you want to do with [${account.name}] ?`)
if (actionToDo === 'send funds') {
const transport = await TransportNodeHid.open(device.path)
const accountToReceive = await chooseAccount('To which account?', accounts)
const receiveAddress = await getFreshAddress({
account: accountToReceive,
core,
njsWalletPool,
})
console.log(`the receive address is ${receiveAddress}`)
const rawAccount = encodeAccount(account)
console.log(`trying to sign and broadcast...`)
const rawOp = await doSignAndBroadcast({
account: rawAccount,
transaction: {
amount: 4200000,
recipient: receiveAddress,
feePerByte: 16,
isRBF: false,
},
deviceId: device.path,
core,
transport,
})
console.log(rawOp)
} else if (actionToDo === 'sync') {
console.log(`\nLaunch sync...\n`)
const rawAccount = encodeAccount(account)
const syncedAccount = await coreHelper.syncAccount({ rawAccount, core, njsWalletPool })
console.log(`\nEnd sync...\n`)
console.log(`updated account: `, displayAccount(syncedAccount, 'red'))
} else if (actionToDo === 'quit') {
return true
}
} catch (err) {
console.log(`x Something went wrong`)
console.log(err)
process.exit(1)
}
return actionLoop(props)
}
async function chooseInList(msg, list, formatItem = i => i) {
const choices = list.map(formatItem)
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: msg,
choices,
},
])
const index = choices.indexOf(choice)
return list[index]
}
async function chooseAction(msg) {
return chooseInList(msg, ['sync', 'send funds', 'quit'])
}
function chooseAccount(msg, accounts) {
return chooseInList(msg, accounts, acc => displayAccount(acc))
}
async function gimmeDeviceAndLibCore(cb) {
withLibcore((core, njsWalletPool) => {
TransportNodeHid.listen({
error: () => {},
complete: () => {},
next: async e => {
if (!e.device) {
return
}
if (e.type === 'add') {
const { device } = e
cb({ device, core, njsWalletPool })
}
},
})
})
}
function displayAccount(acc, color = null) {
const isRawAccount = typeof acc.lastSyncDate === 'string'
if (isRawAccount) {
acc = decodeAccount(acc)
}
const str = `[${acc.name}] ${acc.isSegwit ? '' : '(legacy) '}${acc.unit.code} ${acc.balance} - ${
acc.operations.length
} txs`
return color ? chalk[color](str) : str
}
async function getFreshAddress({ account, core, njsWalletPool }) {
const njsAccount = await coreHelper.getNJSAccount({ account, njsWalletPool })
const unsub = await core.syncAccount(njsAccount)
unsub()
const rawAddresses = await njsAccount.getFreshPublicAddresses()
return rawAddresses[0]
}

63
scripts/parse-accounts.js

@ -1,63 +0,0 @@
// Utility to human-read the accounts.json file
// You have to pass it in parameter, because the location
// differ depending on the OS.
const {
formatCurrencyUnit,
getCryptoCurrencyById,
} = require('@ledgerhq/live-common/lib/helpers/currencies')
const chalk = require('chalk')
const padStart = require('lodash/padStart')
const padEnd = require('lodash/padEnd')
const { argv } = process
const [, , FILE_PATH] = argv
if (!FILE_PATH) {
console.log(`You need to specify a file`)
process.exit(1)
}
const { data: wrappedAccounts } = require(FILE_PATH) // eslint-disable-line
const str = wrappedAccounts
.map(({ data: account }) => {
const currency = getCryptoCurrencyById(account.currencyId)
const unit = currency.units[0]
const headline = `${account.isSegwit ? '[SEGWIT]' : '[NOT SEGWIT]'} ${account.name} | ${
account.id
} | ${account.path} | balance: ${formatCurrencyUnit(unit, account.balance, {
showCode: true,
alwaysShowSign: true,
})}`
return [
headline,
headline
.split('')
.map(() => '-')
.join(''),
account.operations
.map(op => {
const opType = op.amount < 0 ? 'SEND' : 'RECEIVE'
return [
padEnd(opType, 8),
op.date.substr(0, 10),
chalk[opType === 'SEND' ? 'red' : 'green'](
padStart(
formatCurrencyUnit(unit, op.amount, {
showCode: true,
alwaysShowSign: true,
}),
15,
),
),
op.hash,
].join(' ')
})
.join('\n'),
].join('\n')
})
.join('\n\n')
console.log(str)

100
scripts/postinstall.sh

@ -1,45 +1,85 @@
#!/bin/bash
set -e
export JOBS=max
# shellcheck disable=SC1091
source scripts/helpers/display-env.sh
# shellcheck disable=SC1091
source scripts/hash-utils.sh
source scripts/helpers/format.sh
# shellcheck disable=SC1091
source scripts/helpers/hash.sh
# shellcheck disable=SC1091
source scripts/helpers/run-job.sh
latestFlowTypedCommitHash=''
function MAIN {
if ! $CI; then
REBUILD_ELECTRON_NATIVE_DEPS
function main {
# native dependencies
if hashDiffers yarn.lock "$(getYarnHash)"; then
rebuildElectronNativeDeps
else
formatSkip "native module build" "already up-to-date"
fi
INSTALL_FLOW_TYPED
}
function INSTALL_FLOW_TYPED {
LATEST_FLOW_TYPED_COMMIT_HASH=$(curl --silent --header "Accept: application/vnd.github.VERSION.sha" https://api.github.com/repos/flowtype/flow-typed/commits/master)
CURRENT_FLOW_TYPED_HASH=$(GET_HASH 'flow-typed')
if [ "$LATEST_FLOW_TYPED_COMMIT_HASH" == "$CURRENT_FLOW_TYPED_HASH" ]; then
echo "> Flow-typed definitions are up to date. Skipping"
# flow-typed
formatProgress "Checking if flow-typed definitions are up-to-date..."
latestFlowTypedCommitHash=$(curl --silent --header "Accept: application/vnd.github.VERSION.sha" --location https://api.github.com/repos/flowtype/flow-typed/commits/master)
clearLine
if [[ $latestFlowTypedCommitHash =~ ^\{ ]]; then
formatError "Failed to retrieve flow-typed definitions"
echo "$latestFlowTypedCommitHash"
exit 1
else
echo "> Installing flow-typed defs"
flow-typed install -s --overwrite
echo "> Removing broken flow definitions"
rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js,redux_*,winston*}
SET_HASH 'flow-typed' "$LATEST_FLOW_TYPED_COMMIT_HASH"
if hashDiffers flow-typed "$latestFlowTypedCommitHash"; then
installFlowTyped
else
formatSkip "flow-typed installation" "already up-to-date"
fi
fi
echo
}
function REBUILD_ELECTRON_NATIVE_DEPS {
# for strange/fancy os-es
function installFlowTyped {
runJob \
"flow-typed install -s --overwrite" \
"Installing flow-typed definitions..." \
"Installed flow-typed definitions" \
"Failed installing flow-typed definitions"
runJob \
"rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js,redux_*,winston*}" \
"Removing broken flow-typed definitions" \
"Removed broken flow-typed definitions" \
"Failed removing broken flow-typed definitions"
setHash flow-typed "$latestFlowTypedCommitHash"
}
function rebuildElectronNativeDeps {
runJob \
"DEBUG=electron-builder electron-builder install-app-deps" \
"Building native electron dependencies..." \
"Successfully builded native modules for electron" \
"Build failed" \
"verbose"
setHash yarn.lock "$(getYarnHash)"
}
function getYarnHash {
if [[ $(uname) == 'Darwin' ]]; then
PACKAGE_JSON_HASH=$(md5 package.json | cut -d ' ' -f 1)
else
# for normal os-es
PACKAGE_JSON_HASH=$(md5sum package.json | cut -d ' ' -f 1)
fi
CACHED_PACKAGE_JSON_HASH=$(GET_HASH 'package.json')
if [ "$CACHED_PACKAGE_JSON_HASH" == "$PACKAGE_JSON_HASH" ]; then
echo "> Electron native deps are up to date. Skipping"
yarnHash=$(md5 yarn.lock | cut -d ' ' -f 1)
else
echo "> Installing electron native deps"
DEBUG=electron-builder electron-builder install-app-deps
SET_HASH 'package.json' "$PACKAGE_JSON_HASH"
yarnHash=$(md5sum yarn.lock | cut -d ' ' -f 1)
fi
echo "$yarnHash"
}
MAIN
main

42
scripts/publish-arch-package.sh

@ -0,0 +1,42 @@
#!/bin/bash
set -e
# shellcheck disable=SC1091
source scripts/helpers/run-job.sh
# shellcheck disable=SC1091
source scripts/helpers/display-env.sh
gitTag=$(git describe --tags)
tmpDir=$(mktemp -d)
runJob \
"pushd build/linux/arch >/dev/null; makepkg --printsrcinfo > .SRCINFO; popd >/dev/null" \
"creating .SRCINFO" \
"successfully created .SRCINFO" \
"error creating .SRCINFO"
runJob \
"git clone ssh://aur@aur.archlinux.org/ledger-live.git ${tmpDir}" \
"cloning AUR repository" \
"cloned AUR repository" \
"error cloning AUR repository"
runJob \
"cp build/linux/arch/{ledger-live.desktop,PKGBUILD,.SRCINFO} \"${tmpDir}\"" \
"copying files" \
"copied files" \
"error copying files"
# shellcheck disable=SC2164
cd "$tmpDir"
git add .
git commit -m "Build for ${gitTag}"
runJob \
"git push origin master" \
"pushing package" \
"successfully pushed package" \
"error pushing package"

50
scripts/release.sh

@ -2,14 +2,43 @@
set -e
# shellcheck disable=SC1091
source scripts/helpers/run-job.sh
# shellcheck disable=SC1091
source scripts/helpers/display-env.sh
if [ "$(git rev-parse --abbrev-ref HEAD)" != "master" ]; then
echo "You are not on master. Exiting properly. (CI)"
exit 0
fi
if ! git describe --exact-match --tags 2>/dev/null >/dev/null; then
echo "You are not on a tag. Exiting properly. (CI)"
exit 0
fi
if [ -z "$GH_TOKEN" ]; then
echo "GH_TOKEN is unset. can't release" >&2
exit 1
fi
if [ ! -d "static/fonts/museosans" ]; then
echo "static/fonts/museosans is required for a release" >&2
exit 1
if ! command -v aws ; then
runJob "sudo apt install awscli" "installing aws cli..." "installed aws cli" "failed to install aws cli"
fi
runJob \
"set -e ;\
rm -rf /tmp/museosans* ;\
aws s3 cp s3://ledger-ledgerlive-resources-dev/resources/museosans.zip /tmp/museosans.zip ;\
unzip /tmp/museosans.zip -d /tmp/museosans ;\
mv /tmp/museosans/museosans static/fonts ;\
rm static/fonts/museosans/.DS_Store # remove crappy macOS file ;\
rm -rf /tmp/museosans*" \
"no museosans font. fetching it from private bucket..." \
"successfully fetched museosans" \
"error fetching museosans"
fi
if ! git diff-index --quiet HEAD --; then
@ -17,8 +46,17 @@ if ! git diff-index --quiet HEAD --; then
exit 1
fi
# TODO check if version is not already there
# TODO check if local git HEAD is EXACTLY our remote master HEAD
originRemote=$(git config --get remote.origin.url)
if [ "$originRemote" != "https://github.com/LedgerHQ/ledger-live-desktop.git" ]; then
echo "the origin remote is incorrect ($originRemote)"
exit 1
fi
runJob "yarn compile" "compiling..." "compiled" "failed to compile" "verbose"
yarn compile
DEBUG=electron-builder yarn run electron-builder build --publish always
runJob \
"DEBUG=electron-builder electron-builder build --publish always" \
"building, packaging and publishing app..." \
"app built, packaged and published successfully" \
"failed to build app" \
"verbose"

0
scripts/reset-files.sh

3
scripts/start.sh

@ -1,5 +1,8 @@
#!/bin/bash
# shellcheck disable=SC1091
source scripts/helpers/display-env.sh
concurrently --raw --kill-others \
"cross-env NODE_ENV=development webpack-cli --mode development --watch --config webpack/internals.config.js" \
"cross-env NODE_ENV=development electron-webpack dev"

16
src/actions/accounts.js

@ -16,20 +16,12 @@ export const removeAccount: RemoveAccount = payload => ({
payload,
})
export type ReorderAccounts = (string[]) => { type: string, payload: string[] }
export const reorderAccounts: ReorderAccounts = payload => ({
type: 'DB:REORDER_ACCOUNTS',
payload,
})
export type FetchAccounts = () => *
export const fetchAccounts: FetchAccounts = () => {
db.init('accounts', []) // FIXME the "init" pattern to drop imo. a simple get()||[] is enough
const accounts = db.get('accounts')
return {
export const fetchAccounts = () => async (dispatch: *) => {
const accounts = await db.getKey('app', 'accounts', [])
return dispatch({
type: 'SET_ACCOUNTS',
payload: accounts,
}
})
}
export type UpdateAccountWithUpdater = (accountId: string, (Account) => Account) => *

43
src/actions/general.js

@ -0,0 +1,43 @@
// @flow
import { createSelector, createStructuredSelector } from 'reselect'
import CounterValues from 'helpers/countervalues'
import {
intermediaryCurrency,
currencySettingsForAccountSelector,
getOrderAccounts,
} from 'reducers/settings'
import { accountsSelector } from 'reducers/accounts'
import { sortAccounts } from 'helpers/accountOrdering'
const accountsBtcBalanceSelector = createSelector(
accountsSelector,
state => state,
(accounts, state) =>
accounts.map(account => {
const { exchange } = currencySettingsForAccountSelector(state, { account })
return CounterValues.calculateSelector(state, {
from: account.currency,
to: intermediaryCurrency,
exchange,
value: account.balance,
})
}),
)
const selectAccountsBalanceAndOrder = createStructuredSelector({
accounts: accountsSelector,
accountsBtcBalance: accountsBtcBalanceSelector,
orderAccounts: getOrderAccounts,
})
export const refreshAccountsOrdering = () => (dispatch: *, getState: *) => {
const all = selectAccountsBalanceAndOrder(getState())
const allRatesAvailable = all.accountsBtcBalance.every(b => !!b)
if (allRatesAvailable) {
dispatch({
type: 'DB:REORDER_ACCOUNTS',
payload: sortAccounts(all),
})
}
}

4
src/analytics/segment.js

@ -52,9 +52,9 @@ const extraProperties = store => {
let storeInstance // is the redux store. it's also used as a flag to know if analytics is on or off.
export const start = (store: *) => {
export const start = async (store: *) => {
if (!user) return
const { id } = user()
const { id } = await user()
logger.analyticsStart(id)
storeInstance = store
const { analytics } = window

6
src/api/Ethereum.js

@ -1,5 +1,6 @@
// @flow
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { BigNumber } from 'bignumber.js'
import { createCustomErrorClass } from 'helpers/errors'
import network from './network'
import { blockchainBaseURL } from './Ledger'
@ -39,7 +40,7 @@ export type API = {
getCurrentBlock: () => Promise<Block>,
getAccountNonce: (address: string) => Promise<number>,
broadcastTransaction: (signedTransaction: string) => Promise<string>,
getAccountBalance: (address: string) => Promise<number>,
getAccountBalance: (address: string) => Promise<BigNumber>,
}
export const apiForCurrency = (currency: CryptoCurrency): API => {
@ -85,7 +86,8 @@ export const apiForCurrency = (currency: CryptoCurrency): API => {
method: 'GET',
url: `${baseURL}/addresses/${address}/balance`,
})
return data[0].balance
// FIXME precision lost here. nothing we can do easily
return BigNumber(data[0].balance)
},
}
}

5
src/api/Ripple.js

@ -1,5 +1,6 @@
// @flow
import logger from 'logger'
import { BigNumber } from 'bignumber.js'
import { RippleAPI } from 'ripple-lib'
import {
parseCurrencyUnit,
@ -31,12 +32,12 @@ export const parseAPICurrencyObject = ({
}) => {
if (currency !== 'XRP') {
logger.warn(`RippleJS: attempt to parse unknown currency ${currency}`)
return 0
return BigNumber(0)
}
return parseAPIValue(value)
}
export const formatAPICurrencyXRP = (amount: number) => {
export const formatAPICurrencyXRP = (amount: BigNumber) => {
const value = formatCurrencyUnit(rippleUnit, amount, {
showAllDigits: true,
disableRounding: true,

12
src/bridge/BridgeSyncContext.js

@ -15,7 +15,7 @@ import { updateAccountWithUpdater } from 'actions/accounts'
import { setAccountSyncState } from 'actions/bridgeSync'
import { bridgeSyncSelector, syncStateLocalSelector } from 'reducers/bridgeSync'
import type { BridgeSyncState } from 'reducers/bridgeSync'
import { accountsSelector } from 'reducers/accounts'
import { accountsSelector, isUpToDateSelector } from 'reducers/accounts'
import { SYNC_MAX_CONCURRENT, SYNC_TIMEOUT } from 'config/constants'
import { getBridgeForCurrency } from '.'
@ -26,6 +26,7 @@ type BridgeSyncProviderProps = {
type BridgeSyncProviderOwnProps = BridgeSyncProviderProps & {
bridgeSync: BridgeSyncState,
accounts: Account[],
isUpToDate: boolean,
updateAccountWithUpdater: (string, (Account) => Account) => void,
setAccountSyncState: (string, AsyncState) => *,
}
@ -49,6 +50,7 @@ const BridgeSyncContext = React.createContext((_: BehaviorAction) => {})
const mapStateToProps = createStructuredSelector({
accounts: accountsSelector,
bridgeSync: bridgeSyncSelector,
isUpToDate: isUpToDateSelector,
})
const actions = {
@ -103,6 +105,7 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
// by convention we remove concurrent tasks with same priority
// FIXME this is somehow a hack. ideally we should just dedup the account ids in the pending queue...
syncQueue.remove(o => priority === o.priority)
logger.debug('schedule', { type: 'syncQueue', ids })
syncQueue.push(ids, -priority)
}
@ -120,6 +123,10 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
if (priority === skipUnderPriority) return
skipUnderPriority = priority
syncQueue.remove(({ priority }) => priority < skipUnderPriority)
if (priority === -1 && !this.props.isUpToDate) {
// going back to -1 priority => retriggering a background sync if it is "Paused"
schedule(shuffledAccountIds(), -1)
}
},
SYNC_ALL_ACCOUNTS: ({ priority }) => {
@ -138,10 +145,11 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
const sync = (action: BehaviorAction) => {
const handler = handlers[action.type]
if (handler) {
logger.debug(`action ${action.type}`, { action, type: 'syncQueue' })
// $FlowFixMe
handler(action)
} else {
logger.warn('BridgeSyncContext unsupported action', action)
logger.warn('BridgeSyncContext unsupported action', { action, type: 'syncQueue' })
}
}

54
src/bridge/EthereumJSBridge.js

@ -1,5 +1,6 @@
// @flow
import { Observable } from 'rxjs'
import { BigNumber } from 'bignumber.js'
import logger from 'logger'
import React from 'react'
import FeesField from 'components/FeesField/EthereumKind'
@ -22,12 +23,19 @@ const NotEnoughBalance = createCustomErrorClass('NotEnoughBalance')
// TODO in future it would be neat to support eip55
type Transaction = {
amount: number,
recipient: string,
gasPrice: number,
gasLimit: number,
amount: BigNumber,
gasPrice: BigNumber,
gasLimit: BigNumber,
}
const serializeTransaction = t => ({
recipient: t.recipient,
amount: `0x${BigNumber(t.amount).toString(16)}`,
gasPrice: `0x${BigNumber(t.gasPrice).toString(16)}`,
gasLimit: `0x${BigNumber(t.gasLimit).toString(16)}`,
})
const EditFees = ({ account, onChange, value }: EditProps<Transaction>) => (
<FeesField
onChange={gasPrice => {
@ -55,13 +63,15 @@ const txToOps = (account: Account) => (tx: Tx): Operation[] => {
const sending = freshAddress === from
const receiving = freshAddress === to
const ops = []
const fee = tx.gas_price * tx.gas_used
// FIXME problem with our api, precision lost here...
const value = BigNumber(tx.value)
const fee = BigNumber(tx.gas_price * tx.gas_used)
if (sending) {
ops.push({
id: `${account.id}-${tx.hash}-OUT`,
hash: tx.hash,
type: 'OUT',
value: tx.value,
value: value.plus(fee),
fee,
blockHeight: tx.block && tx.block.height,
blockHash: tx.block && tx.block.hash,
@ -76,7 +86,7 @@ const txToOps = (account: Account) => (tx: Tx): Operation[] => {
id: `${account.id}-${tx.hash}-IN`,
hash: tx.hash,
type: 'IN',
value: tx.value,
value,
fee,
blockHeight: tx.block && tx.block.height,
blockHash: tx.block && tx.block.hash,
@ -116,7 +126,7 @@ const signAndBroadcast = async ({
currencyId: a.currency.id,
devicePath: deviceId,
path: a.freshAddressPath,
transaction: { ...t, nonce },
transaction: { ...serializeTransaction(t), nonce },
})
.toPromise()
@ -130,7 +140,7 @@ const signAndBroadcast = async ({
hash,
type: 'OUT',
value: t.amount,
fee: t.gasPrice * t.gasLimit,
fee: t.gasPrice.times(t.gasLimit),
blockHeight: null,
blockHash: null,
accountId: a.id,
@ -189,7 +199,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
const freshAddress = address
const accountId = `ethereumjs:${currency.id}:${address}:${publicKey}`
if (txs.length === 0) {
if (txs.length === 0 && balance.isZero()) {
// this is an empty account
if (isStandard) {
if (newAccountCount === 0) {
@ -312,17 +322,18 @@ const EthereumBridge: WalletBridge<Transaction> = {
const blockHash = operations.length > 0 ? operations[0].blockHash : undefined
const { txs } = await api.getTransactions(freshAddress, blockHash)
if (unsubscribed) return
const balance = await api.getAccountBalance(freshAddress)
if (unsubscribed) return
if (txs.length === 0) {
o.next(a => ({
...a,
balance,
blockHeight: block.height,
lastSyncDate: new Date(),
}))
o.complete()
return
}
const balance = await api.getAccountBalance(freshAddress)
if (unsubscribed) return
const nonce = await api.getAccountNonce(freshAddress)
if (unsubscribed) return
o.next(a => {
@ -362,10 +373,10 @@ const EthereumBridge: WalletBridge<Transaction> = {
isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(currency, recipient)),
createTransaction: () => ({
amount: 0,
amount: BigNumber(0),
recipient: '',
gasPrice: 0,
gasLimit: 0x5208,
gasPrice: BigNumber(0),
gasLimit: BigNumber(0x5208),
}),
editTransactionAmount: (account, t, amount) => ({
@ -382,16 +393,23 @@ const EthereumBridge: WalletBridge<Transaction> = {
getTransactionRecipient: (a, t) => t.recipient,
isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false,
isValidTransaction: (a, t) => (!t.amount.isZero() && t.recipient && true) || false,
EditFees,
EditAdvancedOptions,
checkCanBeSpent: (a, t) =>
t.amount <= a.balance ? Promise.resolve() : Promise.reject(new NotEnoughBalance()),
getTotalSpent: (a, t) => Promise.resolve(t.amount + t.gasPrice * t.gasLimit),
getMaxAmount: (a, t) => Promise.resolve(a.balance - t.gasPrice * t.gasLimit),
t.amount.isLessThanOrEqualTo(a.balance)
? Promise.resolve()
: Promise.reject(new NotEnoughBalance()),
getTotalSpent: (a, t) =>
t.amount.isGreaterThan(0) && t.gasPrice.isGreaterThan(0) && t.gasLimit.isGreaterThan(0)
? Promise.resolve(t.amount.plus(t.gasPrice.times(t.gasLimit)))
: Promise.resolve(BigNumber(0)),
getMaxAmount: (a, t) => Promise.resolve(a.balance.minus(t.gasPrice.times(t.gasLimit))),
signAndBroadcast: (a, t, deviceId) =>
Observable.create(o => {

133
src/bridge/LibcoreBridge.js

@ -1,5 +1,6 @@
// @flow
import React from 'react'
import { BigNumber } from 'bignumber.js'
import { map } from 'rxjs/operators'
import LRU from 'lru-cache'
import type { Account } from '@ledgerhq/live-common/lib/types'
@ -19,11 +20,17 @@ const NotEnoughBalance = createCustomErrorClass('NotEnoughBalance')
const notImplemented = new Error('LibcoreBridge: not implemented')
type Transaction = {
amount: number,
feePerByte: number,
amount: BigNumber,
feePerByte: BigNumber,
recipient: string,
}
const serializeTransaction = t => ({
recipient: t.recipient,
amount: t.amount.toString(),
feePerByte: t.feePerByte.toString(),
})
const decodeOperation = (encodedAccount, rawOp) =>
decodeAccount({ ...encodedAccount, operations: [rawOp] }).operations[0]
@ -69,7 +76,7 @@ const isRecipientValid = (currency, recipient) => {
const feesLRU = LRU({ max: 100 })
const getFeesKey = (a, t) =>
`${a.id}_${a.blockHeight || 0}_${t.amount}_${t.recipient}_${t.feePerByte}`
`${a.id}_${a.blockHeight || 0}_${t.amount.toString()}_${t.recipient}_${t.feePerByte.toString()}`
const getFees = async (a, transaction) => {
const isValid = await isRecipientValid(a.currency, transaction.recipient)
@ -78,9 +85,13 @@ const getFees = async (a, transaction) => {
let promise = feesLRU.get(key)
if (promise) return promise
promise = libcoreGetFees
.send({ accountId: a.id, accountIndex: a.index, transaction })
.send({
accountId: a.id,
accountIndex: a.index,
transaction: serializeTransaction(transaction),
})
.toPromise()
.then(r => r.totalFees)
.then(r => BigNumber(r.totalFees))
feesLRU.set(key, promise)
return promise
}
@ -109,50 +120,57 @@ const LibcoreBridge: WalletBridge<Transaction> = {
},
synchronize: account =>
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,
libcoreSyncAccount
.send({
accountId: account.id,
freshAddressPath: account.freshAddressPath,
index: account.index,
currencyId: account.currency.id,
})
.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,
}
}
}
}),
),
}),
),
pullMoreOperations: () => Promise.reject(notImplemented),
isRecipientValid,
createTransaction: () => ({
amount: 0,
amount: BigNumber(0),
recipient: '',
feePerByte: 0,
feePerByte: BigNumber(0),
isRBF: false,
}),
@ -174,28 +192,32 @@ const LibcoreBridge: WalletBridge<Transaction> = {
// EditAdvancedOptions,
isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false,
isValidTransaction: (a, t) => (!t.amount.isZero() && t.recipient && true) || false,
checkCanBeSpent,
getTotalSpent: (a, t) =>
!t.amount
? Promise.resolve(0)
t.amount.isZero()
? Promise.resolve(BigNumber(0))
: getFees(a, t)
.then(totalFees => t.amount + (totalFees || 0))
.catch(() => 0),
.then(totalFees => t.amount.plus(totalFees || 0))
.catch(() => BigNumber(0)),
getMaxAmount: (a, t) =>
getFees(a, t)
.catch(() => 0)
.then(totalFees => a.balance - (totalFees || 0)),
.catch(() => BigNumber(0))
.then(totalFees => a.balance.minus(totalFees || 0)),
signAndBroadcast: (account, transaction, deviceId) => {
const encodedAccount = encodeAccount(account) // FIXME no need to send the whole account over the threads
return libcoreSignAndBroadcast
signAndBroadcast: (account, transaction, deviceId) =>
libcoreSignAndBroadcast
.send({
account: encodedAccount,
transaction,
accountId: account.id,
currencyId: account.currency.id,
xpub: account.xpub,
freshAddress: account.freshAddress,
freshAddressPath: account.freshAddressPath,
index: account.index,
transaction: serializeTransaction(transaction),
deviceId,
})
.pipe(
@ -204,14 +226,13 @@ const LibcoreBridge: WalletBridge<Transaction> = {
case 'broadcasted':
return {
type: 'broadcasted',
operation: decodeOperation(encodedAccount, e.operation),
operation: decodeOperation(encodeAccount(account), e.operation),
}
default:
return e
}
}),
)
},
),
addPendingOperation: (account, operation) => ({
...account,

36
src/bridge/RippleJSBridge.js

@ -1,5 +1,6 @@
// @flow
import invariant from 'invariant'
import { BigNumber } from 'bignumber.js'
import { Observable } from 'rxjs'
import React from 'react'
import bs58check from 'ripple-bs58check'
@ -25,9 +26,9 @@ import type { WalletBridge, EditProps } from './types'
const NotEnoughBalance = createCustomErrorClass('NotEnoughBalance')
type Transaction = {
amount: number,
amount: BigNumber,
recipient: string,
fee: number,
fee: BigNumber,
tag: ?number,
}
@ -197,11 +198,11 @@ const txToOperation = (account: Account) => ({
specification: { source, destination },
}: Tx): ?Operation => {
const type = source.address === account.freshAddress ? 'OUT' : 'IN'
let value = deliveredAmount ? parseAPICurrencyObject(deliveredAmount) : 0
let value = deliveredAmount ? parseAPICurrencyObject(deliveredAmount) : BigNumber(0)
const feeValue = parseAPIValue(fee)
if (type === 'OUT') {
if (!isNaN(feeValue)) {
value += feeValue
value = value.plus(feeValue)
}
}
@ -293,7 +294,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
name: getNewAccountPlaceholderName(currency, index),
freshAddress,
freshAddressPath,
balance: 0,
balance: BigNumber(0),
blockHeight: maxLedgerVersion,
index,
currency,
@ -310,8 +311,8 @@ const RippleJSBridge: WalletBridge<Transaction> = {
if (finished) return
const balance = parseAPIValue(info.xrpBalance)
invariant(
!isNaN(balance) && isFinite(balance),
`Ripple: invalid balance=${balance} for address ${address}`,
!balance.isNaN() && balance.isFinite(),
`Ripple: invalid balance=${balance.toString()} for address ${address}`,
)
const transactions = await api.getTransactions(address, {
@ -394,8 +395,8 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const balance = parseAPIValue(info.xrpBalance)
invariant(
!isNaN(balance) && isFinite(balance),
`Ripple: invalid balance=${balance} for address ${freshAddress}`,
!balance.isNaN() && balance.isFinite(),
`Ripple: invalid balance=${balance.toString()} for address ${freshAddress}`,
)
o.next(a => ({ ...a, balance }))
@ -450,9 +451,9 @@ const RippleJSBridge: WalletBridge<Transaction> = {
isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(currency, recipient)),
createTransaction: () => ({
amount: 0,
amount: BigNumber(0),
recipient: '',
fee: 0,
fee: BigNumber(0),
tag: undefined,
}),
@ -474,19 +475,24 @@ const RippleJSBridge: WalletBridge<Transaction> = {
getTransactionRecipient: (a, t) => t.recipient,
isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false,
isValidTransaction: (a, t) => (!t.amount.isZero() && t.recipient && true) || false,
checkCanBeSpent: async (a, t) => {
const r = await getServerInfo(a.endpointConfig)
if (t.amount + t.fee + parseAPIValue(r.validatedLedger.reserveBaseXRP) <= a.balance) {
if (
t.amount
.plus(t.fee)
.plus(parseAPIValue(r.validatedLedger.reserveBaseXRP))
.isLessThanOrEqualTo(a.balance)
) {
return
}
throw new NotEnoughBalance()
},
getTotalSpent: (a, t) => Promise.resolve(t.amount + t.fee),
getTotalSpent: (a, t) => Promise.resolve(t.amount.plus(t.fee)),
getMaxAmount: (a, t) => Promise.resolve(a.balance - t.fee),
getMaxAmount: (a, t) => Promise.resolve(a.balance.minus(t.fee)),
signAndBroadcast: (a, t, deviceId) =>
Observable.create(o => {

7
src/bridge/UnsupportedBridge.js

@ -1,5 +1,6 @@
// @flow
import { Observable } from 'rxjs'
import { BigNumber } from 'bignumber.js'
import type { WalletBridge } from './types'
const genericError = new Error('UnsupportedBridge')
@ -23,7 +24,7 @@ const UnsupportedBridge: WalletBridge<*> = {
editTransactionAmount: () => null,
getTransactionAmount: () => 0,
getTransactionAmount: () => BigNumber(0),
isValidTransaction: () => false,
@ -33,9 +34,9 @@ const UnsupportedBridge: WalletBridge<*> = {
checkCanBeSpent: () => Promise.resolve(),
getTotalSpent: () => Promise.resolve(0),
getTotalSpent: () => Promise.resolve(BigNumber(0)),
getMaxAmount: () => Promise.resolve(0),
getMaxAmount: () => Promise.resolve(BigNumber(0)),
signAndBroadcast: () =>
Observable.create(o => {

9
src/bridge/index.js

@ -1,17 +1,24 @@
// @flow
import type { Currency } from '@ledgerhq/live-common/lib/types'
import invariant from 'invariant'
import { USE_MOCK_DATA } from 'config/constants'
import { WalletBridge } from './types'
import LibcoreBridge from './LibcoreBridge'
import EthereumJSBridge from './EthereumJSBridge'
import RippleJSBridge from './RippleJSBridge'
import makeMockBridge from './makeMockBridge'
const perFamily = {
bitcoin: LibcoreBridge,
ripple: RippleJSBridge,
ethereum: EthereumJSBridge,
}
if (USE_MOCK_DATA) {
const mockBridge = makeMockBridge()
perFamily.bitcoin = mockBridge
perFamily.ethereum = mockBridge
perFamily.ripple = mockBridge
}
export const getBridgeForCurrency = (currency: Currency): WalletBridge<any> => {
const bridge = perFamily[currency.family]
invariant(bridge, `${currency.id} currency is not supported`)

53
src/bridge/makeMockBridge.js

@ -8,14 +8,19 @@ import {
} from '@ledgerhq/live-common/lib/mock/account'
import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/operation'
import Prando from 'prando'
import { BigNumber } from 'bignumber.js'
import type { Operation } from '@ledgerhq/live-common/lib/types'
import { validateNameEdition } from 'helpers/accountName'
import { MOCK_DATA_SEED } from 'config/constants'
import type { WalletBridge } from './types'
const defaultOpts = {
syncSuccessRate: 0.8,
scanAccountDeviceSuccessRate: 0.8,
transactionsSizeTarget: 100,
extraInitialTransactionProps: () => null,
checkCanBeSpent: () => Promise.resolve(),
getTotalSpent: (a, t) => Promise.resolve(t.amount),
getMaxAmount: a => Promise.resolve(a.balance),
}
const delay = ms => new Promise(success => setTimeout(success, ms))
@ -24,7 +29,6 @@ type Opts = *
function makeMockBridge(opts?: Opts): WalletBridge<*> {
const {
syncSuccessRate,
transactionsSizeTarget,
EditFees,
EditAdvancedOptions,
@ -42,6 +46,9 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
const syncTimeouts = {}
const substractOneYear = date =>
new Date(new Date(date).setFullYear(new Date(date).getFullYear() - 1))
return {
synchronize: initialAccount =>
Observable.create(o => {
@ -51,23 +58,20 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
logger.warn('synchronize was called multiple pending time for same accounts!!!')
}
syncTimeouts[accountId] = setTimeout(() => {
if (Math.random() < syncSuccessRate) {
const ops = broadcasted[accountId] || []
broadcasted[accountId] = []
o.next(account => {
account = { ...account }
account.blockHeight++
for (const op of ops) {
account.balance += getOperationAmountNumber(op)
}
return account
})
o.complete()
} else {
o.error(new Error('Sync Failed'))
}
const ops = broadcasted[accountId] || []
broadcasted[accountId] = []
o.next(account => {
account = { ...account }
account.lastSyncDate = new Date()
account.blockHeight++
for (const op of ops) {
account.balance = account.balance.plus(getOperationAmountNumber(op))
}
return account
})
o.complete()
syncTimeouts[accountId] = null
}, 20000)
}, 2000)
return () => {
clearTimeout(syncTimeouts[accountId])
@ -78,7 +82,6 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
scanAccountsOnDevice: currency =>
Observable.create(o => {
let unsubscribed = false
async function job() {
if (Math.random() > scanAccountDeviceSuccessRate) {
await delay(1000)
@ -88,11 +91,19 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
const nbAccountToGen = 3
for (let i = 0; i < nbAccountToGen && !unsubscribed; i++) {
await delay(500)
const account = genAccount(String(Math.random()), {
const account = genAccount(`${MOCK_DATA_SEED}_${currency.id}_${i}`, {
operationsSize: 0,
currency,
})
account.unit = currency.units[0]
account.index = i
account.operations = account.operations.map(operation => ({
...operation,
date: substractOneYear(operation.date),
}))
account.name = ''
account.name = validateNameEdition(account)
if (!unsubscribed) o.next(account)
}
if (!unsubscribed) o.complete()
@ -120,7 +131,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
isRecipientValid: (currency, recipient) => Promise.resolve(recipient.length > 0),
createTransaction: () => ({
amount: 0,
amount: BigNumber(0),
recipient: '',
...extraInitialTransactionProps(),
}),

9
src/bridge/types.js

@ -1,6 +1,7 @@
// @flow
import type { Observable } from 'rxjs'
import type { BigNumber } from 'bignumber.js'
import type { Account, Operation, Currency } from '@ledgerhq/live-common/lib/types'
// a WalletBridge is implemented on renderer side.
@ -62,9 +63,9 @@ export interface WalletBridge<Transaction> {
createTransaction(account: Account): Transaction;
editTransactionAmount(account: Account, transaction: Transaction, amount: number): Transaction;
editTransactionAmount(account: Account, transaction: Transaction, amount: BigNumber): Transaction;
getTransactionAmount(account: Account, transaction: Transaction): number;
getTransactionAmount(account: Account, transaction: Transaction): BigNumber;
editTransactionRecipient(
account: Account,
@ -84,10 +85,10 @@ export interface WalletBridge<Transaction> {
checkCanBeSpent(account: Account, transaction: Transaction): Promise<void>;
getTotalSpent(account: Account, transaction: Transaction): Promise<number>;
getTotalSpent(account: Account, transaction: Transaction): Promise<BigNumber>;
// NB this is not used yet but we'll use it when we have MAX
getMaxAmount(account: Account, transaction: Transaction): Promise<number>;
getMaxAmount(account: Account, transaction: Transaction): Promise<BigNumber>;
/**
* finalize the transaction by

28
src/commands/debugAppInfosForCurrency.js

@ -0,0 +1,28 @@
// @flow
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import debugAppInfosForCurrency from 'helpers/debugAppInfosForCurrency'
type Input = {
currencyId: string,
devicePath: string,
}
type Result = {
version?: string,
}
const cmd: Command<Input, Result> = createCommand(
'debugAppInfosForCurrency',
({ currencyId, devicePath }) =>
fromPromise(
withDevice(devicePath)(transport =>
debugAppInfosForCurrency(transport, getCryptoCurrencyById(currencyId)),
),
),
)
export default cmd

5
src/commands/getAddress.js

@ -6,10 +6,7 @@ import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import getAddressForCurrency from 'helpers/getAddressForCurrency'
import { createCustomErrorClass } from 'helpers/errors'
const DeviceAppVerifyNotSupported = createCustomErrorClass('DeviceAppVerifyNotSupported')
const UserRefusedAddress = createCustomErrorClass('UserRefusedAddress')
import { DeviceAppVerifyNotSupported, UserRefusedAddress } from 'config/errors'
type Input = {
currencyId: string,

2
src/commands/index.js

@ -3,6 +3,7 @@
import invariant from 'invariant'
import type { Command } from 'helpers/ipc'
import debugAppInfosForCurrency from 'commands/debugAppInfosForCurrency'
import getAddress from 'commands/getAddress'
import getDeviceInfo from 'commands/getDeviceInfo'
import getCurrentFirmware from 'commands/getCurrentFirmware'
@ -34,6 +35,7 @@ import testInterval from 'commands/testInterval'
import uninstallApp from 'commands/uninstallApp'
const all: Array<Command<any, any>> = [
debugAppInfosForCurrency,
getAddress,
getDeviceInfo,
getCurrentFirmware,

2
src/commands/installApp.js

@ -6,7 +6,7 @@ import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import installApp from 'helpers/apps/installApp'
import type { LedgerScriptParams } from 'helpers/common'
import type { LedgerScriptParams } from 'helpers/types'
type Input = {
app: LedgerScriptParams,

21
src/commands/libcoreGetFees.js

@ -1,18 +1,19 @@
// @flow
import { Observable } from 'rxjs'
import { BigNumber } from 'bignumber.js'
import withLibcore from 'helpers/withLibcore'
import { createCommand, Command } from 'helpers/ipc'
import * as accountIdHelper from 'helpers/accountId'
import { isValidAddress } from 'helpers/libcore'
import { isValidAddress, libcoreAmountToBigNumber, bigNumberToLibcoreAmount } from 'helpers/libcore'
import { createCustomErrorClass } from 'helpers/errors'
const InvalidAddress = createCustomErrorClass('InvalidAddress')
type BitcoinLikeTransaction = {
// TODO we rename this Transaction concept into transactionInput
amount: number,
feePerByte: number,
amount: string,
feePerByte: string,
recipient: string,
}
@ -22,7 +23,7 @@ type Input = {
transaction: BitcoinLikeTransaction,
}
type Result = { totalFees: number }
type Result = { totalFees: string }
const cmd: Command<Input, Result> = createCommand(
'libcoreGetFees',
@ -39,13 +40,15 @@ const cmd: Command<Input, Result> = createCommand(
if (isCancelled()) return
const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount()
const njsWalletCurrency = njsWallet.getCurrency()
const amount = new core.NJSAmount(njsWalletCurrency, transaction.amount).fromLong(
const amount = bigNumberToLibcoreAmount(
core,
njsWalletCurrency,
transaction.amount,
BigNumber(transaction.amount),
)
const feesPerByte = new core.NJSAmount(njsWalletCurrency, transaction.feePerByte).fromLong(
const feesPerByte = bigNumberToLibcoreAmount(
core,
njsWalletCurrency,
transaction.feePerByte,
BigNumber(transaction.feePerByte),
)
const transactionBuilder = bitcoinLikeAccount.buildTransaction()
if (!isValidAddress(core, njsWalletCurrency, transaction.recipient)) {
@ -56,7 +59,7 @@ const cmd: Command<Input, Result> = createCommand(
transactionBuilder.pickInputs(0, 0xffffff)
transactionBuilder.setFeesPerByte(feesPerByte)
const builded = await transactionBuilder.build()
const totalFees = builded.getFees().toLong()
const totalFees = libcoreAmountToBigNumber(builded.getFees()).toString()
o.next({ totalFees })
}).then(() => o.complete(), e => o.error(e))

82
src/commands/libcoreSignAndBroadcast.js

@ -1,11 +1,13 @@
// @flow
import logger from 'logger'
import type { AccountRaw, OperationRaw } from '@ledgerhq/live-common/lib/types'
import { BigNumber } from 'bignumber.js'
import type { OperationRaw } from '@ledgerhq/live-common/lib/types'
import Btc from '@ledgerhq/hw-app-btc'
import { Observable } from 'rxjs'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import { isSegwitAccount } from 'helpers/bip32'
import { isSegwitPath } from 'helpers/bip32'
import { libcoreAmountToBigNumber, bigNumberToLibcoreAmount } from 'helpers/libcore'
import withLibcore from 'helpers/withLibcore'
import { createCommand, Command } from 'helpers/ipc'
@ -13,13 +15,18 @@ import { withDevice } from 'helpers/deviceAccess'
import * as accountIdHelper from 'helpers/accountId'
type BitcoinLikeTransaction = {
amount: number,
feePerByte: number,
amount: string,
feePerByte: string,
recipient: string,
}
type Input = {
account: AccountRaw, // FIXME there is no reason we send the whole AccountRaw
accountId: string,
currencyId: string,
xpub: string,
freshAddress: string,
freshAddressPath: string,
index: number,
transaction: BitcoinLikeTransaction,
deviceId: string,
}
@ -30,13 +37,18 @@ type Result = { type: 'signed' } | { type: 'broadcasted', operation: OperationRa
const cmd: Command<Input, Result> = createCommand(
'libcoreSignAndBroadcast',
({ account, transaction, deviceId }) =>
({ accountId, currencyId, xpub, freshAddress, freshAddressPath, index, transaction, deviceId }) =>
Observable.create(o => {
let unsubscribed = false
const isCancelled = () => unsubscribed
withLibcore(core =>
doSignAndBroadcast({
account,
accountId,
currencyId,
xpub,
freshAddress,
freshAddressPath,
index,
transaction,
deviceId,
core,
@ -124,9 +136,14 @@ async function signTransaction({
const changePath = output ? output.getDerivationPath().toString() : undefined
const outputScriptHex = Buffer.from(transaction.serializeOutputs()).toString('hex')
const lockTime = undefined // TODO: transaction.getLockTime()
const initialTimestamp = hasTimestamp ? transaction.getTimestamp() : undefined
// FIXME
// should be `transaction.getLockTime()` as soon as lock time is
// handled by libcore (actually: it always returns a default value
// and that caused issue with zcash (see #904))
const lockTime = undefined
const signedTransaction = await hwApp.createPaymentTransactionNew(
inputs,
associatedKeysets,
@ -144,7 +161,12 @@ async function signTransaction({
}
export async function doSignAndBroadcast({
account,
accountId,
currencyId,
xpub,
freshAddress,
freshAddressPath,
index,
transaction,
deviceId,
core,
@ -152,7 +174,12 @@ export async function doSignAndBroadcast({
onSigned,
onOperationBroadcasted,
}: {
account: AccountRaw,
accountId: string,
currencyId: string,
xpub: string,
freshAddress: string,
freshAddressPath: string,
index: number,
transaction: BitcoinLikeTransaction,
deviceId: string,
core: *,
@ -160,21 +187,15 @@ export async function doSignAndBroadcast({
onSigned: () => void,
onOperationBroadcasted: (optimisticOp: $Exact<OperationRaw>) => void,
}): Promise<void> {
const { walletName } = accountIdHelper.decode(account.id)
const { walletName } = accountIdHelper.decode(accountId)
const njsWallet = await core.getPoolInstance().getWallet(walletName)
if (isCancelled()) return
const njsAccount = await njsWallet.getAccount(account.index)
const njsAccount = await njsWallet.getAccount(index)
if (isCancelled()) return
const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount()
const njsWalletCurrency = njsWallet.getCurrency()
const amount = new core.NJSAmount(njsWalletCurrency, transaction.amount).fromLong(
njsWalletCurrency,
transaction.amount,
)
const fees = new core.NJSAmount(njsWalletCurrency, transaction.feePerByte).fromLong(
njsWalletCurrency,
transaction.feePerByte,
)
const amount = bigNumberToLibcoreAmount(core, njsWalletCurrency, BigNumber(transaction.amount))
const fees = bigNumberToLibcoreAmount(core, njsWalletCurrency, BigNumber(transaction.feePerByte))
const transactionBuilder = bitcoinLikeAccount.buildTransaction()
// TODO: check if is valid address. if not, it will fail silently on invalid
@ -193,16 +214,16 @@ export async function doSignAndBroadcast({
const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction
// TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay
const currency = getCryptoCurrencyById(account.currencyId)
const currency = getCryptoCurrencyById(currencyId)
const signedTransaction = await withDevice(deviceId)(async transport =>
signTransaction({
hwApp: new Btc(transport),
currencyId: account.currencyId,
currencyId,
transaction: builded,
sigHashType: parseInt(sigHashType, 16),
supportsSegwit: !!currency.supportsSegwit,
isSegwit: isSegwitAccount(account),
isSegwit: isSegwitPath(freshAddressPath),
hasTimestamp,
}),
)
@ -216,20 +237,23 @@ export async function doSignAndBroadcast({
.asBitcoinLikeAccount()
.broadcastRawTransaction(Array.from(Buffer.from(signedTransaction, 'hex')))
const fee = builded.getFees().toLong()
const fee = libcoreAmountToBigNumber(builded.getFees())
// NB we don't check isCancelled() because the broadcast is not cancellable now!
onOperationBroadcasted({
id: `${account.xpub}-${txHash}-OUT`,
id: `${xpub}-${txHash}-OUT`,
hash: txHash,
type: 'OUT',
value: transaction.amount + fee,
fee,
value: BigNumber(transaction.amount)
.plus(fee)
.toString(),
fee: fee.toString(),
blockHash: null,
blockHeight: null,
senders: [account.freshAddress],
// FIXME for senders and recipients, can we ask the libcore?
senders: [freshAddress],
recipients: [transaction.recipient],
accountId: account.id,
accountId,
date: new Date().toISOString(),
})
}

9
src/commands/libcoreSyncAccount.js

@ -8,13 +8,16 @@ import { syncAccount } from 'helpers/libcore'
import withLibcore from 'helpers/withLibcore'
type Input = {
rawAccount: AccountRaw, // FIXME there is no reason we send the whole AccountRaw
accountId: string,
freshAddressPath: string,
currencyId: string,
index: number,
}
type Result = AccountRaw
const cmd: Command<Input, Result> = createCommand('libcoreSyncAccount', ({ rawAccount }) =>
fromPromise(withLibcore(core => syncAccount({ rawAccount, core }))),
const cmd: Command<Input, Result> = createCommand('libcoreSyncAccount', accountInfos =>
fromPromise(withLibcore(core => syncAccount({ ...accountInfos, core }))),
)
export default cmd

56
src/commands/listenDevices.js

@ -4,57 +4,17 @@ import logger from 'logger'
import { createCommand } from 'helpers/ipc'
import { Observable } from 'rxjs'
import CommNodeHid from '@ledgerhq/hw-transport-node-hid'
import { DEVICE_DISCONNECT_DEBOUNCE, LISTEN_DEVICES_POLLING_INTERVAL } from 'config/constants'
import { LISTEN_DEVICES_DEBOUNCE } from 'config/constants'
CommNodeHid.setListenDevicesPollingInterval(LISTEN_DEVICES_POLLING_INTERVAL)
CommNodeHid.setListenDevicesDebounce(LISTEN_DEVICES_DEBOUNCE)
const cmd = createCommand('listenDevices', () =>
Observable.create(o => {
const pendingRemovePerPath = {}
const sub = CommNodeHid.listen({
next: e => {
// debounce the add/remove in case we see quick `remove,add` events on same path.
switch (e.type) {
case 'add': {
const pendingRemove = pendingRemovePerPath[e.descriptor]
if (pendingRemove) {
logger.warn(`Skipping remove/add usb event for ${e.descriptor}`)
// there where a recent "remove" event, we don't emit add because we didn't emit "remove" yet.
clearTimeout(pendingRemove)
delete pendingRemovePerPath[e.descriptor]
} else {
// if there were no recent "remove", we just emit the "add"
o.next(e)
}
break
}
case 'remove': {
// we we always debounce the "remove" event. emit it a bit later in case a "add" of same descriptor happen soon.
if (pendingRemovePerPath[e.descriptor]) {
clearTimeout(pendingRemovePerPath[e.descriptor])
}
pendingRemovePerPath[e.descriptor] = setTimeout(() => {
delete pendingRemovePerPath[e.descriptor]
o.next(e)
}, DEVICE_DISCONNECT_DEBOUNCE)
break
}
default:
o.next(e)
}
},
complete: () => {
o.complete()
},
error: err => {
o.error(err)
},
})
return () => {
Object.keys(pendingRemovePerPath).map(k => clearTimeout(pendingRemovePerPath[k]))
sub.unsubscribe()
}
CommNodeHid.setListenDevicesDebug((msg, ...args) =>
logger.debug(msg, {
type: 'listenDevices',
args,
}),
)
const cmd = createCommand('listenDevices', () => Observable.create(CommNodeHid.listen))
export default cmd

2
src/commands/uninstallApp.js

@ -6,7 +6,7 @@ import { withDevice } from 'helpers/deviceAccess'
import uninstallApp from 'helpers/apps/uninstallApp'
import type { LedgerScriptParams } from 'helpers/common'
import type { LedgerScriptParams } from 'helpers/types'
type Input = {
app: LedgerScriptParams,

7
src/components/AccountPage/AccountBalanceSummaryHeader.js

@ -1,6 +1,7 @@
// @flow
import React, { PureComponent } from 'react'
import type { BigNumber } from 'bignumber.js'
import { createStructuredSelector } from 'reselect'
import { compose } from 'redux'
import { connect } from 'react-redux'
@ -25,9 +26,9 @@ import PillsDaysCount from 'components/PillsDaysCount'
type OwnProps = {
isAvailable: boolean,
totalBalance: number,
sinceBalance: number,
refBalance: number,
totalBalance: BigNumber,
sinceBalance: BigNumber,
refBalance: BigNumber,
accountId: string,
}

3
src/components/AccountPage/AccountHeaderActions.js

@ -24,7 +24,6 @@ import Box, { Tabbable } from 'components/base/Box'
import Button from 'components/base/Button'
const ButtonSettings = styled(Tabbable).attrs({
cursor: 'pointer',
align: 'center',
justify: 'center',
borderRadius: 1,
@ -62,7 +61,7 @@ class AccountHeaderActions extends PureComponent<Props> {
const { account, openModal, t } = this.props
return (
<Box horizontal alignItems="center" justifyContent="flex-end" flow={2}>
{account.operations.length > 0 || account.balance > 0 ? (
{account.operations.length > 0 || !account.balance.isZero() ? (
<Fragment>
<Button small primary onClick={() => openModal(MODAL_SEND, { account })}>
<Box horizontal flow={1} alignItems="center">

2
src/components/AccountPage/EmptyStateAccount.js

@ -32,7 +32,7 @@ class EmptyStateAccount extends PureComponent<Props, *> {
render() {
const { t, account, openModal } = this.props
return (
<Box mt={7} alignItems="center">
<Box mt={7} alignItems="center" selectable>
<img
alt="emptyState Dashboard logo"
src={i('logos/emptyStateAccount.png')}

2
src/components/AccountPage/index.js

@ -82,7 +82,7 @@ class AccountPage extends PureComponent<Props> {
<AccountHeaderActions account={account} />
</Box>
{account.operations.length > 0 || account.balance > 0 ? (
{account.operations.length > 0 || !account.balance.isZero() ? (
<Fragment>
<Box mb={7}>
<BalanceSummary

11
src/components/AdvancedOptions/EthereumKind.js

@ -1,5 +1,6 @@
// @flow
import React from 'react'
import { BigNumber } from 'bignumber.js'
import { translate } from 'react-i18next'
import Box from 'components/base/Box'
@ -8,8 +9,8 @@ import Label from 'components/base/Label'
import Spoiler from 'components/base/Spoiler'
type Props = {
gasLimit: number,
onChangeGasLimit: (?number) => void,
gasLimit: BigNumber,
onChangeGasLimit: BigNumber => void,
t: *,
}
@ -25,9 +26,9 @@ export default translate()(({ gasLimit, onChangeGasLimit, t }: Props) => (
<Input
value={gasLimit}
onChange={str => {
const gasLimit = parseInt(str || 0, 10)
if (!isNaN(gasLimit) && isFinite(gasLimit)) onChangeGasLimit(gasLimit)
else onChangeGasLimit(0x5208)
const gasLimit = BigNumber(str || 0)
if (!gasLimit.isNaN() && gasLimit.isFinite()) onChangeGasLimit(gasLimit)
else onChangeGasLimit(BigNumber(0x5208))
}}
/>
</Box>

11
src/components/BalanceSummary/BalanceInfos.js

@ -1,6 +1,7 @@
// @flow
import React from 'react'
import type { BigNumber } from 'bignumber.js'
import styled from 'styled-components'
import type { Unit, Currency } from '@ledgerhq/live-common/lib/types'
@ -20,9 +21,9 @@ const Sub = styled(Box).attrs({
type BalanceSinceProps = {
since: string,
totalBalance: number,
sinceBalance: number,
refBalance: number,
totalBalance: BigNumber,
sinceBalance: BigNumber,
refBalance: BigNumber,
isAvailable: boolean,
t: T,
}
@ -31,7 +32,7 @@ type BalanceTotalProps = {
children?: any,
unit: Unit,
isAvailable: boolean,
totalBalance: number,
totalBalance: BigNumber,
showCryptoEvenIfNotAvailable?: boolean,
}
@ -77,7 +78,7 @@ export function BalanceSinceDiff(props: Props) {
unit={counterValue.units[0]}
fontSize={7}
showCode
val={totalBalance - sinceBalance}
val={totalBalance.minus(sinceBalance)}
withIcon
/>
)}

18
src/components/BalanceSummary/index.js

@ -1,6 +1,7 @@
// @flow
import React, { Fragment } from 'react'
import { BigNumber } from 'bignumber.js'
import moment from 'moment'
import { formatShort } from '@ledgerhq/live-common/lib/helpers/currencies'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
@ -19,9 +20,9 @@ type Props = {
daysCount: number,
renderHeader?: ({
selectedTimeRange: *,
totalBalance: number,
sinceBalance: number,
refBalance: number,
totalBalance: BigNumber,
sinceBalance: BigNumber,
refBalance: BigNumber,
isAvailable: boolean,
}) => *,
}
@ -64,11 +65,12 @@ const BalanceSummary = ({
? balanceHistory
: balanceHistory.map(i => ({
...i,
value:
value: BigNumber(
10000 *
(1 +
0.1 * Math.sin(i.date * Math.cos(i.date)) + // random-ish
0.5 * Math.cos(i.date / 2000000000 + Math.sin(i.date / 1000000000))), // general curve trend
(1 +
0.1 * Math.sin(i.date * Math.cos(i.date)) + // random-ish
0.5 * Math.cos(i.date / 2000000000 + Math.sin(i.date / 1000000000))),
), // general curve trend
}))
}
height={200}
@ -76,7 +78,7 @@ const BalanceSummary = ({
cvCode={counterValue.ticker}
tickXScale={selectedTimeRange}
renderTickY={
isAvailable ? val => formatShort(counterValue.units[0], val) : () => ''
isAvailable ? val => formatShort(counterValue.units[0], BigNumber(val)) : () => ''
}
isInteractive={isAvailable}
renderTooltip={

17
src/components/CalculateBalance.js

@ -3,6 +3,7 @@
import { Component } from 'react'
import { connect } from 'react-redux'
import { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { getBalanceHistorySum } from '@ledgerhq/live-common/lib/helpers/account'
@ -23,14 +24,14 @@ type OwnProps = {
type Item = {
date: Date,
value: number,
originalValue: number,
value: BigNumber,
originalValue: BigNumber,
}
type Props = OwnProps & {
balanceHistory: Item[],
balanceStart: number,
balanceEnd: number,
balanceStart: BigNumber,
balanceEnd: BigNumber,
isAvailable: boolean,
hash: string,
}
@ -61,15 +62,15 @@ const mapStateToProps = (state: State, props: OwnProps) => {
toExchange: counterValueExchange,
to: counterValueCurrency,
})
if (!cv && cv !== 0) {
if (!cv) {
isAvailable = false
return 0
return BigNumber(0)
}
return cv
},
).map((item, i) =>
// reconciliate balance history with original values
({ ...item, originalValue: originalValues[i] || 0 }),
({ ...item, originalValue: originalValues[i] || BigNumber(0) }),
)
const balanceEnd = balanceHistory[balanceHistory.length - 1].value
@ -81,7 +82,7 @@ const mapStateToProps = (state: State, props: OwnProps) => {
balanceEnd,
hash: `${props.accounts.length > 0 ? props.accounts[0].id : ''}_${
balanceHistory.length
}_${balanceEnd}_${isAvailable.toString()}`,
}_${balanceEnd.toString()}_${isAvailable.toString()}`,
}
}

5
src/components/CounterValue/index.js

@ -1,5 +1,6 @@
// @flow
import type { BigNumber } from 'bignumber.js'
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import type { Currency } from '@ledgerhq/live-common/lib/types'
@ -23,7 +24,7 @@ type OwnProps = {
// when? if not given: take latest
date?: Date,
value: number,
value: BigNumber,
alwaysShowSign?: boolean,
}
@ -61,7 +62,7 @@ class CounterValue extends PureComponent<Props> {
}
render() {
const { value, counterValueCurrency, date, alwaysShowSign, ...props } = this.props
if (!value && value !== 0) {
if (!value) {
return null
}
return (

1
src/components/CurrentAddress/index.js

@ -88,7 +88,6 @@ const FooterButtonWrapper = styled(Box).attrs({
px: 2,
})`
line-height: 1;
cursor: pointer;
height: 55px;
&:hover {

103
src/components/DashboardPage/AccountCard.js

@ -1,103 +0,0 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import type { Account, Currency } from '@ledgerhq/live-common/lib/types'
import Chart from 'components/base/Chart'
import Bar from 'components/base/Bar'
import Box, { Card } from 'components/base/Box'
import CalculateBalance from 'components/CalculateBalance'
import FormattedVal from 'components/base/FormattedVal'
import Ellipsis from 'components/base/Ellipsis'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import DeltaChange from '../DeltaChange'
const Wrapper = styled(Card).attrs({
p: 4,
flex: 1,
})`
cursor: ${p => (p.onClick ? 'pointer' : 'default')};
`
class AccountCard extends PureComponent<{
counterValue: Currency,
account: Account,
onClick?: Account => void,
daysCount: number,
}> {
render() {
const { counterValue, account, onClick, daysCount, ...props } = this.props
return (
<Wrapper onClick={onClick ? () => onClick(account) : null} {...props}>
<Box flow={4}>
<Box horizontal ff="Open Sans|SemiBold" flow={3} alignItems="center">
<Box
alignItems="center"
justifyContent="center"
style={{ color: account.currency.color }}
>
<CryptoCurrencyIcon currency={account.currency} size={20} />
</Box>
<Box grow>
<Box style={{ textTransform: 'uppercase' }} fontSize={0} color="graphite">
{account.currency.name}
</Box>
<Ellipsis fontSize={4} color="dark">
{account.name}
</Ellipsis>
</Box>
</Box>
<Bar size={1} color="fog" />
<Box justifyContent="center">
<FormattedVal
alwaysShowSign={false}
color="dark"
unit={account.unit}
showCode
val={account.balance}
/>
</Box>
</Box>
<CalculateBalance counterValue={counterValue} accounts={[account]} daysCount={daysCount}>
{({ isAvailable, balanceHistory, balanceStart, balanceEnd }) => (
<Box flow={4}>
<Box flow={2} horizontal>
<Box justifyContent="center">
{isAvailable ? (
<FormattedVal
animateTicker
unit={counterValue.units[0]}
val={balanceEnd}
alwaysShowSign={false}
showCode
fontSize={3}
color="graphite"
/>
) : null}
</Box>
<Box grow justifyContent="center">
{balanceStart && isAvailable ? (
<DeltaChange from={balanceStart} to={balanceEnd} alwaysShowSign fontSize={3} />
) : null}
</Box>
</Box>
<Chart
data={balanceHistory}
color={account.currency.color}
height={52}
hideAxis
isInteractive={false}
id={`account-chart-${account.id}`}
unit={account.unit}
/>
</Box>
)}
</CalculateBalance>
</Wrapper>
)
}
}
export default AccountCard

33
src/components/DashboardPage/AccountCard/Header.js

@ -0,0 +1,33 @@
// @flow
import React, { PureComponent } from 'react'
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import Box from 'components/base/Box'
import Ellipsis from 'components/base/Ellipsis'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
class AccountCardHeader extends PureComponent<{
currency: CryptoCurrency,
accountName: string,
}> {
render() {
const { currency, accountName } = this.props
return (
<Box horizontal ff="Open Sans|SemiBold" flow={3} alignItems="center">
<Box alignItems="center" justifyContent="center" style={{ color: currency.color }}>
<CryptoCurrencyIcon currency={currency} size={20} />
</Box>
<Box grow>
<Box style={{ textTransform: 'uppercase' }} fontSize={0} color="graphite">
{currency.name}
</Box>
<Ellipsis fontSize={4} color="dark">
{accountName}
</Ellipsis>
</Box>
</Box>
)
}
}
export default AccountCardHeader

94
src/components/DashboardPage/AccountCard/index.js

@ -0,0 +1,94 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import type { Account, Currency } from '@ledgerhq/live-common/lib/types'
import Chart from 'components/base/Chart'
import Bar from 'components/base/Bar'
import Box, { Card } from 'components/base/Box'
import CalculateBalance from 'components/CalculateBalance'
import FormattedVal from 'components/base/FormattedVal'
import DeltaChange from 'components/DeltaChange'
import AccountCardHeader from './Header'
const Wrapper = styled(Card).attrs({
p: 4,
flex: 1,
})`
cursor: ${p => (p.onClick ? 'pointer' : 'default')};
`
class AccountCard extends PureComponent<{
counterValue: Currency,
account: Account,
onClick: Account => void,
daysCount: number,
}> {
renderBody = ({ isAvailable, balanceHistory, balanceStart, balanceEnd }: *) => {
const { counterValue, account } = this.props
return (
<Box flow={4}>
<Box flow={2} horizontal>
<Box justifyContent="center">
{isAvailable ? (
<FormattedVal
animateTicker
unit={counterValue.units[0]}
val={balanceEnd}
alwaysShowSign={false}
showCode
fontSize={3}
color="graphite"
/>
) : null}
</Box>
<Box grow justifyContent="center">
{isAvailable && !balanceStart.isZero() ? (
<DeltaChange from={balanceStart} to={balanceEnd} alwaysShowSign fontSize={3} />
) : null}
</Box>
</Box>
<Chart
data={balanceHistory}
color={account.currency.color}
height={52}
hideAxis
isInteractive={false}
id={`account-chart-${account.id}`}
unit={account.unit}
/>
</Box>
)
}
onClick = () => {
const { account, onClick } = this.props
onClick(account)
}
render() {
const { counterValue, account, onClick, daysCount, ...props } = this.props
return (
<Wrapper onClick={this.onClick} {...props}>
<Box flow={4}>
<AccountCardHeader accountName={account.name} currency={account.currency} />
<Bar size={1} color="fog" />
<Box justifyContent="center">
<FormattedVal
alwaysShowSign={false}
color="dark"
unit={account.unit}
showCode
val={account.balance}
/>
</Box>
</Box>
<CalculateBalance counterValue={counterValue} accounts={[account]} daysCount={daysCount}>
{this.renderBody}
</CalculateBalance>
</Wrapper>
)
}
}
export default AccountCard

66
src/components/DashboardPage/AccountCardList.js

@ -0,0 +1,66 @@
// @flow
import React, { Component } from 'react'
import type { Account, Currency } from '@ledgerhq/live-common/lib/types'
import Box from 'components/base/Box'
import AccountCard from './AccountCard'
import AccountCardListHeader from './AccountCardListHeader'
import AccountCardPlaceholder from './AccountCardPlaceholder'
type Props = {
accounts: Account[],
onAccountClick: Account => void,
counterValue: Currency,
daysCount: number,
}
class AccountCardList extends Component<Props> {
render() {
const { accounts, counterValue, daysCount, onAccountClick } = this.props
return (
<Box flow={4}>
<AccountCardListHeader accountsLength={accounts.length} />
<Box
horizontal
flexWrap="wrap"
justifyContent="flex-start"
alignItems="center"
style={{ margin: '0 -16px' }}
>
{accounts
.map(account => ({
key: account.id,
account,
}))
.concat(
Array(3 - (accounts.length % 3))
.fill(null)
.map((_, i) => ({
key: `placeholder_${i}`,
withPlaceholder: i === 0,
})),
)
.map(item => (
<Box key={item.key} flex="33%" p={16}>
{item.account ? (
<AccountCard
key={item.account.id}
counterValue={counterValue}
account={item.account}
daysCount={daysCount}
onClick={onAccountClick}
/>
) : item.withPlaceholder ? (
<AccountCardPlaceholder />
) : null}
</Box>
))}
</Box>
</Box>
)
}
}
export default AccountCardList

33
src/components/DashboardPage/AccountCardListHeader.js

@ -0,0 +1,33 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import AccountsOrder from './AccountsOrder'
type Props = {
t: T,
accountsLength: number,
}
class AccountCardListHeader extends PureComponent<Props> {
render() {
const { accountsLength, t } = this.props
return (
<Box horizontal alignItems="flex-end">
<Text color="dark" ff="Museo Sans" fontSize={6}>
{t('app:dashboard.accounts.title', { count: accountsLength })}
</Text>
<Box ml="auto" horizontal flow={1}>
<AccountsOrder />
</Box>
</Box>
)
}
}
export default translate()(AccountCardListHeader)

64
src/components/DashboardPage/AccountCardPlaceholder.js

@ -0,0 +1,64 @@
// @flow
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import { openModal } from 'reducers/modals'
import { MODAL_ADD_ACCOUNTS } from 'config/constants'
import type { T } from 'types/common'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
const Wrapper = styled(Box).attrs({
p: 4,
flex: 1,
alignItems: 'center',
})`
border: 1px dashed ${p => p.theme.colors.fog};
border-radius: 4px;
height: 215px;
`
class AccountCardPlaceholder extends PureComponent<{
t: T,
openModal: string => void,
}> {
onAddAccounts = () => this.props.openModal(MODAL_ADD_ACCOUNTS)
render() {
const { t } = this.props
return (
<Wrapper>
<Box mt={2}>
<img alt="" src={i('empty-account-tile.svg')} />
</Box>
<Box
ff="Open Sans"
fontSize={3}
color="grey"
pb={2}
mt={3}
textAlign="center"
style={{ maxWidth: 150 }}
>
{t('app:dashboard.emptyAccountTile.desc')}
</Box>
<Button primary onClick={this.onAddAccounts}>
{t('app:dashboard.emptyAccountTile.createAccount')}
</Button>
</Wrapper>
)
}
}
export default translate()(
connect(
null,
{
openModal,
},
)(AccountCardPlaceholder),
)

80
src/components/DashboardPage/AccountsOrder.js

@ -1,32 +1,21 @@
// @flow
import logger from 'logger'
import React, { Component } from 'react'
import styled from 'styled-components'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import type { Account } from '@ledgerhq/live-common/lib/types'
import CounterValues from 'helpers/countervalues'
import { createStructuredSelector } from 'reselect'
import type { T } from 'types/common'
import {
getOrderAccounts,
intermediaryCurrency,
currencySettingsForAccountSelector,
} from 'reducers/settings'
import { createStructuredSelector, createSelector } from 'reselect'
import { reorderAccounts } from 'actions/accounts'
import { accountsSelector } from 'reducers/accounts'
import { refreshAccountsOrdering } from 'actions/general'
import { saveSettings } from 'actions/settings'
import { getOrderAccounts } from 'reducers/settings'
import Track from 'analytics/Track'
import BoldToggle from 'components/base/BoldToggle'
import Box from 'components/base/Box'
import DropDown, { DropDownItem } from 'components/base/DropDown'
import Text from 'components/base/Text'
import IconAngleDown from 'icons/AngleDown'
import IconArrowDown from 'icons/ArrowDown'
import IconArrowUp from 'icons/ArrowUp'
@ -34,41 +23,10 @@ import IconArrowUp from 'icons/ArrowUp'
type Props = {
t: T,
orderAccounts: string,
accounts: Account[],
accountsBtcBalance: number[], // eslint-disable-line
reorderAccounts: (string[]) => *,
refreshAccountsOrdering: () => *,
saveSettings: (*) => *,
}
type SortMethod = 'name' | 'balance'
const sortMethod: { [_: SortMethod]: (Account[], Props) => string[] } = {
balance: (accounts, { accountsBtcBalance }: Props) =>
accounts
.map((a, i) => [a.id, accountsBtcBalance[i]])
.sort((a, b) => a[1] - b[1])
.map(o => o[0]),
name: accounts =>
accounts
.slice(0)
.sort((a, b) => a.name.localeCompare(b.name))
.map(a => a.id),
}
function sortAccounts(accounts: Account[], orderAccounts: string, props: Props) {
const [order, sort] = orderAccounts.split('|')
if (order === 'name' || order === 'balance') {
const ids = sortMethod[order](accounts, props)
if (sort === 'asc') {
ids.reverse()
}
return ids
}
logger.warn(`sortAccounts not implemented for ${orderAccounts}`)
return null
}
const OrderIcon = styled(Box).attrs({
alignItems: 'center',
justifyContent: 'center',
@ -77,31 +35,12 @@ const OrderIcon = styled(Box).attrs({
opacity: ${p => (p.isActive ? 1 : 0)};
`
const accountsBtcBalanceSelector = createSelector(
accountsSelector,
state => state,
(accounts, state) =>
accounts.map(account => {
const { exchange } = currencySettingsForAccountSelector(state, { account })
return (
CounterValues.calculateSelector(state, {
from: account.currency,
to: intermediaryCurrency,
exchange,
value: account.balance,
}) || 0
)
}),
)
const mapStateToProps = createStructuredSelector({
orderAccounts: getOrderAccounts,
accounts: accountsSelector,
accountsBtcBalance: accountsBtcBalanceSelector,
})
const mapDispatchToProps = {
reorderAccounts,
refreshAccountsOrdering,
saveSettings,
}
@ -120,12 +59,9 @@ class AccountsOrder extends Component<Props> {
}
setAccountOrder = order => {
const { saveSettings, reorderAccounts } = this.props
const maybeIds = sortAccounts(this.props.accounts, order, this.props)
if (maybeIds) {
reorderAccounts(maybeIds)
saveSettings({ orderAccounts: order })
}
const { saveSettings, refreshAccountsOrdering } = this.props
saveSettings({ orderAccounts: order })
refreshAccountsOrdering()
}
getCurrentOrder = () => {

31
src/components/DashboardPage/CurrentGreetings.js

@ -0,0 +1,31 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Text from 'components/base/Text'
const getCurrentGreetings = () => {
const localTimeHour = new Date().getHours()
const afternoon_breakpoint = 12
const evening_breakpoint = 17
if (localTimeHour >= afternoon_breakpoint && localTimeHour < evening_breakpoint) {
return 'app:dashboard.greeting.afternoon'
} else if (localTimeHour >= evening_breakpoint) {
return 'app:dashboard.greeting.evening'
}
return 'app:dashboard.greeting.morning'
}
class CurrentGettings extends PureComponent<{ t: T }> {
render() {
const { t } = this.props
return (
<Text color="dark" ff="Museo Sans" fontSize={7}>
{t(getCurrentGreetings())}
</Text>
)
}
}
export default translate()(CurrentGettings)

22
src/components/DashboardPage/SummaryDesc.js

@ -0,0 +1,22 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Text from 'components/base/Text'
class SummaryDesc extends PureComponent<{
t: T,
totalAccounts: number,
}> {
render() {
const { totalAccounts, t } = this.props
return (
<Text color="grey" fontSize={5} ff="Museo Sans|Light">
{t('app:dashboard.summary', { count: totalAccounts })}
</Text>
)
}
}
export default translate()(SummaryDesc)

190
src/components/DashboardPage/index.js

@ -5,7 +5,6 @@ import uniq from 'lodash/uniq'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { push } from 'react-router-redux'
import { createStructuredSelector } from 'reselect'
import type { Account, Currency } from '@ledgerhq/live-common/lib/types'
@ -14,46 +13,38 @@ import type { T } from 'types/common'
import { colors } from 'styles/theme'
import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { MODAL_ADD_ACCOUNTS } from 'config/constants'
import {
counterValueCurrencySelector,
localeSelector,
selectedTimeRangeSelector,
timeRangeDaysByKey,
} from 'reducers/settings'
import type { TimeRange } from 'reducers/settings'
import { reorderAccounts } from 'actions/accounts'
import { saveSettings } from 'actions/settings'
import TrackPage from 'analytics/TrackPage'
import RefreshAccountsOrdering from 'components/RefreshAccountsOrdering'
import UpdateNotifier from 'components/UpdateNotifier'
import BalanceInfos from 'components/BalanceSummary/BalanceInfos'
import BalanceSummary from 'components/BalanceSummary'
import Box from 'components/base/Box'
import { i } from 'helpers/staticPath'
import PillsDaysCount from 'components/PillsDaysCount'
import Text from 'components/base/Text'
import OperationsList from 'components/OperationsList'
import StickyBackToTop from 'components/StickyBackToTop'
import Button from 'components/base/Button'
import AccountCard from './AccountCard'
import AccountsOrder from './AccountsOrder'
import EmptyState from './EmptyState'
import CurrentGreetings from './CurrentGreetings'
import SummaryDesc from './SummaryDesc'
import AccountCardList from './AccountCardList'
const mapStateToProps = createStructuredSelector({
accounts: accountsSelector,
counterValue: counterValueCurrencySelector,
locale: localeSelector,
selectedTimeRange: selectedTimeRangeSelector,
})
const mapDispatchToProps = {
push,
reorderAccounts,
saveSettings,
openModal,
}
type Props = {
@ -63,45 +54,38 @@ type Props = {
counterValue: Currency,
selectedTimeRange: TimeRange,
saveSettings: ({ selectedTimeRange: TimeRange }) => *,
openModal: string => void,
}
class DashboardPage extends PureComponent<Props> {
onAccountClick = account => this.props.push(`/account/${account.id}`)
handleGreeting = () => {
const localTimeHour = new Date().getHours()
const afternoon_breakpoint = 12
const evening_breakpoint = 17
if (localTimeHour >= afternoon_breakpoint && localTimeHour < evening_breakpoint) {
return 'app:dashboard.greeting.afternoon'
} else if (localTimeHour >= evening_breakpoint) {
return 'app:dashboard.greeting.evening'
}
return 'app:dashboard.greeting.morning'
}
handleChangeSelectedTime = item => {
this.props.saveSettings({ selectedTimeRange: item.key })
}
_cacheBalance = null
renderHeader = ({ isAvailable, totalBalance, selectedTimeRange, sinceBalance, refBalance }) => (
<BalanceInfos
t={this.props.t}
counterValue={this.props.counterValue}
isAvailable={isAvailable}
totalBalance={totalBalance}
since={selectedTimeRange}
sinceBalance={sinceBalance}
refBalance={refBalance}
/>
)
render() {
const { accounts, t, counterValue, selectedTimeRange, openModal } = this.props
const { accounts, t, counterValue, selectedTimeRange } = this.props
const daysCount = timeRangeDaysByKey[selectedTimeRange]
const timeFrame = this.handleGreeting()
const imagePath = i('empty-account-tile.svg')
const totalAccounts = accounts.length
const totalCurrencies = uniq(accounts.map(a => a.currency.id)).length
const totalOperations = accounts.reduce((sum, a) => sum + a.operations.length, 0)
const displayOperationsHelper = (account: Account) => account.operations.length > 0
const displayOperations = accounts.some(displayOperationsHelper)
return (
<Fragment>
<UpdateNotifier />
<RefreshAccountsOrdering onMount />
<TrackPage
category="Portfolio"
totalAccounts={totalAccounts}
@ -113,12 +97,8 @@ class DashboardPage extends PureComponent<Props> {
<Fragment>
<Box horizontal alignItems="flex-end">
<Box grow>
<Text color="dark" ff="Museo Sans" fontSize={7}>
{t(timeFrame)}
</Text>
<Text color="grey" fontSize={5} ff="Museo Sans|Light">
{t('app:dashboard.summary', { count: totalAccounts })}
</Text>
<CurrentGreetings />
<SummaryDesc totalAccounts={totalAccounts} />
</Box>
<Box>
<PillsDaysCount
@ -127,105 +107,33 @@ class DashboardPage extends PureComponent<Props> {
/>
</Box>
</Box>
<Fragment>
<BalanceSummary
counterValue={counterValue}
chartId="dashboard-chart"
chartColor={colors.wallet}
<BalanceSummary
counterValue={counterValue}
chartId="dashboard-chart"
chartColor={colors.wallet}
accounts={accounts}
selectedTimeRange={selectedTimeRange}
daysCount={daysCount}
renderHeader={this.renderHeader}
/>
<AccountCardList
onAccountClick={this.onAccountClick}
accounts={accounts}
daysCount={daysCount}
counterValue={counterValue}
/>
{totalOperations > 0 && (
<OperationsList
onAccountClick={this.onAccountClick}
accounts={accounts}
selectedTimeRange={selectedTimeRange}
daysCount={daysCount}
renderHeader={({
isAvailable,
totalBalance,
selectedTimeRange,
sinceBalance,
refBalance,
}) => (
<BalanceInfos
t={t}
counterValue={counterValue}
isAvailable={isAvailable}
totalBalance={totalBalance}
since={selectedTimeRange}
sinceBalance={sinceBalance}
refBalance={refBalance}
/>
)}
title={t('app:dashboard.recentActivity')}
withAccount
/>
<Box flow={4}>
<Box horizontal alignItems="flex-end">
<Text color="dark" ff="Museo Sans" fontSize={6}>
{t('app:dashboard.accounts.title', { count: accounts.length })}
</Text>
<Box ml="auto" horizontal flow={1}>
<AccountsOrder />
</Box>
</Box>
<Box
horizontal
flexWrap="wrap"
justifyContent="flex-start"
alignItems="center"
style={{ margin: '0 -16px' }}
>
{accounts
.concat(
Array(3 - (accounts.length % 3))
.fill(null)
.map((_, i) => i === 0),
)
.map((account, i) => (
<Box
key={typeof account === 'object' ? account.id : `placeholder_${i}`}
flex="33%"
p={16}
>
{account ? (
typeof account === 'object' ? (
<AccountCard
key={account.id}
counterValue={counterValue}
account={account}
daysCount={daysCount}
onClick={this.onAccountClick}
/>
) : (
<Wrapper>
<Box mt={2}>
<img alt="" src={imagePath} />
</Box>
<Box
ff="Open Sans"
fontSize={3}
color="grey"
pb={2}
mt={3}
textAlign="center"
style={{ maxWidth: 150 }}
>
{t('app:dashboard.emptyAccountTile.desc')}
</Box>
<Button primary onClick={() => openModal(MODAL_ADD_ACCOUNTS)}>
{t('app:dashboard.emptyAccountTile.createAccount')}
</Button>
</Wrapper>
)
) : null}
</Box>
))}
</Box>
</Box>
{displayOperations && (
<OperationsList
onAccountClick={this.onAccountClick}
accounts={accounts}
title={t('app:dashboard.recentActivity')}
withAccount
/>
)}
<StickyBackToTop />
</Fragment>
)}
<StickyBackToTop />
</Fragment>
) : (
<EmptyState />
@ -243,13 +151,3 @@ export default compose(
),
translate(),
)(DashboardPage)
const Wrapper = styled(Box).attrs({
p: 4,
flex: 1,
alignItems: 'center',
})`
border: 1px dashed ${p => p.theme.colors.fog};
border-radius: 4px;
height: 215px;
`

51
src/components/DebugAppInfosForCurrency.js

@ -0,0 +1,51 @@
// @flow
import { Component } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { getCurrentDevice } from 'reducers/devices'
import debugAppInfosForCurrency from 'commands/debugAppInfosForCurrency'
class DebugAppInfosForCurrency extends Component<
{
children?: (?string) => React$Node,
currencyId: string,
device: *,
},
{
version: ?string,
},
> {
state = {
version: null,
}
componentDidMount() {
const { device, currencyId } = this.props
if (device) {
debugAppInfosForCurrency
.send({ currencyId, devicePath: device.path })
.toPromise()
.then(
({ version }) => {
if (this.unmounted) return
this.setState({ version })
},
() => {},
)
}
}
componentWillUnmount() {
this.unmounted = true
}
unmounted = false
render() {
const { children } = this.props
const { version } = this.state
return children ? children(version) : null
}
}
export default connect(
createStructuredSelector({
device: getCurrentDevice,
}),
)(DebugAppInfosForCurrency)

13
src/components/DeltaChange.js

@ -1,14 +1,21 @@
// @flow
import React, { PureComponent } from 'react'
import { BigNumber } from 'bignumber.js'
import FormattedVal from 'components/base/FormattedVal'
class DeltaChange extends PureComponent<{
from: number,
to: number,
from: BigNumber,
to: BigNumber,
}> {
render() {
const { from, to, ...rest } = this.props
const val = from ? Math.floor(((to - from) / from) * 100) : 0
const val = !from.isZero()
? to
.minus(from)
.div(from)
.times(100)
.integerValue()
: BigNumber(0)
// TODO in future, we also want to diverge rendering when the % is way too high (this can easily happen)
return <FormattedVal isPercent val={val} {...rest} />
}

68
src/components/DeviceInteraction/components.js

@ -2,8 +2,13 @@
import React from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { radii } from 'styles/theme'
import { openURL } from 'helpers/linking'
import { urls } from 'config/urls'
import TranslatedError from 'components/TranslatedError'
import Box from 'components/base/Box'
@ -112,30 +117,41 @@ export const ErrorContainer = ({ isVisible }: { isVisible: boolean }) => (
</ErrorContainerWrapper>
)
export const ErrorDescContainer = ({
error,
onRetry,
...p
}: {
error: Error,
onRetry: void => void,
}) => (
<Box
horizontal
fontSize={3}
color="alertRed"
align="center"
cursor="text"
ff="Open Sans|SemiBold"
style={{ maxWidth: 500 }}
{...p}
>
<IconExclamationCircle size={16} />
<Box ml={2} mr={1} shrink grow style={{ maxWidth: 300 }}>
<TranslatedError error={error} />
</Box>
<FakeLink ml="auto" underline color="alertRed" onClick={onRetry}>
{'Retry'}
</FakeLink>
</Box>
export const ErrorDescContainer = translate()(
({ error, onRetry, t, ...p }: { error: Error, onRetry: void => void, t: T }) => {
const errorHelpURL = urls.errors[error.name] || null
const errorDesc = <TranslatedError error={error} field="description" />
return (
<Box
horizontal
fontSize={3}
color="alertRed"
align="flex-start"
cursor="text"
ff="Open Sans|SemiBold"
style={{ maxWidth: 500 }}
{...p}
>
<IconExclamationCircle size={16} />
<Box ml={2} mr={1} shrink grow style={{ maxWidth: 300 }}>
<TranslatedError error={error} />
{!!errorDesc && (
<Box ff="Open Sans|Regular" mt={1}>
{errorDesc}
</Box>
)}
</Box>
<Box ml="auto" horizontal flow={2}>
{!!errorHelpURL && (
<FakeLink underline color="alertRed" onClick={() => openURL(errorHelpURL)}>
{t('app:common.help')}
</FakeLink>
)}
<FakeLink underline color="alertRed" onClick={onRetry}>
{t('app:common.retry')}
</FakeLink>
</Box>
</Box>
)
},
)

11
src/components/EnsureDeviceApp.js

@ -11,7 +11,7 @@ import logger from 'logger'
import getAddress from 'commands/getAddress'
import { createCancelablePolling } from 'helpers/promise'
import { standardDerivation } from 'helpers/derivations'
import { isSegwitAccount } from 'helpers/bip32'
import { isSegwitPath } from 'helpers/bip32'
import { BtcUnmatchedApp } from 'helpers/getAddressForCurrency/btc'
import DeviceInteraction from 'components/DeviceInteraction'
@ -21,11 +21,9 @@ import IconUsb from 'icons/Usb'
import type { Device } from 'types/common'
import { createCustomErrorClass } from 'helpers/errors'
import { WrongDeviceForAccount, CantOpenDevice } from 'config/errors'
import { getCurrentDevice } from 'reducers/devices'
export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount')
const usbIcon = <IconUsb size={16} />
const Bold = props => <Text ff="Open Sans|SemiBold" {...props} />
@ -66,7 +64,8 @@ class EnsureDeviceApp extends Component<{
shouldThrow: (err: Error) => {
const isWrongApp = err instanceof BtcUnmatchedApp
const isWrongDevice = err instanceof WrongDeviceForAccount
return isWrongApp || isWrongDevice
const isCantOpenDevice = err instanceof CantOpenDevice
return isWrongApp || isWrongDevice || isCantOpenDevice
},
},
)
@ -125,7 +124,7 @@ async function getAddressFromAccountOrCurrency(device, account, currency) {
path: account
? account.freshAddressPath
: standardDerivation({ currency, segwit: false, x: 0 }),
segwit: account ? isSegwitAccount(account) : false,
segwit: account ? isSegwitPath(account.freshAddressPath) : false,
})
.toPromise()
return address

4
src/components/ExchangePage/ExchangeCard.js

@ -26,14 +26,14 @@ export default class ExchangeCard extends PureComponent<{ t: T, card: CardType }
t,
} = this.props
return (
<Card horizontal py={5} px={6} style={{ cursor: 'pointer' }} onClick={this.onClick}>
<Card horizontal py={5} px={6}>
<Box justify="center" style={{ width: 200 }}>
{logo}
</Box>
<Box shrink ff="Open Sans|Regular" fontSize={4} flow={3}>
<Box>{t(`app:exchange.${id}`)}</Box>
<Box horizontal align="center" color="wallet" flow={1}>
<FakeLink>{t('app:exchange.visitWebsite')}</FakeLink>
<FakeLink onClick={this.onClick}>{t('app:exchange.visitWebsite')}</FakeLink>
<ExternalLinkIcon size={14} />
</Box>
</Box>

2
src/components/ExchangePage/index.js

@ -57,7 +57,7 @@ class ExchangePage extends PureComponent<Props> {
render() {
const { t } = this.props
return (
<Box pb={6}>
<Box pb={6} selectable>
<TrackPage category="Exchange" />
<Box ff="Museo Sans|Regular" fontSize={7} color="dark">
{t('app:exchange.title')}

35
src/components/FeesField/BitcoinKind.js

@ -1,6 +1,7 @@
// @flow
import React, { Component } from 'react'
import { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types'
import styled from 'styled-components'
import { translate } from 'react-i18next'
@ -16,16 +17,16 @@ import Box from '../base/Box'
type Props = {
account: Account,
feePerByte: number,
onChange: number => void,
feePerByte: BigNumber,
onChange: BigNumber => void,
t: T,
}
type FeeItem = {
label: string,
value: string,
value: *,
blockCount: number,
feePerByte: number,
feePerByte: BigNumber,
}
const InputRight = styled(Box).attrs({
@ -47,12 +48,14 @@ const customItem = {
label: 'Custom',
value: 'custom',
blockCount: 0,
feePerByte: 0,
feePerByte: BigNumber(0),
}
type State = { isFocused: boolean, items: FeeItem[], selectedItem: FeeItem }
class FeesField extends Component<Props & { fees?: Fees, error?: Error }, State> {
type OwnProps = Props & { fees?: Fees, error?: Error }
class FeesField extends Component<OwnProps, State> {
state = {
items: [customItem],
selectedItem: customItem,
@ -64,9 +67,9 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, State>
let items: FeeItem[] = []
if (fees) {
for (const key of Object.keys(fees)) {
const feePerByte = Math.ceil(fees[key] / 1000)
const feePerByte = BigNumber(Math.ceil(fees[key] / 1000))
const blockCount = parseInt(key, 10)
if (!isNaN(blockCount) && !isNaN(feePerByte)) {
if (!isNaN(blockCount) && !feePerByte.isNaN()) {
items.push({
blockCount,
label: blockCountNameConvention[blockCount] || `${blockCount} blocks`,
@ -78,17 +81,16 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, State>
items = items.sort((a, b) => a.blockCount - b.blockCount)
}
items.push(customItem)
const selectedItem =
prevState.selectedItem.feePerByte === feePerByte
? prevState.selectedItem
: items.find(f => f.feePerByte === feePerByte) || items[items.length - 1]
const selectedItem = prevState.selectedItem.feePerByte.eq(feePerByte)
? prevState.selectedItem
: items.find(f => f.feePerByte.eq(feePerByte)) || items[items.length - 1]
return { items, selectedItem }
}
componentDidUpdate() {
componentDidUpdate({ fees: prevFees }: OwnProps) {
const { feePerByte, fees, onChange } = this.props
const { items, isFocused } = this.state
if (fees && !feePerByte && !isFocused) {
if (fees && fees !== prevFees && feePerByte.isZero() && !isFocused) {
// initialize with the median
const feePerByte = (items.find(item => item.blockCount === defaultBlockCount) || items[0])
.feePerByte
@ -103,11 +105,11 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, State>
onSelectChange = selectedItem => {
const { onChange } = this.props
const patch: $Shape<State> = { selectedItem }
if (selectedItem.feePerByte) {
if (!selectedItem.feePerByte.isZero()) {
onChange(selectedItem.feePerByte)
} else {
const { input } = this
if (!selectedItem.feePerByte && input.current) {
if (selectedItem.feePerByte.isZero() && input.current) {
patch.isFocused = true
input.current.select()
}
@ -140,6 +142,7 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, State>
{t('app:send.steps.amount.unitPerByte', { unit: satoshi.code })}
</InputRight>
}
allowZero
/>
</GenericContainer>
)

9
src/components/FeesField/EthereumKind.js

@ -1,6 +1,7 @@
// @flow
import React, { Component } from 'react'
import { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types'
import InputCurrency from 'components/base/InputCurrency'
@ -10,8 +11,8 @@ import GenericContainer from './GenericContainer'
type Props = {
account: Account,
gasPrice: number,
onChange: number => void,
gasPrice: BigNumber,
onChange: BigNumber => void,
}
class FeesField extends Component<Props & { fees?: Fees, error?: Error }, *> {
@ -21,8 +22,8 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, *> {
componentDidUpdate() {
const { gasPrice, fees, onChange } = this.props
const { isFocused } = this.state
if (!gasPrice && fees && fees.gas_price && !isFocused) {
onChange(fees.gas_price) // we want to set the default to gas_price
if (gasPrice.isZero() && fees && fees.gas_price && !isFocused) {
onChange(BigNumber(fees.gas_price)) // we want to set the default to gas_price
}
}
onChangeFocus = isFocused => {

7
src/components/FeesField/RippleKind.js

@ -1,6 +1,7 @@
// @flow
import React, { Component } from 'react'
import type { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { apiForEndpointConfig, parseAPIValue } from 'api/Ripple'
import InputCurrency from 'components/base/InputCurrency'
@ -8,8 +9,8 @@ import GenericContainer from './GenericContainer'
type Props = {
account: Account,
fee: number,
onChange: number => void,
fee: BigNumber,
onChange: BigNumber => void,
}
type State = {
@ -35,7 +36,7 @@ class FeesField extends Component<Props, State> {
const info = await api.getServerInfo()
if (syncId !== this.syncId) return
const serverFee = parseAPIValue(info.validatedLedger.baseFeeXRP)
if (!this.props.fee) {
if (this.props.fee.isZero()) {
this.props.onChange(serverFee)
}
} catch (error) {

22
src/components/GenuineCheck.js

@ -14,7 +14,7 @@ import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import { GENUINE_TIMEOUT, DEVICE_INFOS_TIMEOUT, GENUINE_CACHE_DELAY } from 'config/constants'
import { getCurrentDevice } from 'reducers/devices'
import { createCustomErrorClass } from 'helpers/errors'
import { CantOpenDevice, DeviceNotGenuineError, DeviceGenuineSocketEarlyClose } from 'config/errors'
import getDeviceInfo from 'commands/getDeviceInfo'
import getIsGenuine from 'commands/getIsGenuine'
@ -26,9 +26,6 @@ import IconUsb from 'icons/Usb'
import IconHome from 'icons/Home'
import IconCheck from 'icons/Check'
const DeviceNotGenuineError = createCustomErrorClass('DeviceNotGenuine')
const DeviceGenuineSocketEarlyClose = createCustomErrorClass('DeviceGenuineSocketEarlyClose')
type Props = {
t: T,
onFail?: Error => void,
@ -59,11 +56,18 @@ class GenuineCheck extends PureComponent<Props> {
})
checkDashboardInteractionHandler = ({ device }: { device: Device }) =>
createCancelablePolling(() =>
getDeviceInfo
.send({ devicePath: device.path })
.pipe(timeout(DEVICE_INFOS_TIMEOUT))
.toPromise(),
createCancelablePolling(
() =>
getDeviceInfo
.send({ devicePath: device.path })
.pipe(timeout(DEVICE_INFOS_TIMEOUT))
.toPromise(),
{
shouldThrow: (err: Error) => {
const isCantOpenDevice = err instanceof CantOpenDevice
return isCantOpenDevice
},
},
)
checkGenuineInteractionHandler = async ({

96
src/components/IsUnlocked.js

@ -1,6 +1,5 @@
// @flow
import bcrypt from 'bcryptjs'
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { compose } from 'redux'
@ -8,23 +7,27 @@ import { remote } from 'electron'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import type { SettingsState as Settings } from 'reducers/settings'
import type { T } from 'types/common'
import { i } from 'helpers/staticPath'
import IconTriangleWarning from 'icons/TriangleWarning'
import get from 'lodash/get'
import { setEncryptionKey } from 'helpers/db'
import db from 'helpers/db'
import hardReset from 'helpers/hardReset'
import { fetchAccounts } from 'actions/accounts'
import { isLocked, unlock } from 'reducers/application'
import { createCustomErrorClass } from 'helpers/errors'
import Box from 'components/base/Box'
import InputPassword from 'components/base/InputPassword'
import LedgerLiveLogo from 'components/base/LedgerLiveLogo'
import IconArrowRight from 'icons/ArrowRight'
import Button from './base/Button/index'
import ConfirmModal from './base/Modal/ConfirmModal'
const PasswordIncorrectError = createCustomErrorClass('PasswordIncorrect')
type InputValue = {
password: string,
}
@ -33,20 +36,18 @@ type Props = {
children: any,
fetchAccounts: Function,
isLocked: boolean,
settings: Settings,
t: T,
unlock: Function,
}
type State = {
inputValue: InputValue,
incorrectPassword: boolean,
incorrectPassword: ?Error,
isHardResetting: boolean,
isHardResetModalOpened: boolean,
}
const mapStateToProps = state => ({
isLocked: isLocked(state),
settings: state.settings,
})
const mapDispatchToProps: Object = {
@ -58,22 +59,18 @@ const defaultState = {
inputValue: {
password: '',
},
incorrectPassword: false,
incorrectPassword: null,
isHardResetting: false,
isHardResetModalOpened: false,
}
export const PageTitle = styled(Box).attrs({
width: 152,
height: 27,
ff: 'Museo Sans|Regular',
fontSize: 7,
color: 'dark',
})``
export const LockScreenDesc = styled(Box).attrs({
width: 340,
height: 36,
ff: 'Open Sans|Regular',
fontSize: 4,
textAlign: 'center',
@ -104,25 +101,27 @@ class IsUnlocked extends Component<Props, State> {
...prev.inputValue,
[key]: value,
},
incorrectPassword: false,
incorrectPassword: null,
}))
handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {
e.preventDefault()
const { settings, unlock, fetchAccounts } = this.props
const { unlock, fetchAccounts } = this.props
const { inputValue } = this.state
if (bcrypt.compareSync(inputValue.password, get(settings, 'password.value'))) {
setEncryptionKey('accounts', inputValue.password)
await fetchAccounts()
const isAccountsDecrypted = await db.hasBeenDecrypted('app', 'accounts')
try {
if (!isAccountsDecrypted) {
await db.setEncryptionKey('app', 'accounts', inputValue.password)
await fetchAccounts()
} else if (!db.isEncryptionKeyCorrect('app', 'accounts', inputValue.password)) {
throw new PasswordIncorrectError()
}
unlock()
this.setState({
...defaultState,
})
} else {
this.setState({ incorrectPassword: true })
this.setState(defaultState)
} catch (err) {
this.setState({ incorrectPassword: new PasswordIncorrectError() })
}
}
@ -152,33 +151,42 @@ class IsUnlocked extends Component<Props, State> {
<Box sticky alignItems="center" justifyContent="center">
<form onSubmit={this.handleSubmit}>
<Box align="center">
<div
style={{
padding: 14,
backgroundColor: 'white',
borderRadius: 80,
fontSize: 0,
marginBottom: 40,
boxShadow: '0 2px 23px 0 rgba(0, 0, 0, 0.08)',
}}
>
<img alt="" src={i('ledgerlive-logo.svg')} width={50} height={50} />
</div>
<LedgerLiveLogo
style={{ marginBottom: 40 }}
icon={
<img
src={i('ledgerlive-logo.svg')}
alt=""
draggable="false"
width={50}
height={50}
/>
}
/>
<PageTitle>{t('app:common.lockScreen.title')}</PageTitle>
<LockScreenDesc>
{t('app:common.lockScreen.subTitle')}
<br />
{t('app:common.lockScreen.description')}
</LockScreenDesc>
<Box style={{ minWidth: 230 }}>
<InputPassword
autoFocus
placeholder={t('app:common.lockScreen.inputPlaceholder')}
type="password"
onChange={this.handleChangeInput('password')}
value={inputValue.password}
error={incorrectPassword && t('app:password.errorMessageIncorrectPassword')}
/>
<Box horizontal align="center">
<Box style={{ width: 280 }}>
<InputPassword
autoFocus
placeholder={t('app:common.lockScreen.inputPlaceholder')}
type="password"
onChange={this.handleChangeInput('password')}
value={inputValue.password}
error={incorrectPassword}
/>
</Box>
<Box ml={2}>
<Button style={{ width: 38, height: 38 }} primary onClick={this.handleSubmit}>
<Box style={{ alignItems: 'center' }}>
<IconArrowRight size={16} />
</Box>
</Button>
</Box>
</Box>
<Button type="button" mt={3} small onClick={this.handleOpenHardResetModal}>
{t('app:common.lockScreen.lostPassword')}

1
src/components/MainSideBar/AddAccountButton.js

@ -11,7 +11,6 @@ import { rgba } from 'styles/helpers'
const PlusWrapper = styled(Tabbable).attrs({
p: 1,
cursor: 'pointer',
borderRadius: 1,
})`
color: ${p => p.theme.colors.smoke};

22
src/components/ManagerPage/AppSearchBar.js

@ -1,8 +1,9 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
import type { LedgerScriptParams } from 'helpers/common'
import type { LedgerScriptParams } from 'helpers/types'
import Box from 'components/base/Box'
import Space from 'components/base/Space'
@ -12,6 +13,15 @@ import Search from 'components/base/Search'
import SearchIcon from 'icons/Search'
import CrossIcon from 'icons/Cross'
const CrossContainer = styled(Box).attrs({
justify: 'center',
px: 3,
})`
&:hover {
color: ${p => p.theme.colors.dark};
}
`
type Props = {
list: Array<LedgerScriptParams>,
children: (list: Array<LedgerScriptParams>) => React$Node,
@ -43,7 +53,7 @@ class AppSearchBar extends PureComponent<Props, State> {
const { children, list } = this.props
const { query, focused } = this.state
const color = focused ? 'black' : 'grey'
const color = focused ? 'dark' : 'grey'
return (
<Fragment>
@ -56,15 +66,15 @@ class AppSearchBar extends PureComponent<Props, State> {
onBlur={this.handleFocus(false)}
placeholder={'Search app'}
renderLeft={
<Box pl={3} justify="center">
<SearchIcon size={16} style={{ color }} />
<Box pl={3} justify="center" color={color}>
<SearchIcon size={16} />
</Box>
}
renderRight={
query ? (
<Box justify="center" cursor="pointer" onClick={this.reset} px={3}>
<CrossContainer justify="center" cursor="default" onClick={this.reset} px={3}>
<CrossIcon size={16} />
</Box>
</CrossContainer>
) : null
}
/>

10
src/components/ManagerPage/AppsList.js

@ -7,7 +7,7 @@ import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { compose } from 'redux'
import type { Device, T } from 'types/common'
import type { LedgerScriptParams } from 'helpers/common'
import type { LedgerScriptParams } from 'helpers/types'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import { developerModeSelector } from 'reducers/settings'
@ -202,7 +202,7 @@ class AppsList extends PureComponent<Props, State> {
error={error && error.name}
app={app}
/>
<ModalContent grow align="center" justify="center" mt={3}>
<ModalContent grow align="center" justify="center" mt={5}>
<Box color="alertRed">
<ExclamationCircleThin size={44} />
</Box>
@ -218,8 +218,8 @@ class AppsList extends PureComponent<Props, State> {
</Box>
<Box
color="graphite"
mt={4}
fontSize={6}
mt={2}
fontSize={4}
ff="Open Sans"
textAlign="center"
style={{ maxWidth: 350 }}
@ -235,7 +235,7 @@ class AppsList extends PureComponent<Props, State> {
</Fragment>
) : status === 'success' ? (
<Fragment>
<ModalContent grow align="center" justify="center" mt={3}>
<ModalContent grow align="center" justify="center" mt={5}>
<Box color="positiveGreen">
<CheckCircle size={44} />
</Box>

17
src/components/ManagerPage/Dashboard.js

@ -23,7 +23,7 @@ type Props = {
}
const Dashboard = ({ device, deviceInfo, t, handleHelpRequest }: Props) => (
<Box flow={4} pb={8}>
<Box flow={4} pb={8} selectable>
<TrackPage category="Manager" name="Dashboard" />
<Box>
<Text ff="Museo Sans|Regular" fontSize={7} color="dark">
@ -33,12 +33,10 @@ const Dashboard = ({ device, deviceInfo, t, handleHelpRequest }: Props) => (
<Text ff="Museo Sans|Light" fontSize={5}>
{t('app:manager.subtitle')}
</Text>
<ContainerToHover>
<FakeLink mr={1} underline color="grey" fontSize={4} onClick={handleHelpRequest}>
{t('app:common.needHelp')}
</FakeLink>
<HelpLink onClick={handleHelpRequest}>
<div style={{ textDecoration: 'underline' }}>{t('app:common.needHelp')}</div>
<IconExternalLink size={14} />
</ContainerToHover>
</HelpLink>
</Box>
</Box>
<Box mt={5}>
@ -54,12 +52,15 @@ const Dashboard = ({ device, deviceInfo, t, handleHelpRequest }: Props) => (
export default translate()(Dashboard)
const ContainerToHover = styled(Box).attrs({
const HelpLink = styled(FakeLink).attrs({
align: 'center',
ml: 'auto',
horizontal: true,
flow: 1,
color: 'grey',
fontSize: 4,
})`
${FakeLink}:hover, &:hover {
&:hover {
color: ${p => p.theme.colors.wallet};
}
`

2
src/components/ManagerPage/FirmwareUpdate.js

@ -9,7 +9,7 @@ import invariant from 'invariant'
import type { Device, T } from 'types/common'
import type { LedgerScriptParams } from 'helpers/common'
import type { LedgerScriptParams } from 'helpers/types'
import type { StepId } from 'components/modals/UpdateFirmware'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'

4
src/components/ManagerPage/ManagerGenuineCheck.js

@ -22,7 +22,7 @@ class ManagerGenuineCheck extends PureComponent<Props> {
render() {
const { t, onSuccess } = this.props
return (
<Box align="center" py={7}>
<Box align="center" py={7} selectable>
<TrackPage category="Manager" name="Genuine Check" />
<Box align="center" style={{ maxWidth: 460 }}>
<img
@ -38,7 +38,7 @@ class ManagerGenuineCheck extends PureComponent<Props> {
</Text>
</Box>
<Space of={40} />
<GenuineCheck shouldRenderRetry onSuccess={onSuccess} />
<GenuineCheck shouldRenderRetry onSuccess={onSuccess} style={{ width: 400 }} />
</Box>
)
}

2
src/components/ManagerPage/UpdateFirmwareButton.js

@ -4,8 +4,6 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common'
// import { EXPERIMENTAL_FIRMWARE_UPDATE } from 'config/constants'
import Button from 'components/base/Button'
import Text from 'components/base/Text'
import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate'

14
src/components/Onboarding/helperComponents.js

@ -52,20 +52,6 @@ export const OnboardingFooterWrapper = styled(Box).attrs({
border-bottom-left-radius: ${radii[1]}px;
border-bottom-right-radius: ${radii[1]}px;
`
// LIVE LOGO
export function LiveLogo({ icon, ...p }: { icon: any }) {
return <LiveLogoContainer {...p}>{icon}</LiveLogoContainer>
}
export const LiveLogoContainer = styled(Box).attrs({
borderRadius: '50%',
alignItems: 'center',
justifyContent: 'center',
})`
background-color: white;
box-shadow: 0 2px 24px 0 #00000014;
width: ${p => (p.width ? p.width : 80)}
height: ${p => (p.height ? p.height : 80)}
`
// INSTRUCTION LIST
type StepType = {

2
src/components/Onboarding/index.js

@ -110,7 +110,6 @@ const CloseContainer = styled(Box).attrs({
p: 4,
color: 'fog',
})`
cursor: pointer;
position: absolute;
top: 0;
right: 0;
@ -202,6 +201,7 @@ class Onboarding extends PureComponent<Props> {
const Container = styled(Box).attrs({
bg: 'white',
p: 60,
selectable: true,
})`
position: fixed;
top: 0;

31
src/components/Onboarding/steps/Finish.js

@ -9,6 +9,7 @@ import { urls } from 'config/urls'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import ConfettiParty from 'components/ConfettiParty'
import LedgerLiveLogo from 'components/base/LedgerLiveLogo'
import TrackPage from 'analytics/TrackPage'
import IconCheckFull from 'icons/CheckFull'
@ -16,8 +17,10 @@ import IconSocialTwitter from 'icons/Twitter'
import IconSocialReddit from 'icons/Reddit'
import IconSocialGithub from 'icons/Github'
import { lighten } from 'styles/helpers'
import type { StepProps } from '..'
import { Title, Description, LiveLogo } from '../helperComponents'
import { Title, Description } from '../helperComponents'
const ConfettiLayer = styled.div`
position: absolute;
@ -81,13 +84,14 @@ export default class Finish extends Component<StepProps, *> {
onMouseUp={this.onMouseUp}
onMouseLeave={this.onMouseLeave}
>
<LiveLogo
style={{ width: 64, height: 64 }}
<LedgerLiveLogo
width="64px"
height="64px"
icon={
<img
draggable="false"
alt=""
src={i('ledgerlive-logo.svg')}
alt=""
draggable="false"
width={40}
height={40}
/>
@ -122,18 +126,19 @@ type SocMed = {
onClick: string => void,
}
const StyledBox = styled(Box)`
cursor: default; // this here needs reset because it inherits from cursor: text from parent
&:hover {
color: ${p => lighten(p.theme.colors.grey, 0.1)};
}
`
export function SocialMediaBox({ socMed }: { socMed: SocMed }) {
const { icon, url, onClick } = socMed
return (
<Box
horizontal
style={{
cursor: 'pointer',
}}
onClick={() => onClick(url)}
>
<StyledBox horizontal onClick={() => onClick(url)}>
{icon}
</Box>
</StyledBox>
)
}

14
src/components/Onboarding/steps/GenuineCheck/GenuineCheckErrorPage.js

@ -5,9 +5,11 @@ import { i } from 'helpers/staticPath'
import type { T } from 'types/common'
import type { OnboardingState } from 'reducers/onboarding'
import { urls } from 'config/urls'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import ExternalLinkButton from 'components/base/ExternalLinkButton'
import TrackPage from 'analytics/TrackPage'
import { Title, Description, OnboardingFooterWrapper } from '../../helperComponents'
@ -15,7 +17,6 @@ import { Title, Description, OnboardingFooterWrapper } from '../../helperCompone
type Props = {
t: T,
redoGenuineCheck: () => void,
contactSupport: () => void,
onboarding: OnboardingState,
}
@ -69,7 +70,7 @@ class GenuineCheckErrorPage extends PureComponent<Props, *> {
}
render() {
const { redoGenuineCheck, contactSupport, t } = this.props
const { redoGenuineCheck, t } = this.props
return (
<Box sticky pt={50}>
<Box grow alignItems="center" justifyContent="center">
@ -79,9 +80,12 @@ class GenuineCheckErrorPage extends PureComponent<Props, *> {
<Button outlineGrey onClick={() => redoGenuineCheck()}>
{t('app:common.back')}
</Button>
<Button danger onClick={() => contactSupport()} ml="auto">
{t('onboarding:genuineCheck.buttons.contactSupport')}
</Button>
<ExternalLinkButton
danger
ml="auto"
label={t('onboarding:genuineCheck.buttons.contactSupport')}
url={urls.contactSupport}
/>
</OnboardingFooterWrapper>
</Box>
)

13
src/components/Onboarding/steps/GenuineCheck/index.js

@ -1,11 +1,9 @@
// @flow
import React, { PureComponent } from 'react'
import { openURL } from 'helpers/linking'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { colors } from 'styles/theme'
import { urls } from 'config/urls'
import { updateGenuineCheck } from 'reducers/onboarding'
@ -143,10 +141,6 @@ class GenuineCheck extends PureComponent<StepProps, State> {
})
}
contactSupport = () => {
openURL(urls.contactSupport)
}
handlePrevStep = () => {
const { prevStep, onboarding, jumpStep } = this.props
onboarding.flowType === 'initializedDevice' ? jumpStep('selectDevice') : prevStep()
@ -158,7 +152,6 @@ class GenuineCheck extends PureComponent<StepProps, State> {
renderGenuineFail = () => (
<GenuineCheckErrorPage
redoGenuineCheck={this.redoGenuineCheck}
contactSupport={this.contactSupport}
t={this.props.t}
onboarding={this.props.onboarding}
/>
@ -185,11 +178,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
{onboarding.flowType === 'restoreDevice' ? (
<Description>{t('onboarding:genuineCheck.descRestore')}</Description>
) : (
<Description>
{onboarding.isLedgerNano
? t('onboarding:genuineCheck.descNano')
: t('onboarding:genuineCheck.descBlue')}
</Description>
<Description>{t('onboarding:genuineCheck.descGeneric')}</Description>
)}
<Box mt={5}>
<GenuineCheckCardWrapper>

15
src/components/Onboarding/steps/Init.js

@ -8,6 +8,7 @@ import styled from 'styled-components'
import { flowType } from 'reducers/onboarding'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import LedgerLiveLogo from 'components/base/LedgerLiveLogo'
import TrackPage from 'analytics/TrackPage'
import IconPlus from 'icons/Plus'
import IconRecover from 'icons/Recover'
@ -15,7 +16,7 @@ import IconCheck from 'icons/Check'
import IconExternalLink from 'icons/ExternalLink'
import IconChevronRight from 'icons/ChevronRight'
import { i } from 'helpers/staticPath'
import { Title, LiveLogo } from '../helperComponents'
import { Title } from '../helperComponents'
import type { StepProps } from '..'
@ -56,7 +57,7 @@ class Init extends PureComponent<StepProps, *> {
{
key: 'noDevice',
icon: <IconExternalLink size={20} />,
title: t('onboarding:init.noDevice.title'),
title: t('onboarding:noDevice.title'),
onClick: () => {
jumpStep('noDevice')
flowType('noDevice')
@ -68,9 +69,12 @@ class Init extends PureComponent<StepProps, *> {
<GrowScroll full justifyContent="center" py={7}>
<TrackPage category="Onboarding" name="Init" />
<Box align="center">
<LiveLogo
style={{ width: 64, height: 64 }}
icon={<img src={i('ledgerlive-logo.svg')} alt="" width={40} height={40} />}
<LedgerLiveLogo
width="64px"
height="64px"
icon={
<img src={i('ledgerlive-logo.svg')} alt="" draggable="false" width={40} height={40} />
}
/>
<Box m={5} style={{ maxWidth: 480 }}>
<Title>{t('onboarding:init.title')}</Title>
@ -122,7 +126,6 @@ const InitCardContainer = styled(Box).attrs({
height: 70px;
transition: all ease-in-out 0.2s;
&:hover {
cursor: pointer;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.05);
}
`

18
src/components/Onboarding/steps/NoDevice.js

@ -6,13 +6,14 @@ import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import LedgerLiveLogo from 'components/base/LedgerLiveLogo'
import TrackPage from 'analytics/TrackPage'
import { urls } from 'config/urls'
import IconCart from 'icons/Cart'
import IconTruck from 'icons/Truck'
import IconInfoCircle from 'icons/InfoCircle'
import Button from '../../base/Button/index'
import { Title, OnboardingFooterWrapper, LiveLogo } from '../helperComponents'
import { Title, OnboardingFooterWrapper } from '../helperComponents'
import { OptionFlowCard } from './Init'
import type { StepProps } from '..'
@ -53,9 +54,18 @@ class NoDevice extends PureComponent<StepProps, *> {
<GrowScroll pb={7} pt={130}>
<TrackPage category="Onboarding" name="No Device" />
<Box grow alignItems="center">
<LiveLogo
style={{ width: 64, height: 64 }}
icon={<img src={i('ledgerlive-logo.svg')} alt="" width={40} height={40} />}
<LedgerLiveLogo
width="64px"
height="64px"
icon={
<img
src={i('ledgerlive-logo.svg')}
alt=""
draggable="false"
width={40}
height={40}
/>
}
/>
<Box m={5} style={{ maxWidth: 480 }}>
<Title>{t('onboarding:noDevice.title')}</Title>

2
src/components/Onboarding/steps/SelectDevice.js

@ -95,7 +95,7 @@ const DeviceContainer = styled(Box).attrs({
height: 204px;
border: ${props => `1px solid ${props.theme.colors[props.isActive ? 'wallet' : 'fog']}`};
&:hover {
cursor: pointer;
cursor: default; // this here needs reset because it inherits from cursor: text from parent
background: ${p => rgba(p.theme.colors.wallet, 0.04)};
}
`

41
src/components/Onboarding/steps/SetPassword.js

@ -1,10 +1,11 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import bcrypt from 'bcryptjs'
import { connect } from 'react-redux'
import { colors } from 'styles/theme'
import { setEncryptionKey } from 'helpers/db'
import db from 'helpers/db'
import { saveSettings } from 'actions/settings'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
@ -28,20 +29,26 @@ type State = {
currentPassword: string,
newPassword: string,
confirmPassword: string,
incorrectPassword: boolean,
}
const mapDispatchToProps = {
saveSettings,
}
const INITIAL_STATE = {
currentPassword: '',
newPassword: '',
confirmPassword: '',
incorrectPassword: false,
}
class SetPassword extends PureComponent<StepProps, State> {
type Props = StepProps & {
saveSettings: any => void,
}
class SetPassword extends PureComponent<Props, State> {
state = INITIAL_STATE
handleSave = (e: SyntheticEvent<HTMLFormElement>) => {
handleSave = async (e: SyntheticEvent<HTMLFormElement>) => {
if (e) {
e.preventDefault()
}
@ -49,19 +56,15 @@ class SetPassword extends PureComponent<StepProps, State> {
return
}
const { newPassword } = this.state
const { nextStep, savePassword } = this.props
const { nextStep, saveSettings } = this.props
setEncryptionKey('accounts', newPassword)
const hash = newPassword ? bcrypt.hashSync(newPassword, 8) : undefined
savePassword(hash)
await db.setEncryptionKey('app', 'accounts', newPassword)
saveSettings({ hasPassword: true })
this.handleReset()
nextStep()
}
handleInputChange = (key: string) => (value: string) => {
if (this.state.incorrectPassword) {
this.setState({ incorrectPassword: false })
}
this.setState({ [key]: value })
}
@ -74,9 +77,9 @@ class SetPassword extends PureComponent<StepProps, State> {
render() {
const { nextStep, prevStep, t, settings, onboarding } = this.props
const { newPassword, currentPassword, incorrectPassword, confirmPassword } = this.state
const { newPassword, currentPassword, confirmPassword } = this.state
const isPasswordEnabled = settings.password.isEnabled === true
const hasPassword = settings.hasPassword === true
const disclaimerNotes = [
{
@ -115,11 +118,10 @@ class SetPassword extends PureComponent<StepProps, State> {
<Box align="center" mt={2}>
<PasswordForm
onSubmit={this.handleSave}
isPasswordEnabled={isPasswordEnabled}
hasPassword={hasPassword}
newPassword={newPassword}
currentPassword={currentPassword}
confirmPassword={confirmPassword}
incorrectPassword={incorrectPassword}
isValid={this.isValid}
onChange={this.handleInputChange}
t={t}
@ -157,4 +159,7 @@ class SetPassword extends PureComponent<StepProps, State> {
}
}
export default SetPassword
export default connect(
null,
mapDispatchToProps,
)(SetPassword)

6
src/components/Onboarding/steps/Start.js

@ -6,9 +6,10 @@ import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import TrackPage from 'analytics/TrackPage'
import LedgerLiveLogo from 'components/base/LedgerLiveLogo'
import type { StepProps } from '..'
import { Title, LiveLogo } from '../helperComponents'
import { Title } from '../helperComponents'
export default (props: StepProps) => {
const { jumpStep, t } = props
@ -16,8 +17,7 @@ export default (props: StepProps) => {
<Box sticky justifyContent="center">
<TrackPage category="Onboarding" name="Start" />
<Box alignItems="center">
<LiveLogo
style={{ width: 80, height: 80 }}
<LedgerLiveLogo
icon={<img src={i('ledgerlive-logo.svg')} alt="" width={50} height={50} />}
/>
<Box my={5}>

5
src/components/OperationsList/AccountCell.js

@ -11,8 +11,9 @@ const Cell = styled(Box).attrs({
horizontal: true,
alignItems: 'center',
})`
width: 150px;
flex: 1;
overflow: hidden;
max-width: 400px;
`
type Props = {
@ -43,6 +44,8 @@ const AccountNameEllipsis = styled(Box).attrs({
color: 'dark',
flexShrink: 1,
})`
flex: 1;
min-width: 0;
display: block;
overflow: hidden;
text-overflow: ellipsis;

2
src/components/OperationsList/AddressCell.js

@ -29,7 +29,7 @@ const Address = ({ value }: { value: string }) => {
const AddressEllipsis = styled.div`
display: block;
flex-shrink: 1;
min-width: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

2
src/components/OperationsList/AmountCell.js

@ -34,7 +34,7 @@ class AmountCell extends PureComponent<Props> {
showCode
fontSize={4}
alwaysShowSign
color={amount < 0 ? 'smoke' : undefined}
color={amount.isNegative() ? 'smoke' : undefined}
/>
<CounterValue
color="grey"

1
src/components/OperationsList/Operation.js

@ -17,7 +17,6 @@ const OperationRow = styled(Box).attrs({
horizontal: true,
alignItems: 'center',
})`
cursor: pointer;
border-bottom: 1px solid ${p => p.theme.colors.lightGrey};
height: 68px;
opacity: ${p => (p.isOptimistic ? 0.5 : 1)};

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save