@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
516 lines (515 loc) • 19.6 kB
JavaScript
import { Logger } from '@btc-vision/logger';
import { address as bitAddress, crypto as bitCrypto, getFinalScripts, isP2MS, isP2PK, isP2PKH, isP2SHScript, isP2TR, isP2WPKH, isP2WSHScript, isUnknownSegwitVersion, opcodes, payments, PaymentType, script, toXOnly, varuint, } from '@btc-vision/bitcoin';
import { TweakedSigner } from '../../signer/TweakedSigner.js';
import { canSignNonTaprootInput, isTaprootInput, pubkeyInScript, } from '../../signer/SignerUtils.js';
export var TransactionSequence;
(function (TransactionSequence) {
TransactionSequence[TransactionSequence["REPLACE_BY_FEE"] = 4294967293] = "REPLACE_BY_FEE";
TransactionSequence[TransactionSequence["FINAL"] = 4294967295] = "FINAL";
})(TransactionSequence || (TransactionSequence = {}));
export class TweakedTransaction extends Logger {
constructor(data) {
super();
this.logColor = '#00ffe1';
this.finalized = false;
this.signed = false;
this.scriptData = null;
this.tapData = null;
this.inputs = [];
this.sequence = TransactionSequence.REPLACE_BY_FEE;
this.tapLeafScript = null;
this.isBrowser = false;
this.regenerated = false;
this.ignoreSignatureErrors = false;
this.noSignatures = false;
this.customFinalizerP2SH = (inputIndex, input, scriptA, isSegwit, isP2SH, isP2WSH) => {
const inputDecoded = this.inputs[inputIndex];
if (isP2SH && input.partialSig && inputDecoded && inputDecoded.redeemScript) {
const signatures = input.partialSig.map((sig) => sig.signature) || [];
const scriptSig = script.compile([...signatures, inputDecoded.redeemScript]);
return {
finalScriptSig: scriptSig,
finalScriptWitness: undefined,
};
}
return getFinalScripts(inputIndex, input, scriptA, isSegwit, isP2SH, isP2WSH, true, this.unlockScript);
};
this.signer = data.signer;
this.network = data.network;
this.noSignatures = data.noSignatures || false;
this.nonWitnessUtxo = data.nonWitnessUtxo;
this.unlockScript = data.unlockScript;
this.isBrowser = typeof window !== 'undefined';
}
static readScriptWitnessToWitnessStack(Buffer) {
let offset = 0;
function readSlice(n) {
const slice = Buffer.subarray(offset, offset + n);
offset += n;
return slice;
}
function readVarInt() {
const varint = varuint.decode(Buffer, offset);
offset += varint.bytes;
return varint.numberValue || 0;
}
function readVarSlice() {
const len = readVarInt();
return readSlice(len);
}
function readVector() {
const count = readVarInt();
const vector = [];
for (let i = 0; i < count; i++) {
vector.push(readVarSlice());
}
return vector;
}
return readVector();
}
static preEstimateTaprootTransactionFees(feeRate, numInputs, numOutputs, numWitnessElements, witnessElementSize, emptyWitness, taprootControlWitnessSize = 32n, taprootScriptSize = 139n) {
const txHeaderSize = 10n;
const inputBaseSize = 41n;
const outputSize = 68n;
const taprootWitnessBaseSize = 1n;
const baseTxSize = txHeaderSize + inputBaseSize * numInputs + outputSize * numOutputs;
const witnessSize = numInputs * taprootWitnessBaseSize +
numWitnessElements * witnessElementSize +
taprootControlWitnessSize * numInputs +
taprootScriptSize * numInputs +
emptyWitness;
const weight = baseTxSize * 3n + (baseTxSize + witnessSize);
const vSize = weight / 4n;
return vSize * feeRate;
}
static signInput(transaction, input, i, signer, sighashTypes) {
if (sighashTypes && sighashTypes[0])
input.sighashType = sighashTypes[0];
transaction.signInput(i, signer, sighashTypes.length ? sighashTypes : undefined);
}
static calculateSignHash(sighashTypes) {
if (!sighashTypes) {
throw new Error('Sighash types are required');
}
let signHash = 0;
for (const sighashType of sighashTypes) {
signHash |= sighashType;
}
return signHash || 0;
}
ignoreSignatureError() {
this.ignoreSignatureErrors = true;
}
getScriptAddress() {
if (!this.scriptData || !this.scriptData.address) {
throw new Error('Tap data is required');
}
return this.scriptData.address;
}
getTransaction() {
return this.transaction.extractTransaction(false);
}
getTapAddress() {
if (!this.tapData || !this.tapData.address) {
throw new Error('Tap data is required');
}
return this.tapData.address;
}
disableRBF() {
if (this.signed)
throw new Error('Transaction is already signed');
this.sequence = TransactionSequence.FINAL;
for (const input of this.inputs) {
input.sequence = TransactionSequence.FINAL;
}
}
getTweakerHash() {
return this.tapData?.hash;
}
preEstimateTransactionFees(feeRate, numInputs, numOutputs, numSignatures, numPubkeys) {
const txHeaderSize = 10n;
const inputBaseSize = 41n;
const outputSize = 68n;
const signatureSize = 144n;
const pubkeySize = 34n;
const baseTxSize = txHeaderSize + inputBaseSize * numInputs + outputSize * numOutputs;
const redeemScriptSize = 1n + numPubkeys * (1n + pubkeySize) + 1n + numSignatures;
const witnessSize = numSignatures * signatureSize + numPubkeys * pubkeySize + redeemScriptSize;
const weight = baseTxSize * 3n + (baseTxSize + witnessSize);
const vSize = weight / 4n;
return vSize * feeRate;
}
generateTapData() {
return {
internalPubkey: this.internalPubKeyToXOnly(),
network: this.network,
name: PaymentType.P2TR,
};
}
generateScriptAddress() {
return {
internalPubkey: this.internalPubKeyToXOnly(),
network: this.network,
name: PaymentType.P2TR,
};
}
getSignerKey() {
return this.signer;
}
async signInput(transaction, input, i, signer, reverse = false, errored = false) {
const publicKey = signer.publicKey;
let isTaproot = isTaprootInput(input);
if (reverse) {
isTaproot = !isTaproot;
}
let signed = false;
let didError = false;
if (isTaproot) {
try {
await this.attemptSignTaproot(transaction, input, i, signer, publicKey);
signed = true;
}
catch (e) {
this.error(`Failed to sign Taproot script path input ${i} (reverse: ${reverse}): ${e.message}`);
didError = true;
}
}
else {
if (!reverse ? canSignNonTaprootInput(input, publicKey) : true) {
try {
await this.signNonTaprootInput(signer, transaction, i);
signed = true;
}
catch (e) {
this.error(`Failed to sign non-Taproot input ${i}: ${e.stack}`);
didError = true;
}
}
}
if (!signed) {
if (didError && errored) {
throw new Error(`Failed to sign input ${i} with the provided signer.`);
}
try {
await this.signInput(transaction, input, i, signer, true, didError);
}
catch {
throw new Error(`Cannot sign input ${i} with the provided signer.`);
}
}
}
splitArray(arr, chunkSize) {
if (chunkSize <= 0) {
throw new Error('Chunk size must be greater than 0.');
}
const result = [];
for (let i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
}
async signInputs(transaction) {
if ('multiSignPsbt' in this.signer) {
await this.signInputsWalletBased(transaction);
return;
}
await this.signInputsNonWalletBased(transaction);
}
async signInputsNonWalletBased(transaction) {
const txs = transaction.data.inputs;
const batchSize = 20;
const batches = this.splitArray(txs, batchSize);
if (!this.noSignatures) {
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
const promises = [];
const offset = i * batchSize;
for (let j = 0; j < batch.length; j++) {
const index = offset + j;
const input = batch[j];
try {
promises.push(this.signInput(transaction, input, index, this.signer));
}
catch (e) {
this.log(`Failed to sign input ${index}: ${e.stack}`);
}
}
await Promise.all(promises);
}
}
for (let i = 0; i < transaction.data.inputs.length; i++) {
transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this));
}
this.finalized = true;
}
internalPubKeyToXOnly() {
return toXOnly(Buffer.from(this.signer.publicKey));
}
internalInit() {
this.scriptData = payments.p2tr(this.generateScriptAddress());
this.tapData = payments.p2tr(this.generateTapData());
}
tweakSigner() {
if (this.tweakedSigner)
return;
this.tweakedSigner = this.getTweakedSigner(true);
}
getTweakedSigner(useTweakedHash = false, signer = this.signer) {
const settings = {
network: this.network,
};
if (useTweakedHash) {
settings.tweakHash = this.getTweakerHash();
}
if (!('privateKey' in signer)) {
return;
}
return TweakedSigner.tweakSigner(signer, settings);
}
generateP2SHRedeemScript(customWitnessScript) {
const p2wsh = payments.p2wsh({
redeem: { output: customWitnessScript },
network: this.network,
});
const p2sh = payments.p2sh({
redeem: p2wsh,
network: this.network,
});
return p2sh.output;
}
generateP2SHRedeemScriptLegacy(inputAddr) {
const pubKeyHash = bitCrypto.hash160(this.signer.publicKey);
const redeemScript = script.compile([
opcodes.OP_DUP,
opcodes.OP_HASH160,
pubKeyHash,
opcodes.OP_EQUALVERIFY,
opcodes.OP_CHECKSIG,
]);
const redeemScriptHash = bitCrypto.hash160(redeemScript);
const outputScript = script.compile([
opcodes.OP_HASH160,
redeemScriptHash,
opcodes.OP_EQUAL,
]);
const p2wsh = payments.p2wsh({
redeem: { output: redeemScript },
network: this.network,
});
const p2sh = payments.p2sh({
redeem: p2wsh,
network: this.network,
});
const address = bitAddress.fromOutputScript(outputScript, this.network);
if (address === inputAddr && p2sh.redeem && p2sh.redeem.output) {
return {
redeemScript,
outputScript: p2sh.redeem.output,
};
}
return;
}
generateP2SHP2PKHRedeemScript(inputAddr) {
const pubkey = Buffer.isBuffer(this.signer.publicKey)
? this.signer.publicKey
: Buffer.from(this.signer.publicKey, 'hex');
const w = payments.p2wpkh({
pubkey: pubkey,
network: this.network,
});
const p = payments.p2sh({
redeem: w,
network: this.network,
});
const address = p.address;
const redeemScript = p.redeem?.output;
if (!redeemScript) {
throw new Error('Failed to generate P2SH-P2WPKH redeem script');
}
if (address === inputAddr && p.redeem && p.redeem.output && p.output) {
return {
redeemScript: p.redeem.output,
outputScript: p.output,
};
}
return;
}
generatePsbtInputExtended(utxo, i, _extra = false) {
const script = Buffer.from(utxo.scriptPubKey.hex, 'hex');
const input = {
hash: utxo.transactionId,
index: utxo.outputIndex,
sequence: this.sequence,
witnessUtxo: {
value: Number(utxo.value),
script,
},
};
if (isP2PKH(script)) {
if (utxo.nonWitnessUtxo) {
input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
? utxo.nonWitnessUtxo
: Buffer.from(utxo.nonWitnessUtxo, 'hex');
}
else {
throw new Error('Missing nonWitnessUtxo for P2PKH UTXO');
}
}
else if (isP2WPKH(script) || isUnknownSegwitVersion(script)) {
}
else if (isP2WSHScript(script)) {
if (!utxo.witnessScript) {
throw new Error('Missing witnessScript for P2WSH UTXO');
}
input.witnessScript = Buffer.isBuffer(utxo.witnessScript)
? utxo.witnessScript
: Buffer.from(utxo.witnessScript, 'hex');
}
else if (isP2SHScript(script)) {
let redeemScriptBuf;
if (utxo.redeemScript) {
redeemScriptBuf = Buffer.isBuffer(utxo.redeemScript)
? utxo.redeemScript
: Buffer.from(utxo.redeemScript, 'hex');
}
else {
if (!utxo.scriptPubKey.address) {
throw new Error('Missing redeemScript and no address to regenerate it for P2SH UTXO');
}
const legacyScripts = this.generateP2SHP2PKHRedeemScript(utxo.scriptPubKey.address);
if (!legacyScripts) {
throw new Error('Missing redeemScript for P2SH UTXO and unable to regenerate');
}
redeemScriptBuf = legacyScripts.redeemScript;
}
input.redeemScript = redeemScriptBuf;
const payment = payments.p2sh({ redeem: { output: input.redeemScript } });
if (!payment.redeem) {
throw new Error('Failed to extract redeem script from P2SH UTXO');
}
const redeemOutput = payment.redeem.output;
if (!redeemOutput) {
throw new Error('Failed to extract redeem output from P2SH UTXO');
}
if (utxo.nonWitnessUtxo) {
input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
? utxo.nonWitnessUtxo
: Buffer.from(utxo.nonWitnessUtxo, 'hex');
}
if (isP2WPKH(redeemOutput)) {
delete input.nonWitnessUtxo;
}
else if (isP2WSHScript(redeemOutput)) {
delete input.nonWitnessUtxo;
if (!input.witnessScript) {
throw new Error('Missing witnessScript for P2SH-P2WSH UTXO');
}
}
else {
delete input.witnessUtxo;
}
}
else if (isP2TR(script)) {
if (this.sighashTypes) {
const inputSign = TweakedTransaction.calculateSignHash(this.sighashTypes);
if (inputSign)
input.sighashType = inputSign;
}
this.tweakSigner();
input.tapInternalKey = this.internalPubKeyToXOnly();
}
else if (isP2PK(script) || isP2MS(script)) {
if (utxo.nonWitnessUtxo) {
input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
? utxo.nonWitnessUtxo
: Buffer.from(utxo.nonWitnessUtxo, 'hex');
}
else {
throw new Error('Missing nonWitnessUtxo for P2PK or P2MS UTXO');
}
}
else {
this.error(`Unknown or unsupported script type for output: ${utxo.scriptPubKey.hex}`);
}
if (i === 0) {
if (this.tapLeafScript) {
input.tapLeafScript = [this.tapLeafScript];
}
if (this.nonWitnessUtxo) {
input.nonWitnessUtxo = this.nonWitnessUtxo;
}
}
return input;
}
async signInputsWalletBased(transaction) {
const signer = this.signer;
await signer.multiSignPsbt([transaction]);
for (let i = 0; i < transaction.data.inputs.length; i++) {
transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this));
}
this.finalized = true;
}
async attemptSignTaproot(transaction, input, i, signer, publicKey) {
const isScriptSpend = this.isTaprootScriptSpend(input, publicKey);
if (isScriptSpend) {
await this.signTaprootInput(signer, transaction, i);
}
else {
let tweakedSigner;
if (signer !== this.signer) {
tweakedSigner = this.getTweakedSigner(true, signer);
}
else {
if (!this.tweakedSigner)
this.tweakSigner();
tweakedSigner = this.tweakedSigner;
}
if (tweakedSigner) {
try {
await this.signTaprootInput(tweakedSigner, transaction, i);
}
catch (e) {
tweakedSigner = this.getTweakedSigner(false, this.signer);
if (!tweakedSigner) {
throw new Error(`Failed to obtain tweaked signer for input ${i}.`);
}
await this.signTaprootInput(tweakedSigner, transaction, i);
}
}
else {
this.error(`Failed to obtain tweaked signer for input ${i}.`);
}
}
}
isTaprootScriptSpend(input, publicKey) {
if (input.tapLeafScript && input.tapLeafScript.length > 0) {
for (const tapLeafScript of input.tapLeafScript) {
if (pubkeyInScript(publicKey, tapLeafScript.script)) {
return true;
}
}
}
return false;
}
async signTaprootInput(signer, transaction, i, tapLeafHash) {
if ('signTaprootInput' in signer) {
try {
await signer.signTaprootInput(transaction, i, tapLeafHash);
}
catch {
throw new Error('Failed to sign Taproot input with provided signer.');
}
}
else {
transaction.signTaprootInput(i, signer);
}
}
async signNonTaprootInput(signer, transaction, i) {
if ('signInput' in signer) {
await signer.signInput(transaction, i);
}
else {
transaction.signInput(i, signer);
}
}
}