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
16 changes: 12 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ STELLAR_NETWORK=testnet
PORT=3000

# Cache
# TTL for in-memory caches (milliseconds)
# Global fallback TTL for in-memory caches (milliseconds).
# Used as the default when a per-endpoint variable is not set.
CACHE_TTL_MS=5000

# Per-endpoint cache TTL overrides (all in milliseconds).
# When set, these take precedence over CACHE_TTL_MS for their respective endpoint.
CACHE_TTL_NETWORK_STATUS_MS=5000
CACHE_TTL_FEE_ESTIMATE_MS=5000
CACHE_TTL_BASE_FEE_MS=5000
CACHE_TTL_VALIDATORS_MS=300000
CACHE_TTL_ASSET_MS=30000
CACHE_TTL_ASSET_PRICE_MS=5000

# API Key authentication
# If set to "true", the API key middleware will require a valid API key
# for protected routes.
Expand All @@ -21,6 +31,4 @@ REQUIRE_API_KEY=false
# Whitespace around keys is trimmed by the middleware.
API_KEYS=validkey1,validkey2

# Cache TTL for asset price endpoint (milliseconds)
# Default: 5000 (5 seconds)
CACHE_TTL_ASSET_PRICE_MS=5000

66 changes: 66 additions & 0 deletions src/config/cacheConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Per-endpoint cache TTL configuration.
*
* Each value is read from a dedicated environment variable (in milliseconds)
* and converted to seconds for use with node-cache. A sensible default is
* provided for every endpoint so the server works out-of-the-box without any
* env configuration.
*
* Environment variables (all in milliseconds):
* CACHE_TTL_NETWORK_STATUS_MS — /network-status (default: 5 000 ms)
* CACHE_TTL_FEE_ESTIMATE_MS — /fee-estimate & surge-status (default: 5 000 ms)
* CACHE_TTL_BASE_FEE_MS — /network/base-fee (default: 5 000 ms)
* CACHE_TTL_VALIDATORS_MS — /network/validators (default: 300 000 ms)
* CACHE_TTL_ASSET_MS — /asset/:code/:issuer (default: 30 000 ms)
* CACHE_TTL_ASSET_PRICE_MS — /asset price endpoint (default: 5 000 ms)
*
* The legacy CACHE_TTL_MS variable is still respected as a global fallback so
* existing deployments are not broken.
*/

function msToSeconds(ms, defaultMs) {
const parsed = parseInt(ms, 10);
return (Number.isFinite(parsed) && parsed > 0 ? parsed : defaultMs) / 1000;
}

const globalFallbackMs = parseInt(process.env.CACHE_TTL_MS, 10) || 5000;

const cacheTTL = {
/** /network-status — one ledger close interval */
networkStatus: msToSeconds(
process.env.CACHE_TTL_NETWORK_STATUS_MS,
globalFallbackMs
),

/** /fee-estimate and /fee-estimate/surge-status */
feeEstimate: msToSeconds(
process.env.CACHE_TTL_FEE_ESTIMATE_MS,
globalFallbackMs
),

/** /network/base-fee */
baseFee: msToSeconds(
process.env.CACHE_TTL_BASE_FEE_MS,
globalFallbackMs
),

/** /network/validators — changes rarely, longer TTL by default */
validators: msToSeconds(
process.env.CACHE_TTL_VALIDATORS_MS,
300000
),

/** /asset/:code/:issuer */
asset: msToSeconds(
process.env.CACHE_TTL_ASSET_MS,
30000
),

/** asset price endpoint */
assetPrice: msToSeconds(
process.env.CACHE_TTL_ASSET_PRICE_MS,
globalFallbackMs
),
};

module.exports = cacheTTL;
9 changes: 4 additions & 5 deletions src/routes/asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
const { formatBalance } = require("../utils/formatBalance");
const { assetHoldersRateLimiter } = require("../middleware/rateLimiter");
const normalizeAssetCode = require("../middleware/normalizeAssetCode");
const { validateAccountId, validateAssetCode, validateAsset, validateLimit } = require("../utils/validators");

