UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

340 lines 10.3 kB
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