Skip to content
Merged
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
27 changes: 18 additions & 9 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ datasource db {

model PricePoint {
id String @default(uuid())
network String @default("testnet")
assetA String @map("asset_a")
assetB String @map("asset_b")
pairKey String @map("pair_key")
Expand All @@ -24,13 +25,14 @@ model PricePoint {
eventId String? @map("event_id")

@@id([id, timestamp])
@@index([pairKey, timestamp(sort: Desc)])
@@index([pairKey, source, timestamp(sort: Desc)])
@@index([network, pairKey, timestamp(sort: Desc)])
@@index([network, pairKey, source, timestamp(sort: Desc)])
@@map("price_points")
}

model PoolSnapshot {
id String @default(uuid())
network String @default("testnet")
poolId String @map("pool_id")
assetA String @map("asset_a")
assetB String @map("asset_b")
Expand All @@ -43,12 +45,13 @@ model PoolSnapshot {
timestamp DateTime

@@id([id, timestamp])
@@index([poolId, timestamp(sort: Desc)])
@@index([network, poolId, timestamp(sort: Desc)])
@@map("pool_snapshots")
}

model PriceAggregate {
pairKey String @map("pair_key")
network String @default("testnet")
window String
bucket DateTime
vwap Decimal @db.Decimal(36, 18)
Expand All @@ -63,39 +66,44 @@ model PriceAggregate {
highPrice Decimal? @map("high_price") @db.Decimal(36, 18)
lowPrice Decimal? @map("low_price") @db.Decimal(36, 18)

@@id([pairKey, window, bucket])
@@id([network, pairKey, window, bucket])
@@map("price_aggregates")
}

model PriceSnapshot {
pair String
network String @default("testnet")
ts DateTime
price Decimal @db.Decimal(36, 18)
volume Decimal @default(0) @db.Decimal(36, 7)

@@id([pair, ts])
@@index([pair, ts])
@@id([network, pair, ts])
@@index([network, pair, ts])
@@map("price_snapshots")
}

model IndexerState {
id String @id
id String
network String @default("testnet")
lastCursor String? @map("last_cursor")
lastLedger Int? @map("last_ledger")
lastProcessedAt DateTime? @map("last_processed_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")

@@id([network, id])
@@map("indexer_state")
}

model PairConfig {
pairKey String @id @map("pair_key")
pairKey String @map("pair_key")
network String @default("testnet")
assetACode String @map("asset_a_code")
assetAIssuer String? @map("asset_a_issuer")
assetBCode String @map("asset_b_code")
assetBIssuer String? @map("asset_b_issuer")
addedAt DateTime @default(now()) @map("added_at")

@@id([network, pairKey])
@@map("pair_configs")
}

Expand Down Expand Up @@ -193,6 +201,7 @@ model BazaarResource {

model Webhook {
id String @id @default(uuid())
network String @default("testnet")
url String
assetA String @map("asset_a")
assetB String @map("asset_b")
Expand All @@ -201,7 +210,7 @@ model Webhook {
secret String
createdAt DateTime @default(now()) @map("created_at")

@@index([assetA, assetB])
@@index([network, assetA, assetB])
@@map("webhooks")
}

Expand Down
93 changes: 0 additions & 93 deletions sql/schema.sql

This file was deleted.

134 changes: 134 additions & 0 deletions src/__tests__/facilitator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { vi, describe, it, expect, beforeEach } from 'vitest'
import Fastify from 'fastify'
import { registerFacilitatorRoutes } from '../api/facilitator'

const mockVerify = vi.fn()
const mockSettle = vi.fn()
const mockGetSupported = vi.fn()
const mockRegister = vi.fn()

vi.mock('@x402/core/facilitator', () => ({
x402Facilitator: class {
register = mockRegister
verify = mockVerify
settle = mockSettle
getSupported = mockGetSupported
}
}))

vi.mock('@x402/stellar/exact/facilitator', () => ({
ExactStellarScheme: vi.fn(),
}))

vi.mock('@x402/stellar', () => ({
createEd25519Signer: vi.fn(),
}))

vi.mock('../config', () => ({
getNetworkConfig: vi.fn(() => ({
rpc: { url: 'http://localhost' },
facilitator: { secretKey: 'S_MOCK_SECRET', feeStroops: 50000 },
}))
}))

describe('Facilitator endpoints', () => {
let app: any

beforeEach(async () => {
vi.clearAllMocks()
app = Fastify()
await app.register(registerFacilitatorRoutes)
await app.ready()
})

it('GET /supported returns supported kinds', async () => {
mockGetSupported.mockReturnValue({ kinds: [{ scheme: 'exact' }] })
const res = await app.inject({ method: 'GET', url: '/supported' })
expect(res.statusCode).toBe(200)
expect(res.json()).toEqual({ kinds: [{ scheme: 'exact' }] })
})

it('POST /verify returns verify response', async () => {
mockVerify.mockResolvedValue({ isValid: true })
const res = await app.inject({
method: 'POST',
url: '/verify',
payload: { paymentPayload: {}, paymentRequirements: {} }
})
expect(res.statusCode).toBe(200)
expect(res.json()).toEqual({ isValid: true })
})

it('POST /verify returns 400 if verify returns isValid: false', async () => {
mockVerify.mockResolvedValue({ isValid: false, invalidReason: 'bad_sig' })
const res = await app.inject({
method: 'POST',
url: '/verify',
payload: { paymentPayload: {}, paymentRequirements: {} }
})
expect(res.statusCode).toBe(400)
expect(res.json()).toEqual({ isValid: false, invalidReason: 'bad_sig' })
})

it('POST /verify returns 400 if payload is missing', async () => {
const res = await app.inject({
method: 'POST',
url: '/verify',
payload: {}
})
expect(res.statusCode).toBe(400)
expect(res.json()).toHaveProperty('isValid', false)
})

it('POST /verify passes caught structured error response', async () => {
const error: any = new Error('Verification failed')
error.statusCode = 422
error.response = { isValid: false, invalidReason: 'expired' }
mockVerify.mockRejectedValue(error)

const res = await app.inject({
method: 'POST',
url: '/verify',
payload: { paymentPayload: {}, paymentRequirements: {} }
})
expect(res.statusCode).toBe(422)
expect(res.json()).toEqual({ isValid: false, invalidReason: 'expired' })
})

it('POST /settle returns settle response', async () => {
mockSettle.mockResolvedValue({ success: true, transaction: 'txhash' })
const res = await app.inject({
method: 'POST',
url: '/settle',
payload: { paymentPayload: {}, paymentRequirements: {} }
})
expect(res.statusCode).toBe(200)
expect(res.json()).toEqual({ success: true, transaction: 'txhash' })
})

it('POST /settle returns 400 if settle returns success: false', async () => {
mockSettle.mockResolvedValue({ success: false, errorReason: 'failed' })
const res = await app.inject({
method: 'POST',
url: '/settle',
payload: { paymentPayload: {}, paymentRequirements: {} }
})
expect(res.statusCode).toBe(400)
expect(res.json()).toEqual({ success: false, errorReason: 'failed' })
})

it('POST /settle passes caught structured error response', async () => {
const error: any = new Error('Settlement failed')
error.statusCode = 400
error.response = { success: false, errorReason: 'tx_failed' }
mockSettle.mockRejectedValue(error)

const res = await app.inject({
method: 'POST',
url: '/settle',
payload: { paymentPayload: {}, paymentRequirements: {} }
})
expect(res.statusCode).toBe(400)
expect(res.json()).toEqual({ success: false, errorReason: 'tx_failed' })
})
})
2 changes: 1 addition & 1 deletion src/__tests__/snapshotIngester.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@ describe('appendSnapshots', () => {

await appendSnapshots()

expect(mockQuery.mock.calls[0][0]).toMatch(/ON CONFLICT \(pair, ts\) DO NOTHING/)
expect(mockQuery.mock.calls[0][0]).toMatch(/ON CONFLICT \(network, pair, ts\) DO NOTHING/)
})
})
7 changes: 4 additions & 3 deletions src/__tests__/snapshotRetention.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vi.mock('bullmq', () => ({
}))

import { pruneOldSnapshots, SNAPSHOT_RETENTION_DAYS } from '../jobs/snapshotRetention'
import { activeNetwork } from '../config'

describe('pruneOldSnapshots', () => {
beforeEach(() => {
Expand All @@ -28,9 +29,9 @@ describe('pruneOldSnapshots', () => {

expect(SNAPSHOT_RETENTION_DAYS).toBe(30)
expect(pruned).toBe(5)
expect(mockQuery.mock.calls[0][1]).toEqual([30])
expect(mockQuery.mock.calls[0][1]).toEqual([activeNetwork, 30])
expect(mockQuery.mock.calls[0][0]).toMatch(/DELETE FROM price_snapshots/)
expect(mockQuery.mock.calls[0][0]).toMatch(/ts < NOW\(\) - \(\$1 \|\| ' days'\)::interval/)
expect(mockQuery.mock.calls[0][0]).toMatch(/ts < NOW\(\) - \(\$2 \|\| ' days'\)::interval/)
})

it('honors a custom retention window', async () => {
Expand All @@ -39,7 +40,7 @@ describe('pruneOldSnapshots', () => {
const pruned = await pruneOldSnapshots(7)

expect(pruned).toBe(0)
expect(mockQuery.mock.calls[0][1]).toEqual([7])
expect(mockQuery.mock.calls[0][1]).toEqual([activeNetwork, 7])
})

it('returns 0 when rowCount is null', async () => {
Expand Down
Loading
Loading