UNPKG

opnet

Version:

The perfect library for building Bitcoin-based applications.

367 lines (366 loc) 15.8 kB
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')); } }