diff --git a/package.json b/package.json index c4f6fd25..739df6a2 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@ledgerhq/hw-app-xrp": "^4.12.0", "@ledgerhq/hw-transport": "^4.12.0", "@ledgerhq/hw-transport-node-hid": "^4.12.0", - "@ledgerhq/ledger-core": "1.4.1", + "@ledgerhq/ledger-core": "1.4.3", "@ledgerhq/live-common": "^2.25.0", "axios": "^0.18.0", "babel-runtime": "^6.26.0", @@ -144,6 +144,7 @@ "flow-typed": "^2.4.0", "hard-source-webpack-plugin": "^0.6.0", "husky": "^0.14.3", + "inquirer": "^6.0.0", "jest": "^22.4.3", "js-yaml": "^3.10.0", "node-loader": "^0.6.0", diff --git a/scripts/hey.js b/scripts/hey.js new file mode 100644 index 00000000..4c361809 --- /dev/null +++ b/scripts/hey.js @@ -0,0 +1,131 @@ +require('babel-polyfill') +require('babel-register') + +const chalk = require('chalk') +const inquirer = require('inquirer') +const path = require('path') +const TransportNodeHid = require('@ledgerhq/hw-transport-node-hid').default + +const { serializeAccounts, encodeAccount, decodeAccount } = require('../src/reducers/accounts') +const { doSignAndBroadcast } = require('../src/commands/libcoreSignAndBroadcast') + +const coreHelper = require('../src/helpers/libcore') +const withLibcore = require('../src/helpers/withLibcore').default + +if (!process.env.LEDGER_LIVE_SQLITE_PATH) { + throw new Error('you must define process.env.LEDGER_LIVE_SQLITE_PATH first') +} + +const LOCAL_DIRECTORY_PATH = path.resolve(process.env.LEDGER_LIVE_SQLITE_PATH, '../') + +gimmeDeviceAndLibCore(async ({ device, core, njsWalletPool }) => { + const raw = require(path.join(LOCAL_DIRECTORY_PATH, 'accounts.json')) // eslint-disable-line import/no-dynamic-require + const accounts = serializeAccounts(raw.data) + const accountToUse = await chooseAccount('Which account to use?', accounts) + await actionLoop({ account: accountToUse, accounts, core, njsWalletPool, device }) + process.exit(0) +}) + +async function actionLoop(props) { + try { + const { account, accounts, core, njsWalletPool, device } = props + const actionToDo = await chooseAction(`What do you want to do with [${account.name}] ?`) + if (actionToDo === 'send funds') { + const transport = await TransportNodeHid.open(device.path) + const accountToReceive = await chooseAccount('To which account?', accounts) + const receiveAddress = await getFreshAddress({ + account: accountToReceive, + core, + njsWalletPool, + }) + console.log(`the receive address is ${receiveAddress}`) + const rawAccount = encodeAccount(account) + console.log(`trying to sign and broadcast...`) + const rawOp = await doSignAndBroadcast({ + account: rawAccount, + transaction: { + amount: 4200000, + recipient: receiveAddress, + feePerByte: 16, + isRBF: false, + }, + deviceId: device.path, + core, + transport, + }) + console.log(rawOp) + } else if (actionToDo === 'sync') { + console.log(`\nLaunch sync...\n`) + const rawAccount = encodeAccount(account) + const syncedAccount = await coreHelper.syncAccount({ rawAccount, core, njsWalletPool }) + console.log(`\nEnd sync...\n`) + console.log(`updated account: `, displayAccount(syncedAccount, 'red')) + } else if (actionToDo === 'quit') { + return true + } + } catch (err) { + console.log(`x Something went wrong`) + console.log(err) + process.exit(1) + } + return actionLoop(props) +} + +async function chooseInList(msg, list, formatItem = i => i) { + const choices = list.map(formatItem) + const { choice } = await inquirer.prompt([ + { + type: 'list', + name: 'choice', + message: msg, + choices, + }, + ]) + const index = choices.indexOf(choice) + return list[index] +} + +async function chooseAction(msg) { + return chooseInList(msg, ['sync', 'send funds', 'quit']) +} + +function chooseAccount(msg, accounts) { + return chooseInList(msg, accounts, acc => displayAccount(acc)) +} + +async function gimmeDeviceAndLibCore(cb) { + withLibcore((core, njsWalletPool) => { + TransportNodeHid.listen({ + error: () => {}, + complete: () => {}, + next: async e => { + if (!e.device) { + return + } + if (e.type === 'add') { + const { device } = e + cb({ device, core, njsWalletPool }) + } + }, + }) + }) +} + +function displayAccount(acc, color = null) { + const isRawAccount = typeof acc.lastSyncDate === 'string' + if (isRawAccount) { + acc = decodeAccount(acc) + } + const str = `[${acc.name}] ${acc.isSegwit ? '' : '(legacy) '}${acc.unit.code} ${acc.balance} - ${ + acc.operations.length + } txs` + return color ? chalk[color](str) : str +} + +async function getFreshAddress({ account, core, njsWalletPool }) { + const njsAccount = await coreHelper.getNJSAccount({ account, njsWalletPool }) + const unsub = await core.syncAccount(njsAccount) + unsub() + const rawAddresses = await njsAccount.getFreshPublicAddresses() + return rawAddresses[0] +} diff --git a/src/actions/accounts.js b/src/actions/accounts.js index 971832b2..fa8be05e 100644 --- a/src/actions/accounts.js +++ b/src/actions/accounts.js @@ -67,9 +67,8 @@ export const fetchAccounts: FetchAccounts = () => (dispatch, getState) => { export type UpdateAccountWithUpdater = (accountId: string, (Account) => Account) => * export const updateAccountWithUpdater: UpdateAccountWithUpdater = (accountId, updater) => ({ - type: 'UPDATE_ACCOUNT', - accountId, - updater, + type: 'DB:UPDATE_ACCOUNT', + payload: { accountId, updater }, }) export type UpdateAccount = ($Shape) => (Function, Function) => void @@ -78,9 +77,11 @@ export const updateAccount: UpdateAccount = payload => (dispatch, getState) => { settings: { orderAccounts }, } = getState() dispatch({ - type: 'UPDATE_ACCOUNT', - updater: account => ({ ...account, ...payload }), - accountId: payload.id, + type: 'DB:UPDATE_ACCOUNT', + payload: { + updater: account => ({ ...account, ...payload }), + accountId: payload.id, + }, }) dispatch(updateOrderAccounts(orderAccounts)) // TODO should not be here IMO.. feels wrong for perf, probably better to move in reducer too diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index 3d15dbec..9e0ad063 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -4,6 +4,7 @@ import { map } from 'rxjs/operators' import { decodeAccount, encodeAccount } from 'reducers/accounts' import FeesBitcoinKind from 'components/FeesField/BitcoinKind' import libcoreScanAccounts from 'commands/libcoreScanAccounts' +import libcoreSyncAccount from 'commands/libcoreSyncAccount' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' // import AdvancedOptionsBitcoinKind from 'components/AdvancedOptions/BitcoinKind' import type { WalletBridge, EditProps } from './types' @@ -49,19 +50,34 @@ const LibcoreBridge: WalletBridge = { .subscribe(observer) }, - synchronize(_initialAccount, _observer) { - // FIXME TODO: use next(), to actually emit account updates..... - // - need to sync the balance - // - need to sync block height & block hash - // - need to sync operations. - // - once all that, need to set lastSyncDate to new Date() + synchronize(account, { next, complete, error }) { + // FIXME TODO: // - when you implement addPendingOperation you also here need to: // - if there were pendingOperations that are now in operations, remove them as well. // - if there are pendingOperations that is older than a threshold (that depends on blockchain speed typically) // then we probably should trash them out? it's a complex question for UI + ;(async () => { + try { + const rawAccount = encodeAccount(account) + const rawSyncedAccount = await libcoreSyncAccount.send({ rawAccount }).toPromise() + const syncedAccount = decodeAccount(rawSyncedAccount) + next(account => ({ + ...account, + freshAddress: syncedAccount.freshAddress, + freshAddressPath: syncedAccount.freshAddressPath, + balance: syncedAccount.balance, + blockHeight: syncedAccount.blockHeight, + operations: syncedAccount.operations, // TODO: is a simple replace enough? + lastSyncDate: new Date(), + })) + complete() + } catch (e) { + error(e) + } + })() return { unsubscribe() { - console.warn('LibcoreBridge: sync not implemented') + console.warn('LibcoreBridge: unsub sync not implemented') }, } }, diff --git a/src/commands/index.js b/src/commands/index.js index 5698f374..86ae1ad5 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -16,6 +16,7 @@ import isCurrencyAppOpened from 'commands/isCurrencyAppOpened' import libcoreGetVersion from 'commands/libcoreGetVersion' import libcoreScanAccounts from 'commands/libcoreScanAccounts' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' +import libcoreSyncAccount from 'commands/libcoreSyncAccount' import listApps from 'commands/listApps' import listenDevices from 'commands/listenDevices' import signTransaction from 'commands/signTransaction' @@ -39,6 +40,7 @@ const all: Array> = [ libcoreGetVersion, libcoreScanAccounts, libcoreSignAndBroadcast, + libcoreSyncAccount, listApps, listenDevices, signTransaction, diff --git a/src/commands/libcoreGetVersion.js b/src/commands/libcoreGetVersion.js index 052079b3..e8fbd065 100644 --- a/src/commands/libcoreGetVersion.js +++ b/src/commands/libcoreGetVersion.js @@ -1,16 +1,17 @@ // @flow -import { createCommand, Command } from 'helpers/ipc' import { fromPromise } from 'rxjs/observable/fromPromise' +import { createCommand, Command } from 'helpers/ipc' +import withLibcore from 'helpers/withLibcore' + type Input = void type Result = { stringVersion: string, intVersion: number } const cmd: Command = createCommand('libcoreGetVersion', () => fromPromise( - Promise.resolve().then(() => { - const ledgerCore = require('init-ledger-core')() + withLibcore(async ledgerCore => { const core = new ledgerCore.NJSLedgerCore() const stringVersion = core.getStringVersion() const intVersion = core.getIntVersion() diff --git a/src/commands/libcoreScanAccounts.js b/src/commands/libcoreScanAccounts.js index c20ab6d1..7044220d 100644 --- a/src/commands/libcoreScanAccounts.js +++ b/src/commands/libcoreScanAccounts.js @@ -4,6 +4,7 @@ import type { AccountRaw } from '@ledgerhq/live-common/lib/types' import { createCommand, Command } from 'helpers/ipc' import { Observable } from 'rxjs' import { scanAccountsOnDevice } from 'helpers/libcore' +import withLibcore from 'helpers/withLibcore' type Input = { devicePath: string, @@ -17,19 +18,22 @@ const cmd: Command = createCommand( ({ devicePath, currencyId }) => Observable.create(o => { // TODO scanAccountsOnDevice should directly return a Observable so we just have to pass-in - scanAccountsOnDevice({ - devicePath, - currencyId, - onAccountScanned: account => { - o.next(account) - }, - }).then( - () => { - o.complete() - }, - e => { - o.error(e) - }, + withLibcore(core => + scanAccountsOnDevice({ + core, + devicePath, + currencyId, + onAccountScanned: account => { + o.next(account) + }, + }).then( + () => { + o.complete() + }, + e => { + o.error(e) + }, + ), ) function unsubscribe() { diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js index 3253a61d..130397f8 100644 --- a/src/commands/libcoreSignAndBroadcast.js +++ b/src/commands/libcoreSignAndBroadcast.js @@ -2,11 +2,14 @@ import type { AccountRaw, OperationRaw } from '@ledgerhq/live-common/lib/types' import Btc from '@ledgerhq/hw-app-btc' +import { fromPromise } from 'rxjs/observable/fromPromise' +import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' +import type Transport from '@ledgerhq/hw-transport' + +import withLibcore from 'helpers/withLibcore' import { createCommand, Command } from 'helpers/ipc' import { withDevice } from 'helpers/deviceAccess' import { getWalletIdentifier } from 'helpers/libcore' -import { fromPromise } from 'rxjs/observable/fromPromise' -import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' type BitcoinLikeTransaction = { amount: number, @@ -24,71 +27,89 @@ type Result = $Exact const cmd: Command = createCommand( 'libcoreSignAndBroadcast', - ({ account, transaction, deviceId }) => { - // TODO: investigate why importing it on file scope causes trouble - const core = require('init-ledger-core')() - - return fromPromise( - withDevice(deviceId)(async transport => { - const hwApp = new Btc(transport) - - const WALLET_IDENTIFIER = await getWalletIdentifier({ - hwApp, - isSegwit: !!account.isSegwit, - currencyId: account.currencyId, - devicePath: deviceId, - }) - - const njsWallet = await core.getWallet(WALLET_IDENTIFIER) - const njsAccount = await njsWallet.getAccount(account.index) - const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() - const njsWalletCurrency = njsWallet.getCurrency() - const amount = core.createAmount(njsWalletCurrency, transaction.amount) - const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte) - const transactionBuilder = bitcoinLikeAccount.buildTransaction() - - // TODO: check if is valid address. if not, it will fail silently on invalid - - transactionBuilder.sendToAddress(amount, transaction.recipient) - // TODO: don't use hardcoded value for sequence (and first also maybe) - transactionBuilder.pickInputs(0, 0xffffff) - transactionBuilder.setFeesPerByte(fees) - - const builded = await transactionBuilder.build() - const sigHashType = core.helpers.bytesToHex( - njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash, - ) - - const currency = getCryptoCurrencyById(account.currencyId) - const signedTransaction = await core.signTransaction({ - hwApp, - transaction: builded, - sigHashType, - supportsSegwit: currency.supportsSegwit, - isSegwit: account.isSegwit, - }) - - const txHash = await njsAccount - .asBitcoinLikeAccount() - .broadcastRawTransaction(signedTransaction) - - // optimistic operation - return { - id: txHash, - hash: txHash, - type: 'OUT', - value: amount, - fee: 0, - blockHash: null, - blockHeight: null, - senders: [account.freshAddress], - recipients: [transaction.recipient], - accountId: account.id, - date: new Date().toISOString(), - } - }), - ) - }, + ({ account, transaction, deviceId }) => + fromPromise( + withDevice(deviceId)(transport => + withLibcore(core => + doSignAndBroadcast({ + account, + transaction, + deviceId, + core, + transport, + }), + ), + ), + ), ) +export async function doSignAndBroadcast({ + account, + transaction, + deviceId, + core, + transport, +}: { + account: AccountRaw, + transaction: BitcoinLikeTransaction, + deviceId: string, + core: *, + transport: Transport<*>, +}) { + const hwApp = new Btc(transport) + + const WALLET_IDENTIFIER = await getWalletIdentifier({ + hwApp, + isSegwit: !!account.isSegwit, + currencyId: account.currencyId, + devicePath: deviceId, + }) + + const njsWallet = await core.getWallet(WALLET_IDENTIFIER) + const njsAccount = await njsWallet.getAccount(account.index) + const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() + const njsWalletCurrency = njsWallet.getCurrency() + const amount = core.createAmount(njsWalletCurrency, transaction.amount) + const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte) + const transactionBuilder = bitcoinLikeAccount.buildTransaction() + + // TODO: check if is valid address. if not, it will fail silently on invalid + + transactionBuilder.sendToAddress(amount, transaction.recipient) + // TODO: don't use hardcoded value for sequence (and first also maybe) + transactionBuilder.pickInputs(0, 0xffffff) + transactionBuilder.setFeesPerByte(fees) + + const builded = await transactionBuilder.build() + const sigHashType = core.helpers.bytesToHex( + njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash, + ) + + const currency = getCryptoCurrencyById(account.currencyId) + const signedTransaction = await core.signTransaction({ + hwApp, + transaction: builded, + sigHashType: parseInt(sigHashType, 16).toString(), + supportsSegwit: !!currency.supportsSegwit, + isSegwit: account.isSegwit, + }) + + const txHash = await njsAccount.asBitcoinLikeAccount().broadcastRawTransaction(signedTransaction) + + // optimistic operation + return { + id: txHash, + hash: txHash, + type: 'OUT', + value: amount, + fee: 0, + blockHash: null, + blockHeight: null, + senders: [account.freshAddress], + recipients: [transaction.recipient], + accountId: account.id, + date: new Date().toISOString(), + } +} + export default cmd diff --git a/src/commands/libcoreSyncAccount.js b/src/commands/libcoreSyncAccount.js new file mode 100644 index 00000000..a6a38ec6 --- /dev/null +++ b/src/commands/libcoreSyncAccount.js @@ -0,0 +1,22 @@ +// @flow + +import type { AccountRaw } from '@ledgerhq/live-common/lib/types' +import { fromPromise } from 'rxjs/observable/fromPromise' + +import { createCommand, Command } from 'helpers/ipc' +import { syncAccount } from 'helpers/libcore' +import withLibcore from 'helpers/withLibcore' + +type Input = { + rawAccount: AccountRaw, +} + +type Result = AccountRaw + +const cmd: Command = createCommand('libcoreSyncAccount', ({ rawAccount }) => + fromPromise( + withLibcore((core, njsWalletPool) => syncAccount({ rawAccount, core, njsWalletPool })), + ), +) + +export default cmd diff --git a/src/components/SideBar/Item.js b/src/components/SideBar/Item.js index 71e48124..932de6da 100644 --- a/src/components/SideBar/Item.js +++ b/src/components/SideBar/Item.js @@ -3,7 +3,7 @@ import React from 'react' import styled from 'styled-components' import { compose } from 'redux' -import { matchPath, withRouter } from 'react-router' +import { withRouter } from 'react-router' import { push } from 'react-router-redux' import { connect } from 'react-redux' @@ -91,13 +91,7 @@ function Item({ highlight, }: Props) { const { pathname } = location - const isActive = linkTo - ? linkTo === '/' - ? linkTo === pathname - : matchPath(pathname, { - path: linkTo, - }) - : false + const isActive = linkTo === pathname return ( { } } -const AccountsList = connect(state => ({ - accounts: accountsSelector(state), -}))(({ accounts }: { accounts: Account[] }) => ( +const AccountsList = compose( + withRouter, + connect( + state => ({ + accounts: accountsSelector(state), + }), + null, + null, + { pure: false }, + ), +)(({ accounts }: { accounts: Account[] }) => ( {accounts.map(account => { const Icon = getCryptoCurrencyIcon(account.currency) @@ -155,6 +164,7 @@ const AccountsList = connect(state => ({ )) export default compose( + withRouter, connect(mapStateToProps, mapDispatchToProps, null, { pure: false }), translate(), )(SideBar) diff --git a/src/components/TopBar/ActivityIndicator.js b/src/components/TopBar/ActivityIndicator.js index 39290a5d..45328010 100644 --- a/src/components/TopBar/ActivityIndicator.js +++ b/src/components/TopBar/ActivityIndicator.js @@ -18,12 +18,11 @@ const Activity = styled.div` ? p.theme.colors.alertRed : p.theme.colors.positiveGreen}; border-radius: 50%; - bottom: 20px; - height: 4px; + bottom: 23px; position: absolute; - right: 8px; - width: 4px; - cursor: pointer; + left: -5px; + width: 12px; + height: 12px; ` const mapStateToProps = createStructuredSelector({ globalSyncState: globalSyncStateSelector }) @@ -32,7 +31,7 @@ class ActivityIndicatorUI extends Component<*> { render() { const { pending, error, onClick } = this.props return ( - + diff --git a/src/components/TopBar/index.js b/src/components/TopBar/index.js index 76452029..a3871d2f 100644 --- a/src/components/TopBar/index.js +++ b/src/components/TopBar/index.js @@ -15,7 +15,6 @@ import { lock } from 'reducers/application' import { hasPassword } from 'reducers/settings' import { openModal } from 'reducers/modals' -import IconDevices from 'icons/Devices' import IconLock from 'icons/Lock' import IconSettings from 'icons/Settings' @@ -98,9 +97,6 @@ class TopBar extends PureComponent { - - - diff --git a/src/components/modals/OperationDetails.js b/src/components/modals/OperationDetails.js index 39956b29..db73a489 100644 --- a/src/components/modals/OperationDetails.js +++ b/src/components/modals/OperationDetails.js @@ -1,6 +1,7 @@ // @flow import React, { Fragment } from 'react' +import uniq from 'lodash/uniq' import { connect } from 'react-redux' import { shell } from 'electron' import { translate } from 'react-i18next' @@ -78,6 +79,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => { const isConfirmed = confirmations >= currencySettings.confirmationsNb const url = getTxURL(account, operation) + const uniqSenders = uniq(senders) return ( @@ -143,7 +145,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => { ) : null} From - {senders.map(v => {v} )} + {uniqSenders.map(v => {v})} diff --git a/src/helpers/accountId.js b/src/helpers/accountId.js new file mode 100644 index 00000000..543a8223 --- /dev/null +++ b/src/helpers/accountId.js @@ -0,0 +1,22 @@ +// @flow + +import invariant from 'invariant' + +type Params = { + type: string, + version: string, + xpub: string, + walletName: string, +} + +export function encode({ type, version, xpub, walletName }: Params) { + return `${type}:${version}:${xpub}:${walletName}` +} + +export function decode(accountId: string): Params { + invariant(typeof accountId === 'string', 'accountId is not a string') + const splitted = accountId.split(':') + invariant(splitted.length === 4, 'invalid size for accountId') + const [type, version, xpub, walletName] = splitted + return { type, version, xpub, walletName } +} diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index d37699da..bdb7ee91 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -1,13 +1,5 @@ // @flow -// Scan accounts on device -// ----------------------- -// -// _ ,--() -// ( )-'-.------|> -// " `--[] -// - import Btc from '@ledgerhq/hw-app-btc' import { withDevice } from 'helpers/deviceAccess' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' @@ -15,7 +7,10 @@ import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currenc import type { AccountRaw, OperationRaw, OperationType } from '@ledgerhq/live-common/lib/types' import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc' +import * as accountIdHelper from 'helpers/accountId' + type Props = { + core: *, devicePath: string, currencyId: string, onAccountScanned: AccountRaw => void, @@ -24,13 +19,14 @@ type Props = { const { SHOW_LEGACY_NEW_ACCOUNT } = process.env export function scanAccountsOnDevice(props: Props): Promise { - const { devicePath, currencyId, onAccountScanned } = props + const { devicePath, currencyId, onAccountScanned, core } = props const currency = getCryptoCurrencyById(currencyId) return withDevice(devicePath)(async transport => { const hwApp = new Btc(transport) const commonParams = { + core, hwApp, currencyId, onAccountScanned, @@ -78,6 +74,7 @@ export async function getWalletIdentifier({ } async function scanAccountsOnDeviceBySegwit({ + core, hwApp, currencyId, onAccountScanned, @@ -85,6 +82,7 @@ async function scanAccountsOnDeviceBySegwit({ isSegwit, showNewAccount, }: { + core: *, hwApp: Object, currencyId: string, onAccountScanned: AccountRaw => void, @@ -96,12 +94,13 @@ async function scanAccountsOnDeviceBySegwit({ const WALLET_IDENTIFIER = await getWalletIdentifier({ hwApp, isSegwit, currencyId, devicePath }) // retrieve or create the wallet - const wallet = await getOrCreateWallet(WALLET_IDENTIFIER, currencyId, isSegwit) + const wallet = await getOrCreateWallet(core, WALLET_IDENTIFIER, currencyId, isSegwit) const accountsCount = await wallet.getAccountCount() // recursively scan all accounts on device on the given app // new accounts will be created in sqlite, existing ones will be updated const accounts = await scanNextAccount({ + core, wallet, hwApp, currencyId, @@ -119,6 +118,7 @@ async function scanAccountsOnDeviceBySegwit({ async function scanNextAccount(props: { // $FlowFixMe wallet: NJSWallet, + core: *, hwApp: Object, currencyId: string, accountsCount: number, @@ -129,6 +129,7 @@ async function scanNextAccount(props: { showNewAccount: boolean, }): Promise { const { + core, wallet, hwApp, currencyId, @@ -140,11 +141,6 @@ async function scanNextAccount(props: { showNewAccount, } = props - // TODO: investigate why importing it on file scope causes trouble - const core = require('init-ledger-core')() - - console.log(`>> Scanning account ${accountIndex} - isSegwit: ${isSegwit.toString()}`) // eslint-disable-line no-console - // create account only if account has not been scanned yet // if it has already been created, we just need to get it, and sync it const hasBeenScanned = accountIndex < accountsCount @@ -168,7 +164,6 @@ async function scanNextAccount(props: { wallet, currencyId, core, - hwApp, ops, }) @@ -188,12 +183,11 @@ async function scanNextAccount(props: { } async function getOrCreateWallet( + core: *, WALLET_IDENTIFIER: string, currencyId: string, isSegwit: boolean, ): NJSWallet { - // TODO: investigate why importing it on file scope causes trouble - const core = require('init-ledger-core')() try { const wallet = await core.getWallet(WALLET_IDENTIFIER) return wallet @@ -217,7 +211,6 @@ async function buildAccountRaw({ wallet, currencyId, core, - hwApp, accountIndex, ops, }: { @@ -227,8 +220,7 @@ async function buildAccountRaw({ wallet: NJSWallet, currencyId: string, accountIndex: number, - core: Object, - hwApp: Object, + core: *, // $FlowFixMe ops: NJSOperation[], }): Promise { @@ -236,15 +228,11 @@ async function buildAccountRaw({ const balance = njsBalance.toLong() const jsCurrency = getCryptoCurrencyById(currencyId) - - // retrieve xpub const { derivations } = await wallet.getAccountCreationInfo(accountIndex) const [walletPath, accountPath] = derivations - const isVerify = false - const { bitcoinAddress } = await hwApp.getWalletPublicKey(accountPath, isVerify, isSegwit) - - const xpub = bitcoinAddress + // retrieve xpub + const xpub = njsAccount.getRestoreKey() // blockHeight const { height: blockHeight } = await njsAccount.getLastBlock() @@ -263,6 +251,8 @@ async function buildAccountRaw({ const { str: freshAddress, path: freshAddressPath } = addresses[0] + ops.sort((a, b) => b.getDate() - a.getDate()) + const operations = ops.map(op => buildOperationRaw({ core, op, xpub })) const currency = getCryptoCurrencyById(currencyId) @@ -272,7 +262,12 @@ async function buildAccountRaw({ } const rawAccount: AccountRaw = { - id: xpub, // FIXME for account id you might want to prepend the crypto currency id to this because it's not gonna be unique. + id: accountIdHelper.encode({ + type: 'libcore', + version: '1', + xpub, + walletName: wallet.getName(), + }), xpub, path: walletPath, name, @@ -298,7 +293,7 @@ function buildOperationRaw({ op, xpub, }: { - core: Object, + core: *, op: NJSOperation, xpub: string, }): OperationRaw { @@ -332,3 +327,53 @@ function buildOperationRaw({ date: op.getDate().toISOString(), } } + +export async function getNJSAccount({ + accountRaw, + njsWalletPool, +}: { + accountRaw: AccountRaw, + njsWalletPool: *, +}) { + const decodedAccountId = accountIdHelper.decode(accountRaw.id) + const njsWallet = await njsWalletPool.getWallet(decodedAccountId.walletName) + const njsAccount = await njsWallet.getAccount(accountRaw.index) + return njsAccount +} + +export async function syncAccount({ + rawAccount, + core, + njsWalletPool, +}: { + core: *, + rawAccount: AccountRaw, + njsWalletPool: *, +}) { + const decodedAccountId = accountIdHelper.decode(rawAccount.id) + const njsWallet = await njsWalletPool.getWallet(decodedAccountId.walletName) + const njsAccount = await njsWallet.getAccount(rawAccount.index) + + const unsub = await core.syncAccount(njsAccount) + unsub() + + const query = njsAccount.queryOperations() + const ops = await query.complete().execute() + const njsBalance = await njsAccount.getBalance() + + const syncedRawAccount = await buildAccountRaw({ + njsAccount, + isSegwit: rawAccount.isSegwit === true, + accountIndex: rawAccount.index, + wallet: njsWallet, + currencyId: rawAccount.currencyId, + core, + ops, + }) + + syncedRawAccount.balance = njsBalance.toLong() + + console.log(`Synced account [${syncedRawAccount.name}]: ${syncedRawAccount.balance}`) + + return syncedRawAccount +} diff --git a/src/helpers/withLibcore.js b/src/helpers/withLibcore.js new file mode 100644 index 00000000..babacb3e --- /dev/null +++ b/src/helpers/withLibcore.js @@ -0,0 +1,30 @@ +// @flow + +import invariant from 'invariant' + +const core = require('@ledgerhq/ledger-core') + +let walletPoolInstance: ?Object = null +let queue = Promise.resolve() + +// TODO: `core` and `NJSWalletPool` should be typed +type Job = (Object, Object) => Promise + +export default function withLibcore(job: Job): Promise { + if (!walletPoolInstance) { + walletPoolInstance = core.instanciateWalletPool({ + // sqlite files will be located in the app local data folder + dbPath: process.env.LEDGER_LIVE_SQLITE_PATH, + }) + } + const walletPool = walletPoolInstance + invariant(walletPool, 'core.instanciateWalletPool returned null !!') + + const p = queue.then(() => job(core, walletPool)) + + queue = p.catch(e => { + console.warn(`withLibCore: Error in job`, e) + }) + + return p +} diff --git a/src/init-ledger-core.js b/src/init-ledger-core.js deleted file mode 100644 index 61e94e88..00000000 --- a/src/init-ledger-core.js +++ /dev/null @@ -1,20 +0,0 @@ -// Yep. That's a singleton. -// -// Electron needs to tell lib ledger core where to store the sqlite files, when -// instanciating wallet pool, but we don't need to do each everytime we -// require ledger-core, only the first time, so, eh. - -const core = require('@ledgerhq/ledger-core') - -let instanciated = false - -module.exports = () => { - if (!instanciated) { - core.instanciateWalletPool({ - // sqlite files will be located in the app local data folder - dbPath: process.env.LEDGER_LIVE_SQLITE_PATH, - }) - instanciated = true - } - return core -} diff --git a/src/reducers/accounts.js b/src/reducers/accounts.js index 6536bc51..33da8838 100644 --- a/src/reducers/accounts.js +++ b/src/reducers/accounts.js @@ -30,7 +30,9 @@ const handlers: Object = { UPDATE_ACCOUNT: ( state: AccountsState, - { accountId, updater }: { accountId: string, updater: Account => Account }, + { + payload: { accountId, updater }, + }: { payload: { accountId: string, updater: Account => Account } }, ): AccountsState => state.map(existingAccount => { if (existingAccount.id !== accountId) { diff --git a/src/reducers/settings.js b/src/reducers/settings.js index bcf5eb6e..8a44a97d 100644 --- a/src/reducers/settings.js +++ b/src/reducers/settings.js @@ -63,7 +63,7 @@ const INITIAL_STATE: SettingsState = { marketIndicator: 'western', currenciesSettings: {}, region, - developerMode: false, + developerMode: !!process.env.__DEV__, loaded: false, shareAnalytics: false, } @@ -106,6 +106,7 @@ const handlers: Object = { ) => ({ ...state, ...settings, + developerMode: settings.developerMode || !!process.env.__DEV__, }), FETCH_SETTINGS: ( state: SettingsState, @@ -113,6 +114,7 @@ const handlers: Object = { ) => ({ ...state, ...settings, + developerMode: settings.developerMode || !!process.env.__DEV__, loaded: true, }), } diff --git a/static/i18n/fr/send.yml b/static/i18n/fr/send.yml index e93f1016..60948fa3 100644 --- a/static/i18n/fr/send.yml +++ b/static/i18n/fr/send.yml @@ -13,6 +13,7 @@ steps: useRBF: Utiliser la transaction RBF message: Laisser un message (140) rippleTag: Tag + ethereumGasLimit: Gas limit connectDevice: title: Connecter l'appareil verification: diff --git a/yarn.lock b/yarn.lock index 7cfd6e34..ba593aec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1482,9 +1482,9 @@ dependencies: events "^2.0.0" -"@ledgerhq/ledger-core@1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-1.4.1.tgz#c12d4a9140765731458ff1c68112818948c7f91d" +"@ledgerhq/ledger-core@1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-1.4.3.tgz#6cc44560e5a8fb35f85c8ad9fbae56436eacfc94" dependencies: "@ledgerhq/hw-app-btc" "^4.7.3" "@ledgerhq/hw-transport-node-hid" "^4.7.6" @@ -4099,6 +4099,10 @@ chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" +chardet@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.5.0.tgz#fe3ac73c00c3d865ffcc02a0682e2c20b6a06029" + charenc@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" @@ -6374,6 +6378,14 @@ external-editor@^2.0.4, external-editor@^2.1.0: iconv-lite "^0.4.17" tmp "^0.0.33" +external-editor@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.0.tgz#dc35c48c6f98a30ca27a20e9687d7f3c77704bb6" + dependencies: + chardet "^0.5.0" + iconv-lite "^0.4.22" + tmp "^0.0.33" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" @@ -7539,7 +7551,7 @@ i18next@^11.2.2: version "11.3.2" resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.3.2.tgz#4a1a7bb14383ba6aed4abca139b03681fc96e023" -iconv-lite@0.4, iconv-lite@^0.4.17, iconv-lite@^0.4.23, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.23, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" dependencies: @@ -7697,6 +7709,24 @@ inquirer@^5.2.0: strip-ansi "^4.0.0" through "^2.3.6" +inquirer@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.0.0.tgz#e8c20303ddc15bbfc2c12a6213710ccd9e1413d8" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.0" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.1.0" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + insert-css@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/insert-css/-/insert-css-2.0.0.tgz#eb5d1097b7542f4c79ea3060d3aee07d053880f4"