From a2f9b7989dbe2a889904d2bbc19ed94edc3a3c90 Mon Sep 17 00:00:00 2001 From: Obiajulu-gif Date: Fri, 27 Mar 2026 22:50:08 +0100 Subject: [PATCH] Add price validation for market rate fetchers --- dist/services/marketRate/ghsFetcher.d.ts.map | 2 +- dist/services/marketRate/ghsFetcher.js | 15 +++++----- dist/services/marketRate/ghsFetcher.js.map | 2 +- dist/services/marketRate/kesFetcher.d.ts.map | 2 +- dist/services/marketRate/kesFetcher.js | 5 ++-- dist/services/marketRate/kesFetcher.js.map | 2 +- dist/services/marketRate/validation.d.ts | 2 ++ dist/services/marketRate/validation.d.ts.map | 1 + dist/services/marketRate/validation.js | 7 +++++ dist/services/marketRate/validation.js.map | 1 + src/services/marketRate/ghsFetcher.ts | 19 +++++++------ src/services/marketRate/kesFetcher.ts | 5 ++-- src/services/marketRate/validation.ts | 7 +++++ test/ghsFetcher.test.ts | 30 ++++++++++++++++++++ test/validation.test.ts | 13 +++++++++ 15 files changed, 89 insertions(+), 24 deletions(-) create mode 100644 dist/services/marketRate/validation.d.ts create mode 100644 dist/services/marketRate/validation.d.ts.map create mode 100644 dist/services/marketRate/validation.js create mode 100644 dist/services/marketRate/validation.js.map create mode 100644 src/services/marketRate/validation.ts create mode 100644 test/validation.test.ts diff --git a/dist/services/marketRate/ghsFetcher.d.ts.map b/dist/services/marketRate/ghsFetcher.d.ts.map index 3359d7be..c65fb1ef 100644 --- a/dist/services/marketRate/ghsFetcher.d.ts.map +++ b/dist/services/marketRate/ghsFetcher.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"ghsFetcher.d.ts","sourceRoot":"","sources":["../../../src/services/marketRate/ghsFetcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAkBxD,qBAAa,cAAe,YAAW,iBAAiB;IACtD,OAAO,CAAC,QAAQ,CAAC,YAAY,CACoF;IAEjH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2C;IAEvE,WAAW,IAAI,MAAM;IAIf,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;IA8DhC,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAQpC"} \ No newline at end of file +{"version":3,"file":"ghsFetcher.d.ts","sourceRoot":"","sources":["../../../src/services/marketRate/ghsFetcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAmBxD,qBAAa,cAAe,YAAW,iBAAiB;IACtD,OAAO,CAAC,QAAQ,CAAC,YAAY,CACoF;IAEjH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2C;IAEvE,WAAW,IAAI,MAAM;IAIf,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;IA8DhC,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAQpC"} \ No newline at end of file diff --git a/dist/services/marketRate/ghsFetcher.js b/dist/services/marketRate/ghsFetcher.js index 2329a11b..4e2f09db 100644 --- a/dist/services/marketRate/ghsFetcher.js +++ b/dist/services/marketRate/ghsFetcher.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import { validatePrice } from './validation'; export class GHSRateFetcher { coinGeckoUrl = 'https://api.coingecko.com/api/v3/simple/price?ids=stellar&vs_currencies=ghs,usd&include_last_updated_at=true'; usdToGhsUrl = 'https://open.er-api.com/v6/latest/USD'; @@ -20,17 +21,18 @@ export class GHSRateFetcher { const lastUpdatedAt = stellarPrice.last_updated_at ? new Date(stellarPrice.last_updated_at * 1000) : new Date(); - if (typeof stellarPrice.ghs === 'number' && stellarPrice.ghs > 0) { + if (typeof stellarPrice.ghs === 'number') { return { currency: 'GHS', - rate: stellarPrice.ghs, + rate: validatePrice(stellarPrice.ghs), timestamp: lastUpdatedAt, source: 'CoinGecko' }; } - if (typeof stellarPrice.usd !== 'number' || stellarPrice.usd <= 0) { + if (typeof stellarPrice.usd !== 'number') { throw new Error('CoinGecko did not return a usable USD price for Stellar'); } + const usdPrice = validatePrice(stellarPrice.usd); const exchangeRateResponse = await axios.get(this.usdToGhsUrl, { timeout: 10000, headers: { @@ -38,17 +40,16 @@ export class GHSRateFetcher { } }); const usdToGhsRate = exchangeRateResponse.data.rates?.GHS; - if (exchangeRateResponse.data.result !== 'success' || - typeof usdToGhsRate !== 'number' || - usdToGhsRate <= 0) { + if (exchangeRateResponse.data.result !== 'success' || typeof usdToGhsRate !== 'number') { throw new Error('USD to GHS conversion feed did not return a usable GHS rate'); } + const validatedUsdToGhsRate = validatePrice(usdToGhsRate); const fxTimestamp = exchangeRateResponse.data.time_last_update_unix ? new Date(exchangeRateResponse.data.time_last_update_unix * 1000) : lastUpdatedAt; return { currency: 'GHS', - rate: stellarPrice.usd * usdToGhsRate, + rate: validatePrice(usdPrice * validatedUsdToGhsRate), timestamp: fxTimestamp > lastUpdatedAt ? fxTimestamp : lastUpdatedAt, source: 'CoinGecko + ExchangeRate API' }; diff --git a/dist/services/marketRate/ghsFetcher.js.map b/dist/services/marketRate/ghsFetcher.js.map index e9b8453b..4ff4b4d6 100644 --- a/dist/services/marketRate/ghsFetcher.js.map +++ b/dist/services/marketRate/ghsFetcher.js.map @@ -1 +1 @@ -{"version":3,"file":"ghsFetcher.js","sourceRoot":"","sources":["../../../src/services/marketRate/ghsFetcher.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ghsFetcher.js","sourceRoot":"","sources":["../../../src/services/marketRate/ghsFetcher.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/services/marketRate/kesFetcher.d.ts.map b/dist/services/marketRate/kesFetcher.d.ts.map index b2ec7198..d38ed0e3 100644 --- a/dist/services/marketRate/kesFetcher.d.ts.map +++ b/dist/services/marketRate/kesFetcher.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"kesFetcher.d.ts","sourceRoot":"","sources":["../../../src/services/marketRate/kesFetcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAc,MAAM,SAAS,CAAC;AAEpE,qBAAa,cAAe,YAAW,iBAAiB;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAatB;IAEF,WAAW,IAAI,MAAM;IAIf,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;YA2BxB,YAAY;YAgCZ,eAAe;IA0BvB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAQpC"} \ No newline at end of file +{"version":3,"file":"kesFetcher.d.ts","sourceRoot":"","sources":["../../../src/services/marketRate/kesFetcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAc,MAAM,SAAS,CAAC;AAGpE,qBAAa,cAAe,YAAW,iBAAiB;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAatB;IAEF,WAAW,IAAI,MAAM;IAIf,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;YA2BxB,YAAY;YAgCZ,eAAe;IA0BvB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAQpC"} \ No newline at end of file diff --git a/dist/services/marketRate/kesFetcher.js b/dist/services/marketRate/kesFetcher.js index 2e82ce78..4e80f172 100644 --- a/dist/services/marketRate/kesFetcher.js +++ b/dist/services/marketRate/kesFetcher.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import { validatePrice } from './validation'; export class KESRateFetcher { sources = [ { @@ -60,7 +61,7 @@ export class KESRateFetcher { const latestRate = rates[0]; return { currency: 'KES', - rate: parseFloat(latestRate.rate), + rate: validatePrice(Number(latestRate.rate)), timestamp: new Date(latestRate.date), source: this.sources[0].name }; @@ -83,7 +84,7 @@ export class KESRateFetcher { } }); // Placeholder rate - in reality, you'd parse the actual response - const placeholderRate = 130.5; // Approximate KES/USD rate + const placeholderRate = validatePrice(130.5); // Approximate KES/USD rate return { currency: 'KES', rate: placeholderRate, diff --git a/dist/services/marketRate/kesFetcher.js.map b/dist/services/marketRate/kesFetcher.js.map index ea3287bb..c15ee281 100644 --- a/dist/services/marketRate/kesFetcher.js.map +++ b/dist/services/marketRate/kesFetcher.js.map @@ -1 +1 @@ -{"version":3,"file":"kesFetcher.js","sourceRoot":"","sources":["../../../src/services/marketRate/kesFetcher.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"kesFetcher.js","sourceRoot":"","sources":["../../../src/services/marketRate/kesFetcher.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/services/marketRate/validation.d.ts b/dist/services/marketRate/validation.d.ts new file mode 100644 index 00000000..5d42d465 --- /dev/null +++ b/dist/services/marketRate/validation.d.ts @@ -0,0 +1,2 @@ +export declare function validatePrice(data: any): number; +//# sourceMappingURL=validation.d.ts.map \ No newline at end of file diff --git a/dist/services/marketRate/validation.d.ts.map b/dist/services/marketRate/validation.d.ts.map new file mode 100644 index 00000000..d8ff478d --- /dev/null +++ b/dist/services/marketRate/validation.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/services/marketRate/validation.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,CAM/C"} \ No newline at end of file diff --git a/dist/services/marketRate/validation.js b/dist/services/marketRate/validation.js new file mode 100644 index 00000000..12b979f0 --- /dev/null +++ b/dist/services/marketRate/validation.js @@ -0,0 +1,7 @@ +export function validatePrice(data) { + if (typeof data !== 'number' || !Number.isFinite(data) || data <= 0) { + throw new Error('Price must be a positive number'); + } + return data; +} +//# sourceMappingURL=validation.js.map \ No newline at end of file diff --git a/dist/services/marketRate/validation.js.map b/dist/services/marketRate/validation.js.map new file mode 100644 index 00000000..aa3feefd --- /dev/null +++ b/dist/services/marketRate/validation.js.map @@ -0,0 +1 @@ +{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../../src/services/marketRate/validation.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,aAAa,CAAC,IAAS;IACrC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"} \ No newline at end of file diff --git a/src/services/marketRate/ghsFetcher.ts b/src/services/marketRate/ghsFetcher.ts index bae58e7c..38588396 100644 --- a/src/services/marketRate/ghsFetcher.ts +++ b/src/services/marketRate/ghsFetcher.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { MarketRateFetcher, MarketRate } from './types'; +import { validatePrice } from './validation'; type CoinGeckoPriceResponse = { stellar?: { @@ -45,19 +46,21 @@ export class GHSRateFetcher implements MarketRateFetcher { ? new Date(stellarPrice.last_updated_at * 1000) : new Date(); - if (typeof stellarPrice.ghs === 'number' && stellarPrice.ghs > 0) { + if (typeof stellarPrice.ghs === 'number') { return { currency: 'GHS', - rate: stellarPrice.ghs, + rate: validatePrice(stellarPrice.ghs), timestamp: lastUpdatedAt, source: 'CoinGecko' }; } - if (typeof stellarPrice.usd !== 'number' || stellarPrice.usd <= 0) { + if (typeof stellarPrice.usd !== 'number') { throw new Error('CoinGecko did not return a usable USD price for Stellar'); } + const usdPrice = validatePrice(stellarPrice.usd); + const exchangeRateResponse = await axios.get(this.usdToGhsUrl, { timeout: 10000, headers: { @@ -66,21 +69,19 @@ export class GHSRateFetcher implements MarketRateFetcher { }); const usdToGhsRate = exchangeRateResponse.data.rates?.GHS; - if ( - exchangeRateResponse.data.result !== 'success' || - typeof usdToGhsRate !== 'number' || - usdToGhsRate <= 0 - ) { + if (exchangeRateResponse.data.result !== 'success' || typeof usdToGhsRate !== 'number') { throw new Error('USD to GHS conversion feed did not return a usable GHS rate'); } + const validatedUsdToGhsRate = validatePrice(usdToGhsRate); + const fxTimestamp = exchangeRateResponse.data.time_last_update_unix ? new Date(exchangeRateResponse.data.time_last_update_unix * 1000) : lastUpdatedAt; return { currency: 'GHS', - rate: stellarPrice.usd * usdToGhsRate, + rate: validatePrice(usdPrice * validatedUsdToGhsRate), timestamp: fxTimestamp > lastUpdatedAt ? fxTimestamp : lastUpdatedAt, source: 'CoinGecko + ExchangeRate API' }; diff --git a/src/services/marketRate/kesFetcher.ts b/src/services/marketRate/kesFetcher.ts index 42876dfb..480c4aa0 100644 --- a/src/services/marketRate/kesFetcher.ts +++ b/src/services/marketRate/kesFetcher.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { MarketRateFetcher, MarketRate, RateSource } from './types'; +import { validatePrice } from './validation'; export class KESRateFetcher implements MarketRateFetcher { private readonly sources: RateSource[] = [ @@ -67,7 +68,7 @@ export class KESRateFetcher implements MarketRateFetcher { const latestRate = rates[0]; return { currency: 'KES', - rate: parseFloat(latestRate.rate), + rate: validatePrice(Number(latestRate.rate)), timestamp: new Date(latestRate.date), source: this.sources[0].name }; @@ -92,7 +93,7 @@ export class KESRateFetcher implements MarketRateFetcher { }); // Placeholder rate - in reality, you'd parse the actual response - const placeholderRate = 130.5; // Approximate KES/USD rate + const placeholderRate = validatePrice(130.5); // Approximate KES/USD rate return { currency: 'KES', diff --git a/src/services/marketRate/validation.ts b/src/services/marketRate/validation.ts new file mode 100644 index 00000000..9225bbe9 --- /dev/null +++ b/src/services/marketRate/validation.ts @@ -0,0 +1,7 @@ +export function validatePrice(data: any): number { + if (typeof data !== 'number' || !Number.isFinite(data) || data <= 0) { + throw new Error('Price must be a positive number'); + } + + return data; +} diff --git a/test/ghsFetcher.test.ts b/test/ghsFetcher.test.ts index a2bd31bb..8a4d08a1 100644 --- a/test/ghsFetcher.test.ts +++ b/test/ghsFetcher.test.ts @@ -64,6 +64,36 @@ async function run() { assert.equal(fallbackRate.currency, 'GHS'); assert.equal(fallbackRate.rate, 2); assert.equal(fallbackRate.source, 'CoinGecko + ExchangeRate API'); + + axios.get = (async (url: string) => { + if (url.includes('api.coingecko.com')) { + return { + data: { + stellar: { + ghs: Number.NaN, + usd: 0.2, + last_updated_at: 1_774_464_561 + } + } + }; + } + + if (url.includes('open.er-api.com')) { + return { + data: { + result: 'success', + rates: { + GHS: undefined + }, + time_last_update_unix: 1_774_396_951 + } + }; + } + + throw new Error(`Unexpected URL: ${url}`); + }) as typeof axios.get; + + await assert.rejects(() => fetcher.fetchRate(), /Price must be a positive number|usable GHS rate/); } finally { axios.get = originalGet; } diff --git a/test/validation.test.ts b/test/validation.test.ts new file mode 100644 index 00000000..9978c11b --- /dev/null +++ b/test/validation.test.ts @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import { validatePrice } from '../src/services/marketRate/validation'; + +function run() { + assert.equal(validatePrice(1), 1); + assert.equal(validatePrice(12.34), 12.34); + + for (const value of [undefined, null, NaN, Infinity, -1, 0, '12']) { + assert.throws(() => validatePrice(value), /Price must be a positive number/); + } +} + +run();