Check warning on line 11 in src/routes/asset.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'validateAccountId' is assigned a value but never used

Check warning on line 11 in src/routes/asset.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'validateAccountId' is assigned a value but never used
const { parsePaginationParams } = require("../utils/pagination");
const { makeAssetNotFoundError } = require("../utils/errors");
const cacheService = require("../services/cache");
const cacheTTL = require("../config/cacheConfig");
router.use(normalizeAssetCode);


Expand Down Expand Up @@ -161,8 +162,8 @@
issuer: issuerInfo,
};

// Cache the response with 30s TTL
cacheService.set(cacheKey, data, 30);
// Cache the response
cacheService.set(cacheKey, data, cacheTTL.asset);

res.set("X-Cache", "MISS");
return success(res, data);
Expand Down Expand Up @@ -419,7 +420,7 @@
try {
issuerAccount = await server.loadAccount(issuer);
checks.accountExists = { passed: true, detail: "Issuer account exists on the Stellar network." };
} catch (err) {

Check warning on line 423 in src/routes/asset.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'err' is defined but never used

Check warning on line 423 in src/routes/asset.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'err' is defined but never used
// All subsequent checks depend on account existing
return success(res, { verified: false, checks });
}
Expand Down Expand Up @@ -466,7 +467,6 @@
}
});

const CACHE_TTL_ASSET_PRICE = parseInt(process.env.CACHE_TTL_ASSET_PRICE_MS || "5000", 10);

/**
* GET /asset/:code/:issuer/price
Expand Down Expand Up @@ -520,8 +520,7 @@
quoteAsset: "XLM",
};

const ttlSeconds = CACHE_TTL_ASSET_PRICE / 1000;
cacheService.set(cacheKey, data, ttlSeconds);
cacheService.set(cacheKey, data, cacheTTL.assetPrice);

res.set("X-Cache", "MISS");
return success(res, data);
Expand Down
9 changes: 3 additions & 6 deletions src/routes/feeEstimate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ const router = express.Router();
const { server } = require("../config/stellar");
const { success } = require("../utils/response");
const cacheService = require("../services/cache");
const cacheTTL = require("../config/cacheConfig");

const STROOPS_PER_XLM = 10_000_000;

const DEFAULT_CACHE_TTL_SECONDS = 5;
const CACHE_TTL =
parseInt(process.env.CACHE_TTL_MS, 10) / 1000 || DEFAULT_CACHE_TTL_SECONDS;

const LEDGER_HISTORY_LIMIT = 5;
const STROOP_DECIMALS = 7;

Expand Down Expand Up @@ -148,7 +145,7 @@ router.get("/", async (req, res, next) => {
};

// Cache the response
cacheService.set(cacheKey, data, CACHE_TTL);
cacheService.set(cacheKey, data, cacheTTL.feeEstimate);

res.set("X-Cache", "MISS");
return success(res, data);
Expand Down Expand Up @@ -263,7 +260,7 @@ router.get("/surge-status", async (req, res, next) => {
};

// Cache the response (surge status can be cached briefly since it's analyzed data)
cacheService.set(cacheKey, data, CACHE_TTL);
cacheService.set(cacheKey, data, cacheTTL.feeEstimate);

res.set("X-Cache", "MISS");
return success(res, data);
Expand Down
8 changes: 3 additions & 5 deletions src/routes/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ const router = express.Router();
const { server, horizonUrl } = require("../config/stellar");
const { success } = require("../utils/response");
const cacheService = require("../services/cache");

const VALIDATORS_CACHE_TTL = 300; // 5 minutes
const BASE_FEE_CACHE_TTL = 5; // 5 seconds — one ledger close interval
const cacheTTL = require("../config/cacheConfig");

/**
* GET /network/base-fee
Expand Down Expand Up @@ -52,7 +50,7 @@ router.get("/base-fee", async (req, res, next) => {
note: "Base fee is in stroops. 1 XLM = 10,000,000 stroops. Cached for 5 seconds.",
};

cacheService.set(cacheKey, data, BASE_FEE_CACHE_TTL);
cacheService.set(cacheKey, data, cacheTTL.baseFee);

res.set("X-Cache", "MISS");
return success(res, data);
Expand Down Expand Up @@ -133,7 +131,7 @@ router.get("/validators", async (req, res, next) => {
ungrouped,
};

cacheService.set(cacheKey, data, VALIDATORS_CACHE_TTL);
cacheService.set(cacheKey, data, cacheTTL.validators);

res.set("X-Cache", "MISS");
return success(res, data);
Expand Down
7 changes: 3 additions & 4 deletions src/routes/networkStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ const router = express.Router();
const { server, horizonUrl, NETWORK } = require("../config/stellar");
const { success, toISOTimestamp } = require("../utils/response");
const cacheService = require("../services/cache");

const CACHE_TTL = 5; // seconds
const cacheTTL = require("../config/cacheConfig");

/**
* GET /network-status
Expand Down Expand Up @@ -57,8 +56,8 @@ router.get("/", async (req, res, next) => {
},
};

// Cache the response with 5s TTL
cacheService.set(cacheKey, data, 5);
// Cache the response
cacheService.set(cacheKey, data, cacheTTL.networkStatus);

res.set("X-Cache", "MISS");
return success(res, data);
Expand Down
177 changes: 177 additions & 0 deletions tests/cacheConfig.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"use strict";

/**
* tests/cacheConfig.test.js
*
* Unit tests for src/config/cacheConfig.js
*
* The module reads environment variables at require-time, so each test group
* sets the relevant env vars, flushes the module from the require cache, and
* re-imports it to pick up the fresh values.
*/

