Browse Source

Merge branch 'develop' into auto-lock

gre-patch-1
Juan Cortés Ross 6 years ago
committed by GitHub
parent
commit
36871534dc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      .circleci/config.yml
  2. 1
      .eslintrc
  3. 2
      .github/ISSUE_TEMPLATE/feature_request.md
  4. 3
      .gitignore
  5. 1
      .prettierignore
  6. 82
      babel.config.js
  7. 58
      package.json
  8. 50
      scripts/check-wordings.js
  9. 9
      scripts/cli/cli.sh
  10. 21
      scripts/cli/getDevice.js
  11. 104
      scripts/cli/txBetweenAccounts.js
  12. 85
      scripts/create-draft-release.js
  13. 17
      scripts/download-xliffs.sh
  14. 8
      scripts/helpers/display-env.sh
  15. 49
      scripts/patch-appimage.sh
  16. 78
      scripts/release.sh
  17. 5
      scripts/shasums/patch-appimage-sums.txt
  18. 61
      scripts/upload-github-release-asset.sh
  19. 2
      src/__mocks__/storybook-state.js
  20. 2
      src/actions/general.js
  21. 5
      src/actions/settings.js
  22. 2
      src/api/Ripple.js
  23. 4
      src/api/network.js
  24. 9
      src/bridge/BridgeSyncContext.js
  25. 103
      src/bridge/EthereumJSBridge.js
  26. 78
      src/bridge/LibcoreBridge.js
  27. 150
      src/bridge/RippleJSBridge.js
  28. 4
      src/bridge/UnsupportedBridge.js
  29. 1
      src/bridge/index.js
  30. 12
      src/bridge/makeMockBridge.js
  31. 7
      src/bridge/types.js
  32. 2
      src/commands/debugAppInfosForCurrency.js
  33. 2
      src/commands/getAddress.js
  34. 9
      src/commands/index.js
  35. 18
      src/commands/killInternalProcess.js
  36. 39
      src/commands/libcoreGetFees.js
  37. 19
      src/commands/libcoreHardReset.js
  38. 29
      src/commands/libcoreScanFromXPUB.js
  39. 123
      src/commands/libcoreSignAndBroadcast.js
  40. 20
      src/commands/libcoreSyncAccount.js
  41. 8
      src/components/AccountPage/AccountHeaderActions.js
  42. 6
      src/components/AccountPage/EmptyStateAccount.js
  43. 4
      src/components/AccountPage/index.js
  44. 8
      src/components/AdvancedOptions/BitcoinKind.js
  45. 4
      src/components/AdvancedOptions/EthereumKind.js
  46. 53
      src/components/AdvancedOptions/RippleKind.js
  47. 14
      src/components/BalanceSummary/BalanceInfos.js
  48. 2
      src/components/BalanceSummary/index.js
  49. 2
      src/components/BalanceSummary/stories.js
  50. 2
      src/components/CalculateBalance.js
  51. 2
      src/components/CounterValue/stories.js
  52. 181
      src/components/CurrenciesStatusBanner.js
  53. 68
      src/components/CurrencyDownStatusAlert.js
  54. 57
      src/components/CurrentAddress/index.js
  55. 2
      src/components/DashboardPage/AccountCard/index.js
  56. 1
      src/components/DashboardPage/AccountCardList.js
  57. 4
      src/components/DashboardPage/AccountCardListHeader.js
  58. 6
      src/components/DashboardPage/AccountCardPlaceholder.js
  59. 8
      src/components/DashboardPage/AccountsOrder.js
  60. 8
      src/components/DashboardPage/CurrentGreetings.js
  61. 8
      src/components/DashboardPage/EmptyState.js
  62. 9
      src/components/DashboardPage/SummaryDesc.js
  63. 2
      src/components/DashboardPage/index.js
  64. 287
      src/components/DevToolsPage/AccountImporter.js
  65. 11
      src/components/DevToolsPage/index.js
  66. 4
      src/components/DeviceInteraction/components.js
  67. 21
      src/components/EnsureDeviceApp.js
  68. 4
      src/components/ExchangePage/ExchangeCard.js
  69. 46
      src/components/ExchangePage/index.js
  70. 2
      src/components/ExportLogsBtn.js
  71. 34
      src/components/FeesField/BitcoinKind.js
  72. 9
      src/components/FeesField/EthereumKind.js
  73. 2
      src/components/FeesField/GenericContainer.js
  74. 9
      src/components/FeesField/RippleKind.js
  75. 8
      src/components/GenuineCheck.js
  76. 2
      src/components/GenuineCheckModal.js
  77. 2
      src/components/GlobalSearch.js
  78. 18
      src/components/IsUnlocked.js
  79. 2
      src/components/MainSideBar/AddAccountButton.js
  80. 72
      src/components/MainSideBar/index.js
  81. 2
      src/components/ManagerPage/AppSearchBar.js
  82. 79
      src/components/ManagerPage/AppsList.js
  83. 6
      src/components/ManagerPage/Dashboard.js
  84. 8
      src/components/ManagerPage/FirmwareUpdate.js
  85. 26
      src/components/ManagerPage/ManagerApp.js
  86. 4
      src/components/ManagerPage/ManagerGenuineCheck.js
  87. 6
      src/components/ManagerPage/PlugYourDevice.js
  88. 4
      src/components/ManagerPage/UpdateFirmwareButton.js
  89. 7
      src/components/ManagerPage/index.js
  90. 4
      src/components/Onboarding/OnboardingFooter.js
  91. 13
      src/components/Onboarding/helperComponents.js
  92. 68
      src/components/Onboarding/steps/Analytics.js
  93. 6
      src/components/Onboarding/steps/Finish.js
  94. 16
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckErrorPage.js
  95. 8
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js
  96. 20
      src/components/Onboarding/steps/GenuineCheck/index.js
  97. 10
      src/components/Onboarding/steps/Init.js
  98. 10
      src/components/Onboarding/steps/NoDevice.js
  99. 6
      src/components/Onboarding/steps/SelectDevice.js
  100. 12
      src/components/Onboarding/steps/SelectPIN/SelectPINblue.js

13
.circleci/config.yml

@ -9,18 +9,25 @@ jobs:
build:
<<: *defaults
steps:
- run: sudo apt-get update
- run: sudo apt-get install -y libudev-dev
- run:
name: Install latest yarn
command: |
curl -sS http://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - ;
echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list ;
sudo apt-get update && sudo apt-get install yarn
sudo rm /usr/local/bin/yarn # remove docker yarn
- checkout
- restore_cache:
keys:
- v7-yarn-packages-{{ checksum "yarn.lock" }}
- v12-yarn-packages-{{ checksum "yarn.lock" }}
- run: yarn install
- save_cache:
key: v7-yarn-packages-{{ checksum "yarn.lock" }}
key: v12-yarn-packages-{{ checksum "yarn.lock" }}
paths:
- 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

