opnet
Version:
The perfect library for building Bitcoin-based applications.
185 lines (184 loc) • 7.66 kB
JavaScript
import { UTXO } from '../bitcoin/UTXOs.js';
import { JSONRpcMethods } from '../providers/interfaces/JSONRpcMethods.js';
const AUTO_PURGE_AFTER = 1000 * 60;
const FETCH_COOLDOWN = 10000;
const MEMPOOL_CHAIN_LIMIT = 25;
export class UTXOsManager {
provider;
dataByAddress = {};
constructor(provider) {
this.provider = provider;
}
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;
}
}
getPendingUTXOs(address) {
const addressData = this.getAddressData(address);
return addressData.pendingUTXOs;
}
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 = {};
}
}
async getUTXOs({ address, optimize = true, mergePendingUTXOs = true, filterSpentUTXOs = true, olderThan = undefined, }) {
const addressData = this.getAddressData(address);
const fetchedData = await this.maybeFetchUTXOs(address, optimize, olderThan);
const utxoKey = (utxo) => `${utxo.transactionId}:${utxo.outputIndex}`;
const pendingUTXOKeys = new Set(addressData.pendingUTXOs.map(utxoKey));
const spentUTXOKeys = new Set(addressData.spentUTXOs.map(utxoKey));
const fetchedSpentKeys = new Set(fetchedData.spentTransactions.map(utxoKey));
const combinedUTXOs = [];
const combinedKeysSet = 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;
}
async getUTXOsForAmount({ address, amount, optimize = true, mergePendingUTXOs = true, filterSpentUTXOs = true, throwErrors = false, olderThan = undefined, }) {
const combinedUTXOs = await this.getUTXOs({
address,
optimize,
mergePendingUTXOs,
filterSpentUTXOs,
olderThan,
});
const utxoUntilAmount = [];
let currentValue = 0n;
for (const utxo of combinedUTXOs) {
utxoUntilAmount.push(utxo);
currentValue += utxo.value;
if (currentValue >= amount) {
break;
}
}
if (currentValue < amount && throwErrors) {
throw new Error(`Insufficient UTXOs to cover amount. Available: ${currentValue}, Needed: ${amount}`);
}
return utxoUntilAmount;
}
getAddressData(address) {
if (!this.dataByAddress[address]) {
this.dataByAddress[address] = {
spentUTXOs: [],
pendingUTXOs: [],
pendingUtxoDepth: {},
lastCleanup: Date.now(),
lastFetchTimestamp: 0,
lastFetchedData: null,
};
}
return this.dataByAddress[address];
}
async maybeFetchUTXOs(address, optimize, olderThan) {
const addressData = this.getAddressData(address);
const now = Date.now();
const age = now - addressData.lastFetchTimestamp;
if (now - addressData.lastCleanup > AUTO_PURGE_AFTER) {
this.clean(address);
}
if (addressData.lastFetchedData && age < FETCH_COOLDOWN) {
return addressData.lastFetchedData;
}
addressData.lastFetchedData = await this.fetchUTXOs(address, optimize, olderThan);
addressData.lastFetchTimestamp = now;
this.syncPendingDepthWithFetched(address);
return addressData.lastFetchedData;
}
async fetchUTXOs(address, optimize = false, olderThan) {
const data = [address, optimize];
if (olderThan !== undefined) {
data.push(olderThan.toString());
}
const payload = this.provider.buildJsonRpcPayload(JSONRpcMethods.GET_UTXOS, data);
const rawUTXOs = await this.provider.callPayloadSingle(payload);
if ('error' in rawUTXOs) {
throw new Error(`Error fetching block: ${rawUTXOs.error}`);
}
const result = rawUTXOs.result || {
confirmed: [],
pending: [],
spentTransactions: [],
};
return {
confirmed: result.confirmed.map((utxo) => new UTXO(utxo)),
pending: result.pending.map((utxo) => new UTXO(utxo)),
spentTransactions: result.spentTransactions.map((utxo) => new UTXO(utxo)),
};
}
syncPendingDepthWithFetched(address) {
const addressData = this.getAddressData(address);
const fetched = addressData.lastFetchedData;
if (!fetched)
return;
const confirmedKeys = new Set(fetched.confirmed.map((u) => `${u.transactionId}:${u.outputIndex}`));
const spentKeys = new Set(fetched.spentTransactions.map((u) => `${u.transactionId}:${u.outputIndex}`));
addressData.pendingUTXOs = addressData.pendingUTXOs.filter((u) => {
const key = `${u.transactionId}:${u.outputIndex}`;
if (confirmedKeys.has(key) || spentKeys.has(key)) {
delete addressData.pendingUtxoDepth[key];
return false;
}
return true;
});
}
}