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/graphql-security-and-pools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"lens": minor
---

GraphQL security hardening, network selection, pair matcher issuer preservation, and pools query:
- Pair matcher issuer preservation: `findPair` in `src/api/rest.ts` and `src/api/graphql.ts` matches both code and issuer for qualified assets, properly canonicalizing `native` to XLM, while preserving bare code matching for REST and GraphQL queries (#357).
- Network selection in GraphQL: added optional `network` argument on `getPrice`, `getBestRoute`, `getPriceHistory`, `listPairs`, `pairs`, and `pools`, with support for the `x-network` header, returning `INVALID_INPUT` when an unrecognized network is specified (#359).
- Maximum query depth ceiling: implemented query depth validation with configurable `GRAPHQL_MAX_DEPTH` (default 6) via environment variable or server options, rejecting overly nested queries before execution while exempting introspection (`__schema`) (#360).
- Typed `pairs(network: String): [PairInfo!]!` query returning structured asset information (`code`, `issuer`), `network`, and update timestamps matching REST `/pairs` (#365).
- `pools(pair: String, network: String): [PoolInfo!]!` query returning AMM reserves, spot prices, and 64-character pool IDs, sharing `queryPools` logic with REST `/pools` (#366).
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,9 @@ 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 Query Depth Ceiling ───────────────────────────────────────────
# Maximum allowable nesting depth for arbitrary GraphQL queries (default: 6).
# Queries exceeding this depth are rejected before execution to prevent resource exhaustion.
GRAPHQL_MAX_DEPTH=6

53 changes: 48 additions & 5 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,38 @@ type PriceUpdate {
network: String!
}

type PairAsset {
code: String!
issuer: String
}

type PairInfo {
pairKey: String!
assetA: PairAsset!
assetB: PairAsset!
network: String!
latestPrice: Float
lastUpdated: String
}

type PoolInfo {
poolId: String!
pool_id: String!
assetA: String!
asset_a: String!
assetB: String!
asset_b: String!
reserveA: Float
reserve_a: Float
reserveB: Float
reserve_b: Float
spotPrice: Float
spot_price: Float
feeBp: Int
fee_bp: Int
timestamp: String
}

enum OracleSourceStatus {
NOT_CONFIGURED
AVAILABLE
Expand Down Expand Up @@ -90,16 +122,16 @@ type Query {

- Malformed asset input (an asset that is not CODE or CODE:ISSUER) fails
with a GraphQL error whose extensions.code is INVALID_INPUT.
- A well-formed pair that is not tracked on the active network fails with
- A well-formed pair that is not tracked on the requested network fails with
a GraphQL error whose extensions.code is PAIR_NOT_FOUND.
- A tracked pair with no trade data yet is not an error: it resolves to an
AggregatedPrice with zeroed figures rather than null or an error.

The result is only null if the resolver itself cannot produce a payload;
clients should branch on extensions.code, not on null.
"""
getPrice(assetA: String!, assetB: String!): AggregatedPrice
getBestRoute(assetA: String!, assetB: String!, amount: Float!): RouteInfo
getPrice(assetA: String!, assetB: String!, network: String): AggregatedPrice
getBestRoute(assetA: String!, assetB: String!, amount: Float!, network: String): RouteInfo

"""
Returns historical price aggregate buckets for a pair over an optional [from, to] time range.
Expand All @@ -108,8 +140,19 @@ type Query {
Because both endpoints are inclusive, querying adjacent windows (e.g. [t0, t1] and [t1, t2]) will
return the boundary bucket at t1 in both results.
"""
getPriceHistory(assetA: String!, assetB: String!, window: String!, limit: Int, from: String, to: String): [PriceBucket]
listPairs: [String]!
getPriceHistory(assetA: String!, assetB: String!, window: String!, limit: Int, from: String, to: String, network: String): [PriceBucket]
listPairs(network: String): [String]!

"""
Returns full metadata for watched pairs on the network, including codes, issuers, and latest prints.
"""
pairs(network: String): [PairInfo!]!

"""
Returns latest snapshot of AMM liquidity pools for a network, optionally filtered by pair.
NOTE: Under L188/x402, this query returns AMM pool reserves and spot prices and should be gated consistently with REST /pools ($0.05).
"""
pools(pair: String, network: String): [PoolInfo!]!

"""
Compares the Lens price for an asset against every configured oracle.
Expand Down
Loading