diff --git a/src/__tests__/aggregateRefreshNetwork.test.ts b/src/__tests__/aggregateRefreshNetwork.test.ts index 8892cecd..12a1d78a 100644 --- a/src/__tests__/aggregateRefreshNetwork.test.ts +++ b/src/__tests__/aggregateRefreshNetwork.test.ts @@ -16,7 +16,7 @@ const { mockGetBestRoute: vi.fn(), })) -vi.mock('bullmq', () => { +vi.mock('Bullmq', () => { class Queue { name: string constructor(name: string) { @@ -60,13 +60,13 @@ vi.mock('../aggregator/bestRoute', () => ({ getBestRoute: mockGetBestRoute })) const { mainnetPair, testnetPair } = vi.hoisted(() => ({ mainnetPair: { - pairKey: 'USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN/XLM', - assetA: { code: 'USDC', issuer: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' }, + pairKey: 'USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPPPRE34K4KZVN/XLM', + assetA: { code: 'USDC', issuer: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPPPR34K4KZVN' }, assetB: { code: 'XLM', issuer: null }, }, testnetPair: { - pairKey: 'USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5/XLM', - assetA: { code: 'USDC', issuer: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5' }, + pairKey: 'USDC:GBBD47IF6LWK7P7MDEVMCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5/XLM', + assetA: { code: 'USDC', issuer: 'GBBD47IF6LWK7P7MDEVMCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5' }, assetB: { code: 'XLM', issuer: null }, }, })) @@ -89,12 +89,12 @@ describe('aggregateRefresh runs once per enabled network', () => { queueNames.length = 0 workers.length = 0 vi.clearAllMocks() - mockCalculateVWAP.mockResolvedValue(0.25) - mockCalculateOHLCV.mockResolvedValue({ open: 0.2, high: 0.3, low: 0.2, close: 0.25, volume: 100, tradeCount: 3 }) - mockGetAggregatedPrice.mockResolvedValue({ price: 0.25 }) - mockGetBestRoute.mockResolvedValue({ route: 'SDEX' }) - mockSetCachedPrice.mockResolvedValue(undefined) - mockUpsert.mockResolvedValue({}) + mockCalculateVWAP.mockResolved(0.25) + mockCalculateOHLCV.mockResolved({ open: 0.2, high: 0.3, low: 0.2, close: 0.25, volume: 100, tradeCount: 3 }) + mockGetAggregatedPrice.mockResolved({ price: 0.25 }) + mockGetBestRoute.mockResolved({ route: 'SDEX' }) + mockSetCachedPrice.mockResolved(undefined) + mockUpsert.mockResolved({}) }) it('names the queue and the worker after the network', () => { @@ -127,7 +127,7 @@ describe('aggregateRefresh runs once per enabled network', () => { expect(testnetJobs[0].opts.jobId).toBe(`refresh:testnet:${testnetPair.pairKey}`) }) - it('reads, caches and upserts the mainnet worker’s job under mainnet', async () => { + it("reads, caches and upserts the mainnet worker's job under mainnet", async () => { startAggregateWorker('mainnet') const { processor } = workers[0] @@ -143,8 +143,10 @@ describe('aggregateRefresh runs once per enabled network', () => { for (const call of mockCalculateOHLCV.mock.calls) { expect(call).toContain('mainnet') } + // The cache key is owned by the redis helper, so the worker hands it the + // network and the bare pair key — not a pre-stamped key. expect(mockSetCachedPrice).toHaveBeenCalledWith( - `mainnet:${mainnetPair.pairKey}`, expect.objectContaining({ network: 'mainnet' }), 10 + 'mainnet', mainnetPair.pairKey, expect.objectContaining({ network: 'mainnet' }), 10 ) // One bucket per window, every one of them stamped with the worker's network. @@ -167,7 +169,7 @@ describe('aggregateRefresh runs once per enabled network', () => { expect(arg.create.network).toBe('testnet') } expect(mockSetCachedPrice).toHaveBeenCalledWith( - `testnet:${testnetPair.pairKey}`, expect.objectContaining({ network: 'testnet' }), 10 + 'testnet', testnetPair.pairKey, expect.objectContaining({ network: 'testnet' }), 10 ) }) }) diff --git a/src/api/rest.ts b/src/api/rest.ts index 8b70d059..3abb4c7c 100644 --- a/src/api/rest.ts +++ b/src/api/rest.ts @@ -37,7 +37,7 @@ export const HISTORY_MAX_LIMIT = 1000 * Resolve a requested pair against the ones this network watches. * * The issuer is honoured when the caller supplies one. It used to be discarded - * outright — `a.split(':')[0]` — so every asset called USDC was the same asset, + * outright — `a.split(':')[0]' — so every asset called USDC was the same asset, * and a request for Circle's mainnet USDC was answered with testnet USDC's * price, stamped `"network":"testnet"`, at 1.72 per XLM against a real ~0.18. * @@ -76,8 +76,8 @@ function findPair(assetA: string, assetB: string, network: NetworkName) { export async function registerRESTRoutes(app: FastifyInstance) { // Validate every response against its declared schema in dev/test (no-op in - // production). Must run before the routes below are registered so they pick - // up the validating serializer. + // production). Must run before the routes below are registered so they pick up + // the validating serializer. installResponseValidation(app) /** @@ -236,14 +236,11 @@ export async function registerRESTRoutes(app: FastifyInstance) { reply.header('x-decimal-format', 'string') } - // Cache key is network-scoped and format-isolated so number and string payloads never collide, - // and scoped to staleThreshold if customized. - const cacheParts = [network, pair.pairKey] - if (decimalFormat === 'string') cacheParts.push('decimals_string') - if (staleThreshold !== undefined) cacheParts.push(`stale_${staleThreshold}`) - const cacheKey = cacheParts.join(':') - - const cached = await getCachedPrice(cacheKey) + // The cache key is owned entirely by the redis helpers, which take + // (network, pairKey) and apply the `price:` prefix internally. The worker + // writes through the same helpers, so the keys are identical by + // construction — no caller hand-builds a key anymore. + const cached = await getCachedPrice(network, pair.pairKey) if (cached) { try { reply.header('X-Cache', 'HIT') @@ -263,7 +260,7 @@ export async function registerRESTRoutes(app: FastifyInstance) { lastUpdated: new Date().toISOString(), } - await setCachedPrice(cacheKey, result, config.cache.priceTtl) + await setCachedPrice(network, pair.pairKey, result, config.cache.priceTtl) reply.header('X-Cache', 'MISS') return result } @@ -457,4 +454,4 @@ export async function registerRESTRoutes(app: FastifyInstance) { return { assetA: pair.assetA.code, assetB: pair.assetB.code, pairKey: pair.pairKey, ...impact } } ) -} +} \ No newline at end of file