opnet
Version:
The perfect library for building Bitcoin-based applications.
367 lines (366 loc) • 15.8 kB
JavaScript
import { Address, BinaryReader, BufferHelper, NetEvent, TransactionFactory, } from '@btc-vision/transaction';
import { decodeRevertData } from '../utils/RevertDecoder.js';
import { TransactionHelper } from './TransactionHelpper.js';
const factory = new TransactionFactory();
export class CallResult {
result;
accessList;
revert;
calldata;
loadedStorage;
estimatedGas;
refundedGas;
properties = {};
estimatedSatGas = 0n;
estimatedRefundedGasInSat = 0n;
events = [];
to;
address;
fromAddress;
csvAddress;
#bitcoinFees;
#rawEvents;
#provider;
constructor(callResult, provider) {
this.#provider = provider;
this.#rawEvents = this.parseEvents(callResult.events);
this.accessList = callResult.accessList;
this.loadedStorage = callResult.loadedStorage;
if (callResult.estimatedGas) {
this.estimatedGas = BigInt(callResult.estimatedGas);
}
if (callResult.specialGas) {
this.refundedGas = BigInt(callResult.specialGas);
}
const revert = typeof callResult.revert === 'string'
? this.base64ToUint8Array(callResult.revert)
: callResult.revert;
if (revert) {
this.revert = CallResult.decodeRevertData(revert);
}
this.result =
typeof callResult.result === 'string'
? new BinaryReader(this.base64ToUint8Array(callResult.result))
: callResult.result;
}
get rawEvents() {
return this.#rawEvents;
}
static decodeRevertData(revertDataBytes) {
return decodeRevertData(revertDataBytes);
}
setTo(to, address) {
this.to = to;
this.address = address;
}
setFromAddress(from) {
this.fromAddress = from;
this.csvAddress =
this.fromAddress && this.fromAddress.originalPublicKey
? this.#provider.getCSV1ForAddress(this.fromAddress)
: undefined;
}
async signTransaction(interactionParams, amountAddition = 0n) {
if (!this.address) {
throw new Error('Contract address not set');
}
if (!this.calldata) {
throw new Error('Calldata not set');
}
if (!this.to) {
throw new Error('To address not set');
}
if (this.revert) {
throw new Error(`Can not send transaction! Simulation reverted: ${this.revert}`);
}
let UTXOs = interactionParams.utxos || (await this.acquire(interactionParams, amountAddition));
if (interactionParams.extraInputs) {
UTXOs = UTXOs.filter((utxo) => {
return (interactionParams.extraInputs?.find((input) => {
return (input.outputIndex === utxo.outputIndex &&
input.transactionId === utxo.transactionId);
}) === undefined);
});
}
if (!UTXOs || UTXOs.length === 0) {
throw new Error('No UTXOs found');
}
const priorityFee = interactionParams.priorityFee || 0n;
const challenge = await this.#provider.getChallenge();
const params = {
contract: this.address.toHex(),
calldata: this.calldata,
priorityFee: priorityFee,
gasSatFee: this.bigintMax(this.estimatedSatGas, interactionParams.minGas || 0n),
feeRate: interactionParams.feeRate || this.#bitcoinFees?.conservative || 10,
from: interactionParams.refundTo,
utxos: UTXOs,
to: this.to,
network: interactionParams.network,
optionalInputs: interactionParams.extraInputs || [],
optionalOutputs: interactionParams.extraOutputs || [],
signer: interactionParams.signer,
challenge: challenge,
note: interactionParams.note,
anchor: interactionParams.anchor || false,
txVersion: interactionParams.txVersion || 2,
mldsaSigner: interactionParams.mldsaSigner,
linkMLDSAPublicKeyToAddress: interactionParams.linkMLDSAPublicKeyToAddress ?? true,
revealMLDSAPublicKey: interactionParams.revealMLDSAPublicKey ?? false,
};
const transaction = await factory.signInteraction(params);
const csvUTXOs = UTXOs.filter((u) => u.isCSV === true);
const p2wdaUTXOs = UTXOs.filter((u) => u.witnessScript && u.isCSV !== true);
const regularUTXOs = UTXOs.filter((u) => !u.witnessScript && u.isCSV !== true);
const refundAddress = interactionParams.sender || interactionParams.refundTo;
const p2wdaAddress = interactionParams.from?.p2wda(this.#provider.network);
let refundToAddress;
if (this.csvAddress && refundAddress === this.csvAddress.address) {
refundToAddress = this.csvAddress.address;
}
else if (p2wdaAddress && refundAddress === p2wdaAddress.address) {
refundToAddress = p2wdaAddress.address;
}
else {
refundToAddress = refundAddress;
}
const utxoTracking = {
csvUTXOs,
p2wdaUTXOs,
regularUTXOs,
refundAddress,
refundToAddress,
csvAddress: this.csvAddress,
p2wdaAddress: p2wdaAddress
? { address: p2wdaAddress.address, witnessScript: p2wdaAddress.witnessScript }
: undefined,
isP2WDA: interactionParams.p2wda || false,
};
return {
fundingTransactionRaw: transaction.fundingTransaction,
interactionTransactionRaw: transaction.interactionTransaction,
nextUTXOs: transaction.nextUTXOs,
estimatedFees: transaction.estimatedFees,
challengeSolution: transaction.challenge,
interactionAddress: transaction.interactionAddress,
fundingUTXOs: transaction.fundingUTXOs,
fundingInputUtxos: transaction.fundingInputUtxos,
compiledTargetScript: transaction.compiledTargetScript,
utxoTracking,
};
}
async sendPresignedTransaction(signedTx) {
if (!signedTx.utxoTracking.isP2WDA) {
if (!signedTx.fundingTransactionRaw) {
throw new Error('Funding transaction not created');
}
const tx1 = await this.#provider.sendRawTransaction(signedTx.fundingTransactionRaw, false);
if (!tx1 || tx1.error) {
throw new Error(`Error sending transaction: ${tx1?.error || 'Unknown error'}`);
}
if (!tx1.success) {
throw new Error(`Error sending transaction: ${tx1.result || 'Unknown error'}`);
}
}
const tx2 = await this.#provider.sendRawTransaction(signedTx.interactionTransactionRaw, false);
if (!tx2 || tx2.error) {
throw new Error(`Error sending transaction: ${tx2?.error || 'Unknown error'}`);
}
if (!tx2.result) {
throw new Error('No transaction ID returned');
}
if (!tx2.success) {
throw new Error(`Error sending transaction: ${tx2.result || 'Unknown error'}`);
}
this.#processUTXOTracking(signedTx);
return {
interactionAddress: signedTx.interactionAddress,
transactionId: tx2.result,
peerAcknowledgements: tx2.peers || 0,
newUTXOs: signedTx.nextUTXOs,
estimatedFees: signedTx.estimatedFees,
challengeSolution: signedTx.challengeSolution,
rawTransaction: signedTx.interactionTransactionRaw,
fundingUTXOs: signedTx.fundingUTXOs,
fundingInputUtxos: signedTx.fundingInputUtxos,
compiledTargetScript: signedTx.compiledTargetScript,
};
}
async sendTransaction(interactionParams, amountAddition = 0n) {
try {
const signedTx = await this.signTransaction(interactionParams, amountAddition);
return await this.sendPresignedTransaction(signedTx);
}
catch (e) {
const msgStr = e.message;
if (msgStr.includes('Insufficient funds to pay the fees') && amountAddition === 0n) {
return await this.sendTransaction(interactionParams, 200000n);
}
this.#provider.utxoManager.clean();
throw e;
}
}
setGasEstimation(estimatedGas, refundedGas) {
this.estimatedSatGas = estimatedGas;
this.estimatedRefundedGasInSat = refundedGas;
}
setBitcoinFee(fees) {
this.#bitcoinFees = fees;
}
setDecoded(decoded) {
this.properties = Object.freeze(decoded.obj);
}
setEvents(events) {
this.events = events;
}
setCalldata(calldata) {
this.calldata = calldata;
}
#cloneUTXOWithWitnessScript(utxo, witnessScript) {
const clone = Object.assign(Object.create(Object.getPrototypeOf(utxo)), utxo);
clone.witnessScript = witnessScript;
return clone;
}
#processUTXOTracking(signedTx) {
const { csvUTXOs, p2wdaUTXOs, regularUTXOs, refundAddress, refundToAddress, csvAddress, p2wdaAddress, } = signedTx.utxoTracking;
if (csvAddress && csvUTXOs.length) {
const finalUTXOs = signedTx.nextUTXOs.map((u) => this.#cloneUTXOWithWitnessScript(u, csvAddress.witnessScript));
this.#provider.utxoManager.spentUTXO(csvAddress.address, csvUTXOs, refundToAddress === csvAddress.address ? finalUTXOs : []);
}
if (p2wdaAddress && p2wdaUTXOs.length) {
const finalUTXOs = signedTx.nextUTXOs.map((u) => this.#cloneUTXOWithWitnessScript(u, p2wdaAddress.witnessScript));
this.#provider.utxoManager.spentUTXO(p2wdaAddress.address, p2wdaUTXOs, refundToAddress === p2wdaAddress.address ? finalUTXOs : []);
}
if (regularUTXOs.length) {
this.#provider.utxoManager.spentUTXO(refundAddress, regularUTXOs, refundToAddress === refundAddress ? signedTx.nextUTXOs : []);
}
if (csvAddress && refundToAddress === csvAddress.address && !csvUTXOs.length) {
const finalUTXOs = signedTx.nextUTXOs.map((u) => this.#cloneUTXOWithWitnessScript(u, csvAddress.witnessScript));
this.#provider.utxoManager.spentUTXO(csvAddress.address, [], finalUTXOs);
}
else if (p2wdaAddress && refundToAddress === p2wdaAddress.address && !p2wdaUTXOs.length) {
const finalUTXOs = signedTx.nextUTXOs.map((u) => this.#cloneUTXOWithWitnessScript(u, p2wdaAddress.witnessScript));
this.#provider.utxoManager.spentUTXO(p2wdaAddress.address, [], finalUTXOs);
}
else if (refundToAddress === refundAddress && !regularUTXOs.length) {
const isSpecialAddress = (csvAddress && refundToAddress === csvAddress.address) ||
(p2wdaAddress && refundToAddress === p2wdaAddress.address);
if (!isSpecialAddress) {
this.#provider.utxoManager.spentUTXO(refundAddress, [], signedTx.nextUTXOs);
}
}
}
async acquire(interactionParams, amountAddition = 0n) {
if (!this.calldata) {
throw new Error('Calldata not set');
}
if (!interactionParams.feeRate) {
interactionParams.feeRate = 1.5;
}
const feeRate = interactionParams.feeRate;
const priority = interactionParams.priorityFee ?? 0n;
const addedOuts = interactionParams.extraOutputs ?? [];
const totalOuts = BigInt(addedOuts.reduce((s, o) => s + o.value, 0));
const gasFee = this.bigintMax(this.estimatedSatGas, interactionParams.minGas ?? 0n);
const preWant = gasFee +
priority +
amountAddition +
totalOuts +
interactionParams.maximumAllowedSatToSpend;
let utxos = interactionParams.utxos ?? (await this.#fetchUTXOs(preWant, interactionParams));
let refetched = false;
while (true) {
const miningCost = TransactionHelper.estimateMiningCost(utxos, addedOuts, this.calldata.length + 200, interactionParams.network, feeRate);
const want = gasFee +
priority +
amountAddition +
totalOuts +
miningCost +
interactionParams.maximumAllowedSatToSpend;
const have = utxos.reduce((s, u) => s + u.value, 0n);
if (have >= want)
break;
if (refetched) {
throw new Error('Not enough sat to complete transaction');
}
utxos = await this.#fetchUTXOs(want, interactionParams);
refetched = true;
const haveAfter = utxos.reduce((s, u) => s + u.value, 0n);
if (haveAfter === have) {
throw new Error('Not enough sat to complete transaction');
}
}
return utxos;
}
bigintMax(a, b) {
return a > b ? a : b;
}
async #fetchUTXOs(amount, interactionParams) {
if (!interactionParams.sender && !interactionParams.refundTo) {
throw new Error('Refund address not set');
}
const utxoSetting = {
address: interactionParams.sender || interactionParams.refundTo,
amount: amount,
throwErrors: true,
maxUTXOs: interactionParams.maxUTXOs,
throwIfUTXOsLimitReached: interactionParams.throwIfUTXOsLimitReached,
csvAddress: !interactionParams.p2wda && !interactionParams.dontUseCSVUtxos
? this.csvAddress?.address
: undefined,
};
const utxos = await this.#provider.utxoManager.getUTXOsForAmount(utxoSetting);
if (!utxos) {
throw new Error('No UTXOs found');
}
if (this.csvAddress) {
const csvUtxos = utxos.filter((u) => u.isCSV === true);
if (csvUtxos.length > 0) {
for (const utxo of csvUtxos) {
utxo.witnessScript = this.csvAddress.witnessScript;
}
}
}
if (interactionParams.p2wda) {
if (!interactionParams.from) {
throw new Error('From address not set in interaction parameters');
}
const p2wda = interactionParams.from.p2wda(this.#provider.network);
if (interactionParams.sender
? p2wda.address === interactionParams.sender
: p2wda.address === interactionParams.refundTo) {
utxos.forEach((utxo) => {
utxo.witnessScript = p2wda.witnessScript;
});
}
}
return utxos;
}
getValuesFromAccessList() {
const storage = {};
for (const contract in this.accessList) {
const contractData = this.accessList[contract];
storage[contract] = Object.keys(contractData);
}
return storage;
}
contractToString(contract) {
const addressCa = Address.fromString(contract);
return addressCa.p2op(this.#provider.network);
}
parseEvents(events) {
const eventsList = {};
for (const [contract, value] of Object.entries(events)) {
const events = [];
for (const event of value) {
const eventData = new NetEvent(event.type, Buffer.from(event.data, 'base64'));
events.push(eventData);
}
eventsList[this.contractToString(contract)] = events;
}
return eventsList;
}
base64ToUint8Array(base64) {
return BufferHelper.bufferToUint8Array(Buffer.from(base64, 'base64'));
}
}