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
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
77 changes: 77 additions & 0 deletions src/routes/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
53 changes: 53 additions & 0 deletions tests/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down