UNPKG

@btc-vision/transaction

Version:

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

280 lines 11.5 kB
import { concat, fromHex, Psbt, toXOnly } from '@btc-vision/bitcoin'; import { TransactionType } from '../enums/TransactionType.js'; import { TransactionBuilder } from './TransactionBuilder.js'; import { MessageSigner } from '../../keypair/MessageSigner.js'; import { Compressor } from '../../bytecode/Compressor.js'; import { P2WDAGenerator } from '../../generators/builders/P2WDAGenerator.js'; import { FeaturePriority, Features } from '../../generators/Features.js'; import { BitcoinUtils } from '../../utils/BitcoinUtils.js'; import { EcKeyPair } from '../../keypair/EcKeyPair.js'; import {} from '@btc-vision/ecpair'; import { P2WDADetector } from '../../p2wda/P2WDADetector.js'; import { TimeLockGenerator } from '../mineable/TimelockGenerator.js'; /** * P2WDA Interaction Transaction * * This transaction type uses the exact same operation data as regular interactions * (via CalldataGenerator), but embeds it in the witness field instead of a taproot script. * This achieves 75% cost reduction through the witness discount. */ export class InteractionTransactionP2WDA extends TransactionBuilder { static MAX_WITNESS_FIELDS = 10; static MAX_BYTES_PER_WITNESS = 80; type = TransactionType.INTERACTION; epochChallenge; /** * Disable auto refund * @protected */ disableAutoRefund; contractSecret; calldata; challenge; randomBytes; p2wdaGenerator; scriptSigner; p2wdaInputIndices = new Set(); /** * The compiled operation data from CalldataGenerator * This is exactly what would go in a taproot script, but we put it in witness instead */ compiledOperationData = null; constructor(parameters) { super(parameters); if (!parameters.to) { throw new Error('Contract address (to) is required'); } if (!parameters.contract) { throw new Error('Contract secret is required'); } if (!parameters.calldata) { throw new Error('Calldata is required'); } if (!parameters.challenge) { throw new Error('Challenge solution is required'); } this.disableAutoRefund = parameters.disableAutoRefund || false; this.contractSecret = fromHex(parameters.contract.startsWith('0x') ? parameters.contract.slice(2) : parameters.contract); this.calldata = Compressor.compress(parameters.calldata); this.challenge = parameters.challenge; this.randomBytes = parameters.randomBytes || BitcoinUtils.rndBytes(); // Create the script signer (same as SharedInteractionTransaction does) this.scriptSigner = this.generateKeyPairFromSeed(); // Create the P2WDA generator instead of CalldataGenerator // P2WDA needs a different data format optimized for witness embedding this.p2wdaGenerator = new P2WDAGenerator(this.signer.publicKey, this.scriptSignerXOnlyPubKey(), this.network); // Validate contract secret if (this.contractSecret.length !== 32) { throw new Error('Invalid contract secret length. Expected 32 bytes.'); } this.epochChallenge = TimeLockGenerator.generateTimeLockAddress(this.challenge.publicKey.originalPublicKeyBuffer(), this.network); // Validate P2WDA inputs this.validateP2WDAInputs(); if (parameters.compiledTargetScript) { if (parameters.compiledTargetScript instanceof Uint8Array) { this.compiledOperationData = parameters.compiledTargetScript; } else if (typeof parameters.compiledTargetScript === 'string') { this.compiledOperationData = fromHex(parameters.compiledTargetScript); } else { throw new Error('Invalid compiled target script format.'); } } else { this.compiledOperationData = this.p2wdaGenerator.compile(this.calldata, this.contractSecret, this.challenge, this.priorityFee, this.generateFeatures(parameters)); } // Validate size early this.validateOperationDataSize(); this.internalInit(); } /** * Get random bytes (for compatibility if needed elsewhere) */ getRndBytes() { return this.randomBytes; } /** * Get the challenge (for compatibility if needed elsewhere) */ getChallenge() { return this.challenge; } /** * Get contract secret (for compatibility if needed elsewhere) */ getContractSecret() { return this.contractSecret; } /** * Build the transaction */ async buildTransaction() { if (!this.regenerated) { this.addInputsFromUTXO(); } // Add refund await this.createMineableRewardOutputs(); } async createMineableRewardOutputs() { if (!this.to) throw new Error('To address is required'); const amountSpent = this.getTransactionOPNetFee(); this.addFeeToOutput(amountSpent, this.to, this.epochChallenge, false); const amount = this.addOptionalOutputsAndGetAmount(); if (!this.disableAutoRefund) { await this.addRefundOutput(amountSpent + amount); } } /** * Sign inputs with P2WDA-specific handling */ async signInputs(transaction) { // Sign all inputs for (let i = 0; i < transaction.data.inputs.length; i++) { await this.signInput(transaction, transaction.data.inputs[i], i, this.signer); } // Finalize with appropriate finalizers for (let i = 0; i < transaction.data.inputs.length; i++) { if (this.p2wdaInputIndices.has(i)) { if (i === 0) { transaction.finalizeInput(i, this.finalizePrimaryP2WDA.bind(this)); } else { transaction.finalizeInput(i, this.finalizeSecondaryP2WDA.bind(this)); } } else { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } } this.finalized = true; } /** * Generate features array (same as InteractionTransaction) */ generateFeatures(parameters) { const features = []; if (parameters.loadedStorage) { features.push({ priority: FeaturePriority.ACCESS_LIST, opcode: Features.ACCESS_LIST, data: parameters.loadedStorage, }); } const submission = parameters.challenge.getSubmission(); if (submission) { features.push({ priority: FeaturePriority.EPOCH_SUBMISSION, opcode: Features.EPOCH_SUBMISSION, data: submission, }); } return features; } /** * Generate keypair from seed (same as SharedInteractionTransaction) */ generateKeyPairFromSeed() { return EcKeyPair.fromSeedKeyPair(this.randomBytes, this.network); } /** * Get script signer x-only pubkey (same as SharedInteractionTransaction) */ scriptSignerXOnlyPubKey() { return toXOnly(this.scriptSigner.publicKey); } /** * Validate that input 0 is P2WDA */ validateP2WDAInputs() { if (this.utxos.length === 0 || !P2WDADetector.isP2WDAUTXO(this.utxos[0])) { throw new Error('Input 0 must be a P2WDA UTXO'); } // Track all P2WDA inputs for (let i = 0; i < this.utxos.length; i++) { if (P2WDADetector.isP2WDAUTXO(this.utxos[i])) { this.p2wdaInputIndices.add(i); } } for (let i = 0; i < this.optionalInputs.length; i++) { const actualIndex = this.utxos.length + i; if (P2WDADetector.isP2WDAUTXO(this.optionalInputs[i])) { this.p2wdaInputIndices.add(actualIndex); } } } /** * Validate the compiled operation data will fit in witness fields */ validateOperationDataSize() { if (!this.compiledOperationData) { throw new Error('Operation data not compiled'); } // The data that goes in witness: COMPRESS(signature + compiledOperationData) // Signature is 64 bytes const estimatedSize = this.compiledOperationData.length; if (!P2WDAGenerator.validateWitnessSize(estimatedSize)) { const signatureSize = 64; const totalSize = estimatedSize + signatureSize; const compressedEstimate = Math.ceil(totalSize * 0.7); const requiredFields = Math.ceil(compressedEstimate / InteractionTransactionP2WDA.MAX_BYTES_PER_WITNESS); throw new Error(`Please dont use P2WDA for this operation. Data too large. Raw size: ${estimatedSize} bytes, ` + `estimated compressed: ${compressedEstimate} bytes, ` + `needs ${requiredFields} witness fields, max is ${InteractionTransactionP2WDA.MAX_WITNESS_FIELDS}`); } } /** * Finalize primary P2WDA input with the operation data * This is where we create the signature and compress everything */ finalizePrimaryP2WDA(inputIndex, input) { if (!input.partialSig || input.partialSig.length === 0) { throw new Error(`No signature for P2WDA input #${inputIndex}`); } if (!input.witnessScript) { throw new Error(`No witness script for P2WDA input #${inputIndex}`); } if (!this.compiledOperationData) { throw new Error('Operation data not compiled'); } const txSignature = input.partialSig[0].signature; const messageToSign = concat([txSignature, this.compiledOperationData]); const signedMessage = MessageSigner.signMessage(this.signer, messageToSign); const schnorrSignature = signedMessage.signature; // Combine and compress: COMPRESS(signature + compiledOperationData) const fullData = concat([schnorrSignature, this.compiledOperationData]); const compressedData = Compressor.compress(fullData); // Split into chunks const chunks = this.splitIntoWitnessChunks(compressedData); if (chunks.length > InteractionTransactionP2WDA.MAX_WITNESS_FIELDS) { throw new Error(`Compressed data needs ${chunks.length} witness fields, max is ${InteractionTransactionP2WDA.MAX_WITNESS_FIELDS}`); } // Build witness stack const witnessStack = [txSignature]; // Add exactly 10 data fields // Bitcoin stack is reversed! for (let i = 0; i < InteractionTransactionP2WDA.MAX_WITNESS_FIELDS; i++) { witnessStack.push(i < chunks.length ? chunks[i] : new Uint8Array(0)); } witnessStack.push(input.witnessScript); return { finalScriptSig: undefined, finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witnessStack), }; } /** * Split data into 80-byte chunks */ splitIntoWitnessChunks(data) { const chunks = []; let offset = 0; while (offset < data.length) { const size = Math.min(InteractionTransactionP2WDA.MAX_BYTES_PER_WITNESS, data.length - offset); chunks.push(data.slice(offset, offset + size)); offset += size; } return chunks; } } //# sourceMappingURL=InteractionTransactionP2WDA.js.map