UNPKG

opnet

Version:

The perfect library for building Bitcoin-based applications.

1,465 lines (1,434 loc) 260 kB
import { d as decompile, B as Buffer, A as Address, N as NetEvent, a as Buffer$1, b as AddressTypes, c as AddressVerificator, T as TransactionFactory, e as BinaryReader, f as BufferHelper, g as AddressMap, C as ChallengeSolution, p as pLimit, h as process$1, L as Logger, i as Long, j as ABIDataTypes, k as BinaryWriter, l as ABICoder, m as BigNumber } from './vendors.js'; import { p as protobuf } from './protobuf.js'; const version = "1.7.35"; var OPNetTransactionTypes = /* @__PURE__ */ ((OPNetTransactionTypes2) => { OPNetTransactionTypes2["Generic"] = "Generic"; OPNetTransactionTypes2["Deployment"] = "Deployment"; OPNetTransactionTypes2["Interaction"] = "Interaction"; return OPNetTransactionTypes2; })(OPNetTransactionTypes || {}); class TransactionInput { originalTransactionId; outputTransactionIndex; scriptSignature; sequenceId; transactionInWitness = []; constructor(data) { this.originalTransactionId = data.originalTransactionId; this.outputTransactionIndex = data.outputTransactionIndex; this.scriptSignature = data.scriptSignature; this.sequenceId = data.sequenceId; this.transactionInWitness = data.transactionInWitness || []; } } class TransactionOutput { value; index; scriptPubKey; script; constructor(data) { this.value = this.convertValue(data.value); this.index = data.index; this.scriptPubKey = data.scriptPubKey; this.script = decompile(Buffer.from(this.scriptPubKey.hex, "hex")); } convertValue(value) { return BigInt(value); } } class LRUCache { cache; maxSize; constructor(maxSize) { this.cache = /* @__PURE__ */ new Map(); this.maxSize = maxSize; } get(key) { const value = this.cache.get(key); if (value !== void 0) { this.cache.delete(key); this.cache.set(key, value); } return value; } set(key, value) { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey !== void 0) { this.cache.delete(firstKey); } } this.cache.set(key, value); } } const P2OP_CACHE_MAX_SIZE = 5e3; const p2opCache = new LRUCache(P2OP_CACHE_MAX_SIZE); const addressCache = new LRUCache(P2OP_CACHE_MAX_SIZE); const getP2op = (rawAddress, network) => { const cacheKey = `${network.bip32}:${network.pubKeyHash}:${network.bech32}:${rawAddress}`; let cached = p2opCache.get(cacheKey); if (cached === void 0) { let addr = addressCache.get(rawAddress); if (addr === void 0) { addr = Address.fromString(rawAddress); addressCache.set(rawAddress, addr); } cached = addr.p2op(network); p2opCache.set(cacheKey, cached); } return cached; }; const ERROR_SELECTOR_BYTES = Uint8Array.from([99, 115, 157, 92]); function 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; } function startsWithErrorSelector(revertDataBytes) { return revertDataBytes.length >= 4 && areBytesEqual(Buffer.from(revertDataBytes.subarray(0, 4)), ERROR_SELECTOR_BYTES); } function bytesToHexString(byteArray) { return Array.from(byteArray, function(byte) { return ("0" + (byte & 255).toString(16)).slice(-2); }).join(""); } function decodeRevertData(revertDataBytes) { if (startsWithErrorSelector(revertDataBytes)) { const decoder = new TextDecoder(); const buf = Buffer.from(revertDataBytes.subarray(8)); return decoder.decode(buf); } else { return `Unknown Revert: 0x${bytesToHexString(revertDataBytes)}`; } } class TransactionReceipt { /** * @description The receipt of the transaction. */ receipt; /** * @description The receipt proofs of the transaction. */ receiptProofs; /** * @description The events of the transaction. */ events; rawEvents = {}; /** * @description If the transaction was reverted, this field will contain the revert message. */ rawRevert; revert; gasUsed; specialGasUsed; constructor(receipt, network) { this.receipt = receipt.receipt ? Buffer.from(receipt.receipt, "base64") : void 0; this.receiptProofs = receipt.receiptProofs || []; this.events = receipt.events ? this.parseEvents(receipt.events, network) : {}; this.rawRevert = receipt.revert ? Buffer.from(receipt.revert, "base64") : void 0; this.revert = this.rawRevert ? decodeRevertData(this.rawRevert) : void 0; this.gasUsed = BigInt(receipt.gasUsed || "0x00") || 0n; this.specialGasUsed = BigInt(receipt.specialGasUsed || "0x00") || 0n; } /** * @description Parse transaction events. * @param events - The events to parse. * @param network - The network to use. * @private */ parseEvents(events, network) { const parsedEvents = {}; if (!Array.isArray(events)) { for (const [key, value] of Object.entries(events)) { const caP2op = getP2op(key, network); const v = value.map((event) => { return this.decodeEvent(event); }); parsedEvents[caP2op] = v; this.rawEvents[key] = v; } } else { for (const event of events) { const parsedEvent = this.decodeEvent(event); const contractAddress = event.contractAddress; const caP2op = getP2op(contractAddress, network); if (!parsedEvents[caP2op]) { parsedEvents[caP2op] = []; } parsedEvents[caP2op].push(parsedEvent); if (!this.rawEvents[contractAddress]) { this.rawEvents[contractAddress] = []; } this.rawEvents[contractAddress].push(parsedEvent); } } return parsedEvents; } decodeEvent(event) { let eventData; if (typeof event.data === "string") { const buf = Buffer.from(event.data, "base64"); eventData = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); } else { eventData = event.data; } return new NetEvent(event.type, eventData); } } class TransactionBase extends TransactionReceipt { /** * @description The transaction ID (hash). */ id; /** * @description The transaction "hash". */ hash; /** * @description The index of the transaction in the block. */ index; /** * @description Returns the amount of satoshi that were burned in the transaction. */ burnedBitcoin; /** * @description The priority fee of the transaction. */ priorityFee; /** * @description The maximum amount of gas that can be spent by the transaction. */ maxGasSat; /** * @description The inputs of the transaction. */ inputs; /** * @description The outputs of the transaction. */ outputs; /** * @description The type of the transaction. */ OPNetType; /** * @description The amount of gas used by the transaction. */ gasUsed; /** * @description Special gas used by the transaction. */ specialGasUsed; /** * @description The proof of work challenge. */ pow; /** * @description The block number in which the transaction was included. */ blockNumber; constructor(transaction, network) { super( { receipt: transaction.receipt, receiptProofs: transaction.receiptProofs, events: transaction.events, revert: transaction.revert, gasUsed: transaction.gasUsed, specialGasUsed: transaction.specialGasUsed }, network ); this.id = transaction.id; this.hash = transaction.hash; this.index = transaction.index; if (transaction.blockNumber) this.blockNumber = BigInt(transaction.blockNumber); this.burnedBitcoin = BigInt(transaction.burnedBitcoin) || 0n; this.priorityFee = BigInt(transaction.priorityFee) || 0n; this.inputs = transaction.inputs.map((input) => new TransactionInput(input)); this.outputs = transaction.outputs.map( (output) => new TransactionOutput(output) ); this.OPNetType = transaction.OPNetType; this.gasUsed = BigInt(transaction.gasUsed || "0x00") || 0n; this.specialGasUsed = BigInt(transaction.specialGasUsed || "0x00") || 0n; if (transaction.pow) { this.pow = this.decodeProofOfWorkChallenge(transaction.pow); } this.maxGasSat = this.burnedBitcoin + (this.pow?.reward || 0n) - this.priorityFee; } decodeProofOfWorkChallenge(challenge) { return { preimage: Buffer.from(challenge.preimage, "base64"), reward: BigInt(challenge.reward) || 0n, difficulty: BigInt(challenge.difficulty || "0"), version: challenge.version || 0 }; } } class DeploymentTransaction extends TransactionBase { contractAddress; contractPublicKey; bytecode; wasCompressed; deployerPubKey; deployerHashedPublicKey; deployerAddress; contractSeed; contractSaltHash; from; constructor(transaction, network) { super(transaction, network); if (!transaction.deployerAddress && !transaction.revert) { throw new Error("Deployer address is missing"); } try { this.from = new Address( Buffer.from(transaction.from, "base64"), Buffer.from(transaction.fromLegacy, "base64") ); this.contractAddress = transaction.contractAddress; this.contractPublicKey = new Address( Buffer.from(transaction.contractPublicKey, "base64") ); this.bytecode = Buffer.from(transaction.bytecode, "base64"); this.wasCompressed = transaction.wasCompressed; if (transaction.deployerPubKey) { this.deployerPubKey = Buffer.from(transaction.deployerPubKey, "base64"); } if (transaction.deployerAddress) { this.deployerHashedPublicKey = Buffer.from( transaction.deployerAddress.replace("0x", ""), "hex" ); } if (this.deployerHashedPublicKey && this.deployerPubKey) { this.deployerAddress = new Address( this.deployerHashedPublicKey, this.deployerPubKey ); } this.contractSeed = Buffer.from(transaction.contractSeed, "base64"); this.contractSaltHash = Buffer.from(transaction.contractSaltHash, "base64"); } catch { } } } class GenericTransaction extends TransactionBase { constructor(transaction, network) { super(transaction, network); } } class InteractionTransaction extends TransactionBase { /** * @description The calldata of the transaction. */ calldata; /** * @description The sender's public key hash. */ senderPubKeyHash; /** * @description The contract secret. */ contractSecret; /** * @description The interaction public key. */ interactionPubKey; /** * @description Whether the transaction data was compressed. */ wasCompressed; /** * @description The from address of the transaction. (ALWAYS TAPROOT. *This address is generated from the P2TR of the pubkey of the deployer.*) */ from; /** * @description The contract address where the transaction was sent. (AKA "to"). */ contractAddress; /** * @description The contract tweaked public key. */ contractPublicKey; constructor(transaction, network) { super(transaction, network); this.contractPublicKey = new Address( Buffer$1.from(transaction.contractPublicKey, "base64") ); if (transaction.calldata) { this.calldata = Buffer$1.from(transaction.calldata, "base64"); } this.senderPubKeyHash = Buffer$1.from(transaction.senderPubKeyHash, "base64"); this.contractSecret = Buffer$1.from(transaction.contractSecret, "base64"); this.interactionPubKey = Buffer$1.from(transaction.interactionPubKey, "base64"); this.wasCompressed = transaction.wasCompressed || false; this.contractAddress = transaction.contractAddress; try { if (transaction.from) { this.from = new Address( Buffer$1.from(transaction.from, "base64"), Buffer$1.from(transaction.fromLegacy, "base64") ); } } catch { } } } BigInt.prototype.toJSON = function() { return this.toString(); }; class TransactionParser { static parseTransactions(transactions, network) { if (!transactions) { return []; } const transactionArray = []; for (const transaction of transactions) { if (!transaction) throw new Error(`Something went wrong while parsing transactions`); transactionArray.push(this.parseTransaction(transaction, network)); } return transactionArray; } static parseTransaction(transaction, network) { if (!transaction) throw new Error("Transaction is required"); const opnetType = transaction.OPNetType; switch (opnetType) { case OPNetTransactionTypes.Generic: return new GenericTransaction(transaction, network); case OPNetTransactionTypes.Interaction: return new InteractionTransaction(transaction, network); case OPNetTransactionTypes.Deployment: return new DeploymentTransaction(transaction, network); default: throw new Error("Unknown transaction type"); } } } class Block { height; hash; previousBlockHash; previousBlockChecksum; bits; nonce; version; size; txCount; weight; strippedSize; time; medianTime; checksumRoot; merkleRoot; storageRoot; receiptRoot; ema; baseGas; gasUsed; checksumProofs; _rawBlock; _network; constructor(block, network) { if (!block) throw new Error("Invalid block."); this._rawBlock = block; this._network = network; this.height = BigInt(block.height.toString()); this.hash = block.hash; this.previousBlockHash = block.previousBlockHash; this.previousBlockChecksum = block.previousBlockChecksum; this.bits = block.bits; this.nonce = block.nonce; this.version = block.version; this.size = block.size; this.txCount = block.txCount; this.ema = BigInt(block.ema); this.baseGas = BigInt(block.baseGas); this.gasUsed = BigInt(block.gasUsed); this.weight = block.weight; this.strippedSize = block.strippedSize; this.time = block.time; this.medianTime = block.medianTime; this.checksumRoot = block.checksumRoot; this.merkleRoot = block.merkleRoot; this.storageRoot = block.storageRoot; this.receiptRoot = block.receiptRoot; this.checksumProofs = block.checksumProofs; } _transactions; get transactions() { if (!this._transactions) { this._transactions = TransactionParser.parseTransactions( this._rawBlock.transactions, this._network ); } return this._transactions; } _deployments; get deployments() { if (!this._deployments) { this._deployments = this._rawBlock.deployments ? this._rawBlock.deployments.map((address) => Address.fromString(address)) : []; } return this._deployments; } // For cases where you need raw without parsing get rawTransactions() { return this._rawBlock.transactions; } } class BlockGasParameters { blockNumber; gasUsed; targetGasLimit; ema; baseGas; gasPerSat; bitcoin; constructor(data) { this.blockNumber = BigInt(data.blockNumber); this.gasUsed = BigInt(data.gasUsed); this.targetGasLimit = BigInt(data.targetGasLimit); this.ema = BigInt(data.ema); this.baseGas = BigInt(data.baseGas); this.gasPerSat = BigInt(data.gasPerSat); this.bitcoin = { conservative: Number(data.bitcoin.conservative), recommended: { low: Number(data.bitcoin.recommended.low), medium: Number(data.bitcoin.recommended.medium), high: Number(data.bitcoin.recommended.high) } }; } } function stringToBuffer(str) { return Buffer.from(str.replace("0x", ""), "hex"); } function stringBase64ToBuffer(str) { return Buffer.from(str, "base64"); } class BlockWitnessAPI { trusted; signature; timestamp; proofs; identity; publicKey; constructor(data) { this.trusted = data.trusted; this.signature = stringBase64ToBuffer(data.signature); this.timestamp = data.timestamp; this.proofs = Object.freeze(data.proofs.map((proof) => stringBase64ToBuffer(proof))); this.identity = data.identity ? stringBase64ToBuffer(data.identity) : void 0; this.publicKey = data.publicKey ? Address.fromString(data.publicKey) : void 0; } } class BlockWitness { blockNumber; witnesses; constructor(data) { this.blockNumber = typeof data.blockNumber === "string" ? BigInt(data.blockNumber) : data.blockNumber; this.witnesses = Object.freeze( data.witnesses.map((witness) => new BlockWitnessAPI(witness)) ); } } function parseBlockWitnesses(rawWitnesses) { return Object.freeze(rawWitnesses.map((rawWitness) => new BlockWitness(rawWitness))); } class TransactionHelper { static estimateMiningCost(utxos, extraOutputs, opReturnLen, network, feeRate) { const vBytes = this.estimateVBytes(utxos, extraOutputs, opReturnLen, network); return BigInt(Math.ceil(vBytes * feeRate)); } static varIntLen(n) { return n < 253 ? 1 : n <= 65535 ? 3 : n <= 4294967295 ? 5 : 9; } static estimateVBytes(utxos, extraOutputs, scriptLength, network) { const INPUT_WU = { [AddressTypes.P2PKH]: 148 * 4, [AddressTypes.P2SH_OR_P2SH_P2WPKH]: 91 * 4 + 107, [AddressTypes.P2WPKH]: 41 * 4 + 107, [AddressTypes.P2TR]: 41 * 4 + 65, [AddressTypes.P2PK]: 148 * 4, [AddressTypes.P2WSH]: 41 * 4 + (1 + 73 + 1 + 33), [AddressTypes.P2OP]: 41 * 4 + 107, [AddressTypes.P2WDA]: 41 * 4 + 253 }; const OUTPUT_BYTES = { [AddressTypes.P2PKH]: 34, [AddressTypes.P2SH_OR_P2SH_P2WPKH]: 32, [AddressTypes.P2WPKH]: 31, [AddressTypes.P2TR]: 43, [AddressTypes.P2PK]: 34, [AddressTypes.P2OP]: 32, [AddressTypes.P2WSH]: 43, [AddressTypes.P2WDA]: 43 }; const ins = utxos.length ? utxos : new Array(3).fill(null); let weight = 0; weight += 8 * 4; const usesWitness = utxos.length === 0 || utxos.some((u) => { const t = AddressVerificator.detectAddressType( u?.scriptPubKey?.address ?? "", network ); return t === AddressTypes.P2WPKH || t === AddressTypes.P2SH_OR_P2SH_P2WPKH || t === AddressTypes.P2TR || t === AddressTypes.P2OP || t === AddressTypes.P2WSH; }); if (usesWitness) weight += 2 * 4; weight += this.varIntLen(ins.length) * 4; weight += this.varIntLen(extraOutputs.length) * 4; for (const u of ins) { const t = utxos.length === 0 ? AddressTypes.P2TR : AddressVerificator.detectAddressType( u?.scriptPubKey?.address ?? "", network ) ?? AddressTypes.P2PKH; weight += INPUT_WU[t] ?? 110 * 4; } for (const o of extraOutputs) { if ("address" in o) { const t = AddressVerificator.detectAddressType(o.address, network) ?? AddressTypes.P2PKH; weight += (OUTPUT_BYTES[t] ?? 40) * 4; } else if ("script" in o) { const scriptLen = o.script.length; const bytes = 8 + this.varIntLen(scriptLen) + scriptLen; weight += bytes * 4; } else { weight += 34 * 4; } } const witnessBytes = 1 + 3 * (this.varIntLen(32) + 32); weight += witnessBytes; const stackItemScript = this.varIntLen(scriptLength) + scriptLength; const controlBlock = 1 + 33; weight += stackItemScript + controlBlock; return Math.ceil(weight / 4); } } const factory = new TransactionFactory(); 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) : void 0; } /** * Signs a bitcoin interaction transaction from a simulated contract call without broadcasting. * @param {TransactionParameters} interactionParams - The parameters for the transaction. * @param {bigint} amountAddition - Additional satoshis to request when acquiring UTXOs. * @returns {Promise<SignedInteractionTransactionReceipt>} The signed transaction data and UTXO tracking info. */ 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; }) === void 0; }); } 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, 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, 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 } : void 0, 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 }; } /** * Broadcasts a pre-signed interaction transaction. * @param {SignedInteractionTransactionReceipt} signedTx - The signed transaction data. * @returns {Promise<InteractionTransactionReceipt>} The transaction receipt with broadcast results. */ 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 }; } /** * Signs and broadcasts a bitcoin interaction transaction from a simulated contract call. * @param {TransactionParameters} interactionParams - The parameters for the transaction. * @param {bigint} amountAddition - Additional satoshis to request when acquiring UTXOs. * @returns {Promise<InteractionTransactionReceipt>} The transaction receipt with broadcast results. */ 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; } } /** * Set the gas estimation values. * @param {bigint} estimatedGas - The estimated gas in satoshis. * @param {bigint} refundedGas - The refunded gas in satoshis. */ setGasEstimation(estimatedGas, refundedGas) { this.estimatedSatGas = estimatedGas; this.estimatedRefundedGasInSat = refundedGas; } /** * Set the Bitcoin fee rates. * @param {BitcoinFees} fees - The Bitcoin fee rates. */ setBitcoinFee(fees) { this.#bitcoinFees = fees; } /** * Set the decoded contract output properties. * @param {DecodedOutput} decoded - The decoded output. */ setDecoded(decoded) { this.properties = Object.freeze(decoded.obj); } /** * Set the contract events. * @param {U} events - The contract events. */ setEvents(events) { this.events = events; } /** * Set the calldata for the transaction. * @param {Buffer} calldata - The calldata buffer. */ setCalldata(calldata) { this.calldata = calldata; } /** * Clone a UTXO and attach a witness script. * @param {UTXO} utxo - The UTXO to clone. * @param {Buffer} witnessScript - The witness script to attach. * @returns {UTXO} The cloned UTXO with witness script. */ #cloneUTXOWithWitnessScript(utxo, witnessScript) { const clone = Object.assign( Object.create(Object.getPrototypeOf(utxo)), utxo ); clone.witnessScript = witnessScript; return clone; } /** * Process UTXO tracking after transaction broadcast. * @param {SignedInteractionTransactionReceipt} signedTx - The signed transaction receipt. */ #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); } } } /** * Acquire UTXOs for the transaction. * @param {TransactionParameters} interactionParams - The transaction parameters. * @param {bigint} amountAddition - Additional amount to request. * @returns {Promise<UTXO[]>} The acquired UTXOs. */ 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; } /** * Return the maximum of two bigints. * @param {bigint} a - First value. * @param {bigint} b - Second value. * @returns {bigint} The maximum value. */ bigintMax(a, b) { return a > b ? a : b; } /** * Fetch UTXOs from the provider. * @param {bigint} amount - The amount needed. * @param {TransactionParameters} interactionParams - The transaction parameters. * @returns {Promise<UTXO[]>} The fetched UTXOs. */ async #fetchUTXOs(amount, interactionParams) { if (!interactionParams.sender && !interactionParams.refundTo) { throw new Error("Refund address not set"); } const utxoSetting = { address: interactionParams.sender || interactionParams.refundTo, amount, throwErrors: true, maxUTXOs: interactionParams.maxUTXOs, throwIfUTXOsLimitReached: interactionParams.throwIfUTXOsLimitReached, csvAddress: !interactionParams.p2wda && !interactionParams.dontUseCSVUtxos ? this.csvAddress?.address : void 0 }; 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; } /** * Get storage keys from access list. * @returns {LoadedStorage} The loaded storage map. */ getValuesFromAccessList() { const storage = {}; for (const contract in this.accessList) { const contractData = this.accessList[contract]; storage[contract] = Object.keys(contractData); } return storage; } /** * Convert contract address to p2op string. * @param {string} contract - The contract address hex. * @returns {string} The p2op address string. */ contractToString(contract) { const addressCa = Address.fromString(contract); return addressCa.p2op(this.#provider.network); } /** * Parse raw events into EventList format. * @param {RawEventList} events - The raw events. * @returns {EventList} The parsed events. */ parseEvents(events) { const eventsList = {}; for (const [contract, value] of Object.entries(events)) { const events2 = []; for (const event of value) { const eventData = new NetEvent(event.type, Buffer.from(event.data, "base64")); events2.push(eventData); } eventsList[this.contractToString(contract)] = events2; } return eventsList; } /** * Convert base64 string to Uint8Array. * @param {string} base64 - The base64 encoded string. * @returns {Uint8Array} The decoded bytes. */ base64ToUint8Array(base64) { return BufferHelper.bufferToUint8Array(Buffer.from(base64, "base64")); } } class ContractData { contractAddress; contractPublicKey; bytecode; wasCompressed; deployedTransactionId; deployedTransactionHash; deployerPubKey; deployerHashedPublicKey; contractSeed; contractSaltHash; deployerAddress; constructor(raw) { this.contractAddress = raw.contractAddress; this.contractPublicKey = Buffer.isBuffer(raw.contractPublicKey) ? new Address(raw.contractPublicKey) : new Address(Buffer.from(raw.contractPublicKey, "base64")); this.bytecode = Buffer.isBuffer(raw.bytecode) ? raw.bytecode : Buffer.from(raw.bytecode, "base64"); this.wasCompressed = raw.wasCompressed; this.deployedTransactionId = raw.deployedTransactionId; this.deployedTransactionHash = raw.deployedTransactionHash; this.deployerPubKey = Buffer.isBuffer(raw.deployerPubKey) ? raw.deployerPubKey : Buffer.from(raw.deployerPubKey, "base64"); this.deployerHashedPublicKey = Buffer.isBuffer(raw.deployerAddress) ? raw.deployerAddress : Buffer.from(raw.deployerAddress.replace("0x", ""), "base64"); this.contractSeed = Buffer.isBuffer(raw.contractSeed) ? raw.contractSeed : Buffer.from(raw.contractSeed, "base64"); this.contractSaltHash = Buffer.isBuffer(raw.contractSaltHash) ? raw.contractSaltHash : Buffer.from(raw.contractSaltHash, "base64"); if (this.deployerHashedPublicKey && this.deployerPubKey) { this.deployerAddress = new Address(this.deployerHashedPublicKey, this.deployerPubKey); } else { throw new Error("Deployer address or public key is missing"); } } } var TransactionInputFlags = /* @__PURE__ */ ((TransactionInputFlags2) => { TransactionInputFlags2[TransactionInputFlags2["hasCoinbase"] = 1] = "hasCoinbase"; TransactionInputFlags2[TransactionInputFlags2["hasWitness"] = 2] = "hasWitness"; return TransactionInputFlags2; })(TransactionInputFlags || {}); var TransactionOutputFlags = /* @__PURE__ */ ((TransactionOutputFlags2) => { TransactionOutputFlags2[TransactionOutputFlags2["hasTo"] = 1] = "hasTo"; TransactionOutputFlags2[TransactionOutputFlags2["hasScriptPubKey"] = 2] = "hasScriptPubKey"; TransactionOutputFlags2[TransactionOutputFlags2["OP_RETURN"] = 4] = "OP_RETURN"; return TransactionOutputFlags2; })(TransactionOutputFlags || {}); class EpochMiner { solution; publicKey; salt; graffiti; constructor(data) { this.solution = stringToBuffer(data.solution); this.publicKey = Address.fromString(data.mldsaPublicKey, data.legacyPublicKey); this.salt = stringToBuffer(data.salt); this.graffiti = data.graffiti ? stringToBuffer(data.graffiti) : void 0; } } class Epoch { epochNumber; epochHash; epochRoot; startBlock; endBlock; difficultyScaled; minDifficulty; targetHash; proposer; proofs; constructor(data) { this.epochNumber = BigInt(data.epochNumber); this.epochHash = stringToBuffer(data.epochHash); this.epochRoot = stringToBuffer(data.epochRoot); this.startBlock = BigInt(data.startBlock); this.endBlock = BigInt(data.endBlock); this.difficultyScaled = BigInt(data.difficultyScaled); this.minDifficulty = data.minDifficulty; this.targetHash = stringToBuffer(data.targetHash); this.proposer = new EpochMiner(data.proposer); this.proofs = Object.freeze(data.proofs.map((proof) => stringToBuffer(proof))); } } class EpochSubmission { submissionTxId; submissionTxHash; submissionHash; confirmedAt; epochProposed; constructor(data) { this.submissionTxId = stringToBuffer(data.submissionTxId); this.submissionTxHash = stringToBuffer(data.submissionTxHash); this.submissionHash = stringToBuffer(data.submissionHash); this.confirmedAt = data.confirmedAt; this.epochProposed = new EpochMiner(data.epochProposed); } } class EpochWithSubmissions extends Epoch { submissions; constructor(data) { super(data); if (data.submissions) { this.submissions = Object.freeze( data.submissions.map((sub) => new EpochSubmission(sub)) ); } } } class EpochTemplate { epochNumber; epochTarget; constructor(data) { this.epochNumber = BigInt(data.epochNumber); this.epochTarget = stringToBuffer(data.epochTarget); } } class SubmittedEpoch { epochNumber; submissionHash; difficulty; timestamp; status; message; constructor(data) { this.epochNumber = BigInt(data.epochNumber); this.submissionHash = stringToBuffer(data.submissionHash); this.difficulty = data.difficulty; this.timestamp = typeof data.timestamp === "number" ? new Date(data.timestamp) : data.timestamp; this.status = data.status; this.message = data.message; } } class StoredValue { pointer; value; height; proofs; constructor(iStoredValue) { this.pointer = typeof iStoredValue.pointer === "string" ? this.base64ToBigInt(iStoredValue.pointer) : iStoredValue.pointer; if (typeof iStoredValue.value !== "string") { this.value = iStoredValue.value; } else { this.value = Buffer.from( iStoredValue.value, iStoredValue.value.startsWith("0x") ? "hex" : "base64" ); } this.height = BigInt(iStoredValue.height); this.proofs = iStoredValue.proofs || []; } base64ToBigInt(base64) { return BufferHelper.uint8ArrayToPointer(Buffer.from(base64, "base64")); } } class UTXO { transactionId; outputIndex; value; scriptPubKey; nonWitnessUtxo; witnessScript; redeemScript; isCSV; /** * Create a UTXO from raw interface data * @param iUTXO - The raw UTXO data from the API * @param isCSV - Whether this is a CSV UTXO */ constructor(iUTXO, isCSV) { this.transactionId = iUTXO.transactionId; this.outputIndex = iUTXO.outputIndex; this.isCSV = isCSV || false; this.value = BigInt(iUTXO.value); this.scriptPubKey = iUTXO.scriptPubKey; this.nonWitnessUtxo = Buffer.from(iUTXO.raw, "base64"); } } var JSONRpcMethods = /* @__PURE__ */ ((JSONRpcMethods2) => { JSONRpcMethods2["BLOCK_BY_NUMBER"] = "btc_blockNumber"; JSONRpcMethods2["CHAIN_ID"] = "btc_chainId"; JSONRpcMethods2["REORG"] = "btc_reorg"; JSONRpcMethods2["GET_BLOCK_BY_HASH"] = "btc_getBlockByHash"; JSONRpcMethods2["GET_BLOCK_BY_CHECKSUM"] = "btc_getBlockByChecksum"; JSONRpcMethods2["GET_BLOCK_BY_NUMBER"] = "btc_getBlockByNumber"; JSONRpcMethods2["GAS"] = "btc_gas"; JSONRpcMethods2["GET_TRANSACTION_BY_HASH"] = "btc_getTransactionByHash"; JSONRpcMethods2["BROADCAST_TRANSACTION"] = "btc_sendRawTransaction"; JSONRpcMethods2["TRANSACTION_PREIMAGE"] = "btc_preimage"; JSONRpcMethods2["PUBLIC_KEY_INFO"] = "btc_publicKeyInfo"; JSONRpcMethods2["GET_UTXOS"] = "btc_getUTXOs"; JSONRpcMethods2["GET_BALANCE"] = "btc_getBalance"; JSONRpcMethods2["BLOCK_WITNESS"] = "btc_blockWitness"; JSONRpcMethods2["GET_TRANSACTION_RECEIPT"] = "btc_getTransactionReceipt"; JSONRpcMethods2["GET_CODE"] = "btc_getCode"; JSONRpcMethods2["GET_STORAGE_AT"] = "btc_getStorageAt"; JSONRpcMethods2["LATEST_EPOCH"] = "btc_latestEpoch"; JSONRpcMethods2["GET_EPOCH_BY_NUMBER"] = "btc_getEpochByNumber"; JSONRpcMethods2["GET_EPOCH_BY_HASH"] = "btc_getEpochByHash"; JSONRpcMethods2["GET_EPOCH_TEMPLATE"] = "btc_getEpochTemplate"; JSONRpcMethods2["SUBMIT_EPOCH"] = "btc_submitEpoch"; JSONRpcMethods2["CALL"] = "btc_call"; return JSONRpcMethods2; })(JSONRpcMethods || {}); const AUTO_PURGE_AFTER = 1e3 * 60; const FETCH_COOLDOWN = 1e4; const MEMPOOL_CHAIN_LIMIT = 25; class UTXOsManager { constructor(provider) { this.provider = provider; } /** * Holds all address-specific data so we don’t mix up UTXOs between addresses/wallets. */ dataByAddress = {}; /** * Mark UTXOs as spent and track new UTXOs created by the transaction, _per address_. * * Enforces a mempool chain limit of 25 unconfirmed transaction descendants. * * @param address - The address these spent/new UTXOs belong to * @param {UTXOs} spent - The UTXOs that were spent. * @param {UTXOs} newUTXOs - The new UTXOs created by the transaction. * @throws {Error} If adding the new unconfirmed outputs would exceed the mempool chain limit. */ spentUTXO(address, spent, newUTXOs) { const addressData = this.getAddressData(address); const utxoKey = (u) => `${u.transactionId}:${u.outputIndex}`; addressData.pendingUTXOs = addressData.pendingUTXOs.filter((utxo) => { return !spent.some( (spentUtxo) => spentUtxo.transactionId === utxo.transactionId && spentUtxo.outputIndex === utxo.outputIndex ); }); for (const spentUtxo of spent) { const key = utxoKey(spentUtxo); delete addressData.pendingUtxoDepth[key]; } addressData.spentUTXOs.push(...spent); let maxParentDepth = 0; for (const spentUtxo of spent) { const key = utxoKey(spentUtxo); const parentDepth = addressData.pendingUtxoDepth[key] ?? 0; if (parentDepth > maxParentDepth) { maxParentDepth = parentDepth; } } const newDepth = maxParentDepth + 1; if (newDepth > MEMPOOL_CHAIN_LIMIT) { throw new Error( `too-long-mempool-chain, too many descendants for tx ... [limit: ${MEMPOOL_CHAIN_LIMIT}]` ); } for (const nu of newUTXOs) { addressData.pendingUTXOs.push(nu); addressData.pendingUtxoDepth[utxoKey(nu)] = newDepth; } } /** * Get the pending UTXOs for a specific address. * @param address */ getPendingUTXOs(address) { const addressData = this.getAddressData(address); return addressData.pendingUTXOs; } /** * Clean (reset) the data for a particular address or for all addresses if none is passed. */ clean(address) { if (address) { const addressData = this.getAddressData(address); addressData.spentUTXOs = []; addressData.pendingUTXOs = []; addressData.pendingUtxoDepth = {}; addressData.lastCleanup = Date.now(); addressData.lastFetchTimestamp = 0; addressData.lastFetchedData = null; } else { this.dataByAddress = {}; } } /** * Get UTXOs with configurable options, specifically for an address. * * If the last UTXO fetch for that address was <10s ago, returns cached data. * Otherwise, fetches fresh data from the provider. * * @param {object} options - The UTXO fetch options * @param {string} options.address - The address to get the UTXOs for * @param {boolean} [options.optimize=true] - Whether to optimize the UTXOs * @param {boolean} [options.mergePendingUTXOs=true] - Merge locally pending UTXOs * @param {boolean} [options.filterSpentUTXOs=true] - Filter out known-spent UTXOs * @param {boolean} [options.isCSV=false] - Whether to this UTXO as a CSV UTXO * @param {bigint} [options.olderThan] - Only fetch UTXOs older than this value * @returns {Promise<UTXOs>} The UTXOs * @throws {Error} If something goes wrong */ async getUTXOs({ address, isCSV = false, optimize = true, mergePendingUTXOs = true, filterSpentUTXOs = true, olderThan }) { const addressData = this.getAddressData(address); const fetchedData = await this.maybeFetchUTXOs(address, optimize, olderThan, isCSV); const utxoKey = (utxo) => `${utxo.transactionId}:${utxo.outputIndex}`; const spentRefKey = (ref) => `${ref.transactionId}:${ref.outputIndex}`; const pendingUTXOKeys = new Set(addressData.pendingUTXOs.map(utxoKey)); const spentUTXOKeys = new Set(addressData.spentUTXOs.map(utxoKey)); const fetchedSpentKeys = new Set(fetchedData.spentTransactions.map(spentRefKey)); const combinedUTXOs = []; const combinedKeysSet = /* @__PURE__ */ new Set(); for (const utxo of fetchedData.confirmed) { const key = utxoKey(utxo); if (!combinedKeysSet.has(key)) { combinedUTXOs.push(utxo); combinedKeysSet.add(key); } } if (mergePendingUTXOs) { for (const utxo of addressData.pendingUTXOs) { const key = utxoKey(utxo); if (!combinedKeysSet.has(key)) { combinedUTXOs.push(utxo); combinedKeysSet.add(key); } } for (const utxo of fetchedData.pending) { const key = utxoKey(utxo); if (!pendingUTXOKeys.has(key) && !combinedKeysSet.has(key)) { combinedUTXOs.push(utxo); combinedKeysSet.add(key); } } } let finalUTXOs = combinedUTXOs.filter((utxo) => !spentUTXOKeys.has(utxoKey(utxo))); if (filterSpentUTXOs && fetchedSpentKeys.size > 0) { finalUTXOs = finalUTXOs.filter((utxo) => !fetchedSpentKeys.has(utxoKey(utxo))); } return finalUTXOs; } /** * Fetch UTXOs for a specific amount needed, from a single address, * merging from pending and confirmed UTXOs. * * @param {object} options * @param {string} options.address The address to fetch UTXOs for * @param {bigint} options.amount The needed amount * @param {boolean} [options.optimize=true] Optimize the UTXOs * @param {boolean} [options.csvAddress] Use CSV UTXOs in priority * @param {boolean} [options.mergePendingUTXOs=true] Merge pending * @param {boolean} [options.filterSpentUTXOs=true] Filter out spent * @param {boolean} [options.throwErrors=false] Throw error if insufficient * @param {bigint} [options.olderThan] Only fetch UTXOs older than this value * @returns {Promise<UTXOs>} */ async getUTXOsForAmount({ address, amount, csvAddress, optimize = true, mergePendingUTXOs = true,