bitcoin-tx-lib
Version:
A Typescript library for building and signing Bitcoin transactions
198 lines • 10.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransactionBuilder = void 0;
const opcodes_1 = require("../constants/opcodes");
const utils_1 = require("../utils");
const address_1 = require("../utils/address");
const buffer_1 = require("../utils/buffer");
const txutils_1 = require("../utils/txutils");
/**
* Base class for building and signing Bitcoin transactions (both Legacy and SegWit).
*/
class TransactionBuilder {
/**
* Determines if any input is a SegWit (P2WPKH or P2WSH) input.
* @param inputs List of transaction inputs.
* @returns True if at least one input is SegWit.
*/
isSegwit(inputs) {
return inputs.some(this.isSegwitInput);
}
/**
* Checks if a specific input is a SegWit input (P2WPKH or P2WSH).
* @param input The input to check.
* @returns True if input is SegWit.
*/
isSegwitInput(input) {
const bytes = (0, utils_1.hexToBytes)(input.scriptPubKey);
return ((bytes.length === 22 && bytes[0] == 0x00 && bytes[1] == 0x14) || // P2WPKH
(bytes.length === 34 && bytes[0] == 0x00 && bytes[1] == 0x20)); // P2WSH
}
/**
* Builds and signs the entire transaction.
* @param params Signing parameters including inputs, outputs, key, version and locktime.
* @param format Whether to generate a "raw" or "txid" version.
* @returns Raw transaction bytes.
*/
buildAndSign(params, format = "raw") {
let witnessData = new buffer_1.ByteBuffer();
let hexTransaction = new buffer_1.ByteBuffer((0, utils_1.numberToHexLE)(params.version, 32)); // version
if (this.isSegwit(params.inputs) && format != "txid") // Marker and Flag for SegWit transactions
hexTransaction.append(new Uint8Array([0x00, 0x01])); //"00" + "01";
// number of inputs
hexTransaction.append((0, utils_1.numberToVarint)(params.inputs.length));
params.inputs.forEach(input => {
var _a;
hexTransaction.append((0, utils_1.hexToBytes)(input.txid).reverse()); // txid
hexTransaction.append((0, utils_1.numberToHexLE)(input.vout, 32)); // index output (vout)
if (this.isSegwitInput(input)) {
witnessData.append(this.generateWitness(input, params));
hexTransaction.append(new Uint8Array([0])); // script sig in witness area // P2WPKH
}
else {
witnessData.append(new Uint8Array([0])); // no witness, only scriptSig
let scriptSig = this.generateScriptSig(input, params);
hexTransaction.append((0, utils_1.numberToVarint)(scriptSig.length));
hexTransaction.append(scriptSig);
}
// 0xfffffffd Replace By Fee (RBF) enabled BIP 125
hexTransaction.append((0, utils_1.hexToBytes)((_a = input.sequence) !== null && _a !== void 0 ? _a : "fffffffd").reverse()); // 0xfffffffd
});
hexTransaction.append((0, utils_1.numberToVarint)(params.outputs.length)); // number of outputs
hexTransaction.append(this.outputsRaw(params.outputs)); // amount+scriptpubkey
if (this.isSegwit(params.inputs) && format != "txid")
hexTransaction.append(witnessData.raw());
hexTransaction.append((0, utils_1.numberToHexLE)(params.locktime, 32)); // locktime
return hexTransaction.raw();
}
/**
* Generates the `scriptSig` for a legacy (non-SegWit) P2PKH input.
* @param input The input to sign.
* @param params All transaction signing context.
* @returns The generated `scriptSig` as a byte array.
*/
generateScriptSig(input, { inputs, outputs, pairkey, locktime, version }) {
let hexTransaction = new buffer_1.ByteBuffer((0, utils_1.numberToHexLE)(version, 32)); // version
hexTransaction.append((0, utils_1.numberToVarint)(inputs.length)); // number of inputs
inputs.forEach(txin => {
var _a;
hexTransaction.append((0, utils_1.hexToBytes)(txin.txid).reverse()); // txid
hexTransaction.append((0, utils_1.numberToHexLE)(txin.vout, 32)); // index output (vout)
if (txin.txid === input.txid) {
let script = (0, utils_1.hexToBytes)(txin.scriptPubKey);
hexTransaction.append((0, utils_1.numberToVarint)(script.length));
hexTransaction.append(script);
}
else
hexTransaction.append(new Uint8Array([0])); // length 0x00 to sign
// 0xfffffffd Replace By Fee (RBF) enabled BIP 125
hexTransaction.append((0, utils_1.hexToBytes)((_a = input.sequence) !== null && _a !== void 0 ? _a : "fffffffd").reverse());
});
hexTransaction.append((0, utils_1.numberToVarint)(outputs.length)); // number of outputs
hexTransaction.append(this.outputsRaw(outputs));
hexTransaction.append((0, utils_1.numberToHexLE)(locktime, 32)); // locktime
hexTransaction.append((0, utils_1.numberToHexLE)(opcodes_1.OP_CODES.SIGHASH_ALL, 32));
let sigHash = (0, utils_1.hash256)(hexTransaction.raw()); // hash256 -> sha256(sha256(content))
let scriptSig = new buffer_1.ByteBuffer(pairkey.signDER(sigHash));
scriptSig.append((0, utils_1.numberToHexLE)(opcodes_1.OP_CODES.SIGHASH_ALL, 8));
scriptSig.prepend((0, utils_1.numberToHex)(scriptSig.length, 8));
let publicKey = pairkey.getPublicKey();
scriptSig.append((0, utils_1.numberToHex)(publicKey.length, 8));
scriptSig.append(publicKey);
return scriptSig.raw();
}
/**
* Generates the witness data for a SegWit input (P2WPKH).
* @param input The input to sign.
* @param params All transaction signing context.
* @returns The witness field as a byte array.
*/
generateWitness(input, { inputs, outputs, pairkey, locktime, version }) {
var _a;
let hexTransaction = new buffer_1.ByteBuffer((0, utils_1.numberToHexLE)(version, 32)); // version
// hashPrevouts
let prevouts = inputs.map(input => {
let build = new buffer_1.ByteBuffer((0, utils_1.hexToBytes)(input.txid).reverse());
build.append((0, utils_1.numberToHexLE)(input.vout, 32));
return build.raw();
});
let hashPrevouts = (0, utils_1.hash256)(buffer_1.ByteBuffer.merge(prevouts));
hexTransaction.append(hashPrevouts);
// hashSequence
let sequence = inputs.map(input => { var _a; return (0, utils_1.hexToBytes)((_a = input.sequence) !== null && _a !== void 0 ? _a : "fffffffd").reverse(); });
let hashSequence = (0, utils_1.hash256)(buffer_1.ByteBuffer.merge(sequence));
hexTransaction.append(hashSequence);
// out point
hexTransaction.append((0, utils_1.hexToBytes)(input.txid).reverse());
hexTransaction.append((0, utils_1.numberToHexLE)(input.vout, 32));
// script code
let scriptCode = (0, txutils_1.scriptPubkeyToScriptCode)(input.scriptPubKey);
hexTransaction.append(scriptCode);
// amount
hexTransaction.append((0, utils_1.numberToHexLE)(input.value, 64));
// sequence
// 0xfffffffd Replace By Fee (RBF) enabled BIP 125
hexTransaction.append((0, utils_1.hexToBytes)((_a = input.sequence) !== null && _a !== void 0 ? _a : "fffffffd").reverse());
// hashOutputs
let hashOutputs = (0, utils_1.hash256)(this.outputsRaw(outputs));
hexTransaction.append(hashOutputs);
hexTransaction.append((0, utils_1.numberToHexLE)(locktime, 32)); // locktime
hexTransaction.append((0, utils_1.numberToHexLE)(opcodes_1.OP_CODES.SIGHASH_ALL, 32)); // sighash
let sigHash = (0, utils_1.hash256)(hexTransaction.raw()); // hash256 -> sha256(sha256(content))
let scriptSig = new buffer_1.ByteBuffer(pairkey.signDER(sigHash));
scriptSig.append((0, utils_1.numberToHex)(opcodes_1.OP_CODES.SIGHASH_ALL, 8));
scriptSig.prepend((0, utils_1.numberToVarint)(scriptSig.length));
let publicKey = pairkey.getPublicKey();
scriptSig.append((0, utils_1.numberToVarint)(publicKey.length));
scriptSig.append(publicKey);
scriptSig.prepend((0, utils_1.numberToHex)(2, 8)); // 2 items(signature & pubkey) 0x02
return scriptSig.raw();
}
/**
* Serializes transaction outputs into their raw binary format.
* @param outputs List of transaction outputs.
* @returns Byte array of all outputs serialized.
*/
outputsRaw(outputs) {
const rows = outputs.map(output => {
let txoutput = new buffer_1.ByteBuffer((0, utils_1.numberToHexLE)(output.amount, 64));
let scriptPubKey = (0, txutils_1.addressToScriptPubKey)(output.address);
txoutput.append((0, utils_1.numberToVarint)(scriptPubKey.length));
txoutput.append(scriptPubKey);
return txoutput.raw();
}).flat();
return buffer_1.ByteBuffer.merge(rows);
}
/**
* Validates a transaction input.
* Throws if txid is invalid, scriptPubKey is missing, or the txid is duplicated.
* @param input The input to validate.
* @param inputs The current list of inputs.
*/
validateInput(input, inputs) {
if (input.txid.length % 2 != 0)
throw new Error("txid is in invalid format, expected a hexadecimal string");
else if ((0, utils_1.getBytesCount)(input.txid) != 32)
throw new Error("Expected a valid txid with 32 bytes");
else if (input.scriptPubKey && input.scriptPubKey.length % 2 != 0)
throw new Error("scriptPubKey is in invalid format, expected a hexadecimal string");
if (inputs.some(i => i.txid == input.txid))
throw new Error("An input with this txid has already been added");
}
/**
* Validates a transaction output.
* Throws if amount is non-positive, address is invalid, or address is duplicated.
* @param output The output to validate.
* @param outputs The current list of outputs.
*/
validateOutput(output, outputs) {
if (output.amount <= 0)
throw new Error("Expected a valid amount");
if (!address_1.Address.isValid(output.address))
throw new Error("Expected a valid address to output");
if (outputs.some(o => o.address == output.address))
throw new Error("An output with this address has already been added");
}
}
exports.TransactionBuilder = TransactionBuilder;
//# sourceMappingURL=txbuilder.js.map