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
12 changes: 6 additions & 6 deletions src/routes/account.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -150,16 +150,16 @@
.order("asc")
.call();
transactions = response.records || [];
} catch (_) {

Check warning on line 153 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'_' is defined but never used

Check warning on line 153 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'_' is defined but never used
transactions = [];
}

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
Expand Down Expand Up @@ -580,7 +580,7 @@
}

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);
Expand Down Expand Up @@ -849,7 +849,7 @@
trustline.assetCode
);
}
} catch (_) {

Check warning on line 852 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'_' is defined but never used

Check warning on line 852 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'_' is defined but never used
// Issuer account info and TOML are optional; continue if unreachable
}

Expand Down Expand Up @@ -995,7 +995,7 @@
},
sender: isPayment ? op.from : op.funder,
receiver: isPayment ? op.to : op.account,
createdAt: op.created_at,
createdAt: toISOTimestamp(op.created_at),
});
lastPaymentIndex = idx;
}
Expand Down Expand Up @@ -1160,7 +1160,7 @@
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,
};
});
Expand Down Expand Up @@ -1368,7 +1368,7 @@
for (const signerKey of signers) {
try {
validateAccountId(signerKey);
} catch (e) {

Check warning on line 1371 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'e' is defined but never used

Check warning on line 1371 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'e' is defined but never used
const err = new Error(`Invalid signer key: "${signerKey}".`);
err.status = 400;
return next(err);
Expand Down Expand Up @@ -1441,7 +1441,7 @@
for (const signerKey of availableSigners) {
try {
validateAccountId(signerKey);
} catch (e) {

Check warning on line 1444 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'e' is defined but never used

Check warning on line 1444 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'e' is defined but never used
const err = new Error(`Invalid signer key: "${signerKey}".`);
err.status = 400;
return next(err);
Expand Down Expand Up @@ -1684,7 +1684,7 @@
let decodedValue = null;
try {
decodedValue = Buffer.from(rawValue, "base64").toString("utf8");
} catch (e) {

Check warning on line 1687 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'e' is defined but never used

Check warning on line 1687 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'e' is defined but never used
// Not decodable as UTF-8
}

Expand Down Expand Up @@ -1728,7 +1728,7 @@
let decodedValue = null;
try {
decodedValue = Buffer.from(rawValue, "base64").toString("utf8");
} catch (e) {

Check warning on line 1731 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'e' is defined but never used

Check warning on line 1731 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'e' is defined but never used
// Not decodable as UTF-8
}

Expand Down Expand Up @@ -2085,7 +2085,7 @@
}

return result;
} catch (err) {

Check warning on line 2088 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'err' is defined but never used

Check warning on line 2088 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'err' is defined but never used
// Path finding might fail if no market exists or 400 from Horizon
return result;
}
Expand Down Expand Up @@ -2430,7 +2430,7 @@
validateAccountId(id);

const LIMIT = 100;
const opResponse = await server

Check warning on line 2433 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'opResponse' is assigned a value but never used

Check warning on line 2433 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'opResponse' is assigned a value but never used
.operations()
.forAccount(id)
.limit(LIMIT)
Expand Down
4 changes: 2 additions & 2 deletions src/routes/networkStatus.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions src/routes/transactions.js
Original file line number Diff line number Diff line change
@@ -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");
/**
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion src/utils/formatTransaction.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { toISOTimestamp } = require("./response");

/**
* Formats a Horizon transaction record into a clean SSE payload.
*
Expand All @@ -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,
Expand Down
25 changes: 18 additions & 7 deletions src/utils/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -37,4 +48,4 @@ function stripLinks(obj) {
return rest;
}

module.exports = { success, formatTimestamp, stripLinks };
module.exports = { success, toISOTimestamp, formatTimestamp, stripLinks };
4 changes: 2 additions & 2 deletions tests/transactions.batch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ 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({
hash: ANOTHER_VALID_HASH,
found: true,
successful: false,
ledger: 12346,
createdAt: "2024-05-28T10:05:00Z",
createdAt: "2024-05-28T10:05:00.000Z",
fee: "200",
});
});
Expand Down
57 changes: 57 additions & 0 deletions tests/utils.toISOTimestamp.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading