From c19cb4a900f96d616e67ddc1f7c80bc40aab5ae8 Mon Sep 17 00:00:00 2001 From: Riel101 <62568485+Riel101@users.noreply.github.com> Date: Wed, 27 May 2026 12:03:29 +0000 Subject: [PATCH] Added a new REST endpoint to return open DEX offers for a Stellar account --- src/index.js | 1 + src/routes/account.js | 77 +++++++++++++++++++++++++++++++++++++++++++ tests/api.test.js | 53 +++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) diff --git a/src/index.js b/src/index.js index f3adf81..dc8b6f4 100644 --- a/src/index.js +++ b/src/index.js @@ -64,6 +64,7 @@ app.get("/", (req, res) => { { method: "GET", path: "/fee-estimate", description: "Fee tiers for transaction submission" }, { method: "GET", path: "/fee-estimate?operations=N", description: "Fee estimate for N operations" }, { method: "GET", path: "/account/:id", description: "Account details, balances, signers" }, + { method: "GET", path: "/account/:id/offers", description: "Open DEX offers for an account" }, { method: "GET", path: "/transactions/:id", description: "Transaction history for an account" }, { method: "GET", path: "/transactions/:id/operations", description: "Operation history for an account" }, { method: "GET", path: "/asset/:code/:issuer", description: "Asset metadata and statistics" }, diff --git a/src/routes/account.js b/src/routes/account.js index 009a0db..337743f 100644 --- a/src/routes/account.js +++ b/src/routes/account.js @@ -116,6 +116,83 @@ router.get("/:id/summary", async (req, res, next) => { } }); +/** + * GET /account/:id/offers + * Returns all open DEX offers for an account. + * + * Query params: + * - limit (number, default: 10, max: 200) + * - cursor (string, pagination cursor from previous response) + * + * @param {string} id - Stellar account public key (G...) + */ +router.get("/:id/offers", async (req, res, next) => { + try { + const { id } = req.params; + validateAccountId(id); + + const limit = validateLimit(req.query.limit || 10, 200); + const cursor = req.query.cursor || undefined; + + let query = server.offers().forAccount(id).limit(limit); + if (cursor) query = query.cursor(cursor); + + const offerResponse = await query.call(); + const offers = offerResponse.records.map((offer) => { + const buildAsset = (assetType, assetCode, assetIssuer) => { + if (assetType === "native") { + return { + assetType: "native", + assetCode: "XLM", + assetIssuer: null, + }; + } + + return { + assetType, + assetCode, + assetIssuer, + }; + }; + + return { + id: offer.id, + selling: { + ...buildAsset( + offer.selling_asset_type, + offer.selling_asset_code, + offer.selling_asset_issuer + ), + amount: offer.amount, + }, + buying: buildAsset( + offer.buying_asset_type, + offer.buying_asset_code, + offer.buying_asset_issuer + ), + price: offer.price, + lastModifiedLedger: offer.last_modified_ledger, + }; + }); + + const hasMore = offerResponse.records.length === limit; + const nextCursor = hasMore + ? offerResponse.records[offerResponse.records.length - 1].paging_token + : null; + + return success(res, offers, { + meta: { + count: offers.length, + limit, + nextCursor, + hasMore, + }, + }); + } catch (err) { + next(err); + } +}); + /** * GET /account/:id/payments * Returns only payment and create_account operations for an account, diff --git a/tests/api.test.js b/tests/api.test.js index c4b4d43..db7a206 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -160,6 +160,59 @@ describe("StellarKit API", () => { }); }); + describe("GET /account/:id/offers", () => { + const VALID_KEY = "GBB67CMSCMGPROSFIVENXMRQ3KJWELDIUYITQI7YCKMSOPR2SNZB5NQ5"; + + it("returns open offers for a valid account", async () => { + const res = await request(app).get(`/account/${VALID_KEY}/offers`); + + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.data).toBeInstanceOf(Array); + expect(res.body).toHaveProperty("meta"); + expect(res.body.meta).toHaveProperty("count"); + expect(res.body.meta).toHaveProperty("limit"); + expect(res.body.meta).toHaveProperty("nextCursor"); + expect(res.body.meta).toHaveProperty("hasMore"); + expect(typeof res.body.meta.hasMore).toBe("boolean"); + + if (res.body.data.length > 0) { + const offer = res.body.data[0]; + expect(offer).toHaveProperty("id"); + expect(offer).toHaveProperty("selling"); + expect(offer).toHaveProperty("buying"); + expect(offer).toHaveProperty("price"); + expect(offer).toHaveProperty("lastModifiedLedger"); + expect(offer.selling).toHaveProperty("assetType"); + expect(offer.selling).toHaveProperty("assetCode"); + expect(offer.selling).toHaveProperty("assetIssuer"); + expect(offer.selling).toHaveProperty("amount"); + expect(offer.buying).toHaveProperty("assetType"); + expect(offer.buying).toHaveProperty("assetCode"); + expect(offer.buying).toHaveProperty("assetIssuer"); + } + }); + + it("respects limit query param", async () => { + const res = await request(app).get( + `/account/${VALID_KEY}/offers?limit=1` + ); + + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.meta.limit).toBe(1); + expect(res.body.data.length).toBeLessThanOrEqual(1); + }); + + it("returns 400 for invalid account ID", async () => { + const res = await request(app).get("/account/INVALID_KEY/offers"); + + expect(res.statusCode).toBe(400); + expect(res.body.success).toBe(false); + expect(res.body.error.type).toBe("ValidationError"); + }); + }); + describe("GET /account/:id/analytics", () => { it("returns analytics for a valid account", async () => { const res = await request(app).get(