UNPKG

opnet

Version:

The perfect library for building Bitcoin-based applications.

290 lines (289 loc) 11.3 kB
import { Address, BinaryReader, BufferHelper, NetEvent, TransactionFactory, } from '@btc-vision/transaction'; 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; #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) { if (this.startsWithErrorSelector(revertDataBytes)) { const decoder = new TextDecoder(); return decoder.decode(revertDataBytes.subarray(8)); } else { return `Unknown Revert: 0x${this.bytesToHexString(revertDataBytes)}`; } } static startsWithErrorSelector(revertDataBytes) { const errorSelectorBytes = Uint8Array.from([0x63, 0x73, 0x9d, 0x5c]); return (revertDataBytes.length >= 4 && this.areBytesEqual(revertDataBytes.subarray(0, 4), errorSelectorBytes)); } static areBytesEqual(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { return false; } } return true; } static bytesToHexString(byteArray) { return Array.from(byteArray, function (byte) { return ('0' + (byte & 0xff).toString(16)).slice(-2); }).join(''); } setTo(to, address) { this.to = to; this.address = address; } async sendTransaction(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}`); } try { 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'); } let totalPointers = 0; if (this.loadedStorage) { for (const obj in this.loadedStorage) { totalPointers += obj.length; } } const storage = interactionParams.dontIncludeAccessList === false ? totalPointers > 100 ? this.loadedStorage : undefined : undefined; 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, loadedStorage: storage, note: interactionParams.note, anchor: interactionParams.anchor || false, txVersion: interactionParams.txVersion || 2, }; const transaction = await factory.signInteraction(params); if (!interactionParams.p2wda) { if (!transaction.fundingTransaction) { throw new Error('Funding transaction not created'); } const tx1 = await this.#provider.sendRawTransaction(transaction.fundingTransaction, false); if (!tx1 || tx1.error) { throw new Error(`Error sending transaction: ${tx1?.error || 'Unknown error'}`); } } const tx2 = await this.#provider.sendRawTransaction(transaction.interactionTransaction, 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'); } this.#provider.utxoManager.spentUTXO(interactionParams.refundTo, UTXOs, transaction.nextUTXOs); return { transactionId: tx2.result, peerAcknowledgements: tx2.peers || 0, newUTXOs: transaction.nextUTXOs, estimatedFees: transaction.estimatedFees, challengeSolution: transaction.challenge, }; } 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; } 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 + BigInt(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 + BigInt(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.refundTo) { throw new Error('Refund address not set'); } const utxoSetting = { address: interactionParams.refundTo, amount: amount, throwErrors: true, }; const utxos = await this.#provider.utxoManager.getUTXOsForAmount(utxoSetting); if (!utxos) { throw new Error('No UTXOs found'); } if (!interactionParams.signer) { throw new Error('Signer not set in interaction parameters'); } 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 (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')); } }