@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
206 lines (205 loc) • 9.03 kB
JavaScript
import { Buffer } from 'buffer';
import { toXOnly } from '@btc-vision/bitcoin';
import { TransactionType } from '../enums/TransactionType.js';
import { MINIMUM_AMOUNT_CA, MINIMUM_AMOUNT_REWARD, TransactionBuilder, } from './TransactionBuilder.js';
import { MessageSigner } from '../../keypair/MessageSigner.js';
import { Compressor } from '../../bytecode/Compressor.js';
import { P2WDAGenerator } from '../../generators/builders/P2WDAGenerator.js';
import { Features } from '../../generators/Features.js';
import { BitcoinUtils } from '../../utils/BitcoinUtils.js';
import { EcKeyPair } from '../../keypair/EcKeyPair.js';
import { P2WDADetector } from '../../p2wda/P2WDADetector.js';
import { TimeLockGenerator } from '../mineable/TimelockGenerator.js';
export class InteractionTransactionP2WDA extends TransactionBuilder {
constructor(parameters) {
super(parameters);
this.type = TransactionType.INTERACTION;
this.p2wdaInputIndices = new Set();
this.compiledOperationData = null;
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.contractAddress = parameters.to;
this.contractSecret = Buffer.from(parameters.contract.replace('0x', ''), 'hex');
this.calldata = Compressor.compress(parameters.calldata);
this.challenge = parameters.challenge;
this.randomBytes = parameters.randomBytes || BitcoinUtils.rndBytes();
this.scriptSigner = this.generateKeyPairFromSeed();
this.p2wdaGenerator = new P2WDAGenerator(Buffer.from(this.signer.publicKey), this.scriptSignerXOnlyPubKey(), this.network);
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);
this.validateP2WDAInputs();
this.compiledOperationData = this.p2wdaGenerator.compile(this.calldata, this.contractSecret, this.challenge, this.priorityFee, this.generateFeatures(parameters));
this.validateOperationDataSize();
this.internalInit();
}
getRndBytes() {
return this.randomBytes;
}
getChallenge() {
return this.challenge;
}
getContractSecret() {
return this.contractSecret;
}
async buildTransaction() {
if (!this.regenerated) {
this.addInputsFromUTXO();
}
await this.createMineableRewardOutputs();
}
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.epochChallenge.address,
});
}
const amount = this.addOptionalOutputsAndGetAmount();
if (!this.disableAutoRefund) {
await this.addRefundOutput(amountSpent + amount);
}
}
async signInputs(transaction) {
for (let i = 0; i < transaction.data.inputs.length; i++) {
await this.signInput(transaction, transaction.data.inputs[i], i, this.signer);
}
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;
}
generateFeatures(parameters) {
const features = [];
if (parameters.loadedStorage) {
features.push({
opcode: Features.ACCESS_LIST,
data: parameters.loadedStorage,
});
}
const submission = parameters.challenge.getSubmission();
if (submission) {
features.push({
opcode: Features.EPOCH_SUBMISSION,
data: submission,
});
}
return features;
}
generateKeyPairFromSeed() {
return EcKeyPair.fromSeedKeyPair(this.randomBytes, this.network);
}
scriptSignerXOnlyPubKey() {
return toXOnly(Buffer.from(this.scriptSigner.publicKey));
}
validateP2WDAInputs() {
if (this.utxos.length === 0 || !P2WDADetector.isP2WDAUTXO(this.utxos[0])) {
throw new Error('Input 0 must be a P2WDA UTXO');
}
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);
}
}
}
validateOperationDataSize() {
if (!this.compiledOperationData) {
throw new Error('Operation data not compiled');
}
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}`);
}
}
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 = Buffer.concat([txSignature, this.compiledOperationData]);
const signedMessage = MessageSigner.signMessage(this.signer, messageToSign);
const schnorrSignature = Buffer.from(signedMessage.signature);
const fullData = Buffer.concat([schnorrSignature, this.compiledOperationData]);
const compressedData = Compressor.compress(fullData);
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}`);
}
const witnessStack = [txSignature];
for (let i = 0; i < InteractionTransactionP2WDA.MAX_WITNESS_FIELDS; i++) {
witnessStack.push(i < chunks.length ? chunks[i] : Buffer.alloc(0));
}
witnessStack.push(input.witnessScript);
return {
finalScriptSig: undefined,
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witnessStack),
};
}
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.subarray(offset, offset + size));
offset += size;
}
return chunks;
}
}
InteractionTransactionP2WDA.MAX_WITNESS_FIELDS = 10;
InteractionTransactionP2WDA.MAX_BYTES_PER_WITNESS = 80;