@trezor/connect
Version:
High-level javascript interface for Trezor hardware wallet.
340 lines (339 loc) • 11.2 kB
JavaScript
"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