From e108082b9915e74c0d4649960be9cc28507ca400 Mon Sep 17 00:00:00 2001 From: sadiqabubakar826-del Date: Tue, 2 Jun 2026 12:07:00 +0000 Subject: [PATCH] feat: add toISOTimestamp utility and apply consistently across routes --- src/routes/account.js | 12 +++---- src/routes/networkStatus.js | 4 +-- src/routes/transactions.js | 8 ++--- src/utils/formatTransaction.js | 4 ++- src/utils/response.js | 25 +++++++++---- tests/transactions.batch.test.js | 4 +-- tests/utils.toISOTimestamp.test.js | 57 ++++++++++++++++++++++++++++++ 7 files changed, 92 insertions(+), 22 deletions(-) create mode 100644 tests/utils.toISOTimestamp.test.js diff --git a/src/routes/account.js b/src/routes/account.js index 5525479..d49e1bc 100644 --- a/src/routes/account.js +++ b/src/routes/account.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); const { server, fetchAccountCreation } = require("../config/stellar"); -const { success } = require("../utils/response"); +const { success, toISOTimestamp } = require("../utils/response"); const { getAssetMetadataFromToml } = require("../utils/tomlResolver"); const { formatBalance } = require("../utils/formatBalance"); const { Asset } = require("@stellar/stellar-sdk"); @@ -157,9 +157,9 @@ router.get("/:id/analytics", async (req, res, next) => { const successfulTransactions = transactions.filter( (transaction) => transaction.successful !== false, ); - const firstSeen = successfulTransactions[0]?.created_at || null; + const firstSeen = toISOTimestamp(successfulTransactions[0]?.created_at) || null; const lastSeen = - successfulTransactions[successfulTransactions.length - 1]?.created_at || + toISOTimestamp(successfulTransactions[successfulTransactions.length - 1]?.created_at) || null; const activeDays = firstSeen && lastSeen @@ -580,7 +580,7 @@ router.get("/:id/inactivity", async (req, res, next) => { } const lastTx = txResponse.records[0]; - const lastTransactionAt = lastTx.created_at; + const lastTransactionAt = toISOTimestamp(lastTx.created_at); const lastTransactionHash = lastTx.hash; const lastTxDate = new Date(lastTransactionAt); @@ -995,7 +995,7 @@ router.get("/:id/payments", async (req, res, next) => { }, sender: isPayment ? op.from : op.funder, receiver: isPayment ? op.to : op.account, - createdAt: op.created_at, + createdAt: toISOTimestamp(op.created_at), }); lastPaymentIndex = idx; } @@ -1160,7 +1160,7 @@ router.get("/:id/offer-history", async (req, res, next) => { buyingAsset: formatAsset(op.buying_asset_type, op.buying_asset_code, op.buying_asset_issuer), amount: op.amount, price: op.price, - timestamp: op.created_at, + timestamp: toISOTimestamp(op.created_at), transactionHash: op.transaction_hash, }; }); diff --git a/src/routes/networkStatus.js b/src/routes/networkStatus.js index 5dcfc20..d5534f2 100644 --- a/src/routes/networkStatus.js +++ b/src/routes/networkStatus.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); const { server, horizonUrl, NETWORK } = require("../config/stellar"); -const { success } = require("../utils/response"); +const { success, toISOTimestamp } = require("../utils/response"); const cache = require("../services/cache"); const CACHE_TTL = 5; // seconds @@ -36,7 +36,7 @@ router.get("/", async (req, res, next) => { horizonUrl, latestLedger: { sequence: latest.sequence, - closedAt: latest.closed_at, + closedAt: toISOTimestamp(latest.closed_at), transactionCount: latest.successful_transaction_count, operationCount: latest.operation_count, totalCoins: latest.total_coins, diff --git a/src/routes/transactions.js b/src/routes/transactions.js index 4252cd5..a65daa8 100644 --- a/src/routes/transactions.js +++ b/src/routes/transactions.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); const { server } = require("../config/stellar"); -const { success } = require("../utils/response"); +const { success, toISOTimestamp } = require("../utils/response"); const { validateAccountId } = require("../utils/validators"); const { parsePaginationParams } = require("../utils/pagination"); /** @@ -93,7 +93,7 @@ router.get("/:id", async (req, res, next) => { id: tx.id, hash: tx.hash, ledger: tx.ledger, - createdAt: tx.created_at, + createdAt: toISOTimestamp(tx.created_at), sourceAccount: tx.source_account, fee: { charged: tx.fee_charged, @@ -202,7 +202,7 @@ router.get("/:id/operations", async (req, res, next) => { const base = { id: op.id, type: op.type, - createdAt: op.created_at, + createdAt: toISOTimestamp(op.created_at), transactionHash: op.transaction_hash, transactionSuccessful: op.transaction_successful, sourceAccount: op.source_account, @@ -312,7 +312,7 @@ router.post("/batch-status", async (req, res, next) => { found: true, successful: tx.successful, ledger: tx.ledger, - createdAt: tx.created_at, + createdAt: toISOTimestamp(tx.created_at), fee: tx.fee_charged, }; } catch (err) { diff --git a/src/utils/formatTransaction.js b/src/utils/formatTransaction.js index 21fdba9..7d22eb2 100644 --- a/src/utils/formatTransaction.js +++ b/src/utils/formatTransaction.js @@ -1,3 +1,5 @@ +const { toISOTimestamp } = require("./response"); + /** * Formats a Horizon transaction record into a clean SSE payload. * @@ -9,7 +11,7 @@ function formatTransaction(tx) { id: tx.id, hash: tx.hash, ledger: tx.ledger, - created_at: tx.created_at, + created_at: toISOTimestamp(tx.created_at), source_account: tx.source_account, fee_charged: tx.fee_charged, operation_count: tx.operation_count, diff --git a/src/utils/response.js b/src/utils/response.js index e3dcc19..d2c1a6b 100644 --- a/src/utils/response.js +++ b/src/utils/response.js @@ -15,16 +15,27 @@ function success(res, data, meta = {}) { } /** - * Formats a Stellar timestamp into a readable ISO string. + * Converts any timestamp value to an ISO 8601 string. * - * @param {string|number|Date|null|undefined} ts - The timestamp to format. - * @returns {string|null} ISO string or null when timestamp is falsy. + * Handles: + * - Unix timestamps in seconds (number < 1e12) + * - Unix timestamps in milliseconds (number >= 1e12) + * - Stellar date strings (e.g. "2024-07-01T12:00:00Z") + * - JavaScript Date objects + * - Falsy values → null + * + * @param {string|number|Date|null|undefined} value - The timestamp to format. + * @returns {string|null} ISO 8601 string or null when value is falsy. */ -function formatTimestamp(ts) { - if (!ts) return null; - return new Date(ts).toISOString(); +function toISOTimestamp(value) { + if (!value && value !== 0) return null; + const ms = typeof value === "number" && value < 1e12 ? value * 1000 : value; + return new Date(ms).toISOString(); } +/** @deprecated Use toISOTimestamp instead */ +const formatTimestamp = toISOTimestamp; + /** * Strips unnecessary Horizon _links fields from a record. * @@ -37,4 +48,4 @@ function stripLinks(obj) { return rest; } -module.exports = { success, formatTimestamp, stripLinks }; +module.exports = { success, toISOTimestamp, formatTimestamp, stripLinks }; diff --git a/tests/transactions.batch.test.js b/tests/transactions.batch.test.js index 15e4bd0..174f006 100644 --- a/tests/transactions.batch.test.js +++ b/tests/transactions.batch.test.js @@ -53,7 +53,7 @@ describe("Transaction Batch Status Checker", () => { found: true, successful: true, ledger: 12345, - createdAt: "2024-05-28T10:00:00Z", + createdAt: "2024-05-28T10:00:00.000Z", fee: "100", }); expect(res.body.data[1]).toEqual({ @@ -61,7 +61,7 @@ describe("Transaction Batch Status Checker", () => { found: true, successful: false, ledger: 12346, - createdAt: "2024-05-28T10:05:00Z", + createdAt: "2024-05-28T10:05:00.000Z", fee: "200", }); }); diff --git a/tests/utils.toISOTimestamp.test.js b/tests/utils.toISOTimestamp.test.js new file mode 100644 index 0000000..d80c53f --- /dev/null +++ b/tests/utils.toISOTimestamp.test.js @@ -0,0 +1,57 @@ +"use strict"; + +const { toISOTimestamp } = require("../src/utils/response"); + +describe("toISOTimestamp", () => { + // Falsy / null handling + it("returns null for null", () => { + expect(toISOTimestamp(null)).toBeNull(); + }); + + it("returns null for undefined", () => { + expect(toISOTimestamp(undefined)).toBeNull(); + }); + + it("returns null for empty string", () => { + expect(toISOTimestamp("")).toBeNull(); + }); + + // Stellar / ISO date strings + it("handles a Stellar ISO date string", () => { + expect(toISOTimestamp("2024-07-01T12:00:00Z")).toBe("2024-07-01T12:00:00.000Z"); + }); + + it("handles a date string with milliseconds", () => { + expect(toISOTimestamp("2024-07-01T12:00:00.500Z")).toBe("2024-07-01T12:00:00.500Z"); + }); + + // JavaScript Date objects + it("handles a Date object", () => { + const d = new Date("2024-07-01T12:00:00.000Z"); + expect(toISOTimestamp(d)).toBe("2024-07-01T12:00:00.000Z"); + }); + + // Unix timestamps in seconds (< 1e12) + it("handles a Unix timestamp in seconds", () => { + // 1719835200 = 2024-07-01T12:00:00.000Z + expect(toISOTimestamp(1719835200)).toBe("2024-07-01T12:00:00.000Z"); + }); + + // Unix timestamps in milliseconds (>= 1e12) + it("handles a Unix timestamp in milliseconds", () => { + expect(toISOTimestamp(1719835200000)).toBe("2024-07-01T12:00:00.000Z"); + }); + + // Output format + it("always returns a string ending in Z (UTC)", () => { + const result = toISOTimestamp("2024-07-01T12:00:00Z"); + expect(result).toMatch(/Z$/); + }); + + it("output matches ISO 8601 pattern", () => { + const iso8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + expect(toISOTimestamp("2024-07-01T12:00:00Z")).toMatch(iso8601); + expect(toISOTimestamp(1719835200)).toMatch(iso8601); + expect(toISOTimestamp(new Date("2024-07-01T12:00:00Z"))).toMatch(iso8601); + }); +});