@coinbase/cdp-sdk
Version:
SDK for interacting with the Coinbase Developer Platform Wallet API
182 lines • 5.87 kB
JavaScript
/*
* Ledger that tracks cumulative spend and backs the onApproachingLimit notifier.
*/
import { SpendControlError } from "./types.js";
/**
* Default maximum number of ledger entries kept in memory.
*/
export const DEFAULT_MAX_LEDGER_ENTRIES = 10_000;
/**
* Default in-memory SpendStore backed by a plain array.
*/
class InMemorySpendStore {
entries = [];
/**
* Returns the current entry count.
*
* @returns The number of entries in the store.
*/
async size() {
return this.entries.length;
}
/**
* Returns all entries currently held by the store.
*
* @returns All ledger entries.
*/
async load() {
return this.entries;
}
/**
* Adds a single entry to the store.
*
* @param entry - The ledger entry to add.
*/
async append(entry) {
this.entries.push(entry);
}
/**
* Drops entries older than `olderThanMs`.
*
* @param olderThanMs - Cutoff timestamp in milliseconds.
*/
async prune(olderThanMs) {
let cut = 0;
while (cut < this.entries.length && this.entries[cut].at < olderThanMs) {
cut++;
}
if (cut > 0)
this.entries.splice(0, cut);
}
/**
* Removes a specific entry by its field values.
*
* @param entry - The ledger entry to remove.
*/
async removeEntry(entry) {
for (let i = this.entries.length - 1; i >= 0; i--) {
const e = this.entries[i];
if (e.at === entry.at &&
e.atomicAmount === entry.atomicAmount &&
e.asset === entry.asset &&
e.network === entry.network &&
e.payTo === entry.payTo) {
this.entries.splice(i, 1);
return;
}
}
}
}
/**
* Tracks spend per asset in an append-only ledger.
*/
export class SpendTracker {
store;
maxLedgerEntries;
warnedOnRemove = false;
/**
* Creates a new SpendTracker.
*
* @param options - Optional configuration for the tracker.
*/
constructor(options = {}) {
this.store = options.store ?? new InMemorySpendStore();
this.maxLedgerEntries = options.maxLedgerEntries ?? DEFAULT_MAX_LEDGER_ENTRIES;
if (!Number.isInteger(this.maxLedgerEntries) || this.maxLedgerEntries <= 0) {
throw new SpendControlError("configuration_invalid", `maxLedgerEntries must be a positive integer, received ${this.maxLedgerEntries}`, { input: options.maxLedgerEntries });
}
}
/**
* Adds a spend entry to the ledger and returns it.
*
* @param input - The spend record to add.
* @returns The recorded ledger entry.
*/
async record(input) {
if (input.atomicAmount < 0n) {
throw new SpendControlError("amount_unparseable", "Cannot record a negative spend", {
attempted: input.atomicAmount.toString(),
asset: input.asset,
network: input.network,
payTo: input.payTo,
});
}
const entry = {
atomicAmount: input.atomicAmount,
asset: input.asset,
network: input.network,
payTo: input.payTo,
at: Date.now(),
};
const currentSize = this.store.size
? await this.store.size()
: (await this.store.load()).length;
if (currentSize >= this.maxLedgerEntries) {
throw new SpendControlError("ledger_capacity_exceeded", `SpendTracker ledger is full (maxLedgerEntries=${this.maxLedgerEntries}); rejecting new spend record to prevent under-counting.`);
}
await this.store.append(entry);
return entry;
}
/**
* Sum atomic amounts for the given asset, optionally restricted to entries
* recorded at or after `since`.
*
* @param query - Query parameters specifying asset and optional time window.
* @returns The total atomic amount spent.
*/
async total(query) {
if (query.since !== undefined && this.store.prune) {
await this.store.prune(query.since);
}
let sum = 0n;
const entries = await this.store.load();
for (const entry of entries) {
if (query.since !== undefined && entry.at < query.since)
continue;
if (entry.asset !== query.asset)
continue;
sum += entry.atomicAmount;
}
return sum;
}
/**
* Drop entries older than `now - windowMs`.
*
* @param now - Current timestamp in milliseconds.
* @param windowMs - Optional rolling window size in milliseconds.
*/
async prune(now, windowMs) {
if (windowMs === undefined || windowMs <= 0)
return;
const cutoff = now - windowMs;
if (this.store.prune) {
await this.store.prune(cutoff);
}
}
/**
* Removes a previously recorded entry, undoing its contribution to the cumulative total.
*
* @param entry - The ledger entry to remove.
*/
async removeEntry(entry) {
if (this.store.removeEntry) {
await this.store.removeEntry(entry);
return;
}
if (!this.warnedOnRemove) {
// eslint-disable-next-line no-console
console.warn(`[/cdp-sdk/x402] SpendTracker: store does not implement removeEntry; ` +
`rollback is a no-op so the cumulative ledger may over-count after a payment failure.`);
this.warnedOnRemove = true;
}
}
/**
* Returns all current ledger entries as a read-only snapshot.
*
* @returns All current entries.
*/
async entries() {
return this.store.load();
}
}
//# sourceMappingURL=spend-tracker.js.map