@ -60,6 +60,7 @@
"react/jsx-no-literals": [1, {"noStrings": false}],
"react/prefer-stateless-function": 0,
"react/require-default-props": 0,
"react/no-multi-comp": 0,
"react/sort-comp": [1, {
order: [
'static-methods',

2
.github/ISSUE_TEMPLATE/feature_request.md

@ -1,6 +1,6 @@
---
name: ✨ Feature Request
about: Any feature you find missing in Ledger Live? Discuss to suggest feature requests.
about: Any feature you find missing in Ledger Live? Discuss to suggest feature requests. For crypto asset support, please read Issue 1650.
---
- [ ] I have checked this feature was not yet requested.

3
.gitignore

@ -1,3 +1,4 @@
xliffs/
/.env
/dist/
/flow-typed/
@ -10,3 +11,5 @@
/build/linux/arch/src
/build/linux/arch/*.tar.gz
/build/linux/arch/*.tar.xz
/test-e2e/sync/data/actual_app.json

1
.prettierignore

@ -1 +1,2 @@
package.json
test-e2e/**/*.json

82
babel.config.js

@ -1,32 +1,62 @@
const { NODE_ENV } = process.env
const { NODE_ENV, CLI } = process.env
const __TEST__ = NODE_ENV === 'test'
const __CLI__ = !!CLI
module.exports = () => ({
presets: [
[
require('@babel/preset-env'),
{
loose: true,
modules: __TEST__ ? 'commonjs' : false,
targets: {
electron: '1.8',
node: 'current',
module.exports = (api) => {
if (api) {
api.cache(true);
}
return {
presets: [
[
require('@babel/preset-env'),
{
loose: true,
modules: __TEST__ || __CLI__ ? 'commonjs' : false,
targets: {
electron: '1.8',
node: 'current',
},
},
},
],
require('@babel/preset-flow'),
require('@babel/preset-react'),
],
require('@babel/preset-flow'),
require('@babel/preset-react'),
require('@babel/preset-stage-0'),
],
plugins: [
[require('babel-plugin-module-resolver'), { root: ['src'] }],
[
require('babel-plugin-styled-components'),
{
displayName: true,
ssr: __TEST__,
},
plugins: [
[require('babel-plugin-module-resolver'), { root: ['src'] }],
[
require('babel-plugin-styled-components'),
{
displayName: true,
ssr: __TEST__,
},
],
// Stage 0
"@babel/plugin-proposal-function-bind",
// Stage 1
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-logical-assignment-operators",
["@babel/plugin-proposal-optional-chaining", { "loose": false }],
["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }],
["@babel/plugin-proposal-nullish-coalescing-operator", { "loose": false }],
"@babel/plugin-proposal-do-expressions",
// Stage 2
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
// Stage 3
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
["@babel/plugin-proposal-class-properties", { "loose": false }],
"@babel/plugin-proposal-json-strings"
],
],
})
}
}

58
package.json

@ -3,7 +3,7 @@
"productName": "Ledger Live",
"description": "Ledger Live - Desktop",
"repository": "https://github.com/LedgerHQ/ledger-live-desktop",
"version": "1.1.5",
"version": "1.2.6",
"author": "Ledger",
"license": "MIT",
"scripts": {
@ -13,10 +13,12 @@
"dist": "bash ./scripts/dist.sh",
"dist:dir": "bash ./scripts/dist-dir.sh",
"compile": "bash ./scripts/compile.sh",
"cli": "bash ./scripts/cli/cli.sh",
"lint": "eslint src webpack .storybook test-e2e",
"flow": "flow",
"test": "jest src",
"test-e2e": "jest test-e2e",
"test-sync": "bash test-e2e/sync/launch.sh",
"prettier": "prettier --write \"{src,webpack,.storybook,test-e2e}/**/*.{js,json}\"",
"ci": "yarn lint && yarn flow && yarn prettier && yarn test",
"storybook": "NODE_ENV=development STORYBOOK_ENV=1 start-storybook -s ./static -p 4444",
@ -33,13 +35,13 @@
}
},
"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.22.0",
"@ledgerhq/ledger-core": "2.0.0-rc.5",
"@ledgerhq/live-common": "3.0.0",
"@ledgerhq/hw-app-btc": "^4.27.0",
"@ledgerhq/hw-app-eth": "^4.24.0",
"@ledgerhq/hw-app-xrp": "^4.25.0",
"@ledgerhq/hw-transport": "^4.24.0",
"@ledgerhq/hw-transport-node-hid": "4.24.0",
"@ledgerhq/ledger-core": "2.0.0-rc.11",
"@ledgerhq/live-common": "4.4.2",
"animated": "^0.2.2",
"async": "^2.6.1",
"axios": "^0.18.0",
@ -62,16 +64,17 @@
"i18next": "^11.2.2",
"i18next-node-fs-backend": "^1.0.0",
"invariant": "^2.2.4",
"jsqr": "^1.1.1",
"lodash": "^4.17.5",
"lru-cache": "^4.1.3",
"measure-scrollbar": "^1.1.0",
"moment": "^2.22.2",
"qrcode": "^1.2.0",
"qrcode-reader": "^1.0.4",
"qrloop": "0.8.1",
"qs": "^6.5.1",
"raven": "^2.5.0",
"raven-js": "^3.24.2",
"react": "^16.4.1",
"react": "^16.6.1",
"react-dom": "^16.4.1",
"react-i18next": "^7.7.0",
"react-key-handler": "^1.0.1",
@ -113,12 +116,29 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "7.0.0-beta.42",
"@babel/polyfill": "7.0.0-beta.42",
"@babel/preset-env": "7.0.0-beta.42",
"@babel/preset-flow": "7.0.0-beta.42",
"@babel/preset-react": "7.0.0-beta.42",
"@babel/preset-stage-0": "7.0.0-beta.42",
"@babel/core": "7.1.2",
"@babel/plugin-proposal-class-properties": "7.1.0",
"@babel/plugin-proposal-decorators": "7.1.2",
"@babel/plugin-proposal-do-expressions": "7.0.0",
"@babel/plugin-proposal-export-default-from": "7.0.0",
"@babel/plugin-proposal-export-namespace-from": "7.0.0",
"@babel/plugin-proposal-function-bind": "7.0.0",
"@babel/plugin-proposal-function-sent": "7.1.0",
"@babel/plugin-proposal-json-strings": "7.0.0",
"@babel/plugin-proposal-logical-assignment-operators": "7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.0.0",
"@babel/plugin-proposal-numeric-separator": "7.0.0",
"@babel/plugin-proposal-optional-chaining": "7.0.0",
"@babel/plugin-proposal-pipeline-operator": "7.0.0",
"@babel/plugin-proposal-throw-expressions": "7.0.0",
"@babel/plugin-syntax-dynamic-import": "7.0.0",
"@babel/plugin-syntax-import-meta": "7.0.0",
"@babel/polyfill": "7.0.0",
"@babel/preset-env": "7.1.0",
"@babel/preset-flow": "7.0.0",
"@babel/preset-react": "7.0.0",
"@babel/register": "7.0.0",
"@octokit/rest": "^15.10.0",
"@storybook/addon-actions": "^3.4.7",
"@storybook/addon-knobs": "^3.4.7",
"@storybook/addon-links": "^3.4.7",
@ -135,7 +155,7 @@
"chance": "^1.0.13",
"concurrently": "3.5.1",
"dotenv": "^5.0.1",
"electron": "1.8.7",
"electron": "1.8.8",
"electron-builder": "20.14.7",
"electron-devtools-installer": "^2.2.3",
"electron-rebuild": "^1.7.3",
@ -165,5 +185,9 @@
"webpack-cli": "^2.0.14",
"yaml-loader": "^0.5.0"
},
"engines": {
"node": ">=8.9.0 <=8.12.0",
"yarn": "^1.10.1"
},
"private": true
}

50
scripts/check-wordings.js

@ -5,34 +5,34 @@ 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',
'operation.type.IN',
'operation.type.OUT',
'exchange.coinhouse',
'exchange.changelly',
'exchange.coinmama',
'exchange.simplex',
'exchange.paybis',
'addAccounts.accountToImportSubtitle_plural',
'dashboard.summary_plural',
'addAccounts.success_plural',
'addAccounts.successDescription_plural',
'time.since.day',
'time.since.week',
'time.since.month',
'time.since.year',
'time.day',
'time.week',
'time.month',
'time.year',
'addAccounts.cta.add_plural',
'manager.apps.installing',
'manager.apps.uninstalling',
'manager.apps.installSuccess',
'manager.apps.uninstallSuccess',
]
const WORDINGS = {
app: require('../static/i18n/en/app.json'),
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'),

9
scripts/cli/cli.sh

@ -0,0 +1,9 @@
#!/bin/bash
# TODO: os specific
export LEDGER_DATA_DIR="$HOME/.config/Electron"
export LEDGER_LOGS_DIRECTORY="$LEDGER_DATA_DIR/logs"
export LEDGER_LIVE_SQLITE_PATH="$LEDGER_DATA_DIR/sqlite"
export CLI=1
node -r @babel/register -r @babel/polyfill scripts/cli/txBetweenAccounts.js

21
scripts/cli/getDevice.js

@ -0,0 +1,21 @@
import CommNodeHid from '@ledgerhq/hw-transport-node-hid'
export default function getDevice() {
return new Promise((resolve, reject) => {
const sub = CommNodeHid.listen({
error: err => {
sub.unsubscribe()
reject(err)
},
next: async e => {
if (!e.device) {
return
}
if (e.type === 'add') {
sub.unsubscribe()
resolve(e.device)
}
},
})
})
}

104
scripts/cli/txBetweenAccounts.js

@ -0,0 +1,104 @@
/* eslint-disable no-console */
import chalk from 'chalk'
import path from 'path'
import fs from 'fs'
import inquirer from 'inquirer'
import { formatCurrencyUnit } from '@ledgerhq/live-common/lib/currencies'
import 'globals'
import withLibcore from 'helpers/withLibcore'
import accountModel from 'helpers/accountModel'
import { doSignAndBroadcast } from 'commands/libcoreSignAndBroadcast'
import getDevice from './getDevice'
async function main() {
try {
// GET ACCOUNTS
const app = await parseAppFile()
const accounts = app.accounts.map(accountModel.decode)
// GET SENDER ACCOUNT
const senderAccount = await chooseAccount(accounts, 'Choose sender account')
// GET RECIPIENT ACCOUNT
const recipientAccount = await chooseAccount(accounts, 'Choose recipient account')
// GET AMOUNT & FEE
const { amount, feePerByte } = await inquirer.prompt([
{
type: 'input',
name: 'amount',
message: 'Amount',
default: 0,
},
{
type: 'input',
name: 'feePerByte',
message: 'Fee per byte',
default: 0,
},
])
// GET DEVICE
console.log(chalk.blue(`Waiting for device...`))
const device = await getDevice()
console.log(chalk.blue(`Using device with path [${device.path}]`))
await withLibcore(async core =>
doSignAndBroadcast({
accountId: senderAccount.id,
currencyId: senderAccount.currency.id,
xpub: senderAccount.xpub,
freshAddress: senderAccount.freshAddress,
freshAddressPath: senderAccount.freshAddressPath,
index: senderAccount.index,
transaction: {
amount,
feePerByte,
recipient: recipientAccount.freshAddress,
},
deviceId: device.path,
core,
isCancelled: () => false,
onSigned: () => {
console.log(`>> signed`)
},
onOperationBroadcasted: operation => {
console.log(`>> broadcasted`, operation)
},
}),
)
} catch (err) {
console.log(`[ERROR]`, err)
}
}
async function parseAppFile() {
const appFilePath = path.resolve(process.env.LEDGER_DATA_DIR, 'app.json')
const appFileContent = fs.readFileSync(appFilePath, 'utf-8')
const parsedApp = JSON.parse(appFileContent)
return parsedApp.data
}
async function chooseAccount(accounts, msg) {
const { account } = await inquirer.prompt([
{
type: 'list',
choices: accounts.map(account => ({
name: `${account.name} | ${chalk.green(
formatCurrencyUnit(account.unit, account.balance, {
showCode: true,
}),
)}`,
value: account,
})),
name: 'account',
message: msg,
},
])
return account
}
main()

85
scripts/create-draft-release.js

@ -0,0 +1,85 @@
#!/usr/bin/env node
/* eslint-disable no-console */
const util = require('util')
const exec = util.promisify(require('child_process').exec)
const octokit = require('@octokit/rest')()
const repo = {
owner: 'LedgerHQ',
repo: 'ledger-live-desktop',
}
async function getTag() {
const { stdout } = await exec('git tag --points-at HEAD')
const tag = stdout.replace('\n', '')
if (!tag) {
throw new Error(`Unable to get current tag. Is your HEAD on a tagged commit?`)
}
return tag
}
async function checkDraft(tag) {
const { status, data } = await octokit.repos.getReleases(repo)
if (status !== 200) {
throw new Error(`Got HTTP status ${status} when trying to fetch releases list.`)
}
for (const release of data) {
if (release.tag_name === tag) {
if (release.draft) {
return true
}
throw new Error(`A release tagged ${tag} exists but is not a draft.`)
}
}
return false
}
async function createDraft(tag) {
const params = {
...repo,
tag_name: tag,
name: tag,
draft: true,
prerelease: true,
}
const { status } = await octokit.repos.createRelease(params)
if (status !== 201) {
throw new Error(`Got HTTP status ${status} when trying to create the release draft.`)
}
}
async function main() {
try {
const token = process.env.GH_TOKEN
const tag = await getTag()
octokit.authenticate({
type: 'token',
token,
})
const existingDraft = await checkDraft(tag)
if (!existingDraft) {
console.log(`No draft exists for ${tag}, creating...`)
createDraft(tag)
} else {
console.log(`A draft already exists for ${tag}, nothing to do.`)
}
} catch (e) {
console.error(e)
process.exit(1)
}
}
main()

17
scripts/download-xliffs.sh

@ -0,0 +1,17 @@
#!/bin/sh
if [ -z "$CROWDIN_TOKEN" ]; then
echo "CROWDIN_TOKEN env required" >&2
exit 1
fi
rm -rf xliffs
mkdir xliffs
cd xliffs
for lang in fr es-ES zh-CN ja ko ru; do
curl "https://api.crowdin.com/api/project/ledger-wallet/export-file?file=develop/static/i18n/en/app.json&language=$lang&format=xliff&key=$CROWDIN_TOKEN" > en-$lang.xliff
done
zip -r ledger-live-langs.zip *.xliff

8
scripts/helpers/display-env.sh

@ -7,9 +7,15 @@ if [ "$GIT_REVISION" == "" ]; then
GIT_REVISION=$(git rev-parse HEAD)
fi
if [[ $(uname) == 'Darwin' ]]; then
osVersion="$(sw_vers -productName) $(sw_vers -productVersion)"
else
osVersion="$(uname -srmo)"
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[1;30m%s\\e[1;0m\\n" "${osVersion}"
printf " │ \\e[2;1mcommit \\e[0;33m%s\\e[0;0m\\n" "$(git rev-parse HEAD)"
echo

49
scripts/patch-appimage.sh

@ -0,0 +1,49 @@
#!/bin/bash
# Patch .AppImage to address libcore crash on some
# distributions, due to loading system libraries
# instead of embedded ones.
#
# see https://github.com/LedgerHQ/ledger-live-desktop/issues/1010
set -e
BASE_URL=http://mirrors.kernel.org/ubuntu/pool/main/k/krb5
PACKAGE_SUFFIX=-2build1_amd64.deb
TMP_DIR=$(mktemp -d)
LEDGER_LIVE_VERSION=$(grep version package.json | sed -E 's/.*: "(.*)",/\1/g')
cp "dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage" "$TMP_DIR"
pushd "$TMP_DIR"
declare -a LIBRARIES=(
"libgssapi-krb5-2_1.16"
"libk5crypto3_1.16"
"libkrb5-3_1.16"
"libkrb5support0_1.16"
)
for PACKAGE in "${LIBRARIES[@]}"; do
curl -fOL "$BASE_URL/$PACKAGE$PACKAGE_SUFFIX"
ar p "$PACKAGE$PACKAGE_SUFFIX" data.tar.xz | tar xvJf >/dev/null - ./usr/lib/x86_64-linux-gnu/
rm "$PACKAGE$PACKAGE_SUFFIX"
done
curl -fOL "https://s3-eu-west-1.amazonaws.com/ledger-ledgerlive-resources-dev/public_resources/appimagetool-x86_64.AppImage"
cp "$OLDPWD/scripts/shasums/patch-appimage-sums.txt" .
sha512sum --quiet --check patch-appimage-sums.txt || exit 1
./ledger-live-desktop-"$LEDGER_LIVE_VERSION"-linux-x86_64.AppImage --appimage-extract
cp -a usr/lib/x86_64-linux-gnu/*.so.* squashfs-root/usr/lib
chmod +x appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage squashfs-root "$OLDPWD/dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage"
popd
MD5_SUM=$(sha512sum "dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage" | cut -f1 -d\ | xxd -r -p | base64 | paste -sd "")
sed -i "s|sha512: .*|sha512: ${MD5_SUM}|g" dist/latest-linux.yml
SIZE=$(stat --printf="%s" "dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage")
sed -i "s|size: .*|size: ${SIZE}|g" dist/latest-linux.yml

78
scripts/release.sh

@ -13,7 +13,9 @@ if [ "$(git rev-parse --abbrev-ref HEAD)" != "master" ]; then
exit 0
fi
if ! git describe --exact-match --tags 2>/dev/null >/dev/null; then
GH_TAG=$(git describe --exact-match --tags 2>/dev/null || echo '')
if [[ $GH_TAG == "" ]]; then
echo "You are not on a tag. Exiting properly. (CI)"
exit 0
fi
@ -25,6 +27,11 @@ fi
if [ ! -d "static/fonts/museosans" ]; then
if ! command -v aws ; then
if ! command -v apt ; then
echo "Museo Sans is missing, and I can't fetch it (no aws, no apt)" >&2
exit 1
fi
runJob "sudo apt install awscli" "installing aws cli..." "installed aws cli" "failed to install aws cli"
fi
@ -52,11 +59,68 @@ fi
# exit 1
# fi
if [[ $(uname) == 'Linux' ]]; then # only run it on one target, to prevent race conditions
runJob \
"node scripts/create-draft-release.js" \
"creating a draft release on GitHub (if needed)..." \
"draft release ready" \
"failed to create a draft release"
fi
runJob "yarn compile" "compiling..." "compiled" "failed to compile" "verbose"
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"
if [[ $(uname) == 'Linux' ]]; then
# --------------------------------------------------------------------
# Linux: Internal process error (null)
#
# context: https://github.com/LedgerHQ/ledger-live-desktop/issues/1010
# Linux: Internal process error (null)
#
# The "fix" is not optimal, as it doesn't really solve the problem
# (electron loading system openssl before we can load our embedded one)
# Quick summary:
#
# - build without publishing
# - unpack the .AppImage
# - download reported working libs from ubuntu mirrors, put it inside
# - re-pack the .AppImage
# - checksum stuff
# - upload to gh
runJob \
"DEBUG=electron-builder electron-builder build --publish never" \
"building and packaging app..." \
"app built and packaged successfully" \
"failed to build app" \
"verbose"
runJob \
"scripts/patch-appimage.sh" \
"patching AppImage..." \
"AppImage patched successfully" \
"failed to patch AppImage"
LEDGER_LIVE_VERSION=$(grep version package.json | sed -E 's/.*: "(.*)",/\1/g')
scripts/upload-github-release-asset.sh \
github_api_token="$GH_TOKEN" \
owner=LedgerHQ \
repo=ledger-live-desktop \
tag="$GH_TAG" \
filename="dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage"
scripts/upload-github-release-asset.sh \
github_api_token="$GH_TOKEN" \
owner=LedgerHQ \
repo=ledger-live-desktop \
tag="$GH_TAG" \
filename="dist/latest-linux.yml"
else
runJob \
"DEBUG=electron-builder electron-builder build --publish always" \
"building and packaging app..." \
"app built and packaged successfully" \
"failed to build app" \
"verbose"
fi

5
scripts/shasums/patch-appimage-sums.txt

@ -0,0 +1,5 @@
bebb42401a43971cfe3e31f2c9ee4efee352ce0d29a8ccc95ca1356a58463afd4876b133d9f4295697f96b76eb21b50c1909a073db753569e8969065eb40b306 usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2.2
d7d2b38a46d65a06560241b226f61d81c4df28d56c6841dd34bb428802ace0fc80cf94de1e5117f0b85b2c69b550df61ac999184d5cfe8ecd3bea4d8394d1d21 usr/lib/x86_64-linux-gnu/libkrb5.so.3.3
b025b755eb9a64f0d03a8e92c9e4b4f95c2c506bf070cf037841ef8cdb9013e16390d0e17330f2ce8c98c3b1f05b917a3018109acfde7aab50bc9d9fa70ea12b usr/lib/x86_64-linux-gnu/libk5crypto.so.3.1
f181e41f306819c10054ff8ceebf4943858f2cd34dea5206b51141877e2f651be3c6435bb02538cbde2cc0415f38e476423a9fd6a428ca9d425e9c662483b9af usr/lib/x86_64-linux-gnu/libkrb5support.so.0.1
dd8d81d4c1485209a65a1446225428a1b919478a74fd5698aff64cb8a67992544e62455f849ad73392505707cb94739de00af5ab340a22a87bb752c3808a55d2 appimagetool-x86_64.AppImage

61
scripts/upload-github-release-asset.sh

@ -0,0 +1,61 @@
#!/usr/bin/env bash
#
# Author: Stefan Buck
# License: MIT
# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447
#
#
# This script accepts the following parameters:
#
# * owner
# * repo
# * filename
# * github_api_token
#
# Script to upload a release asset using the GitHub API v3.
#
# Example:
#
# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground filename=./build.zip
#
# Check dependencies.
set -e
# Validate settings.
[ "$TRACE" ] && set -x
# shellcheck disable=SC2124
CONFIG=$@
for line in $CONFIG; do
eval "$line"
done
# Define variables.
GH_API="https://api.github.com"
# shellcheck disable=SC2154
GH_REPO="$GH_API/repos/$owner/$repo"
# shellcheck disable=SC2154
AUTH="Authorization: token $github_api_token"
# github_api_token=$GH_TOKEN owner=LedgerHQ repo=ledger-live-desktop tag=v1.2.2 filename=./dist/electron-builder-debug.yml
LATEST_RELEASE_ID=$(curl -sH "$AUTH" "$GH_API/repos/LedgerHQ/ledger-live-desktop/releases" | grep '"id":' | head -n 1 | sed -E 's/.*: (.*),/\1/')
# Validate token.
curl -o /dev/null -sH "$AUTH" "$GH_REPO" || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
# Get ID of the asset based on given filename.
# shellcheck disable=SC2154
[ "$LATEST_RELEASE_ID" ] || { echo "Error: Failed to get release id"; exit 1; }
# Upload asset
echo "Uploading asset... "
# Construct url
# shellcheck disable=SC2154
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$LATEST_RELEASE_ID/assets?name=$(basename "$filename")"
curl --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" "$GH_ASSET"

2
src/__mocks__/storybook-state.js

@ -2,7 +2,7 @@ import { genStoreState } from '@ledgerhq/live-common/lib/countervalues/mock'
import {
getCryptoCurrencyById,
getFiatCurrencyByTicker,
} from '@ledgerhq/live-common/lib/helpers/currencies'
} from '@ledgerhq/live-common/lib/currencies'
export default {
countervalues: genStoreState([

2
src/actions/general.js

@ -8,7 +8,7 @@ import {
getOrderAccounts,
} from 'reducers/settings'
import { accountsSelector } from 'reducers/accounts'
import { sortAccounts } from 'helpers/accountOrdering'
import { sortAccounts } from '@ledgerhq/live-common/lib/account'
const accountsBtcBalanceSelector = createSelector(
accountsSelector,

5
src/actions/settings.js

@ -46,3 +46,8 @@ export const setExchangePairsAction: SetExchangePairs = pairs => ({
type: 'SETTINGS_SET_PAIRS',
pairs,
})
export const dismissBanner = (bannerId: string) => ({
type: 'SETTINGS_DISMISS_BANNER',
payload: bannerId,
})

2
src/api/Ripple.js

@ -6,7 +6,7 @@ import {
parseCurrencyUnit,
getCryptoCurrencyById,
formatCurrencyUnit,
} from '@ledgerhq/live-common/lib/helpers/currencies'
} from '@ledgerhq/live-common/lib/currencies'
const rippleUnit = getCryptoCurrencyById('ripple').units[0]

4
src/api/network.js

@ -6,7 +6,7 @@ import logger from 'logger'
import { LedgerAPIErrorWithMessage, LedgerAPIError, NetworkDown } from 'config/errors'
import anonymizer from 'helpers/anonymizer'
const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime }): Promise<A> =>
const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime, ...rest }): Promise<A> =>
p.catch(error => {
let errorToThrow
if (error.response) {
@ -47,6 +47,7 @@ const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime }): Promis
})
}
logger.networkError({
...rest,
status,
url,
method,
@ -80,6 +81,7 @@ let implementation = (arg: Object) => {
const meta = {
url: arg.url,
method: arg.method,
data: arg.data,
startTime: Date.now(),
}
logger.network(meta)

9
src/bridge/BridgeSyncContext.js

@ -16,7 +16,9 @@ import { setAccountSyncState } from 'actions/bridgeSync'
import { bridgeSyncSelector, syncStateLocalSelector } from 'reducers/bridgeSync'
import type { BridgeSyncState } from 'reducers/bridgeSync'
import { accountsSelector, isUpToDateSelector } from 'reducers/accounts'
import { currenciesStatusSelector, currencyDownStatusLocal } from 'reducers/currenciesStatus'
import { SYNC_MAX_CONCURRENT, SYNC_TIMEOUT } from 'config/constants'
import type { CurrencyStatus } from 'reducers/currenciesStatus'
import { getBridgeForCurrency } from '.'
type BridgeSyncProviderProps = {
@ -29,6 +31,7 @@ type BridgeSyncProviderOwnProps = BridgeSyncProviderProps & {
isUpToDate: boolean,
updateAccountWithUpdater: (string, (Account) => Account) => void,
setAccountSyncState: (string, AsyncState) => *,
currenciesStatus: CurrencyStatus[],
}
type AsyncState = {
@ -48,6 +51,7 @@ export type Sync = (action: BehaviorAction) => void
const BridgeSyncContext = React.createContext((_: BehaviorAction) => {})
const mapStateToProps = createStructuredSelector({
currenciesStatus: currenciesStatusSelector,
accounts: accountsSelector,
bridgeSync: bridgeSyncSelector,
isUpToDate: isUpToDateSelector,
@ -73,6 +77,11 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
return
}
if (currencyDownStatusLocal(this.props.currenciesStatus, account.currency)) {
next()
return
}
const bridge = getBridgeForCurrency(account.currency)
this.props.setAccountSyncState(accountId, { pending: true, error: null })

103
src/bridge/EthereumJSBridge.js

@ -8,28 +8,37 @@ import AdvancedOptions from 'components/AdvancedOptions/EthereumKind'
import throttle from 'lodash/throttle'
import flatMap from 'lodash/flatMap'
import uniqBy from 'lodash/uniqBy'
import {
getDerivationModesForCurrency,
getDerivationScheme,
runDerivationScheme,
isIterableDerivationMode,
getMandatoryEmptyAccountSkip,
} from '@ledgerhq/live-common/lib/derivation'
import {
getAccountPlaceholderName,
getNewAccountPlaceholderName,
} from '@ledgerhq/live-common/lib/account'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import eip55 from 'eip55'
import { apiForCurrency } from 'api/Ethereum'
import type { Tx } from 'api/Ethereum'
import { getDerivations } from 'helpers/derivations'
import getAddressCommand from 'commands/getAddress'
import signTransactionCommand from 'commands/signTransaction'
import { getAccountPlaceholderName, getNewAccountPlaceholderName } from 'helpers/accountName'
import { NotEnoughBalance, ETHAddressNonEIP } from 'config/errors'
import { NotEnoughBalance, FeeNotLoaded, ETHAddressNonEIP } from 'config/errors'
import type { EditProps, WalletBridge } from './types'
type Transaction = {
recipient: string,
amount: BigNumber,
gasPrice: 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)}`,
gasPrice: !t.gasPrice ? '0x00' : `0x${BigNumber(t.gasPrice).toString(16)}`,
gasLimit: `0x${BigNumber(t.gasLimit).toString(16)}`,
})
@ -64,7 +73,7 @@ const txToOps = (account: Account) => (tx: Tx): Operation[] => {
const value = BigNumber(tx.value)
const fee = BigNumber(tx.gas_price * tx.gas_used)
if (sending) {
ops.push({
const op: $Exact<Operation> = {
id: `${account.id}-${tx.hash}-OUT`,
hash: tx.hash,
type: 'OUT',
@ -76,10 +85,12 @@ const txToOps = (account: Account) => (tx: Tx): Operation[] => {
senders: [tx.from],
recipients: [tx.to],
date: new Date(tx.received_at),
})
extra: {},
}
ops.push(op)
}
if (receiving) {
ops.push({
const op: $Exact<Operation> = {
id: `${account.id}-${tx.hash}-IN`,
hash: tx.hash,
type: 'IN',
@ -91,7 +102,9 @@ const txToOps = (account: Account) => (tx: Tx): Operation[] => {
senders: [tx.from],
recipients: [tx.to],
date: new Date(new Date(tx.received_at) + 1), // hack: make the IN appear after the OUT in history.
})
extra: {},
}
ops.push(op)
}
return ops
}
@ -140,6 +153,8 @@ const signAndBroadcast = async ({
onSigned,
onOperationBroadcasted,
}) => {
const { gasPrice, amount, gasLimit } = t
if (!gasPrice) throw new FeeNotLoaded()
const api = apiForCurrency(a.currency)
const nonce = await api.getAccountNonce(a.freshAddress)
@ -158,12 +173,12 @@ const signAndBroadcast = async ({
const hash = await api.broadcastTransaction(transaction)
onOperationBroadcasted({
const op: $Exact<Operation> = {
id: `${a.id}-${hash}-OUT`,
hash,
type: 'OUT',
value: t.amount,
fee: t.gasPrice.times(t.gasLimit),
value: amount,
fee: gasPrice.times(gasLimit),
blockHeight: null,
blockHash: null,
accountId: a.id,
@ -171,7 +186,10 @@ const signAndBroadcast = async ({
recipients: [t.recipient],
transactionSequenceNumber: nonce,
date: new Date(),
})
extra: {},
}
onOperationBroadcasted(op)
}
}
@ -208,8 +226,8 @@ const EthereumBridge: WalletBridge<Transaction> = {
async function stepAddress(
index,
{ address, path: freshAddressPath, publicKey },
isStandard,
{ address, path: freshAddressPath },
derivationMode,
shouldSkipEmpty,
): { account?: Account, complete?: boolean } {
const balance = await api.getAccountBalance(address)
@ -220,19 +238,21 @@ const EthereumBridge: WalletBridge<Transaction> = {
if (finished) return { complete: true }
const freshAddress = address
const accountId = `ethereumjs:${currency.id}:${address}:${publicKey}`
const accountId = `ethereumjs:2:${currency.id}:${address}:${derivationMode}`
if (txs.length === 0 && balance.isZero()) {
// this is an empty account
if (isStandard) {
if (derivationMode === '') {
// is standard derivation
if (newAccountCount === 0) {
// first zero account will emit one account as opportunity to create a new account..
const account: $Exact<Account> = {
id: accountId,
xpub: '',
seedIdentifier: freshAddress,
freshAddress,
freshAddressPath,
name: getNewAccountPlaceholderName(currency, index),
derivationMode,
name: getNewAccountPlaceholderName({ currency, index, derivationMode }),
balance,
blockHeight: currentBlock.height,
index,
@ -256,10 +276,11 @@ const EthereumBridge: WalletBridge<Transaction> = {
const account: $Exact<Account> = {
id: accountId,
xpub: '',
seedIdentifier: freshAddress,
freshAddress,
freshAddressPath,
name: getAccountPlaceholderName(currency, index, !isStandard),
derivationMode,
name: getAccountPlaceholderName({ currency, index, derivationMode }),
balance,
blockHeight: currentBlock.height,
index,
@ -286,21 +307,23 @@ const EthereumBridge: WalletBridge<Transaction> = {
async function main() {
try {
const derivations = getDerivations(currency)
const last = derivations[derivations.length - 1]
for (const derivation of derivations) {
const isStandard = last === derivation
const derivationModes = getDerivationModesForCurrency(currency)
for (const derivationMode of derivationModes) {
let emptyCount = 0
const mandatoryEmptyAccountSkip = derivation.mandatoryEmptyAccountSkip || 0
for (let index = 0; index < 255; index++) {
const freshAddressPath = derivation({ currency, x: index, segwit: false })
const mandatoryEmptyAccountSkip = getMandatoryEmptyAccountSkip(derivationMode)
const derivationScheme = getDerivationScheme({ derivationMode, currency })
const stopAt = isIterableDerivationMode(derivationMode) ? 255 : 1
for (let index = 0; index < stopAt; index++) {
const freshAddressPath = runDerivationScheme(derivationScheme, currency, {
account: index,
})
const res = await getAddressCommand
.send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath })
.toPromise()
const r = await stepAddress(
index,
res,
isStandard,
derivationMode,
emptyCount < mandatoryEmptyAccountSkip,
)
logger.log(
@ -402,7 +425,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
createTransaction: () => ({
amount: BigNumber(0),
recipient: '',
gasPrice: BigNumber(0),
gasPrice: null,
gasLimit: BigNumber(0x5208),
}),
@ -420,23 +443,27 @@ const EthereumBridge: WalletBridge<Transaction> = {
getTransactionRecipient: (a, t) => t.recipient,
isValidTransaction: (a, t) => (!t.amount.isZero() && t.recipient && true) || false,
EditFees,
EditAdvancedOptions,
checkCanBeSpent: (a, t) =>
t.amount.isLessThanOrEqualTo(a.balance)
? Promise.resolve()
: Promise.reject(new NotEnoughBalance()),
checkValidTransaction: (a, t) =>
!t.gasPrice
? Promise.reject(new FeeNotLoaded())
: t.amount.isLessThanOrEqualTo(a.balance)
? Promise.resolve(true)
: Promise.reject(new NotEnoughBalance()),
getTotalSpent: (a, t) =>
t.amount.isGreaterThan(0) && t.gasPrice.isGreaterThan(0) && t.gasLimit.isGreaterThan(0)
t.amount.isGreaterThan(0) &&
t.gasPrice &&
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))),
getMaxAmount: (a, t) =>
Promise.resolve(a.balance.minus((t.gasPrice || BigNumber(0)).times(t.gasLimit))),
signAndBroadcast: (a, t, deviceId) =>
Observable.create(o => {

78
src/bridge/LibcoreBridge.js

@ -9,9 +9,9 @@ import FeesBitcoinKind from 'components/FeesField/BitcoinKind'
import libcoreScanAccounts from 'commands/libcoreScanAccounts'
import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import libcoreGetFees from 'commands/libcoreGetFees'
import libcoreGetFees, { extractGetFeesInputFromAccount } from 'commands/libcoreGetFees'
import libcoreValidAddress from 'commands/libcoreValidAddress'
import { NotEnoughBalance } from 'config/errors'
import { NotEnoughBalance, FeeNotLoaded } from 'config/errors'
import type { WalletBridge, EditProps } from './types'
const NOT_ENOUGH_FUNDS = 52
@ -20,15 +20,18 @@ const notImplemented = new Error('LibcoreBridge: not implemented')
type Transaction = {
amount: BigNumber,
feePerByte: BigNumber,
feePerByte: ?BigNumber,
recipient: string,
}
const serializeTransaction = t => ({
recipient: t.recipient,
amount: t.amount.toString(),
feePerByte: t.feePerByte.toString(),
})
const serializeTransaction = t => {
const { feePerByte } = t
return {
recipient: t.recipient,
amount: t.amount.toString(),
feePerByte: (feePerByte && feePerByte.toString()) || '0',
}
}
const decodeOperation = (encodedAccount, rawOp) =>
decodeAccount({ ...encodedAccount, operations: [rawOp] }).operations[0]
@ -75,7 +78,9 @@ const isRecipientValid = (currency, recipient) => {
const feesLRU = LRU({ max: 100 })
const getFeesKey = (a, t) =>
`${a.id}_${a.blockHeight || 0}_${t.amount.toString()}_${t.recipient}_${t.feePerByte.toString()}`
`${a.id}_${a.blockHeight || 0}_${t.amount.toString()}_${t.recipient}_${
t.feePerByte ? t.feePerByte.toString() : ''
}`
const getFees = async (a, transaction) => {
const isValid = await isRecipientValid(a.currency, transaction.recipient)
@ -83,10 +88,10 @@ const getFees = async (a, transaction) => {
const key = getFeesKey(a, transaction)
let promise = feesLRU.get(key)
if (promise) return promise
promise = libcoreGetFees
.send({
accountId: a.id,
accountIndex: a.index,
...extractGetFeesInputFromAccount(a),
transaction: serializeTransaction(transaction),
})
.toPromise()
@ -95,18 +100,20 @@ const getFees = async (a, transaction) => {
return promise
}
const checkCanBeSpent = (a, t) =>
!t.amount
? Promise.resolve()
: getFees(a, t)
.then(() => {})
.catch(e => {
if (e.code === NOT_ENOUGH_FUNDS) {
throw new NotEnoughBalance()
}
feesLRU.del(getFeesKey(a, t))
throw e
})
const checkValidTransaction = (a, t) =>
!t.feePerByte
? Promise.reject(new FeeNotLoaded())
: !t.amount
? Promise.resolve(true)
: getFees(a, t)
.then(() => true)
.catch(e => {
if (e.code === NOT_ENOUGH_FUNDS) {
throw new NotEnoughBalance()
}
feesLRU.del(getFeesKey(a, t))
throw e
})
const LibcoreBridge: WalletBridge<Transaction> = {
scanAccountsOnDevice(currency, devicePath) {
@ -122,13 +129,15 @@ const LibcoreBridge: WalletBridge<Transaction> = {
libcoreSyncAccount
.send({
accountId: account.id,
freshAddressPath: account.freshAddressPath,
derivationMode: account.derivationMode,
xpub: account.xpub || '',
seedIdentifier: account.seedIdentifier,
index: account.index,
currencyId: account.currency.id,
})
.pipe(
map(rawSyncedAccount => {
const syncedAccount = decodeAccount(rawSyncedAccount)
map(({ rawAccount, requiresCacheFlush }) => {
const syncedAccount = decodeAccount(rawAccount)
return account => {
const accountOps = account.operations
const syncedOps = syncedAccount.operations
@ -142,11 +151,11 @@ const LibcoreBridge: WalletBridge<Transaction> = {
}
const hasChanged =
requiresCacheFlush ||
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].id !== syncedOps[0].id || // if same size, only check if the last item has changed.
accountOps[0].blockHeight !== syncedOps[0].blockHeight))
if (hasChanged) {
@ -170,7 +179,7 @@ const LibcoreBridge: WalletBridge<Transaction> = {
createTransaction: () => ({
amount: BigNumber(0),
recipient: '',
feePerByte: BigNumber(0),
feePerByte: null,
isRBF: false,
}),
@ -192,9 +201,7 @@ const LibcoreBridge: WalletBridge<Transaction> = {
// EditAdvancedOptions,
isValidTransaction: (a, t) => (!t.amount.isZero() && t.recipient && true) || false,
checkCanBeSpent,
checkValidTransaction,
getTotalSpent: (a, t) =>
t.amount.isZero()
@ -213,9 +220,10 @@ const LibcoreBridge: WalletBridge<Transaction> = {
.send({
accountId: account.id,
currencyId: account.currency.id,
xpub: account.xpub,
freshAddress: account.freshAddress,
freshAddressPath: account.freshAddressPath,
blockHeight: account.blockHeight,
xpub: account.xpub || '', // FIXME only reason is to build the op id. we need to consider another id for making op id.
derivationMode: account.derivationMode,
seedIdentifier: account.seedIdentifier,
index: account.index,
transaction: serializeTransaction(transaction),
deviceId,

150
src/bridge/RippleJSBridge.js

@ -7,7 +7,16 @@ import bs58check from 'ripple-bs58check'
import { computeBinaryTransactionHash } from 'ripple-hashes'
import throttle from 'lodash/throttle'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import { getDerivations } from 'helpers/derivations'
import {
getDerivationModesForCurrency,
getDerivationScheme,
runDerivationScheme,
isIterableDerivationMode,
} from '@ledgerhq/live-common/lib/derivation'
import {
getAccountPlaceholderName,
getNewAccountPlaceholderName,
} from '@ledgerhq/live-common/lib/account'
import getAddress from 'commands/getAddress'
import signTransaction from 'commands/signTransaction'
import {
@ -19,14 +28,17 @@ import {
} from 'api/Ripple'
import FeesRippleKind from 'components/FeesField/RippleKind'
import AdvancedOptionsRippleKind from 'components/AdvancedOptions/RippleKind'
import { getAccountPlaceholderName, getNewAccountPlaceholderName } from 'helpers/accountName'
import { NotEnoughBalance } from 'config/errors'
import {
NotEnoughBalance,
FeeNotLoaded,
NotEnoughBalanceBecauseDestinationNotCreated,
} from 'config/errors'
import type { WalletBridge, EditProps } from './types'
type Transaction = {
amount: BigNumber,
recipient: string,
fee: BigNumber,
fee: ?BigNumber,
tag: ?number,
}
@ -51,6 +63,8 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps<Transaction>) => (
async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) {
const api = apiForEndpointConfig(a.endpointConfig)
const { fee } = t
if (!fee) throw new FeeNotLoaded()
try {
await api.connect()
const amount = formatAPICurrencyXRP(t.amount)
@ -66,7 +80,7 @@ async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOpera
},
}
const instruction = {
fee: formatAPICurrencyXRP(t.fee).value,
fee: formatAPICurrencyXRP(fee).value,
maxLedgerVersionOffset: 12,
}
@ -91,13 +105,13 @@ async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOpera
const hash = computeBinaryTransactionHash(transaction)
onOperationBroadcasted({
const op: $Exact<Operation> = {
id: `${a.id}-${hash}-OUT`,
hash,
accountId: a.id,
type: 'OUT',
value: t.amount,
fee: t.fee,
fee,
blockHash: null,
blockHeight: null,
senders: [a.freshAddress],
@ -107,14 +121,16 @@ async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOpera
transactionSequenceNumber:
(a.operations.length > 0 ? a.operations[0].transactionSequenceNumber : 0) +
a.pendingOperations.length,
})
extra: {},
}
onOperationBroadcasted(op)
}
} finally {
api.disconnect()
}
}
function isRecipientValid(currency, recipient) {
function isRecipientValid(recipient) {
try {
bs58check.decode(recipient)
return true
@ -205,7 +221,7 @@ const txToOperation = (account: Account) => ({
}
const op: $Exact<Operation> = {
id,
id: `${account.id}-${id}-${type}`,
hash: id,
accountId: account.id,
type,
@ -217,6 +233,7 @@ const txToOperation = (account: Account) => ({
recipients: [destination.address],
date: new Date(timestamp),
transactionSequenceNumber: sequence,
extra: {},
}
return op
}
@ -241,6 +258,31 @@ const getServerInfo = (map => endpointConfig => {
return f()
})({})
const recipientIsNew = async (endpointConfig, recipient) => {
if (!isRecipientValid(recipient)) return false
const api = apiForEndpointConfig(endpointConfig)
try {
await api.connect()
try {
await api.getAccountInfo(recipient)
return false
} catch (e) {
if (e.message !== 'actNotFound') {
throw e
}
return true
}
} finally {
api.disconnect()
}
}
const cacheRecipientsNew = {}
const cachedRecipientIsNew = (endpointConfig, recipient) => {
if (recipient in cacheRecipientsNew) return cacheRecipientsNew[recipient]
return (cacheRecipientsNew[recipient] = recipientIsNew(endpointConfig, recipient))
}
const RippleJSBridge: WalletBridge<Transaction> = {
scanAccountsOnDevice: (currency, deviceId) =>
Observable.create(o => {
@ -258,17 +300,20 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const minLedgerVersion = Number(ledgers[0])
const maxLedgerVersion = Number(ledgers[1])
const derivations = getDerivations(currency)
for (const derivation of derivations) {
const legacy = derivation !== derivations[derivations.length - 1]
for (let index = 0; index < 255; index++) {
const freshAddressPath = derivation({ currency, x: index, segwit: false })
const { address, publicKey } = await await getAddress
const derivationModes = getDerivationModesForCurrency(currency)
for (const derivationMode of derivationModes) {
const derivationScheme = getDerivationScheme({ derivationMode, currency })
const stopAt = isIterableDerivationMode(derivationMode) ? 255 : 1
for (let index = 0; index < stopAt; index++) {
const freshAddressPath = runDerivationScheme(derivationScheme, currency, {
account: index,
})
const { address } = await await getAddress
.send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath })
.toPromise()
if (finished) return
const accountId = `ripplejs:${currency.id}:${address}:${publicKey}`
const accountId = `ripplejs:2:${currency.id}:${address}:${derivationMode}`
let info
try {
@ -285,11 +330,12 @@ const RippleJSBridge: WalletBridge<Transaction> = {
if (!info) {
// account does not exist in Ripple server
// we are generating a new account locally
if (!legacy) {
if (derivationMode === '') {
o.next({
id: accountId,
xpub: '',
name: getNewAccountPlaceholderName(currency, index),
seedIdentifier: freshAddress,
derivationMode,
name: getNewAccountPlaceholderName({ currency, index, derivationMode }),
freshAddress,
freshAddressPath,
balance: BigNumber(0),
@ -322,8 +368,9 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const account: $Exact<Account> = {
id: accountId,
xpub: '',
name: getAccountPlaceholderName(currency, index, legacy),
seedIdentifier: freshAddress,
derivationMode,
name: getAccountPlaceholderName({ currency, index, derivationMode }),
freshAddress,
freshAddressPath,
balance,
@ -446,13 +493,13 @@ const RippleJSBridge: WalletBridge<Transaction> = {
pullMoreOperations: () => Promise.resolve(a => a), // FIXME not implemented
isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(currency, recipient)),
isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(recipient)),
getRecipientWarning: () => Promise.resolve(null),
createTransaction: () => ({
amount: BigNumber(0),
recipient: '',
fee: BigNumber(0),
fee: null,
tag: undefined,
}),
@ -463,10 +510,30 @@ const RippleJSBridge: WalletBridge<Transaction> = {
getTransactionAmount: (a, t) => t.amount,
editTransactionRecipient: (account, t, recipient) => ({
...t,
recipient,
}),
editTransactionRecipient: (account, t, recipient) => {
const parts = recipient.split('?')
const params = new URLSearchParams(parts[1])
recipient = parts[0]
// Extract parameters we may need
for (const [key, value] of params.entries()) {
switch (key) {
case 'dt':
t.tag = parseInt(value, 10) || 0
break
case 'amount':
t.amount = parseAPIValue(value || '0')
break
default:
// do nothing
}
}
return {
...t,
recipient,
}
},
EditFees,
@ -474,27 +541,38 @@ const RippleJSBridge: WalletBridge<Transaction> = {
getTransactionRecipient: (a, t) => t.recipient,
isValidTransaction: (a, t) => (!t.amount.isZero() && t.recipient && true) || false,
checkCanBeSpent: async (a, t) => {
checkValidTransaction: async (a, t) => {
if (!t.fee) throw new FeeNotLoaded()
const r = await getServerInfo(a.endpointConfig)
const reserveBaseXRP = parseAPIValue(r.validatedLedger.reserveBaseXRP)
if (t.recipient) {
if (await cachedRecipientIsNew(a.endpointConfig, t.recipient)) {
if (t.amount.lt(reserveBaseXRP)) {
const f = formatAPICurrencyXRP(reserveBaseXRP)
throw new NotEnoughBalanceBecauseDestinationNotCreated('', {
minimalAmount: `${f.currency} ${f.value}`,
})
}
}
}
if (
t.amount
.plus(t.fee)
.plus(parseAPIValue(r.validatedLedger.reserveBaseXRP))
.plus(t.fee || 0)
.plus(reserveBaseXRP)
.isLessThanOrEqualTo(a.balance)
) {
return
return true
}
throw new NotEnoughBalance()
},
getTotalSpent: (a, t) => Promise.resolve(t.amount.plus(t.fee)),
getTotalSpent: (a, t) => Promise.resolve(t.amount.plus(t.fee || 0)),
getMaxAmount: (a, t) => Promise.resolve(a.balance.minus(t.fee)),
getMaxAmount: (a, t) => Promise.resolve(a.balance.minus(t.fee || 0)),
signAndBroadcast: (a, t, deviceId) =>
Observable.create(o => {
delete cacheRecipientsNew[t.recipient]
let cancelled = false
const isCancelled = () => cancelled
const onSigned = () => {

4
src/bridge/UnsupportedBridge.js

@ -27,13 +27,11 @@ const UnsupportedBridge: WalletBridge<*> = {
getTransactionAmount: () => BigNumber(0),
isValidTransaction: () => false,
editTransactionRecipient: () => null,
getTransactionRecipient: () => '',
checkCanBeSpent: () => Promise.resolve(),
checkValidTransaction: () => Promise.resolve(false),
getTotalSpent: () => Promise.resolve(BigNumber(0)),

1
src/bridge/index.js

@ -12,6 +12,7 @@ const perFamily = {
bitcoin: LibcoreBridge,
ripple: RippleJSBridge,
ethereum: EthereumJSBridge,
stellar: null,
}
if (USE_MOCK_DATA) {
const mockBridge = makeMockBridge()

12
src/bridge/makeMockBridge.js

@ -6,11 +6,11 @@ import {
genAddingOperationsInAccount,
genOperation,
} from '@ledgerhq/live-common/lib/mock/account'
import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/operation'
import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/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 { validateNameEdition } from '@ledgerhq/live-common/lib/account'
import { MOCK_DATA_SEED } from 'config/constants'
import type { WalletBridge } from './types'
@ -18,7 +18,7 @@ const defaultOpts = {
scanAccountDeviceSuccessRate: 0.8,
transactionsSizeTarget: 100,
extraInitialTransactionProps: () => null,
checkCanBeSpent: () => Promise.resolve(),
checkValidTransaction: () => Promise.resolve(),
getTotalSpent: (a, t) => Promise.resolve(t.amount),
getMaxAmount: a => Promise.resolve(a.balance),
}
@ -36,7 +36,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
extraInitialTransactionProps,
getTotalSpent,
getMaxAmount,
checkCanBeSpent,
checkValidTransaction,
} = {
...defaultOpts,
...opts,
@ -155,9 +155,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
EditAdvancedOptions,
isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false,
checkCanBeSpent,
checkValidTransaction,
getTotalSpent,

7
src/bridge/types.js

@ -76,15 +76,16 @@ export interface WalletBridge<Transaction> {
getTransactionRecipient(account: Account, transaction: Transaction): string;
isValidTransaction(account: Account, transaction: Transaction): boolean;
// render the whole Fees section of the form
EditFees?: *; // React$ComponentType<EditProps<Transaction>>;
// render the whole advanced part of the form
EditAdvancedOptions?: *; // React$ComponentType<EditProps<Transaction>>;
checkCanBeSpent(account: Account, transaction: Transaction): Promise<void>;
// validate the transaction and all currency specific validations here, we can return false
// to disable the button without throwing an error if we are handling the error on a different
// input or throw an error that will highlight the issue on the amount field
checkValidTransaction(account: Account, transaction: Transaction): Promise<boolean>;
getTotalSpent(account: Account, transaction: Transaction): Promise<BigNumber>;

2
src/commands/debugAppInfosForCurrency.js

@ -1,6 +1,6 @@
// @flow
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'

2
src/commands/getAddress.js

@ -1,6 +1,6 @@
// @flow
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'

9
src/commands/index.js

@ -15,10 +15,11 @@ import installFinalFirmware from 'commands/installFinalFirmware'
import installMcu from 'commands/installMcu'
import installOsuFirmware from 'commands/installOsuFirmware'
import isDashboardOpen from 'commands/isDashboardOpen'
import killInternalProcess from 'commands/killInternalProcess'
import libcoreGetFees from 'commands/libcoreGetFees'
import libcoreGetVersion from 'commands/libcoreGetVersion'
import libcoreHardReset from 'commands/libcoreHardReset'
import libcoreScanAccounts from 'commands/libcoreScanAccounts'
import libcoreScanFromXPUB from 'commands/libcoreScanFromXPUB'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreValidAddress from 'commands/libcoreValidAddress'
@ -47,10 +48,11 @@ const all: Array<Command<any, any>> = [
installMcu,
installOsuFirmware,
isDashboardOpen,
killInternalProcess,
libcoreGetFees,
libcoreGetVersion,
libcoreHardReset,
libcoreScanAccounts,
libcoreScanFromXPUB,
libcoreSignAndBroadcast,
libcoreSyncAccount,
libcoreValidAddress,
@ -67,8 +69,11 @@ const all: Array<Command<any, any>> = [
uninstallApp,
]
export const commandsById = {}
all.forEach(cmd => {
invariant(!all.some(c => c !== cmd && c.id === cmd.id), `duplicate command '${cmd.id}'`)
commandsById[cmd.id] = cmd
})
export default all

18
src/commands/killInternalProcess.js

@ -0,0 +1,18 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { of } from 'rxjs'
type Input = void
type Result = boolean
const cmd: Command<Input, Result> = createCommand('killInternalProcess', () => {
setTimeout(() => {
// we assume commands are run on the internal process
// special exit code for better identification
process.exit(42)
})
return of(true)
})
export default cmd

39
src/commands/libcoreGetFees.js

@ -4,8 +4,15 @@ 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, libcoreAmountToBigNumber, bigNumberToLibcoreAmount } from 'helpers/libcore'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { getWalletName } from '@ledgerhq/live-common/lib/account'
import type { Account, DerivationMode } from '@ledgerhq/live-common/lib/types'
import {
isValidAddress,
libcoreAmountToBigNumber,
bigNumberToLibcoreAmount,
getOrCreateWallet,
} from 'helpers/libcore'
import { InvalidAddress } from 'config/errors'
type BitcoinLikeTransaction = {
@ -16,23 +23,43 @@ type BitcoinLikeTransaction = {
}
type Input = {
accountId: string,
accountIndex: number,
transaction: BitcoinLikeTransaction,
currencyId: string,
derivationMode: DerivationMode,
seedIdentifier: string,
}
export const extractGetFeesInputFromAccount = (a: Account) => {
const currencyId = a.currency.id
return {
accountIndex: a.index,
currencyId,
derivationMode: a.derivationMode,
seedIdentifier: a.seedIdentifier,
}
}
type Result = { totalFees: string }
const cmd: Command<Input, Result> = createCommand(
'libcoreGetFees',
({ accountId, accountIndex, transaction }) =>
({ currencyId, derivationMode, seedIdentifier, accountIndex, transaction }) =>
Observable.create(o => {
let unsubscribed = false
const isCancelled = () => unsubscribed
const currency = getCryptoCurrencyById(currencyId)
withLibcore(async core => {
const { walletName } = accountIdHelper.decode(accountId)
const njsWallet = await core.getPoolInstance().getWallet(walletName)
const walletName = getWalletName({
currency,
derivationMode,
seedIdentifier,
})
const njsWallet = await getOrCreateWallet(core, walletName, {
currency,
derivationMode,
})
if (isCancelled()) return
const njsAccount = await njsWallet.getAccount(accountIndex)
if (isCancelled()) return

19
src/commands/libcoreHardReset.js

@ -1,19 +0,0 @@
// @flow
import { createCommand } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import withLibcore from 'helpers/withLibcore'
import { HardResetFail } from 'config/errors'
const cmd = createCommand('libcoreHardReset', () =>
fromPromise(
withLibcore(async core => {
const result = await core.getPoolInstance().eraseDataSince(new Date(0))
if (result !== core.ERROR_CODE.FUTURE_WAS_SUCCESSFULL) {
throw new HardResetFail(`Hard reset fail with ${result} (check core.ERROR_CODE)`)
}
}),
),
)
export default cmd

29
src/commands/libcoreScanFromXPUB.js

@ -0,0 +1,29 @@
// @flow
import { fromPromise } from 'rxjs/observable/fromPromise'
import type { AccountRaw, DerivationMode } from '@ledgerhq/live-common/lib/types'
import { createCommand, Command } from 'helpers/ipc'
import withLibcore from 'helpers/withLibcore'
import { scanAccountsFromXPUB } from 'helpers/libcore'
type Input = {
currencyId: string,
xpub: string,
derivationMode: DerivationMode,
seedIdentifier: string,
}
type Result = AccountRaw
const cmd: Command<Input, Result> = createCommand(
'libcoreScanFromXPUB',
({ currencyId, xpub, derivationMode, seedIdentifier }) =>
fromPromise(
withLibcore(async core =>
scanAccountsFromXPUB({ core, currencyId, xpub, derivationMode, seedIdentifier }),
),
),
)
export default cmd

123
src/commands/libcoreSignAndBroadcast.js

@ -2,17 +2,23 @@
import logger from 'logger'
import { BigNumber } from 'bignumber.js'
import type { OperationRaw } from '@ledgerhq/live-common/lib/types'
import { StatusCodes } from '@ledgerhq/hw-transport'
import Btc from '@ledgerhq/hw-app-btc'
import { Observable } from 'rxjs'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import { isSegwitPath } from 'helpers/bip32'
import { libcoreAmountToBigNumber, bigNumberToLibcoreAmount } from 'helpers/libcore'
import { isSegwitDerivationMode } from '@ledgerhq/live-common/lib/derivation'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import type { OperationRaw, DerivationMode, CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { getWalletName } from '@ledgerhq/live-common/lib/account'
import {
libcoreAmountToBigNumber,
bigNumberToLibcoreAmount,
getOrCreateWallet,
} from 'helpers/libcore'
import { UpdateYourApp } from 'config/errors'
import withLibcore from 'helpers/withLibcore'
import { createCommand, Command } from 'helpers/ipc'
import { withDevice } from 'helpers/deviceAccess'
import * as accountIdHelper from 'helpers/accountId'
type BitcoinLikeTransaction = {
amount: string,
@ -22,10 +28,11 @@ type BitcoinLikeTransaction = {
type Input = {
accountId: string,
blockHeight: number,
currencyId: string,
derivationMode: DerivationMode,
seedIdentifier: string,
xpub: string,
freshAddress: string,
freshAddressPath: string,
index: number,
transaction: BitcoinLikeTransaction,
deviceId: string,
@ -37,17 +44,29 @@ type Result = { type: 'signed' } | { type: 'broadcasted', operation: OperationRa
const cmd: Command<Input, Result> = createCommand(
'libcoreSignAndBroadcast',
({ accountId, currencyId, xpub, freshAddress, freshAddressPath, index, transaction, deviceId }) =>
({
accountId,
blockHeight,
currencyId,
derivationMode,
seedIdentifier,
xpub,
index,
transaction,
deviceId,
}) =>
Observable.create(o => {
let unsubscribed = false
const currency = getCryptoCurrencyById(currencyId)
const isCancelled = () => unsubscribed
withLibcore(core =>
doSignAndBroadcast({
accountId,
currencyId,
currency,
blockHeight,
derivationMode,
seedIdentifier,
xpub,
freshAddress,
freshAddressPath,
index,
transaction,
deviceId,
@ -73,28 +92,33 @@ const cmd: Command<Input, Result> = createCommand(
async function signTransaction({
hwApp,
currencyId,
currency,
blockHeight,
transaction,
derivationMode,
sigHashType,
supportsSegwit,
isSegwit,
hasTimestamp,
}: {
hwApp: Btc,
currencyId: string,
currency: CryptoCurrency,
blockHeight: number,
transaction: *,
derivationMode: DerivationMode,
sigHashType: number,
supportsSegwit: boolean,
isSegwit: boolean,
hasTimestamp: boolean,
}) {
const additionals = []
let expiryHeight
if (currencyId === 'bitcoin_cash' || currencyId === 'bitcoin_gold') additionals.push('bip143')
if (currencyId === 'zcash') expiryHeight = Buffer.from([0x00, 0x00, 0x00, 0x00])
if (currency.id === 'bitcoin_cash' || currency.id === 'bitcoin_gold') additionals.push('bip143')
if (currency.id === 'zcash') {
expiryHeight = Buffer.from([0x00, 0x00, 0x00, 0x00])
if (blockHeight >= 419200) {
additionals.push('sapling')
}
}
const rawInputs = transaction.getInputs()
const hasExtraData = currencyId === 'zcash'
const hasExtraData = currency.id === 'zcash'
const inputs = await Promise.all(
rawInputs.map(async input => {
@ -102,7 +126,7 @@ async function signTransaction({
const hexPreviousTransaction = Buffer.from(rawPreviousTransaction).toString('hex')
const previousTransaction = hwApp.splitTransaction(
hexPreviousTransaction,
supportsSegwit,
true, // set to true allow both segwit AND non-segwit
hasTimestamp,
hasExtraData,
)
@ -151,7 +175,7 @@ async function signTransaction({
outputScriptHex,
lockTime,
sigHashType,
isSegwit,
isSegwitDerivationMode(derivationMode),
initialTimestamp,
additionals,
expiryHeight,
@ -162,10 +186,11 @@ async function signTransaction({
export async function doSignAndBroadcast({
accountId,
currencyId,
derivationMode,
blockHeight,
seedIdentifier,
currency,
xpub,
freshAddress,
freshAddressPath,
index,
transaction,
deviceId,
@ -175,10 +200,11 @@ export async function doSignAndBroadcast({
onOperationBroadcasted,
}: {
accountId: string,
currencyId: string,
derivationMode: DerivationMode,
seedIdentifier: string,
blockHeight: number,
currency: CryptoCurrency,
xpub: string,
freshAddress: string,
freshAddressPath: string,
index: number,
transaction: BitcoinLikeTransaction,
deviceId: string,
@ -187,8 +213,9 @@ export async function doSignAndBroadcast({
onSigned: () => void,
onOperationBroadcasted: (optimisticOp: $Exact<OperationRaw>) => void,
}): Promise<void> {
const { walletName } = accountIdHelper.decode(accountId)
const njsWallet = await core.getPoolInstance().getWallet(walletName)
const walletName = getWalletName({ currency, seedIdentifier, derivationMode })
const njsWallet = await getOrCreateWallet(core, walletName, { currency, derivationMode })
if (isCancelled()) return
const njsAccount = await njsWallet.getAccount(index)
if (isCancelled()) return
@ -214,19 +241,22 @@ export async function doSignAndBroadcast({
const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction
// TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay
const currency = getCryptoCurrencyById(currencyId)
const signedTransaction = await withDevice(deviceId)(async transport =>
signTransaction({
hwApp: new Btc(transport),
currencyId,
currency,
blockHeight,
transaction: builded,
sigHashType: parseInt(sigHashType, 16),
supportsSegwit: !!currency.supportsSegwit,
isSegwit: isSegwitPath(freshAddressPath),
hasTimestamp,
derivationMode,
}),
)
).catch(e => {
if (e && e.statusCode === StatusCodes.INCORRECT_P1_P2) {
throw new UpdateYourApp(`UpdateYourApp ${currency.id}`, currency)
}
throw e
})
if (!signedTransaction || isCancelled() || !njsAccount) return
onSigned()
@ -237,10 +267,20 @@ export async function doSignAndBroadcast({
.asBitcoinLikeAccount()
.broadcastRawTransaction(Array.from(Buffer.from(signedTransaction, 'hex')))
const senders = builded
.getInputs()
.map(input => input.getAddress())
.filter(a => a)
const recipients = builded
.getOutputs()
.map(output => output.getAddress())
.filter(a => a)
const fee = libcoreAmountToBigNumber(builded.getFees())
// NB we don't check isCancelled() because the broadcast is not cancellable now!
onOperationBroadcasted({
const op: $Exact<OperationRaw> = {
id: `${xpub}-${txHash}-OUT`,
hash: txHash,
type: 'OUT',
@ -250,12 +290,13 @@ export async function doSignAndBroadcast({
fee: fee.toString(),
blockHash: null,
blockHeight: null,
// FIXME for senders and recipients, can we ask the libcore?
senders: [freshAddress],
recipients: [transaction.recipient],
senders,
recipients,
accountId,
date: new Date().toISOString(),
})
extra: {},
}
onOperationBroadcasted(op)
}
export default cmd

20
src/commands/libcoreSyncAccount.js

@ -1,6 +1,7 @@
// @flow
import type { AccountRaw } from '@ledgerhq/live-common/lib/types'
import type { AccountRaw, DerivationMode } from '@ledgerhq/live-common/lib/types'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { createCommand, Command } from 'helpers/ipc'
@ -9,15 +10,24 @@ import withLibcore from 'helpers/withLibcore'
type Input = {
accountId: string,
freshAddressPath: string,
currencyId: string,
xpub: string,
derivationMode: DerivationMode,
seedIdentifier: string,
index: number,
}
type Result = AccountRaw
type Result = { rawAccount: AccountRaw, requiresCacheFlush: boolean }
const cmd: Command<Input, Result> = createCommand('libcoreSyncAccount', accountInfos =>
fromPromise(withLibcore(core => syncAccount({ ...accountInfos, core }))),
const cmd: Command<Input, Result> = createCommand(
'libcoreSyncAccount',
({ currencyId, ...accountInfos }) =>
fromPromise(
withLibcore(core => {
const currency = getCryptoCurrencyById(currencyId)
return syncAccount({ ...accountInfos, currency, core })
}),
),
)
export default cmd

8
src/components/AccountPage/AccountHeaderActions.js

@ -7,7 +7,7 @@ import { translate } from 'react-i18next'
import styled from 'styled-components'
import type { Account } from '@ledgerhq/live-common/lib/types'
import Tooltip from 'components/base/Tooltip'
import isAccountEmpty from 'helpers/isAccountEmpty'
import { isAccountEmpty } from '@ledgerhq/live-common/lib/account'
import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants'
@ -67,19 +67,19 @@ class AccountHeaderActions extends PureComponent<Props> {
<Button small primary onClick={() => openModal(MODAL_SEND, { account })}>
<Box horizontal flow={1} alignItems="center">
<IconSend size={12} />
<Box>{t('app:send.title')}</Box>
<Box>{t('send.title')}</Box>
</Box>
</Button>
<Button small primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Box horizontal flow={1} alignItems="center">
<IconReceive size={12} />
<Box>{t('app:receive.title')}</Box>
<Box>{t('receive.title')}</Box>
</Box>
</Button>
</Fragment>
) : null}
<Tooltip render={() => t('app:account.settings.title')}>
<Tooltip render={() => t('account.settings.title')}>
<ButtonSettings onClick={() => openModal(MODAL_SETTINGS_ACCOUNT, { account })}>
<Box justifyContent="center">
<IconAccountSettings size={16} />

6
src/components/AccountPage/EmptyStateAccount.js

@ -40,9 +40,9 @@ class EmptyStateAccount extends PureComponent<Props, *> {
height="89"
/>
<Box mt={5} alignItems="center">
<Title>{t('app:account.emptyState.title')}</Title>
<Title>{t('account.emptyState.title')}</Title>
<Description mt={3} style={{ display: 'block' }}>
<Trans i18nKey="app:account.emptyState.desc">
<Trans i18nKey="account.emptyState.desc">
{'Make sure the'}
<Text ff="Open Sans|SemiBold" color="dark">
{account.currency.managerAppName}
@ -53,7 +53,7 @@ class EmptyStateAccount extends PureComponent<Props, *> {
<Button mt={5} primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Box horizontal flow={1} alignItems="center">
<IconReceive size={12} />
<Box>{t('app:account.emptyState.buttons.receiveFunds')}</Box>
<Box>{t('account.emptyState.buttons.receiveFunds')}</Box>
</Box>
</Button>
</Box>

4
src/components/AccountPage/index.js

@ -8,7 +8,7 @@ import { Redirect } from 'react-router'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import { accountSelector } from 'reducers/accounts'
import isAccountEmpty from 'helpers/isAccountEmpty'
import { isAccountEmpty } from '@ledgerhq/live-common/lib/account'
import {
counterValueCurrencySelector,
localeSelector,
@ -97,7 +97,7 @@ class AccountPage extends PureComponent<Props> {
/>
</Box>
<OperationsList account={account} title={t('app:account.lastOperations')} />
<OperationsList account={account} title={t('account.lastOperations')} />
<StickyBackToTop />
</Fragment>

8
src/components/AdvancedOptions/BitcoinKind.js

@ -15,12 +15,12 @@ type Props = {
}
export default translate()(({ isRBF, onChangeRBF, t }: Props) => (
<Spoiler title={t('app:send.steps.amount.advancedOptions')}>
<Spoiler title={t('send.steps.amount.advancedOptions')}>
<Box horizontal align="center" flow={5}>
<Box style={{ width: 200 }}>
<Label>
<span>{t('app:send.steps.amount.useRBF')}</span>
<LabelInfoTooltip ml={1} text={t('app:send.steps.amount.useRBF')} />
<span>{t('send.steps.amount.useRBF')}</span>
<LabelInfoTooltip ml={1} text={t('send.steps.amount.useRBF')} />
</Label>
</Box>
<Box grow>
@ -32,7 +32,7 @@ export default translate()(({ isRBF, onChangeRBF, t }: Props) => (
<Box horizontal align="flex-start" flow={5}>
<Box style={{ width: 200 }}>
<Label>
<span>{t('app:send.steps.amount.message')}</span>
<span>{t('send.steps.amount.message')}</span>
</Label>
</Box>
<Box grow>

4
src/components/AdvancedOptions/EthereumKind.js

@ -15,11 +15,11 @@ type Props = {
}
export default translate()(({ gasLimit, onChangeGasLimit, t }: Props) => (
<Spoiler title={t('app:send.steps.amount.advancedOptions')}>
<Spoiler title={t('send.steps.amount.advancedOptions')}>
<Box horizontal align="center" flow={5}>
<Box style={{ width: 200 }}>
<Label>
<span>{t('app:send.steps.amount.ethereumGasLimit')}</span>
<span>{t('send.steps.amount.ethereumGasLimit')}</span>
</Label>
</Box>
<Box grow>

53
src/components/AdvancedOptions/RippleKind.js

@ -1,11 +1,11 @@
// @flow
import React from 'react'
import React, { Component } from 'react'
import { BigNumber } from 'bignumber.js'
import { translate } from 'react-i18next'
import Box from 'components/base/Box'
import Input from 'components/base/Input'
import Label from 'components/base/Label'
import Spoiler from 'components/base/Spoiler'
type Props = {
tag: ?number,
@ -13,24 +13,33 @@ type Props = {
t: *,
}
export default translate()(({ tag, onChangeTag, t }: Props) => (
<Spoiler title={t('app:send.steps.amount.advancedOptions')}>
<Box horizontal align="center" flow={5}>
<Box style={{ width: 200 }}>
<Label>
<span>{t('app:send.steps.amount.rippleTag')}</span>
</Label>
</Box>
<Box grow>
<Input
value={String(tag || '')}
onChange={str => {
const tag = parseInt(str, 10)
if (!isNaN(tag) && isFinite(tag)) onChangeTag(tag)
else onChangeTag(undefined)
}}
/>
const uint32maxPlus1 = BigNumber(2).pow(32)
class RippleKind extends Component<Props> {
onChange = str => {
const { onChangeTag } = this.props
const tag = BigNumber(str.replace(/[^0-9]/g, ''))
if (!tag.isNaN() && tag.isFinite()) {
if (tag.isInteger() && tag.isPositive() && tag.lt(uint32maxPlus1)) {
onChangeTag(tag.toNumber())
}
} else {
onChangeTag(undefined)
}
}
render() {
const { tag, t } = this.props
return (
<Box vertical flow={5}>
<Box grow>
<Label>
<span>{t('send.steps.amount.rippleTag')}</span>
</Label>
<Input value={String(tag || '')} onChange={this.onChange} />
</Box>
</Box>
</Box>
</Spoiler>
))
)
}
}
export default translate()(RippleKind)

14
src/components/BalanceSummary/BalanceInfos.js

@ -56,11 +56,7 @@ export function BalanceSincePercent(props: BalanceSinceProps) {
withIcon
/>
)}
{!isAvailable ? (
<PlaceholderLine dark width={60} />
) : (
<Sub>{t(`app:time.since.${since}`)}</Sub>
)}
{!isAvailable ? <PlaceholderLine dark width={60} /> : <Sub>{t(`time.since.${since}`)}</Sub>}
</Box>
)
}
@ -82,11 +78,7 @@ export function BalanceSinceDiff(props: Props) {
withIcon
/>
)}
{!isAvailable ? (
<PlaceholderLine dark width={60} />
) : (
<Sub>{t(`app:time.since.${since}`)}</Sub>
)}
{!isAvailable ? <PlaceholderLine dark width={60} /> : <Sub>{t(`time.since.${since}`)}</Sub>}
</Box>
)
}
@ -127,7 +119,7 @@ function BalanceInfos(props: Props) {
isAvailable={isAvailable}
totalBalance={totalBalance}
>
<Sub>{t('app:dashboard.totalBalance')}</Sub>
<Sub>{t('dashboard.totalBalance')}</Sub>
</BalanceTotal>
<BalanceSincePercent
alignItems="flex-end"

2
src/components/BalanceSummary/index.js

@ -3,7 +3,7 @@
import React, { Fragment } from 'react'
import { BigNumber } from 'bignumber.js'
import moment from 'moment'
import { formatShort } from '@ledgerhq/live-common/lib/helpers/currencies'
import { formatShort } from '@ledgerhq/live-common/lib/currencies'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
import Chart from 'components/base/Chart'

2
src/components/BalanceSummary/stories.js

@ -4,7 +4,7 @@ import React from 'react'
import { storiesOf } from '@storybook/react'
import { number } from '@storybook/addon-knobs'
import { translate } from 'react-i18next'
import { getFiatCurrencyByTicker } from '@ledgerhq/live-common/lib/helpers/currencies'
import { getFiatCurrencyByTicker } from '@ledgerhq/live-common/lib/currencies'
import BalanceInfos from './BalanceInfos'

2
src/components/CalculateBalance.js

@ -6,7 +6,7 @@ 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'
import { getBalanceHistorySum } from '@ledgerhq/live-common/lib/account'
import CounterValues from 'helpers/countervalues'
import {
exchangeSettingsForAccountSelector,

2
src/components/CounterValue/stories.js

@ -1,7 +1,7 @@
// @flow
import React from 'react'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { storiesOf } from '@storybook/react'
import { number } from '@storybook/addon-knobs'

181
src/components/CurrenciesStatusBanner.js

@ -0,0 +1,181 @@
// @flow
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import styled from 'styled-components'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { colors } from 'styles/theme'
import { openURL } from 'helpers/linking'
import { CHECK_CUR_STATUS_INTERVAL } from 'config/constants'
import IconCross from 'icons/Cross'
import IconTriangleWarning from 'icons/TriangleWarning'
import IconChevronRight from 'icons/ChevronRight'
import { dismissedBannersSelector } from 'reducers/settings'
import { currenciesStatusSelector, fetchCurrenciesStatus } from 'reducers/currenciesStatus'
import { currenciesSelector } from 'reducers/accounts'
import { dismissBanner } from 'actions/settings'
import type { CurrencyStatus } from 'reducers/currenciesStatus'
import Box from 'components/base/Box'
const mapStateToProps = createStructuredSelector({
dismissedBanners: dismissedBannersSelector,
accountsCurrencies: currenciesSelector,
currenciesStatus: currenciesStatusSelector,
})
const mapDispatchToProps = {
dismissBanner,
fetchCurrenciesStatus,
}
const getItemKey = (item: CurrencyStatus) => `${item.id}_${item.nonce}`
const CloseIconContainer = styled.div`
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-bottom-left-radius: 4px;
opacity: 0.5;
&:hover {
opacity: 1;
}
`
const CloseIcon = (props: *) => (
<CloseIconContainer {...props}>
<IconCross size={16} color="white" />
</CloseIconContainer>
)
type Props = {
accountsCurrencies: Currency[],
dismissedBanners: string[],
dismissBanner: string => void,
currenciesStatus: CurrencyStatus[],
fetchCurrenciesStatus: () => Promise<void>,
t: *,
}
class CurrenciesStatusBanner extends PureComponent<Props> {
componentDidMount() {
this.pollStatus()
}
componentWillUnmount() {
this.unmounted = true
if (this.timeout) {
clearTimeout(this.timeout)
}
}
unmounted = false
timeout: *
pollStatus = async () => {
await this.props.fetchCurrenciesStatus()
if (this.unmounted) return
this.timeout = setTimeout(this.pollStatus, CHECK_CUR_STATUS_INTERVAL)
}
dismiss = item => this.props.dismissBanner(getItemKey(item))
render() {
const { dismissedBanners, accountsCurrencies, currenciesStatus, t } = this.props
const filtered = currenciesStatus.filter(
item =>
accountsCurrencies.find(cur => cur.id === item.id) &&
dismissedBanners.indexOf(getItemKey(item)) === -1,
)
if (!filtered.length) return null
return (
<Box flow={2} style={styles.container}>
{filtered.map(r => <BannerItem key={r.id} t={t} item={r} onItemDismiss={this.dismiss} />)}
</Box>
)
}
}
class BannerItem extends PureComponent<{
item: CurrencyStatus,
onItemDismiss: CurrencyStatus => void,
t: *,
}> {
onLinkClick = () => openURL(this.props.item.link)
dismiss = () => this.props.onItemDismiss(this.props.item)
render() {
const { item, t } = this.props
return (
<Box relative key={item.id} style={styles.banner}>
<CloseIcon onClick={this.dismiss} />
<Box horizontal flow={2}>
<IconTriangleWarning height={16} width={16} color="white" />
<Box shrink ff="Open Sans|SemiBold">
{item.message}
</Box>
</Box>
{item.link && <BannerItemLink t={t} onClick={this.onLinkClick} />}
</Box>
)
}
}
const UnderlinedLink = styled.span`
border-bottom: 1px solid transparent;
&:hover {
border-bottom-color: white;
}
`
const BannerItemLink = ({ t, onClick }: { t: *, onClick: void => * }) => (
<Box
mt={2}
ml={4}
flow={1}
horizontal
align="center"
cursor="pointer"
onClick={onClick}
color="white"
>
<IconChevronRight size={16} color="white" />
<UnderlinedLink>{t('common.learnMore')}</UnderlinedLink>
</Box>
)
const styles = {
container: {
position: 'fixed',
left: 32,
bottom: 32,
},
banner: {
background: colors.alertRed,
overflow: 'hidden',
borderRadius: 4,
fontSize: 13,
padding: 14,
color: 'white',
fontWeight: 'bold',
paddingRight: 50,
width: 350,
},
}
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
translate(),
)(CurrenciesStatusBanner)

68
src/components/CurrencyDownStatusAlert.js

@ -0,0 +1,68 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import type { CurrencyStatus } from 'reducers/currenciesStatus'
import { currencyDownStatus } from 'reducers/currenciesStatus'
import { openURL } from 'helpers/linking'
import Box from 'components/base/Box'
import IconTriangleWarning from 'icons/TriangleWarning'
import IconExternalLink from 'icons/ExternalLink'
type Props = {
t: *,
status: ?CurrencyStatus,
}
const CurrencyDownBox = styled(Box).attrs({
horizontal: true,
align: 'center',
color: 'white',
borderRadius: 1,
fontSize: 1,
px: 4,
py: 2,
mb: 4,
})`
background-color: ${p => p.theme.colors.alertRed};
`
const Link = styled.span`
margin-left: 5px;
margin-right: 5px;
text-decoration: underline;
cursor: pointer;
`
class CurrencyDownStatusAlert extends PureComponent<Props> {
onClick = () => {
const { status } = this.props
if (status) openURL(status.link)
}
render() {
const { status, t } = this.props
if (!status) return null
return (
<CurrencyDownBox>
<Box mr={2}>
<IconTriangleWarning height={16} width={16} />
</Box>
<Box style={{ display: 'block' }} ff="Open Sans|SemiBold" fontSize={3} horizontal shrink>
{status.message}
<Link onClick={this.onClick}>{t('common.learnMore')}</Link>
<IconExternalLink size={12} />
</Box>
</CurrencyDownBox>
)
}
}
export default connect(
createStructuredSelector({
status: currencyDownStatus,
}),
)(translate()(CurrencyDownStatusAlert))

57
src/components/CurrentAddress/index.js

@ -3,7 +3,7 @@
import React, { PureComponent } from 'react'
import { Trans, translate } from 'react-i18next'
import styled from 'styled-components'
import { encodeURIScheme } from '@ledgerhq/live-common/lib/helpers/currencies'
import { encodeURIScheme } from '@ledgerhq/live-common/lib/currencies'
import type { Account } from '@ledgerhq/live-common/lib/types'
import noop from 'lodash/noop'
@ -143,7 +143,26 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
copyFeedback: false,
}
_isUnmounted = false
componentWillUnmount() {
if (this._timeout) clearTimeout(this._timeout)
}
renderCopy = copy => {
const { t } = this.props
return (
<FooterButton
icon={<IconCopy size={16} />}
label={t('common.copyAddress')}
onClick={() => {
this.setState({ copyFeedback: true })
this._timeout = setTimeout(() => this.setState({ copyFeedback: false }), 1e3)
copy()
}}
/>
)
}
_timeout: ?TimeoutID = null
render() {
const {
@ -174,17 +193,17 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
<Label>
<Box>
{accountName ? (
<Trans i18nKey="app:currentAddress.for" parent="div">
<Trans i18nKey="currentAddress.for" parent="div">
{'Address for '}
<strong>{accountName}</strong>
</Trans>
) : (
t('app:currentAddress.title')
t('currentAddress.title')
)}
</Box>
</Label>
<Address>
{copyFeedback && <CopyFeedback>{t('app:common.addressCopied')}</CopyFeedback>}
{copyFeedback && <CopyFeedback>{t('common.addressCopied')}</CopyFeedback>}
{address}
</Address>
<Box horizontal flow={2} mt={2} alignItems="center" style={{ maxWidth: 320 }}>
@ -198,39 +217,21 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
ff="Open Sans"
>
{isAddressVerified === null
? t('app:currentAddress.messageIfUnverified', { currencyName })
? t('currentAddress.messageIfUnverified', { currencyName })
: isAddressVerified
? t('app:currentAddress.messageIfAccepted', { currencyName })
: t('app:currentAddress.messageIfSkipped', { currencyName })}
? t('currentAddress.messageIfAccepted', { currencyName })
: t('currentAddress.messageIfSkipped', { currencyName })}
</Box>
</Box>
<Footer>
{isAddressVerified !== null ? (
<FooterButton
icon={<IconRecheck size={16} />}
label={
isAddressVerified === false ? t('app:common.verify') : t('app:common.reverify')
}
label={isAddressVerified === false ? t('common.verify') : t('common.reverify')}
onClick={onVerify}
/>
) : null}
<CopyToClipboard
data={address}
render={copy => (
<FooterButton
icon={<IconCopy size={16} />}
label={t('app:common.copyAddress')}
onClick={() => {
this.setState({ copyFeedback: true })
setTimeout(() => {
if (this._isUnmounted) return
this.setState({ copyFeedback: false })
}, 1e3)
copy()
}}
/>
)}
/>
<CopyToClipboard data={address} render={this.renderCopy} />
</Footer>
</Container>
)

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

@ -69,7 +69,7 @@ class AccountCard extends PureComponent<{
render() {
const { counterValue, account, onClick, daysCount, ...props } = this.props
return (
<Wrapper onClick={this.onClick} {...props}>
<Wrapper onClick={this.onClick} {...props} data-e2e="dashboard_AccountCardWrapper">
<Box flow={4}>
<AccountCardHeader accountName={account.name} currency={account.currency} />
<Bar size={1} color="fog" />

1
src/components/DashboardPage/AccountCardList.js

@ -28,6 +28,7 @@ class AccountCardList extends Component<Props> {
justifyContent="flex-start"
alignItems="center"
style={{ margin: '0 -16px' }}
data-e2e="dashboard_AccountList"
>
{accounts
.map(account => ({

4
src/components/DashboardPage/AccountCardListHeader.js

@ -19,8 +19,8 @@ class AccountCardListHeader extends PureComponent<Props> {
return (
<Box horizontal alignItems="flex-end">
<Text color="dark" ff="Museo Sans" fontSize={6}>
{t('app:dashboard.accounts.title', { count: accountsLength })}
<Text color="dark" ff="Museo Sans" fontSize={6} data-e2e="dashboard_AccountCount">
{t('dashboard.accounts.title', { count: accountsLength })}
</Text>
<Box ml="auto" horizontal flow={1}>
<AccountsOrder />

6
src/components/DashboardPage/AccountCardPlaceholder.js

@ -31,7 +31,7 @@ class AccountCardPlaceholder extends PureComponent<{
render() {
const { t } = this.props
return (
<Wrapper>
<Wrapper data-e2e="dashboard_AccountPlaceOrder">
<Box mt={2}>
<img alt="" src={i('empty-account-tile.svg')} />
</Box>
@ -44,10 +44,10 @@ class AccountCardPlaceholder extends PureComponent<{
textAlign="center"
style={{ maxWidth: 150 }}
>
{t('app:dashboard.emptyAccountTile.desc')}
{t('dashboard.emptyAccountTile.desc')}
</Box>
<Button primary onClick={this.onAddAccounts}>
{t('app:dashboard.emptyAccountTile.createAccount')}
{t('dashboard.emptyAccountTile.createAccount')}
</Button>
</Wrapper>
)

8
src/components/DashboardPage/AccountsOrder.js

@ -91,11 +91,11 @@ class AccountsOrder extends Component<Props> {
return [
{
key: 'name',
label: t('app:dashboard.accountsOrder.name'),
label: t('dashboard.accountsOrder.name'),
},
{
key: 'balance',
label: t('app:dashboard.accountsOrder.balance'),
label: t('dashboard.accountsOrder.balance'),
},
].map(item => ({
...item,
@ -141,7 +141,7 @@ class AccountsOrder extends Component<Props> {
>
<Track onUpdate event="ChangeSort" orderAccounts={orderAccounts} />
<Text ff="Open Sans|SemiBold" fontSize={4}>
{t('app:common.sortBy')}
{t('common.sortBy')}
</Text>
<Box
alignItems="center"
@ -152,7 +152,7 @@ class AccountsOrder extends Component<Props> {
horizontal
>
<Text color="dark">
{t(`app:dashboard.accountsOrder.${this.getCurrentValue() || 'balance'}`)}
{t(`dashboard.accountsOrder.${this.getCurrentValue() || 'balance'}`)}
</Text>
<IconAngleDown size={16} />
</Box>

8
src/components/DashboardPage/CurrentGreetings.js

@ -10,18 +10,18 @@ const getCurrentGreetings = () => {
const afternoon_breakpoint = 12
const evening_breakpoint = 17
if (localTimeHour >= afternoon_breakpoint && localTimeHour < evening_breakpoint) {
return 'app:dashboard.greeting.afternoon'
return 'dashboard.greeting.afternoon'
} else if (localTimeHour >= evening_breakpoint) {
return 'app:dashboard.greeting.evening'
return 'dashboard.greeting.evening'
}
return 'app:dashboard.greeting.morning'
return 'dashboard.greeting.morning'
}
class CurrentGettings extends PureComponent<{ t: T }> {
render() {
const { t } = this.props
return (
<Text color="dark" ff="Museo Sans" fontSize={7}>
<Text color="dark" ff="Museo Sans" fontSize={7} data-e2e="dashboard_currentGettings">
{t(getCurrentGreetings())}
</Text>
)

8
src/components/DashboardPage/EmptyState.js

@ -45,9 +45,9 @@ class EmptyState extends PureComponent<Props, *> {
height="157"
/>
<Box mt={5} alignItems="center">
<Title data-e2e="dashboard_empty_title">{t('app:emptyState.dashboard.title')}</Title>
<Title data-e2e="dashboard_empty_title">{t('emptyState.dashboard.title')}</Title>
<Description mt={3} style={{ maxWidth: 600 }}>
{t('app:emptyState.dashboard.desc')}
{t('emptyState.dashboard.desc')}
</Description>
<Box mt={5} horizontal style={{ width: 300 }} flow={3} justify="center">
<Button
@ -56,7 +56,7 @@ class EmptyState extends PureComponent<Props, *> {
onClick={this.handleInstallApp}
data-e2e="dashboard_empty_OpenManager"
>
{t('app:emptyState.dashboard.buttons.installApp')}
{t('emptyState.dashboard.buttons.installApp')}
</Button>
<Button
outline
@ -64,7 +64,7 @@ class EmptyState extends PureComponent<Props, *> {
onClick={() => openModal(MODAL_ADD_ACCOUNTS)}
data-e2e="dashboard_empty_AddAccounts"
>
{t('app:emptyState.dashboard.buttons.addAccount')}
{t('emptyState.dashboard.buttons.addAccount')}
</Button>
</Box>
</Box>

9
src/components/DashboardPage/SummaryDesc.js

@ -12,8 +12,13 @@ class SummaryDesc extends PureComponent<{
render() {
const { totalAccounts, t } = this.props
return (
<Text color="grey" fontSize={5} ff="Museo Sans|Light">
{t('app:dashboard.summary', { count: totalAccounts })}
<Text
color="grey"
fontSize={5}
ff="Museo Sans|Light"
data-e2e="dashboard_accountsSummaryDesc"
>
{t('dashboard.summary', { count: totalAccounts })}
</Text>
)
}

2
src/components/DashboardPage/index.js

@ -129,7 +129,7 @@ class DashboardPage extends PureComponent<Props> {
<OperationsList
onAccountClick={this.onAccountClick}
accounts={accounts}
title={t('app:dashboard.recentActivity')}
title={t('dashboard.recentActivity')}
withAccount
/>
)}

287
src/components/DevToolsPage/AccountImporter.js

@ -0,0 +1,287 @@
// @flow
/* eslint-disable react/no-multi-comp */
import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant'
import { connect } from 'react-redux'
import type { Currency, Account, DerivationMode } from '@ledgerhq/live-common/lib/types'
import { decodeAccount } from 'reducers/accounts'
import { addAccount } from 'actions/accounts'
import FakeLink from 'components/base/FakeLink'
import Ellipsis from 'components/base/Ellipsis'
import Switch from 'components/base/Switch'
import Spinner from 'components/base/Spinner'
import Box, { Card } from 'components/base/Box'
import TranslatedError from 'components/TranslatedError'
import Button from 'components/base/Button'
import Input from 'components/base/Input'
import Label from 'components/base/Label'
import SelectCurrency from 'components/SelectCurrency'
import { CurrencyCircleIcon } from 'components/base/CurrencyBadge'
import { idleCallback } from 'helpers/promise'
import scanFromXPUB from 'commands/libcoreScanFromXPUB'
const mapDispatchToProps = {
addAccount,
}
type Props = {
addAccount: Account => void,
}
type ImportableAccountType = {
name: string,
currency: Currency,
derivationMode: DerivationMode,
xpub: string,
}
type State = {
status: string,
importableAccounts: ImportableAccountType[],
currency: ?Currency,
xpub: string,
name: string,
isSegwit: boolean,
isUnsplit: boolean,
error: ?Error,
}
const INITIAL_STATE = {
status: 'idle',
currency: null,
xpub: '',
name: 'dev',
isSegwit: true,
isUnsplit: false,
error: null,
importableAccounts: [],
}
class AccountImporter extends PureComponent<Props, State> {
state = INITIAL_STATE
onChangeCurrency = currency => {
if (currency.family !== 'bitcoin') return
this.setState({
currency,
isSegwit: !!currency.supportsSegwit,
isUnsplit: false,
})
}
onChangeXPUB = xpub => this.setState({ xpub })
onChangeSegwit = isSegwit => this.setState({ isSegwit })
onChangeUnsplit = isUnsplit => this.setState({ isUnsplit })
onChangeName = name => this.setState({ name })
isValid = () => {
const { currency, xpub, status } = this.state
return !!currency && !!xpub && status !== 'scanning'
}
scan = async () => {
this.setState({ status: 'scanning' })
const { importableAccounts } = this.state
try {
for (let i = 0; i < importableAccounts.length; i++) {
const a = importableAccounts[i]
const scanPayload = {
seedIdentifier: `dev_${a.xpub}`,
currencyId: a.currency.id,
xpub: a.xpub,
derivationMode: a.derivationMode,
}
const rawAccount = await scanFromXPUB.send(scanPayload).toPromise()
const account = decodeAccount(rawAccount)
await this.import({
...account,
name: a.name,
})
this.removeImportableAccount(a)
}
this.reset()
} catch (error) {
this.setState({ status: 'error', error })
}
}
addToScan = () => {
const { xpub, currency, isSegwit, isUnsplit, name } = this.state
const derivationMode = isSegwit
? isUnsplit
? 'segwit_unsplit'
: 'segwit'
: isUnsplit
? 'unsplit'
: ''
const importableAccount = { xpub, currency, derivationMode, name }
this.setState(({ importableAccounts }) => ({
importableAccounts: [...importableAccounts, importableAccount],
currency: null,
xpub: '',
name: 'dev',
isSegwit: true,
isUnsplit: false,
}))
}
removeImportableAccount = importableAccount => {
this.setState(({ importableAccounts }) => ({
importableAccounts: importableAccounts.filter(i => i.xpub !== importableAccount.xpub),
}))
}
import = async account => {
invariant(account, 'no account')
await idleCallback()
this.props.addAccount(account)
}
reset = () => this.setState(INITIAL_STATE)
render() {
const {
currency,
xpub,
name,
isSegwit,
isUnsplit,
status,
error,
importableAccounts,
} = this.state
const supportsSplit = !!currency && !!currency.forkedFrom
return (
<Fragment>
<Card title="Import from xpub" flow={3}>
{status === 'idle' || status === 'scanning' ? (
<Fragment>
<Box flow={1}>
<Label>{'currency'}</Label>
<SelectCurrency autoFocus value={currency} onChange={this.onChangeCurrency} />
</Box>
{currency && (currency.supportsSegwit || supportsSplit) ? (
<Box horizontal justify="flex-end" align="center" flow={3}>
{supportsSplit && (
<Box horizontal align="center" flow={1}>
<Box ff="Museo Sans|Bold" fontSize={4}>
{'unsplit'}
</Box>
<Switch isChecked={isUnsplit} onChange={this.onChangeUnsplit} />
</Box>
)}
{currency.supportsSegwit && (
<Box horizontal align="center" flow={1}>
<Box ff="Museo Sans|Bold" fontSize={4}>
{'segwit'}
</Box>
<Switch isChecked={isSegwit} onChange={this.onChangeSegwit} />
</Box>
)}
</Box>
) : null}
<Box flow={1}>
<Label>{'xpub'}</Label>
<Input
placeholder="xpub"
value={xpub}
onChange={this.onChangeXPUB}
onEnter={this.addToScan}
/>
</Box>
<Box flow={1}>
<Label>{'name'}</Label>
<Input
placeholder="name"
value={name}
onChange={this.onChangeName}
onEnter={this.addToScan}
/>
</Box>
<Box align="flex-end">
<Button primary small disabled={!this.isValid()} onClick={this.addToScan}>
{'add to scan'}
</Button>
</Box>
</Fragment>
) : status === 'error' ? (
<Box align="center" justify="center" p={5} flow={4}>
<Box>
<TranslatedError error={error} />
</Box>
<Button primary onClick={this.reset} small autoFocus>
{'Reset'}
</Button>
</Box>
) : null}
</Card>
{!!importableAccounts.length && (
<Card flow={2}>
{importableAccounts.map((acc, i) => (
<ImportableAccount
key={acc.xpub}
importableAccount={acc}
onRemove={this.removeImportableAccount}
isLoading={status === 'scanning' && i === 0}
>
{acc.xpub}
</ImportableAccount>
))}
{status !== 'scanning' && (
<Box mt={4} align="flex-start">
<Button primary onClick={this.scan}>
{'Launch scan'}
</Button>
</Box>
)}
</Card>
)}
</Fragment>
)
}
}
class ImportableAccount extends PureComponent<{
importableAccount: ImportableAccountType,
onRemove: ImportableAccountType => void,
isLoading: boolean,
}> {
remove = () => {
this.props.onRemove(this.props.importableAccount)
}
render() {
const { importableAccount, isLoading } = this.props
return (
<Box horizontal flow={2} align="center">
{isLoading && <Spinner size={16} color="rgba(0, 0, 0, 0.3)" />}
<CurrencyCircleIcon currency={importableAccount.currency} size={24} />
<Box grow ff="Rubik" fontSize={3}>
<Ellipsis>{`[${importableAccount.name}] ${importableAccount.derivationMode ||
'default'} ${importableAccount.xpub}`}</Ellipsis>
</Box>
{!isLoading && (
<FakeLink onClick={this.remove} fontSize={3}>
{'Remove'}
</FakeLink>
)}
</Box>
)
}
}
export default connect(
null,
mapDispatchToProps,
)(AccountImporter)

11
src/components/DevToolsPage/index.js

@ -0,0 +1,11 @@
import React from 'react'
import Box from 'components/base/Box'
import AccountImporter from './AccountImporter'
export default () => (
<Box flow={2}>
<AccountImporter />
</Box>
)

4
src/components/DeviceInteraction/components.js

@ -144,11 +144,11 @@ export const ErrorDescContainer = translate()(
<Box ml="auto" horizontal flow={2}>
{!!errorHelpURL && (
<FakeLink underline color="alertRed" onClick={() => openURL(errorHelpURL)}>
{t('app:common.help')}
{t('common.help')}
</FakeLink>
)}
<FakeLink underline color="alertRed" onClick={onRetry}>
{t('app:common.retry')}
{t('common.retry')}
</FakeLink>
</Box>
</Box>

21
src/components/EnsureDeviceApp.js

@ -10,8 +10,11 @@ import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react'
import logger from 'logger'
import getAddress from 'commands/getAddress'
import { createCancelablePolling } from 'helpers/promise'
import { standardDerivation } from 'helpers/derivations'
import { isSegwitPath } from 'helpers/bip32'
import {
isSegwitDerivationMode,
getDerivationScheme,
runDerivationScheme,
} from '@ledgerhq/live-common/lib/derivation'
import DeviceInteraction from 'components/DeviceInteraction'
import Text from 'components/base/Text'
@ -20,7 +23,7 @@ import IconUsb from 'icons/Usb'
import type { Device } from 'types/common'
import { WrongDeviceForAccount, CantOpenDevice, BtcUnmatchedApp } from 'config/errors'
import { WrongDeviceForAccount, CantOpenDevice, UpdateYourApp } from 'config/errors'
import { getCurrentDevice } from 'reducers/devices'
const usbIcon = <IconUsb size={16} />
@ -61,10 +64,10 @@ class EnsureDeviceApp extends Component<{
},
{
shouldThrow: (err: Error) => {
const isWrongApp = err instanceof BtcUnmatchedApp
const isWrongDevice = err instanceof WrongDeviceForAccount
const isCantOpenDevice = err instanceof CantOpenDevice
return isWrongApp || isWrongDevice || isCantOpenDevice
const isUpdateYourApp = err instanceof UpdateYourApp
return isWrongDevice || isCantOpenDevice || isUpdateYourApp
},
},
)
@ -74,7 +77,7 @@ class EnsureDeviceApp extends Component<{
const cur = account ? account.currency : currency
invariant(cur, 'No currency given')
return (
<Trans i18nKey="app:deviceConnect.step2.open" parent="div">
<Trans i18nKey="deviceConnect.step2" parent="div">
{'Open the '}
<Bold>{cur.managerAppName}</Bold>
{' app on your device'}
@ -94,7 +97,7 @@ class EnsureDeviceApp extends Component<{
{
id: 'device',
title: (
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div">
<Trans i18nKey="deviceConnect.step1" parent="div">
{'Connect and unlock your '}
<Bold>{'Ledger device'}</Bold>
</Trans>
@ -122,8 +125,8 @@ async function getAddressFromAccountOrCurrency(device, account, currency) {
currencyId: currency.id,
path: account
? account.freshAddressPath
: standardDerivation({ currency, segwit: false, x: 0 }),
segwit: account ? isSegwitPath(account.freshAddressPath) : false,
: runDerivationScheme(getDerivationScheme({ currency, derivationMode: '' }), currency),
segwit: account ? isSegwitDerivationMode(account.derivationMode) : false,
})
.toPromise()
return address

4
src/components/ExchangePage/ExchangeCard.js

@ -31,9 +31,9 @@ export default class ExchangeCard extends PureComponent<{ t: T, card: CardType }
{logo}
</Box>
<Box shrink ff="Open Sans|Regular" fontSize={4} flow={3}>
<Box>{t(`app:exchange.${id}`)}</Box>
<Box>{t(`exchange.${id}`)}</Box>
<Box horizontal align="center" color="wallet" flow={1}>
<FakeLink onClick={this.onClick}>{t('app:exchange.visitWebsite')}</FakeLink>
<FakeLink onClick={this.onClick}>{t('exchange.visitWebsite')}</FakeLink>
<ExternalLinkIcon size={14} />
</Box>
</Box>

46
src/components/ExchangePage/index.js

@ -2,9 +2,11 @@
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import shuffle from 'lodash/shuffle'
import type { T } from 'types/common'
import { urls } from 'config/urls'
import { i } from 'helpers/staticPath'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
@ -20,7 +22,7 @@ type Props = {
t: T,
}
const cards = [
const cards = shuffle([
{
key: 'coinhouse',
id: 'coinhouse',
@ -51,7 +53,43 @@ const cards = [
url: urls.paybis,
logo: <PaybisLogo width={150} height={57} />,
},
]
{
key: 'luno',
id: 'luno',
url: urls.luno,
logo: <img src={i('logos/exchanges/luno.svg')} alt="Luno" width={150} />,
},
{
key: 'shapeshift',
id: 'shapeshift',
url: urls.shapeshift,
logo: <img src={i('logos/exchanges/shapeshift.svg')} alt="Shapeshift" width={150} />,
},
{
key: 'genesis',
id: 'genesis',
url: urls.genesis,
logo: <img src={i('logos/exchanges/genesis.svg')} alt="Genesis" width={150} />,
},
{
key: 'kyberSwap',
id: 'kyberSwap',
url: urls.kyberSwap,
logo: <img src={i('logos/exchanges/kyber-swap.png')} alt="KyberSwap" width={150} />,
},
{
key: 'changeNow',
id: 'changeNow',
url: urls.changeNow,
logo: <img src={i('logos/exchanges/change-now.png')} alt="ChangeNow" width={150} />,
},
{
key: 'thorSwap',
id: 'thorSwap',
url: urls.thorSwap,
logo: <img src={i('logos/exchanges/thor-swap.png')} alt="ThorSwap" width={150} />,
},
])
class ExchangePage extends PureComponent<Props> {
render() {
@ -60,10 +98,10 @@ class ExchangePage extends PureComponent<Props> {
<Box pb={6} selectable>
<TrackPage category="Exchange" />
<Box ff="Museo Sans|Regular" fontSize={7} color="dark">
{t('app:exchange.title')}
{t('exchange.title')}
</Box>
<Box ff="Museo Sans|Light" fontSize={5} mb={5}>
{t('app:exchange.desc')}
{t('exchange.desc')}
</Box>
<Box flow={3}>{cards.map(card => <ExchangeCard key={card.key} t={t} card={card} />)}</Box>
</Box>

2
src/components/ExportLogsBtn.js

@ -78,7 +78,7 @@ class ExportLogsBtn extends Component<{
<KeyHandler keyValue="e" onKeyHandle={this.onKeyHandle} />
) : (
<Button small primary event="ExportLogs" onClick={this.handleExportLogs}>
{t('app:settings.exportLogs.btn')}
{t('settings.exportLogs.btn')}
</Button>
)
}

34
src/components/FeesField/BitcoinKind.js

@ -8,6 +8,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { FeeNotLoaded } from 'config/errors'
import InputCurrency from 'components/base/InputCurrency'
import Select from 'components/base/Select'
import type { Fees } from 'api/Fees'
@ -17,7 +18,7 @@ import Box from '../base/Box'
type Props = {
account: Account,
feePerByte: BigNumber,
feePerByte: ?BigNumber,
onChange: BigNumber => void,
t: T,
}
@ -50,6 +51,12 @@ const customItem = {
blockCount: 0,
feePerByte: BigNumber(0),
}
const notLoadedItem = {
label: 'Standard',
value: 'standard',
blockCount: 0,
feePerByte: BigNumber(0),
}
type State = { isFocused: boolean, items: FeeItem[], selectedItem: FeeItem }
@ -57,13 +64,13 @@ type OwnProps = Props & { fees?: Fees, error?: Error }
class FeesField extends Component<OwnProps, State> {
state = {
items: [customItem],
selectedItem: customItem,
items: [notLoadedItem],
selectedItem: notLoadedItem,
isFocused: false,
}
static getDerivedStateFromProps(nextProps, prevState) {
const { fees, feePerByte } = nextProps
const { fees, feePerByte, error } = nextProps
let items: FeeItem[] = []
if (fees) {
for (const key of Object.keys(fees)) {
@ -80,17 +87,18 @@ class FeesField extends Component<OwnProps, State> {
}
items = items.sort((a, b) => a.blockCount - b.blockCount)
}
items.push(customItem)
const selectedItem = prevState.selectedItem.feePerByte.eq(feePerByte)
? prevState.selectedItem
: items.find(f => f.feePerByte.eq(feePerByte)) || items[items.length - 1]
items.push(!feePerByte && !error ? notLoadedItem : customItem)
const selectedItem =
!feePerByte && prevState.selectedItem.feePerByte.eq(feePerByte)
? prevState.selectedItem
: items.find(f => f.feePerByte.eq(feePerByte)) || items[items.length - 1]
return { items, selectedItem }
}
componentDidUpdate({ fees: prevFees }: OwnProps) {
const { feePerByte, fees, onChange } = this.props
const { items, isFocused } = this.state
if (fees && fees !== prevFees && feePerByte.isZero() && !isFocused) {
if (fees && fees !== prevFees && !feePerByte && !isFocused) {
// initialize with the median
const feePerByte = (items.find(item => item.blockCount === defaultBlockCount) || items[0])
.feePerByte
@ -127,7 +135,7 @@ class FeesField extends Component<OwnProps, State> {
const satoshi = units[units.length - 1]
return (
<GenericContainer error={error}>
<GenericContainer>
<Select width={156} options={items} value={selectedItem} onChange={this.onSelectChange} />
<InputCurrency
ref={this.input}
@ -137,10 +145,10 @@ class FeesField extends Component<OwnProps, State> {
value={feePerByte}
onChange={onChange}
onChangeFocus={this.onChangeFocus}
loading={!feePerByte && !error}
error={!feePerByte && error ? new FeeNotLoaded() : null}
renderRight={
<InputRight>
{t('app:send.steps.amount.unitPerByte', { unit: satoshi.code })}
</InputRight>
<InputRight>{t('send.steps.amount.unitPerByte', { unit: satoshi.code })}</InputRight>
}
allowZero
/>

9
src/components/FeesField/EthereumKind.js

@ -4,6 +4,7 @@ import React, { Component } from 'react'
import { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { FeeNotLoaded } from 'config/errors'
import InputCurrency from 'components/base/InputCurrency'
import type { Fees } from 'api/Fees'
import WithFeesAPI from '../WithFeesAPI'
@ -11,7 +12,7 @@ import GenericContainer from './GenericContainer'
type Props = {
account: Account,
gasPrice: BigNumber,
gasPrice: ?BigNumber,
onChange: BigNumber => void,
}
@ -22,7 +23,7 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, *> {
componentDidUpdate() {
const { gasPrice, fees, onChange } = this.props
const { isFocused } = this.state
if (gasPrice.isZero() && fees && fees.gas_price && !isFocused) {
if (!gasPrice && fees && fees.gas_price && !isFocused) {
onChange(BigNumber(fees.gas_price)) // we want to set the default to gas_price
}
}
@ -33,12 +34,14 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, *> {
const { account, gasPrice, error, onChange } = this.props
const { units } = account.currency
return (
<GenericContainer error={error}>
<GenericContainer>
<InputCurrency
defaultUnit={units.length > 1 ? units[1] : units[0]}
units={units}
containerProps={{ grow: true }}
value={gasPrice}
loading={!error && !gasPrice}
error={!gasPrice && error ? new FeeNotLoaded() : null}
onChange={onChange}
onChangeFocus={this.onChangeFocus}
/>

2
src/components/FeesField/GenericContainer.js

@ -16,7 +16,7 @@ export default translate()(({ children, t }: { children: React$Node, t: * }) =>
openURL(urls.feesMoreInfo)
track('Send Flow Fees Help Requested')
}}
label={t('app:send.steps.amount.fees')}
label={t('send.steps.amount.fees')}
/>
<Box horizontal flow={5}>
{children}

9
src/components/FeesField/RippleKind.js

@ -4,12 +4,13 @@ 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 { FeeNotLoaded } from 'config/errors'
import InputCurrency from 'components/base/InputCurrency'
import GenericContainer from './GenericContainer'
type Props = {
account: Account,
fee: BigNumber,
fee: ?BigNumber,
onChange: BigNumber => void,
}
@ -36,7 +37,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.isZero()) {
if (!this.props.fee) {
this.props.onChange(serverFee)
}
} catch (error) {
@ -50,11 +51,13 @@ class FeesField extends Component<Props, State> {
const { error } = this.state
const { units } = account.currency
return (
<GenericContainer error={error}>
<GenericContainer>
<InputCurrency
defaultUnit={units[0]}
units={units}
containerProps={{ grow: true }}
loading={!error && !fee}
error={!fee && error ? new FeeNotLoaded() : null}
value={fee}
onChange={onChange}
/>

8
src/components/GenuineCheck.js

@ -124,7 +124,7 @@ class GenuineCheck extends PureComponent<Props> {
{
id: 'device',
title: (
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div">
<Trans i18nKey="deviceConnect.step1" parent="div">
{'Connect and unlock your '}
<Bold>{'Ledger device'}</Bold>
</Trans>
@ -135,10 +135,10 @@ class GenuineCheck extends PureComponent<Props> {
{
id: 'deviceInfo',
title: (
<Trans i18nKey="deviceConnect:dashboard.open" parent="div">
<Trans i18nKey="deviceConnect.step2" parent="div">
{'Navigate to the '}
<Bold>{'dashboard'}</Bold>
{' on your device'}
{' app on your device'}
</Trans>
),
icon: homeIcon,
@ -147,7 +147,7 @@ class GenuineCheck extends PureComponent<Props> {
{
id: 'isGenuine',
title: (
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div">
<Trans i18nKey="deviceConnect.step3" parent="div">
{'Allow '}
<Bold>{'Ledger Manager'}</Bold>
{' on your device'}

2
src/components/GenuineCheckModal.js

@ -20,7 +20,7 @@ class GenuineCheckModal extends PureComponent<Props> {
const { t, onSuccess, onFail, onUnavailable } = this.props
return (
<ModalBody onClose={onClose}>
<ModalTitle>{t('app:genuinecheck.modal.title')}</ModalTitle>
<ModalTitle>{t('genuinecheck.modal.title')}</ModalTitle>
<ModalContent>
<GenuineCheck onSuccess={onSuccess} onFail={onFail} onUnavailable={onUnavailable} />
</ModalContent>

2
src/components/GlobalSearch.js

@ -66,7 +66,7 @@ class GlobalSearch extends PureComponent<Props, State> {
<IconSearch size={16} />
</Box>
<Input
placeholder={t('app:common.search')}
placeholder={t('common.search')}
innerRef={input => (this._input = input)}
onBlur={this.handleBlur}
onFocus={this.handleFocus}

18
src/components/IsUnlocked.js

@ -12,7 +12,7 @@ import { i } from 'helpers/staticPath'
import IconTriangleWarning from 'icons/TriangleWarning'
import db from 'helpers/db'
import hardReset from 'helpers/hardReset'
import { hardReset } from 'helpers/reset'
import { fetchAccounts } from 'actions/accounts'
import { isLocked, unlock } from 'reducers/application'
@ -161,17 +161,17 @@ class IsUnlocked extends Component<Props, State> {
/>
}
/>
<PageTitle>{t('app:common.lockScreen.title')}</PageTitle>
<PageTitle>{t('common.lockScreen.title')}</PageTitle>
<LockScreenDesc>
{t('app:common.lockScreen.subTitle')}
{t('common.lockScreen.subTitle')}
<br />
{t('app:common.lockScreen.description')}
{t('common.lockScreen.description')}
</LockScreenDesc>
<Box horizontal align="center">
<Box style={{ width: 280 }}>
<InputPassword
autoFocus
placeholder={t('app:common.lockScreen.inputPlaceholder')}
placeholder={t('common.lockScreen.inputPlaceholder')}
type="password"
onChange={this.handleChangeInput('password')}
value={inputValue.password}
@ -187,7 +187,7 @@ class IsUnlocked extends Component<Props, State> {
</Box>
</Box>
<Button type="button" mt={3} small onClick={this.handleOpenHardResetModal}>
{t('app:common.lockScreen.lostPassword')}
{t('common.lockScreen.lostPassword')}
</Button>
</Box>
</form>
@ -199,9 +199,9 @@ class IsUnlocked extends Component<Props, State> {
onClose={this.handleCloseHardResetModal}
onReject={this.handleCloseHardResetModal}
onConfirm={this.handleHardReset}
confirmText={t('app:common.reset')}
title={t('app:settings.hardResetModal.title')}
desc={t('app:settings.hardResetModal.desc')}
confirmText={t('common.reset')}
title={t('settings.hardResetModal.title')}
desc={t('settings.hardResetModal.desc')}
renderIcon={this.hardResetIconRender}
/>
</Box>

2
src/components/MainSideBar/AddAccountButton.js

@ -33,7 +33,7 @@ export default class AddAccountButton extends PureComponent<{
return (
<Tooltip render={() => tooltipText}>
<PlusWrapper onClick={onClick}>
<IconCirclePlus size={16} />
<IconCirclePlus size={16} data-e2e="menuAddAccount_button" />
</PlusWrapper>
</Tooltip>
)

72
src/components/MainSideBar/index.js

@ -1,7 +1,7 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { Trans, translate } from 'react-i18next'
import { connect } from 'react-redux'
import { compose } from 'redux'
import { withRouter } from 'react-router'
@ -19,6 +19,7 @@ import { i } from 'helpers/staticPath'
import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { getUpdateStatus } from 'reducers/update'
import { developerModeSelector } from 'reducers/settings'
import { SideBarList, SideBarListItem } from 'components/base/SideBar'
import Box from 'components/base/Box'
@ -34,10 +35,12 @@ import IconExchange from 'icons/Exchange'
import AccountListItem from './AccountListItem'
import AddAccountButton from './AddAccountButton'
import TopGradient from './TopGradient'
import KeyboardContent from '../KeyboardContent'
const mapStateToProps = state => ({
accounts: accountsSelector(state),
updateStatus: getUpdateStatus(state),
developerMode: developerModeSelector(state),
})
const mapDispatchToProps = {
@ -52,8 +55,26 @@ type Props = {
push: string => void,
openModal: string => void,
updateStatus: UpdateStatus,
developerMode: boolean,
}
const IconDev = () => (
<div
style={{
width: 16,
height: 16,
fontSize: 10,
fontFamily: 'monospace',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{'DEV'}
</div>
)
class MainSideBar extends PureComponent<Props> {
push = (to: string) => {
const { push } = this.props
@ -66,29 +87,27 @@ class MainSideBar extends PureComponent<Props> {
push(to)
}
ADD_ACCOUNT_EMPTY_STATE = (
<Box relative pr={3}>
<img style={{ position: 'absolute', top: -10, right: 5 }} alt="" src={i('arrow-add.svg')} />
{this.props.t('app:emptyState.sidebar.text')}
</Box>
)
handleClickDashboard = () => this.push('/')
handleOpenSendModal = () => this.props.openModal(MODAL_SEND)
handleOpenReceiveModal = () => this.props.openModal(MODAL_RECEIVE)
handleClickManager = () => this.push('/manager')
handleClickExchange = () => this.push('/exchange')
handleClickDev = () => this.push('/dev')
handleOpenImportModal = () => this.props.openModal(MODAL_ADD_ACCOUNTS)
render() {
const { t, accounts, location, updateStatus } = this.props
const { t, accounts, location, updateStatus, developerMode } = this.props
const { pathname } = location
const addAccountButton = (
<AddAccountButton
tooltipText={t('app:addAccounts.title')}
onClick={this.handleOpenImportModal}
/>
<AddAccountButton tooltipText={t('addAccounts.title')} onClick={this.handleOpenImportModal} />
)
const emptyState = (
<Box relative pr={3}>
<img style={{ position: 'absolute', top: -10, right: 5 }} alt="" src={i('arrow-add.svg')} />
<Trans i18nKey="emptyState.sidebar.text" />
</Box>
)
return (
@ -96,9 +115,9 @@ class MainSideBar extends PureComponent<Props> {
<TopGradient />
<GrowScroll>
<Space of={70} />
<SideBarList title={t('app:sidebar.menu')}>
<SideBarList title={t('sidebar.menu')}>
<SideBarListItem
label={t('app:dashboard.title')}
label={t('dashboard.title')}
icon={IconPieChart}
iconActiveColor="wallet"
onClick={this.handleClickDashboard}
@ -106,39 +125,50 @@ class MainSideBar extends PureComponent<Props> {
hasNotif={updateStatus === 'downloaded'}
/>
<SideBarListItem
label={t('app:send.title')}
label={t('send.title')}
icon={IconSend}
iconActiveColor="wallet"
onClick={this.handleOpenSendModal}
disabled={accounts.length === 0}
/>
<SideBarListItem
label={t('app:receive.title')}
label={t('receive.title')}
icon={IconReceive}
iconActiveColor="wallet"
onClick={this.handleOpenReceiveModal}
disabled={accounts.length === 0}
/>
<SideBarListItem
label={t('app:sidebar.manager')}
label={t('sidebar.manager')}
icon={IconManager}
iconActiveColor="wallet"
onClick={this.handleClickManager}
isActive={pathname === '/manager'}
/>
<SideBarListItem
label={t('app:sidebar.exchange')}
label={t('sidebar.exchange')}
icon={IconExchange}
iconActiveColor="wallet"
onClick={this.handleClickExchange}
isActive={pathname === '/exchange'}
/>
{developerMode && (
<KeyboardContent sequence="DEVTOOLS">
<SideBarListItem
label={t('sidebar.developer')}
icon={IconDev}
iconActiveColor="wallet"
onClick={this.handleClickDev}
isActive={pathname === '/dev'}
/>
</KeyboardContent>
)}
</SideBarList>
<Space of={40} />
<SideBarList
title={t('app:sidebar.accounts', { count: accounts.length })}
title={t('sidebar.accounts', { count: accounts.length })}
titleRight={addAccountButton}
emptyState={this.ADD_ACCOUNT_EMPTY_STATE}
emptyState={emptyState}
>
{accounts.map(account => (
<AccountListItem

2
src/components/ManagerPage/AppSearchBar.js

@ -81,7 +81,7 @@ class AppSearchBar extends PureComponent<Props, State> {
<Space of={30} />
<Search
fuseOptions={{
threshold: 0.5,
threshold: 0.1,
keys: ['name'],
}}
value={query}

79
src/components/ManagerPage/AppsList.js

@ -7,15 +7,13 @@ import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { compose } from 'redux'
import type { Device, T } from 'types/common'
import type { Application, ApplicationVersion, DeviceInfo } from 'helpers/types'
import type { ApplicationVersion, DeviceInfo } from 'helpers/types'
import { getFullListSortedCryptoCurrencies } from 'helpers/countervalues'
import { developerModeSelector } from 'reducers/settings'
import listApps from 'commands/listApps'
import listAppVersions from 'commands/listAppVersions'
import installApp from 'commands/installApp'
import uninstallApp from 'commands/uninstallApp'
import Box from 'components/base/Box'
import Space from 'components/base/Space'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
@ -26,13 +24,11 @@ import Spinner from 'components/base/Spinner'
import Button from 'components/base/Button'
import TranslatedError from 'components/TranslatedError'
import TrackPage from 'analytics/TrackPage'
import IconInfoCircle from 'icons/InfoCircle'
import ExclamationCircleThin from 'icons/ExclamationCircleThin'
import Update from 'icons/Update'
import Trash from 'icons/Trash'
import CheckCircle from 'icons/CheckCircle'
import { FreezeDeviceChangeEvents } from './HookDeviceChange'
import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp'
import AppSearchBar from './AppSearchBar'
@ -71,6 +67,9 @@ type State = {
mode: Mode,
}
const oldAppsInstallDisabled = ['ZenCash', 'Ripple']
const canHandleInstall = c => !oldAppsInstallDisabled.includes(c.name)
const LoadingApp = () => (
<FakeManagerAppContainer noShadow align="center" justify="center" style={{ height: 90 }}>
<Spinner size={16} color="rgba(0, 0, 0, 0.3)" />
@ -99,29 +98,53 @@ class AppsList extends PureComponent<Props, State> {
_unmounted = false
filterAppVersions = (applicationsList, compatibleAppVersionsList) => {
if (!this.props.isDevMode) {
return compatibleAppVersionsList.filter(version => {
const app = applicationsList.find(e => e.id === version.app)
if (app) {
return app.category !== 2
}
prepareAppList = ({ applicationsList, compatibleAppVersionsList, sortedCryptoCurrencies }) => {
const filtered = this.props.isDevMode
? compatibleAppVersionsList.slice(0)
: compatibleAppVersionsList.filter(version => {
const app = applicationsList.find(e => e.id === version.app)
if (app) {
return app.category !== 2
}
return false
})
}
return compatibleAppVersionsList
return false
})
const sortedCryptoApps = []
// sort by crypto first
sortedCryptoCurrencies.forEach(crypto => {
const app = filtered.find(
item => item.name.toLowerCase() === crypto.managerAppName.toLowerCase(),
)
if (app) {
filtered.splice(filtered.indexOf(app), 1)
sortedCryptoApps.push(app)
}
})
return sortedCryptoApps.concat(filtered)
}
async fetchAppList() {
try {
const { deviceInfo } = this.props
const applicationsList: Array<Application> = await listApps.send().toPromise()
const compatibleAppVersionsList = await listAppVersions.send(deviceInfo).toPromise()
const filteredAppVersionsList = this.filterAppVersions(
const [
applicationsList,
compatibleAppVersionsList,
)
sortedCryptoCurrencies,
] = await Promise.all([
listApps.send().toPromise(),
listAppVersions.send(deviceInfo).toPromise(),
getFullListSortedCryptoCurrencies(),
])
const filteredAppVersionsList = this.prepareAppList({
applicationsList,
compatibleAppVersionsList,
sortedCryptoCurrencies,
})
if (!this._unmounted) {
this.setState({
@ -191,7 +214,7 @@ class AppsList extends PureComponent<Props, State> {
</ModalTitle>
<ModalContent>
<Text ff="Museo Sans|Regular" fontSize={6} color="dark">
{t(`app:manager.apps.${mode}`, { app })}
{t(`manager.apps.${mode}`, { app })}
</Text>
<Box mt={6}>
<Progress style={{ width: '100%' }} infinite />
@ -233,7 +256,7 @@ class AppsList extends PureComponent<Props, State> {
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}>
{t('app:common.close')}
{t('common.close')}
</Button>
</ModalFooter>
</Fragment>
@ -252,7 +275,7 @@ class AppsList extends PureComponent<Props, State> {
style={{ maxWidth: 350 }}
>
{t(
`app:manager.apps.${
`manager.apps.${
mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'
}`,
{ app },
@ -261,7 +284,7 @@ class AppsList extends PureComponent<Props, State> {
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}>
{t('app:common.close')}
{t('common.close')}
</Button>
</ModalFooter>
</Fragment>
@ -285,7 +308,7 @@ class AppsList extends PureComponent<Props, State> {
name={c.name}
version={`Version ${c.version}`}
icon={ICONS_FALLBACK[c.icon] || c.icon}
onInstall={this.handleInstallApp(c)}
onInstall={canHandleInstall(c) ? this.handleInstallApp(c) : null}
onUninstall={this.handleUninstallApp(c)}
/>
))}
@ -319,11 +342,11 @@ class AppsList extends PureComponent<Props, State> {
<Box flow={6}>
<Box>
<Box mb={4} color="dark" ff="Museo Sans" fontSize={5} flow={2} horizontal align="center">
<span style={{ lineHeight: 1 }}>{t('app:manager.apps.all')}</span>
<span style={{ lineHeight: 1 }}>{t('manager.apps.all')}</span>
<Tooltip
render={() => (
<Box ff="Open Sans|SemiBold" fontSize={2}>
{t('app:manager.apps.help')}
{t('manager.apps.help')}
</Box>
)}
>

6
src/components/ManagerPage/Dashboard.js

@ -27,14 +27,14 @@ const Dashboard = ({ device, deviceInfo, t, handleHelpRequest }: Props) => (
<TrackPage category="Manager" name="Dashboard" />
<Box>
<Text ff="Museo Sans|Regular" fontSize={7} color="dark">
{t('app:manager.title')}
{t('manager.title')}
</Text>
<Box horizontal>
<Text ff="Museo Sans|Light" fontSize={5}>
{t('app:manager.subtitle')}
{t('manager.subtitle')}
</Text>
<HelpLink onClick={handleHelpRequest}>
<div style={{ textDecoration: 'underline' }}>{t('app:common.needHelp')}</div>
<div style={{ textDecoration: 'underline' }}>{t('common.needHelp')}</div>
<IconExternalLink size={14} />
</HelpLink>
</Box>

8
src/components/ManagerPage/FirmwareUpdate.js

@ -136,17 +136,17 @@ class FirmwareUpdate extends PureComponent<Props, State> {
<Box horizontal align="center">
<Text ff="Open Sans|SemiBold" fontSize={4} color="dark">
{device.product === 'Blue'
? t('app:manager.firmware.titleBlue')
: t('app:manager.firmware.titleNano')}
? t('manager.firmware.titleBlue')
: t('manager.firmware.titleNano')}
</Text>
<Box color="wallet" ml={2}>
<Tooltip render={() => t('app:manager.yourDeviceIsGenuine')}>
<Tooltip render={() => t('manager.yourDeviceIsGenuine')}>
<CheckFull size={13} color="wallet" />
</Tooltip>
</Box>
</Box>
<Text ff="Open Sans|SemiBold" fontSize={2}>
{t('app:manager.firmware.installed', {
{t('manager.firmware.installed', {
version: deviceInfo.fullVersion,
})}
</Text>

26
src/components/ManagerPage/ManagerApp.js

@ -49,7 +49,7 @@ type Props = {
name: string,
version: string,
icon: string,
onInstall: Function,
onInstall?: Function,
onUninstall: Function,
}
@ -64,17 +64,19 @@ function ManagerApp({ name, version, icon, onInstall, onUninstall, t }: Props) {
{version}
</Text>
</Box>
<Button
outline
onClick={onInstall}
event={'Manager Install Click'}
eventProperties={{
appName: name,
appVersion: version,
}}
>
{t('app:manager.apps.install')}
</Button>
{onInstall ? (
<Button
outline
onClick={onInstall}
event={'Manager Install Click'}
eventProperties={{
appName: name,
appVersion: version,
}}
>
{t('manager.apps.install')}
</Button>
) : null}
<Button
outline
onClick={onUninstall}

4
src/components/ManagerPage/ManagerGenuineCheck.js

@ -31,10 +31,10 @@ class ManagerGenuineCheck extends PureComponent<Props> {
style={{ marginBottom: 30, maxWidth: 362, width: '100%' }}
/>
<Text ff="Museo Sans|Regular" fontSize={7} color="dark" style={{ marginBottom: 10 }}>
{t('app:manager.device.title')}
{t('manager.device.title')}
</Text>
<Text ff="Museo Sans|Light" fontSize={5} color="grey" align="center">
{t('app:manager.device.desc')}
{t('manager.device.desc')}
</Text>
</Box>
<Space of={40} />

6
src/components/ManagerPage/PlugYourDevice.js

@ -22,12 +22,12 @@ function PlugYourDevice(props: Props) {
<Box align="center" style={{ width: 365 }}>
<Box mb={5}>hey</Box>
<Box textAlign="center" mb={1} ff="Museo Sans|Regular" color="dark" fontSize={6}>
{t('app:manager.device.title')}
{t('manager.device.title')}
</Box>
<Box textAlign="center" mb={5} ff="Open Sans|Regular" color="smoke" fontSize={4}>
{t('app:manager.device.desc')}
{t('manager.device.desc')}
</Box>
<Button primary>{t('app:manager.device.cta')}</Button>
<Button primary>{t('manager.device.cta')}</Button>
</Box>
</Card>
)

4
src/components/ManagerPage/UpdateFirmwareButton.js

@ -23,7 +23,7 @@ const UpdateFirmwareButton = ({ t, firmware, onClick }: Props) =>
firmware ? (
<Fragment>
<Text ff="Open Sans|Regular" fontSize={4} style={{ marginLeft: 'auto', marginRight: 15 }}>
{t('app:manager.firmware.latest', { version: getCleanVersion(firmware.name) })}
{t('manager.firmware.latest', { version: getCleanVersion(firmware.name) })}
</Text>
<Button
primary
@ -33,7 +33,7 @@ const UpdateFirmwareButton = ({ t, firmware, onClick }: Props) =>
firmwareName: firmware.name,
}}
>
{t('app:manager.firmware.update')}
{t('manager.firmware.update')}
</Button>
</Fragment>
) : null

7
src/components/ManagerPage/index.js

@ -4,12 +4,11 @@ import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant'
import { openURL } from 'helpers/linking'
import { urls } from 'config/urls'
import type { Device } from 'types/common'
import type { DeviceInfo } from 'helpers/types'
import { getFullListSortedCryptoCurrencies } from 'helpers/countervalues'
import Dashboard from './Dashboard'
import ManagerGenuineCheck from './ManagerGenuineCheck'
import HookDeviceChange from './HookDeviceChange'
@ -30,6 +29,10 @@ const INITIAL_STATE = {
class ManagerPage extends PureComponent<Props, State> {
state = INITIAL_STATE
componentDidMount() {
getFullListSortedCryptoCurrencies() // start fetching the crypto currencies ordering
}
// prettier-ignore
handleSuccessGenuine = ({ device, deviceInfo }: { device: Device, deviceInfo: DeviceInfo }) => { // eslint-disable-line react/no-unused-prop-types
this.setState({ isGenuine: true, device, deviceInfo })

4
src/components/Onboarding/OnboardingFooter.js

@ -24,7 +24,7 @@ const OnboardingFooter = ({
}: Props) => (
<OnboardingFooterWrapper {...props}>
<Button outlineGrey onClick={() => prevStep()}>
{t('app:common.back')}
{t('common.back')}
</Button>
<Button
data-e2e="continue_button"
@ -33,7 +33,7 @@ const OnboardingFooter = ({
onClick={() => nextStep()}
ml="auto"
>
{t('app:common.continue')}
{t('common.continue')}
</Button>
</OnboardingFooterWrapper>
)

13
src/components/Onboarding/helperComponents.js

@ -69,6 +69,19 @@ export function OptionRow({ step, ...p }: { step: StepType }) {
</Box>
)
}
export function BulletRow({ step, ...p }: { step: StepType }) {
const { icon, desc } = step
return (
<Box horizontal my="7px">
<Box {...p} mr="7px">
{icon}
</Box>
<Box justify="center" shrink>
<OptionRowDesc>{desc}</OptionRowDesc>
</Box>
</Box>
)
}
export const OptionRowDesc = styled(Box).attrs({
ff: 'Open Sans|Regular',
fontSize: 4,

68
src/components/Onboarding/steps/Analytics.js

@ -7,10 +7,13 @@ import { saveSettings } from 'actions/settings'
import Box from 'components/base/Box'
import Switch from 'components/base/Switch'
import FakeLink from 'components/base/FakeLink'
import { Trans } from 'react-i18next'
import TrackPage from 'analytics/TrackPage'
import Track from 'analytics/Track'
import { openModal } from 'reducers/modals'
import { MODAL_SHARE_ANALYTICS, MODAL_TECHNICAL_DATA } from 'config/constants'
import { openURL } from 'helpers/linking'
import { urls } from 'config/urls'
import ShareAnalytics from '../../modals/ShareAnalytics'
import TechnicalData from '../../modals/TechnicalData'
import { Title, Description, FixedTopContainer, StepContainerInner } from '../helperComponents'
@ -26,7 +29,7 @@ type State = {
}
const INITIAL_STATE = {
analyticsToggle: false,
analyticsToggle: true,
sentryLogsToggle: true,
}
@ -46,6 +49,9 @@ class Analytics extends PureComponent<StepProps, State> {
})
}
onClickTerms = () => openURL(urls.terms)
onClickPrivacy = () => openURL(urls.privacyPolicy)
handleNavBack = () => {
const { savePassword, prevStep } = this.props
savePassword(undefined)
@ -70,13 +76,15 @@ class Analytics extends PureComponent<StepProps, State> {
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/>
<StepContainerInner>
<Title data-e2e="onboarding_title">{t('onboarding:analytics.title')}</Title>
<Description>{t('onboarding:analytics.desc')}</Description>
<Title data-e2e="onboarding_title">{t('onboarding.analytics.title')}</Title>
<Description>{t('onboarding.analytics.desc')}</Description>
<Box mt={5}>
<Container>
<Box>
<Box horizontal mb={1}>
<AnalyticsTitle>{t('onboarding:analytics.technicalData.title')}</AnalyticsTitle>
<AnalyticsTitle data-e2e="analytics_techData">
{t('onboarding.analytics.technicalData.title')}
</AnalyticsTitle>
<LearnMoreWrapper>
<FakeLink
underline
@ -84,15 +92,16 @@ class Analytics extends PureComponent<StepProps, State> {
color="smoke"
ml={2}
onClick={this.handleTechnicalDataModal}
data-e2e="analytics_techData_Link"
>
{t('app:common.learnMore')}
{t('common.learnMore')}
</FakeLink>
</LearnMoreWrapper>
</Box>
<TechnicalData />
<AnalyticsText>{t('onboarding:analytics.technicalData.desc')}</AnalyticsText>
<AnalyticsText>{t('onboarding.analytics.technicalData.desc')}</AnalyticsText>
<MandatoryText>
{t('onboarding:analytics.technicalData.mandatoryText')}
{t('onboarding.analytics.technicalData.mandatoryText')}
</MandatoryText>
</Box>
<Box justifyContent="center">
@ -102,7 +111,9 @@ class Analytics extends PureComponent<StepProps, State> {
<Container>
<Box>
<Box horizontal mb={1}>
<AnalyticsTitle>{t('onboarding:analytics.shareAnalytics.title')}</AnalyticsTitle>
<AnalyticsTitle data-e2e="analytics_shareAnalytics">
{t('onboarding.analytics.shareAnalytics.title')}
</AnalyticsTitle>
<LearnMoreWrapper>
<FakeLink
style={{ textDecoration: 'underline' }}
@ -110,13 +121,14 @@ class Analytics extends PureComponent<StepProps, State> {
color="smoke"
ml={2}
onClick={this.handleShareAnalyticsModal}
data-e2e="analytics_shareAnalytics_Link"
>
{t('app:common.learnMore')}
{t('common.learnMore')}
</FakeLink>
</LearnMoreWrapper>
<ShareAnalytics />
</Box>
<AnalyticsText>{t('onboarding:analytics.shareAnalytics.desc')}</AnalyticsText>
<AnalyticsText>{t('onboarding.analytics.shareAnalytics.desc')}</AnalyticsText>
</Box>
<Box justifyContent="center">
<Track
@ -133,9 +145,11 @@ class Analytics extends PureComponent<StepProps, State> {
<Container>
<Box>
<Box mb={1}>
<AnalyticsTitle>{t('onboarding:analytics.sentryLogs.title')}</AnalyticsTitle>
<AnalyticsTitle data-e2e="analytics_reportBugs">
{t('onboarding.analytics.sentryLogs.title')}
</AnalyticsTitle>
</Box>
<AnalyticsText>{t('onboarding:analytics.sentryLogs.desc')}</AnalyticsText>
<AnalyticsText>{t('onboarding.analytics.sentryLogs.desc')}</AnalyticsText>
</Box>
<Box justifyContent="center">
<Track
@ -149,6 +163,26 @@ class Analytics extends PureComponent<StepProps, State> {
<Switch isChecked={sentryLogsToggle} onChange={this.handleSentryLogsToggle} />
</Box>
</Container>
<Container>
<Box>
<Box mb={1}>
<AnalyticsTitle data-e2e="analytics_terms">
{t('onboarding.analytics.terms.title')}
</AnalyticsTitle>
</Box>
<AnalyticsText>
<div>
<Trans i18nKey="onboarding.analytics.terms.desc">
{'Accept the '}
<HoveredLink onClick={this.onClickTerms}>{'terms of license'}</HoveredLink>
{'and'}
<HoveredLink onClick={this.onClickPrivacy}>{'privacy'}</HoveredLink>
{'.'}
</Trans>
</div>
</AnalyticsText>
</Box>
</Container>
</Box>
</StepContainerInner>
<OnboardingFooter
@ -196,8 +230,16 @@ const Container = styled(Box).attrs({
width: 550px;
justify-content: space-between;
`
const LearnMoreWrapper = styled(Box).attrs({})`
const LearnMoreWrapper = styled(Box)`
${FakeLink}:hover {
color: ${p => p.theme.colors.wallet};
}
`
const HoveredLink = styled.span`
cursor: pointer;
text-decoration: underline;
&:hover {
color: ${p => p.theme.colors.wallet};
}
`

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

@ -103,12 +103,12 @@ export default class Finish extends Component<StepProps, *> {
</Box>
<Box pt={5} align="center">
<Title data-e2e="finish_title">{t('onboarding:finish.title')}</Title>
<Description>{t('onboarding:finish.desc')}</Description>
<Title data-e2e="finish_title">{t('onboarding.finish.title')}</Title>
<Description>{t('onboarding.finish.desc')}</Description>
</Box>
<Box p={5}>
<Button primary onClick={() => finish()} data-e2e="continue_button">
{t('onboarding:finish.openAppButton')}
{t('onboarding.finish.openAppButton')}
</Button>
</Box>
<Box horizontal mt={3} flow={5} color="grey">

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

@ -40,21 +40,21 @@ class GenuineCheckErrorPage extends PureComponent<Props, *> {
{onboarding.genuine.isGenuineFail ? (
<Fragment>
{this.trackErrorPage('Not Genuine')}
<Title>{t('onboarding:genuineCheck.errorPage.title.isGenuineFail')}</Title>
<Description>{t('onboarding:genuineCheck.errorPage.desc.isGenuineFail')}</Description>
<Title>{t('onboarding.genuineCheck.errorPage.title.isGenuineFail')}</Title>
<Description>{t('onboarding.genuineCheck.errorPage.desc.isGenuineFail')}</Description>
</Fragment>
) : !onboarding.genuine.pinStepPass ? (
<Fragment>
{this.trackErrorPage('PIN Step')}
<Title>{t('onboarding:genuineCheck.errorPage.title.pinFailed')}</Title>
<Description>{t('onboarding:genuineCheck.errorPage.desc.pinFailed')}</Description>
<Title>{t('onboarding.genuineCheck.errorPage.title.pinFailed')}</Title>
<Description>{t('onboarding.genuineCheck.errorPage.desc.pinFailed')}</Description>
</Fragment>
) : (
<Fragment>
{this.trackErrorPage('Recovery Phase Step')}
<Title>{t('onboarding:genuineCheck.errorPage.title.recoveryPhraseFailed')}</Title>
<Title>{t('onboarding.genuineCheck.errorPage.title.recoveryPhraseFailed')}</Title>
<Description>
{t('onboarding:genuineCheck.errorPage.desc.recoveryPhraseFailed')}
{t('onboarding.genuineCheck.errorPage.desc.recoveryPhraseFailed')}
</Description>
</Fragment>
)}
@ -78,12 +78,12 @@ class GenuineCheckErrorPage extends PureComponent<Props, *> {
</Box>
<OnboardingFooterWrapper>
<Button outlineGrey onClick={() => redoGenuineCheck()}>
{t('app:common.back')}
{t('common.back')}
</Button>
<ExternalLinkButton
danger
ml="auto"
label={t('onboarding:genuineCheck.buttons.contactSupport')}
label={t('onboarding.genuineCheck.buttons.contactSupport')}
url={urls.contactSupport}
/>
</OnboardingFooterWrapper>

8
src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js

@ -27,7 +27,7 @@ export function GenuineCheckUnavailableFooter({
return (
<OnboardingFooterWrapper>
<Button outlineGrey onClick={() => prevStep()}>
{t('app:common.back')}
{t('common.back')}
</Button>
<Box horizontal ml="auto">
<Button
@ -36,10 +36,10 @@ export function GenuineCheckUnavailableFooter({
onClick={() => nextStep()}
mx={2}
>
{t('app:common.skipThisStep')}
{t('common.skipThisStep')}
</Button>
<Button onClick={nextStep} disabled primary>
{t('app:common.continue')}
{t('common.continue')}
</Button>
</Box>
</OnboardingFooterWrapper>
@ -81,7 +81,7 @@ export function GenuineCheckUnavailableMessage({
})
}}
>
{t('app:common.retry')}
{t('common.retry')}
</FakeLink>
</Box>
)

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

@ -59,12 +59,12 @@ class GenuineCheck extends PureComponent<StepProps, State> {
const { t } = this.props
return [
{
label: t('app:common.labelYes'),
label: t('common.labelYes'),
key: 'yes',
pass: true,
},
{
label: t('app:common.labelNo'),
label: t('common.labelNo'),
key: 'no',
pass: false,
},
@ -174,18 +174,18 @@ class GenuineCheck extends PureComponent<StepProps, State> {
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/>
<StepContainerInner>
<Title>{t('onboarding:genuineCheck.title')}</Title>
<Title>{t('onboarding.genuineCheck.title')}</Title>
{onboarding.flowType === 'restoreDevice' ? (
<Description>{t('onboarding:genuineCheck.descRestore')}</Description>
<Description>{t('onboarding.genuineCheck.descRestore')}</Description>
) : (
<Description>{t('onboarding:genuineCheck.descGeneric')}</Description>
<Description>{t('onboarding.genuineCheck.descGeneric')}</Description>
)}
<Box mt={5}>
<GenuineCheckCardWrapper>
<Box justify="center">
<Box horizontal>
<IconOptionRow>{'1.'}</IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.step1.title')}</CardTitle>
<CardTitle>{t('onboarding.genuineCheck.step1.title')}</CardTitle>
</Box>
</Box>
<Box justify="center">
@ -204,7 +204,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<IconOptionRow color={!genuine.pinStepPass ? 'grey' : 'wallet'}>
{'2.'}
</IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.step2.title')}</CardTitle>
<CardTitle>{t('onboarding.genuineCheck.step2.title')}</CardTitle>
</Box>
</Box>
<Box justify="center">
@ -228,7 +228,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<IconOptionRow color={!genuine.recoveryStepPass ? 'grey' : 'wallet'}>
{'3.'}
</IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.step3.title')}</CardTitle>
<CardTitle>{t('onboarding.genuineCheck.step3.title')}</CardTitle>
</Box>
</Box>
{genuine.recoveryStepPass && (
@ -237,7 +237,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<Box horizontal align="center" flow={1} color={colors.wallet}>
<IconCheck size={16} />
<Box ff="Open Sans|SemiBold" fontSize={4}>
{t('onboarding:genuineCheck.isGenuinePassed')}
{t('onboarding.genuineCheck.isGenuinePassed')}
</Box>
</Box>
) : genuine.genuineCheckUnavailable ? (
@ -250,7 +250,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
disabled={!genuine.recoveryStepPass}
onClick={this.handleOpenGenuineCheckModal}
>
{t('onboarding:genuineCheck.buttons.genuineCheck')}
{t('onboarding.genuineCheck.buttons.genuineCheck')}
</Button>
)}
</Box>

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

@ -30,7 +30,7 @@ class Init extends PureComponent<StepProps, *> {
{
key: 'newDevice',
icon: <IconPlus size={20} />,
title: t('onboarding:init.newDevice.title'),
title: t('onboarding.init.newDevice.title'),
onClick: () => {
jumpStep('selectDevice')
flowType('newDevice')
@ -39,7 +39,7 @@ class Init extends PureComponent<StepProps, *> {
{
key: 'restoreDevice',
icon: <IconRecover size={20} />,
title: t('onboarding:init.restoreDevice.title'),
title: t('onboarding.init.restoreDevice.title'),
onClick: () => {
jumpStep('selectDevice')
flowType('restoreDevice')
@ -48,7 +48,7 @@ class Init extends PureComponent<StepProps, *> {
{
key: 'initializedDevice',
icon: <IconCheck size={20} />,
title: t('onboarding:init.initializedDevice.title'),
title: t('onboarding.init.initializedDevice.title'),
onClick: () => {
jumpStep('selectDevice')
flowType('initializedDevice')
@ -57,7 +57,7 @@ class Init extends PureComponent<StepProps, *> {
{
key: 'noDevice',
icon: <IconExternalLink size={20} />,
title: t('onboarding:noDevice.title'),
title: t('onboarding.noDevice.title'),
onClick: () => {
jumpStep('noDevice')
flowType('noDevice')
@ -77,7 +77,7 @@ class Init extends PureComponent<StepProps, *> {
}
/>
<Box m={5} style={{ maxWidth: 480 }}>
<Title>{t('onboarding:init.title')}</Title>
<Title>{t('onboarding.init.title')}</Title>
</Box>
<Box pt={4} flow={4}>
{optionCards.map(card => <OptionFlowCard key={card.key} card={card} />)}

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

@ -26,7 +26,7 @@ class NoDevice extends PureComponent<StepProps, *> {
{
key: 'buyNew',
icon: <IconCart size={20} />,
title: t('onboarding:noDevice.buyNew.title'),
title: t('onboarding.noDevice.buyNew.title'),
onClick: () => {
openURL(urls.noDeviceBuyNew)
},
@ -34,7 +34,7 @@ class NoDevice extends PureComponent<StepProps, *> {
{
key: 'trackOrder',
icon: <IconTruck size={20} />,
title: t('onboarding:noDevice.trackOrder.title'),
title: t('onboarding.noDevice.trackOrder.title'),
onClick: () => {
openURL(urls.noDeviceTrackOrder)
},
@ -42,7 +42,7 @@ class NoDevice extends PureComponent<StepProps, *> {
{
key: 'learnMore',
icon: <IconInfoCircle size={20} />,
title: t('onboarding:noDevice.learnMore.title'),
title: t('onboarding.noDevice.learnMore.title'),
onClick: () => {
openURL(urls.noDeviceLearnMore)
},
@ -68,7 +68,7 @@ class NoDevice extends PureComponent<StepProps, *> {
}
/>
<Box m={5} style={{ maxWidth: 480 }}>
<Title>{t('onboarding:noDevice.title')}</Title>
<Title>{t('onboarding.noDevice.title')}</Title>
</Box>
<Box pt={4} flow={4}>
{optionCards.map(card => <OptionFlowCard key={card.key} card={card} />)}
@ -77,7 +77,7 @@ class NoDevice extends PureComponent<StepProps, *> {
</GrowScroll>
<OnboardingFooterWrapper>
<Button outlineGrey onClick={() => prevStep()} mr="auto">
{t('app:common.back')}
{t('common.back')}
</Button>
</OnboardingFooterWrapper>
</Box>

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

@ -41,7 +41,7 @@ class SelectDevice extends PureComponent<StepProps, {}> {
<TrackPage category="Onboarding" name="Select Device" flowType={onboarding.flowType} />
<StepContainerInner>
<Box mb={5}>
<Title>{t('onboarding:selectDevice.title')}</Title>
<Title>{t('onboarding.selectDevice.title')}</Title>
</Box>
<Box pt={4}>
<Inner>
@ -53,7 +53,7 @@ class SelectDevice extends PureComponent<StepProps, {}> {
<DeviceIcon>
<img alt="" src={i('ledger-nano-onb.svg')} />
</DeviceIcon>
<BlockTitle>{t('onboarding:selectDevice.ledgerNanoCard.title')}</BlockTitle>
<BlockTitle>{t('onboarding.selectDevice.ledgerNanoCard.title')}</BlockTitle>
</DeviceContainer>
<DeviceContainer
isActive={!onboarding.isLedgerNano && onboarding.isLedgerNano !== null}
@ -63,7 +63,7 @@ class SelectDevice extends PureComponent<StepProps, {}> {
<DeviceIcon>
<img alt="" src={i('ledger-blue-onb.svg')} />
</DeviceIcon>
<BlockTitle>{t('onboarding:selectDevice.ledgerBlueCard.title')}</BlockTitle>
<BlockTitle>{t('onboarding.selectDevice.ledgerBlueCard.title')}</BlockTitle>
</DeviceContainer>
</Inner>
</Box>

12
src/components/Onboarding/steps/SelectPIN/SelectPINblue.js

@ -25,14 +25,14 @@ class SelectPIN extends PureComponent<Props, *> {
{
key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.initialize.instructions.blue.step1'),
desc: t('onboarding.selectPIN.initialize.instructions.blue.step1'),
},
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:selectPIN.initialize.instructions.blue.step2">
<Trans i18nKey="onboarding.selectPIN.initialize.instructions.blue.step2">
{'Tap on'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Configure as new device'}
@ -44,7 +44,7 @@ class SelectPIN extends PureComponent<Props, *> {
{
key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.initialize.instructions.blue.step3'),
desc: t('onboarding.selectPIN.initialize.instructions.blue.step3'),
},
]
@ -52,17 +52,17 @@ class SelectPIN extends PureComponent<Props, *> {
{
key: 'note1',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note1'),
desc: t('onboarding.selectPIN.disclaimer.note1'),
},
{
key: 'note2',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note2'),
desc: t('onboarding.selectPIN.disclaimer.note2'),
},
{
key: 'note3',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note3'),
desc: t('onboarding.selectPIN.disclaimer.note3'),
},
]

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

Loading…
Cancel
Save