Skip to content
Merged
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
50 changes: 50 additions & 0 deletions src/routes/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ const router = express.Router();
const { success } = require("../utils/response");
const { validateAccountId } = require("../utils/validators");
const { Transaction, Networks, Keypair } = require("@stellar/stellar-sdk");
const { server } = require("../config/stellar");

const FRIENDBOT_URL = "https://friendbot.stellar.org";
const STROOPS_PER_XLM = 10000000n;
const AVERAGE_LEDGER_CLOSE_SECONDS = 5;
const { decodeMemo } = require("../utils/memo");

function createValidationError(message) {
Expand Down Expand Up @@ -240,6 +242,54 @@ router.get("/convert", (req, res, next) => {
}
});

function parseLedgerSequence(value) {
if (typeof value !== "string" || value.trim() === "") {
throw createValidationError("Query parameter 'sequence' is required and must be a positive integer.");
}

if (!/^\d+$/.test(value)) {
throw createValidationError("Query parameter 'sequence' must be a positive integer.");
}

const sequence = Number(value);
if (sequence <= 0 || !Number.isSafeInteger(sequence)) {
throw createValidationError("Query parameter 'sequence' must be a positive integer.");
}

return sequence;
}

/**
* GET /utils/ledger-date?sequence={sequence}
* Estimate the approximate date and time a Stellar ledger sequence was closed.
*/
router.get("/ledger-date", async (req, res, next) => {
try {
const sequence = parseLedgerSequence(req.query.sequence);
const latestResponse = await server.ledgers().order("desc").limit(1).call();
const latestLedger = latestResponse.records && latestResponse.records[0];

if (!latestLedger || !latestLedger.closed_at || !latestLedger.sequence) {
throw new Error("Unable to determine the latest ledger from Horizon.");
}

const latestSequence = Number(latestLedger.sequence);
const latestClosedAt = new Date(latestLedger.closed_at);
const sequenceDelta = latestSequence - sequence;
const estimatedDate = new Date(
latestClosedAt.getTime() - sequenceDelta * AVERAGE_LEDGER_CLOSE_SECONDS * 1000,
);

return success(res, {
sequence,
estimatedDate: estimatedDate.toISOString(),
note: "This date is an approximation based on an average Stellar ledger close time of ~5 seconds.",
});
} catch (err) {
next(err);
}
});

/**
* GET /utils/validate-asset?code={code}
* Validate whether a given string is a valid Stellar asset code.
Expand Down
50 changes: 50 additions & 0 deletions tests/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,56 @@ describe("StellarKit API", () => {
});
});

describe("GET /utils/ledger-date", () => {
afterEach(() => {
jest.restoreAllMocks();
});

it("returns estimated ledger close date for a valid sequence", async () => {
const stellarConfig = require("../src/config/stellar");
jest.spyOn(stellarConfig.server, "ledgers").mockReturnValue({
order: () => ({
limit: () => ({
call: async () => ({
records: [
{
sequence: "12350",
closed_at: "2026-06-02T12:00:00Z",
},
],
}),
}),
}),
});

const res = await request(app).get("/utils/ledger-date?sequence=12345");

expect(res.statusCode).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.data).toHaveProperty("sequence", 12345);
expect(res.body.data).toHaveProperty("estimatedDate");
expect(res.body.data.estimatedDate).toBe("2026-06-02T11:59:35.000Z");
expect(res.body.data).toHaveProperty("note");
expect(res.body.data.note).toContain("approximation");
});

it("returns 400 for non-positive sequence values", async () => {
const res = await request(app).get("/utils/ledger-date?sequence=0");

expect(res.statusCode).toBe(400);
expect(res.body.success).toBe(false);
expect(res.body.error.type).toBe("ValidationError");
});

it("returns 400 for invalid sequence format", async () => {
const res = await request(app).get("/utils/ledger-date?sequence=abc");

expect(res.statusCode).toBe(400);
expect(res.body.success).toBe(false);
expect(res.body.error.type).toBe("ValidationError");
});
});

// ── DEX Price ──────────────────────────────────────────────────────────────
describe("GET /dex/price/:sellAsset/:buyAsset", () => {
const USDC_ISSUER = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
Expand Down
Loading