opnet
Version:
The perfect library for building Bitcoin-based applications.
1,465 lines (1,434 loc) • 260 kB
JavaScript
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,