diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index f95d8911..3d15dbec 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -1,21 +1,20 @@ // @flow import React from 'react' -import { ipcRenderer } from 'electron' - +import { map } from 'rxjs/operators' import { decodeAccount, encodeAccount } from 'reducers/accounts' -import runJob from 'renderer/runJob' import FeesBitcoinKind from 'components/FeesField/BitcoinKind' -import AdvancedOptionsBitcoinKind from 'components/AdvancedOptions/BitcoinKind' +import libcoreScanAccounts from 'commands/libcoreScanAccounts' +import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' +// import AdvancedOptionsBitcoinKind from 'components/AdvancedOptions/BitcoinKind' import type { WalletBridge, EditProps } from './types' const notImplemented = new Error('LibcoreBridge: not implemented') -// TODO for ipcRenderer listeners we should have a concept of requestId because -// to be able to listen to events that only concerns you - -// IMPORTANT: please read ./types.js that specify & document everything - -type Transaction = * +type Transaction = { + amount: number, + feePerByte: number, + recipient: string, +} const EditFees = ({ account, onChange, value }: EditProps) => ( ) => ( /> ) +const EditAdvancedOptions = undefined // Not implemented yet +/* const EditAdvancedOptions = ({ onChange, value }: EditProps) => ( ) => ( }} /> ) +*/ const LibcoreBridge: WalletBridge = { - synchronize(initialAccount, { next, complete, error }) { - const unbind = () => ipcRenderer.removeListener('msg', handleAccountSync) - - function handleAccountSync(e, msg) { - switch (msg.type) { - case 'account.sync.progress': { - next(a => a) - // 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() - - // - 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 - break - } - case 'account.sync.fail': { - unbind() - error(new Error('failed')) // TODO more error detail - break - } - case 'account.sync.success': { - unbind() - complete() - break - } - default: - } - } - - ipcRenderer.on('msg', handleAccountSync) - - // TODO how to start the sync ?! - - return { - unsubscribe() { - unbind() - console.warn('LibcoreBridge: interrupting synchronization is not supported') - }, - } - }, - - scanAccountsOnDevice(currency, deviceId, { next, complete, error }) { - const unbind = () => ipcRenderer.removeListener('msg', handleMsgEvent) - - function handleMsgEvent(e, { data, type }) { - if (type === 'accounts.scanAccountsOnDevice.accountScanned') { - next({ ...decodeAccount(data), archived: true }) - } - } - - ipcRenderer.on('msg', handleMsgEvent) - - let unsubscribed - - runJob({ - channel: 'accounts', - job: 'scan', - successResponse: 'accounts.scanAccountsOnDevice.success', - errorResponse: 'accounts.scanAccountsOnDevice.fail', - data: { - devicePath: deviceId, + scanAccountsOnDevice(currency, devicePath, observer) { + return libcoreScanAccounts + .send({ + devicePath, currencyId: currency.id, - }, - }).then( - () => { - if (unsubscribed) return - unbind() - complete() - }, - e => { - if (unsubscribed) return - unbind() - error(e) - }, - ) + }) + .pipe(map(decodeAccount)) + .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() + // - 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 return { unsubscribe() { - unsubscribed = true - unbind() - console.warn('LibcoreBridge: interrupting scanAccounts is not implemented') // FIXME + console.warn('LibcoreBridge: sync not implemented') }, } }, @@ -165,15 +105,20 @@ const LibcoreBridge: WalletBridge = { getMaxAmount: (a, _t) => Promise.resolve(a.balance), // FIXME - signAndBroadcast: (account, transaction, deviceId) => { - const rawAccount = encodeAccount(account) - return runJob({ - channel: 'accounts', - job: 'signAndBroadcastTransactionBTCLike', - successResponse: 'accounts.signAndBroadcastTransactionBTCLike.success', - errorResponse: 'accounts.signAndBroadcastTransactionBTCLike.fail', - data: { account: rawAccount, transaction, deviceId }, - }) + signAndBroadcast: async (account, transaction, deviceId) => { + const encodedAccount = encodeAccount(account) + const rawOp = await libcoreSignAndBroadcast + .send({ + account: encodedAccount, + transaction, + deviceId, + }) + .toPromise() + + // quick HACK + const [op] = decodeAccount({ ...encodedAccount, operations: [rawOp] }).operations + + return op }, } diff --git a/src/bridge/types.js b/src/bridge/types.js index 97a6f211..7be81a50 100644 --- a/src/bridge/types.js +++ b/src/bridge/types.js @@ -15,7 +15,7 @@ export type Observer = { } export type Subscription = { - unsubscribe: () => void, + +unsubscribe: () => void, } export type EditProps = { diff --git a/src/commands/libcoreScanAccounts.js b/src/commands/libcoreScanAccounts.js new file mode 100644 index 00000000..278a4664 --- /dev/null +++ b/src/commands/libcoreScanAccounts.js @@ -0,0 +1,35 @@ +// @flow + +import type { AccountRaw } from '@ledgerhq/live-common/lib/types' +import { createCommand, Command } from 'helpers/ipc' +import { Observable } from 'rxjs' +import { scanAccountsOnDevice } from 'helpers/libcore' + +type Input = { + devicePath: string, + currencyId: string, +} + +type Result = AccountRaw + +const cmd: Command = createCommand( + 'devices', + 'libcoreScanAccounts', + ({ 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)) + + function unsubscribe() { + // FIXME not implemented + } + + return unsubscribe + }), +) + +export default cmd diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js new file mode 100644 index 00000000..310da766 --- /dev/null +++ b/src/commands/libcoreSignAndBroadcast.js @@ -0,0 +1,82 @@ +// @flow + +import type { AccountRaw, OperationRaw } from '@ledgerhq/live-common/lib/types' +import Btc from '@ledgerhq/hw-app-btc' +import { createCommand, Command } from 'helpers/ipc' +import { withDevice } from 'helpers/deviceAccess' +import { getWalletIdentifier } from 'helpers/libcore' +import { fromPromise } from 'rxjs/observable/fromPromise' + +type BitcoinLikeTransaction = { + amount: number, + feePerByte: number, + recipient: string, +} + +type Input = { + account: AccountRaw, + transaction: BitcoinLikeTransaction, + deviceId: string, +} + +type Result = $Exact + +const cmd: Command = createCommand( + 'devices', + '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 signedTransaction = await core.signTransaction(hwApp, builded) + + 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 diff --git a/src/helpers/ipc.js b/src/helpers/ipc.js index 7270b071..3494dabc 100644 --- a/src/helpers/ipc.js +++ b/src/helpers/ipc.js @@ -41,6 +41,7 @@ export class Command { }) }, error: error => { + console.log('exec error:', error) send({ type: `ERROR_${requestId}`, data: { diff --git a/src/internals/accounts/scanAccountsOnDevice.js b/src/helpers/libcore.js similarity index 98% rename from src/internals/accounts/scanAccountsOnDevice.js rename to src/helpers/libcore.js index 109c623a..bda260ac 100644 --- a/src/internals/accounts/scanAccountsOnDevice.js +++ b/src/helpers/libcore.js @@ -18,10 +18,10 @@ import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgerc type Props = { devicePath: string, currencyId: string, - onAccountScanned: Function, + onAccountScanned: AccountRaw => *, } -export default function scanAccountsOnDevice(props: Props): Promise { +export function scanAccountsOnDevice(props: Props): Promise { const { devicePath, currencyId, onAccountScanned } = props return withDevice(devicePath)(async transport => { @@ -269,7 +269,7 @@ async function buildAccountRaw({ freshAddressPath, balance, blockHeight, - archived: false, + archived: true, index: accountIndex, operations, pendingOperations: [], diff --git a/src/internals/accounts/index.js b/src/internals/accounts/index.js deleted file mode 100644 index 6a18e8e8..00000000 --- a/src/internals/accounts/index.js +++ /dev/null @@ -1,48 +0,0 @@ -// @flow - -import type { IPCSend } from 'types/electron' - -import scanAccountsOnDevice from './scanAccountsOnDevice' -import signAndBroadcastTransactionBTCLike from './signAndBroadcastTransaction/btc' - -import sync from './sync' - -export default { - sync, - signAndBroadcastTransactionBTCLike, - scan: async ( - send: IPCSend, - { - devicePath, - currencyId, - }: { - devicePath: string, - currencyId: string, - }, - ) => { - try { - send('accounts.scanAccountsOnDevice.start', { pid: process.pid }, { kill: false }) - const accounts = await scanAccountsOnDevice({ - devicePath, - currencyId, - onAccountScanned: account => { - send('accounts.scanAccountsOnDevice.accountScanned', account, { kill: false }) - }, - }) - send('accounts.scanAccountsOnDevice.success', accounts) - } catch (err) { - send('accounts.scanAccountsOnDevice.fail', formatErr(err)) - } - }, -} - -// TODO: move this to a helper -function formatErr(err) { - if (err instanceof Error) { - return err.message || err.code - } - if (typeof err === 'string') { - return err - } - return 'unknown error' -} diff --git a/src/internals/accounts/signAndBroadcastTransaction/btc.js b/src/internals/accounts/signAndBroadcastTransaction/btc.js deleted file mode 100644 index f0e88a49..00000000 --- a/src/internals/accounts/signAndBroadcastTransaction/btc.js +++ /dev/null @@ -1,71 +0,0 @@ -// @flow - -import Btc from '@ledgerhq/hw-app-btc' -import { withDevice } from 'helpers/deviceAccess' -import type { AccountRaw } from '@ledgerhq/live-common/lib/types' - -import type { IPCSend } from 'types/electron' -import { getWalletIdentifier } from '../scanAccountsOnDevice' - -type BitcoinLikeTransaction = { - amount: number, - feePerByte: number, - recipient: string, -} - -export default async function signAndBroadcastTransactionBTCLike( - send: IPCSend, - { - account, - transaction, - deviceId, // which is in fact `devicePath` - }: { - account: AccountRaw, - transaction: BitcoinLikeTransaction, - deviceId: string, - }, -) { - try { - // TODO: investigate why importing it on file scope causes trouble - const core = require('init-ledger-core')() - - const txHash = await 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 signedTransaction = await core.signTransaction(hwApp, builded) - - const txHash = await njsAccount - .asBitcoinLikeAccount() - .broadcastRawTransaction(signedTransaction) - - return txHash - }) - - send('accounts.signAndBroadcastTransactionBTCLike.success', txHash) - } catch (err) { - send('accounts.signAndBroadcastTransactionBTCLike.fail', err) - } -} diff --git a/src/internals/accounts/sync.js b/src/internals/accounts/sync.js deleted file mode 100644 index 67281475..00000000 --- a/src/internals/accounts/sync.js +++ /dev/null @@ -1,7 +0,0 @@ -// @flow - -import type { IPCSend } from 'types/electron' - -export default (send: IPCSend) => { - setTimeout(() => send('accounts.sync.success'), 5e3) -} diff --git a/src/internals/devices/index.js b/src/internals/devices/index.js index ac971950..6679260e 100644 --- a/src/internals/devices/index.js +++ b/src/internals/devices/index.js @@ -1,6 +1,8 @@ // @flow import type { Command } from 'helpers/ipc' +import libcoreScanAccounts from 'commands/libcoreScanAccounts' +import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' import getAddress from 'commands/getAddress' import signTransaction from 'commands/signTransaction' import getDeviceInfo from 'commands/getDeviceInfo' @@ -21,4 +23,6 @@ export const commands: Array> = [ getIsGenuine, getLatestFirmwareForDevice, installApp, + libcoreScanAccounts, + libcoreSignAndBroadcast, ]