Browse Source

Merge pull request #429 from meriadec/libcore-sync

Libcore synchronisation
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
72fb7dd09b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      package.json
  2. 131
      scripts/hey.js
  3. 13
      src/actions/accounts.js
  4. 30
      src/bridge/LibcoreBridge.js
  5. 2
      src/commands/index.js
  6. 7
      src/commands/libcoreGetVersion.js
  7. 30
      src/commands/libcoreScanAccounts.js
  8. 153
      src/commands/libcoreSignAndBroadcast.js
  9. 22
      src/commands/libcoreSyncAccount.js
  10. 10
      src/components/SideBar/Item.js
  11. 16
      src/components/SideBar/index.js
  12. 11
      src/components/TopBar/ActivityIndicator.js
  13. 4
      src/components/TopBar/index.js
  14. 4
      src/components/modals/OperationDetails.js
  15. 22
      src/helpers/accountId.js
  16. 103
      src/helpers/libcore.js
  17. 30
      src/helpers/withLibcore.js
  18. 20
      src/init-ledger-core.js
  19. 4
      src/reducers/accounts.js
  20. 4
      src/reducers/settings.js
  21. 38
      yarn.lock

3
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.24.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",

131
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]
}

13
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<Account>) => (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

30
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<Transaction> = {
.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')
},
}
},

2
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<Command<any, any>> = [
libcoreGetVersion,
libcoreScanAccounts,
libcoreSignAndBroadcast,
libcoreSyncAccount,
listApps,
listenDevices,
signTransaction,

7
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<Input, Result> = 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()

30
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<Input, Result> = 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() {

153
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,70 +27,88 @@ type Result = $Exact<OperationRaw>
const cmd: Command<Input, Result> = 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,
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,
blockHash: null,
blockHeight: null,
senders: [account.freshAddress],
recipients: [transaction.recipient],
accountId: account.id,
date: new Date().toISOString(),
}
}
export default cmd

22
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<Input, Result> = createCommand('libcoreSyncAccount', ({ rawAccount }) =>
fromPromise(
withLibcore((core, njsWalletPool) => syncAccount({ rawAccount, core, njsWalletPool })),
),
)
export default cmd

10
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 (
<Container
big={big}

16
src/components/SideBar/index.js

@ -4,6 +4,7 @@ import React, { PureComponent, Fragment } from 'react'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react'
import type { Account } from '@ledgerhq/live-common/lib/types'
@ -124,9 +125,17 @@ class SideBar extends PureComponent<Props> {
}
}
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[] }) => (
<Fragment>
{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)

11
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 (
<ItemContainer relative onClick={onClick}>
<ItemContainer cursor="pointer" relative onClick={onClick}>
<IconActivity size={16} />
<Activity pending={pending} error={error} />
</ItemContainer>

4
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<Props> {
<Inner>
<Box grow horizontal>
<GlobalSearch t={t} isHidden />
<ItemContainer justifyContent="center">
<IconDevices size={16} />
</ItemContainer>
<ActivityIndicator />
<Box justifyContent="center">
<Bar />

4
src/components/modals/OperationDetails.js

@ -1,6 +1,7 @@
// @flow
import React 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 (
<ModalBody onClose={onClose}>
@ -132,7 +134,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
<B />
<Line>
<ColLeft>From</ColLeft>
<ColRight>{senders.map(v => <CanSelect key={v}> {v} </CanSelect>)}</ColRight>
<ColRight>{uniqSenders.map(v => <CanSelect key={v}>{v}</CanSelect>)}</ColRight>
</Line>
<B />
<Line>

22
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 }
}

103
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<AccountRaw[]> {
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<AccountRaw[]> {
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<AccountRaw> {
@ -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 {
@ -330,3 +325,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
}

30
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<A> = (Object, Object) => Promise<A>
export default function withLibcore<A>(job: Job<A>): Promise<A> {
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
}

20
src/init-ledger-core.js

@ -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
}

4
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) {

4
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,
}),
}

38
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"

Loading…
Cancel
Save