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
10 changes: 10 additions & 0 deletions .changeset/numeric-price-precision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'lens': patch
---

Fix numeric price and volume precision across aggregations (#230).

- Remove `::float` casts inside intermediate aggregate expressions in `/candles`, `/prices/history`, and GraphQL `getPriceHistory`.
- Remove `::float` casts from `/price/:assetA/:assetB/history` and query raw numerics directly from `price_aggregates`.
- Add explicit, documented edge serialization formatters in `src/pricing/precision.ts`: prices up to 18 decimal places matching `Decimal(36, 18)`, and volumes up to 7 decimal places matching Stellar stroops (`Decimal(36, 7)`).
- Prevent multi-venue floating-point accumulation drift in `/volumes/:asset`.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: npm
- run: npm ci || npm install
- run: node node_modules/prisma/build/index.js generate
Expand Down Expand Up @@ -59,7 +59,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: npm
- run: npm ci || npm install
- run: node node_modules/prisma/build/index.js generate
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dropped-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '20'
node-version: '22'

- name: Compare test cases against the base
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/live-spec-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: npm

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/load.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: npm

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/openapi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: npm
- run: npm ci
- run: npm run openapi:gen
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: npm

- name: Install dependencies
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Stage 1: Build
FROM node:20-slim AS builder
FROM node:22-slim AS builder

WORKDIR /app

Expand All @@ -13,7 +13,7 @@ COPY . .
RUN npm run build

# Stage 2: Run
FROM node:20-slim
FROM node:22-slim

WORKDIR /app

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,15 @@ curl -s http://localhost:3002/price/XLM/USDC | jq '{price, ammPrice, lastUpdated

*(Note: timestamp values above reflect the dynamic hourly anchor at seed execution time and request time).*

## Numeric Precision & Serialization

Lens enforces strict arbitrary-precision numeric aggregation across SQL queries to eliminate floating-point representation artifacts and order-dependent rounding errors:

- **Database Precision**: Declared in `prisma/schema.prisma` as `Decimal(36, 18)` for prices and `Decimal(36, 7)` for volumes (matching Stellar 7-decimal stroop precision).
- **Exact Aggregations**: All intermediate SQL aggregations (`SUM`, `array_agg`, `MAX`, `MIN`) are computed in PostgreSQL using exact `numeric` arithmetic without `::float` casts inside aggregate expressions.
- **Edge Serialization**: Values are cast once at the serialization boundary to documented precision:
- Prices: up to 18 decimal places (`PRICE_DECIMAL_PLACES = 18`).
- Volumes: up to 7 decimal places (`VOLUME_DECIMAL_PLACES = 7`).

## Rate Limiting & 429 Handling

Expand Down
99 changes: 96 additions & 3 deletions clients/typescript/src/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,10 @@ export interface paths {
path?: never;
cookie?: never;
};
/** Get price history buckets */
/**
* Get price history buckets
* @description Returns price aggregate buckets (1m, 5m, 1h, 24h). Prices are serialized up to 18 decimal places and volumes up to 7 decimal places matching Stellar stroop precision.
*/
get: operations["getPriceHistory"];
put?: never;
post?: never;
Expand Down Expand Up @@ -214,7 +217,10 @@ export interface paths {
path?: never;
cookie?: never;
};
/** Get OHLCV candle data */
/**
* Get OHLCV candle data
* @description Aggregates price_points into interval-sized buckets using exact numeric arithmetic. Prices are serialized up to 18 decimal places and volumes up to 7 decimal places matching Stellar stroop precision.
*/
get: operations["getCandles"];
put?: never;
post?: never;
Expand All @@ -233,7 +239,7 @@ export interface paths {
};
/**
* Get historical 1-minute price snapshots
* @description Returns minute-resolution price snapshots for a pair over [from, to], optionally aggregated into 5m or 1h buckets. Snapshots are retained for ~30 days. Gated by x402 (matches the /price prefix).
* @description Returns minute-resolution price snapshots for a pair over [from, to], optionally aggregated into 5m or 1h buckets. Aggregations use exact numeric arithmetic; prices are serialized up to 18 decimal places and volumes up to 7 decimal places. Snapshots are retained for ~30 days. Gated by x402 (matches the /price prefix).
*/
get: operations["getPriceSnapshots"];
put?: never;
Expand Down Expand Up @@ -705,10 +711,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<string, never>;
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
Expand Down Expand Up @@ -3118,4 +3174,41 @@ 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"];
};
};
/** @description Invalid parameters */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
429: components["responses"]["TooManyRequests"];
};
};
}
6 changes: 4 additions & 2 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@
"get": {
"operationId": "getPriceHistory",
"summary": "Get price history buckets",
"description": "Returns price aggregate buckets (1m, 5m, 1h, 24h). Prices are serialized up to 18 decimal places and volumes up to 7 decimal places matching Stellar stroop precision.",
"security": [],
"parameters": [
{
Expand Down Expand Up @@ -838,6 +839,7 @@
"get": {
"operationId": "getCandles",
"summary": "Get OHLCV candle data",
"description": "Aggregates price_points into interval-sized buckets using exact numeric arithmetic. Prices are serialized up to 18 decimal places and volumes up to 7 decimal places matching Stellar stroop precision.",
"security": [
{
"x402": []
Expand Down Expand Up @@ -971,7 +973,7 @@
"get": {
"operationId": "getPriceSnapshots",
"summary": "Get historical 1-minute price snapshots",
"description": "Returns minute-resolution price snapshots for a pair over [from, to], optionally aggregated into 5m or 1h buckets. Snapshots are retained for ~30 days. Gated by x402 (matches the /price prefix).",
"description": "Returns minute-resolution price snapshots for a pair over [from, to], optionally aggregated into 5m or 1h buckets. Aggregations use exact numeric arithmetic; prices are serialized up to 18 decimal places and volumes up to 7 decimal places. Snapshots are retained for ~30 days. Gated by x402 (matches the /price prefix).",
"security": [
{
"x402": []
Expand Down Expand Up @@ -2845,7 +2847,7 @@
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
"description": "Invalid parameters"
},
"429": {
"$ref": "#/components/responses/TooManyRequests"
Expand Down
13 changes: 10 additions & 3 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,9 @@ paths:
get:
operationId: getPriceHistory
summary: Get price history buckets
description: >-
Returns price aggregate buckets (1m, 5m, 1h, 24h). Prices are serialized up to 18
decimal places and volumes up to 7 decimal places matching Stellar stroop precision.
security: []
parameters:
- name: assetA
Expand Down Expand Up @@ -572,6 +575,9 @@ paths:
get:
operationId: getCandles
summary: Get OHLCV candle data
description: >-
Aggregates price_points into interval-sized buckets using exact numeric arithmetic.
Prices are serialized up to 18 decimal places and volumes up to 7 decimal places matching Stellar stroop precision.
security:
- x402: []
parameters:
Expand Down Expand Up @@ -668,8 +674,9 @@ paths:
summary: Get historical 1-minute price snapshots
description: >-
Returns minute-resolution price snapshots for a pair over [from, to],
optionally aggregated into 5m or 1h buckets. Snapshots are retained for
~30 days. Gated by x402 (matches the /price prefix).
optionally aggregated into 5m or 1h buckets. Aggregations use exact numeric arithmetic;
prices are serialized up to 18 decimal places and volumes up to 7 decimal places.
Snapshots are retained for ~30 days. Gated by x402 (matches the /price prefix).
security:
- x402: []
parameters:
Expand Down Expand Up @@ -1921,7 +1928,7 @@ paths:
schema:
$ref: '#/components/schemas/LendingRatesResponse'
'400':
$ref: '#/components/responses/BadRequest'
description: Invalid parameters
'429':
$ref: '#/components/responses/TooManyRequests'

Expand Down
Loading
Loading