UNPKG

@btc-vision/transaction

Version:

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

259 lines (258 loc) 9.9 kB
import { TransactionType } from '../enums/TransactionType.js'; import { crypto as bitCrypto, PaymentType, toXOnly, } from '@btc-vision/bitcoin'; import { MINIMUM_AMOUNT_CA, MINIMUM_AMOUNT_REWARD, TransactionBuilder, } from './TransactionBuilder.js'; import { DeploymentGenerator, versionBuffer, } from '../../generators/builders/DeploymentGenerator.js'; import { EcKeyPair } from '../../keypair/EcKeyPair.js'; import { BitcoinUtils } from '../../utils/BitcoinUtils.js'; import { Compressor } from '../../bytecode/Compressor.js'; import { SharedInteractionTransaction } from './SharedInteractionTransaction.js'; import { Address } from '../../keypair/Address.js'; import { ChallengeGenerator } from '../mineable/ChallengeGenerator.js'; export class DeploymentTransaction extends TransactionBuilder { constructor(parameters) { super(parameters); this.type = TransactionType.DEPLOYMENT; this.tapLeafScript = null; this.deploymentVersion = 0x00; this.targetScriptRedeem = null; this.leftOverFundsScriptRedeem = null; this.customFinalizer = (_inputIndex, input) => { if (!this.tapLeafScript) { throw new Error('Tap leaf script is required'); } if (!input.tapScriptSig) { throw new Error('Tap script signature is required'); } const scriptSolution = [ this.randomBytes, input.tapScriptSig[0].signature, input.tapScriptSig[1].signature, ]; const witness = scriptSolution .concat(this.tapLeafScript.script) .concat(this.tapLeafScript.controlBlock); return { finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witness), }; }; this.bytecode = Compressor.compress(Buffer.concat([versionBuffer, parameters.bytecode])); this.verifyBytecode(); if (parameters.calldata) { this.calldata = parameters.calldata; this.verifyCalldata(); } if (!parameters.preimage) throw new Error('Preimage is required'); this.randomBytes = parameters.randomBytes || BitcoinUtils.rndBytes(); this.preimage = parameters.preimage; this.rewardChallenge = ChallengeGenerator.generateMineableReward(this.preimage, this.network); this.contractSeed = this.getContractSeed(); this.contractSigner = EcKeyPair.fromSeedKeyPair(this.contractSeed, this.network); this.deploymentGenerator = new DeploymentGenerator(Buffer.from(this.signer.publicKey), this.contractSignerXOnlyPubKey(), this.network); this.compiledTargetScript = this.deploymentGenerator.compile(this.bytecode, this.randomBytes, this.preimage, this.priorityFee, this.calldata); this.scriptTree = this.getScriptTree(); this.internalInit(); this._contractPubKey = '0x' + this.contractSeed.toString('hex'); this._contractAddress = new Address(this.contractSeed); } get contractPubKey() { return this._contractPubKey; } get contractAddress() { return this._contractAddress; } get p2trAddress() { return this.to || this.getScriptAddress(); } getRndBytes() { return this.randomBytes; } getPreimage() { return this.preimage; } getContractAddress() { if (this._computedAddress) { return this._computedAddress; } this._computedAddress = EcKeyPair.p2op(this.contractSeed, this.network, this.deploymentVersion); return this._computedAddress; } contractSignerXOnlyPubKey() { return toXOnly(Buffer.from(this.contractSigner.publicKey)); } 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(); let amountToCA; if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) { amountToCA = MINIMUM_AMOUNT_CA; } else { amountToCA = amountSpent; } this.addOutput({ value: Number(amountToCA), address: this.getContractAddress(), }); if (amountToCA === MINIMUM_AMOUNT_CA && amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD) { this.addOutput({ value: Number(amountSpent - amountToCA), address: this.rewardChallenge.address, }); } await this.addRefundOutput(amountSpent + this.addOptionalOutputsAndGetAmount()); } async signInputsWalletBased(transaction) { const signer = this.signer; await this.signInput(transaction, transaction.data.inputs[0], 0, this.contractSigner); 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 signInputs(transaction) { if (!this.contractSigner) { await super.signInputs(transaction); return; } if ('multiSignPsbt' in this.signer) { await this.signInputsWalletBased(transaction); return; } for (let i = 0; i < transaction.data.inputs.length; i++) { if (i === 0) { transaction.signInput(0, this.contractSigner); transaction.signInput(0, this.getSignerKey()); transaction.finalizeInput(0, this.customFinalizer.bind(this)); } else { transaction.signInput(i, this.getSignerKey()); try { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } catch (e) { transaction.finalizeInput(i); } } } } generateScriptAddress() { return { name: PaymentType.P2TR, internalPubkey: this.internalPubKeyToXOnly(), network: this.network, scriptTree: this.scriptTree, }; } 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'); } return { name: PaymentType.P2TR, internalPubkey: this.internalPubKeyToXOnly(), network: this.network, scriptTree: this.scriptTree, redeem: selectedRedeem, }; } verifyCalldata() { if (this.calldata && this.calldata.length > SharedInteractionTransaction.MAXIMUM_CALLDATA_SIZE) { throw new Error('Calldata size overflow.'); } } verifyBytecode() { if (!this.bytecode) throw new Error('Bytecode is required'); if (this.bytecode.length > DeploymentTransaction.MAXIMUM_CONTRACT_SIZE) { throw new Error('Contract size overflow.'); } } getContractSeed() { if (!this.bytecode) { throw new Error('Bytecode is required'); } const deployerPubKey = this.internalPubKeyToXOnly(); const salt = bitCrypto.hash256(this.randomBytes); const sha256OfBytecode = bitCrypto.hash256(this.bytecode); const buf = Buffer.concat([deployerPubKey, salt, sha256OfBytecode]); return bitCrypto.hash256(buf); } getPubKeys() { const pubkeys = [Buffer.from(this.signer.publicKey)]; if (this.contractSigner) { pubkeys.push(Buffer.from(this.contractSigner.publicKey)); } return pubkeys; } generateRedeemScripts() { this.targetScriptRedeem = { name: PaymentType.P2TR, output: this.compiledTargetScript, redeemVersion: 192, }; this.leftOverFundsScriptRedeem = { name: PaymentType.P2TR, output: this.getLeafScript(), redeemVersion: 192, }; } getLeafScript() { return TransactionBuilder.LOCK_LEAF_SCRIPT; } getScriptTree() { if (!this.bytecode) { throw new Error('Contract bytecode is required'); } this.generateRedeemScripts(); return [ { output: this.compiledTargetScript, version: 192, }, { output: this.getLeafScript(), version: 192, }, ]; } } DeploymentTransaction.MAXIMUM_CONTRACT_SIZE = 128 * 1024;