Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions src/__tests__/aggregateRefreshNetwork.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const {
mockGetBestRoute: vi.fn(),
}))

vi.mock('bullmq', () => {
vi.mock('Bullmq', () => {
class Queue {
name: string
constructor(name: string) {
Expand Down Expand Up @@ -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 },
},
}))
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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]

Expand All @@ -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.
Expand All @@ -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
)
})
})
23 changes: 10 additions & 13 deletions src/api/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
* 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.
*
Expand Down Expand Up @@ -76,8 +76,8 @@

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)

/**
Expand Down Expand Up @@ -236,14 +236,11 @@
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)

Check failure on line 243 in src/api/rest.ts

View workflow job for this annotation

GitHub Actions / Typecheck & build

Expected 1 arguments, but got 2.
if (cached) {
try {
reply.header('X-Cache', 'HIT')
Expand All @@ -263,7 +260,7 @@
lastUpdated: new Date().toISOString(),
}

await setCachedPrice(cacheKey, result, config.cache.priceTtl)
await setCachedPrice(network, pair.pairKey, result, config.cache.priceTtl)

Check failure on line 263 in src/api/rest.ts

View workflow job for this annotation

GitHub Actions / Typecheck & build

Expected 3 arguments, but got 4.
reply.header('X-Cache', 'MISS')
return result
}
Expand Down Expand Up @@ -457,4 +454,4 @@
return { assetA: pair.assetA.code, assetB: pair.assetB.code, pairKey: pair.pairKey, ...impact }
}
)
}
}
Loading