Browse Source

Merge pull request #347 from meriadec/btc-like-transaction

Btc like transaction & fix confirm address
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
6351e488a1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 141
      alix.js
  2. 4
      package.json
  3. 10
      src/bridge/EthereumJSBridge.js
  4. 14
      src/bridge/LibcoreBridge.js
  5. 2
      src/bridge/makeMockBridge.js
  6. 6
      src/bridge/types.js
  7. 22
      src/components/CurrentAddressForAccount.js
  8. 66
      src/components/DeviceCheckAddress.js
  9. 2
      src/components/DeviceSignTransaction.js
  10. 4
      src/components/modals/Receive/03-step-confirm-address.js
  11. 25
      src/components/modals/Receive/04-step-receive-funds.js
  12. 3
      src/helpers/getAddressForCurrency/index.js
  13. 3
      src/internals/accounts/index.js
  14. 62
      src/internals/accounts/scanAccountsOnDevice.js
  15. 71
      src/internals/accounts/signAndBroadcastTransaction/btc.js
  16. 1
      src/internals/index.js
  17. 4
      src/reducers/accounts.js
  18. 2
      src/renderer/runJob.js
  19. 12
      yarn.lock

141
alix.js

@ -1,141 +0,0 @@
/* eslint-disable no-console */
const CommNodeHid = require('@ledgerhq/hw-transport-node-hid').default
const Btc = require('@ledgerhq/hw-app-btc').default
const { CREATE } = process.env
const {
createWallet,
createAccount,
createAmount,
getCurrency,
getWallet,
syncAccount,
signTransaction,
} = require('@ledgerhq/ledger-core')
async function getOrCreateWallet(currencyId) {
try {
const wallet = await getWallet(currencyId)
return wallet
} catch (err) {
const currency = await getCurrency(currencyId)
const wallet = await createWallet(currencyId, currency)
return wallet
}
}
async function scanNextAccount(wallet, hwApp, accountIndex = 0) {
console.log(`creating an account with index ${accountIndex}`)
const account = await createAccount(wallet, hwApp)
console.log(`synchronizing account ${accountIndex}`)
await syncAccount(account)
console.log(`finished sync`)
const utxoCount = await account.asBitcoinLikeAccount().getUTXOCount()
console.log(`utxoCount = ${utxoCount}`)
}
async function scanAccountsOnDevice(props) {
try {
const { devicePath, currencyId } = props
console.log(`get or create wallet`)
const wallet = await getOrCreateWallet(currencyId)
console.log(`open device`)
const transport = await CommNodeHid.open(devicePath)
console.log(`create app`)
const hwApp = new Btc(transport)
console.log(`scan account`)
const accounts = await scanNextAccount(wallet, hwApp)
console.log(accounts)
return []
} catch (err) {
console.log(err)
}
}
waitForDevices(async device => {
// const accounts = await scanAccountsOnDevice({
// devicePath: device.path,
// currencyId: 'bitcoin_testnet',
// })
// console.log(accounts)
try {
console.log(`> Creating transport`)
const transport = await CommNodeHid.open(device.path)
// transport.setDebugMode(true)
console.log(`> Instanciate BTC app`)
const hwApp = new Btc(transport)
console.log(`> Get currency`)
const currency = await getCurrency('bitcoin_testnet')
console.log(`> Create wallet`)
const wallet = CREATE ? await createWallet('khalil', currency) : await getWallet('khalil')
console.log(`> Create account`)
const account = CREATE ? await createAccount(wallet, hwApp) : await wallet.getAccount(0)
console.log(`> Sync account`)
if (CREATE) {
await syncAccount(account)
}
console.log(`> Create transaction`)
const transaction = await createTransaction(wallet, account)
const signedTransaction = await signTransaction(hwApp, transaction)
await account.asBitcoinLikeAccount().broadcastRawTransaction(signedTransaction)
// console.log(signedTransaction);
process.exit(0)
// console.log(account.getIndex());
// console.log(account.isSynchronizing());
} catch (err) {
console.log(err.message)
process.exit(1)
}
})
function waitForDevices(onDevice) {
console.log(`> Waiting for device...`)
CommNodeHid.listen({
error: () => {},
complete: () => {},
next: async e => {
if (!e.device) {
return
}
if (e.type === 'add') {
console.log(`> Detected ${e.device.manufacturer} ${e.device.product}`)
onDevice(e.device)
}
if (e.type === 'remove') {
console.log(`removed ${JSON.stringify(e)}`)
}
},
})
}
async function createTransaction(wallet, account) {
const ADDRESS_TO_SEND = 'n2jdejywRogCunR2ozZAfXp1jMnfGpGXGR'
const bitcoinLikeAccount = account.asBitcoinLikeAccount()
const walletCurrency = wallet.getCurrency()
const amount = createAmount(walletCurrency, 10000)
console.log(`--------------------------------`)
console.log(amount.toLong())
console.log(`-----------------after `)
const fees = createAmount(walletCurrency, 1000)
const transactionBuilder = bitcoinLikeAccount.buildTransaction()
transactionBuilder.sendToAddress(amount, ADDRESS_TO_SEND)
// TODO: don't use hardcoded value for sequence (and first also maybe)
transactionBuilder.pickInputs(0, 0xffffff)
transactionBuilder.setFeesPerByte(fees)
return transactionBuilder.build()
}

