UNPKG

@trezor/connect

Version:

High-level javascript interface for Trezor hardware wallet.

289 lines 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const bigNumber_1 = require("@trezor/utils/lib/bigNumber"); const promiseAllSequence_1 = require("@trezor/utils/lib/promiseAllSequence"); const resolveAfter_1 = require("@trezor/utils/lib/resolveAfter"); const BlockchainLink_1 = require("../backend/BlockchainLink"); const constants_1 = require("../constants"); const utxo_1 = require("../constants/utxo"); const AbstractMethod_1 = require("../core/AbstractMethod"); const events_1 = require("../events"); const bitcoin_1 = require("./bitcoin"); const Discovery_1 = require("./common/Discovery"); const paramsValidator_1 = require("./common/paramsValidator"); const coinInfo_1 = require("../data/coinInfo"); const formatUtils_1 = require("../utils/formatUtils"); const pathUtils = tslib_1.__importStar(require("../utils/pathUtils")); class ComposeTransaction extends AbstractMethod_1.AbstractMethod { discovery; init() { this.requiredPermissions = ['read', 'write']; const { payload } = this; (0, paramsValidator_1.validateParams)(payload, [ { name: 'outputs', type: 'array', required: true }, { name: 'coin', type: 'string', required: true }, { name: 'identity', type: 'string' }, { name: 'push', type: 'boolean' }, { name: 'account', type: 'object' }, { name: 'feeLevels', type: 'array' }, { name: 'baseFee', type: 'number' }, { name: 'floorBaseFee', type: 'boolean' }, { name: 'sequence', type: 'number' }, { name: 'skipPermutation', type: 'boolean' }, { name: 'sortingStrategy', type: 'string' }, ]); const coinInfo = (0, coinInfo_1.getBitcoinNetwork)(payload.coin); if (!coinInfo) { throw constants_1.ERRORS.TypedError('Method_UnknownCoin'); } (0, BlockchainLink_1.isBackendSupported)(coinInfo); this.firmwareRange = (0, paramsValidator_1.getFirmwareRange)(this.name, coinInfo, this.firmwareRange); const outputs = []; let total = new bigNumber_1.BigNumber(0); payload.outputs.forEach(out => { const output = (0, bitcoin_1.validateHDOutput)(out, coinInfo); if ('amount' in output && typeof output.amount === 'string') { total = total.plus(output.amount); } outputs.push(output); }); this.useDevice = !payload.account && !payload.feeLevels; this.useUi = this.useDevice; this.params = { outputs, coinInfo, identity: payload.identity, account: payload.account, feeLevels: payload.feeLevels, baseFee: payload.baseFee, floorBaseFee: payload.floorBaseFee, sequence: payload.sequence, sortingStrategy: payload.skipPermutation === true ? 'none' : payload.sortingStrategy, push: typeof payload.push === 'boolean' ? payload.push : false, total, }; if (this.params.push) { this.requiredPermissions.push('push_tx'); } } get info() { const sendMax = this.params?.outputs.find(o => o.type === 'send-max') !== undefined; if (sendMax) { return 'Send maximum amount'; } return `Send ${(0, formatUtils_1.formatAmount)(this.params.total.toString(), this.params.coinInfo)}`; } getBlockchain() { return (0, BlockchainLink_1.initBlockchain)(this.params.coinInfo, this.postMessage, this.params.identity); } async precompose(account, feeLevels) { const { coinInfo, outputs, baseFee, sortingStrategy } = this.params; const address_n = pathUtils.validatePath(account.path); const composer = new bitcoin_1.TransactionComposer({ account: { type: pathUtils.getAccountType(address_n), label: 'Account', descriptor: account.path, address_n, addresses: account.addresses, }, utxos: account.utxo, coinInfo, outputs, baseFee, sortingStrategy: sortingStrategy ?? utxo_1.DEFAULT_SORTING_STRATEGY, }); const blockchain = await this.getBlockchain(); await composer.init(blockchain); return feeLevels.map(level => { composer.composeCustomFee(level.feePerUnit); const tx = { ...composer.composed.custom }; if (tx.type === 'final') { return { ...tx, inputs: tx.inputs.map(inp => (0, bitcoin_1.inputToTrezor)(inp, this.params.sequence)), outputs: tx.outputs.map(bitcoin_1.outputToTrezor), }; } if (tx.type === 'nonfinal') { return { ...tx, inputs: tx.inputs.map(inp => (0, bitcoin_1.inputToTrezor)(inp, this.params.sequence)), }; } return tx; }); } async run() { if (this.params.account && this.params.feeLevels) { return this.precompose(this.params.account, this.params.feeLevels); } const { account, utxo } = await this.selectAccount(); const response = await this.selectFee(account, utxo); if (!this.discovery) { throw constants_1.ERRORS.TypedError('Runtime', 'ComposeTransaction: selectFee response received after dispose'); } if (typeof response === 'string') { return this.run(); } return response; } async selectAccount() { const { coinInfo } = this.params; const blockchain = await this.getBlockchain(); const dfd = this.createUiPromise(events_1.UI.RECEIVE_ACCOUNT); if (this.discovery && this.discovery.completed) { const { discovery } = this; this.postMessage((0, events_1.createUiMessage)(events_1.UI.SELECT_ACCOUNT, { type: 'end', coinInfo, accountTypes: discovery.types.map(t => t.type), accounts: discovery.accounts, })); const uiResp = await dfd.promise; const account = discovery.accounts[uiResp.payload]; const utxo = await blockchain.getAccountUtxo(account.descriptor); return { account, utxo, }; } const discovery = this.discovery || new Discovery_1.Discovery({ blockchain, getDescriptor: path => this.device.getCommands().getAccountDescriptor(this.params.coinInfo, path), }); this.discovery = discovery; discovery.on('progress', accounts => { this.postMessage((0, events_1.createUiMessage)(events_1.UI.SELECT_ACCOUNT, { type: 'progress', coinInfo, accounts, })); }); discovery.on('complete', () => { this.postMessage((0, events_1.createUiMessage)(events_1.UI.SELECT_ACCOUNT, { type: 'end', coinInfo, })); }); discovery.start('tokens').catch(error => { dfd.reject(error); }); this.postMessage((0, events_1.createUiMessage)(events_1.UI.SELECT_ACCOUNT, { type: 'start', accountTypes: discovery.types.map(t => t.type), coinInfo, })); const uiResp = await dfd.promise; discovery.removeAllListeners(); discovery.stop(); if (!discovery.completed) { await (0, resolveAfter_1.resolveAfter)(501); } const account = discovery.accounts[uiResp.payload]; this.params.coinInfo = (0, coinInfo_1.fixCoinInfoNetwork)(this.params.coinInfo, account.address_n); const utxo = await blockchain.getAccountUtxo(account.descriptor); return { account, utxo, }; } async selectFee(account, utxos) { const { coinInfo, outputs, sortingStrategy, skipPermutation } = this.params; const blockchain = await this.getBlockchain(); const composer = new bitcoin_1.TransactionComposer({ account, utxos, coinInfo, outputs, sortingStrategy: skipPermutation === true ? 'none' : (sortingStrategy ?? utxo_1.DEFAULT_SORTING_STRATEGY), }); await composer.init(blockchain); const hasFunds = composer.composeAllFeeLevels(); if (!hasFunds) { this.postMessage((0, events_1.createUiMessage)(events_1.UI.INSUFFICIENT_FUNDS)); await (0, resolveAfter_1.resolveAfter)(2000); return 'change-account'; } this.postMessage((0, events_1.createUiMessage)(events_1.UI.SELECT_FEE, { feeLevels: composer.getFeeLevelList(), coinInfo: this.params.coinInfo, })); return this._selectFeeUiResponse(composer); } async _selectFeeUiResponse(composer) { const resp = await this.createUiPromise(events_1.UI.RECEIVE_FEE).promise; switch (resp.payload.type) { case 'compose-custom': composer.composeCustomFee(resp.payload.value); this.postMessage((0, events_1.createUiMessage)(events_1.UI.UPDATE_CUSTOM_FEE, { feeLevels: composer.getFeeLevelList(), coinInfo: this.params.coinInfo, })); return this._selectFeeUiResponse(composer); case 'send': return this._sign(composer.composed[resp.payload.value]); default: return 'change-account'; } } async _sign(tx) { const { device, params } = this; if (tx.type !== 'final') throw constants_1.ERRORS.TypedError('Runtime', 'ComposeTransaction: Trying to sign unfinished tx'); const { coinInfo } = params; const options = (0, bitcoin_1.enhanceSignTx)({}, coinInfo); const inputs = tx.inputs.map(inp => (0, bitcoin_1.inputToTrezor)(inp, params.sequence)); const outputs = tx.outputs.map(bitcoin_1.outputToTrezor); let refTxs = []; const requiredRefTxs = (0, bitcoin_1.requireReferencedTransactions)(inputs, options, coinInfo); const refTxsIds = (0, bitcoin_1.getReferencedTransactions)(inputs); if (requiredRefTxs && refTxsIds.length > 0) { refTxs = await this.getBlockchain() .then(blockchain => blockchain.getTransactionHexes(refTxsIds)) .then((0, bitcoin_1.parseTransactionHexes)(coinInfo.network)) .then(bitcoin_1.transformReferencedTransactions); } const getHDNode = (address_n) => device.getCommands().getHDNode({ address_n }, { coinInfo: params.coinInfo }); const outputScripts = await (0, promiseAllSequence_1.promiseAllSequence)(outputs.map(output => () => (0, bitcoin_1.deriveOutputScript)(getHDNode, output, coinInfo.network))); const signTxMethod = !device.unavailableCapabilities.replaceTransaction ? bitcoin_1.signTx : bitcoin_1.signTxLegacy; const cmd = device.getCommands(); const response = await signTxMethod({ typedCall: cmd.typedCall, inputs, outputs, refTxs, options, coinInfo, }); (0, bitcoin_1.verifyTx)(response.serializedTx, { inputs, outputs, outputScripts, network: coinInfo.network, }); if (params.push) { const blockchain = await this.getBlockchain(); const txid = await blockchain.pushTransaction(response.serializedTx); return { ...response, txid, }; } return response; } dispose() { const { discovery } = this; if (discovery) { discovery.stop(); discovery.removeAllListeners(); this.discovery = undefined; } } } exports.default = ComposeTransaction; //# sourceMappingURL=composeTransaction.js.map