@trezor/connect
Version:
High-level javascript interface for Trezor hardware wallet.
348 lines (347 loc) • 11.7 kB
JavaScript
;
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