4
package.json

@ -41,7 +41,7 @@
"@ledgerhq/hw-transport": "^4.12.0",
"@ledgerhq/hw-transport-node-hid": "^4.12.0",
"@ledgerhq/ledger-core": "^1.0.1",
"@ledgerhq/live-common": "^2.7.2",
"@ledgerhq/live-common": "^2.7.5",
"axios": "^0.18.0",
"babel-runtime": "^6.26.0",
"bcryptjs": "^2.4.3",
@ -84,7 +84,7 @@
"redux-thunk": "^2.2.0",
"reselect": "^3.0.1",
"rxjs": "^6.2.0",
"rxjs-compat": "^6.2.0",
"rxjs-compat": "^6.1.0",
"smooth-scrollbar": "^8.2.7",
"source-map": "0.7.2",
"source-map-support": "^0.5.4",

10
src/bridge/EthereumJSBridge.js

@ -97,12 +97,12 @@ const EthereumBridge: WalletBridge<Transaction> = {
const account: Account = {
id: accountId,
xpub: '',
path,
path, // FIXME we probably not want the address path in the account.path
walletPath: String(index),
name: 'New Account',
isSegwit: false,
address,
addresses: [address],
addresses: [{ str: address, path, }],
balance,
blockHeight: currentBlock.height,
archived: true,
@ -128,12 +128,12 @@ const EthereumBridge: WalletBridge<Transaction> = {
const account: Account = {
id: accountId,
xpub: '',
path,
path, // FIXME we probably not want the address path in the account.path
walletPath: String(index),
name: address.slice(32),
isSegwit: false,
address,
addresses: [address],
addresses: [{ str: address, path, }],
balance,
blockHeight: currentBlock.height,
archived: true,
@ -263,7 +263,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
getMaxAmount: (a, t) => Promise.resolve(a.balance - t.gasPrice),
signAndBroadcast: async (a, t, deviceId) => {
signAndBroadcast: async ({ account: a, transaction: t, deviceId }) => {
const api = apiForCurrency(a.currency)
const nonce = await api.getAccountNonce(a.address)

14
src/bridge/LibcoreBridge.js

@ -1,7 +1,8 @@
// @flow
import React from 'react'
import { ipcRenderer } from 'electron'
import { decodeAccount } from 'reducers/accounts'
import { decodeAccount, encodeAccount } from 'reducers/accounts'
import runJob from 'renderer/runJob'
import FeesBitcoinKind from 'components/FeesField/BitcoinKind'
import AdvancedOptionsBitcoinKind from 'components/AdvancedOptions/BitcoinKind'
@ -153,7 +154,16 @@ const LibcoreBridge: WalletBridge<Transaction> = {
getMaxAmount: (a, t) => Promise.resolve(a.balance - t.feePerByte),
signAndBroadcast: () => Promise.reject(notImplemented),
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 },
})
},
}
export default LibcoreBridge

2
src/bridge/makeMockBridge.js

@ -146,7 +146,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
getMaxAmount,
signAndBroadcast: async (account, t) => {
signAndBroadcast: async ({ account, transaction: t }) => {
const rng = new Prando()
const op = genOperation(account, account.operations, account.currency, rng)
op.amount = -t.amount

6
src/bridge/types.js

@ -98,5 +98,9 @@ export interface WalletBridge<Transaction> {
* NOTE: in future, when transaction balance is close to account.balance, we could wipe it all at this level...
* to implement that, we might want to have special logic `account.balance-transaction.amount < dust` but not sure where this should leave (i would say on UI side because we need to inform user visually).
*/
signAndBroadcast(account: Account, transaction: Transaction, deviceId: DeviceId): Promise<string>;
signAndBroadcast({
account: Account,
transaction: Transaction,
deviceId: DeviceId,
}): Promise<string>;
}

22
src/components/CurrentAddressForAccount.js

@ -0,0 +1,22 @@
// @flow
import React from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import CurrentAddress from 'components/CurrentAddress'
type Props = {
account: Account,
}
export default function CurrentAddressForAccount(props: Props) {
const { account, ...p } = props
// TODO: handle other cryptos than BTC-like
let freshAddress = account.addresses[0]
if (!freshAddress) {
freshAddress = { str: '', path: '' }
}
return <CurrentAddress accountName={account.name} address={freshAddress.str} {...p} />
}

66
src/components/DeviceCheckAddress.js

@ -27,35 +27,47 @@ class CheckAddress extends PureComponent<Props, State> {
this.verifyAddress({ device, account })
}
componentDidUnmount() {
if (this.sub) this.sub.unsubscribe()
componentWillUnmount() {
this._isUnmounted = true
}
sub: *
verifyAddress = ({ device, account }: { device: Device, account: Account }) => {
this.sub = getAddress
.send({
currencyId: account.currency.id,
devicePath: device.path,
path: account.path,
segwit: account.isSegwit,
verify: true,
})
.subscribe({
next: () => {
this.setState({
isVerified: true,
})
this.props.onCheck(true)
},
error: () => {
this.setState({
isVerified: false,
})
this.props.onCheck(false)
},
})
_isUnmounted = false
safeSetState = (...args: *) => {
if (this._isUnmounted) {
return
}
this.setState(...args)
}
verifyAddress = async ({ device, account }: { device: Device, account: Account }) => {
try {
// TODO: this will work only for BTC-like accounts
const freshAddress = account.addresses[0]
if (!freshAddress) {
throw new Error('Account doesnt have fresh addresses')
}
const { address } = await getAddress
.send({
currencyId: account.currency.id,
devicePath: device.path,
path: freshAddress.path,
segwit: account.isSegwit,
verify: true,
})
.toPromise()
if (address !== freshAddress.str) {
throw new Error('Confirmed address is different')
}
this.safeSetState({ isVerified: true })
this.props.onCheck(true)
} catch (err) {
this.safeSetState({ isVerified: false })
this.props.onCheck(false)
}
}
render() {

2
src/components/DeviceSignTransaction.js

@ -34,7 +34,7 @@ class DeviceSignTransaction extends PureComponent<Props, State> {
sign = async () => {
const { device, account, transaction, bridge, onSuccess } = this.props
try {
const txid = await bridge.signAndBroadcast(account, transaction, device.path)
const txid = await bridge.signAndBroadcast({ account, transaction, deviceId: device.path })
onSuccess(txid)
} catch (error) {
this.setState({ error })

4
src/components/modals/Receive/03-step-confirm-address.js

@ -7,7 +7,7 @@ import type { Account } from '@ledgerhq/live-common/lib/types'
import type { Device, T } from 'types/common'
import Box from 'components/base/Box'
import CurrentAddress from 'components/CurrentAddress'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import DeviceConfirm from 'components/DeviceConfirm'
import DeviceCheckAddress from 'components/DeviceCheckAddress'
@ -50,7 +50,7 @@ export default (props: Props) => (
<Fragment>
<Title>{props.t('receive:steps.confirmAddress.action')}</Title>
<Text>{props.t('receive:steps.confirmAddress.text')}</Text>
{props.account && <CurrentAddress address={props.account.address} />}
{props.account && <CurrentAddressForAccount account={props.account} />}
{props.device &&
props.account && (
<Box mb={2} mt={-1}>

25
src/components/modals/Receive/04-step-receive-funds.js

@ -6,7 +6,7 @@ import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import CurrentAddress from 'components/CurrentAddress'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import Label from 'components/base/Label'
import RequestAmount from 'components/RequestAmount'
@ -30,16 +30,17 @@ export default (props: Props) => (
withMax={false}
/>
</Box>
<CurrentAddress
accountName={props.account && props.account.name}
address={props.account && props.account.address}
addressVerified={props.addressVerified}
amount={props.amount}
onVerify={props.onVerify}
withBadge
withFooter
withQRCode
withVerify={props.addressVerified === false}
/>
{props.account && (
<CurrentAddressForAccount
account={props.account}
addressVerified={props.addressVerified}
amount={props.amount}
onVerify={props.onVerify}
withBadge
withFooter
withQRCode
withVerify={props.addressVerified === false}
/>
)}
</Box>
)

3
src/helpers/getAddressForCurrency/index.js

@ -26,6 +26,7 @@ const all = {
ethereum_classic_testnet: ethereum,
}
const getAddressForCurrency: Module = (currencyId: string) => all[currencyId] || fallback(currencyId)
const getAddressForCurrency: Module = (currencyId: string) =>
all[currencyId] || fallback(currencyId)
export default getAddressForCurrency

3
src/internals/accounts/index.js

@ -3,11 +3,13 @@
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,
{
@ -29,7 +31,6 @@ export default {
})
send('accounts.scanAccountsOnDevice.success', accounts)
} catch (err) {
console.log(err)
send('accounts.scanAccountsOnDevice.fail', formatErr(err))
}
},

62
src/internals/accounts/scanAccountsOnDevice.js

@ -46,19 +46,39 @@ export default async function scanAccountsOnDevice(props: Props): Promise<Accoun
return accounts
}
async function scanAccountsOnDeviceBySegwit({
export async function getWalletIdentifier({
hwApp,
isSegwit,
currencyId,
onAccountScanned,
devicePath,
isSegwit,
}) {
// compute wallet identifier
}: {
hwApp: Object,
isSegwit: boolean,
currencyId: string,
devicePath: string,
}): Promise<string> {
const isVerify = false
const deviceIdentifiers = await hwApp.getWalletPublicKey(devicePath, isVerify, isSegwit)
const { publicKey } = deviceIdentifiers
const WALLET_IDENTIFIER = `${publicKey}__${currencyId}${isSegwit ? '_segwit' : ''}`
return WALLET_IDENTIFIER
}
async function scanAccountsOnDeviceBySegwit({
hwApp,
currencyId,
onAccountScanned,
devicePath,
isSegwit,
}: {
hwApp: Object,
currencyId: string,
onAccountScanned: AccountRaw => void,
devicePath: string,
isSegwit: boolean,
}): Promise<AccountRaw[]> {
// compute wallet identifier
const WALLET_IDENTIFIER = await getWalletIdentifier({ hwApp, isSegwit, currencyId, devicePath })
// retrieve or create the wallet
const wallet = await getOrCreateWallet(WALLET_IDENTIFIER, currencyId, isSegwit)
@ -80,7 +100,17 @@ async function scanAccountsOnDeviceBySegwit({
return accounts
}
async function scanNextAccount(props) {
async function scanNextAccount(props: {
// $FlowFixMe
wallet: NJSWallet,
hwApp: Object,
currencyId: string,
accountsCount: number,
accountIndex: number,
accounts: AccountRaw[],
onAccountScanned: AccountRaw => void,
isSegwit: boolean,
}): Promise<AccountRaw[]> {
const {
wallet,
hwApp,
@ -136,7 +166,11 @@ async function scanNextAccount(props) {
return scanNextAccount({ ...props, accountIndex: accountIndex + 1 })
}
async function getOrCreateWallet(WALLET_IDENTIFIER, currencyId, isSegwit) {
async function getOrCreateWallet(
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 {
@ -176,7 +210,7 @@ async function buildAccountRaw({
hwApp: Object,
// $FlowFixMe
ops: NJSOperation[],
}) {
}): Promise<AccountRaw> {
const balanceByDay = ops.length
? await getBalanceByDaySinceOperation({
njsAccount,
@ -204,10 +238,10 @@ async function buildAccountRaw({
// get a bunch of fresh addresses
const rawAddresses = await njsAccount.getFreshPublicAddresses()
// TODO: waiting for libcore
const addresses = rawAddresses.map((strAddr, i) => ({
str: strAddr,
path: `${accountPath}/${i}'`,
const addresses = rawAddresses.map(njsAddress => ({
str: njsAddress.toString(),
path: `${accountPath}/${njsAddress.getDerivationPath()}`,
}))
const operations = ops.map(op => buildOperationRaw({ core, op, xpub }))
@ -295,7 +329,7 @@ async function getBalanceByDaySinceOperation({
return res
}
function areSameDay(date1, date2) {
function areSameDay(date1: Date, date2: Date): boolean {
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&

71
src/internals/accounts/signAndBroadcastTransaction/btc.js

@ -0,0 +1,71 @@
// @flow
import Btc from '@ledgerhq/hw-app-btc'
import CommNodeHid from '@ledgerhq/hw-transport-node-hid'
import type { AccountRaw } from '@ledgerhq/live-common/lib/types'
import type Transport from '@ledgerhq/hw-transport'
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')()
// instanciate app on device
const transport: Transport<*> = await CommNodeHid.open(deviceId)
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)
send('accounts.signAndBroadcastTransactionBTCLike.success', txHash)
} catch (err) {
send('accounts.signAndBroadcastTransactionBTCLike.fail', err)
}
}

1
src/internals/index.js

@ -24,7 +24,6 @@ if (handlers.default) {
}
process.on('message', payload => {
console.log(payload)
if (payload.data && payload.data.requestId) {
const { data, requestId, id } = payload.data
// this is the new type of "command" payload!

4
src/reducers/accounts.js

@ -119,6 +119,10 @@ export function decodeAccount(account: AccountRaw): Account {
})
}
export function encodeAccount(account: Account): AccountRaw {
return accountModel.encode(account).data
}
// Yeah. `any` should be `AccountRaw[]` but it can also be a map
// of wrapped accounts. And as flow is apparently incapable of doing
// such a simple thing, let's put any, right? I don't care.

2
src/renderer/runJob.js

@ -14,7 +14,7 @@ export default function runJob({
successResponse: string,
errorResponse: string,
data?: any,
}): Promise<void> {
}): Promise<any> {
return new Promise((resolve, reject) => {
ipcRenderer.send(channel, { type: job, data })
ipcRenderer.on('msg', handler)

12
yarn.lock

@ -1473,9 +1473,9 @@
npm "^5.7.1"
prebuild-install "^2.2.2"
"@ledgerhq/live-common@^2.7.2":
version "2.7.2"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.7.2.tgz#abfa71428c186220006d35baca44261a1c3ef9ed"
"@ledgerhq/live-common@^2.7.5":
version "2.7.5"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.7.5.tgz#5434bf2e708aaca471be4ca823e613cf27ba700c"
dependencies:
axios "^0.18.0"
invariant "^2.2.2"
@ -12051,9 +12051,9 @@ rx@2.3.24:
version "2.3.24"
resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
rxjs-compat@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/rxjs-compat/-/rxjs-compat-6.2.0.tgz#2eb49cc6ac20d0d7057c6887d1895beaab0966f9"
rxjs-compat@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/rxjs-compat/-/rxjs-compat-6.1.0.tgz#935059623ee4c167728c9dd03ee6e4468cc5b583"
rxjs@^5.1.1, rxjs@^5.4.2, rxjs@^5.5.2:
version "5.5.10"

Loading…
Cancel
Save