@ledgerhq/hw-app-btc
Version:
Ledger Hardware Wallet Bitcoin Application API
321 lines • 14.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTransaction = exports.getDefaultVersions = void 0;
const logs_1 = require("@ledgerhq/logs");
const hashPublicKey_1 = require("./hashPublicKey");
const getWalletPublicKey_1 = require("./getWalletPublicKey");
const getTrustedInput_1 = require("./getTrustedInput");
const startUntrustedHashTransactionInput_1 = require("./startUntrustedHashTransactionInput");
const serializeTransaction_1 = require("./serializeTransaction");
const getTrustedInputBIP143_1 = require("./getTrustedInputBIP143");
const compressPublicKey_1 = require("./compressPublicKey");
const signTransaction_1 = require("./signTransaction");
const finalizeInput_1 = require("./finalizeInput");
const getAppAndVersion_1 = require("./getAppAndVersion");
const constants_1 = require("./constants");
const shouldUseTrustedInputForSegwit_1 = require("./shouldUseTrustedInputForSegwit");
const defaultsSignTransaction = {
lockTime: constants_1.DEFAULT_LOCKTIME,
sigHashType: constants_1.SIGHASH_ALL,
segwit: false,
additionals: [],
onDeviceStreaming: _e => { },
onDeviceSignatureGranted: () => { },
onDeviceSignatureRequested: () => { },
};
const getZcashTransactionVersion = (blockHeight) => {
const version = Buffer.alloc(4);
if (blockHeight && blockHeight < constants_1.ZCASH_NU6_ACTIVATION_HEIGHT) {
version.writeUInt32LE(0x80000005, 0);
}
else {
// NOTE: null and undefined should default to latest version
version.writeUInt32LE(0x80000006, 0);
}
return version;
};
const getDefaultVersions = ({ isZcash, sapling, isDecred, expiryHeight, blockHeight, }) => {
let defaultVersion = Buffer.alloc(4);
const defaultVersionNu5Only = Buffer.alloc(4);
if (!!expiryHeight && !isDecred) {
if (isZcash) {
defaultVersion = getZcashTransactionVersion(blockHeight);
defaultVersionNu5Only.writeUInt32LE(0x80000005, 0);
}
else {
const version = sapling ? 0x80000004 : 0x80000003;
defaultVersion.writeUInt32LE(version, 0);
defaultVersionNu5Only.writeUInt32LE(version, 0);
}
}
else {
defaultVersion.writeUInt32LE(1, 0);
defaultVersionNu5Only.writeUInt32LE(1, 0);
}
return { defaultVersion, defaultVersionNu5Only };
};
exports.getDefaultVersions = getDefaultVersions;
async function createTransaction(transport, arg) {
const signTx = { ...defaultsSignTransaction, ...arg };
const { inputs, associatedKeysets, blockHeight, changePath, outputScriptHex, lockTime, sigHashType, segwit, additionals, expiryHeight, onDeviceStreaming, onDeviceSignatureGranted, onDeviceSignatureRequested, } = signTx;
let useTrustedInputForSegwit = signTx.useTrustedInputForSegwit;
if (useTrustedInputForSegwit === undefined) {
try {
const a = await (0, getAppAndVersion_1.getAppAndVersion)(transport);
useTrustedInputForSegwit = (0, shouldUseTrustedInputForSegwit_1.shouldUseTrustedInputForSegwit)(a);
}
catch (e) {
if (e.statusCode === 0x6d00) {
useTrustedInputForSegwit = false;
}
else {
throw e;
}
}
}
// loop: 0 or 1 (before and after)
// i: index of the input being streamed
// i goes on 0...n, inluding n. in order for the progress value to go to 1
// we normalize the 2 loops to make a global percentage
const notify = (loop, i) => {
const { length } = inputs;
if (length < 3)
return; // there is not enough significant event to worth notifying (aka just use a spinner)
const index = length * loop + i;
const total = 2 * length;
const progress = index / total;
onDeviceStreaming({
progress,
total,
index,
});
};
const isDecred = additionals.includes("decred");
const isZcash = additionals.includes("zcash");
const sapling = additionals.includes("sapling");
const bech32 = segwit && additionals.includes("bech32");
const useBip143 = segwit ||
(!!additionals &&
(additionals.includes("abc") ||
additionals.includes("gold") ||
additionals.includes("bip143"))) ||
(!!expiryHeight && !isDecred);
// Inputs are provided as arrays of [transaction, output_index, optional redeem script, optional sequence]
// associatedKeysets are provided as arrays of [path]
const lockTimeBuffer = Buffer.alloc(4);
lockTimeBuffer.writeUInt32LE(lockTime, 0);
const nullScript = Buffer.alloc(0);
const nullPrevout = Buffer.alloc(0);
const { defaultVersion, defaultVersionNu5Only } = (0, exports.getDefaultVersions)({
isZcash,
sapling,
isDecred,
expiryHeight,
blockHeight,
});
// Default version to 2 for XST not to have timestamp
const trustedInputs = [];
const regularOutputs = [];
const signatures = [];
const publicKeys = [];
let firstRun = true;
const resuming = false;
const targetTransaction = {
inputs: [],
version: defaultVersion,
timestamp: Buffer.alloc(0),
};
const getTrustedInputCall = useBip143 && !useTrustedInputForSegwit ? getTrustedInputBIP143_1.getTrustedInputBIP143 : getTrustedInput_1.getTrustedInput;
const outputScript = Buffer.from(outputScriptHex, "hex");
notify(0, 0);
// first pass on inputs to get trusted inputs
for (const input of inputs) {
if (!resuming) {
if (isZcash) {
input[0].version = getZcashTransactionVersion(input[4]);
}
const trustedInput = await getTrustedInputCall(transport, input[1], input[0], additionals);
(0, logs_1.log)("hw", "got trustedInput=" + trustedInput);
const sequence = Buffer.alloc(4);
sequence.writeUInt32LE(input.length >= 4 && typeof input[3] === "number" ? input[3] : constants_1.DEFAULT_SEQUENCE, 0);
trustedInputs.push({
trustedInput: true,
value: Buffer.from(trustedInput, "hex"),
sequence,
});
}
const { outputs } = input[0];
const index = input[1];
if (outputs && index <= outputs.length - 1) {
regularOutputs.push(outputs[index]);
}
if (expiryHeight && !isDecred) {
targetTransaction.nVersionGroupId = Buffer.from(
// nVersionGroupId is 0x26A7270A for zcash NU5 upgrade
// refer to https://github.com/zcash/zcash/blob/master/src/primitives/transaction.h
isZcash
? [0x0a, 0x27, 0xa7, 0x26]
: sapling
? [0x85, 0x20, 0x2f, 0x89]
: [0x70, 0x82, 0xc4, 0x03]);
targetTransaction.nExpiryHeight = expiryHeight;
// For sapling : valueBalance (8), nShieldedSpend (1), nShieldedOutput (1), nJoinSplit (1)
// Overwinter : use nJoinSplit (1)
targetTransaction.extraData = Buffer.from(sapling ? [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] : [0x00]);
}
else if (isDecred) {
targetTransaction.nExpiryHeight = expiryHeight;
}
}
targetTransaction.inputs = inputs.map((input, idx) => {
const sequence = Buffer.alloc(4);
sequence.writeUInt32LE(input.length >= 4 && typeof input[3] === "number" ? input[3] : constants_1.DEFAULT_SEQUENCE, 0);
return {
script: isZcash ? regularOutputs[idx].script : nullScript,
prevout: nullPrevout,
sequence,
};
});
if (!resuming) {
// Collect public keys
const result = [];
for (let i = 0; i < inputs.length; i++) {
const r = await (0, getWalletPublicKey_1.getWalletPublicKey)(transport, {
path: associatedKeysets[i],
});
notify(0, i + 1);
result.push(r);
}
for (let i = 0; i < result.length; i++) {
publicKeys.push((0, compressPublicKey_1.compressPublicKey)(Buffer.from(result[i].publicKey, "hex")));
}
}
onDeviceSignatureRequested();
if (useBip143) {
// Do the first run with all inputs
await (0, startUntrustedHashTransactionInput_1.startUntrustedHashTransactionInput)(transport, true, targetTransaction, trustedInputs, true, !!expiryHeight, additionals, useTrustedInputForSegwit);
if (!resuming && changePath) {
await (0, finalizeInput_1.provideOutputFullChangePath)(transport, changePath);
}
await (0, finalizeInput_1.hashOutputFull)(transport, outputScript);
}
if (!!expiryHeight && !isDecred) {
await (0, signTransaction_1.signTransaction)(transport, "", lockTime, constants_1.SIGHASH_ALL, expiryHeight);
}
// Do the second run with the individual transaction
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
const script = inputs[i].length >= 3 && typeof input[2] === "string"
? Buffer.from(input[2], "hex")
: !segwit
? regularOutputs[i].script
: Buffer.concat([
Buffer.from([constants_1.OP_DUP, constants_1.OP_HASH160, constants_1.HASH_SIZE]),
(0, hashPublicKey_1.hashPublicKey)(publicKeys[i]),
Buffer.from([constants_1.OP_EQUALVERIFY, constants_1.OP_CHECKSIG]),
]);
const pseudoTX = Object.assign({}, targetTransaction);
const pseudoTrustedInputs = useBip143 ? [trustedInputs[i]] : trustedInputs;
if (useBip143) {
pseudoTX.inputs = [{ ...pseudoTX.inputs[i], script }];
}
else {
pseudoTX.inputs[i].script = script;
}
await (0, startUntrustedHashTransactionInput_1.startUntrustedHashTransactionInput)(transport, !useBip143 && firstRun, pseudoTX, pseudoTrustedInputs, useBip143, !!expiryHeight && !isDecred, additionals, useTrustedInputForSegwit);
if (!useBip143) {
if (!resuming && changePath) {
await (0, finalizeInput_1.provideOutputFullChangePath)(transport, changePath);
}
await (0, finalizeInput_1.hashOutputFull)(transport, outputScript, additionals);
}
if (firstRun) {
onDeviceSignatureGranted();
notify(1, 0);
}
const signature = await (0, signTransaction_1.signTransaction)(transport, associatedKeysets[i], lockTime, sigHashType, expiryHeight, additionals);
notify(1, i + 1);
signatures.push(signature);
targetTransaction.inputs[i].script = nullScript;
if (firstRun) {
firstRun = false;
}
}
targetTransaction.version = defaultVersionNu5Only;
// Populate the final input scripts
for (let i = 0; i < inputs.length; i++) {
if (segwit) {
targetTransaction.witness = Buffer.alloc(0);
if (!bech32) {
targetTransaction.inputs[i].script = Buffer.concat([
Buffer.from("160014", "hex"),
(0, hashPublicKey_1.hashPublicKey)(publicKeys[i]),
]);
}
}
else {
const signatureSize = Buffer.alloc(1);
const keySize = Buffer.alloc(1);
signatureSize[0] = signatures[i].length;
keySize[0] = publicKeys[i].length;
targetTransaction.inputs[i].script = Buffer.concat([
signatureSize,
signatures[i],
keySize,
publicKeys[i],
]);
}
const offset = useBip143 && !useTrustedInputForSegwit ? 0 : 4;
targetTransaction.inputs[i].prevout = trustedInputs[i].value.slice(offset, offset + 0x24);
}
targetTransaction.locktime = lockTimeBuffer;
let result = Buffer.concat([
(0, serializeTransaction_1.serializeTransaction)(targetTransaction, false, targetTransaction.timestamp, additionals),
outputScript,
]);
if (segwit && !isDecred) {
let witness = Buffer.alloc(0);
for (let i = 0; i < inputs.length; i++) {
const tmpScriptData = Buffer.concat([
Buffer.from("02", "hex"),
Buffer.from([signatures[i].length]),
signatures[i],
Buffer.from([publicKeys[i].length]),
publicKeys[i],
]);
witness = Buffer.concat([witness, tmpScriptData]);
}
result = Buffer.concat([result, witness]);
}
// from to https://zips.z.cash/zip-0225, zcash is different with other coins, the lock_time and nExpiryHeight fields are before the inputs and outputs
if (!isZcash) {
result = Buffer.concat([result, lockTimeBuffer]);
if (expiryHeight) {
result = Buffer.concat([
result,
targetTransaction.nExpiryHeight || Buffer.alloc(0),
targetTransaction.extraData || Buffer.alloc(0),
]);
}
}
if (isDecred) {
let decredWitness = Buffer.from([targetTransaction.inputs.length]);
inputs.forEach((input, inputIndex) => {
decredWitness = Buffer.concat([
decredWitness,
Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
Buffer.from([0x00, 0x00, 0x00, 0x00]), //Block height
Buffer.from([0xff, 0xff, 0xff, 0xff]), //Block index
Buffer.from([targetTransaction.inputs[inputIndex].script.length]),
targetTransaction.inputs[inputIndex].script,
]);
});
result = Buffer.concat([result, decredWitness]);
}
if (isZcash) {
result = Buffer.concat([result, Buffer.from([0x00, 0x00, 0x00])]);
}
return result.toString("hex");
}
exports.createTransaction = createTransaction;
//# sourceMappingURL=createTransaction.js.map