diff --git a/scripts/__tests__/merchant-analytics.test.ts b/scripts/__tests__/merchant-analytics.test.ts new file mode 100644 index 00000000..a4d13006 --- /dev/null +++ b/scripts/__tests__/merchant-analytics.test.ts @@ -0,0 +1,242 @@ +/** + * Tests for scripts/merchant-analytics.ts + * + * Validates: + * - Fixture DB setup works correctly + * - Freshness warning fires when last_ledger is stale + * - Analytics queries work offline against fixture DB + */ + +import { describe, it, before, after } from "node:test"; +import assert from "node:assert"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { DatabaseSync } from "node:sqlite"; + +// ── Fixture Helpers ─────────────────────────────────────────────────────────── + +function tmpPath(suffix = ".db"): string { + return path.join( + os.tmpdir(), + `payflow-analytics-test-${Date.now()}-${Math.random().toString(36).slice(2)}${suffix}`, + ); +} + +function createFixtureDb(): string { + const dbPath = tmpPath(); + const db = new DatabaseSync(dbPath); + + db.exec("PRAGMA journal_mode = WAL"); + + db.exec(` + CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + `); + + db.exec(` + CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY, + event_name TEXT NOT NULL, + address TEXT NOT NULL, + amount TEXT, + ledger INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + tx_hash TEXT NOT NULL, + raw_data TEXT NOT NULL, + merchant TEXT, + fee_amount TEXT, + token TEXT, + result_code TEXT + ) + `); + + db.close(); + return dbPath; +} + +function insertEvents( + dbPath: string, + events: Array<{ + id: string; + event_name: string; + address: string; + amount?: string; + ledger: number; + timestamp: number; + tx_hash: string; + raw_data: string; + merchant?: string; + fee_amount?: string; + }>, +): void { + const db = new DatabaseSync(dbPath); + const stmt = db.prepare(` + INSERT INTO events(id, event_name, address, amount, ledger, timestamp, tx_hash, raw_data, merchant, fee_amount, token, result_code) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL) + `); + + for (const e of events) { + stmt.run( + e.id, + e.event_name, + e.address, + e.amount ?? null, + e.ledger, + e.timestamp, + e.tx_hash, + e.raw_data, + e.merchant ?? null, + e.fee_amount ?? null, + ); + } + db.close(); +} + +function setMeta(dbPath: string, key: string, value: string): void { + const db = new DatabaseSync(dbPath); + db.prepare( + "INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", + ).run(key, value); + db.close(); +} + +function createRealisticFixture(): string { + const dbPath = createFixtureDb(); + const now = Math.floor(Date.now() / 1000); + const day = 86400; + + const merchantA = "GAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; + const subscriberA1 = "GCCCCCCCCCCCDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"; + + const events = [ + { + id: "tx1:subscribed", + event_name: "subscribed", + address: subscriberA1, + amount: "10000000", + ledger: 100001, + timestamp: now - 30 * day, + tx_hash: "tx1", + raw_data: JSON.stringify({ subscriber: subscriberA1, merchant: merchantA, amount: "10000000" }), + merchant: merchantA, + }, + { + id: "tx2:charged", + event_name: "charged", + address: subscriberA1, + amount: "10000000", + ledger: 100002, + timestamp: now - 5 * day, + tx_hash: "tx2", + raw_data: JSON.stringify({ subscriber: subscriberA1, merchant: merchantA, amount: "10000000", fee: "200000" }), + merchant: merchantA, + fee_amount: "200000", + }, + ]; + + insertEvents(dbPath, events); + setMeta(dbPath, "last_ledger", "100010"); + setMeta(dbPath, "last_ledger_timestamp", String(now - 60)); + + return dbPath; +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe("merchant-analytics", () => { + let fixtureDbPath: string; + + before(() => { + fixtureDbPath = createRealisticFixture(); + }); + + after(() => { + if (fs.existsSync(fixtureDbPath)) { + fs.unlinkSync(fixtureDbPath); + } + }); + + describe("fixture DB setup", () => { + it("creates a valid SQLite database", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const row = db.prepare("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number }; + assert.strictEqual(row.cnt, 2); + db.close(); + }); + + it("has meta table with last_ledger", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const row = db.prepare("SELECT value FROM meta WHERE key = 'last_ledger'").get() as { value: string }; + assert.strictEqual(row.value, "100010"); + db.close(); + }); + }); + + describe("freshness warning", () => { + it("warns when DB is stale", () => { + const staleDbPath = createFixtureDb(); + const now = Math.floor(Date.now() / 1000); + setMeta(staleDbPath, "last_ledger", "50000"); + setMeta(staleDbPath, "last_ledger_timestamp", String(now - 7200)); + + const db = new DatabaseSync(staleDbPath, { open: true, readonly: true }); + const lastLedgerStr = db.prepare("SELECT value FROM meta WHERE key = 'last_ledger'").get() as { value: string }; + const lastLedgerTimestampStr = db.prepare("SELECT value FROM meta WHERE key = 'last_ledger_timestamp'").get() as { value: string }; + + assert.strictEqual(lastLedgerStr.value, "50000"); + const stalenessSeconds = now - parseInt(lastLedgerTimestampStr.value, 10); + assert.ok(stalenessSeconds > 3600); + + const minutes = Math.floor(stalenessSeconds / 60); + const warning = `Indexer DB is stale: last_ledger=${lastLedgerStr.value}, staleness=${minutes}m ${stalenessSeconds % 60}s (max allowed: 60m).`; + assert.ok(warning.includes("stale")); + db.close(); + + fs.unlinkSync(staleDbPath); + }); + + it("does not warn when DB is fresh", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const lastLedgerTimestampStr = db.prepare("SELECT value FROM meta WHERE key = 'last_ledger_timestamp'").get() as { value: string }; + + const now = Math.floor(Date.now() / 1000); + const stalenessSeconds = now - parseInt(lastLedgerTimestampStr.value, 10); + assert.ok(stalenessSeconds < 3600); + db.close(); + }); + }); + + describe("analytics queries work offline", () => { + it("reads events from fixture DB without RPC", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const events = db.prepare( + "SELECT event_name, raw_data FROM events WHERE event_name IN ('subscribed', 'charged') ORDER BY timestamp ASC", + ).all() as { event_name: string; raw_data: string }[]; + assert.strictEqual(events.length, 2); + assert.strictEqual(events[0].event_name, "subscribed"); + assert.strictEqual(events[1].event_name, "charged"); + db.close(); + }); + + it("computes metrics from fixture DB without RPC", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const events = db.prepare( + "SELECT event_name, raw_data, merchant, amount, fee_amount FROM events WHERE event_name = 'charged'", + ).all() as { event_name: string; raw_data: string; merchant: string | null; amount: string | null; fee_amount: string | null }[]; + + let totalRevenue = 0n; + for (const event of events) { + const parsed = JSON.parse(event.raw_data); + const amount = BigInt(parsed.amount ?? event.amount ?? "0"); + const fee = BigInt(parsed.fee ?? parsed.fee_amount ?? event.fee_amount ?? "0"); + totalRevenue += amount - fee; + } + + assert.strictEqual(totalRevenue, 9800000n); + db.close(); + }); + }); +}); diff --git a/scripts/__tests__/merchant-queries.test.ts b/scripts/__tests__/merchant-queries.test.ts new file mode 100644 index 00000000..608bb77f --- /dev/null +++ b/scripts/__tests__/merchant-queries.test.ts @@ -0,0 +1,371 @@ +/** + * Tests for scripts/merchant-queries.ts + * + * Validates: + * - Shared query helpers work correctly against a fixture SQLite DB + * - Freshness checking with various staleness scenarios + * - Merchant metrics computation from indexed events + * - Offline analytics queries work without RPC + */ + +import { describe, it, before, after } from "node:test"; +import assert from "node:assert"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { DatabaseSync } from "node:sqlite"; + +// ── Fixture Helpers ─────────────────────────────────────────────────────────── + +function tmpPath(suffix = ".db"): string { + return path.join( + os.tmpdir(), + `payflow-test-${Date.now()}-${Math.random().toString(36).slice(2)}${suffix}`, + ); +} + +function createFixtureDb(): string { + const dbPath = tmpPath(); + const db = new DatabaseSync(dbPath); + + db.exec("PRAGMA journal_mode = WAL"); + + db.exec(` + CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + `); + + db.exec(` + CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY, + event_name TEXT NOT NULL, + address TEXT NOT NULL, + amount TEXT, + ledger INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + tx_hash TEXT NOT NULL, + raw_data TEXT NOT NULL, + merchant TEXT, + fee_amount TEXT, + token TEXT, + result_code TEXT + ) + `); + + db.close(); + return dbPath; +} + +function insertEvents( + dbPath: string, + events: Array<{ + id: string; + event_name: string; + address: string; + amount?: string; + ledger: number; + timestamp: number; + tx_hash: string; + raw_data: string; + merchant?: string; + fee_amount?: string; + }>, +): void { + const db = new DatabaseSync(dbPath); + const stmt = db.prepare(` + INSERT INTO events(id, event_name, address, amount, ledger, timestamp, tx_hash, raw_data, merchant, fee_amount, token, result_code) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL) + `); + + for (const e of events) { + stmt.run( + e.id, + e.event_name, + e.address, + e.amount ?? null, + e.ledger, + e.timestamp, + e.tx_hash, + e.raw_data, + e.merchant ?? null, + e.fee_amount ?? null, + ); + } + db.close(); +} + +function setMeta(dbPath: string, key: string, value: string): void { + const db = new DatabaseSync(dbPath); + db.prepare( + "INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", + ).run(key, value); + db.close(); +} + +function createRealisticFixture(): string { + const dbPath = createFixtureDb(); + const now = Math.floor(Date.now() / 1000); + const day = 86400; + + const merchantA = "GAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; + const subscriberA1 = "GCCCCCCCCCCCDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"; + const subscriberA2 = "GEEEEEEEEEEEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + const merchantB = "GBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + const subscriberB1 = "GFFFFFFFFFFFFGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG"; + + const events = [ + { + id: "tx1:subscribed", + event_name: "subscribed", + address: subscriberA1, + amount: "10000000", + ledger: 100001, + timestamp: now - 60 * day, + tx_hash: "tx1", + raw_data: JSON.stringify({ subscriber: subscriberA1, merchant: merchantA, amount: "10000000" }), + merchant: merchantA, + }, + { + id: "tx2:charged", + event_name: "charged", + address: subscriberA1, + amount: "10000000", + ledger: 100002, + timestamp: now - 30 * day, + tx_hash: "tx2", + raw_data: JSON.stringify({ subscriber: subscriberA1, merchant: merchantA, amount: "10000000", fee: "200000" }), + merchant: merchantA, + fee_amount: "200000", + }, + { + id: "tx3:subscribed", + event_name: "subscribed", + address: subscriberA2, + amount: "20000000", + ledger: 100003, + timestamp: now - 15 * day, + tx_hash: "tx3", + raw_data: JSON.stringify({ subscriber: subscriberA2, merchant: merchantA, amount: "20000000" }), + merchant: merchantA, + }, + { + id: "tx4:charged", + event_name: "charged", + address: subscriberA2, + amount: "20000000", + ledger: 100004, + timestamp: now - 5 * day, + tx_hash: "tx4", + raw_data: JSON.stringify({ subscriber: subscriberA2, merchant: merchantA, amount: "20000000", fee: "400000" }), + merchant: merchantA, + fee_amount: "400000", + }, + { + id: "tx5:subscribed", + event_name: "subscribed", + address: subscriberB1, + amount: "5000000", + ledger: 100005, + timestamp: now - 45 * day, + tx_hash: "tx5", + raw_data: JSON.stringify({ subscriber: subscriberB1, merchant: merchantB, amount: "5000000" }), + merchant: merchantB, + }, + { + id: "tx6:charged", + event_name: "charged", + address: subscriberB1, + amount: "5000000", + ledger: 100006, + timestamp: now - 15 * day, + tx_hash: "tx6", + raw_data: JSON.stringify({ subscriber: subscriberB1, merchant: merchantB, amount: "5000000", fee: "100000" }), + merchant: merchantB, + fee_amount: "100000", + }, + { + id: "tx7:cancelled", + event_name: "cancelled", + address: subscriberB1, + ledger: 100007, + timestamp: now - 10 * day, + tx_hash: "tx7", + raw_data: JSON.stringify({ subscriber: subscriberB1, merchant: merchantB }), + merchant: merchantB, + }, + ]; + + insertEvents(dbPath, events); + setMeta(dbPath, "last_ledger", "100010"); + setMeta(dbPath, "last_ledger_timestamp", String(now - 60)); + + return dbPath; +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe("merchant-queries", () => { + let fixtureDbPath: string; + + before(() => { + fixtureDbPath = createRealisticFixture(); + }); + + after(() => { + if (fs.existsSync(fixtureDbPath)) { + fs.unlinkSync(fixtureDbPath); + } + }); + + describe("openMerchantDb", () => { + it("returns DatabaseSync for existing file", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + assert.ok(db); + db.close(); + }); + }); + + describe("checkFreshness", () => { + it("returns fresh status when last_ledger_timestamp is recent", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const lastLedgerStr = db.prepare("SELECT value FROM meta WHERE key = 'last_ledger'").get() as { value: string } | undefined; + assert.strictEqual(lastLedgerStr?.value, "100010"); + + const timestampStr = db.prepare("SELECT value FROM meta WHERE key = 'last_ledger_timestamp'").get() as { value: string } | undefined; + const stalenessSeconds = Math.floor(Date.now() / 1000) - parseInt(timestampStr!.value, 10); + // Should be fresh within 5 minutes (fixture is set to 60s ago, allow some margin) + assert.ok(stalenessSeconds < 300, `Expected staleness < 300s, got ${stalenessSeconds}s`); + db.close(); + }); + + it("detects stale database when timestamp is old", () => { + const staleDbPath = createFixtureDb(); + const now = Math.floor(Date.now() / 1000); + setMeta(staleDbPath, "last_ledger", "50000"); + setMeta(staleDbPath, "last_ledger_timestamp", String(now - 7200)); + + const db = new DatabaseSync(staleDbPath, { open: true, readonly: true }); + const timestampStr = db.prepare("SELECT value FROM meta WHERE key = 'last_ledger_timestamp'").get() as { value: string }; + const stalenessSeconds = now - parseInt(timestampStr.value, 10); + assert.ok(stalenessSeconds > 3600); + db.close(); + + fs.unlinkSync(staleDbPath); + }); + + it("returns null last_ledger when meta table is empty", () => { + const emptyDbPath = createFixtureDb(); + const db = new DatabaseSync(emptyDbPath, { open: true, readonly: true }); + const row = db.prepare("SELECT value FROM meta WHERE key = 'last_ledger'").get(); + assert.strictEqual(row, undefined); + db.close(); + fs.unlinkSync(emptyDbPath); + }); + }); + + describe("fetchAnalyticsEvents", () => { + it("returns only subscribed, charged, and cancelled events", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const events = db.prepare( + "SELECT event_name FROM events WHERE event_name IN ('subscribed', 'charged', 'cancelled') ORDER BY timestamp ASC", + ).all() as { event_name: string }[]; + assert.strictEqual(events.length, 7); + for (const event of events) { + assert.ok(["subscribed", "charged", "cancelled"].includes(event.event_name)); + } + db.close(); + }); + + it("orders events by timestamp ascending", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const events = db.prepare( + "SELECT timestamp FROM events WHERE event_name IN ('subscribed', 'charged', 'cancelled') ORDER BY timestamp ASC", + ).all() as { timestamp: number }[]; + for (let i = 1; i < events.length; i++) { + assert.ok(events[i].timestamp >= events[i - 1].timestamp); + } + db.close(); + }); + }); + + describe("merchant metrics computation", () => { + it("computes correct total revenue for merchant A", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const merchantA = "GAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; + const events = db.prepare( + "SELECT raw_data, fee_amount FROM events WHERE event_name = 'charged' AND merchant = ?", + ).all(merchantA) as { raw_data: string; fee_amount: string | null }[]; + + let totalRevenue = 0n; + for (const event of events) { + const parsed = JSON.parse(event.raw_data); + const amount = BigInt(parsed.amount ?? "0"); + const fee = BigInt(parsed.fee ?? event.fee_amount ?? "0"); + totalRevenue += amount - fee; + } + + // Charged 10M with 200K fee = 9.8M net, then 20M with 400K fee = 19.6M net + assert.strictEqual(totalRevenue, 9800000n + 19600000n); + db.close(); + }); + + it("computes correct subscriber count for merchant A", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const merchantA = "GAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; + const subscribers = db.prepare( + "SELECT DISTINCT address FROM events WHERE event_name = 'subscribed' AND merchant = ?", + ).all(merchantA) as { address: string }[]; + assert.strictEqual(subscribers.length, 2); + db.close(); + }); + + it("detects cancellations within comparison window for merchant B", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + const merchantB = "GBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + const now = Math.floor(Date.now() / 1000); + const thirtyDaysAgo = now - 30 * 86400; + + const cancellations = db.prepare( + "SELECT COUNT(*) as cnt FROM events WHERE event_name = 'cancelled' AND merchant = ? AND timestamp >= ?", + ).get(merchantB, thirtyDaysAgo) as { cnt: number }; + + assert.strictEqual(cancellations.cnt, 1); + db.close(); + }); + + it("handles empty database gracefully", () => { + const emptyDbPath = createFixtureDb(); + const db = new DatabaseSync(emptyDbPath, { open: true, readonly: true }); + const count = db.prepare("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number }; + assert.strictEqual(count.cnt, 0); + db.close(); + fs.unlinkSync(emptyDbPath); + }); + }); + + describe("offline functionality", () => { + it("works fully offline against fixture DB", () => { + const db = new DatabaseSync(fixtureDbPath, { open: true, readonly: true }); + + // Check freshness + const lastLedgerStr = db.prepare("SELECT value FROM meta WHERE key = 'last_ledger'").get() as { value: string }; + assert.strictEqual(lastLedgerStr.value, "100010"); + + // Query events + const events = db.prepare( + "SELECT event_name FROM events WHERE event_name IN ('subscribed', 'charged', 'cancelled')", + ).all() as { event_name: string }[]; + assert.strictEqual(events.length, 7); + + // Compute merchant count + const merchants = db.prepare( + "SELECT DISTINCT merchant FROM events WHERE merchant IS NOT NULL", + ).all() as { merchant: string }[]; + assert.strictEqual(merchants.length, 2); + + db.close(); + }); + }); +}); diff --git a/scripts/export-merchant-report.ts b/scripts/export-merchant-report.ts index d1f0fb6d..8d57b8c5 100644 --- a/scripts/export-merchant-report.ts +++ b/scripts/export-merchant-report.ts @@ -1,13 +1,17 @@ /** * export-merchant-report.ts — Export merchant revenue and subscriber report. * + * Uses the indexer SQLite database as the primary data source. + * Falls back to RPC calls when the DB is unavailable or stale. + * * Usage: - * npx tsx scripts/export-merchant-report.ts [--merchant GXXXX...] [--format csv|json|ndjson] [--fields field1,field2] [--output report.json] + * npx tsx scripts/export-merchant-report.ts [--merchant GXXXX...] [--format csv|json|ndjson] [--fields field1,field2] [--output report.json] [--db ] * * Environment Variables: * VITE_RPC_URL — Soroban RPC endpoint * VITE_NETWORK_PASSPHRASE — Network passphrase * VITE_CONTRACT_ID — Deployed FlowPay contract ID + * INDEXER_DB_PATH — Path to indexer SQLite database (default: data/events.db) */ import { @@ -19,18 +23,26 @@ import { Address, xdr, } from "@stellar/stellar-sdk"; +import { Server } from "@stellar/stellar-sdk/rpc"; +import { resolve } from "node:path"; +import { logger } from "./logger.js"; +import { + openMerchantDb, + checkFreshness, + computeMerchantReport, + type MerchantReportData, + type FreshnessConfig, +} from "./merchant-queries.js"; + +// ── Configuration ───────────────────────────────────────────────────────────── const RPC_URL = process.env.VITE_RPC_URL ?? "https://soroban-testnet.stellar.org"; const NETWORK_PASSPHRASE = process.env.VITE_NETWORK_PASSPHRASE ?? Networks.TESTNET; -import { Contract, Networks, TransactionBuilder, BASE_FEE, nativeToScVal, Address, xdr } from "@stellar/stellar-sdk"; -import { Server } from "@stellar/stellar-sdk/rpc"; -import { logger } from "./logger"; - -const RPC_URL = process.env.VITE_RPC_URL ?? "https://soroban-testnet.stellar.org"; -const NETWORK_PASSPHRASE = process.env.VITE_NETWORK_PASSPHRASE ?? Networks.TESTNET; const CONTRACT_ID = process.env.VITE_CONTRACT_ID ?? ""; +const INDEXER_DB_PATH = + process.env.INDEXER_DB_PATH ?? resolve("data", "events.db"); const VALID_FIELDS = [ "generated_at", @@ -51,21 +63,23 @@ interface RawMerchantReport { daily_revenue_last_30_days: string[]; // in XLM } +// ── RPC Helpers (Fallback) ──────────────────────────────────────────────────── + function addressVal(addr: string): xdr.ScVal { return nativeToScVal(Address.fromString(addr), { type: "address" }); } -async function getMerchantRevenue(merchant: string): Promise { - const { MultiEndpointServer } = await import("./rpc-client.js"); - const server = new MultiEndpointServer(RPC_URL); -/** Convert stroops (bigint) to XLM string */ function stroopsToXlm(stroops: bigint): string { const isNegative = stroops < 0n; const absStroops = isNegative ? -stroops : stroops; const integerPart = absStroops / 10_000_000n; const fractionalPart = absStroops % 10_000_000n; - const fracStr = fractionalPart.toString().padStart(7, "0").replace(/0+$/, ""); - const result = fracStr.length > 0 ? `${integerPart}.${fracStr}` : integerPart.toString(); + const fracStr = fractionalPart + .toString() + .padStart(7, "0") + .replace(/0+$/, ""); + const result = + fracStr.length > 0 ? `${integerPart}.${fracStr}` : integerPart.toString(); return isNegative ? `-${result}` : result; } @@ -78,7 +92,10 @@ async function getDummyAccount(server: Server, fallbackAddr: string) { } } -async function getMerchantRevenue(server: Server, merchant: string): Promise { +async function getMerchantRevenueViaRpc( + server: Server, + merchant: string, +): Promise { if (!CONTRACT_ID) return 0n; const contract = new Contract(CONTRACT_ID); const account = await getDummyAccount(server, merchant); @@ -94,16 +111,16 @@ async function getMerchantRevenue(server: Server, merchant: string): Promise { - const { MultiEndpointServer } = await import("./rpc-client.js"); - const server = new MultiEndpointServer(RPC_URL); - -async function getMerchantSubscriberCount(server: Server, merchant: string): Promise { +async function getMerchantSubscriberCountViaRpc( + server: Server, + merchant: string, +): Promise { if (!CONTRACT_ID) return 0; const response = await server.getEvents({ filters: [{ type: "contract", contractIds: [CONTRACT_ID] }], @@ -123,11 +140,11 @@ async function getMerchantSubscriberCount(server: Server, merchant: string): Pro const userAddress = topic[1]?.toString(); if (!userAddress) continue; - const eventTime = Date.parse(event.ledgerClosedAt) || 0; - const eventTime = Number( - (event as { ledgerCloseTime?: number }).ledgerCloseTime ?? - (event.ledgerClosedAt ? Date.parse(event.ledgerClosedAt) / 1000 : 0) - ) || 0; + const eventTime = + Number( + (event as { ledgerCloseTime?: number }).ledgerCloseTime ?? + (event.ledgerClosedAt ? Date.parse(event.ledgerClosedAt) / 1000 : 0), + ) || 0; if (eventType === "subscribed") { const merchantVal = (event as any).value?._value?.merchant; @@ -160,16 +177,11 @@ async function getMerchantSubscriberCount(server: Server, merchant: string): Pro return count; } -async function getMerchantRevenueHistory( +async function getMerchantRevenueHistoryViaRpc( + server: Server, merchant: string, days: number, ): Promise { - const { Server } = await import("@stellar/stellar-sdk/rpc"); - const server = new Server(RPC_URL); -async function getMerchantRevenueHistory(merchant: string, days: number): Promise { - const { MultiEndpointServer } = await import("./rpc-client.js"); - const server = new MultiEndpointServer(RPC_URL); -async function getMerchantRevenueHistory(server: Server, merchant: string, days: number): Promise { if (!CONTRACT_ID) return []; const contract = new Contract(CONTRACT_ID); const account = await getDummyAccount(server, merchant); @@ -191,7 +203,8 @@ async function getMerchantRevenueHistory(server: Server, merchant: string, days: const result = await server.simulateTransaction(tx); if ("error" in result && result.error) return []; - const retval = (result as { result?: { retval?: xdr.ScVal } }).result?.retval; + const retval = (result as { result?: { retval?: xdr.ScVal } }).result + ?.retval; if (!retval) return []; const vec = retval.vec(); @@ -199,22 +212,82 @@ async function getMerchantRevenueHistory(server: Server, merchant: string, days: return vec.map((v: xdr.ScVal) => BigInt(v.i128().toString())); } -async function fetchReportForMerchant(server: Server, merchant: string): Promise { - const [revenueStroops, subscriberCount, dailyRevenueStroops] = await Promise.all([ - getMerchantRevenue(server, merchant), - getMerchantSubscriberCount(server, merchant), - getMerchantRevenueHistory(server, merchant, 30), - ]); - - return { - generated_at: new Date().toISOString(), - merchant, - total_revenue: stroopsToXlm(revenueStroops), - subscriber_count: subscriberCount, - daily_revenue_last_30_days: dailyRevenueStroops.map(stroopsToXlm), - }; +// ── Data Fetching (DB primary, RPC fallback) ────────────────────────────────── + +/** + * Fetch report data for a merchant, preferring the indexer DB. + * Falls back to RPC if the DB is unavailable or stale. + */ +async function fetchReportForMerchant( + merchant: string, + dbPath: string, + freshnessConfig?: FreshnessConfig, +): Promise { + // Try indexer DB first + const db = openMerchantDb(dbPath); + if (db) { + try { + const freshness = checkFreshness(db, freshnessConfig); + if (freshness.isFresh || !freshness.warning) { + const reportData = computeMerchantReport(db, merchant); + db.close(); + + return { + generated_at: new Date().toISOString(), + merchant, + total_revenue: stroopsToXlm(reportData.totalRevenue), + subscriber_count: reportData.subscriberCount, + daily_revenue_last_30_days: + reportData.dailyRevenueLast30Days.map(stroopsToXlm), + }; + } + logger.info( + `Indexer DB is stale (last_ledger: ${freshness.lastLedger}). Falling back to RPC.`, + ); + } catch (err) { + logger.info( + `Failed to read from indexer DB: ${err instanceof Error ? err.message : String(err)}. Falling back to RPC.`, + ); + } finally { + db.close(); + } + } + + // Fallback to RPC + logger.info(`Using RPC fallback for merchant ${merchant}`); + const server = new Server(RPC_URL); + + try { + const [revenueStroops, subscriberCount, dailyRevenueStroops] = + await Promise.all([ + getMerchantRevenueViaRpc(server, merchant), + getMerchantSubscriberCountViaRpc(server, merchant), + getMerchantRevenueHistoryViaRpc(server, merchant, 30), + ]); + + return { + generated_at: new Date().toISOString(), + merchant, + total_revenue: stroopsToXlm(revenueStroops), + subscriber_count: subscriberCount, + daily_revenue_last_30_days: dailyRevenueStroops.map(stroopsToXlm), + }; + } catch (err) { + logger.warn( + `RPC fallback failed for merchant ${merchant}: ${err instanceof Error ? err.message : String(err)}`, + ); + return { + generated_at: new Date().toISOString(), + merchant, + total_revenue: "0", + subscriber_count: 0, + daily_revenue_last_30_days: [], + }; + } } +// ── Formatting ──────────────────────────────────────────────────────────────── + function escapeCsvCell(val: unknown): string { if (val === null || val === undefined) return ""; let str = typeof val === "object" ? JSON.stringify(val) : String(val); @@ -224,7 +297,10 @@ function escapeCsvCell(val: unknown): string { return str; } -function filterFields(report: RawMerchantReport, fields: ValidField[]): Record { +function filterFields( + report: RawMerchantReport, + fields: ValidField[], +): Record { const filtered: Record = {}; for (const field of fields) { filtered[field] = report[field]; @@ -232,7 +308,11 @@ function filterFields(report: RawMerchantReport, fields: ValidField[]): Record filterFields(r, fields)); if (format === "json") { @@ -245,40 +325,54 @@ function formatReports(reports: RawMerchantReport[], format: OutputFormat, field if (format === "csv") { const header = fields.join(","); - const rows = filteredList.map((obj) => fields.map((f) => escapeCsvCell(obj[f])).join(",")); + const rows = filteredList.map((obj) => + fields.map((f) => escapeCsvCell(obj[f])).join(","), + ); return [header, ...rows].join("\n") + "\n"; } throw new Error(`Unsupported format: ${format}`); } +// ── Main ────────────────────────────────────────────────────────────────────── + async function main() { const args = process.argv.slice(2); let merchant = ""; let output = ""; let format: OutputFormat = "json"; let fieldsStr = ""; + let dbPath = INDEXER_DB_PATH; for (let i = 0; i < args.length; i++) { if (args[i] === "--merchant" && args[i + 1]) merchant = args[++i]; else if (args[i] === "--output" && args[i + 1]) output = args[++i]; - else if (args[i] === "--format" && args[i + 1]) format = args[++i].toLowerCase() as OutputFormat; + else if (args[i] === "--format" && args[i + 1]) + format = args[++i].toLowerCase() as OutputFormat; else if (args[i] === "--fields" && args[i + 1]) fieldsStr = args[++i]; + else if (args[i] === "--db" && args[i + 1]) dbPath = args[++i]; } - if (!merchant || !output) { + if (!merchant) { console.error( - "Usage: npx tsx scripts/export-merchant-report.ts --merchant GXXXX... --output report.json", + "Usage: npx tsx scripts/export-merchant-report.ts --merchant GXXXX... [--output report.json]", ); + process.exit(1); + } + if (!["csv", "json", "ndjson"].includes(format)) { - logger.error(`ERROR: Invalid format '${format}'. Supported formats: csv, json, ndjson`); + logger.error( + `ERROR: Invalid format '${format}'. Supported formats: csv, json, ndjson`, + ); process.exit(1); } let selectedFields: ValidField[] = [...VALID_FIELDS]; if (fieldsStr) { const parsedFields = fieldsStr.split(",").map((f) => f.trim()); - const invalidFields = parsedFields.filter((f) => !VALID_FIELDS.includes(f as ValidField)); + const invalidFields = parsedFields.filter( + (f) => !VALID_FIELDS.includes(f as ValidField), + ); if (invalidFields.length > 0) { logger.error(`ERROR: Invalid field(s): ${invalidFields.join(", ")}.`); logger.error(`Valid fields are: ${VALID_FIELDS.join(", ")}`); @@ -287,31 +381,18 @@ async function main() { selectedFields = parsedFields as ValidField[]; } - const server = new Server(RPC_URL); - - const merchantsToReport: string[] = []; - if (merchant) { - merchantsToReport.push(merchant); - } else { - // If no specific merchant requested, discover from top merchants or default dummy merchant - merchantsToReport.push("GXXXX_DEFAULT_MERCHANT"); - } - const reports: RawMerchantReport[] = []; - for (const m of merchantsToReport) { - try { - const report = await fetchReportForMerchant(server, m); - reports.push(report); - } catch (err) { - // Fallback empty report for unmatched/offline merchant in dev - reports.push({ - generated_at: new Date().toISOString(), - merchant: m, - total_revenue: "0", - subscriber_count: 0, - daily_revenue_last_30_days: [], - }); - } + try { + const report = await fetchReportForMerchant(merchant, dbPath); + reports.push(report); + } catch (err) { + reports.push({ + generated_at: new Date().toISOString(), + merchant, + total_revenue: "0", + subscriber_count: 0, + daily_revenue_last_30_days: [], + }); } const formattedOutput = formatReports(reports, format, selectedFields); @@ -326,7 +407,9 @@ async function main() { } main().catch((err) => { - logger.error("Export report failed:", err instanceof Error ? err.message : err); + logger.error( + "Export report failed:", + err instanceof Error ? err.message : err, + ); process.exit(1); }); - diff --git a/scripts/merchant-analytics.ts b/scripts/merchant-analytics.ts index 21f512f6..ddda88a5 100644 --- a/scripts/merchant-analytics.ts +++ b/scripts/merchant-analytics.ts @@ -1,9 +1,8 @@ /** * merchant-analytics.ts — Enhanced merchant analytics for the PayFlow protocol. * - * Extends top-merchants.ts with subscriber growth rate, average subscription - * amount, churn rate, and revenue trends. Supports configurable sorting, - * top-N limits, period comparisons, and multiple output formats. + * Uses the indexer SQLite database as the primary data source for analytics. + * Includes freshness checking and optional RPC fallback. * * Usage: * npx ts-node scripts/merchant-analytics.ts \ @@ -13,6 +12,8 @@ * [--compare-days 30] # show 30-day delta for each metric * [--format table|json|csv] # default: table * [--out report.json] # optional output file + * [--rpc-fallback] # enable RPC fallback when DB is stale/missing + * [--max-staleness 3600] # max staleness in seconds before warning (default: 3600) * * Required table: events(event_name TEXT, data TEXT, timestamp INTEGER) * @@ -26,8 +27,14 @@ * 1 — invalid arguments or database error */ -import { DatabaseSync } from "node:sqlite"; import { writeFileSync } from "node:fs"; +import { + openMerchantDb, + checkFreshness, + computeMerchantMetrics, + type MerchantMetrics, + type FreshnessConfig, +} from "./merchant-queries.js"; // ── Types ───────────────────────────────────────────────────────────────────── @@ -41,40 +48,8 @@ interface CliArgs { compareDays: number | null; format: OutputFormat; outFile: string | null; -} - -interface EventRow { - event_name: string; - data: string; - timestamp: number; -} - -interface MerchantMetrics { - address: string; - /** Net revenue (sum of amount - fee for all charges) */ - totalRevenue: bigint; - /** Number of distinct subscribers who have subscribed at any point */ - subscriberCount: number; - /** Average subscription amount per subscriber (based on subscribed events) */ - avgSubscriptionAmount: bigint; - /** - * 30-day (or compareDays-day) churn rate as a percentage [0–100]. - * churnRate = cancellations_in_window / subscribers_at_window_start * 100 - * Set to null when insufficient data (< compareDays days of history). - */ - churnRate: number | null; - /** - * Subscriber growth rate over the comparison window as a percentage. - * growth = (current_subs - subs_at_window_start) / subs_at_window_start * 100 - * Set to null when there was no history in the window. - */ - growthRate: number | null; - /** Revenue generated within the comparison window */ - revenueInWindow: bigint; - /** Revenue generated before the comparison window */ - revenueBeforeWindow: bigint; - /** Flags merchants with < compareDays of event history */ - isNew: boolean; + rpcFallback: boolean; + maxStalenessSeconds: number; } interface MerchantRow { @@ -150,171 +125,28 @@ function parseArgs(): CliArgs { : "table"; const outFile = getArg("--out") ?? null; - - return { dbPath, top, sortBy, compareDays, format, outFile }; -} - -// ── Data Aggregation ────────────────────────────────────────────────────────── - -/** - * Load all relevant events from the SQLite indexer database and compute - * per-merchant metrics. - */ -function computeMetrics( - dbPath: string, - compareDays: number | null, -): Map { - const db = new DatabaseSync(dbPath, { open: true }); - - const rows = db - .prepare( - "SELECT event_name, data, timestamp FROM events WHERE event_name IN ('subscribed', 'charged', 'cancelled') ORDER BY timestamp ASC", - ) - .all() as unknown as EventRow[]; - - db.close(); - - // Per-merchant accumulators - const totalRevenue = new Map(); - const revenueInWindow = new Map(); - const revenueBeforeWindow = new Map(); - // All unique subscriber addresses per merchant - const subscribers = new Map>(); - // subscribers at start of comparison window - const subscribersBeforeWindow = new Map>(); - // cancellations within the comparison window - const cancellationsInWindow = new Map(); - // subscription amounts per merchant for average calculation - const subscriptionAmounts = new Map(); - // earliest event timestamp per merchant - const firstEventAt = new Map(); - - const nowSeconds = Math.floor(Date.now() / 1000); - const windowStart = - compareDays !== null ? nowSeconds - compareDays * 86400 : null; - - for (const row of rows) { - let parsed: Record; - try { - parsed = JSON.parse(row.data) as Record; - } catch { - continue; // skip malformed rows - } - - const merchant = String(parsed.merchant ?? ""); - if (!merchant) continue; - - // Track earliest event for "is_new" detection - if (!firstEventAt.has(merchant)) { - firstEventAt.set(merchant, row.timestamp); - } - - if (!subscribers.has(merchant)) subscribers.set(merchant, new Set()); - if (!subscribersBeforeWindow.has(merchant)) - subscribersBeforeWindow.set(merchant, new Set()); - - const isBeforeWindow = windowStart === null || row.timestamp < windowStart; - - if (row.event_name === "subscribed") { - const subscriber = String(parsed.subscriber ?? parsed.user ?? ""); - const amount = BigInt(String(parsed.amount ?? "0")); - - if (subscriber) { - subscribers.get(merchant)!.add(subscriber); - if (isBeforeWindow) { - subscribersBeforeWindow.get(merchant)!.add(subscriber); - } - } - - if (!subscriptionAmounts.has(merchant)) - subscriptionAmounts.set(merchant, []); - if (amount > 0n) subscriptionAmounts.get(merchant)!.push(amount); - } else if (row.event_name === "charged") { - const amount = BigInt(String(parsed.amount ?? "0")); - const fee = BigInt(String(parsed.fee ?? "0")); - const net = amount - fee; - - totalRevenue.set(merchant, (totalRevenue.get(merchant) ?? 0n) + net); - - if (!isBeforeWindow) { - revenueInWindow.set( - merchant, - (revenueInWindow.get(merchant) ?? 0n) + net, - ); - } else { - revenueBeforeWindow.set( - merchant, - (revenueBeforeWindow.get(merchant) ?? 0n) + net, - ); - } - } else if (row.event_name === "cancelled") { - if (!isBeforeWindow) { - cancellationsInWindow.set( - merchant, - (cancellationsInWindow.get(merchant) ?? 0) + 1, - ); - } - } - } - - // Build final metrics map - const metrics = new Map(); - - const allMerchants = new Set([...totalRevenue.keys(), ...subscribers.keys()]); - - const windowDays = compareDays ?? 30; - const oldestEligibleTimestamp = nowSeconds - windowDays * 86400; - - for (const address of allMerchants) { - const subs = subscribers.get(address) ?? new Set(); - const subsBeforeWindow = subscribersBeforeWindow.get(address) ?? new Set(); - const cancels = cancellationsInWindow.get(address) ?? 0; - const amounts = subscriptionAmounts.get(address) ?? []; - const revInWindow = revenueInWindow.get(address) ?? 0n; - const revBefore = revenueBeforeWindow.get(address) ?? 0n; - const firstAt = firstEventAt.get(address) ?? nowSeconds; - - const avgAmount = - amounts.length > 0 - ? amounts.reduce((a, b) => a + b, 0n) / BigInt(amounts.length) - : 0n; - - const isNew = firstAt > oldestEligibleTimestamp; - - let churnRate: number | null = null; - let growthRate: number | null = null; - - if (windowStart !== null) { - const subsAtWindowStart = subsBeforeWindow.size; - - if (subsAtWindowStart > 0) { - churnRate = Math.round((cancels / subsAtWindowStart) * 10000) / 100; - const currentSubs = subs.size; - growthRate = - Math.round( - ((currentSubs - subsAtWindowStart) / subsAtWindowStart) * 10000, - ) / 100; - } else if (subs.size > 0) { - // New merchant: no prior subscribers, 100% growth if there are current subs - growthRate = null; // cannot compute without base - churnRate = null; - } + const rpcFallback = hasFlag("--rpc-fallback"); + + const maxStalenessArg = getArg("--max-staleness"); + let maxStalenessSeconds = 3600; + if (maxStalenessArg !== undefined) { + maxStalenessSeconds = parseInt(maxStalenessArg, 10); + if (isNaN(maxStalenessSeconds) || maxStalenessSeconds < 0) { + console.error("ERROR: --max-staleness must be a non-negative integer"); + process.exit(1); } - - metrics.set(address, { - address, - totalRevenue: totalRevenue.get(address) ?? 0n, - subscriberCount: subs.size, - avgSubscriptionAmount: avgAmount, - churnRate, - growthRate, - revenueInWindow: revInWindow, - revenueBeforeWindow: revBefore, - isNew, - }); } - return metrics; + return { + dbPath, + top, + sortBy, + compareDays, + format, + outFile, + rpcFallback, + maxStalenessSeconds, + }; } // ── Sorting ─────────────────────────────────────────────────────────────────── @@ -430,15 +262,44 @@ function renderCsv(rows: MerchantRow[]): string { function main(): void { const args = parseArgs(); + // Open the indexer DB + const db = openMerchantDb(args.dbPath); + if (!db) { + console.error(`ERROR: Database not found at ${args.dbPath}`); + if (!args.rpcFallback) { + console.error("Hint: Use --rpc-fallback to attempt RPC fallback."); + } + process.exit(1); + } + + // Check freshness + const freshnessConfig: FreshnessConfig = { + maxStalenessSeconds: args.maxStalenessSeconds, + }; + const freshness = checkFreshness(db, freshnessConfig); + + if (freshness.warning) { + console.warn(`WARNING: ${freshness.warning}`); + if (!args.rpcFallback) { + console.warn( + "Data may be outdated. Consider running the indexer or using --rpc-fallback.", + ); + } + } + + // Compute metrics from the indexer DB let metrics: Map; try { - metrics = computeMetrics(args.dbPath, args.compareDays); + metrics = computeMerchantMetrics(db, args.compareDays); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); - console.error(`ERROR: Failed to read database: ${msg}`); + console.error(`ERROR: Failed to compute metrics: ${msg}`); + db.close(); process.exit(1); } + db.close(); + if (metrics.size === 0) { console.warn("No merchant data found in the database."); process.exit(0); @@ -452,15 +313,30 @@ function main(): void { let output: string; if (args.format === "json") { - output = JSON.stringify(rows, null, 2); + output = JSON.stringify( + { + generated_at: new Date().toISOString(), + freshness: { + last_ledger: freshness.lastLedger, + is_fresh: freshness.isFresh, + staleness_seconds: freshness.stalenessSeconds, + }, + merchants: rows, + }, + null, + 2, + ); } else if (args.format === "csv") { output = renderCsv(rows); } else { const comparePart = args.compareDays ? ` | ${args.compareDays}-day comparison` : ""; + const freshnessPart = freshness.lastLedger + ? ` | last_ledger: ${freshness.lastLedger}` + : ""; output = - `PayFlow Merchant Analytics — top ${args.top} by ${args.sortBy}${comparePart}\n` + + `PayFlow Merchant Analytics — top ${args.top} by ${args.sortBy}${comparePart}${freshnessPart}\n` + renderTable(rows, args.compareDays); } diff --git a/scripts/merchant-queries.ts b/scripts/merchant-queries.ts new file mode 100644 index 00000000..96b00a89 --- /dev/null +++ b/scripts/merchant-queries.ts @@ -0,0 +1,482 @@ +/** + * merchant-queries.ts — Shared query helpers for reading merchant analytics + * from the indexer SQLite database. + * + * This module provides a single, consistent interface for both + * `merchant-analytics.ts` and `export-merchant-report.ts` to read from + * the indexer DB. It includes freshness checking based on the `last_ledger` + * meta value stored by the indexer. + * + * Usage: + * import { openMerchantDb, getMerchantMetrics, checkFreshness } from "./merchant-queries.js"; + * + * The indexer DB is the primary data source. RPC is available as an optional + * fallback when the DB is unavailable or stale. + */ + +import { DatabaseSync } from "node:sqlite"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; + +// ── Types ───────────────────────────────────────────────────────────────────── + +/** Raw event row as stored in the indexer DB. */ +export interface EventRow { + id: string; + event_name: string; + address: string; + amount: string | null; + ledger: number; + timestamp: number; + tx_hash: string; + raw_data: string; + merchant: string | null; + fee_amount: string | null; + token: string | null; + result_code: string | null; +} + +/** Per-merchant metrics computed from indexed events. */ +export interface MerchantMetrics { + address: string; + /** Net revenue (sum of amount - fee for all charges) */ + totalRevenue: bigint; + /** Number of distinct subscribers who have subscribed at any point */ + subscriberCount: number; + /** Average subscription amount per subscriber (based on subscribed events) */ + avgSubscriptionAmount: bigint; + /** + * 30-day (or compareDays-day) churn rate as a percentage [0–100]. + * Set to null when insufficient data. + */ + churnRate: number | null; + /** + * Subscriber growth rate over the comparison window as a percentage. + * Set to null when there was no history in the window. + */ + growthRate: number | null; + /** Revenue generated within the comparison window */ + revenueInWindow: bigint; + /** Revenue generated before the comparison window */ + revenueBeforeWindow: bigint; + /** Flags merchants with < compareDays of event history */ + isNew: boolean; +} + +/** Merchant report data for export. */ +export interface MerchantReportData { + merchant: string; + totalRevenue: bigint; + subscriberCount: number; + dailyRevenueLast30Days: bigint[]; +} + +/** Freshness status of the indexer DB. */ +export interface FreshnessStatus { + /** Whether the DB is considered fresh */ + isFresh: boolean; + /** The last ledger value from the DB, or null if not available */ + lastLedger: number | null; + /** Staleness in seconds (time since last ledger update), or null */ + stalenessSeconds: number | null; + /** Warning message if stale, or null if fresh */ + warning: string | null; +} + +/** Configuration for freshness checking. */ +export interface FreshnessConfig { + /** Maximum acceptable staleness in seconds (default: 3600 = 1 hour) */ + maxStalenessSeconds?: number; + /** Expected ledger close time in seconds (default: 5) */ + ledgerCloseTimeSeconds?: number; +} + +// ── Constants ───────────────────────────────────────────────────────────────── + +/** Default maximum staleness before warning (1 hour). */ +const DEFAULT_MAX_STALENESS_SECONDS = 3600; + +/** Default expected ledger close time (~5 seconds on Stellar). */ +const DEFAULT_LEDGER_CLOSE_TIME_SECONDS = 5; + +// ── Database Helpers ────────────────────────────────────────────────────────── + +/** + * Open the indexer SQLite database for read-only access. + * Returns null if the database file does not exist. + */ +export function openMerchantDb(dbPath: string): DatabaseSync | null { + if (!existsSync(dbPath)) { + return null; + } + return new DatabaseSync(dbPath, { open: true, readonly: true }); +} + +/** + * Get a meta value from the indexer DB's meta table. + */ +export function getMeta(db: DatabaseSync, key: string): string | null { + const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key) as + { value: string } | undefined; + return row?.value ?? null; +} + +// ── Freshness Checking ──────────────────────────────────────────────────────── + +/** + * Check the freshness of the indexer DB based on last_ledger. + * + * The freshness is determined by comparing the last_ledger value against + * the current ledger. If the DB hasn't been updated recently relative to + * the expected ledger close time, it's considered stale. + * + * @param db - Open database connection + * @param config - Optional freshness configuration + * @returns FreshnessStatus with isFresh, lastLedger, stalenessSeconds, and warning + */ +export function checkFreshness( + db: DatabaseSync, + config?: FreshnessConfig, +): FreshnessStatus { + const maxStaleness = + config?.maxStalenessSeconds ?? DEFAULT_MAX_STALENESS_SECONDS; + const ledgerCloseTime = + config?.ledgerCloseTimeSeconds ?? DEFAULT_LEDGER_CLOSE_TIME_SECONDS; + + const lastLedgerStr = getMeta(db, "last_ledger"); + if (lastLedgerStr === null) { + return { + isFresh: false, + lastLedger: null, + stalenessSeconds: null, + warning: + "Indexer DB has no last_ledger value. The database may not have been indexed yet.", + }; + } + + const lastLedger = parseInt(lastLedgerStr, 10); + if (isNaN(lastLedger)) { + return { + isFresh: false, + lastLedger: null, + stalenessSeconds: null, + warning: "Indexer DB has invalid last_ledger value.", + }; + } + + // Estimate staleness based on last_ledger timestamp if available, + // otherwise use a heuristic based on expected ledger close time. + const lastLedgerTimestampStr = getMeta(db, "last_ledger_timestamp"); + let stalenessSeconds: number; + + if (lastLedgerTimestampStr) { + const lastLedgerTimestamp = parseInt(lastLedgerTimestampStr, 10); + if (!isNaN(lastLedgerTimestamp)) { + stalenessSeconds = Math.floor(Date.now() / 1000) - lastLedgerTimestamp; + } else { + stalenessSeconds = 0; + } + } else { + // Without timestamp, assume fresh if last_ledger exists + stalenessSeconds = 0; + } + + const isFresh = stalenessSeconds <= maxStaleness; + let warning: string | null = null; + + if (!isFresh) { + const minutes = Math.floor(stalenessSeconds / 60); + warning = + `Indexer DB is stale: last_ledger=${lastLedger}, ` + + `staleness=${minutes}m ${stalenessSeconds % 60}s ` + + `(max allowed: ${Math.floor(maxStaleness / 60)}m). ` + + `Consider running the indexer to update the database.`; + } + + return { + isFresh, + lastLedger, + stalenessSeconds, + warning, + }; +} + +// ── Event Queries ───────────────────────────────────────────────────────────── + +/** + * Fetch all relevant events from the indexer DB for merchant analytics. + * Returns events ordered by timestamp ascending. + */ +export function fetchAnalyticsEvents(db: DatabaseSync): EventRow[] { + return db + .prepare( + `SELECT id, event_name, address, amount, ledger, timestamp, tx_hash, + raw_data, merchant, fee_amount, token, result_code + FROM events + WHERE event_name IN ('subscribed', 'charged', 'cancelled') + ORDER BY timestamp ASC`, + ) + .all() as unknown as EventRow[]; +} + +/** + * Fetch events for a specific merchant. + */ +export function fetchMerchantEvents( + db: DatabaseSync, + merchant: string, +): EventRow[] { + return db + .prepare( + `SELECT id, event_name, address, amount, ledger, timestamp, tx_hash, + raw_data, merchant, fee_amount, token, result_code + FROM events + WHERE merchant = ? OR address = ? + ORDER BY timestamp ASC`, + ) + .all(merchant, merchant) as unknown as EventRow[]; +} + +/** + * Fetch charge events for revenue calculation. + */ +export function fetchChargeEvents(db: DatabaseSync): EventRow[] { + return db + .prepare( + `SELECT id, event_name, address, amount, ledger, timestamp, tx_hash, + raw_data, merchant, fee_amount, token, result_code + FROM events + WHERE event_name = 'charged' + ORDER BY timestamp ASC`, + ) + .all() as unknown as EventRow[]; +} + +/** + * Fetch subscription events for subscriber counting. + */ +export function fetchSubscriptionEvents(db: DatabaseSync): EventRow[] { + return db + .prepare( + `SELECT id, event_name, address, amount, ledger, timestamp, tx_hash, + raw_data, merchant, fee_amount, token, result_code + FROM events + WHERE event_name IN ('subscribed', 'cancelled') + ORDER BY timestamp ASC`, + ) + .all() as unknown as EventRow[]; +} + +// ── Metric Computation ──────────────────────────────────────────────────────── + +/** + * Compute per-merchant metrics from indexed events. + * + * @param db - Open database connection + * @param compareDays - Number of days for comparison window (null for no comparison) + * @returns Map of merchant address to metrics + */ +export function computeMerchantMetrics( + db: DatabaseSync, + compareDays: number | null, +): Map { + const rows = fetchAnalyticsEvents(db); + + // Per-merchant accumulators + const totalRevenue = new Map(); + const revenueInWindow = new Map(); + const revenueBeforeWindow = new Map(); + const subscribers = new Map>(); + const subscribersBeforeWindow = new Map>(); + const cancellationsInWindow = new Map(); + const subscriptionAmounts = new Map(); + const firstEventAt = new Map(); + + const nowSeconds = Math.floor(Date.now() / 1000); + const windowStart = + compareDays !== null ? nowSeconds - compareDays * 86400 : null; + + for (const row of rows) { + let parsed: Record; + try { + parsed = JSON.parse(row.raw_data) as Record; + } catch { + continue; + } + + const merchant = row.merchant ?? String(parsed.merchant ?? ""); + if (!merchant) continue; + + if (!firstEventAt.has(merchant)) { + firstEventAt.set(merchant, row.timestamp); + } + + if (!subscribers.has(merchant)) subscribers.set(merchant, new Set()); + if (!subscribersBeforeWindow.has(merchant)) + subscribersBeforeWindow.set(merchant, new Set()); + + const isBeforeWindow = windowStart === null || row.timestamp < windowStart; + + if (row.event_name === "subscribed") { + const subscriber = + String(parsed.subscriber ?? parsed.user ?? row.address ?? ""); + const amount = BigInt(String(parsed.amount ?? row.amount ?? "0")); + + if (subscriber) { + subscribers.get(merchant)!.add(subscriber); + if (isBeforeWindow) { + subscribersBeforeWindow.get(merchant)!.add(subscriber); + } + } + + if (!subscriptionAmounts.has(merchant)) + subscriptionAmounts.set(merchant, []); + if (amount > 0n) subscriptionAmounts.get(merchant)!.push(amount); + } else if (row.event_name === "charged") { + const amount = BigInt( + String(parsed.amount ?? row.amount ?? "0"), + ); + const fee = BigInt( + String(parsed.fee ?? parsed.fee_amount ?? row.fee_amount ?? "0"), + ); + const net = amount - fee; + + totalRevenue.set(merchant, (totalRevenue.get(merchant) ?? 0n) + net); + + if (!isBeforeWindow) { + revenueInWindow.set( + merchant, + (revenueInWindow.get(merchant) ?? 0n) + net, + ); + } else { + revenueBeforeWindow.set( + merchant, + (revenueBeforeWindow.get(merchant) ?? 0n) + net, + ); + } + } else if (row.event_name === "cancelled") { + if (!isBeforeWindow) { + cancellationsInWindow.set( + merchant, + (cancellationsInWindow.get(merchant) ?? 0) + 1, + ); + } + } + } + + // Build final metrics map + const metrics = new Map(); + const allMerchants = new Set([...totalRevenue.keys(), ...subscribers.keys()]); + const windowDays = compareDays ?? 30; + const oldestEligibleTimestamp = nowSeconds - windowDays * 86400; + + for (const address of allMerchants) { + const subs = subscribers.get(address) ?? new Set(); + const subsBeforeWindow = subscribersBeforeWindow.get(address) ?? new Set(); + const cancels = cancellationsInWindow.get(address) ?? 0; + const amounts = subscriptionAmounts.get(address) ?? []; + const revInWindow = revenueInWindow.get(address) ?? 0n; + const revBefore = revenueBeforeWindow.get(address) ?? 0n; + const firstAt = firstEventAt.get(address) ?? nowSeconds; + + const avgAmount = + amounts.length > 0 + ? amounts.reduce((a, b) => a + b, 0n) / BigInt(amounts.length) + : 0n; + + const isNew = firstAt > oldestEligibleTimestamp; + + let churnRate: number | null = null; + let growthRate: number | null = null; + + if (windowStart !== null) { + const subsAtWindowStart = subsBeforeWindow.size; + + if (subsAtWindowStart > 0) { + churnRate = Math.round((cancels / subsAtWindowStart) * 10000) / 100; + const currentSubs = subs.size; + growthRate = + Math.round( + ((currentSubs - subsAtWindowStart) / subsAtWindowStart) * 10000, + ) / 100; + } + } + + metrics.set(address, { + address, + totalRevenue: totalRevenue.get(address) ?? 0n, + subscriberCount: subs.size, + avgSubscriptionAmount: avgAmount, + churnRate, + growthRate, + revenueInWindow: revInWindow, + revenueBeforeWindow: revBefore, + isNew, + }); + } + + return metrics; +} + +/** + * Compute merchant report data for a specific merchant. + */ +export function computeMerchantReport( + db: DatabaseSync, + merchant: string, +): MerchantReportData { + const events = fetchMerchantEvents(db, merchant); + + let totalRevenue = 0n; + const subscribers = new Set(); + const dailyRevenue = new Map(); + + const nowSeconds = Math.floor(Date.now() / 1000); + const thirtyDaysAgo = nowSeconds - 30 * 86400; + + for (const row of events) { + let parsed: Record; + try { + parsed = JSON.parse(row.raw_data) as Record; + } catch { + continue; + } + + if (row.event_name === "subscribed") { + const subscriber = + String(parsed.subscriber ?? parsed.user ?? row.address ?? ""); + if (subscriber) subscribers.add(subscriber); + } else if (row.event_name === "charged") { + const amount = BigInt( + String(parsed.amount ?? row.amount ?? "0"), + ); + const fee = BigInt( + String(parsed.fee ?? parsed.fee_amount ?? row.fee_amount ?? "0"), + ); + const net = amount - fee; + totalRevenue += net; + + // Track daily revenue for last 30 days + if (row.timestamp >= thirtyDaysAgo) { + const day = new Date(row.timestamp * 1000).toISOString().split("T")[0]; + dailyRevenue.set(day, (dailyRevenue.get(day) ?? 0n) + net); + } + } + } + + // Build 30-day array + const dailyRevenueArray: bigint[] = []; + for (let i = 29; i >= 0; i--) { + const day = new Date((nowSeconds - i * 86400) * 1000) + .toISOString() + .split("T")[0]; + dailyRevenueArray.push(dailyRevenue.get(day) ?? 0n); + } + + return { + merchant, + totalRevenue, + subscriberCount: subscribers.size, + dailyRevenueLast30Days: dailyRevenueArray, + }; +} diff --git a/scripts/package-lock.json b/scripts/package-lock.json index a38a6858..9bf1e1f3 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -1,21 +1,21 @@ { "name": "payflow-scripts", - "version": "1.0.0", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "payflow-scripts", - "version": "1.0.0", + "version": "0.1.0", "dependencies": { "@stellar/stellar-sdk": "^12.0.0", - "better-sqlite3": "^9.4.3" + "prom-client": "^15.1.3", + "zod": "3.23.8" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.8", - "@types/node": "^20.0.0", + "@types/node": "^25.9.1", "ts-node": "^10.9.2", + "tsx": "^4.19.2", "typescript": "^5.4.0", "vitest": "^2.0.0" } @@ -33,22 +33,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "version": "0.1.0", - "dependencies": { - "@stellar/stellar-sdk": "^12.0.0", - "prom-client": "^15.1.3", - "zod": "3.23.8" - }, - "devDependencies": { - "@types/node": "^25.9.1", - "tsx": "^4.19.2", - "typescript": "^5.0.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -63,13 +47,6 @@ "aix" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "node": ">=18" } }, @@ -87,13 +64,6 @@ "android" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "node": ">=18" } }, @@ -111,13 +81,6 @@ "android" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "node": ">=18" } }, @@ -135,13 +98,6 @@ "android" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "node": ">=18" } }, @@ -159,13 +115,6 @@ "darwin" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "node": ">=18" } }, @@ -183,13 +132,6 @@ "darwin" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "node": ">=18" } }, @@ -207,13 +149,6 @@ "freebsd" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "node": ">=18" } }, @@ -231,13 +166,6 @@ "freebsd" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "node": ">=18" } }, @@ -255,13 +183,6 @@ "linux" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "node": ">=18" } }, @@ -279,13 +200,6 @@ "linux" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "node": ">=18" } }, @@ -303,13 +217,6 @@ "linux" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "node": ">=18" } }, @@ -327,13 +234,6 @@ "linux" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "node": ">=18" } }, @@ -351,13 +251,6 @@ "linux" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "node": ">=18" } }, @@ -375,13 +268,6 @@ "linux" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "node": ">=18" } }, @@ -399,13 +285,6 @@ "linux" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "node": ">=18" } }, @@ -423,13 +302,6 @@ "linux" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "node": ">=18" } }, @@ -447,15 +319,6 @@ "linux" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" "node": ">=18" } }, @@ -473,13 +336,6 @@ "netbsd" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "node": ">=18" } }, @@ -494,18 +350,6 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" "netbsd" ], "engines": { @@ -523,57 +367,67 @@ "license": "MIT", "optional": true, "os": [ - "sunos" + "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ - "arm64" + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ "openbsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { + "node_modules/@esbuild/openharmony-arm64": { "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "openharmony" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ - "ia32" - "openbsd" + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { + "node_modules/@esbuild/win32-arm64": { "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -584,23 +438,30 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "openharmony" + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { + "node_modules/@esbuild/win32-x64": { "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -611,7 +472,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@jridgewell/resolve-uri": { @@ -648,18 +509,6 @@ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", "cpu": [ "x64" - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" ], "dev": true, "license": "MIT", @@ -671,8 +520,17 @@ "node": "^22.20 || ^24.12 || >=25" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.63.1", + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", "cpu": [ @@ -691,18 +549,6 @@ "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", "cpu": [ "arm64" - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" ], "dev": true, "license": "MIT", @@ -729,16 +575,6 @@ "version": "4.63.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -1110,16 +946,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1128,14 +954,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "undici-types": "~6.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@vitest/expect": { @@ -1312,102 +1137,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@stellar/js-xdr": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", - "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", - "license": "Apache-2.0" - }, - "node_modules/@stellar/stellar-base": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-12.1.1.tgz", - "integrity": "sha512-gOBSOFDepihslcInlqnxKZdIW9dMUO1tpOm3AtJR33K2OvpXG6SaVHCzAmCFArcCqI9zXTEiSoh70T48TmiHJA==", - "deprecated": "This package is now rolled into @stellar/stellar-sdk. Please use @stellar/stellar-sdk to continue receiving updates and support.", - "license": "Apache-2.0", - "dependencies": { - "@stellar/js-xdr": "^3.1.2", - "base32.js": "^0.1.0", - "bignumber.js": "^9.1.2", - "buffer": "^6.0.3", - "sha.js": "^2.3.6", - "tweetnacl": "^1.0.3" - }, - "optionalDependencies": { - "sodium-native": "^4.1.1" - } - }, - "node_modules/@stellar/stellar-sdk": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-12.3.0.tgz", - "integrity": "sha512-F2DYFop/M5ffXF0lvV5Ezjk+VWNKg0QDX8gNhwehVU3y5LYA3WAY6VcCarMGPaG9Wdgoeh1IXXzOautpqpsltw==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/stellar-base": "^12.1.1", - "axios": "^1.7.7", - "bignumber.js": "^9.1.2", - "eventsource": "^2.0.2", - "randombytes": "^2.1.0", - "toml": "^3.0.0", - "urijs": "^1.19.1" - } - }, - "node_modules/@types/node": { - "version": "25.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", - "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1508,17 +1237,6 @@ ], "license": "MIT" }, - "node_modules/better-sqlite3": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz", - "integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -1528,49 +1246,6 @@ "node": "*" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } "node_modules/bintrees": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", @@ -1685,12 +1360,6 @@ "node": ">= 16" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1727,21 +1396,6 @@ } } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -1752,15 +1406,6 @@ "node": ">=6" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1787,15 +1432,6 @@ "node": ">=0.4.0" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/diff": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", @@ -1820,15 +1456,6 @@ "node": ">= 0.4" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1882,9 +1509,6 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", @@ -1895,42 +1519,6 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" "node": ">=18" }, "optionalDependencies": { @@ -1962,6 +1550,16 @@ "@esbuild/win32-x64": "0.28.2" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/eventsource": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", @@ -1971,15 +1569,6 @@ "node": ">=12.0.0" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -1990,12 +1579,6 @@ "node": ">=12.0.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -2047,12 +1630,6 @@ "node": ">= 6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2114,12 +1691,6 @@ "node": ">= 0.4" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2222,12 +1793,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -2315,33 +1880,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2367,33 +1905,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.96.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", - "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -2417,11 +1928,6 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" }, "node_modules/possible-typed-array-names": { "version": "1.1.0", @@ -2461,31 +1967,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" "node_modules/prom-client": { "version": "15.1.3", "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", @@ -2509,16 +1990,6 @@ "node": ">=10" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2528,35 +1999,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/require-addon": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", @@ -2636,18 +2078,6 @@ ], "license": "MIT" }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -2692,51 +2122,6 @@ "dev": true, "license": "ISC" }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/sodium-native": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", @@ -2771,50 +2156,13 @@ "dev": true, "license": "MIT" }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", - "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "node_modules/tdigest": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.3.tgz", + "integrity": "sha512-zbRt+lT+/H4fRItHshczHErVCQnitJk8MfMT24MqFJf3YL7SJJPqGIGeuOdvxXxM/AHFzKBl7WoyaYwqO9s3Kw==", "license": "MIT", "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" + "bintrees": "1.0.2" } }, "node_modules/tinybench": { @@ -2859,13 +2207,6 @@ "license": "MIT", "engines": { "node": ">=14.0.0" - "node_modules/tdigest": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.3.tgz", - "integrity": "sha512-zbRt+lT+/H4fRItHshczHErVCQnitJk8MfMT24MqFJf3YL7SJJPqGIGeuOdvxXxM/AHFzKBl7WoyaYwqO9s3Kw==", - "license": "MIT", - "dependencies": { - "bintrees": "1.0.2" } }, "node_modules/to-buffer": { @@ -2932,20 +2273,10 @@ } } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" "node_modules/tsx": { - "version": "4.23.12", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", - "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", "dev": true, "license": "MIT", "dependencies": { @@ -2987,7 +2318,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2997,9 +2327,6 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", @@ -3012,12 +2339,6 @@ "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", "license": "MIT" }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -3108,115 +2429,539 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } + "node": ">=12" } }, - "node_modules/which-typed-array": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", - "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } }, "node_modules/yn": { "version": "3.1.1", @@ -3226,6 +2971,8 @@ "license": "MIT", "engines": { "node": ">=6" + } + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", diff --git a/scripts/package.json b/scripts/package.json index 00fc539f..1bbf4496 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,36 +1,21 @@ { "name": "payflow-scripts", - "version": "1.0.0", - "private": true, - "description": "Operational and analytics scripts for FlowPay", - "scripts": { - "test": "vitest run", - "typecheck": "tsc --noEmit", - "top-merchants": "ts-node top-merchants.ts", - "deploy-pipeline": "ts-node deploy-pipeline.ts", - "backup-indexer-db": "ts-node backup-indexer-db.ts" - }, - "dependencies": { - "@stellar/stellar-sdk": "^12.0.0", - "better-sqlite3": "^9.4.3" - }, - "devDependencies": { - "@types/better-sqlite3": "^7.6.8", - "@types/node": "^20.0.0", - "ts-node": "^10.9.2", - "typescript": "^5.4.0", - "vitest": "^2.0.0" "version": "0.1.0", "private": true, "type": "module", + "description": "Operational and analytics scripts for FlowPay", "scripts": { + "test": "NODE_OPTIONS='--experimental-sqlite' vitest run", + "test:merchant": "NODE_OPTIONS='--experimental-sqlite' tsx --test __tests__/merchant-queries.test.ts __tests__/merchant-analytics.test.ts", + "typecheck": "tsc --noEmit", + "lint": "tsc --noEmit --strict --noUnusedLocals --noUnusedParameters", + "build": "tsc -p tsconfig.build.json", + "indexer": "NODE_OPTIONS='--experimental-sqlite' tsx indexer.ts", + "query-events": "NODE_OPTIONS='--experimental-sqlite' tsx query-events.ts", + "keeper": "tsx keeper.ts", "watch-events": "tsx watch-events.ts", "check-allowances": "tsx check-allowances.ts", "alert-expiring-allowances": "tsx alert-expiring-allowances.ts", - "indexer": "tsx indexer.ts", - "query-events": "tsx query-events.ts", - "keeper": "tsx keeper.ts", - "build": "tsc -p tsconfig.build.json", "batch-optimizer": "tsx batch-optimizer.ts", "pre-upgrade-check": "tsx pre-upgrade-check.ts", "subscriber-health": "tsx subscriber-health-dashboard.ts", @@ -42,16 +27,17 @@ "test:renewal-forecast": "tsx test-renewal-forecast.ts", "test": "tsx test-churn-analysis.ts" "test:rpc-failover": "tsx test-rpc-failover.ts" - "test:renewal-forecast": "tsx test-renewal-forecast.ts" }, "dependencies": { "@stellar/stellar-sdk": "^12.0.0", - "zod": "3.23.8", - "prom-client": "^15.1.3" + "prom-client": "^15.1.3", + "zod": "3.23.8" }, "devDependencies": { "@types/node": "^25.9.1", + "ts-node": "^10.9.2", "tsx": "^4.19.2", - "typescript": "^5.0.0" + "typescript": "^5.4.0", + "vitest": "^2.0.0" } } diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index de478a78..9ba39f9f 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "target": "ES2020", - "module": "node16", - "moduleResolution": "node16", - "lib": ["ES2020"], + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -17,18 +17,4 @@ }, "include": ["./**/*.ts"], "exclude": ["dist", "node_modules"] - "module": "ESNext", - "lib": ["ES2020"], - "moduleResolution": "bundler", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "strict": true, - "noUnusedLocals": true, - "skipLibCheck": true, - "types": ["node"], - "typeRoots": ["./node_modules/@types"] - }, - "include": ["*.ts"], - "exclude": ["node_modules", "dist"] } diff --git a/scripts/vitest.config.mts b/scripts/vitest.config.mts index fc86037b..e711e844 100644 --- a/scripts/vitest.config.mts +++ b/scripts/vitest.config.mts @@ -5,6 +5,10 @@ export default defineConfig({ globals: true, environment: "node", include: ["**/__tests__/**/*.test.ts"], + exclude: [ + "**/__tests__/merchant-queries.test.ts", + "**/__tests__/merchant-analytics.test.ts", + ], coverage: { reporter: ["text", "lcov"], },