function loadCacheConfig() {
// Clear the module from the require cache so env changes take effect.
jest.resetModules();
return require("../src/config/cacheConfig");
}

describe("cacheConfig — default values", () => {
beforeEach(() => {
// Remove all cache-TTL-related env vars so defaults are used.
delete process.env.CACHE_TTL_MS;
delete process.env.CACHE_TTL_NETWORK_STATUS_MS;
delete process.env.CACHE_TTL_FEE_ESTIMATE_MS;
delete process.env.CACHE_TTL_BASE_FEE_MS;
delete process.env.CACHE_TTL_VALIDATORS_MS;
delete process.env.CACHE_TTL_ASSET_MS;
delete process.env.CACHE_TTL_ASSET_PRICE_MS;
});

it("networkStatus defaults to 5 seconds", () => {
const cfg = loadCacheConfig();
expect(cfg.networkStatus).toBe(5);
});

it("feeEstimate defaults to 5 seconds", () => {
const cfg = loadCacheConfig();
expect(cfg.feeEstimate).toBe(5);
});

it("baseFee defaults to 5 seconds", () => {
const cfg = loadCacheConfig();
expect(cfg.baseFee).toBe(5);
});

it("validators defaults to 300 seconds (5 minutes)", () => {
const cfg = loadCacheConfig();
expect(cfg.validators).toBe(300);
});

it("asset defaults to 30 seconds", () => {
const cfg = loadCacheConfig();
expect(cfg.asset).toBe(30);
});

it("assetPrice defaults to 5 seconds", () => {
const cfg = loadCacheConfig();
expect(cfg.assetPrice).toBe(5);
});
});

