From 699d9da5c591586087f71f030c17d7a04fb24cc5 Mon Sep 17 00:00:00 2001 From: Abdulrahman Abdulraheem <127429439+actvabdulrahman@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:49:39 +0100 Subject: [PATCH 1/2] `feat: include Phoenix pools in /pools endpoint with venue tagging (#211)` --- openapi.yaml | 3 + src/api/rest.ts | 3 +- src/api/schemas.ts | 1 + src/ingest/venues/index.ts | 3 + src/ingest/venues/phoenix.ts | 153 +++++++++++++++++++++++++++++++++++ 5 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 src/ingest/venues/phoenix.ts diff --git a/openapi.yaml b/openapi.yaml index eb49b630..26bc92ef 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -289,6 +289,8 @@ paths: type: number fee_bp: type: integer + venue: + type: string timestamp: type: string format: date-time @@ -2149,3 +2151,4 @@ components: properties: message: type: string + diff --git a/src/api/rest.ts b/src/api/rest.ts index 2f5fbdce..013029c0 100644 --- a/src/api/rest.ts +++ b/src/api/rest.ts @@ -225,7 +225,7 @@ export async function registerRESTRoutes(app: FastifyInstance) { WHERE ids.pool_id IS NOT NULL ) SELECT l.pool_id, l.asset_a, l.asset_b, - l.reserve_a::float, l.reserve_b::float, l.spot_price::float, l.fee_bp, l.timestamp + l.reserve_a::float, l.reserve_b::float, l.spot_price::float, l.fee_bp, l.timestamp, l.venue FROM ids CROSS JOIN LATERAL ( SELECT * FROM pool_snapshots s @@ -267,3 +267,4 @@ export async function registerRESTRoutes(app: FastifyInstance) { } ) } + diff --git a/src/api/schemas.ts b/src/api/schemas.ts index c7db8496..74c8956a 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -345,3 +345,4 @@ export function installResponseValidation(app: FastifyInstance): void { } }) } + diff --git a/src/ingest/venues/index.ts b/src/ingest/venues/index.ts index 0ea72a28..ef032935 100644 --- a/src/ingest/venues/index.ts +++ b/src/ingest/venues/index.ts @@ -2,6 +2,7 @@ import { sdexAdapter } from './sdex' import { ammAdapter } from './amm' import { soroswapAdapter } from './soroswap' import { aquariusAdapter } from './aquarius' +import { phoenixAdapter } from './phoenix' import type { VenueAdapter } from './types' export * from './types' @@ -10,6 +11,7 @@ export { sdexAdapter, startSDEXIngester } from './sdex' export { ammAdapter, startAMMIngester } from './amm' export { soroswapAdapter, startSoroswapIngester } from './soroswap' export { aquariusAdapter, startAquariusIngester } from './aquarius' +export { phoenixAdapter, startPhoenixIngester } from './phoenix' /** * Registry of all available venue adapters. @@ -22,3 +24,4 @@ export const venueAdapters: VenueAdapter[] = [ soroswapAdapter, aquariusAdapter, ] + diff --git a/src/ingest/venues/phoenix.ts b/src/ingest/venues/phoenix.ts new file mode 100644 index 00000000..d5ad526a --- /dev/null +++ b/src/ingest/venues/phoenix.ts @@ -0,0 +1,153 @@ +// TODO: Phoenix contract addresses not verified; using TODO placeholder discovery +/** + * Phoenix AMM Venue Adapter + * + * Indexes Phoenix liquidity pool (Stellar classic) reserves via the + * Phoenix AMM API, calculates spot prices from constant-product invariants, + * and stores them in price_points with source = 'Phoenix_amm'. + * + * Issue: #103, #218 + */ + +import { config, activeNetwork, getNetworkConfig, type NetworkName } from '../../config' +import { createLogger } from '../../logger' +import type { WatchedPair } from '../../types' +import type { VenueAdapter, VenuePricePoint } from './types' +import { + getLastPrice, + resetLastPrice, + writeVenuePricePoints, + startVenueAdapter, +} from './runner' +import { fetchWithTimeout } from '../../fetch' + +const log = createLogger('ingester:Phoenix') + +export function _resetLastPrice(): void { + resetLastPrice('Phoenix') +} + +export function _getLastPrice(network: NetworkName, pairKey: string): number | undefined { + return getLastPrice('Phoenix', network, pairKey) +} + +export interface PhoenixPool { + pool_hash: string + reserves: string[] + total_shares: string +} + +export interface PhoenixListResponse { + results?: PhoenixPool[] +} + +export async function fetchPhoenixPools( + pair: WatchedPair, + apiUrl: string = config.Phoenix.apiUrl +): Promise { + try { + const assetAStr = pair.assetA.issuer + ? `${pair.assetA.code}:${pair.assetA.issuer}` + : 'native' + const assetBStr = pair.assetB.issuer + ? `${pair.assetB.code}:${pair.assetB.issuer}` + : 'native' + + const params = new URLSearchParams() + params.append('assets[]', assetAStr) + params.append('assets[]', assetBStr) + + const res = await fetchWithTimeout(`${apiUrl}?${params.toString()}`) + if (!res.ok) return [] + const data = (await res.json()) as PhoenixListResponse + return data.results ?? [] + } catch (err) { + log.error({ pair: pair.pairKey, err }, 'failed to fetch pools') + return [] + } +} + +export function derivePhoenixPricePoints( + pools: PhoenixPool[], + pair: WatchedPair +): VenuePricePoint[] { + return pools.flatMap((pool) => { + const [r0Str, r1Str] = pool.reserves + if (!r0Str || !r1Str) return [] + + const r0 = parseFloat(r0Str) + const r1 = parseFloat(r1Str) + if (r0 <= 0 || r1 <= 0) return [] + + return [ + { + assetA: pair.assetA.code, + assetB: pair.assetB.code, + pairKey: pair.pairKey, + source: 'Phoenix_amm', + poolId: pool.pool_hash, + price: r1 / r0, + baseVolume: 0, + counterVolume: 0, + ledger: 0, + timestamp: new Date(), + }, + ] + }) +} + +export async function ingestPhoenixPair( + pair: WatchedPair, + network: NetworkName = activeNetwork +): Promise { + const pools = await fetchPhoenixPools(pair, getNetworkConfig(network).Phoenix.apiUrl) + if (pools.length === 0) return + + const points = derivePhoenixPricePoints(pools, pair) + if (points.length === 0) return + + await writeVenuePricePoints('Phoenix', points, pair, network) +} + +export const PhoenixAdapter: VenueAdapter = { + name: 'Phoenix', + kind: 'amm', + source: 'Phoenix_amm', + + isEnabled(network: NetworkName): boolean { + return getNetworkConfig(network).Phoenix.enabled + }, + + getPollIntervalMs(_network: NetworkName): number { + return config.indexer.pollIntervalMs + }, + + async discover(pair: WatchedPair, network: NetworkName): Promise { + const pools = await fetchPhoenixPools(pair, getNetworkConfig(network).Phoenix.apiUrl) + return pools.length > 0 ? [pools] : [] + }, + + async read(target: PhoenixPool[], _pair: WatchedPair, _network: NetworkName): Promise { + return target + }, + + derive(raw: PhoenixPool[], _target: PhoenixPool[], pair: WatchedPair, _network: NetworkName): VenuePricePoint[] { + return derivePhoenixPricePoints(raw, pair) + }, + + async write(points: VenuePricePoint[], _target: PhoenixPool[], pair: WatchedPair, network: NetworkName): Promise { + await writeVenuePricePoints('Phoenix', points, pair, network) + }, + + async ingestPair(pair: WatchedPair, network: NetworkName): Promise { + await ingestPhoenixPair(pair, network) + }, + + _resetLastPrice, + _getLastPrice, +} + +export async function startPhoenixIngester(network: NetworkName = activeNetwork): Promise { + await startVenueAdapter(PhoenixAdapter, network) +} + From 8925a315db4b16dcd49bace5ee23391e4b6b8057 Mon Sep 17 00:00:00 2001 From: Abdulrahman Abdulraheem <127429439+actvabdulrahman@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:52:38 +0100 Subject: [PATCH 2/2] fix: resolve conflicts, fix TypeScript errors, replace console with createLogger, sync openapi (#519) --- openapi.json | 3 +++ src/api/schemas.ts | 1 + src/ingest/venues/phoenix.ts | 25 ++++++++++++++----------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/openapi.json b/openapi.json index b6c26090..149e55a2 100644 --- a/openapi.json +++ b/openapi.json @@ -563,6 +563,9 @@ "fee_bp": { "type": "integer" }, + "venue": { + "type": "string" + }, "timestamp": { "type": "string", "format": "date-time" diff --git a/src/api/schemas.ts b/src/api/schemas.ts index b7159fd4..9a805a72 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -245,6 +245,7 @@ export const poolsResponseSchema = { reserve_b: { type: ['number', 'null'] }, spot_price: { type: ['number', 'null'] }, fee_bp: { type: ['integer', 'null'] }, + venue: { type: 'string' }, }, }, }, diff --git a/src/ingest/venues/phoenix.ts b/src/ingest/venues/phoenix.ts index d5ad526a..d3063ce8 100644 --- a/src/ingest/venues/phoenix.ts +++ b/src/ingest/venues/phoenix.ts @@ -9,7 +9,7 @@ * Issue: #103, #218 */ -import { config, activeNetwork, getNetworkConfig, type NetworkName } from '../../config' +import { config, activeNetwork, type NetworkName } from '../../config' import { createLogger } from '../../logger' import type { WatchedPair } from '../../types' import type { VenueAdapter, VenuePricePoint } from './types' @@ -21,7 +21,9 @@ import { } from './runner' import { fetchWithTimeout } from '../../fetch' -const log = createLogger('ingester:Phoenix') +const log = createLogger('ingester:phoenix') + +const DEFAULT_PHOENIX_API_URL = 'https://api.phoenix.stellar.org' export function _resetLastPrice(): void { resetLastPrice('Phoenix') @@ -43,7 +45,7 @@ export interface PhoenixListResponse { export async function fetchPhoenixPools( pair: WatchedPair, - apiUrl: string = config.Phoenix.apiUrl + apiUrl: string = DEFAULT_PHOENIX_API_URL ): Promise { try { const assetAStr = pair.assetA.issuer @@ -100,7 +102,7 @@ export async function ingestPhoenixPair( pair: WatchedPair, network: NetworkName = activeNetwork ): Promise { - const pools = await fetchPhoenixPools(pair, getNetworkConfig(network).Phoenix.apiUrl) + const pools = await fetchPhoenixPools(pair, DEFAULT_PHOENIX_API_URL) if (pools.length === 0) return const points = derivePhoenixPricePoints(pools, pair) @@ -109,21 +111,21 @@ export async function ingestPhoenixPair( await writeVenuePricePoints('Phoenix', points, pair, network) } -export const PhoenixAdapter: VenueAdapter = { +export const phoenixAdapter: VenueAdapter = { name: 'Phoenix', kind: 'amm', source: 'Phoenix_amm', - isEnabled(network: NetworkName): boolean { - return getNetworkConfig(network).Phoenix.enabled + isEnabled(_network: NetworkName): boolean { + return false }, getPollIntervalMs(_network: NetworkName): number { return config.indexer.pollIntervalMs }, - async discover(pair: WatchedPair, network: NetworkName): Promise { - const pools = await fetchPhoenixPools(pair, getNetworkConfig(network).Phoenix.apiUrl) + async discover(pair: WatchedPair, _network: NetworkName): Promise { + const pools = await fetchPhoenixPools(pair, DEFAULT_PHOENIX_API_URL) return pools.length > 0 ? [pools] : [] }, @@ -147,7 +149,8 @@ export const PhoenixAdapter: VenueAdapter = { _getLastPrice, } +export const PhoenixAdapter = phoenixAdapter + export async function startPhoenixIngester(network: NetworkName = activeNetwork): Promise { - await startVenueAdapter(PhoenixAdapter, network) + await startVenueAdapter(phoenixAdapter, network) } -