@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
280 lines • 11.5 kB
JavaScript
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