describe("cacheConfig — per-endpoint overrides", () => {
afterEach(() => {
delete process.env.CACHE_TTL_NETWORK_STATUS_MS;
delete process.env.CACHE_TTL_FEE_ESTIMATE_MS;
delete process.env.CACHE_TTL_BASE_FEE_MS;
delete process.env.CACHE_TTL_VALIDATORS_MS;
delete process.env.CACHE_TTL_ASSET_MS;
delete process.env.CACHE_TTL_ASSET_PRICE_MS;
});

it("CACHE_TTL_NETWORK_STATUS_MS overrides networkStatus TTL", () => {
process.env.CACHE_TTL_NETWORK_STATUS_MS = "10000";
const cfg = loadCacheConfig();
expect(cfg.networkStatus).toBe(10);
});

it("CACHE_TTL_FEE_ESTIMATE_MS overrides feeEstimate TTL", () => {
process.env.CACHE_TTL_FEE_ESTIMATE_MS = "20000";
const cfg = loadCacheConfig();
expect(cfg.feeEstimate).toBe(20);
});

it("CACHE_TTL_BASE_FEE_MS overrides baseFee TTL", () => {
process.env.CACHE_TTL_BASE_FEE_MS = "3000";
const cfg = loadCacheConfig();
expect(cfg.baseFee).toBe(3);
});

it("CACHE_TTL_VALIDATORS_MS overrides validators TTL", () => {
process.env.CACHE_TTL_VALIDATORS_MS = "60000";
const cfg = loadCacheConfig();
expect(cfg.validators).toBe(60);
});

it("CACHE_TTL_ASSET_MS overrides asset TTL", () => {
process.env.CACHE_TTL_ASSET_MS = "15000";
const cfg = loadCacheConfig();
expect(cfg.asset).toBe(15);
});

it("CACHE_TTL_ASSET_PRICE_MS overrides assetPrice TTL", () => {
process.env.CACHE_TTL_ASSET_PRICE_MS = "8000";
const cfg = loadCacheConfig();
expect(cfg.assetPrice).toBe(8);
});
});

describe("cacheConfig — global CACHE_TTL_MS fallback", () => {
afterEach(() => {
delete process.env.CACHE_TTL_MS;
delete process.env.CACHE_TTL_NETWORK_STATUS_MS;
delete process.env.CACHE_TTL_FEE_ESTIMATE_MS;
delete process.env.CACHE_TTL_BASE_FEE_MS;
delete process.env.CACHE_TTL_ASSET_PRICE_MS;
});

it("CACHE_TTL_MS is used as fallback for networkStatus when per-endpoint var is absent", () => {
process.env.CACHE_TTL_MS = "12000";
const cfg = loadCacheConfig();
expect(cfg.networkStatus).toBe(12);
});

it("CACHE_TTL_MS is used as fallback for feeEstimate when per-endpoint var is absent", () => {
process.env.CACHE_TTL_MS = "12000";
const cfg = loadCacheConfig();
expect(cfg.feeEstimate).toBe(12);
});

it("per-endpoint var takes precedence over CACHE_TTL_MS", () => {
process.env.CACHE_TTL_MS = "12000";
process.env.CACHE_TTL_NETWORK_STATUS_MS = "2000";
const cfg = loadCacheConfig();
expect(cfg.networkStatus).toBe(2);
});

it("CACHE_TTL_MS does NOT override the validators hard-coded default (300 s)", () => {
// validators has its own hard-coded default of 300 000 ms; CACHE_TTL_MS
// is only used for endpoints whose default is 5 000 ms.
process.env.CACHE_TTL_MS = "12000";
const cfg = loadCacheConfig();
// validators still falls back to its own 300 s default when
// CACHE_TTL_VALIDATORS_MS is absent
expect(cfg.validators).toBe(300);
});
});

describe("cacheConfig — invalid / edge-case values", () => {
afterEach(() => {
delete process.env.CACHE_TTL_NETWORK_STATUS_MS;
delete process.env.CACHE_TTL_MS;
});

it("falls back to default when env var is NaN", () => {
process.env.CACHE_TTL_NETWORK_STATUS_MS = "not-a-number";
const cfg = loadCacheConfig();
expect(cfg.networkStatus).toBe(5);
});

it("falls back to default when env var is zero", () => {
process.env.CACHE_TTL_NETWORK_STATUS_MS = "0";
const cfg = loadCacheConfig();
expect(cfg.networkStatus).toBe(5);
});

it("falls back to default when env var is negative", () => {
process.env.CACHE_TTL_NETWORK_STATUS_MS = "-1000";
const cfg = loadCacheConfig();
expect(cfg.networkStatus).toBe(5);
});

it("returns correct seconds for a large valid value", () => {
process.env.CACHE_TTL_NETWORK_STATUS_MS = "600000"; // 10 minutes
const cfg = loadCacheConfig();
expect(cfg.networkStatus).toBe(600);
});
});
Loading