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
5 changes: 5 additions & 0 deletions .changeset/dx-platform-hardening-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'lens': minor
---

DX & Platform Hardening: /v1 Route Versioning, Venues Registry, Snapshots, Typed Client SDK, GraphQL Decimal Scalar, and Centralized Config Validation (#264, #228, #269, #270, #271, #274, #276, #277, #278, #279, #283, #209, #174, #299, #300, #338, #346, #362, #363, #371)
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,12 @@ MCP_PAID_CALL_ALLOWED_HOSTS=
# against a table that has never been pruned cannot hold a long lock or blow
# the WAL. Default 1000.
SNAPSHOT_PRUNE_BATCH_SIZE=1000

# ─── GraphQL & WebSocket Subscription Hardening (#362, #363) ──────────────
# Whether GraphiQL browser playground is enabled at /graphiql. Default: true in dev, false in prod.
ENABLE_GRAPHIQL=true
# Whether GraphQL schema introspection is enabled. Default: true in dev, false in prod.
ENABLE_INTROSPECTION=true
# Maximum number of concurrent active WebSocket GraphQL price subscriptions per API key. Default: 10.
MAX_WS_SUBSCRIPTIONS_PER_KEY=10

69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,59 @@ Aggregates price data from Stellar's Classic Order Book (SDEX) and AMM Liquidity

**What Horizon doesn't do:** Horizon's `/trade_aggregations` only covers SDEX. AMM pool prices (derived from reserve ratios) are not aggregated anywhere. Lens fills this gap.

## Sixty-Second Quickstart

Get a live price in one command:

### 1. REST API
```bash
curl -s "https://lens.divinitylabs.com/v1/price/XLM/USDC?network=testnet"
```
**Response:**
```json
{
"pair": "XLM/USDC",
"price": 0.12543,
"sources": {
"sdex": 0.1254,
"amm": 0.12545
},
"network": "testnet",
"timestamp": "2026-03-30T12:00:00.000Z"
}
```
*The response returns the volume-weighted spot price (`0.12543`), individual venue quotes across SDEX and Soroswap AMM, and ISO timestamp.*

### 2. GraphQL API
```bash
curl -s -X POST https://lens.divinitylabs.com/v1/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ getPrice(assetA: \"XLM\", assetB: \"USDC\") { pair priceDecimal network } }"}'
```
**Response:**
```json
{
"data": {
"getPrice": {
"pair": "XLM/USDC",
"priceDecimal": "0.125430000000000000",
"network": "testnet"
}
}
}
```
*Returns exact 18-decimal fixed-point precision `Decimal` scalar without floating-point IEEE-754 rounding.*

### 3. Model Context Protocol (MCP) Server
Run the local MCP tool server directly:
```bash
npx -y @divinity-labs/lens-mcp --network=testnet
```
*Exposes `get_price`, `get_candles`, `get_screener`, and `get_venues` as native AI agent tools.*

## Endpoints


### REST
| Method | Path | Description |
|---|---|---|
Expand Down Expand Up @@ -508,8 +559,26 @@ 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).*


## Deployment Smoke Check

Lens includes an automated synthetic smoke check (`scripts/smoke-check.ts`) that verifies an active deployment end-to-end within seconds:

```bash
# Run against local, staging, or production
npm run smoke -- http://localhost:3002
npm run smoke -- https://lens.divinitylabs.com
```

### What it checks:
1. **Health & Readiness:** Probes `/status` to confirm database connectivity and indexer responsiveness within 10 seconds.
2. **Price & Freshness:** Queries `/v1/price/XLM/USDC?network=testnet` and asserts not only presence, but that the price timestamp is within the freshness threshold.
3. **WebSocket Lifecycle:** Connects to `/ws`, registers a pair subscription, and asserts receipt of a real streaming price update before closing cleanly.

Exits `0` on total health, or `1` with explicit failure logs on any degraded component.

## Environment Variables


| Variable | Description | Default | Required |
|---|---|---|---|
| `NODE_ENV` | Environment mode (`development`, `test`, `production`) | `development` | No |
Expand Down
57 changes: 57 additions & 0 deletions docs/graphql-decimal-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# GraphQL Decimal Scalar Migration Guide

