@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
372 lines (371 loc) • 14.3 kB
JavaScript
import { crypto as bitcoinCrypto, opcodes, PaymentType, Psbt, script, toXOnly, } from '@btc-vision/bitcoin';
import { TransactionBuilder } from './TransactionBuilder.js';
import { TransactionType } from '../enums/TransactionType.js';
import { MultiSignGenerator } from '../../generators/builders/MultiSignGenerator.js';
import { EcKeyPair } from '../../keypair/EcKeyPair.js';
export class MultiSignTransaction extends TransactionBuilder {
constructor(parameters) {
if (!parameters.refundVault) {
throw new Error('Refund vault is required');
}
if (!parameters.requestedAmount) {
throw new Error('Requested amount is required');
}
if (!parameters.receiver) {
throw new Error('Receiver is required');
}
super({
...parameters,
signer: EcKeyPair.fromPrivateKey(bitcoinCrypto.sha256(Buffer.from('aaaaaaaa', 'utf-8'))),
priorityFee: 0n,
gasSatFee: 0n,
});
this.type = TransactionType.MULTI_SIG;
this.targetScriptRedeem = null;
this.leftOverFundsScriptRedeem = null;
this.originalInputCount = 0;
this.sighashTypes = MultiSignTransaction.signHashTypesArray;
this.customFinalizer = (_inputIndex, input) => {
if (!this.tapLeafScript) {
throw new Error('Tap leaf script is required');
}
const scriptSolution = this.getScriptSolution(input);
const witness = scriptSolution
.concat(this.tapLeafScript.script)
.concat(this.tapLeafScript.controlBlock);
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witness),
};
};
if (!parameters.pubkeys) {
throw new Error('Pubkeys are required');
}
if (parameters.psbt) {
this.log(`Using provided PSBT.`);
this.transaction = parameters.psbt;
this.originalInputCount = this.transaction.data.inputs.length;
}
this.refundVault = parameters.refundVault;
this.requestedAmount = parameters.requestedAmount;
this.receiver = parameters.receiver;
this.publicKeys = parameters.pubkeys;
this.minimumSignatures = parameters.minimumSignatures;
this.compiledTargetScript = MultiSignGenerator.compile(parameters.pubkeys, this.minimumSignatures);
this.scriptTree = this.getScriptTree();
this.internalInit();
}
static fromBase64(params) {
const psbt = Psbt.fromBase64(params.psbt, { network: params.network });
return new MultiSignTransaction({
...params,
psbt,
});
}
static verifyIfSigned(psbt, signerPubKey) {
let alreadySigned = false;
for (let i = 1; i < psbt.data.inputs.length; i++) {
const input = psbt.data.inputs[i];
if (!input.finalScriptWitness) {
continue;
}
const decoded = TransactionBuilder.readScriptWitnessToWitnessStack(input.finalScriptWitness);
if (decoded.length < 3) {
continue;
}
for (let j = 0; j < decoded.length - 2; j += 3) {
const pubKey = decoded[j + 2];
if (pubKey.equals(signerPubKey)) {
alreadySigned = true;
break;
}
}
}
return alreadySigned;
}
static signPartial(psbt, signer, originalInputCount, minimums) {
let signed = false;
let final = true;
for (let i = originalInputCount; i < psbt.data.inputs.length; i++) {
const input = psbt.data.inputs[i];
if (!input.tapInternalKey) {
input.tapInternalKey = toXOnly(MultiSignTransaction.numsPoint);
}
const partialSignatures = [];
if (input.finalScriptWitness) {
const decoded = TransactionBuilder.readScriptWitnessToWitnessStack(input.finalScriptWitness);
input.tapLeafScript = [
{
leafVersion: 192,
script: decoded[decoded.length - 2],
controlBlock: decoded[decoded.length - 1],
},
];
for (let j = 0; j < decoded.length - 2; j += 3) {
partialSignatures.push({
signature: decoded[j],
leafHash: decoded[j + 1],
pubkey: decoded[j + 2],
});
}
input.tapScriptSig = (input.tapScriptSig || []).concat(partialSignatures);
}
delete input.finalScriptWitness;
const signHashTypes = MultiSignTransaction.signHashTypesArray
? [MultiSignTransaction.calculateSignHash(MultiSignTransaction.signHashTypesArray)]
: [];
try {
MultiSignTransaction.signInput(psbt, input, i, signer, signHashTypes);
signed = true;
}
catch (e) {
console.log(e);
}
if (signed) {
if (!input.tapScriptSig)
throw new Error('No new signatures for input');
if (input.tapScriptSig.length !== minimums[i - originalInputCount]) {
final = false;
}
}
}
return {
signed,
final: !signed ? false : final,
};
}
static dedupeSignatures(original, partial) {
const signatures = new Map();
for (const sig of original) {
signatures.set(sig.pubkey.toString('hex'), sig);
}
for (const sig of partial) {
if (!signatures.has(sig.pubkey.toString('hex'))) {
signatures.set(sig.pubkey.toString('hex'), sig);
}
}
return Array.from(signatures.values());
}
static attemptFinalizeInputs(psbt, startIndex, orderedPubKeys, isFinal) {
let finalizedInputs = 0;
for (let i = startIndex; i < psbt.data.inputs.length; i++) {
try {
const input = psbt.data.inputs[i];
if (!input.tapInternalKey) {
input.tapInternalKey = toXOnly(MultiSignTransaction.numsPoint);
}
const partialSignatures = [];
if (input.finalScriptWitness) {
const decoded = TransactionBuilder.readScriptWitnessToWitnessStack(input.finalScriptWitness);
for (let j = 0; j < decoded.length - 2; j += 3) {
partialSignatures.push({
signature: decoded[j],
leafHash: decoded[j + 1],
pubkey: decoded[j + 2],
});
}
input.tapLeafScript = [
{
leafVersion: 192,
script: decoded[decoded.length - 2],
controlBlock: decoded[decoded.length - 1],
},
];
input.tapScriptSig = MultiSignTransaction.dedupeSignatures(input.tapScriptSig || [], partialSignatures);
}
delete input.finalScriptWitness;
psbt.finalizeInput(i, (inputIndex, input) => {
return MultiSignTransaction.partialFinalizer(inputIndex, input, [], orderedPubKeys[i - startIndex], isFinal);
});
finalizedInputs++;
}
catch (e) { }
}
return finalizedInputs === psbt.data.inputs.length - startIndex;
}
finalizeTransactionInputs() {
let finalized = false;
try {
for (let i = this.originalInputCount; i < this.transaction.data.inputs.length; i++) {
this.transaction.finalizeInput(i, this.customFinalizer.bind(this));
}
finalized = true;
}
catch (e) {
this.error(`Error finalizing transaction inputs: ${e.stack}`);
}
return finalized;
}
async signPSBT() {
if (await this.signTransaction()) {
return this.transaction;
}
throw new Error('Could not sign transaction');
}
async buildTransaction() {
const selectedRedeem = this.targetScriptRedeem;
if (!selectedRedeem) {
throw new Error('Left over funds script redeem is required');
}
if (!selectedRedeem.redeemVersion) {
throw new Error('Left over funds script redeem version is required');
}
if (!selectedRedeem.output) {
throw new Error('Left over funds script redeem output is required');
}
this.tapLeafScript = {
leafVersion: selectedRedeem.redeemVersion,
script: selectedRedeem.output,
controlBlock: this.getWitness(),
};
this.addInputsFromUTXO();
const outputLeftAmount = this.calculateOutputLeftAmountFromVaults(this.utxos);
if (outputLeftAmount < 0) {
throw new Error(`Output value left is negative ${outputLeftAmount}.`);
}
this.addOutput({
address: this.refundVault,
value: Number(outputLeftAmount),
});
this.addOutput({
address: this.receiver,
value: Number(this.requestedAmount),
});
}
async internalBuildTransaction(transaction, checkPartialSigs = false) {
const inputs = this.getInputs();
const outputs = this.getOutputs();
transaction.setMaximumFeeRate(this._maximumFeeRate);
transaction.addInputs(inputs, checkPartialSigs);
for (let i = 0; i < this.updateInputs.length; i++) {
transaction.updateInput(i, this.updateInputs[i]);
}
transaction.addOutputs(outputs);
try {
await this.signInputs(transaction);
return this.finalizeTransactionInputs();
}
catch (e) {
const err = e;
this.error(`[internalBuildTransaction] Something went wrong while getting building the transaction: ${err.stack}`);
}
return false;
}
async signInputs(_transaction) { }
generateScriptAddress() {
return {
internalPubkey: toXOnly(MultiSignTransaction.numsPoint),
network: this.network,
scriptTree: this.scriptTree,
name: PaymentType.P2TR,
};
}
generateTapData() {
const selectedRedeem = this.targetScriptRedeem;
if (!selectedRedeem) {
throw new Error('Left over funds script redeem is required');
}
if (!this.scriptTree) {
throw new Error('Script tree is required');
}
return {
internalPubkey: toXOnly(MultiSignTransaction.numsPoint),
network: this.network,
scriptTree: this.scriptTree,
redeem: selectedRedeem,
name: PaymentType.P2TR,
};
}
getScriptSolution(input) {
if (!input.tapScriptSig) {
return [];
}
return input.tapScriptSig.map((sig) => {
return sig.signature;
});
}
getScriptTree() {
this.generateRedeemScripts();
return [
{
output: this.compiledTargetScript,
version: 192,
},
{
output: MultiSignTransaction.LOCK_LEAF_SCRIPT,
version: 192,
},
];
}
getTotalOutputAmount(utxos) {
let total = BigInt(0);
for (const utxo of utxos) {
total += BigInt(utxo.value);
}
return total;
}
calculateOutputLeftAmountFromVaults(utxos) {
const total = this.getTotalOutputAmount(utxos);
return total - this.requestedAmount;
}
generateRedeemScripts() {
this.targetScriptRedeem = {
name: PaymentType.P2TR,
output: this.compiledTargetScript,
redeemVersion: 192,
};
this.leftOverFundsScriptRedeem = {
name: PaymentType.P2TR,
output: MultiSignTransaction.LOCK_LEAF_SCRIPT,
redeemVersion: 192,
};
}
}
MultiSignTransaction.LOCK_LEAF_SCRIPT = script.compile([
opcodes.OP_XOR,
opcodes.OP_NOP,
opcodes.OP_CODESEPARATOR,
]);
MultiSignTransaction.signHashTypesArray = [];
MultiSignTransaction.numsPoint = Buffer.from('50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0', 'hex');
MultiSignTransaction.partialFinalizer = (inputIndex, input, partialSignatures, orderedPubKeys, isFinal) => {
if (!input.tapLeafScript ||
!input.tapLeafScript[0].script ||
!input.tapLeafScript[0].controlBlock) {
throw new Error('Tap leaf script is required');
}
if (!input.tapScriptSig) {
throw new Error(`No new signatures for input ${inputIndex}.`);
}
let scriptSolution = [];
if (!isFinal) {
scriptSolution = input.tapScriptSig
.map((sig) => {
return [sig.signature, sig.leafHash, sig.pubkey];
})
.flat();
}
else {
for (const pubKey of orderedPubKeys) {
let found = false;
for (const sig of input.tapScriptSig) {
if (sig.pubkey.equals(toXOnly(pubKey))) {
scriptSolution.push(sig.signature);
found = true;
}
}
if (!found) {
scriptSolution.push(Buffer.alloc(0));
}
}
scriptSolution = scriptSolution.reverse();
}
if (partialSignatures.length > 0) {
scriptSolution = scriptSolution.concat(partialSignatures);
}
const witness = scriptSolution
.concat(input.tapLeafScript[0].script)
.concat(input.tapLeafScript[0].controlBlock);
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witness),
};
};