diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c6dcdcc..309c8ae2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,8 +19,7 @@ lens/ │ ├── db.ts # Postgres pool / Prisma client │ ├── api/ # REST + GraphQL route implementations │ ├── routes/ # Additional REST route modules -│ ├── ingesters/ # SDEX + AMM ingester loops -│ ├── ingest/ # Venue/oracle adapters (Horizon AMM, Aquarius, Reflector) +│ ├── ingest/ # Venue/oracle adapters + ingester loops (SDEX, Horizon AMM, Soroswap, Aquarius, Reflector) │ ├── publish/ # SEP-40 publisher: encoding, publication policy, on-chain feed │ ├── aggregator/ # VWAP + best-route calculation │ ├── pricing/ # TWAP, depth and aggregate helpers diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 6b07abba..4c14b6d0 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -705,10 +705,60 @@ export interface paths { patch?: never; trace?: never; }; + "/rates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Query latest lending and borrow rates across pools + * @description Returns the latest supply and borrow rates, utilisation, and reserves for lending markets. + */ + get: operations["getLendingRates"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { schemas: { + LendingRateEntry: { + id: string; + /** @example blend */ + venue: string; + /** @example blend */ + source: string; + /** @example mainnet */ + network: string; + poolId: string; + poolName?: string | null; + /** @example USDC */ + asset: string; + assetAddress?: string | null; + supplyApy: number; + borrowApy: number; + supplyApr?: number | null; + borrowApr?: number | null; + /** @description Borrowed / supplied as a fraction. Null for venues that publish rates only. */ + utilization?: number | null; + totalSupply?: number | null; + totalBorrowed?: number | null; + reserves?: number | null; + ledger?: number; + /** Format: date-time */ + timestamp: string; + }; + LendingRatesResponse: { + network: string; + rates: components["schemas"]["LendingRateEntry"][]; + }; InternalErrorResponse: { /** * @example internal_error @@ -1456,6 +1506,18 @@ export interface components { }; }; responses: { + /** @description Bad request — a parameter failed validation. */ + BadRequest: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error?: string; + code?: string; + }; + }; + }; /** @description Payment Required */ PaymentRequired: { headers: { @@ -3118,4 +3180,35 @@ export interface operations { 402: components["responses"]["PaymentRequired"]; }; }; + getLendingRates: { + parameters: { + query?: { + /** @description Filter by asset symbol (e.g. USDC, XLM) */ + asset?: string; + /** @description Filter by pool contract ID */ + poolId?: string; + /** @description Stellar network to query (testnet or mainnet) */ + network?: "testnet" | "mainnet"; + /** @description Maximum number of rate records to return */ + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Latest lending rates */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LendingRatesResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 429: components["responses"]["TooManyRequests"]; + }; + }; } diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index 51ec58ff..f2ee5778 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -21,7 +21,7 @@ worker, ingester or scheduler process: | HTTP API | REST routes, `/graphql` + GraphiQL, webhooks, x402 facilitator, admin/usage routes | `buildApp()` in `src/app.ts`, routes under `src/api/` and `src/routes/` | | WebSocket server | `graphql-transport-ws` subscriptions on `/graphql`; local fan-out plus cross-instance fan-out over Redis pub/sub | `src/ws/fanout.ts`, `src/ws/` | | Venue ingesters | One infinite poll loop per (venue × enabled network) for **SDEX**, **Horizon AMM**, **Soroswap**, **Aquarius** | registry `src/ingest/venues/index.ts`, started in `main()` (`src/index.ts`) via `startVenueAdapter` (`src/ingest/venues/runner.ts`) | -| Snapshot ingester | Appends one `price_snapshots` row per active pair at every minute boundary, per network | `src/ingesters/snapshot.ts` | +| Snapshot ingester | Appends one `price_snapshots` row per active pair at every minute boundary, per network | `src/ingest/snapshot.ts` | | BullMQ workers | `aggregate-refresh` (one queue/worker per network), `snapshot-retention` (hourly), `price_points` partition maintenance (hourly) — **in the same process** | `src/jobs/aggregateRefresh.ts`, `src/jobs/snapshotRetention.ts`, `src/jobs/pricePointsPartitionJob.ts` | | Webhook dispatcher | Threshold alerts, delivered inline after each ingested price write | `src/webhookDispatcher.ts`, `src/alerts/delivery.ts` | | Schema migration | `prisma migrate deploy` runs **at every boot, before the API listens** — committed migrations only, never a diff of the live schema (#233). On a database that predates the migration history the baseline is recorded as applied first, without running it | `applyMigrations()` in `src/db/migrate.ts`, called from `main()` (`src/index.ts`) — doc: [`docs/database-migrations.md`](database-migrations.md) | diff --git a/docs/ingest-metrics.md b/docs/ingest-metrics.md index 5ec5eedf..50fdbe84 100644 --- a/docs/ingest-metrics.md +++ b/docs/ingest-metrics.md @@ -37,7 +37,7 @@ queryable at all. `last_trade_timestamp` was worse. It is a `Gauge.set()`, not an increment, so whichever loop ticked last won. The testnet loop at -[`src/ingesters/sdex.ts`](../src/ingesters/sdex.ts) overwrote mainnet's value +[`src/ingest/venues/sdex.ts`](../src/ingest/venues/sdex.ts) overwrote mainnet's value for the same pair every poll interval, and the series reported a timestamp belonging to exactly one network while looking network-agnostic. That is the only staleness signal Lens exports, and on a dual-network deployment it was diff --git a/openapi.json b/openapi.json index 0c401f5e..bfe85685 100644 --- a/openapi.json +++ b/openapi.json @@ -2904,6 +2904,24 @@ } }, "responses": { + "BadRequest": { + "description": "Bad request — a parameter failed validation.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "code": { + "type": "string" + } + } + } + } + } + }, "PaymentRequired": { "description": "Payment Required", "content": { diff --git a/openapi.yaml b/openapi.yaml index 72d5778b..ef7b96b6 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1974,6 +1974,17 @@ components: default: testnet responses: + BadRequest: + description: Bad request — a parameter failed validation. + content: + application/json: + schema: + type: object + properties: + error: + type: string + code: + type: string PaymentRequired: description: Payment Required content: diff --git a/package-lock.json b/package-lock.json index cfe90959..c3aa959c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1256,114 +1256,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@redocly/ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js-replace": "^1.0.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@redocly/config": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", - "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@redocly/openapi-core": { - "version": "1.34.21", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.21.tgz", - "integrity": "sha512-yXMiBFYJbP5JOaxPzx2tJhK+v8h2Wu2uhad1Ro6GzVT8E3F4GcQ7sbLPJ9w4zvGjfxroD0bIh5B0pDtnLV/qUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@redocly/ajv": "8.11.2", - "@redocly/config": "0.22.0", - "colorette": "1.4.0", - "https-proxy-agent": "7.0.6", - "js-levenshtein": "1.1.6", - "js-yaml": "4.3.2", - "minimatch": "5.1.9", - "pluralize": "8.0.0", - "yaml-ast-parser": "0.0.43" - }, - "engines": { - "node": ">=18.17.0", - "npm": ">=9.5.0" - } - }, - "node_modules/@redocly/openapi-core/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/@redocly/openapi-core/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@redocly/openapi-core/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@redocly/openapi-core/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", "license": "MIT", "engines": { "node": ">=8" @@ -1701,6 +1597,106 @@ "@prisma/debug": "5.22.0" } }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.21", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.21.tgz", + "integrity": "sha512-yXMiBFYJbP5JOaxPzx2tJhK+v8h2Wu2uhad1Ro6GzVT8E3F4GcQ7sbLPJ9w4zvGjfxroD0bIh5B0pDtnLV/qUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.3.2", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@redocly/openapi-core/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.16", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", diff --git a/src/__tests__/ammIngester.test.ts b/src/__tests__/ammIngester.test.ts index 22ca7cd9..f4a31b01 100644 --- a/src/__tests__/ammIngester.test.ts +++ b/src/__tests__/ammIngester.test.ts @@ -15,7 +15,7 @@ vi.mock('../webhookDispatcher', () => ({ })) // ── Imports ────────────────────────────────────────────────────────────────── -import { snapshotPool, ingestPoolTrades } from '../ingesters/amm' +import { snapshotPool, ingestPoolTrades } from '../ingest/venues/amm' import { prisma, upsertPricePoints, getIndexerState, setIndexerCursor } from '../db' import { dispatchPriceUpdate } from '../webhookDispatcher' @@ -66,7 +66,7 @@ describe('AMM Ingester', () => { getIndexerCursor: vi.fn(), setIndexerCursor: vi.fn(), })) - const { snapshotPool: snapshotOnMainnet } = await import('../ingesters/amm') + const { snapshotPool: snapshotOnMainnet } = await import('../ingest/venues/amm') await snapshotOnMainnet({ id: 'pool-mainnet', diff --git a/src/__tests__/auth.test.ts b/src/__tests__/auth.test.ts index ea3237c4..1f816445 100644 --- a/src/__tests__/auth.test.ts +++ b/src/__tests__/auth.test.ts @@ -82,6 +82,13 @@ beforeEach(() => { mockFindMany.mockReset() mockCreate.mockReset() mockUpdate.mockReset() + // webhookDeliveryFailure mocks are shared across the admin-endpoint tests; + // reset them too, or a prior test's call count / mockResolvedValue leaks into + // the next one under shuffled order (the "recent webhook failure endpoint" + // test then sees findMany already called and fails). + vi.mocked(prisma.webhookDeliveryFailure.findMany).mockReset() + vi.mocked(prisma.webhookDeliveryFailure.create).mockReset() + vi.mocked(prisma.webhookDeliveryFailure.deleteMany).mockReset() // The lookup cache is module-global; a stale entry from a previous test // would otherwise mask a different mock for the same key. clearApiKeyCache() diff --git a/src/__tests__/dualNetworkAlerts.test.ts b/src/__tests__/dualNetworkAlerts.test.ts index 34977b1e..1f43065b 100644 --- a/src/__tests__/dualNetworkAlerts.test.ts +++ b/src/__tests__/dualNetworkAlerts.test.ts @@ -43,9 +43,9 @@ vi.mock('../network/clients', () => ({ }), })) -import { ingestPair as ingestSdexPair, _getLastPrice as getSdexLastPrice, _resetLastPrice as resetSdexLastPrice } from '../ingesters/sdex' -import { snapshotPool, ingestPoolTrades, _getLastPrice as getAmmLastPrice, _resetLastPrice as resetAmmLastPrice } from '../ingesters/amm' -import { ingestPool as ingestSoroswapPool, _getLastPrice as getSoroswapLastPrice, _resetLastPrice as resetSoroswapLastPrice } from '../ingesters/soroswap' +import { ingestPair as ingestSdexPair, _getLastPrice as getSdexLastPrice, _resetLastPrice as resetSdexLastPrice } from '../ingest/venues/sdex' +import { snapshotPool, ingestPoolTrades, _getLastPrice as getAmmLastPrice, _resetLastPrice as resetAmmLastPrice } from '../ingest/venues/amm' +import { ingestPool as ingestSoroswapPool, _getLastPrice as getSoroswapLastPrice, _resetLastPrice as resetSoroswapLastPrice } from '../ingest/venues/soroswap' import { ingestAquariusPair, _getLastPrice as getAquariusLastPrice, _resetLastPrice as resetAquariusLastPrice } from '../ingest/venues/aquarius' import { dispatchPriceUpdate } from '../webhookDispatcher' import type { WatchedPair } from '../types' diff --git a/src/__tests__/sdexIngester.test.ts b/src/__tests__/sdexIngester.test.ts index 9b9b32d8..1aa24c8c 100644 --- a/src/__tests__/sdexIngester.test.ts +++ b/src/__tests__/sdexIngester.test.ts @@ -38,7 +38,7 @@ vi.mock('@stellar/stellar-sdk', async (importOriginal) => { }) // ── Imports ────────────────────────────────────────────────────────────────── -import { ingestPair } from '../ingesters/sdex' +import { ingestPair } from '../ingest/venues/sdex' describe('SDEX Ingester', () => { const mockPair = { diff --git a/src/__tests__/snapshotIngester.test.ts b/src/__tests__/snapshotIngester.test.ts index e3ff1688..bfe42b4d 100644 --- a/src/__tests__/snapshotIngester.test.ts +++ b/src/__tests__/snapshotIngester.test.ts @@ -14,7 +14,7 @@ vi.mock('../pairsRegistry', () => ({ getActivePairs: mockGetActivePairs, })) -import { appendSnapshots, floorToMinute } from '../ingesters/snapshot' +import { appendSnapshots, floorToMinute } from '../ingest/snapshot' describe('floorToMinute', () => { it('zeros seconds and milliseconds', () => { diff --git a/src/__tests__/soroswapEnabled.test.ts b/src/__tests__/soroswapEnabled.test.ts index 14edb9bf..4ab3a901 100644 --- a/src/__tests__/soroswapEnabled.test.ts +++ b/src/__tests__/soroswapEnabled.test.ts @@ -32,7 +32,7 @@ vi.mock('../pairsRegistry', () => mocks.pairsRegistry) vi.mock('../db', () => ({ upsertPricePoints: vi.fn().mockResolvedValue(undefined) })) vi.mock('../webhookDispatcher', () => ({ dispatchPriceUpdate: vi.fn().mockResolvedValue(undefined) })) -import { startSoroswapIngester } from '../ingesters/soroswap' +import { startSoroswapIngester } from '../ingest/venues/soroswap' describe('startSoroswapIngester', () => { beforeEach(() => { diff --git a/src/__tests__/soroswapIngester.test.ts b/src/__tests__/soroswapIngester.test.ts index 0ea2ffca..d7c2c037 100644 --- a/src/__tests__/soroswapIngester.test.ts +++ b/src/__tests__/soroswapIngester.test.ts @@ -30,8 +30,10 @@ vi.mock('../webhookDispatcher', () => ({ dispatchPriceUpdate: vi.fn().mockResolvedValue(undefined), })) -vi.mock('../ingesters/soroswap', async (importOriginal) => { - const actual = await importOriginal() +// We mock fetchPoolReserves and fetchPoolsFromFactory at module level so +// ingestPool / ingestPair tests can control RPC responses without a live node. +vi.mock('../ingest/venues/soroswap', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, fetchPoolReserves: vi.fn(), @@ -56,7 +58,7 @@ import { isPricePlausible, type PoolEntry, type SoroswapToken, -} from '../ingesters/soroswap' +} from '../ingest/venues/soroswap' import { upsertPricePoints } from '../db' import { dispatchPriceUpdate } from '../webhookDispatcher' import { price_rejected_total } from '../metrics' diff --git a/src/__tests__/toid.test.ts b/src/__tests__/toid.test.ts index 657aaf1d..2b1dfc18 100644 --- a/src/__tests__/toid.test.ts +++ b/src/__tests__/toid.test.ts @@ -1,4 +1,4 @@ -import { ledgerFromPagingToken, resolvePageLedgers } from '../ingesters/toid' +import { ledgerFromPagingToken, resolvePageLedgers } from '../ingest/toid' describe('ledgerFromPagingToken', () => { it('derives the ledger from real Horizon trade ids', () => { diff --git a/src/db.ts b/src/db.ts index c74ff322..ea696da6 100644 --- a/src/db.ts +++ b/src/db.ts @@ -268,7 +268,7 @@ export function synthesizeEventId( /** * The cursor and the ledger it reached, read in one round trip. Ingesters need * both: the cursor resumes pagination, and the ledger is the fallback when a - * trade's TOID can't be parsed (see ingesters/toid.ts). + * trade's TOID can't be parsed (see ingest/toid.ts). */ export interface IndexerState { cursor: string | null diff --git a/src/index.ts b/src/index.ts index 7d4bb162..b5c37672 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { buildApp } from './app' import { fanOutManager } from './ws/fanout' import { venueAdapters, startVenueAdapter, startBlendIngester } from './ingest/venues' -import { startSnapshotIngester } from './ingesters/snapshot' +import { startSnapshotIngester } from './ingest/snapshot' import { createAggregateQueue, startAggregateWorker, scheduleAggregateRefresh } from './jobs/aggregateRefresh' import { createSnapshotRetentionQueue, startSnapshotRetentionWorker, scheduleSnapshotRetention, pruneAllNetworks, SNAPSHOT_RETENTION_DAYS } from './jobs/snapshotRetention' import { createRateRetentionQueue, startRateRetentionWorker, scheduleRateRetention, pruneAllRateNetworks, RATE_RETENTION_DAYS } from './jobs/lendingRateRetention' diff --git a/src/ingesters/snapshot.ts b/src/ingest/snapshot.ts similarity index 100% rename from src/ingesters/snapshot.ts rename to src/ingest/snapshot.ts diff --git a/src/ingesters/toid.ts b/src/ingest/toid.ts similarity index 100% rename from src/ingesters/toid.ts rename to src/ingest/toid.ts diff --git a/src/ingest/venues/amm.ts b/src/ingest/venues/amm.ts index ac1f79c5..189f5753 100644 --- a/src/ingest/venues/amm.ts +++ b/src/ingest/venues/amm.ts @@ -1,7 +1,7 @@ import { amm_snapshots_total, trades_ingested_total, last_trade_timestamp, markIngestSuccess } from '../../metrics' import { config, activeNetwork, getNetworkConfig, type NetworkName } from '../../config' import { upsertPricePoints, getIndexerState, setIndexerCursor, prisma } from '../../db' -import { resolvePageLedgers } from '../../ingesters/toid' +import { resolvePageLedgers } from '../toid' import { dispatchPriceUpdate } from '../../webhookDispatcher' import { publishPriceUpdate } from '../../events' import { createLogger } from '../../logger' diff --git a/src/ingest/venues/sdex.ts b/src/ingest/venues/sdex.ts index 8f2bd787..6c215303 100644 --- a/src/ingest/venues/sdex.ts +++ b/src/ingest/venues/sdex.ts @@ -3,7 +3,7 @@ import { trades_ingested_total, last_trade_timestamp, markIngestSuccess } from ' import { config, activeNetwork, type NetworkName } from '../../config' import { getHorizonServer } from '../../network/clients' import { upsertPricePoints, getIndexerState, setIndexerCursor } from '../../db' -import { resolvePageLedgers } from '../../ingesters/toid' +import { resolvePageLedgers } from '../toid' import { dispatchPriceUpdate } from '../../webhookDispatcher' import { publishPriceUpdate } from '../../events' import { createLogger } from '../../logger' diff --git a/src/ingesters/amm.ts b/src/ingesters/amm.ts deleted file mode 100644 index aee66c9d..00000000 --- a/src/ingesters/amm.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * AMM Venue Ingester (Backward compatibility proxy) - * - * Venue adapters have been unified under src/ingest/venues/. - * See src/ingest/venues/amm.ts. - */ -export * from '../ingest/venues/amm' diff --git a/src/ingesters/sdex.ts b/src/ingesters/sdex.ts deleted file mode 100644 index ca362d03..00000000 --- a/src/ingesters/sdex.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * SDEX Venue Ingester (Backward compatibility proxy) - * - * Venue adapters have been unified under src/ingest/venues/. - * See src/ingest/venues/sdex.ts. - */ -export * from '../ingest/venues/sdex' diff --git a/src/ingesters/soroswap.ts b/src/ingesters/soroswap.ts deleted file mode 100644 index 517a348a..00000000 --- a/src/ingesters/soroswap.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Soroswap Venue Ingester (Backward compatibility proxy) - * - * Venue adapters have been unified under src/ingest/venues/. - * See src/ingest/venues/soroswap.ts. - */ -export * from '../ingest/venues/soroswap' diff --git a/tests/ingestMetrics.test.ts b/tests/ingestMetrics.test.ts index a94d69e4..f9a354ca 100644 --- a/tests/ingestMetrics.test.ts +++ b/tests/ingestMetrics.test.ts @@ -63,9 +63,9 @@ import { price_snapshots_total, last_trade_timestamp, } from '../src/metrics' -import { ingestPair } from '../src/ingesters/sdex' -import { snapshotPool, ingestPoolTrades } from '../src/ingesters/amm' -import { appendSnapshots } from '../src/ingesters/snapshot' +import { ingestPair } from '../src/ingest/venues/sdex' +import { snapshotPool, ingestPoolTrades } from '../src/ingest/venues/amm' +import { appendSnapshots } from '../src/ingest/snapshot' const USDC_ISSUER = 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' diff --git a/vitest.config.ts b/vitest.config.ts index 85100f42..57c8c05d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,5 +15,10 @@ export default defineConfig({ // a little headroom over the 5s default. Kept deliberately tight: a loose // ceiling hides a genuine hang as a slow pass. testTimeout: 20_000, + // Match testTimeout for hooks. Some suites build the whole app in + // `beforeAll` (e.g. tests/rest-route.test.ts), which imports the heavy + // @stellar/stellar-sdk; under parallel-fork contention that first build can + // exceed the 10s hook default and time out even though it is not hung. + hookTimeout: 20_000, }, })