## Problem & Context

GraphQL's built-in `Float` type is an IEEE 754 64-bit double-precision floating-point number.
In Stellar, prices are stored on-chain and in Postgres as high-precision decimals (`Decimal(36, 18)`).
When long-tail assets (e.g. priced at `0.000000000000001234`) or large quantities in the billions are cast to `Float`, IEEE 754 precision limits silently round or lose significant digits.

## The Solution: `Decimal` Scalar

Lens GraphQL schema introduces a custom `Decimal` scalar:
- Serializes as a precise **string** (e.g. `"0.000000000000001234"`).
- Preserves full 18-decimal precision end-to-end through the entire query pipeline.
- Added alongside existing `Float` fields to preserve backward compatibility for legacy clients.

## Field Additions

| Existing `Float` Field | New `Decimal` Field | Type | Description |
| :--- | :--- | :--- | :--- |
| `price` | `priceDecimal` | `Decimal!` | 18-decimal aggregated price string |
| `sdexPrice` | `sdexPriceDecimal` | `Decimal` | SDEX price string |
| `ammPrice` | `ammPriceDecimal` | `Decimal` | AMM pool price string |
| `volume24h` | `volume24hDecimal` | `Decimal` | 24-hour volume string |
| `vwap24h` | `vwap24hDecimal` | `Decimal` | 24-hour VWAP string |

## Migration Steps for Integrators

### Before:

```graphql
query GetXlmPrice {
getPrice(assetA: "XLM", assetB: "USDC") {
price
vwap24h
}
}
```

### After (Recommended for Precision):

```graphql
query GetXlmPrice {
getPrice(assetA: "XLM", assetB: "USDC") {
priceDecimal
vwap24hDecimal
}
}
```

### Client-side handling:
In TypeScript / JavaScript, parse the returned string using `BigInt`, `bignumber.js`, or `@stellar/stellar-sdk`:

```ts
import BigNumber from 'bignumber.js'

const exactPrice = new BigNumber(data.getPrice.priceDecimal)
```
49 changes: 49 additions & 0 deletions docs/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Structured Logging & Observability

Lens uses structured JSON logging via Pino (`src/logger/structured.ts`) to ensure operational telemetry is queryable by downstream aggregation engines without brittle regex parsing.

## Log Schema

Every log line is emitted as a JSON object carrying top-level metadata:

| Field | Type | Description |
| :--- | :--- | :--- |
| `level` | `number` | Pino log level (30 = info, 40 = warn, 50 = error, 60 = fatal) |
| `time` | `number` | Unix epoch timestamp in milliseconds |
| `msg` | `string` | Static human-readable message description |
| `route` | `string` | Fastify route template (e.g. `/price/:assetA/:assetB`) |
| `pair` | `string` | Asset pair identifier (e.g. `XLM/USDC`) |
| `network` | `string` | Stellar network (`testnet` or `mainnet`) |
| `venue` | `string` | Venue source (`sdex`, `amm`, `soroswap`, `aquarius`) |
| `status` | `number` | HTTP status code or operational result |
| `durationMs`| `number` | Request or task processing latency in milliseconds |
| `requestId` | `string` | Unique trace request identifier (`x-request-id`) |

---

## Example Queries

### Query 1: Every failure for a given pair in the last hour

Using `jq` on raw JSON log streams:

```bash
# Filter logs from the last 1 hour where level >= 40 (warn/error) for pair XLM/USDC
NOW_MS=$(date +%s%3N)
ONE_HOUR_AGO=$((NOW_MS - 3600000))

cat /var/log/lens/lens.log | jq -c --argjson since "$ONE_HOUR_AGO" \
'select(.time >= $since and .level >= 40 and .pair == "XLM/USDC")'
```

### Query 2: Ingestion failures grouped by venue

```bash
cat /var/log/lens/lens.log | jq -r 'select(.level >= 50 and .venue != null) | .venue' | sort | uniq -c
```

### Query 3: Slow routes (> 500ms latency)

```bash
cat /var/log/lens/lens.log | jq -c 'select(.durationMs > 500) | {route, durationMs, requestId}'
```
Loading