UNPKG

@trezor/connect

Version:

High-level javascript interface for Trezor hardware wallet.

348 lines (347 loc) 11.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const bigNumber_1 = require("@trezor/utils/lib/bigNumber"); const promiseAllSequence_1 = require("@trezor/utils/lib/promiseAllSequence"); const constants_1 = require("../constants"); const bitcoin_1 = require("./bitcoin"); const BlockchainLink_1 = require("../backend/BlockchainLink"); const AbstractMethod_1 = require("../core/AbstractMethod"); const paramsValidator_1 = require("./common/paramsValidator"); const coinInfo_1 = require("../data/coinInfo"); const pathUtils_1 = require("../utils/pathUtils"); const transactionBytes_1 = require("./bitcoin/transactionBytes"); class SignTransaction extends AbstractMethod_1.AbstractMethod { init() { this.requiredPermissions = ['read', 'write']; const { payload } = this; (0, paramsValidator_1.validateParams)(payload, [{ name: 'coin', type: 'string', required: true }, { name: 'identity', type: 'string' }, { name: 'inputs', type: 'array', required: true }, { name: 'outputs', type: 'array', required: true }, { name: 'paymentRequests', type: 'array', allowEmpty: true }, { name: 'coinjoinRequest', type: 'object' }, { name: 'refTxs', type: 'array', allowEmpty: true }, { name: 'account', type: 'object' }, { name: 'locktime', type: 'number' }, { name: 'timestamp', type: 'number' }, { name: 'version', type: 'number' }, { name: 'expiry', type: 'number' }, { name: 'overwintered', type: 'boolean' }, { name: 'versionGroupId', type: 'number' }, { name: 'branchId', type: 'number' }, { name: 'decredStakingTicket', type: 'boolean' }, { name: 'push', type: 'boolean' }, { name: 'preauthorized', type: 'boolean' }, { name: 'amountUnit', type: ['number', 'string'] }, { name: 'unlockPath', type: 'object' }, { name: 'serialize', type: 'boolean' }, { name: 'chunkify', type: 'boolean' }]); if (payload.unlockPath) { (0, paramsValidator_1.validateParams)(payload.unlockPath, [{ name: 'address_n', required: true, type: 'array' }, { name: 'mac', required: true, type: 'string' }]); } const coinInfo = (0, coinInfo_1.getBitcoinNetwork)(payload.coin); if (!coinInfo) { throw constants_1.ERRORS.TypedError('Method_UnknownCoin'); } this.firmwareRange = (0, paramsValidator_1.getFirmwareRange)(this.name, coinInfo, this.firmwareRange); this.preauthorized = payload.preauthorized; const inputs = (0, bitcoin_1.validateTrezorInputs)(payload.inputs, coinInfo); const outputs = (0, bitcoin_1.validateTrezorOutputs)(payload.outputs, coinInfo); if (payload.paymentRequests && payload.paymentRequests.length > 0) { outputs.forEach(output => { output.payment_req_index = 0; }); } if (payload.refTxs && payload.account?.transactions) { console.warn('two sources of referential transactions were passed. payload.refTxs have precedence'); } const refTxs = (0, bitcoin_1.validateReferencedTransactions)({ transactions: payload.refTxs || payload.account?.transactions, inputs, outputs, coinInfo, addresses: payload.account?.addresses }); const outputsWithAmount = outputs.filter(output => typeof output.amount === 'string' && !Object.prototype.hasOwnProperty.call(output, 'op_return_data')); if (outputsWithAmount.length > 0) { const total = outputsWithAmount.reduce((bn, output) => bn.plus(typeof output.amount === 'string' ? output.amount : '0'), new bigNumber_1.BigNumber(0)); if (total.lt(coinInfo.dustLimit)) { throw constants_1.ERRORS.TypedError('Method_InvalidParameter', 'Total amount is below dust limit.'); } } const paymentRequests = payload.paymentRequests?.map(p => { if (typeof p.amount === 'number') { const buffer = Buffer.allocUnsafe(8); buffer.writeBigInt64LE(BigInt(p.amount), 0); return { ...p, amount: buffer.toString('hex') }; } return { ...p, amount: p.amount }; }) ?? []; this.params = { inputs, outputs, paymentRequests, refTxs, addresses: payload.account ? payload.account.addresses : undefined, options: { lock_time: payload.locktime, timestamp: payload.timestamp, version: payload.version, expiry: payload.expiry, overwintered: payload.overwintered, version_group_id: payload.versionGroupId, branch_id: payload.branchId, decred_staking_ticket: payload.decredStakingTicket, amount_unit: payload.amountUnit, serialize: payload.serialize, coinjoin_request: payload.coinjoinRequest, chunkify: typeof payload.chunkify === 'boolean' ? payload.chunkify : false }, coinInfo, identity: payload.identity, push: typeof payload.push === 'boolean' ? payload.push : false, unlockPath: payload.unlockPath }; this.params.options = (0, bitcoin_1.enhanceSignTx)(this.params.options, coinInfo); if (this.params.push) { this.requiredPermissions.push('push_tx'); } } get info() { const coinInfo = (0, coinInfo_1.getBitcoinNetwork)(this.payload.coin); return (0, pathUtils_1.getLabel)('Sign #NETWORK transaction', coinInfo); } async payloadToPrecomposed() { try { const { inputs, outputs, coinInfo } = this.params; const refTxs = this.params.refTxs ?? (await this.fetchRefTxs(false)); const inputsTotal = inputs.reduce((bn, input) => { if (typeof input.amount === 'string') { return bn.plus(input.amount); } else { const refTx = refTxs.find(tx => tx.hash === input.prev_hash); const refOutput = refTx?.outputs?.[input.prev_index] ?? refTx?.bin_outputs?.[input.prev_index]; if (refOutput) return bn.plus(refOutput.amount); } return bn; }, new bigNumber_1.BigNumber(0)); const outputsTotal = outputs.reduce((bn, output) => bn.plus(typeof output.amount === 'string' ? output.amount : '0'), new bigNumber_1.BigNumber(0)); const bytes = (0, transactionBytes_1.getTransactionVbytes)(inputs, outputs, coinInfo); if (!bytes) throw constants_1.ERRORS.TypedError('Runtime', 'Transaction bytes not calculated'); const fee = inputsTotal.minus(outputsTotal); if (fee.lte(0)) { throw constants_1.ERRORS.TypedError('Runtime', 'Computed fee is non-positive'); } const feePerByte = fee.dividedBy(bytes); return { type: 'final', inputs, outputs, outputsPermutation: Array.from({ length: outputs.length }, (_, i) => i), totalSpent: inputsTotal.toString(), fee: fee.toString(), feePerByte: feePerByte.toString(), bytes }; } catch (e) { console.error('Error in payloadToPrecomposed', e); return undefined; } } async fetchAddresses(blockchain) { const { device, params: { inputs, coinInfo } } = this; const accountPath = inputs.find(i => i.address_n); if (!accountPath || !accountPath.address_n) { throw constants_1.ERRORS.TypedError('Runtime', 'Account not found'); } const address_n = accountPath.address_n.slice(0, 3); const node = await device.getCommands().getHDNode({ address_n }, { coinInfo }); const account = await blockchain.getAccountInfo({ descriptor: node.xpubSegwit || node.xpub, details: 'tokens' }); return account.addresses; } async fetchRefTxs(useLegacySignProcess) { const { params: { inputs, outputs, options, coinInfo, identity, addresses } } = this; const requiredRefTxs = (0, bitcoin_1.requireReferencedTransactions)(inputs, options, coinInfo); const refTxsIds = requiredRefTxs ? (0, bitcoin_1.getReferencedTransactions)(inputs) : []; const origTxsIds = !useLegacySignProcess ? (0, bitcoin_1.getOrigTransactions)(inputs, outputs) : []; if (!refTxsIds.length && !origTxsIds.length) { return []; } (0, BlockchainLink_1.isBackendSupported)(coinInfo); const blockchain = await (0, BlockchainLink_1.initBlockchain)(coinInfo, this.postMessage, identity); const refTxs = !refTxsIds.length ? [] : await blockchain.getTransactionHexes(refTxsIds).then((0, bitcoin_1.parseTransactionHexes)(coinInfo.network)).then(rawTxs => { (0, bitcoin_1.enhanceTrezorInputs)(this.params.inputs, rawTxs); return (0, bitcoin_1.transformReferencedTransactions)(rawTxs); }); const origTxs = !origTxsIds.length ? [] : await blockchain.getTransactionHexes(origTxsIds).then((0, bitcoin_1.parseTransactionHexes)(coinInfo.network)).then(async rawOrigTxs => { const accountAddresses = addresses ?? (await this.fetchAddresses(blockchain)); if (!accountAddresses) return []; return (0, bitcoin_1.transformOrigTransactions)(rawOrigTxs, coinInfo, inputs, accountAddresses); }); return refTxs.concat(origTxs); } async run() { const { device, params } = this; const { inputs, outputs, coinInfo } = params; const useLegacySignProcess = !!device.unavailableCapabilities.replaceTransaction; const refTxs = params.refTxs ?? (await this.fetchRefTxs(useLegacySignProcess)); let outputScripts = []; if (params.options.serialize !== false) { const getHDNode = address_n => device.getCommands().getHDNode({ address_n }, { coinInfo, unlockPath: params.unlockPath }); outputScripts = await (0, promiseAllSequence_1.promiseAllSequence)(outputs.map(output => () => (0, bitcoin_1.deriveOutputScript)(getHDNode, output, coinInfo.network))); } if (this.preauthorized) { await device.getCommands().preauthorize(true); } else if (params.unlockPath) { await device.getCommands().unlockPath(params.unlockPath); } const signTxMethod = !useLegacySignProcess ? bitcoin_1.signTx : bitcoin_1.signTxLegacy; const response = await signTxMethod({ ...params, refTxs, typedCall: device.getCommands().typedCall }); if (!response.serializedTx) { return response; } let bitcoinTx; if (params.options.decred_staking_ticket) {} else { bitcoinTx = (0, bitcoin_1.verifyTx)(response.serializedTx, { inputs, outputs, outputScripts, network: coinInfo.network }); if (bitcoinTx.hasWitnesses()) { response.witnesses = bitcoinTx.ins.map((_, i) => bitcoinTx?.getWitness(i)?.toString('hex')); } } if (bitcoinTx && params.addresses) { response.signedTransaction = (0, bitcoin_1.createPendingTransaction)(bitcoinTx, { addresses: params.addresses, inputs, outputs }); } if (params.push) { (0, BlockchainLink_1.isBackendSupported)(coinInfo); const blockchain = await (0, BlockchainLink_1.initBlockchain)(coinInfo, this.postMessage, params.identity); const txid = await blockchain.pushTransaction(response.serializedTx); return { ...response, txid }; } return response; } } exports.default = SignTransaction; //# sourceMappingURL=signTransaction.js.map