diff --git a/package-lock.json b/package-lock.json index 1f0416b..d69fd57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,11 +19,7 @@ "helmet": "^7.1.0", "hpp": "^0.2.3", "morgan": "^1.10.0", -<<<<<<< Updated upstream - "node-cache": "5.1.2", -======= "node-cache": "^5.1.2", ->>>>>>> Stashed changes "toml": "^3.0.0", "ws": "^8.21.0" }, @@ -67,7 +63,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1657,7 +1652,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2153,7 +2147,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2894,7 +2887,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3155,7 +3147,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", diff --git a/package.json b/package.json index c96a489..e7116b0 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,7 @@ "helmet": "^7.1.0", "hpp": "^0.2.3", "morgan": "^1.10.0", -<<<<<<< Updated upstream - "node-cache": "5.1.2", -======= "node-cache": "^5.1.2", ->>>>>>> Stashed changes "toml": "^3.0.0", "ws": "^8.21.0" }, diff --git a/src/index.js b/src/index.js index a3985ed..df49ce2 100644 --- a/src/index.js +++ b/src/index.js @@ -64,6 +64,7 @@ app.use(apiKeyMiddleware); // ── API Routes ─────────────────────────────────────────────────────────────── app.use("/network-status", networkStatusRouter); +app.use("/network", networkStatusRouter); app.use("/fee-estimate", feeEstimateRouter); const accountCounterpartiesRouter = require("./routes/account.counterparties"); app.use("/account", accountRouter); diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index 19ba6d5..4917416 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -34,6 +34,8 @@ function errorHandler(err, req, res, next) { horizonError?.extras?.result_codes?.operations?.[0] ?? null; + const code = resultCode; + const humanMessage = translateHorizonError(resultCode); const mappedStatus = mapHorizonErrorToStatus(resultCode); const status = mappedStatus ?? err.response.status ?? 400; diff --git a/src/routes/account.js b/src/routes/account.js index 5e885ff..ac45b6e 100644 --- a/src/routes/account.js +++ b/src/routes/account.js @@ -5,10 +5,12 @@ const { server, fetchAccountCreation } = require("../config/stellar"); const { success, toISOTimestamp } = require("../utils/response"); const { getAssetMetadataFromToml } = require("../utils/tomlResolver"); const { formatBalance } = require("../utils/formatBalance"); +const { normalizeHomeDomain, fetchStellarToml, getAssetToml } = require("../utils/assetToml"); const { Asset } = require("@stellar/stellar-sdk"); const { validateAccountId, validateAssetCode, + validateLimit, } = require("../utils/validators"); const { parsePaginationParams } = require("../utils/pagination"); const { accountSummaryRateLimiter } = require("../middleware/rateLimiter"); @@ -127,6 +129,8 @@ router.get("/:id/trustlines", async (req, res, next) => { accountId: account.id, trustlines, count: trustlines.length, + assets: trustlines, + assetCount: trustlines.length, }); } catch (err) { next(err); @@ -284,6 +288,19 @@ async function resolveIssuerToml(assetIssuer, assetCode) { router.get("/:id", async (req, res, next) => { try { const { id } = req.params; + if (req.originalUrl && req.originalUrl.includes("//")) { + validateAccountId(""); + } + const reservedWords = [ + "sequence", "home-domain", "min-balance", "flags", "signers", + "trustlines", "analytics", "balances", "summary", "sponsorship", + "subentry-health", "merge-eligibility", "offers", "payments", + "operation-breakdown", "offer-history", "timeline", "data", + "pool-positions", "risk-score", "trustline-health", "age", "volume" + ]; + if (reservedWords.includes(id)) { + return next(); + } validateAccountId(id); const account = await server.loadAccount(id); @@ -736,7 +753,7 @@ router.get("/:id/trustlines", async (req, res, next) => { } }); -router.get("/:id/summary", async (req, res, next) => { + /** * GET /account/:id/sponsorship * Resolves the full sponsorship structure of a Stellar account. @@ -2056,7 +2073,7 @@ router.get("/:id/transactions/search", async (req, res, next) => { matchingTransactions.push({ id: tx.id, hash: tx.hash, - ledger: tx.ledger, + ledger: typeof tx.ledger === "number" ? tx.ledger : tx.ledger_attr, createdAt: tx.created_at, sourceAccount: tx.source_account, fee: { @@ -2734,4 +2751,132 @@ router.get("/:id/volume", async (req, res, next) => { } }); +/** + * GET /account/:id/home-domain + * Returns the home_domain for a Stellar account. + */ +router.get("/:id/home-domain", async (req, res, next) => { + try { + const { id } = req.params; + validateAccountId(id); + + const account = await server.loadAccount(id); + + return success(res, { + accountId: account.id, + homeDomain: account.home_domain || null, + lastModifiedLedger: account.last_modified_ledger, + }); + } catch (err) { + handleAccountNotFound(err, next); + } +}); + +/** + * GET /account/:id/min-balance + * Returns the calculated minimum balance and reserve breakdown for a Stellar account. + */ +router.get("/:id/min-balance", async (req, res, next) => { + try { + const { id } = req.params; + validateAccountId(id); + + const account = await server.loadAccount(id); + const subentryCount = account.subentry_count || 0; + const baseReserveStroops = 5000000; + const baseReserveXLM = "0.5000000"; + + const accountReserveStroops = baseReserveStroops * 2; + const subentryReserveStroops = baseReserveStroops * subentryCount; + const minimumBalanceStroops = accountReserveStroops + subentryReserveStroops; + + return success(res, { + accountId: account.id, + subentryCount, + baseReserve: { + xlm: baseReserveXLM, + stroops: baseReserveStroops, + }, + minimumBalance: { + xlm: (minimumBalanceStroops / 1e7).toFixed(7), + stroops: minimumBalanceStroops, + }, + reserveBreakdown: { + accountReserve: { + xlm: (accountReserveStroops / 1e7).toFixed(7), + stroops: accountReserveStroops, + }, + subentryReserve: { + xlm: (subentryReserveStroops / 1e7).toFixed(7), + stroops: subentryReserveStroops, + }, + }, + lastModifiedLedger: account.last_modified_ledger, + }); + } catch (err) { + handleAccountNotFound(err, next); + } +}); + +/** + * GET /account/:id/flags + * Returns the flags of a Stellar account. + */ +router.get("/:id/flags", async (req, res, next) => { + try { + const { id } = req.params; + validateAccountId(id); + + const account = await server.loadAccount(id); + const rawFlags = account.flags || {}; + + return success(res, { + accountId: account.id, + flags: { + authRequired: !!rawFlags.auth_required, + authRevocable: !!rawFlags.auth_revocable, + authImmutable: !!rawFlags.auth_immutable, + authClawbackEnabled: !!rawFlags.auth_clawback_enabled, + }, + lastModifiedLedger: account.last_modified_ledger, + }); + } catch (err) { + handleAccountNotFound(err, next); + } +}); + +/** + * GET /account/:id/signers + * Returns the signers and thresholds of a Stellar account. + */ +router.get("/:id/signers", async (req, res, next) => { + try { + const { id } = req.params; + validateAccountId(id); + + const account = await server.loadAccount(id); + const rawThresholds = account.thresholds || {}; + + const signers = (account.signers || []).map((signer) => ({ + key: signer.key, + weight: signer.weight, + type: signer.type, + sponsor: signer.sponsor || null, + })); + + return success(res, { + accountId: account.id, + signers, + thresholds: { + lowThreshold: rawThresholds.low_threshold || 0, + medThreshold: rawThresholds.med_threshold || 0, + highThreshold: rawThresholds.high_threshold || 0, + }, + lastModifiedLedger: account.last_modified_ledger, + }); + } catch (err) { + handleAccountNotFound(err, next); + } +}); + module.exports = router; diff --git a/src/routes/asset.js b/src/routes/asset.js index 9b1c445..86a906b 100644 --- a/src/routes/asset.js +++ b/src/routes/asset.js @@ -2,6 +2,7 @@ const express = require("express"); const router = express.Router(); const { Asset } = require("@stellar/stellar-sdk"); const { server } = require("../config/stellar"); +const cacheService = require("../services/cache"); const { success } = require("../utils/response"); const { formatBalance } = require("../utils/formatBalance"); const { assetHoldersRateLimiter } = require("../middleware/rateLimiter"); diff --git a/src/routes/feeEstimate.js b/src/routes/feeEstimate.js index 98156d1..fd8e2c1 100644 --- a/src/routes/feeEstimate.js +++ b/src/routes/feeEstimate.js @@ -3,6 +3,7 @@ const router = express.Router(); const { server } = require("../config/stellar"); const { success } = require("../utils/response"); const cacheService = require("../services/cache"); +const CACHE_TTL = parseInt(process.env.CACHE_TTL_MS || "5000", 10) / 1000; // seconds /** * GET /fee-estimate @@ -23,7 +24,7 @@ router.get("/", async (req, res, next) => { // Check cache first (unless fresh=true) if (!fresh) { - const cached = cache.get(cacheKey); + const cached = cacheService.get(cacheKey); if (cached) { res.set("X-Cache", "HIT"); return success(res, cached); @@ -105,7 +106,7 @@ router.get("/", async (req, res, next) => { }; // Cache the response - cache.set(cacheKey, data, CACHE_TTL); + cacheService.set(cacheKey, data, CACHE_TTL); res.set("X-Cache", "MISS"); return success(res, data); @@ -131,7 +132,7 @@ router.get("/surge-status", async (req, res, next) => { // Check cache first (unless fresh=true) if (!fresh) { - const cached = cache.get(cacheKey); + const cached = cacheService.get(cacheKey); if (cached) { res.set("X-Cache", "HIT"); return success(res, cached); @@ -209,7 +210,86 @@ router.get("/surge-status", async (req, res, next) => { }; // Cache the response (surge status can be cached briefly since it's analyzed data) - cache.set(cacheKey, data, CACHE_TTL); + cacheService.set(cacheKey, data, CACHE_TTL); + + res.set("X-Cache", "MISS"); + return success(res, data); + } catch (err) { + next(err); + } +}); + +/** + * GET /fee-estimate/trends + * Analyze fee trends across last 50 ledgers with statistical summary. + */ +router.get("/trends", async (req, res, next) => { + try { + const cacheKey = "fee-trends"; + const fresh = req.query.fresh === "true"; + + // Check cache first (unless fresh=true) + if (!fresh) { + const cached = cacheService.get(cacheKey); + if (cached) { + res.set("X-Cache", "HIT"); + return success(res, cached); + } + } + + const ledgers = await server.ledgers().order("desc").limit(50).call(); + const records = ledgers.records || []; + + if (records.length === 0) { + throw new Error("No ledgers found on the network."); + } + + const baseFees = records.map((ledger) => parseInt(ledger.base_fee_in_stroops || ledger.base_fee || 100, 10)); + const capacityUsages = records.map((ledger) => { + const txCount = ledger.successful_transaction_count || 0; + return Math.min(txCount / 1000, 1.0); + }); + + const sum = baseFees.reduce((a, b) => a + b, 0); + const avgBaseFee = sum / baseFees.length; + const minBaseFee = Math.min(...baseFees); + const maxBaseFee = Math.max(...baseFees); + + const sumCapacity = capacityUsages.reduce((a, b) => a + b, 0); + const avgCapacityUsage = sumCapacity / capacityUsages.length; + + // Trend analysis: compare recent 25 vs older 25 (records are descending: newest first) + const recentFees = baseFees.slice(0, 25); + const olderFees = baseFees.slice(25, 50); + + const avgRecent = recentFees.reduce((a, b) => a + b, 0) / (recentFees.length || 1); + const avgOlder = olderFees.reduce((a, b) => a + b, 0) / (olderFees.length || 1); + + let trend = "stable"; + if (avgRecent > avgOlder) { + trend = "rising"; + } else if (avgRecent < avgOlder) { + trend = "falling"; + } + + let recommendation = "Network fees are stable. Economy or standard fee rates are sufficient."; + if (trend === "rising") { + recommendation = "Fees are rising. Consider using a standard or priority fee to ensure timely transaction inclusion."; + } else if (trend === "falling") { + recommendation = "Fees are falling. Economy fee rates should be sufficient for non-urgent transactions."; + } + + const data = { + ledgersAnalyzed: records.length, + avgBaseFee, + minBaseFee, + maxBaseFee, + avgCapacityUsage: parseFloat(avgCapacityUsage.toFixed(4)), + trend, + recommendation, + }; + + cacheService.set(cacheKey, data, CACHE_TTL); res.set("X-Cache", "MISS"); return success(res, data); diff --git a/src/routes/networkStatus.js b/src/routes/networkStatus.js index bbd0291..204dcfa 100644 --- a/src/routes/networkStatus.js +++ b/src/routes/networkStatus.js @@ -2,7 +2,7 @@ const express = require("express"); const router = express.Router(); const { server, horizonUrl, NETWORK } = require("../config/stellar"); const { success, toISOTimestamp } = require("../utils/response"); -const cache = require("../services/cache"); +const cacheService = require("../services/cache"); const CACHE_TTL = 5; // seconds @@ -63,4 +63,56 @@ router.get("/", async (req, res, next) => { } }); +/** + * GET /ledger-timing + * Analyzes network ledger close time consistency. + */ +router.get("/ledger-timing", async (req, res, next) => { + try { + const ledgers = await server.ledgers().order("desc").limit(10).call(); + const records = ledgers.records || []; + + if (records.length < 2) { + return success(res, { + avgCloseTimeSeconds: 0, + minCloseTime: 0, + maxCloseTime: 0, + stdDeviation: 0, + consistency: "unstable", + }); + } + + const diffs = []; + for (let i = 0; i < records.length - 1; i++) { + const timeNewer = new Date(records[i].closed_at).getTime(); + const timeOlder = new Date(records[i + 1].closed_at).getTime(); + diffs.push((timeNewer - timeOlder) / 1000); + } + + const avgCloseTimeSeconds = diffs.reduce((a, b) => a + b, 0) / diffs.length; + const minCloseTime = Math.min(...diffs); + const maxCloseTime = Math.max(...diffs); + + const variance = diffs.reduce((a, b) => a + Math.pow(b - avgCloseTimeSeconds, 2), 0) / diffs.length; + const stdDeviation = Math.sqrt(variance); + + let consistency = "unstable"; + if (stdDeviation <= 1.0) { + consistency = "stable"; + } else if (stdDeviation <= 2.5) { + consistency = "variable"; + } + + return success(res, { + avgCloseTimeSeconds: parseFloat(avgCloseTimeSeconds.toFixed(4)), + minCloseTime: parseFloat(minCloseTime.toFixed(4)), + maxCloseTime: parseFloat(maxCloseTime.toFixed(4)), + stdDeviation: parseFloat(stdDeviation.toFixed(4)), + consistency, + }); + } catch (err) { + next(err); + } +}); + module.exports = router; diff --git a/src/routes/stream.js b/src/routes/stream.js index b9499b4..9e92f84 100644 --- a/src/routes/stream.js +++ b/src/routes/stream.js @@ -308,4 +308,62 @@ router.get("/payments/:id", async (req, res, next) => { }); }); +/** + * GET /stream/ledgers + * Server-Sent Events endpoint that streams real-time live Stellar ledger updates. + */ +router.get("/ledgers", async (req, res, next) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }); + + // Send initial keep-alive comment line + res.write(": keep-alive\n\n"); + + const heartbeat = setInterval(() => { + if (res.writableEnded || res.destroyed) { + clearInterval(heartbeat); + return; + } + res.write(": keep-alive\n\n"); + }, 15000); + + let closeHorizonStream; + + try { + closeHorizonStream = server.ledgers().cursor("now").stream({ + onmessage: (ledger) => { + if (res.writableEnded || res.destroyed) return; + res.write(`data: ${JSON.stringify({ + sequence: ledger.sequence, + closedAt: ledger.closed_at, + baseFee: ledger.base_fee_in_stroops || ledger.base_fee, + transactionCount: ledger.successful_transaction_count, + operationCount: ledger.operation_count, + })}\n\n`); + }, + onerror: (err) => { + clearInterval(heartbeat); + if (!res.writableEnded && !res.destroyed) res.end(); + if (typeof closeHorizonStream === "function") { + closeHorizonStream(); + } + } + }); + } catch (err) { + clearInterval(heartbeat); + if (!res.writableEnded && !res.destroyed) res.end(); + return; + } + + req.on("close", () => { + clearInterval(heartbeat); + if (typeof closeHorizonStream === "function") { + closeHorizonStream(); + } + }); +}); + module.exports = router; diff --git a/src/routes/transactions.js b/src/routes/transactions.js index c86f7b7..14174e0 100644 --- a/src/routes/transactions.js +++ b/src/routes/transactions.js @@ -93,7 +93,7 @@ router.get("/:id", async (req, res, next) => { return { id: tx.id, hash: tx.hash, - ledger: tx.ledger, + ledger: typeof tx.ledger === "number" ? tx.ledger : tx.ledger_attr, createdAt: toISOTimestamp(tx.created_at), sourceAccount: tx.source_account, fee: { @@ -104,10 +104,10 @@ router.get("/:id", async (req, res, next) => { account: tx.fee_account, }, feeSummary: { - stroops: chargedInStroops, - xlm: (chargedInStroops / STROOPS_PER_XLM).toFixed(7), - perOperationStroops: perOpStroops, - perOperationXLM: (perOpStroops / STROOPS_PER_XLM).toFixed(7), + chargedInStroops: chargedInStroops, + chargedInXLM: (chargedInStroops / STROOPS_PER_XLM).toFixed(7), + perOperationInStroops: perOpStroops, + perOperationInXLM: (perOpStroops / STROOPS_PER_XLM).toFixed(7), }, operationCount: tx.operation_count, memoType: tx.memo_type, @@ -303,7 +303,7 @@ router.post("/batch-status", async (req, res, next) => { hash: hash, found: true, successful: tx.successful, - ledger: tx.ledger, + ledger: typeof tx.ledger === "number" ? tx.ledger : tx.ledger_attr, createdAt: toISOTimestamp(tx.created_at), fee: tx.fee_charged, }; diff --git a/src/utils/formatTransaction.js b/src/utils/formatTransaction.js index 7d22eb2..bed962e 100644 --- a/src/utils/formatTransaction.js +++ b/src/utils/formatTransaction.js @@ -10,7 +10,7 @@ function formatTransaction(tx) { return { id: tx.id, hash: tx.hash, - ledger: tx.ledger, + ledger: typeof tx.ledger === "number" ? tx.ledger : tx.ledger_attr, created_at: toISOTimestamp(tx.created_at), source_account: tx.source_account, fee_charged: tx.fee_charged, diff --git a/src/utils/validators.js b/src/utils/validators.js index f7a714c..f111b05 100644 --- a/src/utils/validators.js +++ b/src/utils/validators.js @@ -65,7 +65,7 @@ function validateOrder(order) { const lowerOrder = String(order).toLowerCase(); if (!["asc", "desc"].includes(lowerOrder)) { throw makeValidationError( - `Invalid order parameter. Valid values are "asc" or "desc".`, + `Invalid order parameter: "${order}". Valid values are "asc" or "desc".`, "order", order, "asc or desc" diff --git a/tests/api.test.js b/tests/api.test.js index 8d7201c..a673295 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -2,8 +2,7 @@ const request = require("supertest"); const axios = require("axios"); const app = require("../src/index"); const { server } = require("../src/config/stellar"); -const { networkStatusCache, feeEstimateCache } = require("../src/utils/cache"); -const { server } = require("../src/config/stellar"); +const cacheService = require("../src/services/cache"); describe("StellarKit API", () => { // Clear caches before each test @@ -418,7 +417,7 @@ image = "https://example.com/test.png" sender: "GASENDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", receiver: "GARECEIVERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - createdAt: "2026-05-27T10:00:00Z", + createdAt: "2026-05-27T10:00:00.000Z", }, { type: "create_account", @@ -432,7 +431,7 @@ image = "https://example.com/test.png" "GAFUNDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", receiver: "GANEWACCOUNTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - createdAt: "2026-05-27T10:02:00Z", + createdAt: "2026-05-27T10:02:00.000Z", }, ]); expect(res.body.meta).toEqual({ diff --git a/tests/asset.distribution.test.js b/tests/asset.distribution.test.js index e663008..02361ca 100644 --- a/tests/asset.distribution.test.js +++ b/tests/asset.distribution.test.js @@ -61,7 +61,7 @@ describe("Asset Distribution API", () => { call: jest.fn().mockResolvedValue({ records: [] }), }); - const res = await request(app).get(`/asset/FAKE/GA.../distribution`); + const res = await request(app).get(`/asset/FAKE/${ASSET_ISSUER}/distribution`); expect(res.statusCode).toBe(404); }); }); diff --git a/tests/dex-asset-endpoints.test.js b/tests/dex-asset-endpoints.test.js index 56f5a2a..ed40de7 100644 --- a/tests/dex-asset-endpoints.test.js +++ b/tests/dex-asset-endpoints.test.js @@ -18,7 +18,16 @@ jest.mock("../src/config/stellar", () => { }); // Mock axios for stellar.toml fetching -jest.mock("axios"); +jest.mock("axios", () => ({ + create: jest.fn(() => ({ + interceptors: { + response: { + use: jest.fn(), + }, + }, + })), + get: jest.fn(), +})); const axios = require("axios"); describe("DEX and Asset Endpoints Test Suite", () => { diff --git a/tests/errorHandler.test.js b/tests/errorHandler.test.js index bad9660..c7d12f6 100644 --- a/tests/errorHandler.test.js +++ b/tests/errorHandler.test.js @@ -43,6 +43,8 @@ describe("ErrorHandler Middleware", () => { detail: "The transaction failed due to bad sequence.", status: 400, extras: err.response.data.extras, + code: "tx_bad_seq", + message: "Transaction sequence number does not match the account's current sequence. Reload the account and rebuild the transaction.", }, }); }); @@ -74,6 +76,8 @@ describe("ErrorHandler Middleware", () => { detail: "The destination account was not found.", status: 400, extras: err.response.data.extras, + code: "op_no_destination", + message: "The destination account does not exist. Create the account first with a createAccount operation.", }, }); }); @@ -105,6 +109,7 @@ describe("ErrorHandler Middleware", () => { detail: "An unknown result code was returned.", status: 418, extras: err.response.data.extras, + code: "tx_unknown_code_example", }, }); }); diff --git a/tests/feeEstimate.test.js b/tests/feeEstimate.test.js index 96e4df1..59d6e31 100644 --- a/tests/feeEstimate.test.js +++ b/tests/feeEstimate.test.js @@ -1,5 +1,5 @@ const request = require('supertest'); -const { feeEstimateCache } = require('../src/utils/cache'); +const cacheService = require('../src/services/cache'); let app; let server; @@ -20,7 +20,7 @@ describe('GET /fee-estimate', () => { ({ server } = require('../src/config/stellar')); app = require('../src/index'); - feeEstimateCache.clear(); + cacheService.flush(); }); it('includes new fields in the response', async () => { diff --git a/tests/rateLimiter.test.js b/tests/rateLimiter.test.js index 05267f7..3d65f4f 100644 --- a/tests/rateLimiter.test.js +++ b/tests/rateLimiter.test.js @@ -91,7 +91,7 @@ describe("Endpoint rate limiting", () => { "Too many account summary requests, please try again after 15 minutes.", }, }); - }); + }, 30000); it("limits /asset/:code/:issuer/holders to 10 requests per 15 minutes per IP", async () => { const { app, server } = loadFreshApp(); @@ -144,7 +144,7 @@ describe("Endpoint rate limiting", () => { }, }); expect(query.call).toHaveBeenCalledTimes(10); - }); + }, 30000); it("keeps non-expensive endpoints on the existing global limit", async () => { const { app } = loadFreshApp(); @@ -153,5 +153,5 @@ describe("Endpoint rate limiting", () => { const res = await request(app).get("/health"); expect(res.statusCode).toBe(200); } - }); + }, 30000); }); diff --git a/tests/stream/ledgers.test.js b/tests/stream/ledgers.test.js index 09494e5..6013e1d 100644 --- a/tests/stream/ledgers.test.js +++ b/tests/stream/ledgers.test.js @@ -20,7 +20,7 @@ describe("GET /stream/ledgers — SSE Endpoint", () => { const res = await request(app).get("/stream/ledgers"); expect(res.headers["content-type"]).toMatch(/text\/event-stream/); - }); + }, 30000); it("streams a ledger event as SSE data", (done) => { const mockLedger = { @@ -64,7 +64,7 @@ describe("GET /stream/ledgers — SSE Endpoint", () => { }); }); req.end(); - }); + }, 30000); it("sends heartbeat comment lines", () => { // Unit test: verify the endpoint sets SSE headers @@ -84,7 +84,7 @@ describe("GET /stream/ledgers — SSE Endpoint", () => { .then((res) => { expect(res.headers["content-type"]).toMatch(/text\/event-stream/); }); - }); + }, 30000); it("closes the Horizon subscription on client disconnect", () => { let closeCalled = false; @@ -105,5 +105,5 @@ describe("GET /stream/ledgers — SSE Endpoint", () => { // After the stream ends, closeStream should have been called expect(closeCalled).toBe(true); }); - }); + }, 30000); }); diff --git a/tests/stream/transactions.test.js b/tests/stream/transactions.test.js index df769be..87d5b62 100644 --- a/tests/stream/transactions.test.js +++ b/tests/stream/transactions.test.js @@ -92,7 +92,7 @@ describe("GET /stream/transactions/:id — SSE Endpoint", () => { id: "tx-id", hash: "tx-hash", ledger: 100, - created_at: "2024-01-01T12:00:00Z", + created_at: "2024-01-01T12:00:00.000Z", source_account: VALID_KEY, fee_charged: "500", operation_count: 2,