You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
227 lines
6.5 KiB
227 lines
6.5 KiB
// @flow
|
|
|
|
import logger from 'logger'
|
|
import type { AccountRaw, OperationRaw } from '@ledgerhq/live-common/lib/types'
|
|
import Btc from '@ledgerhq/hw-app-btc'
|
|
import { Observable } from 'rxjs'
|
|
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
|
|
import { isSegwitAccount } from 'helpers/bip32'
|
|
|
|
import withLibcore from 'helpers/withLibcore'
|
|
import { createCommand, Command } from 'helpers/ipc'
|
|
import { withDevice } from 'helpers/deviceAccess'
|
|
import * as accountIdHelper from 'helpers/accountId'
|
|
|
|
type BitcoinLikeTransaction = {
|
|
amount: number,
|
|
feePerByte: number,
|
|
recipient: string,
|
|
}
|
|
|
|
type Input = {
|
|
account: AccountRaw,
|
|
transaction: BitcoinLikeTransaction,
|
|
deviceId: string,
|
|
}
|
|
|
|
type Result = { type: 'signed' } | { type: 'broadcasted', operation: OperationRaw }
|
|
|
|
const cmd: Command<Input, Result> = createCommand(
|
|
'libcoreSignAndBroadcast',
|
|
({ account, transaction, deviceId }) =>
|
|
Observable.create(o => {
|
|
let unsubscribed = false
|
|
const isCancelled = () => unsubscribed
|
|
withLibcore(core =>
|
|
doSignAndBroadcast({
|
|
account,
|
|
transaction,
|
|
deviceId,
|
|
core,
|
|
isCancelled,
|
|
onSigned: () => {
|
|
o.next({ type: 'signed' })
|
|
},
|
|
onOperationBroadcasted: operation => {
|
|
o.next({
|
|
type: 'broadcasted',
|
|
operation,
|
|
})
|
|
},
|
|
}),
|
|
).then(() => o.complete(), e => o.error(e))
|
|
|
|
return () => {
|
|
unsubscribed = true
|
|
}
|
|
}),
|
|
)
|
|
|
|
async function signTransaction({
|
|
hwApp,
|
|
currencyId,
|
|
transaction,
|
|
sigHashType,
|
|
supportsSegwit,
|
|
isSegwit,
|
|
hasTimestamp,
|
|
}: {
|
|
hwApp: Btc,
|
|
currencyId: string,
|
|
transaction: *,
|
|
sigHashType: number,
|
|
supportsSegwit: boolean,
|
|
isSegwit: boolean,
|
|
hasTimestamp: boolean,
|
|
}) {
|
|
const additionals = []
|
|
if (currencyId === 'bitcoin_cash' || currencyId === 'bitcoin_gold') additionals.push('bip143')
|
|
const rawInputs = transaction.getInputs()
|
|
|
|
const inputs = await Promise.all(
|
|
rawInputs.map(async input => {
|
|
const rawPreviousTransaction = await input.getPreviousTransaction()
|
|
const hexPreviousTransaction = Buffer.from(rawPreviousTransaction).toString('hex')
|
|
const previousTransaction = hwApp.splitTransaction(
|
|
hexPreviousTransaction,
|
|
supportsSegwit,
|
|
hasTimestamp,
|
|
)
|
|
const outputIndex = input.getPreviousOutputIndex()
|
|
const sequence = input.getSequence()
|
|
return [
|
|
previousTransaction,
|
|
outputIndex,
|
|
undefined, // we don't use that TODO: document
|
|
sequence, // 0xffffffff,
|
|
]
|
|
}),
|
|
)
|
|
|
|
const associatedKeysets = rawInputs.map(input => {
|
|
const derivationPaths = input.getDerivationPath()
|
|
return derivationPaths[0].toString()
|
|
})
|
|
|
|
const outputs = transaction.getOutputs()
|
|
|
|
const output = outputs.find(output => {
|
|
const derivationPath = output.getDerivationPath()
|
|
if (derivationPath.isNull()) {
|
|
return false
|
|
}
|
|
const strDerivationPath = derivationPath.toString()
|
|
const derivationArr = strDerivationPath.split('/')
|
|
return derivationArr[derivationArr.length - 2] === '1'
|
|
})
|
|
|
|
const changePath = output ? output.getDerivationPath().toString() : undefined
|
|
const outputScriptHex = Buffer.from(transaction.serializeOutputs()).toString('hex')
|
|
const lockTime = transaction.getLockTime()
|
|
const initialTimestamp = hasTimestamp ? transaction.getTimestamp() : undefined
|
|
|
|
const signedTransaction = await hwApp.createPaymentTransactionNew(
|
|
inputs,
|
|
associatedKeysets,
|
|
changePath,
|
|
outputScriptHex,
|
|
lockTime,
|
|
sigHashType,
|
|
isSegwit,
|
|
initialTimestamp,
|
|
additionals,
|
|
)
|
|
|
|
return signedTransaction
|
|
}
|
|
|
|
export async function doSignAndBroadcast({
|
|
account,
|
|
transaction,
|
|
deviceId,
|
|
core,
|
|
isCancelled,
|
|
onSigned,
|
|
onOperationBroadcasted,
|
|
}: {
|
|
account: AccountRaw,
|
|
transaction: BitcoinLikeTransaction,
|
|
deviceId: string,
|
|
core: *,
|
|
isCancelled: () => boolean,
|
|
onSigned: () => void,
|
|
onOperationBroadcasted: (optimisticOp: $Exact<OperationRaw>) => void,
|
|
}): Promise<void> {
|
|
const { walletName } = accountIdHelper.decode(account.id)
|
|
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 = 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
|
|
|
|
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()
|
|
if (isCancelled()) return
|
|
const sigHashType = Buffer.from(njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash).toString(
|
|
'hex',
|
|
)
|
|
|
|
const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction
|
|
// TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay
|
|
|
|
const currency = getCryptoCurrencyById(account.currencyId)
|
|
|
|
const signedTransaction = await withDevice(deviceId)(async transport =>
|
|
signTransaction({
|
|
hwApp: new Btc(transport),
|
|
currencyId: account.currencyId,
|
|
transaction: builded,
|
|
sigHashType: parseInt(sigHashType, 16),
|
|
supportsSegwit: !!currency.supportsSegwit,
|
|
isSegwit: isSegwitAccount(account),
|
|
hasTimestamp,
|
|
}),
|
|
)
|
|
|
|
if (!signedTransaction || isCancelled() || !njsAccount) return
|
|
onSigned()
|
|
|
|
logger.log(signedTransaction)
|
|
|
|
const txHash = await njsAccount
|
|
.asBitcoinLikeAccount()
|
|
.broadcastRawTransaction(Array.from(Buffer.from(signedTransaction, 'hex')))
|
|
|
|
// NB we don't check isCancelled() because the broadcast is not cancellable now!
|
|
onOperationBroadcasted({
|
|
id: txHash,
|
|
hash: txHash,
|
|
type: 'OUT',
|
|
value: transaction.amount,
|
|
fee: 0,
|
|
blockHash: null,
|
|
blockHeight: null,
|
|
senders: [account.freshAddress],
|
|
recipients: [transaction.recipient],
|
|
accountId: account.id,
|
|
date: new Date().toISOString(),
|
|
})
|
|
}
|
|
|
|
export default cmd
|
|
|