diff --git a/src/openapi.ts b/src/openapi.ts index bf08b0c..48fe219 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -52,6 +52,9 @@ export function buildOpenApiSpec(): Record { "/api/v1/liquidity/entries": { get: { summary: "List raw per-anchor liquidity entries" }, }, + "/api/v1/liquidity/anchors/{anchor}": { + get: { summary: "List raw liquidity entries for a single anchor" }, + }, "/api/v1/liquidity/{asset}": { get: { summary: "Read the aggregated pool for one asset" }, }, diff --git a/src/repositories/liquidityRepository.test.ts b/src/repositories/liquidityRepository.test.ts index cede6a0..fe5f5ec 100644 --- a/src/repositories/liquidityRepository.test.ts +++ b/src/repositories/liquidityRepository.test.ts @@ -37,6 +37,17 @@ describe("LiquidityRepository", () => { expect(repo.byAsset("EURC")).toHaveLength(1); }); + it("filters entries by anchor", () => { + const repo = new LiquidityRepository(); + repo.upsert(entry("anchorA", "USDC", 100)); + repo.upsert(entry("anchorB", "USDC", 50)); + repo.upsert(entry("anchorA", "EURC", 75)); + + expect(repo.byAnchor("anchorA")).toHaveLength(2); + expect(repo.byAnchor("anchorB")).toHaveLength(1); + expect(repo.byAnchor("anchorC")).toHaveLength(0); + }); + it("aggregates pools per asset", () => { const repo = new LiquidityRepository(); repo.upsert(entry("anchorA", "USDC", 100)); diff --git a/src/repositories/liquidityRepository.ts b/src/repositories/liquidityRepository.ts index 3114105..b1e14e1 100644 --- a/src/repositories/liquidityRepository.ts +++ b/src/repositories/liquidityRepository.ts @@ -34,6 +34,11 @@ export class LiquidityRepository extends InMemoryRepository e.asset === asset); } + /** Returns all entries for a given anchor. */ + byAnchor(anchor: string): LiquidityEntry[] { + return this.listAll().filter((e) => e.anchor === anchor); + } + /** Returns every stored entry. */ all(): LiquidityEntry[] { return this.listAll(); diff --git a/src/routes/liquidity.test.ts b/src/routes/liquidity.test.ts index dfdb13f..5a276dd 100644 --- a/src/routes/liquidity.test.ts +++ b/src/routes/liquidity.test.ts @@ -171,4 +171,37 @@ describe("liquidity routes", () => { expect(res.body.error.code).toBe("BAD_REQUEST"); } }); + + it("lists entries by anchor via GET /anchors/:anchor", async () => { + const app = createApp(); + await request(app) + .post("/api/v1/liquidity") + .send({ anchor: "anchorA", asset: "USDC", amount: 500 }); + await request(app) + .post("/api/v1/liquidity") + .send({ anchor: "anchorB", asset: "USDC", amount: 300 }); + await request(app) + .post("/api/v1/liquidity") + .send({ anchor: "anchorA", asset: "EURC", amount: 150 }); + + const res = await request(app).get("/api/v1/liquidity/anchors/anchorA"); + + expect(res.status).toBe(200); + expect(res.body.entries).toHaveLength(2); + + const assets = res.body.entries.map((e: any) => e.asset).sort(); + expect(assets).toEqual(["EURC", "USDC"]); + }); + + it("does not shadow /:asset with /anchors/:anchor", async () => { + const app = createApp(); + // Insert an asset named "anchors" just to be sure it can still be fetched + await request(app) + .post("/api/v1/liquidity") + .send({ anchor: "anchorA", asset: "ANCHORS", amount: 500 }); + + const res = await request(app).get("/api/v1/liquidity/ANCHORS"); + expect(res.status).toBe(200); + expect(res.body.total).toBe(500); + }); }); diff --git a/src/routes/liquidity.ts b/src/routes/liquidity.ts index ea5ad9b..843a7c9 100644 --- a/src/routes/liquidity.ts +++ b/src/routes/liquidity.ts @@ -35,6 +35,11 @@ export function liquidityRouter(service: LiquidityService): Router { res.json(service.removeEntry(req.params.anchor, req.params.asset)); }); + // Read the raw liquidity entries for a single anchor. + router.get("/anchors/:anchor", (req: Request, res: Response) => { + res.json({ entries: service.listByAnchor(req.params.anchor) }); + }); + // Read the aggregated pool for a single asset. router.get("/:asset", (req: Request, res: Response) => { res.json(service.getPool(req.params.asset)); diff --git a/src/services/liquidityService.test.ts b/src/services/liquidityService.test.ts index 5e390c3..266cb61 100644 --- a/src/services/liquidityService.test.ts +++ b/src/services/liquidityService.test.ts @@ -128,4 +128,19 @@ describe("LiquidityService", () => { expect.objectContaining({ status: 404, code: "NOT_FOUND" }), ); }); + + it("lists entries by anchor", () => { + const service = makeService(); + service.addLiquidity({ anchor: "anchorA", asset: "USDC", amount: 100 }); + service.addLiquidity({ anchor: "anchorB", asset: "USDC", amount: 50 }); + service.addLiquidity({ anchor: "anchorA", asset: "EURC", amount: 75 }); + + const entriesA = service.listByAnchor("anchorA"); + expect(entriesA).toHaveLength(2); + expect(entriesA.map(e => e.asset).sort()).toEqual(["EURC", "USDC"]); + + const entriesB = service.listByAnchor("anchorB"); + expect(entriesB).toHaveLength(1); + expect(entriesB[0].asset).toBe("USDC"); + }); }); diff --git a/src/services/liquidityService.ts b/src/services/liquidityService.ts index cf9555a..62583dc 100644 --- a/src/services/liquidityService.ts +++ b/src/services/liquidityService.ts @@ -131,4 +131,10 @@ export class LiquidityService { listEntries(): LiquidityEntry[] { return this.repo.all(); } + + /** Returns all raw liquidity entries for a given anchor. */ + listByAnchor(anchorInput: unknown): LiquidityEntry[] { + const anchor = requireString(anchorInput, "anchor"); + return this.repo.byAnchor(anchor); + } }