@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
340 lines • 10.3 kB
JavaScript
import { crypto as bitCrypto, PaymentType, Psbt, toSatoshi, toXOnly, } from '@btc-vision/bitcoin';
import { TransactionType } from '../enums/TransactionType.js';
import { TransactionBuilder } from './TransactionBuilder.js';
import { CustomGenerator } from '../../generators/builders/CustomGenerator.js';
import { BitcoinUtils } from '../../utils/BitcoinUtils.js';
import { EcKeyPair } from '../../keypair/EcKeyPair.js';
import { AddressGenerator } from '../../generators/AddressGenerator.js';
import {} from '@btc-vision/ecpair';
import { isUniversalSigner } from '../../signer/TweakedSigner.js';
/**
* Class for interaction transactions
* @class CustomScriptTransaction
*/
export class CustomScriptTransaction extends TransactionBuilder {
type = TransactionType.CUSTOM_CODE;
/**
* The contract address
* @protected
*/
_scriptAddress;
/**
* The tap leaf script
* @private
*/
tapLeafScript = null;
/**
* The target script redeem
* @private
*/
targetScriptRedeem = null;
/**
* The left over funds script redeem
* @private
*/
leftOverFundsScriptRedeem = null;
/**
* The compiled target script
* @private
*/
compiledTargetScript;
/**
* The script tree
* @private
*/
scriptTree;
/**
* The deployment bitcoin generator
* @private
*/
generator;
/**
* The contract seed
* @private
*/
scriptSeed;
/**
* The contract signer
* @private
*/
contractSigner;
/**
* The contract salt random bytes
* @private
*/
randomBytes;
/**
* The witnesses
* @private
*/
witnesses;
annexData;
constructor(parameters) {
super(parameters);
if (!parameters.script)
throw new Error('Bitcoin script is required');
if (!parameters.witnesses)
throw new Error('Witness(es) are required');
this.witnesses = parameters.witnesses;
this.randomBytes = parameters.randomBytes || BitcoinUtils.rndBytes();
this.LOCK_LEAF_SCRIPT = this.defineLockScript();
this.scriptSeed = this.getContractSeed();
this.contractSigner = EcKeyPair.fromSeedKeyPair(this.scriptSeed, this.network);
this.generator = new CustomGenerator(this.internalPubKeyToXOnly(), this.network);
this.compiledTargetScript = this.generator.compile(parameters.script);
this.scriptTree = this.getScriptTree();
this.internalInit();
this._scriptAddress = AddressGenerator.generatePKSH(this.scriptSeed, this.network);
}
/**
* @description Get the contract address (PKSH)
*/
get scriptAddress() {
return this._scriptAddress;
}
/**
* @description Get the P2TR address
*/
get p2trAddress() {
return this.to || this.getScriptAddress();
}
exportCompiledTargetScript() {
return this.compiledTargetScript;
}
/**
* Get the random bytes used for the interaction
* @returns {Uint8Array} The random bytes
*/
getRndBytes() {
return this.randomBytes;
}
/**
* Get the contract signer public key
* @protected
*/
contractSignerXOnlyPubKey() {
return toXOnly(this.contractSigner.publicKey);
}
/**
* Build the transaction
* @protected
*/
async buildTransaction() {
if (!this.to) {
this.to = this.getScriptAddress();
}
const selectedRedeem = this.contractSigner
? this.targetScriptRedeem
: this.leftOverFundsScriptRedeem;
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 amountSpent = this.getTransactionOPNetFee();
this.addOutput({
value: toSatoshi(amountSpent),
address: this.to,
});
await this.addRefundOutput(amountSpent + this.addOptionalOutputsAndGetAmount());
}
/**
* Sign the inputs
* @param {Psbt} transaction The transaction to sign
* @protected
*/
async signInputs(transaction) {
if (!this.contractSigner) {
await super.signInputs(transaction);
return;
}
// Input 0: sequential (contractSigner + main signer, custom finalizer)
try {
transaction.signInput(0, this.contractSigner);
}
catch {
// contractSigner may fail for some script types
}
transaction.signInput(0, this.getSignerKey());
transaction.finalizeInput(0, this.customFinalizer);
// Inputs 1+: parallel key-path if available, then sequential for remaining
const signedIndices = new Set([0]);
if (this.canUseParallelSigning && isUniversalSigner(this.signer)) {
try {
const result = await this.signKeyPathInputsParallel(transaction, new Set([0]));
if (result.success) {
for (const idx of result.signatures.keys())
signedIndices.add(idx);
}
}
catch (e) {
this.error(`Parallel signing failed: ${e.message}`);
}
}
for (let i = 1; i < transaction.data.inputs.length; i++) {
if (!signedIndices.has(i)) {
transaction.signInput(i, this.getSignerKey());
}
}
// Finalize inputs 1+
for (let i = 1; i < transaction.data.inputs.length; i++) {
try {
transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this));
}
catch {
transaction.finalizeInput(i);
}
}
}
/**
* Get the tap output
* @protected
*/
generateScriptAddress() {
if (this.useP2MR) {
return {
network: this.network,
scriptTree: this.scriptTree,
name: PaymentType.P2MR,
};
}
return {
internalPubkey: this.internalPubKeyToXOnly(),
network: this.network,
scriptTree: this.scriptTree,
name: PaymentType.P2TR,
};
}
/**
* Generate the tap data
* @protected
*/
generateTapData() {
const selectedRedeem = this.contractSigner
? this.targetScriptRedeem
: this.leftOverFundsScriptRedeem;
if (!selectedRedeem) {
throw new Error('Left over funds script redeem is required');
}
if (!this.scriptTree) {
throw new Error('Script tree is required');
}
if (this.useP2MR) {
return {
network: this.network,
scriptTree: this.scriptTree,
redeem: selectedRedeem,
name: PaymentType.P2MR,
};
}
return {
internalPubkey: this.internalPubKeyToXOnly(),
network: this.network,
scriptTree: this.scriptTree,
redeem: selectedRedeem,
name: PaymentType.P2TR,
};
}
getScriptSolution(input) {
if (!input.tapScriptSig) {
throw new Error('Tap script signature is required');
}
const witnesses = [...this.witnesses];
if (input.tapScriptSig) {
for (const sig of input.tapScriptSig) {
witnesses.push(sig.signature);
}
}
return witnesses;
}
/**
* Generate the contract seed for the deployment
* @private
*/
getContractSeed() {
return bitCrypto.hash256(this.randomBytes);
}
/**
* Finalize the transaction
* @param _inputIndex
* @param input
*/
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);
if (this.annexData && this.annexData.length > 0) {
let annex;
if (this.annexData[0] === 0x50) {
annex = this.annexData;
}
else {
const prefixed = new Uint8Array(this.annexData.length + 1);
prefixed[0] = 0x50;
prefixed.set(this.annexData, 1);
annex = prefixed;
}
witness.push(annex);
}
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witness),
};
};
/**
* Generate the redeem scripts
* @private
*/
generateRedeemScripts() {
this.targetScriptRedeem = {
name: PaymentType.P2TR,
//pubkeys: this.getPubKeys(),
output: this.compiledTargetScript,
redeemVersion: 192,
};
this.leftOverFundsScriptRedeem = {
name: PaymentType.P2TR,
//pubkeys: this.getPubKeys(),
output: this.getLeafScript(),
redeemVersion: 192,
};
}
/**
* Get the second leaf script
* @private
*/
getLeafScript() {
return this.LOCK_LEAF_SCRIPT;
}
/**
* Get the script tree
* @private
*/
getScriptTree() {
this.generateRedeemScripts();
return [
{
output: this.compiledTargetScript,
version: 192,
},
{
output: this.getLeafScript(),
version: 192,
},
];
}
}
//# sourceMappingURL=CustomScriptTransaction.js.map