UNPKG

@btc-vision/transaction

Version:

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

228 lines (227 loc) 8.45 kB
import { PaymentType, toXOnly } from '@btc-vision/bitcoin'; import { MINIMUM_AMOUNT_CA, MINIMUM_AMOUNT_REWARD, TransactionBuilder } from './TransactionBuilder.js'; import { CalldataGenerator } from '../../generators/builders/CalldataGenerator.js'; import { Compressor } from '../../bytecode/Compressor.js'; import { EcKeyPair } from '../../keypair/EcKeyPair.js'; import { BitcoinUtils } from '../../utils/BitcoinUtils.js'; import { ChallengeGenerator } from '../mineable/ChallengeGenerator.js'; export class SharedInteractionTransaction extends TransactionBuilder { constructor(parameters) { super(parameters); this.targetScriptRedeem = null; this.leftOverFundsScriptRedeem = null; this.customFinalizer = (_inputIndex, input) => { if (!this.tapLeafScript) { throw new Error('Tap leaf script is required'); } if (!this.contractSecret) { throw new Error('Contract secret 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.calldata) { throw new Error('Calldata is required'); } if (!parameters.preimage) { throw new Error('Preimage is required'); } this.preimage = parameters.preimage; this.disableAutoRefund = parameters.disableAutoRefund || false; this.rewardChallenge = ChallengeGenerator.generateMineableReward(this.preimage, this.network); this.calldata = Compressor.compress(parameters.calldata); this.randomBytes = parameters.randomBytes || BitcoinUtils.rndBytes(); this.scriptSigner = this.generateKeyPairFromSeed(); this.calldataGenerator = new CalldataGenerator(Buffer.from(this.signer.publicKey), this.scriptSignerXOnlyPubKey(), this.network); } getContractSecret() { return this.contractSecret; } getRndBytes() { return this.randomBytes; } getPreimage() { return this.preimage; } scriptSignerXOnlyPubKey() { return toXOnly(Buffer.from(this.scriptSigner.publicKey)); } generateKeyPairFromSeed() { return EcKeyPair.fromSeedKeyPair(this.randomBytes, this.network); } async buildTransaction() { const selectedRedeem = this.scriptSigner ? 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(), }; if (!this.regenerated) { this.addInputsFromUTXO(); } await this.createMineableRewardOutputs(); } async signInputs(transaction) { if (!this.scriptSigner) { await super.signInputs(transaction); return; } if ('multiSignPsbt' in this.signer) { await this.signInputsWalletBased(transaction); } else { await this.signInputsNonWalletBased(transaction); } } generateScriptAddress() { return { internalPubkey: this.internalPubKeyToXOnly(), network: this.network, scriptTree: this.scriptTree, name: PaymentType.P2TR, }; } generateTapData() { const selectedRedeem = this.scriptSigner ? 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'); } 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'); } return [ this.contractSecret, input.tapScriptSig[0].signature, input.tapScriptSig[1].signature, ]; } getScriptTree() { if (!this.calldata) { throw new Error('Calldata is required'); } this.generateRedeemScripts(); return [ { output: this.compiledTargetScript, version: 192, }, { output: SharedInteractionTransaction.LOCK_LEAF_SCRIPT, version: 192, }, ]; } async signInputsWalletBased(transaction) { const signer = this.signer; await this.signInput(transaction, transaction.data.inputs[0], 0, this.scriptSigner); await signer.multiSignPsbt([transaction]); for (let i = 0; i < transaction.data.inputs.length; i++) { if (i === 0) { transaction.finalizeInput(i, this.customFinalizer.bind(this)); } else { try { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } catch (e) { transaction.finalizeInput(i); } } } } async signInputsNonWalletBased(transaction) { for (let i = 0; i < transaction.data.inputs.length; i++) { if (i === 0) { await this.signInput(transaction, transaction.data.inputs[i], i, this.scriptSigner); await this.signInput(transaction, transaction.data.inputs[i], i, this.getSignerKey()); transaction.finalizeInput(0, this.customFinalizer.bind(this)); } else { await this.signInput(transaction, transaction.data.inputs[i], i, this.signer); try { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } catch (e) { transaction.finalizeInput(i); } } } } async createMineableRewardOutputs() { if (!this.to) throw new Error('To address is required'); const amountSpent = this.getTransactionOPNetFee(); let amountToCA; if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) { amountToCA = MINIMUM_AMOUNT_CA; } else { amountToCA = amountSpent; } this.addOutput({ value: Number(amountToCA), address: this.to, }); if (amountToCA === MINIMUM_AMOUNT_CA && amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD) { this.addOutput({ value: Number(amountSpent - amountToCA), address: this.rewardChallenge.address, }); } const amount = this.addOptionalOutputsAndGetAmount(); if (!this.disableAutoRefund) { await this.addRefundOutput(amountSpent + amount); } } getPubKeys() { const pubKeys = [Buffer.from(this.signer.publicKey)]; if (this.scriptSigner) { pubKeys.push(Buffer.from(this.scriptSigner.publicKey)); } return pubKeys; } generateRedeemScripts() { this.targetScriptRedeem = { name: PaymentType.P2TR, output: this.compiledTargetScript, redeemVersion: 192, }; this.leftOverFundsScriptRedeem = { name: PaymentType.P2TR, output: SharedInteractionTransaction.LOCK_LEAF_SCRIPT, redeemVersion: 192, }; } } SharedInteractionTransaction.MAXIMUM_CALLDATA_SIZE = 1024 * 1024;