diff --git a/package.json b/package.json index 67328fc4..84f1a205 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@ledgerhq/hw-app-xrp": "^4.13.0", "@ledgerhq/hw-transport": "^4.13.0", "@ledgerhq/hw-transport-node-hid": "^4.13.0", - "@ledgerhq/ledger-core": "1.9.0", + "@ledgerhq/ledger-core": "2.0.0-rc.1", "@ledgerhq/live-common": "2.30.0", "async": "^2.6.1", "axios": "^0.18.0", diff --git a/src/commands/libcoreGetFees.js b/src/commands/libcoreGetFees.js index b08ec777..9aea3046 100644 --- a/src/commands/libcoreGetFees.js +++ b/src/commands/libcoreGetFees.js @@ -33,14 +33,20 @@ const cmd: Command = createCommand( withLibcore(async core => { const { walletName } = accountIdHelper.decode(accountId) - const njsWallet = await core.getWallet(walletName) + const njsWallet = await core.getPoolInstance().getWallet(walletName) if (isCancelled()) return const njsAccount = await njsWallet.getAccount(accountIndex) if (isCancelled()) return const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() const njsWalletCurrency = njsWallet.getCurrency() - const amount = core.createAmount(njsWalletCurrency, transaction.amount) - const feesPerByte = core.createAmount(njsWalletCurrency, transaction.feePerByte) + const amount = new core.NJSAmount(njsWalletCurrency, transaction.amount).fromLong( + njsWalletCurrency, + transaction.amount, + ) + const feesPerByte = new core.NJSAmount(njsWalletCurrency, transaction.feePerByte).fromLong( + njsWalletCurrency, + transaction.feePerByte, + ) const transactionBuilder = bitcoinLikeAccount.buildTransaction() if (!isValidAddress(core, njsWalletCurrency, transaction.recipient)) { // FIXME this is a bug in libcore. later it will probably check this and we can remove this check diff --git a/src/commands/libcoreHardReset.js b/src/commands/libcoreHardReset.js index 7da0f3c6..a195d520 100644 --- a/src/commands/libcoreHardReset.js +++ b/src/commands/libcoreHardReset.js @@ -6,9 +6,9 @@ import withLibcore from 'helpers/withLibcore' const cmd = createCommand('libcoreHardReset', () => Observable.create(o => { - withLibcore(async (core, njsWalletPool) => { + withLibcore(async core => { try { - njsWalletPool.eraseDataSince(new Date(0)) + core.getPoolInstance().eraseDataSince(new Date(0)) o.complete() } catch (e) { o.error(e) diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js index 489f3759..63740653 100644 --- a/src/commands/libcoreSignAndBroadcast.js +++ b/src/commands/libcoreSignAndBroadcast.js @@ -153,14 +153,20 @@ export async function doSignAndBroadcast({ onOperationBroadcasted: (optimisticOp: $Exact) => void, }): Promise { const { walletName } = accountIdHelper.decode(account.id) - const njsWallet = await core.getWallet(walletName) + const njsWallet = await core.getPoolInstance().getWallet(walletName) if (isCancelled()) return const njsAccount = await njsWallet.getAccount(account.index) if (isCancelled()) return const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() const njsWalletCurrency = njsWallet.getCurrency() - const amount = core.createAmount(njsWalletCurrency, transaction.amount) - const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte) + const amount = new core.NJSAmount(njsWalletCurrency, transaction.amount).fromLong( + njsWalletCurrency, + transaction.amount, + ) + const fees = new core.NJSAmount(njsWalletCurrency, transaction.feePerByte).fromLong( + njsWalletCurrency, + transaction.feePerByte, + ) const transactionBuilder = bitcoinLikeAccount.buildTransaction() // TODO: check if is valid address. if not, it will fail silently on invalid diff --git a/src/commands/libcoreSyncAccount.js b/src/commands/libcoreSyncAccount.js index a6a38ec6..d2677b9e 100644 --- a/src/commands/libcoreSyncAccount.js +++ b/src/commands/libcoreSyncAccount.js @@ -14,9 +14,7 @@ type Input = { type Result = AccountRaw const cmd: Command = createCommand('libcoreSyncAccount', ({ rawAccount }) => - fromPromise( - withLibcore((core, njsWalletPool) => syncAccount({ rawAccount, core, njsWalletPool })), - ), + fromPromise(withLibcore(core => syncAccount({ rawAccount, core }))), ) export default cmd diff --git a/src/commands/libcoreValidAddress.js b/src/commands/libcoreValidAddress.js index fc563d35..7693f092 100644 --- a/src/commands/libcoreValidAddress.js +++ b/src/commands/libcoreValidAddress.js @@ -15,7 +15,7 @@ const cmd: Command = createCommand( ({ currencyId, address }) => fromPromise( withLibcore(async core => { - const currency = await core.getCurrency(currencyId) + const currency = await core.getPoolInstance().getCurrency(currencyId) return isValidAddress(core, currency, address) }), ), diff --git a/src/helpers/init-libcore.js b/src/helpers/init-libcore.js new file mode 100644 index 00000000..f1df2ee0 --- /dev/null +++ b/src/helpers/init-libcore.js @@ -0,0 +1,193 @@ +// @flow + +import logger from 'logger' +import invariant from 'invariant' +import network from 'api/network' + +const lib = require('@ledgerhq/ledger-core') + +const crypto = require('crypto') +const path = require('path') +const fs = require('fs') + +const MAX_RANDOM = 2684869021 + +const bytesArrayToString = (bytesArray = []) => Buffer.from(bytesArray).toString() + +const stringToBytesArray = str => Array.from(Buffer.from(str)) + +const NJSExecutionContextImpl = { + execute: runnable => { + try { + runnable.run() + } catch (e) { + logger.log(e) + } + }, + delay: (runnable, ms) => setTimeout(() => runnable.run(), ms), +} + +const ThreadContexts = {} + +const getSerialExecutionContext = name => { + let currentContext = ThreadContexts[name] + if (!currentContext) { + currentContext = new lib.NJSExecutionContext(NJSExecutionContextImpl) + ThreadContexts[name] = currentContext + } + return currentContext +} + +const NJSThreadDispatcher = new lib.NJSThreadDispatcher({ + contexts: ThreadContexts, + getThreadPoolExecutionContext: name => getSerialExecutionContext(name), + getMainExecutionContext: () => getSerialExecutionContext('main'), + getSerialExecutionContext, + newLock: () => { + logger.warn('libcore NJSThreadDispatcher: newLock: Not implemented') + }, +}) + +function createHttpConnection(res, err) { + if (!res) { + return null + } + const headersMap = new Map() + Object.keys(res.headers).forEach(key => { + if (typeof res.headers[key] === 'string') { + headersMap.set(key, res.headers[key]) + } + }) + const NJSHttpUrlConnectionImpl = { + getStatusCode: () => Number(res.status), + getStatusText: () => res.statusText, + getHeaders: () => headersMap, + readBody: () => ({ + error: err ? { code: 0, message: 'something went wrong' } : null, + data: stringToBytesArray(JSON.stringify(res.data)), + }), + } + return new lib.NJSHttpUrlConnection(NJSHttpUrlConnectionImpl) +} + +const NJSHttpClient = new lib.NJSHttpClient({ + execute: async r => { + const method = r.getMethod() + const headersMap = r.getHeaders() + let data = r.getBody() + if (Array.isArray(data)) { + const dataStr = bytesArrayToString(data) + try { + data = JSON.parse(dataStr) + } catch (e) { + // not a json !? + } + } + const url = r.getUrl() + const headers = {} + headersMap.forEach((v, k) => { + headers[k] = v + }) + let res + try { + // $FlowFixMe + res = await network({ method: lib.METHODS[method], url, headers, data }) + const urlConnection = createHttpConnection(res) + r.complete(urlConnection, null) + } catch (err) { + const urlConnection = createHttpConnection(res, err.message) + r.complete(urlConnection, { code: 0, message: err.message }) + } + }, +}) + +const NJSWebSocketClient = new lib.NJSWebSocketClient({ + connect: (url, connection) => { + connection.OnConnect() + }, + send: (connection, data) => { + connection.OnMessage(data) + }, + disconnect: connection => { + connection.OnClose() + }, +}) + +const NJSLogPrinter = new lib.NJSLogPrinter({ + context: {}, + printError: message => logger.libcore('Error', message), + printInfo: message => logger.libcore('Info', message), + printDebug: message => logger.libcore('Debug', message), + printWarning: message => logger.libcore('Warning', message), + printApdu: message => logger.libcore('Apdu', message), + printCriticalError: message => logger.libcore('CriticalError', message), + getContext: () => NJSThreadDispatcher.getMainExecutionContext(), +}) + +const NJSRandomNumberGenerator = new lib.NJSRandomNumberGenerator({ + getRandomBytes: size => crypto.randomBytes(size), + getRandomInt: () => Math.random() * MAX_RANDOM, + getRandomLong: () => Math.random() * MAX_RANDOM * MAX_RANDOM, +}) + +const NJSDatabaseBackend = new lib.NJSDatabaseBackend() +const NJSDynamicObject = new lib.NJSDynamicObject() + +let walletPoolInstance = null + +const instanciateWalletPool = ({ dbPath }) => { + try { + fs.mkdirSync(dbPath) + } catch (err) { + if (err.code !== 'EEXIST') { + throw err + } + } + + const NJSPathResolver = new lib.NJSPathResolver({ + resolveLogFilePath: pathToResolve => { + const hash = pathToResolve.replace(/\//g, '__') + return path.resolve(dbPath, `./log_file_${hash}`) + }, + resolvePreferencesPath: pathToResolve => { + const hash = pathToResolve.replace(/\//g, '__') + return path.resolve(dbPath, `./preferences_${hash}`) + }, + resolveDatabasePath: pathToResolve => { + const hash = pathToResolve.replace(/\//g, '__') + return path.resolve(dbPath, `./database_${hash}`) + }, + }) + + walletPoolInstance = new lib.NJSWalletPool( + 'ledger_live_desktop', + '', + NJSHttpClient, + NJSWebSocketClient, + NJSPathResolver, + NJSLogPrinter, + NJSThreadDispatcher, + NJSRandomNumberGenerator, + NJSDatabaseBackend, + NJSDynamicObject, + ) + + return walletPoolInstance +} + +const getPoolInstance = () => { + if (!walletPoolInstance) { + instanciateWalletPool({ + // sqlite files will be located in the app local data folder + dbPath: process.env.LEDGER_LIVE_SQLITE_PATH, + }) + } + invariant(walletPoolInstance, "can't initialize walletPoolInstance") + return walletPoolInstance +} + +export default { + ...lib, + getSerialExecutionContext, + getPoolInstance, +} diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index 5607fda8..b04049c3 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -123,6 +123,58 @@ async function scanAccountsOnDeviceBySegwit({ return accounts } +const hexToBytes = str => Array.from(Buffer.from(str, 'hex')) + +const createAccount = async (wallet, hwApp) => { + const accountCreationInfos = await wallet.getNextAccountCreationInfo() + await accountCreationInfos.derivations.reduce( + (promise, derivation) => + promise.then(async () => { + const { publicKey, chainCode } = await hwApp.getWalletPublicKey(derivation) + accountCreationInfos.publicKeys.push(hexToBytes(publicKey)) + accountCreationInfos.chainCodes.push(hexToBytes(chainCode)) + }), + Promise.resolve(), + ) + return wallet.newAccountWithInfo(accountCreationInfos) +} + +function createEventReceiver(core, cb) { + return new core.NJSEventReceiver({ + onEvent: event => cb(event), + }) +} + +function subscribeToEventBus(core, eventBus, receiver) { + eventBus.subscribe(core.getSerialExecutionContext('main'), receiver) +} + +const coreSyncAccount = (core, account) => + new Promise((resolve, reject) => { + const eventReceiver = createEventReceiver(core, e => { + const code = e.getCode() + if (code === core.EVENT_CODE.UNDEFINED || code === core.EVENT_CODE.SYNCHRONIZATION_FAILED) { + const payload = e.getPayload() + const message = ( + (payload && payload.getString('EV_SYNC_ERROR_MESSAGE')) || + 'Sync failed' + ).replace(' (EC_PRIV_KEY_INVALID_FORMAT)', '') + reject(new Error(message)) + return + } + if ( + code === core.EVENT_CODE.SYNCHRONIZATION_SUCCEED || + code === core.EVENT_CODE.SYNCHRONIZATION_SUCCEED_ON_PREVIOUSLY_EMPTY_ACCOUNT + ) { + resolve(() => { + eventBus.unsubscribe(eventReceiver) + }) + } + }) + const eventBus = account.synchronize() + subscribeToEventBus(core, eventBus, eventReceiver) + }) + async function scanNextAccount(props: { // $FlowFixMe wallet: NJSWallet, @@ -155,11 +207,11 @@ async function scanNextAccount(props: { const njsAccount = hasBeenScanned ? await wallet.getAccount(accountIndex) - : await core.createAccount(wallet, hwApp) + : await createAccount(wallet, hwApp) const shouldSyncAccount = true // TODO: let's sync everytime. maybe in the future we can optimize. if (shouldSyncAccount) { - await core.syncAccount(njsAccount) + await coreSyncAccount(core, njsAccount) } const query = njsAccount.queryOperations() @@ -190,25 +242,38 @@ async function scanNextAccount(props: { return scanNextAccount({ ...props, accountIndex: accountIndex + 1 }) } +const createWalletConfig = (core, configMap = {}) => { + const config = new core.NJSDynamicObject() + for (const i in configMap) { + if (configMap.hasOwnProperty(i)) { + config.putString(i, configMap[i]) + } + } + return config +} + async function getOrCreateWallet( core: *, WALLET_IDENTIFIER: string, currencyId: string, isSegwit: boolean, ): NJSWallet { + const pool = core.getPoolInstance() try { - const wallet = await core.getWallet(WALLET_IDENTIFIER) + const wallet = await pool.getWallet(WALLET_IDENTIFIER) return wallet } catch (err) { - const currency = await core.getCurrency(currencyId) + const currency = await pool.getCurrency(currencyId) const walletConfig = isSegwit ? { KEYCHAIN_ENGINE: 'BIP49_P2SH', KEYCHAIN_DERIVATION_SCHEME: "49'/'/'//
", } : undefined - const njsWalletConfig = core.createWalletConfig(walletConfig) - const wallet = await core.createWallet(WALLET_IDENTIFIER, currency, njsWalletConfig) + const njsWalletConfig = createWalletConfig(core, walletConfig) + const wallet = await core + .getPoolInstance() + .createWallet(WALLET_IDENTIFIER, currency, njsWalletConfig) return wallet } } @@ -342,33 +407,12 @@ function buildOperationRaw({ } } -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: *, -}) { +export async function syncAccount({ rawAccount, core }: { core: *, rawAccount: AccountRaw }) { const decodedAccountId = accountIdHelper.decode(rawAccount.id) - const njsWallet = await njsWalletPool.getWallet(decodedAccountId.walletName) + const njsWallet = await core.getPoolInstance().getWallet(decodedAccountId.walletName) const njsAccount = await njsWallet.getAccount(rawAccount.index) - const unsub = await core.syncAccount(njsAccount) + const unsub = await coreSyncAccount(core, njsAccount) unsub() const query = njsAccount.queryOperations() diff --git a/src/helpers/withLibcore.js b/src/helpers/withLibcore.js index 9a8ea130..e2d42054 100644 --- a/src/helpers/withLibcore.js +++ b/src/helpers/withLibcore.js @@ -1,26 +1,10 @@ // @flow -import invariant from 'invariant' -import network from 'api/network' - -const core = require('@ledgerhq/ledger-core') - -core.setHttpQueryImplementation(network) - -let walletPoolInstance: ?Object = null - -// TODO: `core` and `NJSWalletPool` should be typed -type Job = (Object, Object) => Promise +// TODO: `core` should be typed +type Job = 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 !!') - - return job(core, walletPool) + const core = require('./init-libcore').default + core.getPoolInstance() + return job(core) } diff --git a/src/logger.js b/src/logger.js index c48d8f40..e9f47e1a 100644 --- a/src/logger.js +++ b/src/logger.js @@ -51,6 +51,7 @@ const logCmds = !__DEV__ || process.env.DEBUG_COMMANDS const logDb = !__DEV__ || process.env.DEBUG_DB const logRedux = !__DEV__ || process.env.DEBUG_ACTION const logTabkey = !__DEV__ || process.env.DEBUG_TAB_KEY +const logLibcore = !__DEV__ || process.env.DEBUG_LIBCORE export default { onCmd: (type: string, id: string, spentTime: number, data?: any) => { @@ -103,6 +104,13 @@ export default { addLog('keydown', msg) }, + libcore: (level: string, msg: string) => { + if (logLibcore) { + console.log(`🛠 ${level}: ${msg}`) + } + addLog('action', `🛠 ${level}: ${msg}`) + }, + // General functions in case the hooks don't apply log: (...args: any) => { diff --git a/yarn.lock b/yarn.lock index 98e3bbef..93960899 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1502,9 +1502,9 @@ dependencies: events "^2.0.0" -"@ledgerhq/ledger-core@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-1.9.0.tgz#3240857e76f3c2b17ba02d120b96fd1b13b50789" +"@ledgerhq/ledger-core@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-2.0.0-rc.1.tgz#0b31f7d2c693b9c11d4093dbb0896f13c33bf141" dependencies: "@ledgerhq/hw-app-btc" "^4.7.3" "@ledgerhq/hw-transport-node-hid" "^4.7.6"