diff --git a/.env.example b/.env.example index b16edf3..dc73935 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,8 @@ CRYPTO_COMPARE_KEY='YOUR_CRYPTOCOMPARE_API_KEY' LIVE_COIN_WATCH_KEY='YOUR_LIVECOINWATCH_API_KEY' NODE_ENV=development BASE_URL=http://localhost:3333 + +# Kill switch for the Binance bStocks synthetic markets in /v2/rates. +# Set to "false" (case-insensitive) to stop serving bstock-* entries without +# a redeploy. Any other value, or unset, leaves them enabled. +BSTOCKS_ENABLED=true diff --git a/.eslintignore b/.eslintignore index 26f8ce4..ef333ca 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -config/coinsSimple.js \ No newline at end of file +config/coinsSimple.js +docs/ diff --git a/.eslintrc.js b/.eslintrc.js index e543be8..3c2cc8e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,42 +1,151 @@ +// Rules the repo has chosen for itself. They have to be applied inside the +// TypeScript override as well: an override's `extends` is resolved after the +// top-level `rules`, so airbnb's own defaults would otherwise win there (a +// 100-character max-len, for one). +const projectRules = { + 'max-len': [ + 'error', + { + code: 300, + ignoreUrls: true, + ignoreTrailingComments: true, + }, + ], + 'no-console': 'off', + 'linebreak-style': [ + 'error', + 'unix', + ], + + // A leading underscore marks an internal or test-only name here. TypeScript's + // `private` covers real privacy on the provider classes, and `__retryCount` + // is axios's own convention for the counter it hangs off a request config. + 'no-underscore-dangle': [ + 'error', + { + allow: ['__retryCount'], + allowAfterThis: true, + enforceInMethodNames: false, + }, + ], + + // airbnb bans for..of because transpiling it used to pull in + // regenerator-runtime. This service runs ES2020 on Node and pays no such + // cost; the rest of airbnb's restrictions are kept as-is. + 'no-restricted-syntax': [ + 'error', + { + selector: 'ForInStatement', + message: 'for..in iterates the prototype chain and needs a hasOwnProperty guard. Use Object.{keys,values,entries} instead.', + }, + { + selector: 'LabeledStatement', + message: 'Labels are a form of GOTO; use a function instead.', + }, + { + selector: 'WithStatement', + message: '`with` is disallowed in strict mode and makes scope ambiguous.', + }, + ], + + // Worth enforcing when introducing a binding, but rewriting an assignment + // such as `rates[2] = fetched[2]` as destructuring reads worse than the + // line it replaces. + 'prefer-destructuring': [ + 'error', + { + VariableDeclarator: { + array: true, + object: true, + }, + AssignmentExpression: { + array: false, + object: false, + }, + }, + ], + + // This repo allows 300-character lines; airbnb's rule additionally breaks + // any object literal with four or more properties, which contradicts that + // and turns compact fixtures into three-line blocks. Keep the consistency + // checks, drop the property-count trigger. + 'object-curly-newline': [ + 'error', + { + multiline: true, + consistent: true, + }, + ], + + // config/index.ts and lib/axios.ts deliberately export the same value both + // named and default, which is the entirety of what this rule sees. + 'import/no-named-as-default': 'off', + + // Named exports are the convention here. How many exports a module happens + // to have today is not a reason to change how callers import it. + 'import/prefer-default-export': 'off', +}; + module.exports = { root: true, env: { - commonjs: true, node: true, - mocha: true, + es2022: true, + jest: true, }, extends: [ 'airbnb-base', ], - rules: { - 'max-len': [ - 'error', - { - code: 300, - ignoreUrls: true, - ignoreTrailingComments: true, - }, - ], - 'no-console': 'off', - 'import/extensions': [ - 'error', - 'never', - ], - 'linebreak-style': [ - 'error', - 'unix', - ], - }, - parserOptions: { - parser: 'babel-eslint', - }, + rules: projectRules, overrides: [ + // TypeScript sources. The parser and the type-aware config live here + // rather than at the top level so plain JS (this file, config/*.js) is + // still linted without having to be part of the tsconfig project. { - files: [ - '**/__tests__/*.{j,t}s?(x)', + files: ['**/*.ts'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: __dirname, + }, + plugins: [ + '@typescript-eslint', + ], + extends: [ + 'airbnb-base', + 'airbnb-typescript/base', ], - env: { - mocha: true, + settings: { + 'import/resolver': { + typescript: { + project: './tsconfig.json', + }, + }, + }, + rules: { + ...projectRules, + // TypeScript resolves module specifiers without a file extension, and + // writing one would break `module: commonjs` resolution. + 'import/extensions': [ + 'error', + 'ignorePackages', + { + ts: 'never', + js: 'never', + }, + ], + }, + }, + // Tests import jest and the other devDependencies by design. + { + files: ['tests/**/*.ts'], + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + }, + ], }, }, ], diff --git a/README.md b/README.md index bd95b1a..3264571 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,36 @@ Example: http://localhost:3333/rates docker run -e API_KEY=yourApiKey -p 4444:3333 zelcash/rates-api ``` +## bStocks (Binance tokenized equities) + +`GET /v2/rates` emits one synthetic `crypto` entry per Binance bStock — a +tokenized US equity on BNB Smart Chain (e.g. `bstock-tslab` for Tesla). Prices +come straight from Binance's public Spot API (no API key required): + +- Universe: the intersection of Binance's tokenised-asset list + (`GET https://www.binance.com/bapi/asset/v2/public/asset/asset/get-tokenised-asset`, + filtered to assets with a BSC contract) with Spot symbols currently in + `TRADING` status — about 56 of the ~66 listed assets qualify today. +- Quote currency: **USDT**, not USDC — verified live, no USDC pairs exist for + these symbols. +- `rates.usd` = `USDT` last price; `rates.btc` = that price divided by + `BTCUSDT` from the same ticker batch (same venue, no cross-venue basis). + `change24h`/`change7d` come from Binance's 24h ticker and 7d rolling-window + ticker respectively. +- `provider` is always the literal string `"coingecko"`, never `"binance"`. + The ZelCore client keys its market store on `${provider}-${id}` and the + sibling `api` repo advertises each bStock's `coinInfo.coingeckoID` as + `bstock-`; the two literals only meet if the provider here is exactly + `"coingecko"`. This is a cross-repo contract — do not change it in + isolation. +- Binance does **not** omit a halted symbol (e.g. during a stock split) from + its ticker response — it returns the symbol present with + `lastPrice: "0.00000000"`. Prices are therefore accepted only when finite + and strictly positive; a halted/zero-priced symbol keeps serving its last + known-good price rather than a stale zero or a dropped entry, per the + bStocks partner guide's "display-only during halts is acceptable" allowance. +- Toggle via `config.bStocksEnabled` (`config/index.ts`). + ## Update Documentation To update typedoc documentation please run. diff --git a/config/index.ts b/config/index.ts index d320eba..2e08790 100644 --- a/config/index.ts +++ b/config/index.ts @@ -11,6 +11,20 @@ export const config = { liveCoinWatchUrl: 'https://api.livecoinwatch.com/', zelCoinsUrl: 'https://raw.githubusercontent.com/ZelCore-io/Zelcore/master/coins.json', zelCoinInfoUrl: 'https://raw.githubusercontent.com/ZelCore-io/Zelcore/master/coininfo.json', + binanceApiUrl: 'https://api.binance.com/', + binanceAssetUrl: 'https://www.binance.com/', + // Env kill switch: disabling in production should not require a code + // change + redeploy. Defaults to enabled when unset. + bStocksEnabled: (process.env.BSTOCKS_ENABLED ?? '').toLowerCase() !== 'false', + // How long a failed Binance request (tokenised-asset list / trading-symbol + // set) is negatively-cached before retrying, so an outage doesn't re-spend + // the full AxiosWrapper retry budget on every 30s refresh cycle. + binanceFailureCacheMs: 60 * 1000, + // How long a bStock's last-known-good price is served after Binance stops + // pricing it fresh. The outage this exists for (a stock split halt, + // exchange maintenance) is naturally multi-day, so this is deliberately far + // longer than the ticker windows themselves. + bstocksLastGoodMaxAgeMs: 7 * 24 * 60 * 60 * 1000, }; -export default config; \ No newline at end of file +export default config; diff --git a/docs/README.md b/docs/README.md index 1fdedbc..901a611 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -**rates-api v3.0.0** • [**Docs**](modules.md) +**rates-api v3.0.0** *** @@ -35,6 +35,36 @@ Example: http://localhost:3333/rates docker run -e API_KEY=yourApiKey -p 4444:3333 zelcash/rates-api ``` +## bStocks (Binance tokenized equities) + +`GET /v2/rates` emits one synthetic `crypto` entry per Binance bStock — a +tokenized US equity on BNB Smart Chain (e.g. `bstock-tslab` for Tesla). Prices +come straight from Binance's public Spot API (no API key required): + +- Universe: the intersection of Binance's tokenised-asset list + (`GET https://www.binance.com/bapi/asset/v2/public/asset/asset/get-tokenised-asset`, + filtered to assets with a BSC contract) with Spot symbols currently in + `TRADING` status — about 56 of the ~66 listed assets qualify today. +- Quote currency: **USDT**, not USDC — verified live, no USDC pairs exist for + these symbols. +- `rates.usd` = `USDT` last price; `rates.btc` = that price divided by + `BTCUSDT` from the same ticker batch (same venue, no cross-venue basis). + `change24h`/`change7d` come from Binance's 24h ticker and 7d rolling-window + ticker respectively. +- `provider` is always the literal string `"coingecko"`, never `"binance"`. + The ZelCore client keys its market store on `${provider}-${id}` and the + sibling `api` repo advertises each bStock's `coinInfo.coingeckoID` as + `bstock-`; the two literals only meet if the provider here is exactly + `"coingecko"`. This is a cross-repo contract — do not change it in + isolation. +- Binance does **not** omit a halted symbol (e.g. during a stock split) from + its ticker response — it returns the symbol present with + `lastPrice: "0.00000000"`. Prices are therefore accepted only when finite + and strictly positive; a halted/zero-priced symbol keeps serving its last + known-good price rather than a stale zero or a dropped entry, per the + bStocks partner guide's "display-only during halts is acceptable" allowance. +- Toggle via `config.bStocksEnabled` (`config/index.ts`). + ## Update Documentation To update typedoc documentation please run. diff --git a/docs/index/README.md b/docs/index/README.md index 96cb1bd..1100c05 100644 --- a/docs/index/README.md +++ b/docs/index/README.md @@ -1,7 +1,7 @@ -[**rates-api v3.0.0**](../README.md) • **Docs** +[**rates-api v3.0.0**](../README.md) *** -[rates-api v3.0.0](../modules.md) / index +[rates-api](../modules.md) / index # index diff --git a/docs/modules.md b/docs/modules.md index 675e24d..1a06768 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -1,4 +1,4 @@ -[**rates-api v3.0.0**](README.md) • **Docs** +[**rates-api v3.0.0**](README.md) *** @@ -14,9 +14,11 @@ - [src/lib/utils](src/lib/utils/README.md) - [src/routes](src/routes/README.md) - [src/services/apiServices](src/services/apiServices/README.md) +- [src/services/bstocks](src/services/bstocks/README.md) - [src/services/coinAggregatorIDs](src/services/coinAggregatorIDs/README.md) - [src/services/newContracts](src/services/newContracts/README.md) - [src/services/providers](src/services/providers/README.md) +- [src/services/providers/binance](src/services/providers/binance/README.md) - [src/services/providers/bitpay](src/services/providers/bitpay/README.md) - [src/services/providers/coinGecko](src/services/providers/coinGecko/README.md) - [src/services/providers/cryptoCompare](src/services/providers/cryptoCompare/README.md) diff --git a/docs/src/lib/axios/README.md b/docs/src/lib/axios/README.md index f97f5f7..b520a32 100644 --- a/docs/src/lib/axios/README.md +++ b/docs/src/lib/axios/README.md @@ -1,14 +1,12 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/lib/axios +[rates-api](../../../modules.md) / src/lib/axios # src/lib/axios -## Index - -### Classes +## Classes - [AxiosWrapper](classes/AxiosWrapper.md) diff --git a/docs/src/lib/axios/classes/AxiosWrapper.md b/docs/src/lib/axios/classes/AxiosWrapper.md index c06f42b..677411d 100644 --- a/docs/src/lib/axios/classes/AxiosWrapper.md +++ b/docs/src/lib/axios/classes/AxiosWrapper.md @@ -1,11 +1,13 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/lib/axios](../README.md) / AxiosWrapper +[rates-api](../../../../modules.md) / [src/lib/axios](../README.md) / AxiosWrapper # Class: AxiosWrapper +Defined in: [src/lib/axios.ts:26](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/axios.ts#L26) + A wrapper around Axios to handle automatic retries and customizable configurations. This class provides a simplified interface over Axios, adding automatic retry functionality @@ -31,29 +33,37 @@ apiClient.post('/users', { name: 'John Doe' }) ## Constructors -### new AxiosWrapper() +### Constructor + +> **new AxiosWrapper**(`baseURL`, `maxRetries?`, `timeout?`): `AxiosWrapper` -> **new AxiosWrapper**(`baseURL`, `maxRetries`, `timeout`): [`AxiosWrapper`](AxiosWrapper.md) +Defined in: [src/lib/axios.ts:45](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/axios.ts#L45) Creates an instance of AxiosWrapper. #### Parameters -• **baseURL**: `string` +##### baseURL + +`string` The base URL for all requests. -• **maxRetries**: `number` = `3` +##### maxRetries? + +`number` = `3` The maximum number of retry attempts for failed requests (default is 3). -• **timeout**: `number` = `5000` +##### timeout? + +`number` = `5000` The timeout in milliseconds for requests (default is 5000 ms). #### Returns -[`AxiosWrapper`](AxiosWrapper.md) +`AxiosWrapper` #### Example @@ -61,25 +71,27 @@ The timeout in milliseconds for requests (default is 5000 ms). const apiClient = new AxiosWrapper('https://api.example.com', 5, 10000); ``` -#### Defined in - -[src/lib/axios.ts:43](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/lib/axios.ts#L43) - ## Methods ### delete() -> **delete**(`url`, `config`?): `Promise`\<`AxiosResponse`\<`any`, `any`\>\> +> **delete**(`url`, `config?`): `Promise`\<`AxiosResponse`\<`any`, `any`\>\> + +Defined in: [src/lib/axios.ts:173](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/axios.ts#L173) Performs a DELETE request. #### Parameters -• **url**: `string` +##### url + +`string` The URL to send the DELETE request to. -• **config?**: `AxiosRequestConfig`\<`any`\> +##### config? + +`AxiosRequestConfig`\<`any`\> Optional Axios request configuration. @@ -97,25 +109,27 @@ apiClient.delete('/users/123') .catch(error => console.error(error)); ``` -#### Defined in - -[src/lib/axios.ts:171](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/lib/axios.ts#L171) - *** ### get() -> **get**(`url`, `config`?): `Promise`\<`AxiosResponse`\<`any`, `any`\>\> +> **get**(`url`, `config?`): `Promise`\<`AxiosResponse`\<`any`, `any`\>\> + +Defined in: [src/lib/axios.ts:117](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/axios.ts#L117) Performs a GET request. #### Parameters -• **url**: `string` +##### url + +`string` The URL to send the GET request to. -• **config?**: `AxiosRequestConfig`\<`any`\> +##### config? + +`AxiosRequestConfig`\<`any`\> Optional Axios request configuration. @@ -133,29 +147,33 @@ apiClient.get('/users') .catch(error => console.error(error)); ``` -#### Defined in - -[src/lib/axios.ts:115](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/lib/axios.ts#L115) - *** ### post() -> **post**(`url`, `data`?, `config`?): `Promise`\<`AxiosResponse`\<`any`, `any`\>\> +> **post**(`url`, `data?`, `config?`): `Promise`\<`AxiosResponse`\<`any`, `any`\>\> + +Defined in: [src/lib/axios.ts:136](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/axios.ts#L136) Performs a POST request. #### Parameters -• **url**: `string` +##### url + +`string` The URL to send the POST request to. -• **data?**: `any` +##### data? + +`any` The data to send with the POST request. -• **config?**: `AxiosRequestConfig`\<`any`\> +##### config? + +`AxiosRequestConfig`\<`any`\> Optional Axios request configuration. @@ -173,29 +191,33 @@ apiClient.post('/users', { name: 'John Doe' }) .catch(error => console.error(error)); ``` -#### Defined in - -[src/lib/axios.ts:134](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/lib/axios.ts#L134) - *** ### put() -> **put**(`url`, `data`?, `config`?): `Promise`\<`AxiosResponse`\<`any`, `any`\>\> +> **put**(`url`, `data?`, `config?`): `Promise`\<`AxiosResponse`\<`any`, `any`\>\> + +Defined in: [src/lib/axios.ts:155](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/axios.ts#L155) Performs a PUT request. #### Parameters -• **url**: `string` +##### url + +`string` The URL to send the PUT request to. -• **data?**: `any` +##### data? + +`any` The data to send with the PUT request. -• **config?**: `AxiosRequestConfig`\<`any`\> +##### config? + +`AxiosRequestConfig`\<`any`\> Optional Axios request configuration. @@ -212,7 +234,3 @@ apiClient.put('/users/123', { name: 'Jane Doe' }) .then(response => console.log(response.data)) .catch(error => console.error(error)); ``` - -#### Defined in - -[src/lib/axios.ts:153](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/lib/axios.ts#L153) diff --git a/docs/src/lib/objects/README.md b/docs/src/lib/objects/README.md index c1c1472..274164e 100644 --- a/docs/src/lib/objects/README.md +++ b/docs/src/lib/objects/README.md @@ -1,13 +1,12 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/lib/objects +[rates-api](../../../modules.md) / src/lib/objects # src/lib/objects -## Index - -### Functions +## Functions - [mergeDeep](functions/mergeDeep.md) +- [replaceCryptoByKey](functions/replaceCryptoByKey.md) diff --git a/docs/src/lib/objects/functions/mergeDeep.md b/docs/src/lib/objects/functions/mergeDeep.md index 4a2b428..43adf12 100644 --- a/docs/src/lib/objects/functions/mergeDeep.md +++ b/docs/src/lib/objects/functions/mergeDeep.md @@ -1,13 +1,15 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/lib/objects](../README.md) / mergeDeep +[rates-api](../../../../modules.md) / [src/lib/objects](../README.md) / mergeDeep # Function: mergeDeep() > **mergeDeep**(`target`, `source`): `any` +Defined in: [src/lib/objects.ts:23](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/objects.ts#L23) + Deeply merges two objects or arrays. This function takes a target and a source and recursively merges properties. @@ -16,11 +18,15 @@ This function takes a target and a source and recursively merges properties. ## Parameters -• **target**: `any` +### target + +`any` The target object or array to merge into. -• **source**: `any` +### source + +`any` The source object or array to merge from. @@ -38,7 +44,3 @@ const obj2 = { b: { d: 3 }, e: 4 }; const result = mergeDeep(obj1, obj2); // result: { a: 1, b: { c: 2, d: 3 }, e: 4 } ``` - -## Defined in - -[src/lib/objects.ts:20](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/lib/objects.ts#L20) diff --git a/docs/src/lib/objects/functions/replaceCryptoByKey.md b/docs/src/lib/objects/functions/replaceCryptoByKey.md new file mode 100644 index 0000000..aed45c5 --- /dev/null +++ b/docs/src/lib/objects/functions/replaceCryptoByKey.md @@ -0,0 +1,52 @@ +[**rates-api v3.0.0**](../../../../README.md) + +*** + +[rates-api](../../../../modules.md) / [src/lib/objects](../README.md) / replaceCryptoByKey + +# Function: replaceCryptoByKey() + +> **replaceCryptoByKey**\<`T`\>(`source`): `T`[] + +Defined in: [src/lib/objects.ts:78](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/objects.ts#L78) + +Rebuilds the crypto array from `source` alone, de-duplicated by +`${provider}-${id}`, preserving source order with last-write-wins. + +This deliberately does NOT merge with the previous array — hence the name. +The positional `mergeDeep` it replaced overlaid the new array onto the old +one index by index, which is only correct while every provider block returns +exactly the same number of rows in the same order. When a block shrank (a +provider outage, a delisted coin), two things went wrong: fields from the +old entry at that index survived onto a different coin — a CryptoCompare row +inheriting CoinGecko's `rank` and `change7d` — and entries past the new +length lived on as stale duplicates. Because the ZelCore client re-keys on +`${provider}-${id}` with last-write-wins, and the stale duplicates sat after +the fresh ones, wallet users were served the STALE price on any cycle where +a block's row count shifted. + +Two behaviour changes a caller should know about: + - entries repeating the same `provider`+`id` collapse to one, keeping the + last value at the first occurrence's position; + - an entry the fetch no longer produces disappears immediately, rather than + persisting from the previous cycle. + +## Type Parameters + +### T + +`T` *extends* `object` + +## Parameters + +### source + +`T`[] + +The freshly fetched entries. + +## Returns + +`T`[] + +The de-duplicated entries, in source order. diff --git a/docs/src/lib/server/README.md b/docs/src/lib/server/README.md index 1bd3634..1620b5c 100644 --- a/docs/src/lib/server/README.md +++ b/docs/src/lib/server/README.md @@ -1,13 +1,11 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/lib/server +[rates-api](../../../modules.md) / src/lib/server # src/lib/server -## Index +## Variables -### Functions - -- [default](functions/default.md) +- [default](variables/default.md) diff --git a/docs/src/lib/server/functions/default.md b/docs/src/lib/server/functions/default.md deleted file mode 100644 index b52dfad..0000000 --- a/docs/src/lib/server/functions/default.md +++ /dev/null @@ -1,100 +0,0 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** - -*** - -[rates-api v3.0.0](../../../../modules.md) / [src/lib/server](../README.md) / default - -# Function: default() - -The main Express application instance. - -## Remarks - -This instance is configured with middleware and routes and is exported for use in the server. - -## Example - -```typescript -import app from './server'; - -const port = process.env.PORT || 3000; - -app.listen(port, () => { - console.log(`Server is running on port ${port}`); -}); -``` - -## default(req, res) - -> **default**(`req`, `res`): `any` - -Express instance itself is a request handler, which could be invoked without -third argument. - -### Parameters - -• **req**: `IncomingMessage` \| `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> - -• **res**: `ServerResponse`\<`IncomingMessage`\> \| `Response`\<`any`, `Record`\<`string`, `any`\>, `number`\> - -### Returns - -`any` - -### Remarks - -This instance is configured with middleware and routes and is exported for use in the server. - -### Example - -```typescript -import app from './server'; - -const port = process.env.PORT || 3000; - -app.listen(port, () => { - console.log(`Server is running on port ${port}`); -}); -``` - -### Defined in - -[src/lib/server.ts:32](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/lib/server.ts#L32) - -## default(req, res, next) - -> **default**(`req`, `res`, `next`): `void` - -The main Express application instance. - -### Parameters - -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> - -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>, `number`\> - -• **next**: `NextFunction` - -### Returns - -`void` - -### Remarks - -This instance is configured with middleware and routes and is exported for use in the server. - -### Example - -```typescript -import app from './server'; - -const port = process.env.PORT || 3000; - -app.listen(port, () => { - console.log(`Server is running on port ${port}`); -}); -``` - -### Defined in - -[src/lib/server.ts:32](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/lib/server.ts#L32) diff --git a/docs/src/lib/server/variables/default.md b/docs/src/lib/server/variables/default.md new file mode 100644 index 0000000..a631b95 --- /dev/null +++ b/docs/src/lib/server/variables/default.md @@ -0,0 +1,29 @@ +[**rates-api v3.0.0**](../../../../README.md) + +*** + +[rates-api](../../../../modules.md) / [src/lib/server](../README.md) / default + +# Variable: default + +> `const` **default**: `Express` + +Defined in: [src/lib/server.ts:33](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/server.ts#L33) + +The main Express application instance. + +## Remarks + +This instance is configured with middleware and routes and is exported for use in the server. + +## Example + +```typescript +import app from './server'; + +const port = process.env.PORT || 3000; + +app.listen(port, () => { + console.log(`Server is running on port ${port}`); +}); +``` diff --git a/docs/src/lib/utils/README.md b/docs/src/lib/utils/README.md index 0801b2a..30efb7b 100644 --- a/docs/src/lib/utils/README.md +++ b/docs/src/lib/utils/README.md @@ -1,14 +1,12 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/lib/utils +[rates-api](../../../modules.md) / src/lib/utils # src/lib/utils -## Index - -### Functions +## Functions - [arraySplit](functions/arraySplit.md) - [makeRequestStrings](functions/makeRequestStrings.md) diff --git a/docs/src/lib/utils/functions/arraySplit.md b/docs/src/lib/utils/functions/arraySplit.md index 0becf82..8cb711f 100644 --- a/docs/src/lib/utils/functions/arraySplit.md +++ b/docs/src/lib/utils/functions/arraySplit.md @@ -1,22 +1,28 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/lib/utils](../README.md) / arraySplit +[rates-api](../../../../modules.md) / [src/lib/utils](../README.md) / arraySplit # Function: arraySplit() > **arraySplit**(`arr`, `size`): `string`[][] +Defined in: [src/lib/utils.ts:15](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/utils.ts#L15) + Splits an array into chunks of a specified size. ## Parameters -• **arr**: `string`[] +### arr + +`string`[] The array to split. -• **size**: `number` +### size + +`number` The maximum size of each chunk. @@ -33,7 +39,3 @@ const array = ['a', 'b', 'c', 'd', 'e']; const chunks = arraySplit(array, 2); // chunks: [['a', 'b'], ['c', 'd'], ['e']] ``` - -## Defined in - -[src/lib/utils.ts:15](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/lib/utils.ts#L15) diff --git a/docs/src/lib/utils/functions/makeRequestStrings.md b/docs/src/lib/utils/functions/makeRequestStrings.md index 40bcf79..3f140c5 100644 --- a/docs/src/lib/utils/functions/makeRequestStrings.md +++ b/docs/src/lib/utils/functions/makeRequestStrings.md @@ -1,13 +1,15 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/lib/utils](../README.md) / makeRequestStrings +[rates-api](../../../../modules.md) / [src/lib/utils](../README.md) / makeRequestStrings # Function: makeRequestStrings() > **makeRequestStrings**(`elements`, `maxLength`): `string`[] +Defined in: [src/lib/utils.ts:41](https://github.com/ZelCore-io/rates-api/blob/master/src/lib/utils.ts#L41) + Combines elements of a string array into comma-separated strings, ensuring that each combined string does not exceed a specified maximum length. This function iterates over the input `elements` and concatenates them with commas. @@ -15,11 +17,15 @@ If adding another element would exceed the `maxLength`, it pushes the current st ## Parameters -• **elements**: `string`[] +### elements + +`string`[] The array of strings to combine. -• **maxLength**: `number` +### maxLength + +`number` The maximum length of each combined string. @@ -37,7 +43,3 @@ const maxLength = 15; const result = makeRequestStrings(elements, maxLength); // result: ['apple,banana', 'cherry,date', 'fig'] ``` - -## Defined in - -[src/lib/utils.ts:41](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/lib/utils.ts#L41) diff --git a/docs/src/routes/README.md b/docs/src/routes/README.md index 2d8673c..95643ca 100644 --- a/docs/src/routes/README.md +++ b/docs/src/routes/README.md @@ -1,13 +1,11 @@ -[**rates-api v3.0.0**](../../README.md) • **Docs** +[**rates-api v3.0.0**](../../README.md) *** -[rates-api v3.0.0](../../modules.md) / src/routes +[rates-api](../../modules.md) / src/routes # src/routes -## Index +## Variables -### Functions - -- [default](functions/default.md) +- [default](variables/default.md) diff --git a/docs/src/routes/functions/default.md b/docs/src/routes/variables/default.md similarity index 52% rename from docs/src/routes/functions/default.md rename to docs/src/routes/variables/default.md index c1e0287..515e3a0 100644 --- a/docs/src/routes/functions/default.md +++ b/docs/src/routes/variables/default.md @@ -1,18 +1,22 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/routes](../README.md) / default +[rates-api](../../../modules.md) / [src/routes](../README.md) / default -# Function: default() +# Variable: default -> **default**(`app`): `void` +> **default**: (`app`) => `void` + +Defined in: [src/routes.ts:26](https://github.com/ZelCore-io/rates-api/blob/master/src/routes.ts#L26) Configures the Express application by setting up routes, middleware, and caching. ## Parameters -• **app**: `Application` +### app + +`Application` The Express application instance. @@ -33,7 +37,3 @@ app.listen(3000, () => { console.log('Server is running on port 3000'); }); ``` - -## Defined in - -[src/routes.ts:26](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/routes.ts#L26) diff --git a/docs/src/services/apiServices/README.md b/docs/src/services/apiServices/README.md index eaa4660..1f7fc04 100644 --- a/docs/src/services/apiServices/README.md +++ b/docs/src/services/apiServices/README.md @@ -1,18 +1,16 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/services/apiServices +[rates-api](../../../modules.md) / src/services/apiServices # src/services/apiServices -## Index - -### Variables +## Variables - [default](variables/default.md) -### Functions +## Functions - [checkContractsV2](functions/checkContractsV2.md) - [dataRefresher](functions/dataRefresher.md) diff --git a/docs/src/services/apiServices/functions/checkContractsV2.md b/docs/src/services/apiServices/functions/checkContractsV2.md index 7a20b11..d833c1c 100644 --- a/docs/src/services/apiServices/functions/checkContractsV2.md +++ b/docs/src/services/apiServices/functions/checkContractsV2.md @@ -1,22 +1,28 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/apiServices](../README.md) / checkContractsV2 +[rates-api](../../../../modules.md) / [src/services/apiServices](../README.md) / checkContractsV2 # Function: checkContractsV2() > **checkContractsV2**(`req`, `res`): `Promise`\<`void`\> +Defined in: [src/services/apiServices.ts:156](https://github.com/ZelCore-io/rates-api/blob/master/src/services/apiServices.ts#L156) + Handles the request to check for new contracts. ## Parameters -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> +### req + +`Request` The Express request object containing `contracts` in the body. -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>\> +### res + +`Response` The Express response object. @@ -29,7 +35,3 @@ The Express response object. ```typescript app.post('/contracts/check', checkContractsV2); ``` - -## Defined in - -[src/services/apiServices.ts:156](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/apiServices.ts#L156) diff --git a/docs/src/services/apiServices/functions/dataRefresher.md b/docs/src/services/apiServices/functions/dataRefresher.md index 86e918b..00bd174 100644 --- a/docs/src/services/apiServices/functions/dataRefresher.md +++ b/docs/src/services/apiServices/functions/dataRefresher.md @@ -1,13 +1,15 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/apiServices](../README.md) / dataRefresher +[rates-api](../../../../modules.md) / [src/services/apiServices](../README.md) / dataRefresher # Function: dataRefresher() > **dataRefresher**(): `Promise`\<`void`\> +Defined in: [src/services/apiServices.ts:195](https://github.com/ZelCore-io/rates-api/blob/master/src/services/apiServices.ts#L195) + Periodically refreshes coin information and aggregator IDs. This function logs the start of the refresh process, calls `getLatestCoinInfo`, @@ -23,7 +25,3 @@ logs the error and retries after 30 minutes. ```typescript dataRefresher(); ``` - -## Defined in - -[src/services/apiServices.ts:195](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/apiServices.ts#L195) diff --git a/docs/src/services/apiServices/functions/getData.md b/docs/src/services/apiServices/functions/getData.md index 97fda87..5de8925 100644 --- a/docs/src/services/apiServices/functions/getData.md +++ b/docs/src/services/apiServices/functions/getData.md @@ -1,36 +1,45 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/apiServices](../README.md) / getData +[rates-api](../../../../modules.md) / [src/services/apiServices](../README.md) / getData # Function: getData() > **getData**(): `object` +Defined in: [src/services/apiServices.ts:105](https://github.com/ZelCore-io/rates-api/blob/master/src/services/apiServices.ts#L105) + Retrieves the current rates and market data. ## Returns -`object` - An object containing `rates` and `marketsUSD`. ### marketsUSD > **marketsUSD**: [`MarketsData`](../../../types/type-aliases/MarketsData.md) +Stores market data in USD. + +Structure: +- `marketsUSD[0]`: BTC to USD market data. +- `marketsUSD[1]`: Errors object. + ### rates > **rates**: [`RatesData`](../../../types/type-aliases/RatesData.md) +Stores exchange rates data. + +Structure: +- `rates[0]`: BTC to fiat exchange rates. +- `rates[1]`: Alternative coins to fiat exchange rates. +- `rates[2]`: Errors object. + ## Example ```typescript const data = getData(); console.log(data.rates, data.marketsUSD); ``` - -## Defined in - -[src/services/apiServices.ts:105](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/apiServices.ts#L105) diff --git a/docs/src/services/apiServices/functions/getFoundContracts.md b/docs/src/services/apiServices/functions/getFoundContracts.md index 37b8f3b..aef8f88 100644 --- a/docs/src/services/apiServices/functions/getFoundContracts.md +++ b/docs/src/services/apiServices/functions/getFoundContracts.md @@ -1,13 +1,15 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/apiServices](../README.md) / getFoundContracts +[rates-api](../../../../modules.md) / [src/services/apiServices](../README.md) / getFoundContracts # Function: getFoundContracts() > **getFoundContracts**(): [`FoundContractStore`](../../../types/type-aliases/FoundContractStore.md) +Defined in: [src/services/apiServices.ts:141](https://github.com/ZelCore-io/rates-api/blob/master/src/services/apiServices.ts#L141) + Retrieves the found contracts. ## Returns @@ -21,7 +23,3 @@ The `foundContracts` object. ```typescript const contracts = getFoundContracts(); ``` - -## Defined in - -[src/services/apiServices.ts:141](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/apiServices.ts#L141) diff --git a/docs/src/services/apiServices/functions/getMarketsUsd.md b/docs/src/services/apiServices/functions/getMarketsUsd.md index dead2ba..ecf9f39 100644 --- a/docs/src/services/apiServices/functions/getMarketsUsd.md +++ b/docs/src/services/apiServices/functions/getMarketsUsd.md @@ -1,22 +1,28 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/apiServices](../README.md) / getMarketsUsd +[rates-api](../../../../modules.md) / [src/services/apiServices](../README.md) / getMarketsUsd # Function: getMarketsUsd() > **getMarketsUsd**(`req`, `res`): `Promise`\<`void`\> +Defined in: [src/services/apiServices.ts:123](https://github.com/ZelCore-io/rates-api/blob/master/src/services/apiServices.ts#L123) + Handles the GET request to retrieve market data in USD. ## Parameters -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> +### req + +`Request` The Express request object. -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>\> +### res + +`Response` The Express response object. @@ -29,7 +35,3 @@ The Express response object. ```typescript app.get('/markets/usd', getMarketsUsd); ``` - -## Defined in - -[src/services/apiServices.ts:123](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/apiServices.ts#L123) diff --git a/docs/src/services/apiServices/functions/getRates.md b/docs/src/services/apiServices/functions/getRates.md index 4f1cedf..eecfe98 100644 --- a/docs/src/services/apiServices/functions/getRates.md +++ b/docs/src/services/apiServices/functions/getRates.md @@ -1,22 +1,28 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/apiServices](../README.md) / getRates +[rates-api](../../../../modules.md) / [src/services/apiServices](../README.md) / getRates # Function: getRates() > **getRates**(`req`, `res`): `Promise`\<`void`\> +Defined in: [src/services/apiServices.ts:47](https://github.com/ZelCore-io/rates-api/blob/master/src/services/apiServices.ts#L47) + Handles the GET request to retrieve exchange rates. ## Parameters -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> +### req + +`Request` The Express request object. -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>\> +### res + +`Response` The Express response object. @@ -29,7 +35,3 @@ The Express response object. ```typescript app.get('/rates', getRates); ``` - -## Defined in - -[src/services/apiServices.ts:47](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/apiServices.ts#L47) diff --git a/docs/src/services/apiServices/functions/getRatesV2.md b/docs/src/services/apiServices/functions/getRatesV2.md index ce16bcf..4faccee 100644 --- a/docs/src/services/apiServices/functions/getRatesV2.md +++ b/docs/src/services/apiServices/functions/getRatesV2.md @@ -1,22 +1,28 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/apiServices](../README.md) / getRatesV2 +[rates-api](../../../../modules.md) / [src/services/apiServices](../README.md) / getRatesV2 # Function: getRatesV2() > **getRatesV2**(`req`, `res`): `Promise`\<`void`\> +Defined in: [src/services/apiServices.ts:66](https://github.com/ZelCore-io/rates-api/blob/master/src/services/apiServices.ts#L66) + Handles the GET request to retrieve version 2 of the exchange rates. ## Parameters -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> +### req + +`Request` The Express request object. -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>\> +### res + +`Response` The Express response object. @@ -29,7 +35,3 @@ The Express response object. ```typescript app.get('/rates/v2', getRatesV2); ``` - -## Defined in - -[src/services/apiServices.ts:66](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/apiServices.ts#L66) diff --git a/docs/src/services/apiServices/functions/getRatesV2Compressed.md b/docs/src/services/apiServices/functions/getRatesV2Compressed.md index 52b3800..770a9e2 100644 --- a/docs/src/services/apiServices/functions/getRatesV2Compressed.md +++ b/docs/src/services/apiServices/functions/getRatesV2Compressed.md @@ -1,22 +1,28 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/apiServices](../README.md) / getRatesV2Compressed +[rates-api](../../../../modules.md) / [src/services/apiServices](../README.md) / getRatesV2Compressed # Function: getRatesV2Compressed() > **getRatesV2Compressed**(`req`, `res`): `Promise`\<`void`\> +Defined in: [src/services/apiServices.ts:85](https://github.com/ZelCore-io/rates-api/blob/master/src/services/apiServices.ts#L85) + Handles the GET request to retrieve compressed version of the exchange rates (version 2). ## Parameters -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> +### req + +`Request` The Express request object. -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>\> +### res + +`Response` The Express response object. @@ -29,7 +35,3 @@ The Express response object. ```typescript app.get('/rates/v2/compressed', getRatesV2Compressed); ``` - -## Defined in - -[src/services/apiServices.ts:85](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/apiServices.ts#L85) diff --git a/docs/src/services/apiServices/functions/serviceRefresher.md b/docs/src/services/apiServices/functions/serviceRefresher.md index 8d4e9bc..b4a4392 100644 --- a/docs/src/services/apiServices/functions/serviceRefresher.md +++ b/docs/src/services/apiServices/functions/serviceRefresher.md @@ -1,13 +1,15 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/apiServices](../README.md) / serviceRefresher +[rates-api](../../../../modules.md) / [src/services/apiServices](../README.md) / serviceRefresher # Function: serviceRefresher() > **serviceRefresher**(): `Promise`\<`void`\> +Defined in: [src/services/apiServices.ts:224](https://github.com/ZelCore-io/rates-api/blob/master/src/services/apiServices.ts#L224) + Periodically refreshes market data and exchange rates. Fetches data from `zelcoreRates`, `zelcoreMarketsUSD`, and `zelcoreRatesV2`, @@ -23,7 +25,3 @@ Sets a delay before calling itself again. ```typescript serviceRefresher(); ``` - -## Defined in - -[src/services/apiServices.ts:224](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/apiServices.ts#L224) diff --git a/docs/src/services/apiServices/variables/default.md b/docs/src/services/apiServices/variables/default.md index 50ba3b5..fad3409 100644 --- a/docs/src/services/apiServices/variables/default.md +++ b/docs/src/services/apiServices/variables/default.md @@ -1,16 +1,18 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/apiServices](../README.md) / default +[rates-api](../../../../modules.md) / [src/services/apiServices](../README.md) / default # Variable: default > **default**: `object` -## Type declaration +Defined in: [src/services/apiServices.ts:280](https://github.com/ZelCore-io/rates-api/blob/master/src/services/apiServices.ts#L280) -### checkContractsV2() +## Type Declaration + +### checkContractsV2 > **checkContractsV2**: (`req`, `res`) => `Promise`\<`void`\> @@ -18,11 +20,15 @@ Handles the request to check for new contracts. #### Parameters -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> +##### req + +`Request` The Express request object containing `contracts` in the body. -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>\> +##### res + +`Response` The Express response object. @@ -36,7 +42,7 @@ The Express response object. app.post('/contracts/check', checkContractsV2); ``` -### dataRefresher() +### dataRefresher > **dataRefresher**: () => `Promise`\<`void`\> @@ -56,7 +62,7 @@ logs the error and retries after 30 minutes. dataRefresher(); ``` -### getData() +### getData > **getData**: () => `object` @@ -64,18 +70,29 @@ Retrieves the current rates and market data. #### Returns -`object` - An object containing `rates` and `marketsUSD`. ##### marketsUSD > **marketsUSD**: [`MarketsData`](../../../types/type-aliases/MarketsData.md) +Stores market data in USD. + +Structure: +- `marketsUSD[0]`: BTC to USD market data. +- `marketsUSD[1]`: Errors object. + ##### rates > **rates**: [`RatesData`](../../../types/type-aliases/RatesData.md) +Stores exchange rates data. + +Structure: +- `rates[0]`: BTC to fiat exchange rates. +- `rates[1]`: Alternative coins to fiat exchange rates. +- `rates[2]`: Errors object. + #### Example ```typescript @@ -83,7 +100,7 @@ const data = getData(); console.log(data.rates, data.marketsUSD); ``` -### getFoundContracts() +### getFoundContracts > **getFoundContracts**: () => [`FoundContractStore`](../../../types/type-aliases/FoundContractStore.md) @@ -101,7 +118,7 @@ The `foundContracts` object. const contracts = getFoundContracts(); ``` -### getMarketsUsd() +### getMarketsUsd > **getMarketsUsd**: (`req`, `res`) => `Promise`\<`void`\> @@ -109,11 +126,15 @@ Handles the GET request to retrieve market data in USD. #### Parameters -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> +##### req + +`Request` The Express request object. -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>\> +##### res + +`Response` The Express response object. @@ -127,7 +148,7 @@ The Express response object. app.get('/markets/usd', getMarketsUsd); ``` -### getRates() +### getRates > **getRates**: (`req`, `res`) => `Promise`\<`void`\> @@ -135,11 +156,15 @@ Handles the GET request to retrieve exchange rates. #### Parameters -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> +##### req + +`Request` The Express request object. -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>\> +##### res + +`Response` The Express response object. @@ -153,7 +178,7 @@ The Express response object. app.get('/rates', getRates); ``` -### getRatesV2() +### getRatesV2 > **getRatesV2**: (`req`, `res`) => `Promise`\<`void`\> @@ -161,11 +186,15 @@ Handles the GET request to retrieve version 2 of the exchange rates. #### Parameters -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> +##### req + +`Request` The Express request object. -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>\> +##### res + +`Response` The Express response object. @@ -179,7 +208,7 @@ The Express response object. app.get('/rates/v2', getRatesV2); ``` -### getRatesV2Compressed() +### getRatesV2Compressed > **getRatesV2Compressed**: (`req`, `res`) => `Promise`\<`void`\> @@ -187,11 +216,15 @@ Handles the GET request to retrieve compressed version of the exchange rates (ve #### Parameters -• **req**: `Request`\<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`\<`string`, `any`\>\> +##### req + +`Request` The Express request object. -• **res**: `Response`\<`any`, `Record`\<`string`, `any`\>\> +##### res + +`Response` The Express response object. @@ -205,7 +238,7 @@ The Express response object. app.get('/rates/v2/compressed', getRatesV2Compressed); ``` -### serviceRefresher() +### serviceRefresher > **serviceRefresher**: () => `Promise`\<`void`\> @@ -224,7 +257,3 @@ Sets a delay before calling itself again. ```typescript serviceRefresher(); ``` - -## Defined in - -[src/services/apiServices.ts:263](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/apiServices.ts#L263) diff --git a/docs/src/services/bstocks/README.md b/docs/src/services/bstocks/README.md new file mode 100644 index 0000000..885fcac --- /dev/null +++ b/docs/src/services/bstocks/README.md @@ -0,0 +1,14 @@ +[**rates-api v3.0.0**](../../../README.md) + +*** + +[rates-api](../../../modules.md) / src/services/bstocks + +# src/services/bstocks + +## Functions + +- [\_clearLastGoodForTests](functions/clearLastGoodForTests.md) +- [getBstockPrices](functions/getBstockPrices.md) +- [getLastGoodBstockPrices](functions/getLastGoodBstockPrices.md) +- [isBstocksDegraded](functions/isBstocksDegraded.md) diff --git a/docs/src/services/bstocks/functions/clearLastGoodForTests.md b/docs/src/services/bstocks/functions/clearLastGoodForTests.md new file mode 100644 index 0000000..4b9511b --- /dev/null +++ b/docs/src/services/bstocks/functions/clearLastGoodForTests.md @@ -0,0 +1,15 @@ +[**rates-api v3.0.0**](../../../../README.md) + +*** + +[rates-api](../../../../modules.md) / [src/services/bstocks](../README.md) / \_clearLastGoodForTests + +# Function: \_clearLastGoodForTests() + +> **\_clearLastGoodForTests**(): `void` + +Defined in: [src/services/bstocks.ts:25](https://github.com/ZelCore-io/rates-api/blob/master/src/services/bstocks.ts#L25) + +## Returns + +`void` diff --git a/docs/src/services/bstocks/functions/getBstockPrices.md b/docs/src/services/bstocks/functions/getBstockPrices.md new file mode 100644 index 0000000..559ffa1 --- /dev/null +++ b/docs/src/services/bstocks/functions/getBstockPrices.md @@ -0,0 +1,48 @@ +[**rates-api v3.0.0**](../../../../README.md) + +*** + +[rates-api](../../../../modules.md) / [src/services/bstocks](../README.md) / getBstockPrices + +# Function: getBstockPrices() + +> **getBstockPrices**(): `Promise`\<[`CryptoPrice`](../../../types/type-aliases/CryptoPrice.md)[]\> + +Defined in: [src/services/bstocks.ts:101](https://github.com/ZelCore-io/rates-api/blob/master/src/services/bstocks.ts#L101) + +Assembles the bStocks synthetic market: the intersection of Binance's +tokenised-asset universe (already filtered to BSC-listed assets by +`Binance.getTokenisedAssets`) with Spot symbols currently in `TRADING` +status, quoted in USDT. + +BTC/USD conversion uses BTCUSDT fetched in the same 24h-ticker batch as the +bStock symbols, so both legs come from the same venue and no cross-venue +basis is introduced. + +Emitted ids are `bstock-` under `provider: "coingecko"` +— NOT `"binance"`. The client does no prefix parsing: ZelCore's +`store/actions.js` (`applyMarkets`) keys the market store on the literal +string `${provider}-${id}`, and `use-fiat.js` builds the same literal from +`coininfo.json`'s `coingeckoID` as `coingecko-${coingeckoID}`. The sibling +`api` repo serves `coinInfo.coingeckoID = "bstock-"`, so the two +literals only meet if the provider here is exactly `"coingecko"`. Any other +value makes the lookup miss silently — no error, just no price. This +id/provider pairing is a cross-repo contract; do not change it in isolation. + +`rank` is intentionally omitted (not zeroed) to match CryptoCompare's rows +elsewhere in this repo, which also carry no `rank`: a literal `rank: 0` +would sort every bStock ahead of Bitcoin in any ascending rank-ordered list. + +A module-level last-known-good map means a symbol that drops out of a given +refresh (CEX halt, e.g. around a stock split) keeps being served at its +previous price rather than disappearing from the response, bounded by +`config.bstocksLastGoodMaxAgeMs` (see the halting comment on `lastGood` +above) so a permanently-delisted symbol doesn't get served forever. + +## Returns + +`Promise`\<[`CryptoPrice`](../../../types/type-aliases/CryptoPrice.md)[]\> + +One `CryptoPrice` per tradable bStock (BSC contract + TRADING +`USDT` Spot symbol) still within the staleness bound, including any +carried over from a prior refresh. diff --git a/docs/src/services/bstocks/functions/getLastGoodBstockPrices.md b/docs/src/services/bstocks/functions/getLastGoodBstockPrices.md new file mode 100644 index 0000000..7f00ebc --- /dev/null +++ b/docs/src/services/bstocks/functions/getLastGoodBstockPrices.md @@ -0,0 +1,25 @@ +[**rates-api v3.0.0**](../../../../README.md) + +*** + +[rates-api](../../../../modules.md) / [src/services/bstocks](../README.md) / getLastGoodBstockPrices + +# Function: getLastGoodBstockPrices() + +> **getLastGoodBstockPrices**(): [`CryptoPrice`](../../../types/type-aliases/CryptoPrice.md)[] + +Defined in: [src/services/bstocks.ts:60](https://github.com/ZelCore-io/rates-api/blob/master/src/services/bstocks.ts#L60) + +The current last-known-good rows, without touching Binance. + +Used when a caller has given up waiting on `getBstockPrices()`. Returning +an empty array there would drop every bStock from the response while the +provider-level carry-forward in apiServices cannot help: bStock rows carry +`provider: 'coingecko'` but their failure is reported under +`errors.binance`, so nothing would carry them. + +## Returns + +[`CryptoPrice`](../../../types/type-aliases/CryptoPrice.md)[] + +The last-known-good rows, stale entries already pruned. diff --git a/docs/src/services/bstocks/functions/isBstocksDegraded.md b/docs/src/services/bstocks/functions/isBstocksDegraded.md new file mode 100644 index 0000000..e7f8f42 --- /dev/null +++ b/docs/src/services/bstocks/functions/isBstocksDegraded.md @@ -0,0 +1,24 @@ +[**rates-api v3.0.0**](../../../../README.md) + +*** + +[rates-api](../../../../modules.md) / [src/services/bstocks](../README.md) / isBstocksDegraded + +# Function: isBstocksDegraded() + +> **isBstocksDegraded**(): `boolean` + +Defined in: [src/services/bstocks.ts:40](https://github.com/ZelCore-io/rates-api/blob/master/src/services/bstocks.ts#L40) + +True when the most recent `getBstockPrices()` call priced nothing fresh — +every underlying Binance call failed, returned unusable data, or served +only prices carried over from an earlier refresh. Distinguishes "Binance is +down and we're serving frozen prices" from a normal, healthy refresh, which +`getBstockPrices()`'s return value alone cannot express since it never +rejects and unconditionally re-emits `lastGood` either way. + +## Returns + +`boolean` + +Whether the bStocks pipeline is currently degraded. diff --git a/docs/src/services/coinAggregatorIDs/README.md b/docs/src/services/coinAggregatorIDs/README.md index cbc85b7..b89b0a3 100644 --- a/docs/src/services/coinAggregatorIDs/README.md +++ b/docs/src/services/coinAggregatorIDs/README.md @@ -1,20 +1,18 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/services/coinAggregatorIDs +[rates-api](../../../modules.md) / src/services/coinAggregatorIDs # src/services/coinAggregatorIDs -## Index - -### Variables +## Variables - [cgContractMap](variables/cgContractMap.md) - [cgTokens](variables/cgTokens.md) - [coinAggregatorIDs](variables/coinAggregatorIDs.md) - [zelData](variables/zelData.md) -### Functions +## Functions - [getLatestCoinInfo](functions/getLatestCoinInfo.md) diff --git a/docs/src/services/coinAggregatorIDs/functions/getLatestCoinInfo.md b/docs/src/services/coinAggregatorIDs/functions/getLatestCoinInfo.md index 6ce46f4..d71ec2e 100644 --- a/docs/src/services/coinAggregatorIDs/functions/getLatestCoinInfo.md +++ b/docs/src/services/coinAggregatorIDs/functions/getLatestCoinInfo.md @@ -1,13 +1,15 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/coinAggregatorIDs](../README.md) / getLatestCoinInfo +[rates-api](../../../../modules.md) / [src/services/coinAggregatorIDs](../README.md) / getLatestCoinInfo # Function: getLatestCoinInfo() > **getLatestCoinInfo**(): `Promise`\<`void`\> +Defined in: [src/services/coinAggregatorIDs.ts:94](https://github.com/ZelCore-io/rates-api/blob/master/src/services/coinAggregatorIDs.ts#L94) + Fetches the latest coin information and updates the global data. This function retrieves coin information from a specified URL, updates the CoinGecko IDs, @@ -27,7 +29,3 @@ A promise that resolves when the operation is complete. await getLatestCoinInfo(); console.log(zelData.coinInfo); ``` - -## Defined in - -[src/services/coinAggregatorIDs.ts:92](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/coinAggregatorIDs.ts#L92) diff --git a/docs/src/services/coinAggregatorIDs/variables/cgContractMap.md b/docs/src/services/coinAggregatorIDs/variables/cgContractMap.md index 866065d..46e67ae 100644 --- a/docs/src/services/coinAggregatorIDs/variables/cgContractMap.md +++ b/docs/src/services/coinAggregatorIDs/variables/cgContractMap.md @@ -1,15 +1,13 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/coinAggregatorIDs](../README.md) / cgContractMap +[rates-api](../../../../modules.md) / [src/services/coinAggregatorIDs](../README.md) / cgContractMap # Variable: cgContractMap > `const` **cgContractMap**: `Record`\<`string`, [`CoinGeckoToken`](../../../types/type-aliases/CoinGeckoToken.md)\> = `{}` -Map of contract addresses to CoinGecko tokens. - -## Defined in +Defined in: [src/services/coinAggregatorIDs.ts:77](https://github.com/ZelCore-io/rates-api/blob/master/src/services/coinAggregatorIDs.ts#L77) -[src/services/coinAggregatorIDs.ts:75](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/coinAggregatorIDs.ts#L75) +Map of contract addresses to CoinGecko tokens. diff --git a/docs/src/services/coinAggregatorIDs/variables/cgTokens.md b/docs/src/services/coinAggregatorIDs/variables/cgTokens.md index e41d01a..8c255be 100644 --- a/docs/src/services/coinAggregatorIDs/variables/cgTokens.md +++ b/docs/src/services/coinAggregatorIDs/variables/cgTokens.md @@ -1,15 +1,13 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/coinAggregatorIDs](../README.md) / cgTokens +[rates-api](../../../../modules.md) / [src/services/coinAggregatorIDs](../README.md) / cgTokens # Variable: cgTokens > **cgTokens**: [`CoinGeckoToken`](../../../types/type-aliases/CoinGeckoToken.md)[] = `cgCoins` -Array of CoinGecko tokens. - -## Defined in +Defined in: [src/services/coinAggregatorIDs.ts:72](https://github.com/ZelCore-io/rates-api/blob/master/src/services/coinAggregatorIDs.ts#L72) -[src/services/coinAggregatorIDs.ts:70](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/coinAggregatorIDs.ts#L70) +Array of CoinGecko tokens. diff --git a/docs/src/services/coinAggregatorIDs/variables/coinAggregatorIDs.md b/docs/src/services/coinAggregatorIDs/variables/coinAggregatorIDs.md index a9cbde1..ff14829 100644 --- a/docs/src/services/coinAggregatorIDs/variables/coinAggregatorIDs.md +++ b/docs/src/services/coinAggregatorIDs/variables/coinAggregatorIDs.md @@ -1,16 +1,18 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/coinAggregatorIDs](../README.md) / coinAggregatorIDs +[rates-api](../../../../modules.md) / [src/services/coinAggregatorIDs](../README.md) / coinAggregatorIDs # Variable: coinAggregatorIDs > `const` **coinAggregatorIDs**: `object` +Defined in: [src/services/coinAggregatorIDs.ts:14](https://github.com/ZelCore-io/rates-api/blob/master/src/services/coinAggregatorIDs.ts#L14) + An object containing arrays of cryptocurrency IDs used by different data aggregators. -## Type declaration +## Type Declaration ### coingecko @@ -36,7 +38,3 @@ Add the CryptoCompare IDs at the end of this list. LiveCoinWatch API IDs. ## Const - -## Defined in - -[src/services/coinAggregatorIDs.ts:14](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/coinAggregatorIDs.ts#L14) diff --git a/docs/src/services/coinAggregatorIDs/variables/zelData.md b/docs/src/services/coinAggregatorIDs/variables/zelData.md index 7c79465..3cc90ca 100644 --- a/docs/src/services/coinAggregatorIDs/variables/zelData.md +++ b/docs/src/services/coinAggregatorIDs/variables/zelData.md @@ -1,21 +1,19 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/coinAggregatorIDs](../README.md) / zelData +[rates-api](../../../../modules.md) / [src/services/coinAggregatorIDs](../README.md) / zelData # Variable: zelData > `const` **zelData**: `object` +Defined in: [src/services/coinAggregatorIDs.ts:61](https://github.com/ZelCore-io/rates-api/blob/master/src/services/coinAggregatorIDs.ts#L61) + Global object to store coin information. -## Type declaration +## Type Declaration ### coinInfo > **coinInfo**: `Record`\<`string`, [`CoinInfo`](../../../types/type-aliases/CoinInfo.md)\> - -## Defined in - -[src/services/coinAggregatorIDs.ts:61](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/coinAggregatorIDs.ts#L61) diff --git a/docs/src/services/newContracts/README.md b/docs/src/services/newContracts/README.md index 345ae48..3a960da 100644 --- a/docs/src/services/newContracts/README.md +++ b/docs/src/services/newContracts/README.md @@ -1,17 +1,15 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/services/newContracts +[rates-api](../../../modules.md) / src/services/newContracts # src/services/newContracts -## Index - -### Variables +## Variables - [foundContracts](variables/foundContracts.md) -### Functions +## Functions - [checkContracts](functions/checkContracts.md) diff --git a/docs/src/services/newContracts/functions/checkContracts.md b/docs/src/services/newContracts/functions/checkContracts.md index 4c82e91..9c007f0 100644 --- a/docs/src/services/newContracts/functions/checkContracts.md +++ b/docs/src/services/newContracts/functions/checkContracts.md @@ -1,13 +1,15 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/newContracts](../README.md) / checkContracts +[rates-api](../../../../modules.md) / [src/services/newContracts](../README.md) / checkContracts # Function: checkContracts() > **checkContracts**(`contracts`): `boolean` +Defined in: [src/services/newContracts.ts:32](https://github.com/ZelCore-io/rates-api/blob/master/src/services/newContracts.ts#L32) + Checks the provided contracts against the CoinGecko contract map and updates the `foundContracts` store. This function iterates over an array of contracts, checks if they exist in the CoinGecko contract map, @@ -15,7 +17,9 @@ and updates the `foundContracts` object by incrementing the count or adding a ne ## Parameters -• **contracts**: [`ContractWithType`](../../../types/type-aliases/ContractWithType.md)[] +### contracts + +[`ContractWithType`](../../../types/type-aliases/ContractWithType.md)[] An array of contracts with their types. @@ -38,7 +42,3 @@ const contracts = [ const success = checkContracts(contracts); console.log('Contracts checked:', success); ``` - -## Defined in - -[src/services/newContracts.ts:32](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/newContracts.ts#L32) diff --git a/docs/src/services/newContracts/variables/foundContracts.md b/docs/src/services/newContracts/variables/foundContracts.md index bc92307..51d9d26 100644 --- a/docs/src/services/newContracts/variables/foundContracts.md +++ b/docs/src/services/newContracts/variables/foundContracts.md @@ -1,15 +1,13 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/newContracts](../README.md) / foundContracts +[rates-api](../../../../modules.md) / [src/services/newContracts](../README.md) / foundContracts # Variable: foundContracts > `const` **foundContracts**: [`FoundContractStore`](../../../types/type-aliases/FoundContractStore.md) = `{}` -Stores the found contracts with their occurrence count. - -## Defined in +Defined in: [src/services/newContracts.ts:8](https://github.com/ZelCore-io/rates-api/blob/master/src/services/newContracts.ts#L8) -[src/services/newContracts.ts:8](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/newContracts.ts#L8) +Stores the found contracts with their occurrence count. diff --git a/docs/src/services/providers/README.md b/docs/src/services/providers/README.md index b80fc59..0707039 100644 --- a/docs/src/services/providers/README.md +++ b/docs/src/services/providers/README.md @@ -1,13 +1,19 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/services/providers +[rates-api](../../../modules.md) / src/services/providers # src/services/providers ## References +### Binance + +Re-exports [Binance](binance/classes/Binance.md) + +*** + ### BitPay Re-exports [BitPay](bitpay/classes/BitPay.md) diff --git a/docs/src/services/providers/binance/README.md b/docs/src/services/providers/binance/README.md new file mode 100644 index 0000000..b343326 --- /dev/null +++ b/docs/src/services/providers/binance/README.md @@ -0,0 +1,17 @@ +[**rates-api v3.0.0**](../../../../README.md) + +*** + +[rates-api](../../../../modules.md) / src/services/providers/binance + +# src/services/providers/binance + +## Classes + +- [Binance](classes/Binance.md) + +## References + +### default + +Renames and re-exports [Binance](classes/Binance.md) diff --git a/docs/src/services/providers/binance/classes/Binance.md b/docs/src/services/providers/binance/classes/Binance.md new file mode 100644 index 0000000..06b115d --- /dev/null +++ b/docs/src/services/providers/binance/classes/Binance.md @@ -0,0 +1,281 @@ +[**rates-api v3.0.0**](../../../../../README.md) + +*** + +[rates-api](../../../../../modules.md) / [src/services/providers/binance](../README.md) / Binance + +# Class: Binance + +Defined in: [src/services/providers/binance.ts:38](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/binance.ts#L38) + +Singleton class to interact with Binance's public (no-API-key) endpoints. + +Provides the tokenised-asset universe (bStocks with a BSC contract) and Spot +24h/7d tickers, quoted in USDT. Mirrors `CoinGecko`'s shape: an `AxiosWrapper` +per base URL, an `LRUCache` per refresh cadence, and defensive error handling +that never lets a single failed refresh drop a symbol that was previously +known good (e.g. during a CEX trading halt around a stock split). + +## Example + +```typescript +import { Binance } from './binance'; + +async function fetchBStocks() { + const binance = Binance.getInstance(); + const assets = await binance.getTokenisedAssets(); + const trading = await binance.getTradingSymbols(); + const tickers = await binance.getTicker24h([...trading]); + console.log(tickers); +} +``` + +## Constructors + +### Constructor + +> **new Binance**(): `Binance` + +#### Returns + +`Binance` + +## Methods + +### chunkSymbols() + +> **chunkSymbols**(`symbols`): `string`[][] + +Defined in: [src/services/providers/binance.ts:137](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/binance.ts#L137) + +Splits a symbol list into chunks of at most `TICKER_CHUNK` symbols, to stay +under Binance's per-request weight cap on the 7d rolling-window ticker. + +#### Parameters + +##### symbols + +`string`[] + +The full symbol list to split. + +#### Returns + +`string`[][] + +An array of symbol chunks. + +*** + +### filterBscAssets() + +> **filterBscAssets**(`assets`): [`BinanceTokenisedAsset`](../../../../types/type-aliases/BinanceTokenisedAsset.md)[] + +Defined in: [src/services/providers/binance.ts:124](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/binance.ts#L124) + +Filters tokenised assets down to those with a BSC (BNB Smart Chain) contract listed. + +#### Parameters + +##### assets + +[`BinanceTokenisedAsset`](../../../../types/type-aliases/BinanceTokenisedAsset.md)[] + +The raw tokenised-asset list from Binance. + +#### Returns + +[`BinanceTokenisedAsset`](../../../../types/type-aliases/BinanceTokenisedAsset.md)[] + +Only the assets with at least one BSC entry in `caList`. + +*** + +### getTicker24h() + +> **getTicker24h**(`symbols`): `Promise`\<[`BinanceTicker`](../../../../types/type-aliases/BinanceTicker.md)[]\> + +Defined in: [src/services/providers/binance.ts:291](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/binance.ts#L291) + +Retrieves 24h tickers for the given symbols in a single request. + +On a failed or partial refresh, missing symbols are backfilled from the +last-known-good store rather than dropped. Cached for 60 seconds per +requested symbol set. + +#### Parameters + +##### symbols + +`string`[] + +The Spot symbols to fetch (e.g. `TSLABUSDT`). + +#### Returns + +`Promise`\<[`BinanceTicker`](../../../../types/type-aliases/BinanceTicker.md)[]\> + +One ticker per requested symbol that has ever been seen. + +*** + +### getTicker7d() + +> **getTicker7d**(`symbols`): `Promise`\<[`BinanceTicker`](../../../../types/type-aliases/BinanceTicker.md)[]\> + +Defined in: [src/services/providers/binance.ts:321](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/binance.ts#L321) + +Retrieves 7d rolling-window tickers for the given symbols, chunked to stay +under Binance's per-request weight cap. + +Each chunk is fetched independently, so one failing chunk never drops the +symbols in the others; any symbol whose chunk failed (or that was omitted, +e.g. a halt) is backfilled from the last-known-good store. Cached for 60 +seconds per requested symbol set. + +#### Parameters + +##### symbols + +`string`[] + +The Spot symbols to fetch (e.g. `TSLABUSDT`). + +#### Returns + +`Promise`\<[`BinanceTicker`](../../../../types/type-aliases/BinanceTicker.md)[]\> + +One ticker per requested symbol that has ever been seen. + +*** + +### getTokenisedAssets() + +> **getTokenisedAssets**(): `Promise`\<[`BinanceTokenisedAsset`](../../../../types/type-aliases/BinanceTokenisedAsset.md)[]\> + +Defined in: [src/services/providers/binance.ts:226](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/binance.ts#L226) + +Retrieves the tokenised-asset universe (bStocks), filtered to those with a BSC contract. + +Cached for 1 hour. + +#### Returns + +`Promise`\<[`BinanceTokenisedAsset`](../../../../types/type-aliases/BinanceTokenisedAsset.md)[]\> + +The BSC-listed tokenised assets. + +*** + +### getTradingSymbols() + +> **getTradingSymbols**(): `Promise`\<`Set`\<`string`\>\> + +Defined in: [src/services/providers/binance.ts:258](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/binance.ts#L258) + +Retrieves the set of Spot symbols currently in `TRADING` status. + +A symbol dropping to `BREAK` (as happens during trading halts, e.g. around +a stock split) simply falls out of this set on the next refresh; callers +should keep serving the last-known-good ticker for it rather than treating +its absence here as "delisted". + +Cached for 1 hour. + +#### Returns + +`Promise`\<`Set`\<`string`\>\> + +The set of currently-trading symbols. + +*** + +### lastGoodAgeMs() + +> **lastGoodAgeMs**(`symbol`, `window?`): `number` \| `null` + +Defined in: [src/services/providers/binance.ts:186](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/binance.ts#L186) + +Age in milliseconds of the last-known-good price for a symbol, or null if +none has ever been recorded for the requested window(s). Lets a caller +distinguish a live price from one carried through a long halt, which the +ticker itself cannot express. + +#### Parameters + +##### symbol + +`string` + +The Binance symbol, e.g. `TSLABUSDT`. + +##### window? + +`"7d"` \| `"24h"` + +Which window's last-known-good entry to check (`24h` or +`7d`). Omit to get the freshest of the two — the age of whichever window +priced most recently — which is what a caller asking "how stale is this +symbol overall" generally wants. + +#### Returns + +`number` \| `null` + +Age in ms, or null when the symbol has never priced successfully +for the requested window (or for either window, when unspecified). + +*** + +### pricedFresh() + +> **pricedFresh**(`symbol`, `window`): `boolean` + +Defined in: [src/services/providers/binance.ts:214](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/binance.ts#L214) + +Whether the price currently served for a symbol comes from a live quote +rather than the last-known-good backfill. + +`mergeTickers` returns a plain `BinanceTicker` whether it was fetched or +carried, so a caller cannot tell the two apart from the returned value — +and a carried price is a valid, positive number, which makes the +difference invisible to any price check. A batch answered from +`quoteCache` legitimately carries a price up to one cache TTL old, so +anything within that window is live; past it, nothing has priced the +symbol since, so every batch in between was backfilled. + +#### Parameters + +##### symbol + +`string` + +The Binance symbol, e.g. `TSLABUSDT`. + +##### window + +`"7d"` \| `"24h"` + +Which ticker window to check (`24h` or `7d`). + +#### Returns + +`boolean` + +True when the symbol priced live within the quote-cache window. + +*** + +### getInstance() + +> `static` **getInstance**(): `Binance` + +Defined in: [src/services/providers/binance.ts:112](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/binance.ts#L112) + +Returns the singleton instance of the Binance class. + +#### Returns + +`Binance` + +The singleton instance of Binance. diff --git a/docs/src/services/providers/bitpay/README.md b/docs/src/services/providers/bitpay/README.md index 55b02a1..2a78cda 100644 --- a/docs/src/services/providers/bitpay/README.md +++ b/docs/src/services/providers/bitpay/README.md @@ -1,13 +1,11 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / src/services/providers/bitpay +[rates-api](../../../../modules.md) / src/services/providers/bitpay # src/services/providers/bitpay -## Index - -### Classes +## Classes - [BitPay](classes/BitPay.md) diff --git a/docs/src/services/providers/bitpay/classes/BitPay.md b/docs/src/services/providers/bitpay/classes/BitPay.md index 12f72dd..c678d94 100644 --- a/docs/src/services/providers/bitpay/classes/BitPay.md +++ b/docs/src/services/providers/bitpay/classes/BitPay.md @@ -1,11 +1,13 @@ -[**rates-api v3.0.0**](../../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../../README.md) *** -[rates-api v3.0.0](../../../../../modules.md) / [src/services/providers/bitpay](../README.md) / BitPay +[rates-api](../../../../../modules.md) / [src/services/providers/bitpay](../README.md) / BitPay # Class: BitPay +Defined in: [src/services/providers/bitpay.ts:24](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/bitpay.ts#L24) + Singleton class to interact with the BitPay API. This class provides methods to retrieve fiat currency exchange rates from the BitPay API. @@ -27,9 +29,11 @@ fetchRates(); ## Constructors -### new BitPay() +### Constructor + +> **new BitPay**(): `BitPay` -> **new BitPay**(): [`BitPay`](BitPay.md) +Defined in: [src/services/providers/bitpay.ts:58](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/bitpay.ts#L58) Private constructor to enforce the singleton pattern. @@ -37,22 +41,20 @@ Initializes the AxiosWrapper and the LRU cache. #### Returns -[`BitPay`](BitPay.md) +`BitPay` #### Throws If an instance already exists. -#### Defined in - -[src/services/providers/bitpay.ts:58](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/bitpay.ts#L58) - ## Methods ### getFiatRates() > **getFiatRates**(): `Promise`\<`any`\> +Defined in: [src/services/providers/bitpay.ts:115](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/bitpay.ts#L115) + Retrieves fiat currency exchange rates from the BitPay API. Utilizes caching to prevent unnecessary API calls. If the rates are cached and valid, @@ -72,21 +74,19 @@ const rates = await bitPay.getFiatRates(); console.log(rates); ``` -#### Defined in - -[src/services/providers/bitpay.ts:115](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/bitpay.ts#L115) - *** ### getInstance() -> `static` **getInstance**(): [`BitPay`](BitPay.md) +> `static` **getInstance**(): `BitPay` + +Defined in: [src/services/providers/bitpay.ts:81](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/bitpay.ts#L81) Returns the singleton instance of the BitPay class. #### Returns -[`BitPay`](BitPay.md) +`BitPay` The singleton instance of BitPay. @@ -95,7 +95,3 @@ The singleton instance of BitPay. ```typescript const bitPay = BitPay.getInstance(); ``` - -#### Defined in - -[src/services/providers/bitpay.ts:81](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/bitpay.ts#L81) diff --git a/docs/src/services/providers/coinGecko/README.md b/docs/src/services/providers/coinGecko/README.md index bed55e4..3f5a61d 100644 --- a/docs/src/services/providers/coinGecko/README.md +++ b/docs/src/services/providers/coinGecko/README.md @@ -1,13 +1,11 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / src/services/providers/coinGecko +[rates-api](../../../../modules.md) / src/services/providers/coinGecko # src/services/providers/coinGecko -## Index - -### Classes +## Classes - [CoinGecko](classes/CoinGecko.md) diff --git a/docs/src/services/providers/coinGecko/classes/CoinGecko.md b/docs/src/services/providers/coinGecko/classes/CoinGecko.md index c7853bb..9382ae9 100644 --- a/docs/src/services/providers/coinGecko/classes/CoinGecko.md +++ b/docs/src/services/providers/coinGecko/classes/CoinGecko.md @@ -1,11 +1,13 @@ -[**rates-api v3.0.0**](../../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../../README.md) *** -[rates-api v3.0.0](../../../../../modules.md) / [src/services/providers/coinGecko](../README.md) / CoinGecko +[rates-api](../../../../../modules.md) / [src/services/providers/coinGecko](../README.md) / CoinGecko # Class: CoinGecko +Defined in: [src/services/providers/coinGecko.ts:40](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/coinGecko.ts#L40) + Singleton class to interact with the CoinGecko API. This class provides methods to retrieve cryptocurrency data from CoinGecko. @@ -27,9 +29,11 @@ fetchRates(); ## Constructors -### new CoinGecko() +### Constructor + +> **new CoinGecko**(): `CoinGecko` -> **new CoinGecko**(): [`CoinGecko`](CoinGecko.md) +Defined in: [src/services/providers/coinGecko.ts:81](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/coinGecko.ts#L81) Private constructor to enforce the singleton pattern. @@ -37,22 +41,20 @@ Initializes the AxiosWrapper and the LRU cache. #### Returns -[`CoinGecko`](CoinGecko.md) +`CoinGecko` #### Throws If an instance already exists. -#### Defined in - -[src/services/providers/coinGecko.ts:81](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/coinGecko.ts#L81) - ## Methods ### getAssetPlatformData() > **getAssetPlatformData**(): `Promise`\<`any`\> +Defined in: [src/services/providers/coinGecko.ts:207](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/coinGecko.ts#L207) + Retrieves asset platform data from CoinGecko. #### Returns @@ -69,21 +71,21 @@ const assetPlatforms = await coinGecko.getAssetPlatformData(); console.log('Asset Platforms:', assetPlatforms); ``` -#### Defined in - -[src/services/providers/coinGecko.ts:207](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/coinGecko.ts#L207) - *** ### getCoinsList() -> **getCoinsList**(`includePlatform`): `Promise`\<`any`\> +> **getCoinsList**(`includePlatform?`): `Promise`\<`any`\> + +Defined in: [src/services/providers/coinGecko.ts:173](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/coinGecko.ts#L173) Retrieves a list of all coins supported by CoinGecko. #### Parameters -• **includePlatform**: `boolean` = `true` +##### includePlatform? + +`boolean` = `true` Whether to include platform data in the response (default is `true`). @@ -101,15 +103,13 @@ const coinsList = await coinGecko.getCoinsList(); console.log('Coins List:', coinsList); ``` -#### Defined in - -[src/services/providers/coinGecko.ts:173](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/coinGecko.ts#L173) - *** ### getExchangeRates() -> **getExchangeRates**(`ids`, `vsCurrency`): `Promise`\<[`CoinGeckoPrice`](../../../../types/type-aliases/CoinGeckoPrice.md)[]\> +> **getExchangeRates**(`ids`, `vsCurrency?`): `Promise`\<[`CoinGeckoPrice`](../../../../types/type-aliases/CoinGeckoPrice.md)[]\> + +Defined in: [src/services/providers/coinGecko.ts:278](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/coinGecko.ts#L278) Retrieves exchange rates for an array of coin IDs. @@ -117,11 +117,15 @@ Handles splitting the IDs into batches to comply with API limitations. #### Parameters -• **ids**: `string`[] +##### ids + +`string`[] An array of coin IDs. -• **vsCurrency**: `string` = `'btc'` +##### vsCurrency? + +`string` = `'btc'` The target currency (default is 'btc'). @@ -139,15 +143,13 @@ const rates = await coinGecko.getExchangeRates(['bitcoin', 'ethereum', 'litecoin console.log('Exchange Rates:', rates); ``` -#### Defined in - -[src/services/providers/coinGecko.ts:278](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/coinGecko.ts#L278) - *** ### getKeyUsage() -> **getKeyUsage**(): `Promise`\<`null` \| `KeyUsage`\> +> **getKeyUsage**(): `Promise`\<`KeyUsage` \| `null`\> + +Defined in: [src/services/providers/coinGecko.ts:138](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/coinGecko.ts#L138) Retrieves the usage statistics of the CoinGecko API key. @@ -156,7 +158,7 @@ it returns it directly from the cache. Otherwise, it fetches new data from the A #### Returns -`Promise`\<`null` \| `KeyUsage`\> +`Promise`\<`KeyUsage` \| `null`\> The key usage data or `null` if an error occurs. @@ -168,21 +170,19 @@ const usage = await coinGecko.getKeyUsage(); console.log('API Key Usage:', usage); ``` -#### Defined in - -[src/services/providers/coinGecko.ts:138](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/coinGecko.ts#L138) - *** ### getInstance() -> `static` **getInstance**(): [`CoinGecko`](CoinGecko.md) +> `static` **getInstance**(): `CoinGecko` + +Defined in: [src/services/providers/coinGecko.ts:104](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/coinGecko.ts#L104) Returns the singleton instance of the CoinGecko class. #### Returns -[`CoinGecko`](CoinGecko.md) +`CoinGecko` The singleton instance of CoinGecko. @@ -191,7 +191,3 @@ The singleton instance of CoinGecko. ```typescript const coinGecko = CoinGecko.getInstance(); ``` - -#### Defined in - -[src/services/providers/coinGecko.ts:104](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/coinGecko.ts#L104) diff --git a/docs/src/services/providers/cryptoCompare/README.md b/docs/src/services/providers/cryptoCompare/README.md index 931e9ee..a974ec4 100644 --- a/docs/src/services/providers/cryptoCompare/README.md +++ b/docs/src/services/providers/cryptoCompare/README.md @@ -1,13 +1,11 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / src/services/providers/cryptoCompare +[rates-api](../../../../modules.md) / src/services/providers/cryptoCompare # src/services/providers/cryptoCompare -## Index - -### Classes +## Classes - [CryptoCompare](classes/CryptoCompare.md) diff --git a/docs/src/services/providers/cryptoCompare/classes/CryptoCompare.md b/docs/src/services/providers/cryptoCompare/classes/CryptoCompare.md index 7d95fb6..2b89b98 100644 --- a/docs/src/services/providers/cryptoCompare/classes/CryptoCompare.md +++ b/docs/src/services/providers/cryptoCompare/classes/CryptoCompare.md @@ -1,11 +1,13 @@ -[**rates-api v3.0.0**](../../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../../README.md) *** -[rates-api v3.0.0](../../../../../modules.md) / [src/services/providers/cryptoCompare](../README.md) / CryptoCompare +[rates-api](../../../../../modules.md) / [src/services/providers/cryptoCompare](../README.md) / CryptoCompare # Class: CryptoCompare +Defined in: [src/services/providers/cryptoCompare.ts:28](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/cryptoCompare.ts#L28) + Singleton class to interact with the CryptoCompare API. This class provides methods to retrieve cryptocurrency exchange rates and market data from CryptoCompare. @@ -27,9 +29,11 @@ fetchExchangeRates(); ## Constructors -### new CryptoCompare() +### Constructor + +> **new CryptoCompare**(): `CryptoCompare` -> **new CryptoCompare**(): [`CryptoCompare`](CryptoCompare.md) +Defined in: [src/services/providers/cryptoCompare.ts:69](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/cryptoCompare.ts#L69) Private constructor to enforce the singleton pattern. @@ -37,21 +41,19 @@ Initializes the AxiosWrapper and the LRU cache. #### Returns -[`CryptoCompare`](CryptoCompare.md) +`CryptoCompare` #### Throws If an instance already exists. -#### Defined in - -[src/services/providers/cryptoCompare.ts:69](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/cryptoCompare.ts#L69) - ## Methods ### getExchangeRates() -> **getExchangeRates**(`ids`, `vsCurrency`): `Promise`\<[`CryptoComparePrice`](../../../../types/type-aliases/CryptoComparePrice.md)\> +> **getExchangeRates**(`ids`, `vsCurrency?`): `Promise`\<[`CryptoComparePrice`](../../../../types/type-aliases/CryptoComparePrice.md)\> + +Defined in: [src/services/providers/cryptoCompare.ts:163](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/cryptoCompare.ts#L163) Retrieves exchange rates for an array of cryptocurrency symbols. @@ -59,11 +61,15 @@ Handles splitting the symbols into batches to comply with API limitations. #### Parameters -• **ids**: `string`[] +##### ids + +`string`[] An array of cryptocurrency symbols (e.g., ['BTC', 'ETH']). -• **vsCurrency**: `string` = `'BTC'` +##### vsCurrency? + +`string` = `'BTC'` The target currency symbol (default is 'BTC'). @@ -81,15 +87,13 @@ const rates = await cryptoCompare.getExchangeRates(['BTC', 'ETH'], 'USD'); console.log('Exchange Rates:', rates); ``` -#### Defined in - -[src/services/providers/cryptoCompare.ts:163](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/cryptoCompare.ts#L163) - *** ### getMarketData() -> **getMarketData**(`ids`, `vsCurrency`): `Promise`\<[`CryptoCompareMarkets`](../../../../types/type-aliases/CryptoCompareMarkets.md)\> +> **getMarketData**(`ids`, `vsCurrency?`): `Promise`\<[`CryptoCompareMarkets`](../../../../types/type-aliases/CryptoCompareMarkets.md)\> + +Defined in: [src/services/providers/cryptoCompare.ts:227](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/cryptoCompare.ts#L227) Retrieves market data for an array of cryptocurrency symbols. @@ -97,11 +101,15 @@ Handles splitting the symbols into batches to comply with API limitations. #### Parameters -• **ids**: `string`[] +##### ids + +`string`[] An array of cryptocurrency symbols (e.g., ['BTC', 'ETH']). -• **vsCurrency**: `string` = `'BTC'` +##### vsCurrency? + +`string` = `'BTC'` The target currency symbol (default is 'BTC'). @@ -119,21 +127,19 @@ const marketData = await cryptoCompare.getMarketData(['BTC', 'ETH'], 'USD'); console.log('Market Data:', marketData); ``` -#### Defined in - -[src/services/providers/cryptoCompare.ts:226](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/cryptoCompare.ts#L226) - *** ### getInstance() -> `static` **getInstance**(): [`CryptoCompare`](CryptoCompare.md) +> `static` **getInstance**(): `CryptoCompare` + +Defined in: [src/services/providers/cryptoCompare.ts:92](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/cryptoCompare.ts#L92) Returns the singleton instance of the CryptoCompare class. #### Returns -[`CryptoCompare`](CryptoCompare.md) +`CryptoCompare` The singleton instance of CryptoCompare. @@ -142,7 +148,3 @@ The singleton instance of CryptoCompare. ```typescript const cryptoCompare = CryptoCompare.getInstance(); ``` - -#### Defined in - -[src/services/providers/cryptoCompare.ts:92](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/cryptoCompare.ts#L92) diff --git a/docs/src/services/providers/liveCoinWatch/README.md b/docs/src/services/providers/liveCoinWatch/README.md index 1aebaf3..1b09805 100644 --- a/docs/src/services/providers/liveCoinWatch/README.md +++ b/docs/src/services/providers/liveCoinWatch/README.md @@ -1,13 +1,11 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / src/services/providers/liveCoinWatch +[rates-api](../../../../modules.md) / src/services/providers/liveCoinWatch # src/services/providers/liveCoinWatch -## Index - -### Classes +## Classes - [LiveCoinWatch](classes/LiveCoinWatch.md) diff --git a/docs/src/services/providers/liveCoinWatch/classes/LiveCoinWatch.md b/docs/src/services/providers/liveCoinWatch/classes/LiveCoinWatch.md index aab64b4..a9f33cf 100644 --- a/docs/src/services/providers/liveCoinWatch/classes/LiveCoinWatch.md +++ b/docs/src/services/providers/liveCoinWatch/classes/LiveCoinWatch.md @@ -1,11 +1,13 @@ -[**rates-api v3.0.0**](../../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../../README.md) *** -[rates-api v3.0.0](../../../../../modules.md) / [src/services/providers/liveCoinWatch](../README.md) / LiveCoinWatch +[rates-api](../../../../../modules.md) / [src/services/providers/liveCoinWatch](../README.md) / LiveCoinWatch # Class: LiveCoinWatch +Defined in: [src/services/providers/liveCoinWatch.ts:28](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/liveCoinWatch.ts#L28) + Singleton class to interact with the LiveCoinWatch API. This class provides methods to retrieve cryptocurrency exchange rates from LiveCoinWatch. @@ -27,9 +29,11 @@ fetchExchangeRates(); ## Constructors -### new LiveCoinWatch() +### Constructor + +> **new LiveCoinWatch**(): `LiveCoinWatch` -> **new LiveCoinWatch**(): [`LiveCoinWatch`](LiveCoinWatch.md) +Defined in: [src/services/providers/liveCoinWatch.ts:69](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/liveCoinWatch.ts#L69) Private constructor to enforce the singleton pattern. @@ -37,21 +41,19 @@ Initializes the AxiosWrapper and the LRU cache. #### Returns -[`LiveCoinWatch`](LiveCoinWatch.md) +`LiveCoinWatch` #### Throws If an instance already exists. -#### Defined in - -[src/services/providers/liveCoinWatch.ts:69](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/liveCoinWatch.ts#L69) - ## Methods ### getExchangeRates() -> **getExchangeRates**(`ids`, `vsCurrency`): `Promise`\<[`LiveCoinWatchMarket`](../../../../types/type-aliases/LiveCoinWatchMarket.md)[]\> +> **getExchangeRates**(`ids`, `vsCurrency?`): `Promise`\<[`LiveCoinWatchMarket`](../../../../types/type-aliases/LiveCoinWatchMarket.md)[]\> + +Defined in: [src/services/providers/liveCoinWatch.ts:164](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/liveCoinWatch.ts#L164) Retrieves exchange rates for an array of cryptocurrency symbols. @@ -59,11 +61,15 @@ Handles splitting the symbols into batches to comply with API limitations. #### Parameters -• **ids**: `string`[] +##### ids + +`string`[] An array of cryptocurrency symbols (e.g., ['BTC', 'ETH']). -• **vsCurrency**: `string` = `'BTC'` +##### vsCurrency? + +`string` = `'BTC'` The target currency symbol (default is 'BTC'). @@ -81,21 +87,19 @@ const rates = await liveCoinWatch.getExchangeRates(['BTC', 'ETH'], 'USD'); console.log('Exchange Rates:', rates); ``` -#### Defined in - -[src/services/providers/liveCoinWatch.ts:164](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/liveCoinWatch.ts#L164) - *** ### getInstance() -> `static` **getInstance**(): [`LiveCoinWatch`](LiveCoinWatch.md) +> `static` **getInstance**(): `LiveCoinWatch` + +Defined in: [src/services/providers/liveCoinWatch.ts:92](https://github.com/ZelCore-io/rates-api/blob/master/src/services/providers/liveCoinWatch.ts#L92) Returns the singleton instance of the LiveCoinWatch class. #### Returns -[`LiveCoinWatch`](LiveCoinWatch.md) +`LiveCoinWatch` The singleton instance of LiveCoinWatch. @@ -104,7 +108,3 @@ The singleton instance of LiveCoinWatch. ```typescript const liveCoinWatch = LiveCoinWatch.getInstance(); ``` - -#### Defined in - -[src/services/providers/liveCoinWatch.ts:92](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/providers/liveCoinWatch.ts#L92) diff --git a/docs/src/services/zelcoreMarketsUSD/README.md b/docs/src/services/zelcoreMarketsUSD/README.md index b789833..505013b 100644 --- a/docs/src/services/zelcoreMarketsUSD/README.md +++ b/docs/src/services/zelcoreMarketsUSD/README.md @@ -1,17 +1,15 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/services/zelcoreMarketsUSD +[rates-api](../../../modules.md) / src/services/zelcoreMarketsUSD # src/services/zelcoreMarketsUSD -## Index - -### Variables +## Variables - [default](variables/default.md) -### Functions +## Functions - [getAll](functions/getAll.md) diff --git a/docs/src/services/zelcoreMarketsUSD/functions/getAll.md b/docs/src/services/zelcoreMarketsUSD/functions/getAll.md index f207b35..30e9a89 100644 --- a/docs/src/services/zelcoreMarketsUSD/functions/getAll.md +++ b/docs/src/services/zelcoreMarketsUSD/functions/getAll.md @@ -1,13 +1,15 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/zelcoreMarketsUSD](../README.md) / getAll +[rates-api](../../../../modules.md) / [src/services/zelcoreMarketsUSD](../README.md) / getAll # Function: getAll() > **getAll**(): `Promise`\<[`MarketsData`](../../../types/type-aliases/MarketsData.md)\> +Defined in: [src/services/zelcoreMarketsUSD.ts:21](https://github.com/ZelCore-io/rates-api/blob/master/src/services/zelcoreMarketsUSD.ts#L21) + Fetches market data from multiple providers and aggregates it. This function retrieves market data from CryptoCompare, CoinGecko, and LiveCoinWatch, @@ -27,7 +29,3 @@ The aggregated market data. const marketData = await getAll(); console.log(marketData); ``` - -## Defined in - -[src/services/zelcoreMarketsUSD.ts:21](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/zelcoreMarketsUSD.ts#L21) diff --git a/docs/src/services/zelcoreMarketsUSD/variables/default.md b/docs/src/services/zelcoreMarketsUSD/variables/default.md index ed8ccd0..b5f5740 100644 --- a/docs/src/services/zelcoreMarketsUSD/variables/default.md +++ b/docs/src/services/zelcoreMarketsUSD/variables/default.md @@ -1,16 +1,18 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/zelcoreMarketsUSD](../README.md) / default +[rates-api](../../../../modules.md) / [src/services/zelcoreMarketsUSD](../README.md) / default # Variable: default > **default**: `object` -## Type declaration +Defined in: [src/services/zelcoreMarketsUSD.ts:138](https://github.com/ZelCore-io/rates-api/blob/master/src/services/zelcoreMarketsUSD.ts#L138) -### getAll() +## Type Declaration + +### getAll > **getAll**: () => `Promise`\<[`MarketsData`](../../../types/type-aliases/MarketsData.md)\> @@ -33,7 +35,3 @@ The aggregated market data. const marketData = await getAll(); console.log(marketData); ``` - -## Defined in - -[src/services/zelcoreMarketsUSD.ts:137](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/zelcoreMarketsUSD.ts#L137) diff --git a/docs/src/services/zelcoreRates/README.md b/docs/src/services/zelcoreRates/README.md index 6533591..7a4815f 100644 --- a/docs/src/services/zelcoreRates/README.md +++ b/docs/src/services/zelcoreRates/README.md @@ -1,17 +1,15 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/services/zelcoreRates +[rates-api](../../../modules.md) / src/services/zelcoreRates # src/services/zelcoreRates -## Index - -### Variables +## Variables - [default](variables/default.md) -### Functions +## Functions - [getAll](functions/getAll.md) diff --git a/docs/src/services/zelcoreRates/functions/getAll.md b/docs/src/services/zelcoreRates/functions/getAll.md index ff1d835..73f61fd 100644 --- a/docs/src/services/zelcoreRates/functions/getAll.md +++ b/docs/src/services/zelcoreRates/functions/getAll.md @@ -1,13 +1,15 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/zelcoreRates](../README.md) / getAll +[rates-api](../../../../modules.md) / [src/services/zelcoreRates](../README.md) / getAll # Function: getAll() > **getAll**(): `Promise`\<[`RatesData`](../../../types/type-aliases/RatesData.md)\> +Defined in: [src/services/zelcoreRates.ts:34](https://github.com/ZelCore-io/rates-api/blob/master/src/services/zelcoreRates.ts#L34) + Fetches exchange rates and price data from various providers and aggregates them. This function retrieves fiat rates from BitPay and cryptocurrency prices from CoinGecko, @@ -34,7 +36,3 @@ async function fetchRates() { fetchRates(); ``` - -## Defined in - -[src/services/zelcoreRates.ts:34](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/zelcoreRates.ts#L34) diff --git a/docs/src/services/zelcoreRates/variables/default.md b/docs/src/services/zelcoreRates/variables/default.md index d9727e3..ec7a6e2 100644 --- a/docs/src/services/zelcoreRates/variables/default.md +++ b/docs/src/services/zelcoreRates/variables/default.md @@ -1,16 +1,18 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/zelcoreRates](../README.md) / default +[rates-api](../../../../modules.md) / [src/services/zelcoreRates](../README.md) / default # Variable: default > **default**: `object` -## Type declaration +Defined in: [src/services/zelcoreRates.ts:153](https://github.com/ZelCore-io/rates-api/blob/master/src/services/zelcoreRates.ts#L153) -### getAll() +## Type Declaration + +### getAll > **getAll**: () => `Promise`\<[`RatesData`](../../../types/type-aliases/RatesData.md)\> @@ -40,7 +42,3 @@ async function fetchRates() { fetchRates(); ``` - -## Defined in - -[src/services/zelcoreRates.ts:151](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/zelcoreRates.ts#L151) diff --git a/docs/src/services/zelcoreRatesV2/README.md b/docs/src/services/zelcoreRatesV2/README.md index 565f0d4..1e19fa6 100644 --- a/docs/src/services/zelcoreRatesV2/README.md +++ b/docs/src/services/zelcoreRatesV2/README.md @@ -1,17 +1,15 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / src/services/zelcoreRatesV2 +[rates-api](../../../modules.md) / src/services/zelcoreRatesV2 # src/services/zelcoreRatesV2 -## Index - -### Variables +## Variables - [default](variables/default.md) -### Functions +## Functions - [getAll](functions/getAll.md) diff --git a/docs/src/services/zelcoreRatesV2/functions/getAll.md b/docs/src/services/zelcoreRatesV2/functions/getAll.md index 6cb03da..099f0ad 100644 --- a/docs/src/services/zelcoreRatesV2/functions/getAll.md +++ b/docs/src/services/zelcoreRatesV2/functions/getAll.md @@ -1,13 +1,15 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/zelcoreRatesV2](../README.md) / getAll +[rates-api](../../../../modules.md) / [src/services/zelcoreRatesV2](../README.md) / getAll # Function: getAll() > **getAll**(): `Promise`\<[`PricesResponse`](../../../types/type-aliases/PricesResponse.md)\> +Defined in: [src/services/zelcoreRatesV2.ts:47](https://github.com/ZelCore-io/rates-api/blob/master/src/services/zelcoreRatesV2.ts#L47) + Fetches and aggregates cryptocurrency prices and fiat rates from multiple providers. This function retrieves fiat rates from BitPay and cryptocurrency prices from CoinGecko, @@ -34,7 +36,3 @@ async function fetchPrices() { fetchPrices(); ``` - -## Defined in - -[src/services/zelcoreRatesV2.ts:28](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/zelcoreRatesV2.ts#L28) diff --git a/docs/src/services/zelcoreRatesV2/variables/default.md b/docs/src/services/zelcoreRatesV2/variables/default.md index 9c9a173..49753de 100644 --- a/docs/src/services/zelcoreRatesV2/variables/default.md +++ b/docs/src/services/zelcoreRatesV2/variables/default.md @@ -1,16 +1,18 @@ -[**rates-api v3.0.0**](../../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../../README.md) *** -[rates-api v3.0.0](../../../../modules.md) / [src/services/zelcoreRatesV2](../README.md) / default +[rates-api](../../../../modules.md) / [src/services/zelcoreRatesV2](../README.md) / default # Variable: default > **default**: `object` -## Type declaration +Defined in: [src/services/zelcoreRatesV2.ts:193](https://github.com/ZelCore-io/rates-api/blob/master/src/services/zelcoreRatesV2.ts#L193) -### getAll() +## Type Declaration + +### getAll > **getAll**: () => `Promise`\<[`PricesResponse`](../../../types/type-aliases/PricesResponse.md)\> @@ -40,7 +42,3 @@ async function fetchPrices() { fetchPrices(); ``` - -## Defined in - -[src/services/zelcoreRatesV2.ts:142](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/services/zelcoreRatesV2.ts#L142) diff --git a/docs/src/types/README.md b/docs/src/types/README.md index ab08f68..f64ef33 100644 --- a/docs/src/types/README.md +++ b/docs/src/types/README.md @@ -1,21 +1,21 @@ -[**rates-api v3.0.0**](../../README.md) • **Docs** +[**rates-api v3.0.0**](../../README.md) *** -[rates-api v3.0.0](../../modules.md) / src/types +[rates-api](../../modules.md) / src/types # src/types -## Index - -### Interfaces +## Interfaces - [ICurrencyData](interfaces/ICurrencyData.md) - [ICurrencyRate](interfaces/ICurrencyRate.md) - [IErrorObject](interfaces/IErrorObject.md) -### Type Aliases +## Type Aliases +- [BinanceTicker](type-aliases/BinanceTicker.md) +- [BinanceTokenisedAsset](type-aliases/BinanceTokenisedAsset.md) - [CodeRates](type-aliases/CodeRates.md) - [CoinGeckoPrice](type-aliases/CoinGeckoPrice.md) - [CoinGeckoToken](type-aliases/CoinGeckoToken.md) diff --git a/docs/src/types/interfaces/ICurrencyData.md b/docs/src/types/interfaces/ICurrencyData.md index 7b7ab2f..5e9a2e7 100644 --- a/docs/src/types/interfaces/ICurrencyData.md +++ b/docs/src/types/interfaces/ICurrencyData.md @@ -1,30 +1,28 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / ICurrencyData +[rates-api](../../../modules.md) / [src/types](../README.md) / ICurrencyData # Interface: ICurrencyData +Defined in: [src/types.ts:81](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L81) + ## Properties ### change > **change**: `number` -#### Defined in - -[src/types.ts:84](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L84) +Defined in: [src/types.ts:84](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L84) *** ### change7d? -> `optional` **change7d**: `number` +> `optional` **change7d?**: `number` -#### Defined in - -[src/types.ts:88](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L88) +Defined in: [src/types.ts:88](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L88) *** @@ -32,19 +30,15 @@ > **market**: `number` -#### Defined in - -[src/types.ts:85](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L85) +Defined in: [src/types.ts:85](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L85) *** ### rank? -> `optional` **rank**: `number` +> `optional` **rank?**: `number` -#### Defined in - -[src/types.ts:86](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L86) +Defined in: [src/types.ts:86](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L86) *** @@ -52,19 +46,15 @@ > **supply**: `number` -#### Defined in - -[src/types.ts:82](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L82) +Defined in: [src/types.ts:82](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L82) *** ### total\_supply? -> `optional` **total\_supply**: `number` +> `optional` **total\_supply?**: `number` -#### Defined in - -[src/types.ts:87](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L87) +Defined in: [src/types.ts:87](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L87) *** @@ -72,6 +62,4 @@ > **volume**: `number` -#### Defined in - -[src/types.ts:83](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L83) +Defined in: [src/types.ts:83](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L83) diff --git a/docs/src/types/interfaces/ICurrencyRate.md b/docs/src/types/interfaces/ICurrencyRate.md index 896019a..4521bec 100644 --- a/docs/src/types/interfaces/ICurrencyRate.md +++ b/docs/src/types/interfaces/ICurrencyRate.md @@ -1,20 +1,20 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / ICurrencyRate +[rates-api](../../../modules.md) / [src/types](../README.md) / ICurrencyRate # Interface: ICurrencyRate +Defined in: [src/types.ts:67](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L67) + ## Properties ### code > **code**: `string` -#### Defined in - -[src/types.ts:68](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L68) +Defined in: [src/types.ts:68](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L68) *** @@ -22,9 +22,7 @@ > **name**: `string` -#### Defined in - -[src/types.ts:69](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L69) +Defined in: [src/types.ts:69](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L69) *** @@ -32,6 +30,4 @@ > **rate**: `number` -#### Defined in - -[src/types.ts:70](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L70) +Defined in: [src/types.ts:70](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L70) diff --git a/docs/src/types/interfaces/IErrorObject.md b/docs/src/types/interfaces/IErrorObject.md index b4137ea..cc19e37 100644 --- a/docs/src/types/interfaces/IErrorObject.md +++ b/docs/src/types/interfaces/IErrorObject.md @@ -1,21 +1,21 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / IErrorObject +[rates-api](../../../modules.md) / [src/types](../README.md) / IErrorObject # Interface: IErrorObject +Defined in: [src/types.ts:75](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L75) + ## Properties ### errors > **errors**: `object` -#### Index Signature - - \[`key`: `string`\]: `any` +Defined in: [src/types.ts:76](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L76) -#### Defined in +#### Index Signature -[src/types.ts:76](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L76) +\[`key`: `string`\]: `any` diff --git a/docs/src/types/type-aliases/BinanceTicker.md b/docs/src/types/type-aliases/BinanceTicker.md new file mode 100644 index 0000000..ad54096 --- /dev/null +++ b/docs/src/types/type-aliases/BinanceTicker.md @@ -0,0 +1,43 @@ +[**rates-api v3.0.0**](../../../README.md) + +*** + +[rates-api](../../../modules.md) / [src/types](../README.md) / BinanceTicker + +# Type Alias: BinanceTicker + +> **BinanceTicker** = `object` + +Defined in: [src/types.ts:137](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L137) + +## Properties + +### lastPrice + +> **lastPrice**: `string` + +Defined in: [src/types.ts:139](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L139) + +*** + +### priceChangePercent + +> **priceChangePercent**: `string` + +Defined in: [src/types.ts:140](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L140) + +*** + +### quoteVolume + +> **quoteVolume**: `string` + +Defined in: [src/types.ts:141](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L141) + +*** + +### symbol + +> **symbol**: `string` + +Defined in: [src/types.ts:138](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L138) diff --git a/docs/src/types/type-aliases/BinanceTokenisedAsset.md b/docs/src/types/type-aliases/BinanceTokenisedAsset.md new file mode 100644 index 0000000..fedca1c --- /dev/null +++ b/docs/src/types/type-aliases/BinanceTokenisedAsset.md @@ -0,0 +1,59 @@ +[**rates-api v3.0.0**](../../../README.md) + +*** + +[rates-api](../../../modules.md) / [src/types](../README.md) / BinanceTokenisedAsset + +# Type Alias: BinanceTokenisedAsset + +> **BinanceTokenisedAsset** = `object` + +Defined in: [src/types.ts:129](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L129) + +## Properties + +### assetCode + +> **assetCode**: `string` + +Defined in: [src/types.ts:130](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L130) + +*** + +### assetName + +> **assetName**: `string` + +Defined in: [src/types.ts:131](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L131) + +*** + +### caList? + +> `optional` **caList?**: `object`[] + +Defined in: [src/types.ts:134](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L134) + +#### ca + +> **ca**: `string` + +#### network + +> **network**: `string` + +*** + +### logo? + +> `optional` **logo?**: `string` + +Defined in: [src/types.ts:133](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L133) + +*** + +### uq? + +> `optional` **uq?**: `string` + +Defined in: [src/types.ts:132](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L132) diff --git a/docs/src/types/type-aliases/CodeRates.md b/docs/src/types/type-aliases/CodeRates.md index d14e504..b6e4bf4 100644 --- a/docs/src/types/type-aliases/CodeRates.md +++ b/docs/src/types/type-aliases/CodeRates.md @@ -1,17 +1,15 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / CodeRates +[rates-api](../../../modules.md) / [src/types](../README.md) / CodeRates # Type Alias: CodeRates -> **CodeRates**: `object` +> **CodeRates** = `object` -## Index Signature - - \[`code`: `string`\]: `number` \| `null` +Defined in: [src/types.ts:73](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L73) -## Defined in +## Index Signature -[src/types.ts:73](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L73) +\[`code`: `string`\]: `number` \| `null` diff --git a/docs/src/types/type-aliases/CoinGeckoPrice.md b/docs/src/types/type-aliases/CoinGeckoPrice.md index f20e5e5..b9616f9 100644 --- a/docs/src/types/type-aliases/CoinGeckoPrice.md +++ b/docs/src/types/type-aliases/CoinGeckoPrice.md @@ -1,123 +1,227 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / CoinGeckoPrice +[rates-api](../../../modules.md) / [src/types](../README.md) / CoinGeckoPrice # Type Alias: CoinGeckoPrice -> **CoinGeckoPrice**: `object` +> **CoinGeckoPrice** = `object` -## Type declaration +Defined in: [src/types.ts:95](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L95) + +## Properties ### ath > **ath**: `number` +Defined in: [src/types.ts:114](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L114) + +*** + ### ath\_change\_percentage > **ath\_change\_percentage**: `number` +Defined in: [src/types.ts:115](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L115) + +*** + ### ath\_date > **ath\_date**: `string` +Defined in: [src/types.ts:116](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L116) + +*** + ### atl > **atl**: `number` +Defined in: [src/types.ts:117](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L117) + +*** + ### atl\_change\_percentage > **atl\_change\_percentage**: `number` +Defined in: [src/types.ts:118](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L118) + +*** + ### atl\_date > **atl\_date**: `string` +Defined in: [src/types.ts:119](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L119) + +*** + ### circulating\_supply > **circulating\_supply**: `number` +Defined in: [src/types.ts:111](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L111) + +*** + ### current\_price > **current\_price**: `number` +Defined in: [src/types.ts:100](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L100) + +*** + ### fully\_diluted\_valuation > **fully\_diluted\_valuation**: `number` +Defined in: [src/types.ts:103](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L103) + +*** + ### high\_24h > **high\_24h**: `number` +Defined in: [src/types.ts:105](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L105) + +*** + ### id > **id**: `string` +Defined in: [src/types.ts:96](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L96) + +*** + ### image > **image**: `string` +Defined in: [src/types.ts:99](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L99) + +*** + ### last\_updated > **last\_updated**: `string` +Defined in: [src/types.ts:125](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L125) + +*** + ### low\_24h > **low\_24h**: `number` +Defined in: [src/types.ts:106](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L106) + +*** + ### market\_cap > **market\_cap**: `number` +Defined in: [src/types.ts:101](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L101) + +*** + ### market\_cap\_change\_24h > **market\_cap\_change\_24h**: `number` +Defined in: [src/types.ts:109](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L109) + +*** + ### market\_cap\_change\_percentage\_24h > **market\_cap\_change\_percentage\_24h**: `number` +Defined in: [src/types.ts:110](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L110) + +*** + ### market\_cap\_rank > **market\_cap\_rank**: `number` +Defined in: [src/types.ts:102](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L102) + +*** + ### max\_supply > **max\_supply**: `number` +Defined in: [src/types.ts:113](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L113) + +*** + ### name > **name**: `string` +Defined in: [src/types.ts:98](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L98) + +*** + ### price\_change\_24h > **price\_change\_24h**: `number` +Defined in: [src/types.ts:107](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L107) + +*** + ### price\_change\_percentage\_24h > **price\_change\_percentage\_24h**: `number` +Defined in: [src/types.ts:108](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L108) + +*** + ### price\_change\_percentage\_7d\_in\_currency > **price\_change\_percentage\_7d\_in\_currency**: `number` +Defined in: [src/types.ts:126](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L126) + +*** + ### roi -> **roi**: `null` \| `object` +> **roi**: `null` \| \{ `currency`: `string`; `percentage`: `number`; `times`: `number`; \} + +Defined in: [src/types.ts:120](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L120) + +*** ### symbol > **symbol**: `string` +Defined in: [src/types.ts:97](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L97) + +*** + ### total\_supply > **total\_supply**: `number` +Defined in: [src/types.ts:112](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L112) + +*** + ### total\_volume > **total\_volume**: `number` -## Defined in - -[src/types.ts:95](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L95) +Defined in: [src/types.ts:104](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L104) diff --git a/docs/src/types/type-aliases/CoinGeckoToken.md b/docs/src/types/type-aliases/CoinGeckoToken.md index 1d419c8..5d959c6 100644 --- a/docs/src/types/type-aliases/CoinGeckoToken.md +++ b/docs/src/types/type-aliases/CoinGeckoToken.md @@ -1,31 +1,43 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / CoinGeckoToken +[rates-api](../../../modules.md) / [src/types](../README.md) / CoinGeckoToken # Type Alias: CoinGeckoToken -> **CoinGeckoToken**: `object` +> **CoinGeckoToken** = `object` -## Type declaration +Defined in: [src/types.ts:58](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L58) + +## Properties ### id > **id**: `string` +Defined in: [src/types.ts:59](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L59) + +*** + ### name > **name**: `string` +Defined in: [src/types.ts:61](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L61) + +*** + ### platforms > **platforms**: `Record`\<`string`, `string`\> +Defined in: [src/types.ts:62](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L62) + +*** + ### symbol > **symbol**: `string` -## Defined in - -[src/types.ts:58](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L58) +Defined in: [src/types.ts:60](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L60) diff --git a/docs/src/types/type-aliases/CoinInfo.md b/docs/src/types/type-aliases/CoinInfo.md index 01525f4..0e663d2 100644 --- a/docs/src/types/type-aliases/CoinInfo.md +++ b/docs/src/types/type-aliases/CoinInfo.md @@ -1,107 +1,195 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / CoinInfo +[rates-api](../../../modules.md) / [src/types](../README.md) / CoinInfo # Type Alias: CoinInfo -> **CoinInfo**: `object` +> **CoinInfo** = `object` -## Type declaration +Defined in: [src/types.ts:32](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L32) + +## Properties ### auditInfos > **auditInfos**: `string`[] +Defined in: [src/types.ts:54](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L54) + +*** + ### bitcointalk > **bitcointalk**: `string` +Defined in: [src/types.ts:41](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L41) + +*** + ### circulating\_supply > **circulating\_supply**: `number` \| `null` +Defined in: [src/types.ts:35](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L35) + +*** + ### coingeckoID > **coingeckoID**: `string` +Defined in: [src/types.ts:53](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L53) + +*** + ### coinMarketCapID > **coinMarketCapID**: `string` +Defined in: [src/types.ts:52](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L52) + +*** + ### cryptoCompareID > **cryptoCompareID**: `string` +Defined in: [src/types.ts:51](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L51) + +*** + ### description > **description**: `string` +Defined in: [src/types.ts:33](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L33) + +*** + ### discord > **discord**: `string` +Defined in: [src/types.ts:39](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L39) + +*** + ### explorers > **explorers**: `string`[] +Defined in: [src/types.ts:37](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L37) + +*** + ### facebook > **facebook**: `string` +Defined in: [src/types.ts:42](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L42) + +*** + ### instagram > **instagram**: `string` +Defined in: [src/types.ts:47](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L47) + +*** + ### linkedin > **linkedin**: `string` +Defined in: [src/types.ts:50](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L50) + +*** + ### medium > **medium**: `string` +Defined in: [src/types.ts:38](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L38) + +*** + ### reddit > **reddit**: `string` +Defined in: [src/types.ts:44](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L44) + +*** + ### repository > **repository**: `string` +Defined in: [src/types.ts:45](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L45) + +*** + ### telegram > **telegram**: `string` +Defined in: [src/types.ts:40](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L40) + +*** + ### tiktok > **tiktok**: `string` +Defined in: [src/types.ts:48](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L48) + +*** + ### total\_supply > **total\_supply**: `number` \| `null` +Defined in: [src/types.ts:34](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L34) + +*** + ### twitch > **twitch**: `string` +Defined in: [src/types.ts:49](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L49) + +*** + ### twitter > **twitter**: `string` +Defined in: [src/types.ts:43](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L43) + +*** + ### websites > **websites**: `string`[] +Defined in: [src/types.ts:36](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L36) + +*** + ### whitepaper > **whitepaper**: `string`[] +Defined in: [src/types.ts:55](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L55) + +*** + ### youtube > **youtube**: `string` -## Defined in - -[src/types.ts:32](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L32) +Defined in: [src/types.ts:46](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L46) diff --git a/docs/src/types/type-aliases/ContractWithType.md b/docs/src/types/type-aliases/ContractWithType.md index 8683a4f..6df2b62 100644 --- a/docs/src/types/type-aliases/ContractWithType.md +++ b/docs/src/types/type-aliases/ContractWithType.md @@ -1,23 +1,27 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / ContractWithType +[rates-api](../../../modules.md) / [src/types](../README.md) / ContractWithType # Type Alias: ContractWithType -> **ContractWithType**: `object` +> **ContractWithType** = `object` -## Type declaration +Defined in: [src/types.ts:27](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L27) + +## Properties ### address > **address**: `string` +Defined in: [src/types.ts:28](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L28) + +*** + ### type > **type**: `string` -## Defined in - -[src/types.ts:27](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L27) +Defined in: [src/types.ts:29](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L29) diff --git a/docs/src/types/type-aliases/CryptoCompareMarkets.md b/docs/src/types/type-aliases/CryptoCompareMarkets.md index 64e3682..b821206 100644 --- a/docs/src/types/type-aliases/CryptoCompareMarkets.md +++ b/docs/src/types/type-aliases/CryptoCompareMarkets.md @@ -1,17 +1,15 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / CryptoCompareMarkets +[rates-api](../../../modules.md) / [src/types](../README.md) / CryptoCompareMarkets # Type Alias: CryptoCompareMarkets -> **CryptoCompareMarkets**: `object` +> **CryptoCompareMarkets** = `object` -## Index Signature - - \[`key`: `string`\]: `object` +Defined in: [src/types.ts:149](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L149) -## Defined in +## Index Signature -[src/types.ts:134](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L134) +\[`key`: `string`\]: `object` diff --git a/docs/src/types/type-aliases/CryptoComparePrice.md b/docs/src/types/type-aliases/CryptoComparePrice.md index c60d105..02cda1c 100644 --- a/docs/src/types/type-aliases/CryptoComparePrice.md +++ b/docs/src/types/type-aliases/CryptoComparePrice.md @@ -1,17 +1,15 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / CryptoComparePrice +[rates-api](../../../modules.md) / [src/types](../README.md) / CryptoComparePrice # Type Alias: CryptoComparePrice -> **CryptoComparePrice**: `object` +> **CryptoComparePrice** = `object` -## Index Signature - - \[`key`: `string`\]: `object` +Defined in: [src/types.ts:144](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L144) -## Defined in +## Index Signature -[src/types.ts:129](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L129) +\[`key`: `string`\]: `object` diff --git a/docs/src/types/type-aliases/CryptoPrice.md b/docs/src/types/type-aliases/CryptoPrice.md index 9726dbb..4e19ef0 100644 --- a/docs/src/types/type-aliases/CryptoPrice.md +++ b/docs/src/types/type-aliases/CryptoPrice.md @@ -1,55 +1,91 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / CryptoPrice +[rates-api](../../../modules.md) / [src/types](../README.md) / CryptoPrice # Type Alias: CryptoPrice -> **CryptoPrice**: `object` +> **CryptoPrice** = `object` -## Type declaration +Defined in: [src/types.ts:1](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L1) + +## Properties ### change24h > **change24h**: `number` +Defined in: [src/types.ts:7](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L7) + +*** + ### change7d? -> `optional` **change7d**: `number` +> `optional` **change7d?**: `number` + +Defined in: [src/types.ts:11](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L11) + +*** ### id > **id**: `string` +Defined in: [src/types.ts:2](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L2) + +*** + ### market > **market**: `number` +Defined in: [src/types.ts:8](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L8) + +*** + ### provider > **provider**: `string` +Defined in: [src/types.ts:3](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L3) + +*** + ### rank? -> `optional` **rank**: `number` +> `optional` **rank?**: `number` + +Defined in: [src/types.ts:9](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L9) + +*** ### rates > **rates**: `Record`\<`string`, `number`\> +Defined in: [src/types.ts:4](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L4) + +*** + ### supply > **supply**: `number` +Defined in: [src/types.ts:5](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L5) + +*** + ### total\_supply? -> `optional` **total\_supply**: `number` +> `optional` **total\_supply?**: `number` + +Defined in: [src/types.ts:10](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L10) + +*** ### volume > **volume**: `number` -## Defined in - -[src/types.ts:1](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L1) +Defined in: [src/types.ts:6](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L6) diff --git a/docs/src/types/type-aliases/CurrencyMap.md b/docs/src/types/type-aliases/CurrencyMap.md index c34d182..64a431a 100644 --- a/docs/src/types/type-aliases/CurrencyMap.md +++ b/docs/src/types/type-aliases/CurrencyMap.md @@ -1,17 +1,15 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / CurrencyMap +[rates-api](../../../modules.md) / [src/types](../README.md) / CurrencyMap # Type Alias: CurrencyMap -> **CurrencyMap**: `object` +> **CurrencyMap** = `object` -## Index Signature - - \[`code`: `string`\]: [`ICurrencyData`](../interfaces/ICurrencyData.md) +Defined in: [src/types.ts:91](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L91) -## Defined in +## Index Signature -[src/types.ts:91](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L91) +\[`code`: `string`\]: [`ICurrencyData`](../interfaces/ICurrencyData.md) diff --git a/docs/src/types/type-aliases/FiatPrice.md b/docs/src/types/type-aliases/FiatPrice.md index 488363f..4d406c6 100644 --- a/docs/src/types/type-aliases/FiatPrice.md +++ b/docs/src/types/type-aliases/FiatPrice.md @@ -1,31 +1,43 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / FiatPrice +[rates-api](../../../modules.md) / [src/types](../README.md) / FiatPrice # Type Alias: FiatPrice -> **FiatPrice**: `object` +> **FiatPrice** = `object` -## Type declaration +Defined in: [src/types.ts:14](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L14) + +## Properties ### code > **code**: `string` +Defined in: [src/types.ts:15](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L15) + +*** + ### name > **name**: `string` +Defined in: [src/types.ts:16](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L16) + +*** + ### provider? -> `optional` **provider**: `string` +> `optional` **provider?**: `string` + +Defined in: [src/types.ts:18](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L18) + +*** ### rate > **rate**: `number` -## Defined in - -[src/types.ts:14](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L14) +Defined in: [src/types.ts:17](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L17) diff --git a/docs/src/types/type-aliases/FoundContractStore.md b/docs/src/types/type-aliases/FoundContractStore.md index f263a13..439a7bb 100644 --- a/docs/src/types/type-aliases/FoundContractStore.md +++ b/docs/src/types/type-aliases/FoundContractStore.md @@ -1,13 +1,11 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / FoundContractStore +[rates-api](../../../modules.md) / [src/types](../README.md) / FoundContractStore # Type Alias: FoundContractStore -> **FoundContractStore**: `Record`\<`string`, `object`\> +> **FoundContractStore** = `Record`\<`string`, \{ `cg`: [`CoinGeckoToken`](CoinGeckoToken.md); `count`: `number`; `zel`: [`ContractWithType`](ContractWithType.md); \}\> -## Defined in - -[src/types.ts:65](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L65) +Defined in: [src/types.ts:65](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L65) diff --git a/docs/src/types/type-aliases/LiveCoinWatchMarket.md b/docs/src/types/type-aliases/LiveCoinWatchMarket.md index 6e4a101..6aa34b5 100644 --- a/docs/src/types/type-aliases/LiveCoinWatchMarket.md +++ b/docs/src/types/type-aliases/LiveCoinWatchMarket.md @@ -1,191 +1,275 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / LiveCoinWatchMarket +[rates-api](../../../modules.md) / [src/types](../README.md) / LiveCoinWatchMarket # Type Alias: LiveCoinWatchMarket -> **LiveCoinWatchMarket**: `object` +> **LiveCoinWatchMarket** = `object` -## Type declaration +Defined in: [src/types.ts:204](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L204) + +## Properties ### age > **age**: `number` +Defined in: [src/types.ts:207](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L207) + +*** + ### allTimeHighUSD > **allTimeHighUSD**: `number` +Defined in: [src/types.ts:217](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L217) + +*** + ### cap > **cap**: `number` \| `null` +Defined in: [src/types.ts:242](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L242) + +*** + ### categories > **categories**: `string`[] +Defined in: [src/types.ts:216](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L216) + +*** + ### circulatingSupply > **circulatingSupply**: `number` \| `null` +Defined in: [src/types.ts:218](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L218) + +*** + ### code > **code**: `string` +Defined in: [src/types.ts:239](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L239) + +*** + ### color > **color**: `string` +Defined in: [src/types.ts:208](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L208) + +*** + ### delta > **delta**: `object` -### delta.day +Defined in: [src/types.ts:243](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L243) + +#### day > **day**: `number` \| `null` -### delta.hour +#### hour > **hour**: `number` \| `null` -### delta.month +#### month > **month**: `number` \| `null` -### delta.quarter +#### quarter > **quarter**: `number` \| `null` -### delta.week +#### week > **week**: `number` \| `null` -### delta.year +#### year > **year**: `number` \| `null` +*** + ### exchanges > **exchanges**: `number` +Defined in: [src/types.ts:213](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L213) + +*** + ### links > **links**: `object` -### links.discord +Defined in: [src/types.ts:221](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L221) + +#### discord > **discord**: `string` \| `null` -### links.instagram +#### instagram > **instagram**: `string` \| `null` -### links.linkedin +#### linkedin > **linkedin**: `string` \| `null` -### links.medium +#### medium > **medium**: `string` \| `null` -### links.naver +#### naver > **naver**: `string` \| `null` -### links.reddit +#### reddit > **reddit**: `string` \| `null` -### links.soundcloud +#### soundcloud > **soundcloud**: `string` \| `null` -### links.spotify +#### spotify > **spotify**: `string` \| `null` -### links.telegram +#### telegram > **telegram**: `string` \| `null` -### links.tiktok +#### tiktok > **tiktok**: `string` \| `null` -### links.twitch +#### twitch > **twitch**: `string` \| `null` -### links.twitter +#### twitter > **twitter**: `string` \| `null` -### links.website +#### website > **website**: `string` \| `null` -### links.wechat +#### wechat > **wechat**: `string` \| `null` -### links.whitepaper +#### whitepaper > **whitepaper**: `string` \| `null` -### links.youtube +#### youtube > **youtube**: `string` \| `null` +*** + ### markets > **markets**: `number` +Defined in: [src/types.ts:214](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L214) + +*** + ### maxSupply > **maxSupply**: `number` \| `null` +Defined in: [src/types.ts:220](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L220) + +*** + ### name > **name**: `string` +Defined in: [src/types.ts:205](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L205) + +*** + ### pairs > **pairs**: `number` +Defined in: [src/types.ts:215](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L215) + +*** + ### png32 > **png32**: `string` +Defined in: [src/types.ts:209](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L209) + +*** + ### png64 > **png64**: `string` +Defined in: [src/types.ts:210](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L210) + +*** + ### rank > **rank**: `number` +Defined in: [src/types.ts:206](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L206) + +*** + ### rate > **rate**: `number` \| `null` +Defined in: [src/types.ts:240](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L240) + +*** + ### totalSupply > **totalSupply**: `number` +Defined in: [src/types.ts:219](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L219) + +*** + ### volume > **volume**: `number` \| `null` +Defined in: [src/types.ts:241](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L241) + +*** + ### webp32 > **webp32**: `string` +Defined in: [src/types.ts:211](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L211) + +*** + ### webp64 > **webp64**: `string` -## Defined in - -[src/types.ts:189](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L189) +Defined in: [src/types.ts:212](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L212) diff --git a/docs/src/types/type-aliases/MarketsData.md b/docs/src/types/type-aliases/MarketsData.md index 3f8e707..bbe302b 100644 --- a/docs/src/types/type-aliases/MarketsData.md +++ b/docs/src/types/type-aliases/MarketsData.md @@ -1,13 +1,11 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / MarketsData +[rates-api](../../../modules.md) / [src/types](../README.md) / MarketsData # Type Alias: MarketsData -> **MarketsData**: [[`CurrencyMap`](CurrencyMap.md), [`IErrorObject`](../interfaces/IErrorObject.md)] +> **MarketsData** = \[[`CurrencyMap`](CurrencyMap.md), [`IErrorObject`](../interfaces/IErrorObject.md)\] -## Defined in - -[src/types.ts:93](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L93) +Defined in: [src/types.ts:93](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L93) diff --git a/docs/src/types/type-aliases/PricesResponse.md b/docs/src/types/type-aliases/PricesResponse.md index a9835b2..95bed5a 100644 --- a/docs/src/types/type-aliases/PricesResponse.md +++ b/docs/src/types/type-aliases/PricesResponse.md @@ -1,27 +1,35 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / PricesResponse +[rates-api](../../../modules.md) / [src/types](../README.md) / PricesResponse # Type Alias: PricesResponse -> **PricesResponse**: `object` +> **PricesResponse** = `object` -## Type declaration +Defined in: [src/types.ts:21](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L21) + +## Properties ### crypto > **crypto**: [`CryptoPrice`](CryptoPrice.md)[] +Defined in: [src/types.ts:22](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L22) + +*** + ### errors? -> `optional` **errors**: `Record`\<`string`, `any`\> +> `optional` **errors?**: `Record`\<`string`, `any`\> + +Defined in: [src/types.ts:24](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L24) + +*** ### fiat > **fiat**: [`FiatPrice`](FiatPrice.md)[] -## Defined in - -[src/types.ts:21](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L21) +Defined in: [src/types.ts:23](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L23) diff --git a/docs/src/types/type-aliases/RatesData.md b/docs/src/types/type-aliases/RatesData.md index 3cdde34..b170dff 100644 --- a/docs/src/types/type-aliases/RatesData.md +++ b/docs/src/types/type-aliases/RatesData.md @@ -1,13 +1,11 @@ -[**rates-api v3.0.0**](../../../README.md) • **Docs** +[**rates-api v3.0.0**](../../../README.md) *** -[rates-api v3.0.0](../../../modules.md) / [src/types](../README.md) / RatesData +[rates-api](../../../modules.md) / [src/types](../README.md) / RatesData # Type Alias: RatesData -> **RatesData**: [[`ICurrencyRate`](../interfaces/ICurrencyRate.md)[], [`CodeRates`](CodeRates.md), [`IErrorObject`](../interfaces/IErrorObject.md)] +> **RatesData** = \[[`ICurrencyRate`](../interfaces/ICurrencyRate.md)[], [`CodeRates`](CodeRates.md), [`IErrorObject`](../interfaces/IErrorObject.md)\] -## Defined in - -[src/types.ts:79](https://github.com/ZelCore-io/rates-api/blob/6ee8192dea404fd0a0f6ba9b7352f3b7673523eb/src/types.ts#L79) +Defined in: [src/types.ts:79](https://github.com/ZelCore-io/rates-api/blob/master/src/types.ts#L79) diff --git a/index.ts b/index.ts index 1070b3d..b908724 100644 --- a/index.ts +++ b/index.ts @@ -16,7 +16,7 @@ const { port } = config.server; */ function startService(): void { const data = apiServices.getData(); - console.log("startService -> data", !!data.rates[0][0], !!Object.keys(data.marketsUSD[0]).length); + console.log('startService -> data', !!data.rates[0][0], !!Object.keys(data.marketsUSD[0]).length); if (data.rates[0][0] && Object.keys(data.marketsUSD[0]).length) { setTimeout(() => { server.listen(port, () => { diff --git a/jest.config.ts b/jest.config.ts index 27f1d87..c805967 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,11 +1,9 @@ -import type {Config} from 'jest'; +import type { Config } from 'jest'; -export default async (): Promise => { - return { - testEnvironment: 'node', - transform: { - '^.+.tsx?$': ['ts-jest', {}], - }, - rootDir: './tests', - }; -} +export default async (): Promise => ({ + testEnvironment: 'node', + transform: { + '^.+.tsx?$': ['ts-jest', {}], + }, + rootDir: './tests', +}); diff --git a/package.json b/package.json index 90555ac..b7e364e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "npx nodemon index.ts", "test": "jest", - "lint": "eslint ./ --fix" + "lint": "eslint . --ext .js,.ts --fix" }, "author": "Zelcore Technologies", "license": "MIT", @@ -38,14 +38,16 @@ "@types/morgan": "^1.9.9", "@types/swagger-ui-express": "^4.1.6", "@types/yamljs": "^0.2.34", + "@typescript-eslint/eslint-plugin": "~7.18.0", + "@typescript-eslint/parser": "~7.18.0", "eslint": "~8.57.0", - "eslint-config-airbnb": "~19.0.4", + "eslint-config-airbnb-base": "~15.0.0", + "eslint-config-airbnb-typescript": "~18.0.0", + "eslint-import-resolver-typescript": "~3.6.3", "eslint-plugin-import": "~2.30.0", - "eslint-plugin-jsx-a11y": "~6.10.0", - "eslint-plugin-react": "~7.35.2", "jest": "^29.7.0", "ts-jest": "^29.2.5", - "typedoc": "^0.26.7", + "typedoc": "^0.28.0", "typedoc-plugin-markdown": "^4.2.7" } } diff --git a/src/lib/axios.ts b/src/lib/axios.ts index f864c18..7cf4546 100644 --- a/src/lib/axios.ts +++ b/src/lib/axios.ts @@ -1,4 +1,4 @@ -import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from "axios"; +import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; /** * A wrapper around Axios to handle automatic retries and customizable configurations. @@ -24,11 +24,13 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from "axios"; * ``` */ export class AxiosWrapper { - private axiosInstance: AxiosInstance; - private maxRetries: number; - private timeout: number; + private axiosInstance: AxiosInstance; - /** + private maxRetries: number; + + private timeout: number; + + /** * Creates an instance of AxiosWrapper. * * @param baseURL - The base URL for all requests. @@ -40,19 +42,19 @@ export class AxiosWrapper { * const apiClient = new AxiosWrapper('https://api.example.com', 5, 10000); * ``` */ - constructor(baseURL: string, maxRetries: number = 3, timeout: number = 5000) { - this.maxRetries = maxRetries; - this.timeout = timeout; + constructor(baseURL: string, maxRetries: number = 3, timeout: number = 5000) { + this.maxRetries = maxRetries; + this.timeout = timeout; - this.axiosInstance = axios.create({ - baseURL, - timeout: this.timeout, - }); + this.axiosInstance = axios.create({ + baseURL, + timeout: this.timeout, + }); - this.initializeInterceptors(); - } + this.initializeInterceptors(); + } - /** + /** * Initializes response interceptors to handle retries for failed requests. * * This method sets up an interceptor that listens for response errors @@ -60,14 +62,14 @@ export class AxiosWrapper { * * @private */ - private initializeInterceptors() { - this.axiosInstance.interceptors.response.use( - response => response, - (error: AxiosError) => this.handleRetry(error) - ); - } + private initializeInterceptors() { + this.axiosInstance.interceptors.response.use( + (response) => response, + (error: AxiosError) => this.handleRetry(error), + ); + } - /** + /** * Handles retry logic for failed requests. * * If a request fails, this method checks if the maximum number of retries @@ -77,28 +79,28 @@ export class AxiosWrapper { * @param error - The error received from a failed request. * @returns A promise that resolves with the retried request or rejects with the error. */ - private async handleRetry(error: AxiosError): Promise { - const config = error.config as AxiosRequestConfig & { __retryCount?: number }; - - // Check if retry has been initialized - if (!config.__retryCount) { - config.__retryCount = 0; - } - - // If max retries have not been met, retry the request - if (config.__retryCount < this.maxRetries) { - config.__retryCount += 1; - // Delay before retrying - return new Promise((resolve) => - setTimeout(() => resolve(this.axiosInstance(config)), 1000) - ); - } - - // If max retries exceeded, reject the promise - return Promise.reject(error); + private async handleRetry(error: AxiosError): Promise { + const config = error.config as AxiosRequestConfig & { __retryCount?: number }; + + // Check if retry has been initialized + if (!config.__retryCount) { + config.__retryCount = 0; + } + + // If max retries have not been met, retry the request + if (config.__retryCount < this.maxRetries) { + config.__retryCount += 1; + // Delay before retrying + return new Promise((resolve) => { + setTimeout(() => resolve(this.axiosInstance(config)), 1000); + }); } - /** + // If max retries exceeded, reject the promise + return Promise.reject(error); + } + + /** * Performs a GET request. * * @param url - The URL to send the GET request to. @@ -112,11 +114,11 @@ export class AxiosWrapper { * .catch(error => console.error(error)); * ``` */ - public async get(url: string, config?: AxiosRequestConfig) { - return this.axiosInstance.get(url, config); - } + public async get(url: string, config?: AxiosRequestConfig) { + return this.axiosInstance.get(url, config); + } - /** + /** * Performs a POST request. * * @param url - The URL to send the POST request to. @@ -131,11 +133,11 @@ export class AxiosWrapper { * .catch(error => console.error(error)); * ``` */ - public async post(url: string, data?: any, config?: AxiosRequestConfig) { - return this.axiosInstance.post(url, data, config); - } + public async post(url: string, data?: any, config?: AxiosRequestConfig) { + return this.axiosInstance.post(url, data, config); + } - /** + /** * Performs a PUT request. * * @param url - The URL to send the PUT request to. @@ -150,11 +152,11 @@ export class AxiosWrapper { * .catch(error => console.error(error)); * ``` */ - public async put(url: string, data?: any, config?: AxiosRequestConfig) { - return this.axiosInstance.put(url, data, config); - } + public async put(url: string, data?: any, config?: AxiosRequestConfig) { + return this.axiosInstance.put(url, data, config); + } - /** + /** * Performs a DELETE request. * * @param url - The URL to send the DELETE request to. @@ -168,9 +170,9 @@ export class AxiosWrapper { * .catch(error => console.error(error)); * ``` */ - public async delete(url: string, config?: AxiosRequestConfig) { - return this.axiosInstance.delete(url, config); - } + public async delete(url: string, config?: AxiosRequestConfig) { + return this.axiosInstance.delete(url, config); + } } export default AxiosWrapper; diff --git a/src/lib/log.ts b/src/lib/log.ts index ca201d7..81062a2 100644 --- a/src/lib/log.ts +++ b/src/lib/log.ts @@ -69,8 +69,8 @@ function writeToFile(filepath: string, args: { message?: string; stack?: string const stream = fs.createWriteStream(filepath, { flags: flag }); stream.write( `${new Date().toISOString()} ${ensureString( - typeof args === 'object' && args.message ? args.message : args - )}\n` + typeof args === 'object' && args.message ? args.message : args, + )}\n`, ); if (typeof args === 'object' && args.stack && typeof args.stack === 'string') { stream.write(`${args.stack}\n`); diff --git a/src/lib/objects.ts b/src/lib/objects.ts index 0e3d809..7b6af97 100644 --- a/src/lib/objects.ts +++ b/src/lib/objects.ts @@ -17,6 +17,9 @@ * // result: { a: 1, b: { c: 2, d: 3 }, e: 4 } * ``` */ +// mergeDeep merges INTO `target` and returns it -- mutating the argument is +// the documented contract callers rely on, not an oversight. +/* eslint-disable no-param-reassign */ export function mergeDeep(target: any, source: any) { if (Array.isArray(source)) { if (!Array.isArray(target)) { @@ -45,3 +48,37 @@ export function mergeDeep(target: any, source: any) { } return target; } +/* eslint-enable no-param-reassign */ + +/** + * Rebuilds the crypto array from `source` alone, de-duplicated by + * `${provider}-${id}`, preserving source order with last-write-wins. + * + * This deliberately does NOT merge with the previous array — hence the name. + * The positional `mergeDeep` it replaced overlaid the new array onto the old + * one index by index, which is only correct while every provider block returns + * exactly the same number of rows in the same order. When a block shrank (a + * provider outage, a delisted coin), two things went wrong: fields from the + * old entry at that index survived onto a different coin — a CryptoCompare row + * inheriting CoinGecko's `rank` and `change7d` — and entries past the new + * length lived on as stale duplicates. Because the ZelCore client re-keys on + * `${provider}-${id}` with last-write-wins, and the stale duplicates sat after + * the fresh ones, wallet users were served the STALE price on any cycle where + * a block's row count shifted. + * + * Two behaviour changes a caller should know about: + * - entries repeating the same `provider`+`id` collapse to one, keeping the + * last value at the first occurrence's position; + * - an entry the fetch no longer produces disappears immediately, rather than + * persisting from the previous cycle. + * + * @param source - The freshly fetched entries. + * @returns The de-duplicated entries, in source order. + */ +export function replaceCryptoByKey( + source: T[], +): T[] { + const byKey = new Map(); + for (const entry of source) byKey.set(`${entry.provider}-${entry.id}`, entry); + return Array.from(byKey.values()); +} diff --git a/src/lib/server.ts b/src/lib/server.ts index bc0148d..6c29ca0 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -86,7 +86,7 @@ app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); * * @remarks * The `routes` function is responsible for setting up all the necessary routes in the Express application. - * + * * @param app - The Express application instance. * * @example diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 2f22fa6..04cfab5 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -53,4 +53,4 @@ export function makeRequestStrings(elements: string[], maxLength: number): strin } }); return result; -} \ No newline at end of file +} diff --git a/src/routes.ts b/src/routes.ts index cf6327d..d22015b 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -30,7 +30,7 @@ export default (app: Application): void => { metricsPath: '/metrics', collectDefaultMetrics: true, requestDurationBuckets: [0.1, 0.5, 1, 1.5], - }) + }), ); /** diff --git a/src/services/apiServices.ts b/src/services/apiServices.ts index 14adbe3..21d2daf 100644 --- a/src/services/apiServices.ts +++ b/src/services/apiServices.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import zlib from 'zlib'; import * as log from '../lib/log'; -import { mergeDeep } from '../lib/objects'; +import { mergeDeep, replaceCryptoByKey } from '../lib/objects'; import zelcoreRates from './zelcoreRates'; import zelcoreMarketsUSD from './zelcoreMarketsUSD'; import zelcoreRatesV2 from './zelcoreRatesV2'; @@ -155,7 +155,7 @@ export function getFoundContracts(): FoundContractStore { */ export async function checkContractsV2(req: Request, res: Response): Promise { try { - const contracts = req.body.contracts; + const { contracts } = req.body; const success = checkContracts(contracts); res.json({ success }); } catch (error) { @@ -201,7 +201,7 @@ export async function dataRefresher(): Promise { dataRefresher(); }, 60 * 60 * 1000); // 1 hour } catch (error) { - log.error("Error in dataRefresher"); + log.error('Error in dataRefresher'); log.error(error); setTimeout(() => { dataRefresher(); @@ -227,14 +227,14 @@ export async function serviceRefresher(): Promise { const ratesFetched = await zelcoreRates.getAll(); const marketsUSDFetched = await zelcoreMarketsUSD.getAll(); const ratesV2Fetched = await zelcoreRatesV2.getAll(); - + if (ratesFetched && ratesFetched[0]?.length > 20 && ratesFetched[1]) { if (Object.keys(ratesFetched[1]).length > 300) { rates = mergeDeep(rates, ratesFetched); rates[2] = ratesFetched[2]; // replace errors } } - + if (marketsUSDFetched && marketsUSDFetched[0]) { log.info(Object.keys(marketsUSDFetched[0])); log.info(Object.keys(marketsUSDFetched[0]).length); @@ -244,9 +244,26 @@ export async function serviceRefresher(): Promise { } } - if (ratesV2Fetched && ratesV2Fetched.fiat.length > 20 && ratesV2Fetched.crypto.length > 300) { + // Count only real-provider rows. The floor was calibrated before bStocks + // existed, and ~56 synthetic bStock entries would otherwise mask a + // degraded provider response that the floor is meant to reject — pushing + // an under-strength payload past the guard and truncating /v2/rates. + const providerCryptoCount = ratesV2Fetched + ? ratesV2Fetched.crypto.filter((c) => !c.id.startsWith('bstock-')).length + : 0; + if (ratesV2Fetched && ratesV2Fetched.fiat.length > 20 && providerCryptoCount > 300) { ratesV2.fiat = mergeDeep(ratesV2.fiat, ratesV2Fetched.fiat); - ratesV2.crypto = mergeDeep(ratesV2.crypto, ratesV2Fetched.crypto); + // Carry forward only the rows belonging to providers that errored THIS + // cycle, then key-merge with the fresh fetch last so fresh data always + // wins. Without this, a provider whose block failed (CryptoCompare, + // LiveCoinWatch) simply vanishes from /v2/rates the moment the + // remaining providers alone still clear the >300 floor -- there is no + // positional stale tail to fall back on any more (replaceCryptoByKey + // rebuilds from `source` alone). `ratesV2.crypto` is genuinely + // undefined on the first cycle, hence the `??`. + const failedProviders = new Set(Object.keys(ratesV2Fetched.errors ?? {})); + const carried = (ratesV2.crypto ?? []).filter((c) => failedProviders.has(c.provider)); + ratesV2.crypto = replaceCryptoByKey([...carried, ...ratesV2Fetched.crypto]); ratesV2.errors = ratesV2Fetched.errors; } diff --git a/src/services/bstocks.ts b/src/services/bstocks.ts new file mode 100644 index 0000000..80c562e --- /dev/null +++ b/src/services/bstocks.ts @@ -0,0 +1,165 @@ +import config from '../../config'; +import { Binance } from './providers/binance'; +import type { CryptoPrice } from '../types'; + +// Last-known-good per bStock id, with the epoch-ms timestamp it was accepted +// at. During a CEX halt (stock splits) Binance returns the symbol PRESENT +// with lastPrice "0.00000000" rather than omitting it — measured live, 20/20 +// halted symbols came back present, 9 priced zero — so the guard below is on +// the price being finite and positive, not on the ticker being absent. We +// keep serving the previous price (display-only per the bStocks partner +// guide) until it exceeds `config.bstocksLastGoodMaxAgeMs`. +let lastGood = new Map(); + +// How many symbols were priced FRESH (not carried from last-known-good) on +// the most recent call to getBstockPrices(). Every failure inside this +// module's Binance calls is caught internally (see the Binance class docs), +// so Promise.all never rejects and a total outage would otherwise look +// identical to a healthy refresh from the outside. isBstocksDegraded() below +// exposes that distinction so a caller can surface it (e.g. errors.binance). +let freshPricedLastRun = 0; + +// The leading underscore marks this as a test-only escape hatch rather than +// part of the module's API; nothing in src/ calls it. +// eslint-disable-next-line no-underscore-dangle, @typescript-eslint/naming-convention +export function _clearLastGoodForTests(): void { + lastGood = new Map(); + freshPricedLastRun = 0; +} + +/** + * True when the most recent `getBstockPrices()` call priced nothing fresh — + * every underlying Binance call failed, returned unusable data, or served + * only prices carried over from an earlier refresh. Distinguishes "Binance is + * down and we're serving frozen prices" from a normal, healthy refresh, which + * `getBstockPrices()`'s return value alone cannot express since it never + * rejects and unconditionally re-emits `lastGood` either way. + * + * @returns Whether the bStocks pipeline is currently degraded. + */ +export function isBstocksDegraded(): boolean { + if (!config.bStocksEnabled) return false; + // A cold start during a Binance outage has nothing in lastGood yet, so + // requiring lastGood.size > 0 would report a healthy service that is + // serving no bStocks at all. Any run that priced nothing fresh while the + // feature is enabled is degraded, whether or not we have stale data. + return freshPricedLastRun === 0; +} + +/** + * The current last-known-good rows, without touching Binance. + * + * Used when a caller has given up waiting on `getBstockPrices()`. Returning + * an empty array there would drop every bStock from the response while the + * provider-level carry-forward in apiServices cannot help: bStock rows carry + * `provider: 'coingecko'` but their failure is reported under + * `errors.binance`, so nothing would carry them. + * + * @returns The last-known-good rows, stale entries already pruned. + */ +export function getLastGoodBstockPrices(): CryptoPrice[] { + const cutoff = Date.now() - config.bstocksLastGoodMaxAgeMs; + return Array.from(lastGood.values()) + .filter((e) => e.at >= cutoff) + .map((e) => e.price); +} + +/** + * Assembles the bStocks synthetic market: the intersection of Binance's + * tokenised-asset universe (already filtered to BSC-listed assets by + * `Binance.getTokenisedAssets`) with Spot symbols currently in `TRADING` + * status, quoted in USDT. + * + * BTC/USD conversion uses BTCUSDT fetched in the same 24h-ticker batch as the + * bStock symbols, so both legs come from the same venue and no cross-venue + * basis is introduced. + * + * Emitted ids are `bstock-` under `provider: "coingecko"` + * — NOT `"binance"`. The client does no prefix parsing: ZelCore's + * `store/actions.js` (`applyMarkets`) keys the market store on the literal + * string `${provider}-${id}`, and `use-fiat.js` builds the same literal from + * `coininfo.json`'s `coingeckoID` as `coingecko-${coingeckoID}`. The sibling + * `api` repo serves `coinInfo.coingeckoID = "bstock-"`, so the two + * literals only meet if the provider here is exactly `"coingecko"`. Any other + * value makes the lookup miss silently — no error, just no price. This + * id/provider pairing is a cross-repo contract; do not change it in isolation. + * + * `rank` is intentionally omitted (not zeroed) to match CryptoCompare's rows + * elsewhere in this repo, which also carry no `rank`: a literal `rank: 0` + * would sort every bStock ahead of Bitcoin in any ascending rank-ordered list. + * + * A module-level last-known-good map means a symbol that drops out of a given + * refresh (CEX halt, e.g. around a stock split) keeps being served at its + * previous price rather than disappearing from the response, bounded by + * `config.bstocksLastGoodMaxAgeMs` (see the halting comment on `lastGood` + * above) so a permanently-delisted symbol doesn't get served forever. + * + * @returns One `CryptoPrice` per tradable bStock (BSC contract + TRADING + * `USDT` Spot symbol) still within the staleness bound, including any + * carried over from a prior refresh. + */ +export async function getBstockPrices(): Promise { + if (!config.bStocksEnabled) return []; + const binance = Binance.getInstance(); + const [assets, trading] = await Promise.all([ + binance.getTokenisedAssets(), + binance.getTradingSymbols(), + ]); + const tradable = assets.filter((a) => trading.has(`${a.assetCode}USDT`)); + const symbols = tradable.map((a) => `${a.assetCode}USDT`); + const withBtc = symbols.includes('BTCUSDT') ? symbols : [...symbols, 'BTCUSDT']; + const [t24, t7d] = await Promise.all([ + binance.getTicker24h(withBtc), + binance.getTicker7d(symbols), + ]); + const t24Map = new Map(t24.map((t) => [t.symbol, t])); + const t7dMap = new Map(t7d.map((t) => [t.symbol, t])); + const btcUsd = Number(t24Map.get('BTCUSDT')?.lastPrice); + + const now = Date.now(); + let freshCount = 0; + tradable.forEach((asset) => { + const id = `bstock-${asset.assetCode.toLowerCase()}`; + const symbol = `${asset.assetCode}USDT`; + const ticker = t24Map.get(symbol); + const px = Number(ticker?.lastPrice); + if (!ticker || !Number.isFinite(px) || px <= 0 || !Number.isFinite(btcUsd) || btcUsd <= 0) { + return; // keep lastGood entry as-is + } + // `ticker` is whatever the provider served, and for a symbol that did not + // price this batch that is its backfilled last-known-good value (see + // Binance.mergeTickers) — a positive number indistinguishable here from a + // live quote. Only a live quote may count as fresh or move the staleness + // clock: counting a carried price hides a ticker-endpoint outage from + // isBstocksDegraded(), and stamping `at: now` for one pushes the bound + // below out of reach on every refresh, so a symbol that never prices + // again would be served at its frozen price forever. + const ageMs = binance.lastGoodAgeMs(symbol, '24h') ?? 0; + if (binance.pricedFresh(symbol, '24h')) freshCount += 1; + lastGood.set(id, { + at: now - ageMs, + price: { + id, + provider: 'coingecko', + rates: { btc: px / btcUsd, usd: px }, + supply: 0, + volume: Number(ticker.quoteVolume) || 0, + change24h: Number(ticker.priceChangePercent) || 0, + market: 0, + total_supply: 0, + change7d: Number(t7dMap.get(symbol)?.priceChangePercent) || 0, + }, + }); + }); + freshPricedLastRun = freshCount; + + // Bound the staleness: drop any entry that hasn't priced fresh within the + // configured window rather than serving it forever. + lastGood.forEach((entry, id) => { + if (now - entry.at > config.bstocksLastGoodMaxAgeMs) { + lastGood.delete(id); + } + }); + + return Array.from(lastGood.values()).map((entry) => entry.price); +} diff --git a/src/services/coinAggregatorIDs.ts b/src/services/coinAggregatorIDs.ts index 07dc71c..9c3c997 100644 --- a/src/services/coinAggregatorIDs.ts +++ b/src/services/coinAggregatorIDs.ts @@ -18,9 +18,9 @@ export const coinAggregatorIDs = { * Add the CryptoCompare IDs at the end of this list. */ cryptoCompare: [ - 'CONI', 'PAX', 'SPHTX', 'GVT', 'INS', 'MDA', 'QSP', 'SNGLS', 'TNB', 'WABI', 'DGD', 'TENT', 'BBO', 'ICN', 'MCO', 'EDO', 'WINGS', 'DTA', 'ADT', 'ATL', + 'CONI', 'PAX', 'SPHTX', 'GVT', 'INS', 'MDA', 'QSP', 'SNGLS', 'TNB', 'WABI', 'DGD', 'TENT', 'BBO', 'ICN', 'MCO', 'EDO', 'WINGS', 'DTA', 'ADT', 'ATL', 'BCPT', 'BTH', 'USDS', 'VIDT', 'VBK', 'UST', 'GTO', 'ONGAS', 'MIOTA', 'TOK', - 'GNT', 'AGI', 'ETHOS', 'BSV', 'AMB', 'SIN', 'QTUM', 'XEM', 'XCASH', // These are not actually used in ZelCore or some tickers; just for testing until merge + 'GNT', 'AGI', 'ETHOS', 'BSV', 'AMB', 'SIN', 'QTUM', 'XEM', 'XCASH', // These are not actually used in ZelCore or some tickers; just for testing until merge ], /** * CoinGecko API IDs. @@ -67,6 +67,8 @@ export const zelData: { /** * Array of CoinGecko tokens. */ +// Reassigned wholesale by the refresher below once CoinGecko answers. +// eslint-disable-next-line import/no-mutable-exports export let cgTokens: CoinGeckoToken[] = cgCoins; /** @@ -94,17 +96,18 @@ export async function getLatestCoinInfo(): Promise { const coinInfo: Record = (await axios.get(config.zelCoinInfoUrl)).data; const coinGeckoKeys = Object.values(coinInfo) .map((coin) => coin.coingeckoID) - .filter((id) => !!id); + .filter((id) => !!id) + .filter((id: string) => !id.startsWith('bstock-')); const uniqueCoinGeckoKeys = [...new Set(coinGeckoKeys)]; coinAggregatorIDs.coingecko = [...new Set([...coinAggregatorIDs.coingecko, ...uniqueCoinGeckoKeys])]; zelData.coinInfo = coinInfo; - const cgCoins = await CoinGecko.getInstance().getCoinsList(); - if (cgCoins) { - cgTokens = cgCoins as CoinGeckoToken[]; - cgCoins.forEach((coin: CoinGeckoToken) => { - for (const _contract of Object.values(coin.platforms)) { - if (_contract) { - cgContractMap[_contract] = coin; + const coinsList = await CoinGecko.getInstance().getCoinsList(); + if (coinsList) { + cgTokens = coinsList as CoinGeckoToken[]; + coinsList.forEach((coin: CoinGeckoToken) => { + for (const contract of Object.values(coin.platforms)) { + if (contract) { + cgContractMap[contract] = coin; } } }); diff --git a/src/services/newContracts.ts b/src/services/newContracts.ts index b0ae26c..866c970 100644 --- a/src/services/newContracts.ts +++ b/src/services/newContracts.ts @@ -35,7 +35,7 @@ export function checkContracts(contracts: ContractWithType[]): boolean { const cg = cgContractMap[contract.address]; if (cg) { if (foundContracts[contract.address]) { - foundContracts[contract.address].count++; + foundContracts[contract.address].count += 1; } else { foundContracts[contract.address] = { zel: contract, cg, count: 1 }; } @@ -46,4 +46,4 @@ export function checkContracts(contracts: ContractWithType[]): boolean { log.error(error); return false; } -} \ No newline at end of file +} diff --git a/src/services/providers/binance.ts b/src/services/providers/binance.ts new file mode 100644 index 0000000..d6f236c --- /dev/null +++ b/src/services/providers/binance.ts @@ -0,0 +1,345 @@ +import { LRUCache as LRU } from 'lru-cache'; +import config from '../../../config'; +import * as log from '../../lib/log'; +import { AxiosWrapper } from '../../lib/axios'; +import { arraySplit } from '../../lib/utils'; +import type { BinanceTicker, BinanceTokenisedAsset } from '../../types'; + +// 7d ticker window is fetched per-symbol; stay far under Binance's 200-weight/request cap. +const TICKER_CHUNK = 20; + +// Quote cache TTL, and with it the bound on how old a served price may be and +// still count as live: a batch answered from `quoteCache` legitimately carries +// a price up to one TTL old. See `pricedFresh`. +const QUOTE_CACHE_MS = 60 * 1000; + +/** + * Singleton class to interact with Binance's public (no-API-key) endpoints. + * + * Provides the tokenised-asset universe (bStocks with a BSC contract) and Spot + * 24h/7d tickers, quoted in USDT. Mirrors `CoinGecko`'s shape: an `AxiosWrapper` + * per base URL, an `LRUCache` per refresh cadence, and defensive error handling + * that never lets a single failed refresh drop a symbol that was previously + * known good (e.g. during a CEX trading halt around a stock split). + * + * @example + * ```typescript + * import { Binance } from './binance'; + * + * async function fetchBStocks() { + * const binance = Binance.getInstance(); + * const assets = await binance.getTokenisedAssets(); + * const trading = await binance.getTradingSymbols(); + * const tickers = await binance.getTicker24h([...trading]); + * console.log(tickers); + * } + * ``` + */ +export class Binance { + /** + * The singleton instance of the Binance class. + * @private + */ + private static instance: Binance; + + /** + * AxiosWrapper for the api.binance.com host (exchangeInfo, tickers). + * @private + */ + private api = new AxiosWrapper(config.binanceApiUrl); + + /** + * AxiosWrapper for the www.binance.com host (tokenised-asset listing). + * @private + */ + private assetApi = new AxiosWrapper(config.binanceAssetUrl); + + /** + * Cache for slow-moving data (tokenised-asset list, trading symbol set): 1 hour. + * @private + */ + private longCache = new LRU({ max: 10, ttl: 60 * 60 * 1000 }); + + /** + * Cache for ticker quotes, keyed by requested symbol set: 60 seconds. + * @private + */ + private quoteCache = new LRU({ max: 50, ttl: QUOTE_CACHE_MS }); + + /** + * Last-known-good ticker per `${window}:${symbol}`, independent of + * `quoteCache`'s TTL, with the epoch-ms timestamp at which it was accepted. + * Used to backfill a symbol whose fresh value is unusable — a halted symbol + * priced at zero, a symbol omitted from the batch, or an entire request + * that failed — so a transient gap upstream never drops the symbol or + * fabricates a price for it. + * + * Keyed per window (not bare symbol) because `getTicker24h` and + * `getTicker7d` both resolve the same symbol but with window-scoped + * `priceChangePercent`/`quoteVolume`. A single symbol-keyed store would let + * whichever window last wrote silently overwrite the other's fallback — + * e.g. a 7d fetch populating the store, then a later 24h failure serving + * the 7-day change/volume as the 24-hour figure. + * @private + */ + private lastGoodTicker = new Map(); + + /** + * Whether a freshly-fetched ticker carries a usable price. + * + * Binance does NOT omit a halted symbol from the ticker response: it returns + * the symbol present with `lastPrice: "0.00000000"`. Measured against live + * data, 20 of 20 requested BREAK-status symbols came back present and 9 of + * those 20 were priced at zero. So a presence check alone never triggers the + * last-known-good fallback, and accepting the zero would both serve $0 and + * overwrite the good value — worse than dropping the entry. + * + * @private + * @param ticker - A ticker straight from Binance. + * @returns True when the ticker has a finite, strictly positive last price. + */ + private static isUsable(ticker: BinanceTicker | undefined): ticker is BinanceTicker { + if (!ticker) return false; + const px = parseFloat(String(ticker.lastPrice)); + return Number.isFinite(px) && px > 0; + } + + /** + * Returns the singleton instance of the Binance class. + * + * @returns The singleton instance of Binance. + */ + static getInstance(): Binance { + if (!Binance.instance) Binance.instance = new Binance(); + return Binance.instance; + } + + /** + * Filters tokenised assets down to those with a BSC (BNB Smart Chain) contract listed. + * + * @param assets - The raw tokenised-asset list from Binance. + * @returns Only the assets with at least one BSC entry in `caList`. + */ + // eslint-disable-next-line class-methods-use-this -- pure helper, but part of the provider's instance API like the rest of the class. + filterBscAssets(assets: BinanceTokenisedAsset[]): BinanceTokenisedAsset[] { + return (assets || []).filter((a) => (a.caList || []) + .some((c) => String(c.network).toUpperCase() === 'BSC' && !!c.ca)); + } + + /** + * Splits a symbol list into chunks of at most `TICKER_CHUNK` symbols, to stay + * under Binance's per-request weight cap on the 7d rolling-window ticker. + * + * @param symbols - The full symbol list to split. + * @returns An array of symbol chunks. + */ + // eslint-disable-next-line class-methods-use-this -- pure helper, but part of the provider's instance API like the rest of the class. + chunkSymbols(symbols: string[]): string[][] { + return arraySplit(symbols, TICKER_CHUNK); + } + + /** + * Merges a freshly-fetched ticker batch into the last-known-good store, then + * returns the requested symbols using the fresh value where available and + * falling back to the last-known-good value otherwise (halted/omitted symbol, + * or the whole request failed and `fetched` is empty). + * + * `window` scopes both the write and the fallback read to `${window}:${symbol}` + * so the 24h and 7d stores never collide — see the `lastGoodTicker` doc. + * + * @private + * @param window - Which ticker window this batch belongs to (`24h` or `7d`). + * @param symbols - The symbols that were requested. + * @param fetched - Whatever tickers were actually returned (possibly a subset, possibly empty on error). + * @returns One ticker per requested symbol that has ever been seen for this window; halted/never-seen symbols are omitted. + */ + private mergeTickers(window: '24h' | '7d', symbols: string[], fetched: BinanceTicker[]): BinanceTicker[] { + const now = Date.now(); + const bySymbol = new Map(); + fetched.forEach((t) => { + // Only a usable price is allowed to become the new last-known-good. + // A halted symbol comes back present but priced at zero; letting it + // through would overwrite the real price and serve $0 from then on. + if (!Binance.isUsable(t)) return; + bySymbol.set(t.symbol, t); + this.lastGoodTicker.set(`${window}:${t.symbol}`, { ticker: t, at: now }); + }); + return symbols + .map((s) => bySymbol.get(s) ?? this.lastGoodTicker.get(`${window}:${s}`)?.ticker) + .filter((t): t is BinanceTicker => !!t); + } + + /** + * Age in milliseconds of the last-known-good price for a symbol, or null if + * none has ever been recorded for the requested window(s). Lets a caller + * distinguish a live price from one carried through a long halt, which the + * ticker itself cannot express. + * + * @param symbol - The Binance symbol, e.g. `TSLABUSDT`. + * @param window - Which window's last-known-good entry to check (`24h` or + * `7d`). Omit to get the freshest of the two — the age of whichever window + * priced most recently — which is what a caller asking "how stale is this + * symbol overall" generally wants. + * @returns Age in ms, or null when the symbol has never priced successfully + * for the requested window (or for either window, when unspecified). + */ + lastGoodAgeMs(symbol: string, window?: '24h' | '7d'): number | null { + if (window) { + const entry = this.lastGoodTicker.get(`${window}:${symbol}`); + return entry ? Date.now() - entry.at : null; + } + const ages = (['24h', '7d'] as const) + .map((w) => this.lastGoodTicker.get(`${w}:${symbol}`)) + .filter((e): e is { ticker: BinanceTicker; at: number } => !!e) + .map((e) => Date.now() - e.at); + return ages.length ? Math.min(...ages) : null; + } + + /** + * Whether the price currently served for a symbol comes from a live quote + * rather than the last-known-good backfill. + * + * `mergeTickers` returns a plain `BinanceTicker` whether it was fetched or + * carried, so a caller cannot tell the two apart from the returned value — + * and a carried price is a valid, positive number, which makes the + * difference invisible to any price check. A batch answered from + * `quoteCache` legitimately carries a price up to one cache TTL old, so + * anything within that window is live; past it, nothing has priced the + * symbol since, so every batch in between was backfilled. + * + * @param symbol - The Binance symbol, e.g. `TSLABUSDT`. + * @param window - Which ticker window to check (`24h` or `7d`). + * @returns True when the symbol priced live within the quote-cache window. + */ + pricedFresh(symbol: string, window: '24h' | '7d'): boolean { + const age = this.lastGoodAgeMs(symbol, window); + return age !== null && age <= QUOTE_CACHE_MS; + } + + /** + * Retrieves the tokenised-asset universe (bStocks), filtered to those with a BSC contract. + * + * Cached for 1 hour. + * + * @returns The BSC-listed tokenised assets. + */ + async getTokenisedAssets(): Promise { + const key = 'tokenised'; + if (this.longCache.has(key)) return this.longCache.get(key) as BinanceTokenisedAsset[]; + + try { + const res = await this.assetApi.get('bapi/asset/v2/public/asset/asset/get-tokenised-asset'); + const assets = this.filterBscAssets(res.data?.data ?? []); + this.longCache.set(key, assets); + return assets; + } catch (err) { + log.error('Error getting tokenised assets from Binance'); + log.error(err); + // Negatively-cache the failure briefly (well under the 1h success TTL) + // so a Binance outage doesn't re-spend the full ~23s AxiosWrapper retry + // budget on every 30s refresh cycle, forever, until Binance recovers. + this.longCache.set(key, [], { ttl: config.binanceFailureCacheMs }); + return []; + } + } + + /** + * Retrieves the set of Spot symbols currently in `TRADING` status. + * + * A symbol dropping to `BREAK` (as happens during trading halts, e.g. around + * a stock split) simply falls out of this set on the next refresh; callers + * should keep serving the last-known-good ticker for it rather than treating + * its absence here as "delisted". + * + * Cached for 1 hour. + * + * @returns The set of currently-trading symbols. + */ + async getTradingSymbols(): Promise> { + const key = 'trading'; + if (this.longCache.has(key)) return this.longCache.get(key) as Set; + + try { + const res = await this.api.get('api/v3/exchangeInfo?permissions=SPOT'); + const set = new Set( + (res.data?.symbols ?? []) + .filter((s: { status: string }) => s.status === 'TRADING') + .map((s: { symbol: string }) => s.symbol), + ); + this.longCache.set(key, set); + return set; + } catch (err) { + log.error('Error getting trading symbols from Binance'); + log.error(err); + // See the matching comment in getTokenisedAssets: negatively-cache so + // the retry storm doesn't repeat every cycle while Binance is down. + this.longCache.set(key, new Set(), { ttl: config.binanceFailureCacheMs }); + return new Set(); + } + } + + /** + * Retrieves 24h tickers for the given symbols in a single request. + * + * On a failed or partial refresh, missing symbols are backfilled from the + * last-known-good store rather than dropped. Cached for 60 seconds per + * requested symbol set. + * + * @param symbols - The Spot symbols to fetch (e.g. `TSLABUSDT`). + * @returns One ticker per requested symbol that has ever been seen. + */ + async getTicker24h(symbols: string[]): Promise { + const key = `t24:${[...symbols].sort().join(',')}`; + if (this.quoteCache.has(key)) return this.quoteCache.get(key) as BinanceTicker[]; + + let fetched: BinanceTicker[] = []; + try { + const res = await this.api.get(`api/v3/ticker/24hr?symbols=${encodeURIComponent(JSON.stringify(symbols))}`); + fetched = res.data ?? []; + } catch (err) { + log.error('Error getting 24h tickers from Binance'); + log.error(err); + } + + const merged = this.mergeTickers('24h', symbols, fetched); + this.quoteCache.set(key, merged); + return merged; + } + + /** + * Retrieves 7d rolling-window tickers for the given symbols, chunked to stay + * under Binance's per-request weight cap. + * + * Each chunk is fetched independently, so one failing chunk never drops the + * symbols in the others; any symbol whose chunk failed (or that was omitted, + * e.g. a halt) is backfilled from the last-known-good store. Cached for 60 + * seconds per requested symbol set. + * + * @param symbols - The Spot symbols to fetch (e.g. `TSLABUSDT`). + * @returns One ticker per requested symbol that has ever been seen. + */ + async getTicker7d(symbols: string[]): Promise { + const key = `t7d:${[...symbols].sort().join(',')}`; + if (this.quoteCache.has(key)) return this.quoteCache.get(key) as BinanceTicker[]; + + const chunks = this.chunkSymbols(symbols); + const fetched: BinanceTicker[] = []; + /* eslint-disable no-await-in-loop */ + for (const chunk of chunks) { + try { + const res = await this.api.get(`api/v3/ticker?symbols=${encodeURIComponent(JSON.stringify(chunk))}&windowSize=7d`); + fetched.push(...(res.data ?? [])); + } catch (err) { + log.error('Error getting 7d tickers from Binance'); + log.error(err); + } + } + /* eslint-enable no-await-in-loop */ + + const merged = this.mergeTickers('7d', symbols, fetched); + this.quoteCache.set(key, merged); + return merged; + } +} + +export default Binance; diff --git a/src/services/providers/bitpay.ts b/src/services/providers/bitpay.ts index 9b85e2d..6beea0a 100644 --- a/src/services/providers/bitpay.ts +++ b/src/services/providers/bitpay.ts @@ -1,6 +1,6 @@ -import AxiosWrapper from "../../lib/axios"; -import config from "../../../config"; import { LRUCache as LRU } from 'lru-cache'; +import AxiosWrapper from '../../lib/axios'; +import config from '../../../config'; /** * Singleton class to interact with the BitPay API. @@ -57,7 +57,7 @@ export class BitPay { */ constructor() { if (BitPay.instance) { - throw new Error("Use BitPay.getInstance()"); + throw new Error('Use BitPay.getInstance()'); } BitPay.instance = this; BitPay.axiosWrapper = new AxiosWrapper(config.bitPayUrl); @@ -115,17 +115,17 @@ export class BitPay { public async getFiatRates(): Promise { const cacheKey = 'fiatRates'; const cachedRates = this.cache.get(cacheKey); - + if (cachedRates) { return cachedRates; } try { const response = await this.get('rates/BTC'); - const data = response.data.data; + const { data } = response.data; this.cache.set(cacheKey, data); - + return data; } catch (error) { return null; diff --git a/src/services/providers/coinGecko.ts b/src/services/providers/coinGecko.ts index 519f71c..2831fd0 100644 --- a/src/services/providers/coinGecko.ts +++ b/src/services/providers/coinGecko.ts @@ -1,9 +1,9 @@ -import AxiosWrapper from "../../lib/axios"; -import config from "../../../config"; -import * as log from "../../lib/log"; -import { arraySplit } from "../../lib/utils"; import { LRUCache as LRU } from 'lru-cache'; -import { CoinGeckoPrice } from "../../types"; +import AxiosWrapper from '../../lib/axios'; +import config from '../../../config'; +import * as log from '../../lib/log'; +import { arraySplit } from '../../lib/utils'; +import { CoinGeckoPrice } from '../../types'; const MAX_IDS_PER_REQUEST = 250; @@ -42,7 +42,7 @@ export class CoinGecko { * The API key for authenticating with the CoinGecko API. * @private */ - private readonly apiKey: string = process.env['COIN_GECKO_KEY'] || config.coinGeckoApiKey; + private readonly apiKey: string = process.env.COIN_GECKO_KEY || config.coinGeckoApiKey; /** * The singleton instance of the CoinGecko class. @@ -80,7 +80,7 @@ export class CoinGecko { */ constructor() { if (CoinGecko.instance) { - throw new Error("Use CoinGecko.getInstance()"); + throw new Error('Use CoinGecko.getInstance()'); } CoinGecko.instance = this; CoinGecko.axiosWrapper = new AxiosWrapper(config.coinGeckoUrl); @@ -145,7 +145,7 @@ export class CoinGecko { try { const response = await this.get('key'); - const data = response.data; + const { data } = response; this.cache.set(cacheKey, data); @@ -180,7 +180,7 @@ export class CoinGecko { try { const response = await this.get('coins/list', { include_platform: includePlatform }); - const data = response.data; + const { data } = response; this.cache.set(cacheKey, data); @@ -214,7 +214,7 @@ export class CoinGecko { try { const response = await this.get('asset_platforms'); - const data = response.data; + const { data } = response; this.cache.set(cacheKey, data); @@ -244,7 +244,7 @@ export class CoinGecko { const response = await this.get('coins/markets', { vs_currency: vsCurrency, - ids: ids, + ids, order: 'market_cap_desc', per_page: 250, page: 1, @@ -252,7 +252,7 @@ export class CoinGecko { price_change_percentage: '7d', }); - const data = response.data; + const { data } = response; this.cache.set(cacheKey, data); @@ -280,6 +280,7 @@ export class CoinGecko { const allRates: CoinGeckoPrice[] = []; for (const id of newIds) { + // eslint-disable-next-line no-await-in-loop -- deliberately sequential: one id per request keeps us inside the upstream rate limit. const response = await this._getExchangeRates(id, vsCurrency); allRates.push(...response); } diff --git a/src/services/providers/cryptoCompare.ts b/src/services/providers/cryptoCompare.ts index 7996ee5..e23cedc 100644 --- a/src/services/providers/cryptoCompare.ts +++ b/src/services/providers/cryptoCompare.ts @@ -1,8 +1,8 @@ -import AxiosWrapper from "../../lib/axios"; -import config from "../../../config"; -import { makeRequestStrings } from "../../lib/utils"; import { LRUCache as LRU } from 'lru-cache'; -import { CryptoCompareMarkets, CryptoComparePrice } from "../../types"; +import AxiosWrapper from '../../lib/axios'; +import config from '../../../config'; +import { makeRequestStrings } from '../../lib/utils'; +import { CryptoCompareMarkets, CryptoComparePrice } from '../../types'; const MAX_LENGTH_PER_REQUEST = 300; @@ -30,7 +30,7 @@ export class CryptoCompare { * The API key for authenticating with the CryptoCompare API. * @private */ - private readonly apiKey: string = process.env['CRYPTO_COMPARE_KEY'] || config.cryptoCompareApiKey; + private readonly apiKey: string = process.env.CRYPTO_COMPARE_KEY || config.cryptoCompareApiKey; /** * The singleton instance of the CryptoCompare class. @@ -50,7 +50,7 @@ export class CryptoCompare { */ private readonly headers = { 'Content-Type': 'application/json', - 'authorization': `Apikey ${this.apiKey}`, + authorization: `Apikey ${this.apiKey}`, }; /** @@ -68,7 +68,7 @@ export class CryptoCompare { */ constructor() { if (CryptoCompare.instance) { - throw new Error("Use CryptoCompare.getInstance()"); + throw new Error('Use CryptoCompare.getInstance()'); } CryptoCompare.instance = this; CryptoCompare.axiosWrapper = new AxiosWrapper(config.cryptoCompareUrl); @@ -136,7 +136,7 @@ export class CryptoCompare { fsyms: ids, }); - const data: CryptoComparePrice = response.data; + const { data } = response; // Store in cache this.cache.set(cacheKey, data); @@ -165,6 +165,7 @@ export class CryptoCompare { let allRates: CryptoComparePrice = {}; for (const id of newIds) { + // eslint-disable-next-line no-await-in-loop -- deliberately sequential: one id per request keeps us inside the upstream rate limit. const response = await this._getExchangeRates(id, vsCurrency); allRates = { ...allRates, ...response }; } @@ -228,6 +229,7 @@ export class CryptoCompare { let allData: CryptoCompareMarkets = {}; for (const id of newIds) { + // eslint-disable-next-line no-await-in-loop -- deliberately sequential: one id per request keeps us inside the upstream rate limit. const response = await this._getMarketData(id, vsCurrency); allData = { ...allData, ...response }; } diff --git a/src/services/providers/index.ts b/src/services/providers/index.ts index 2f0dc80..2a60ef0 100644 --- a/src/services/providers/index.ts +++ b/src/services/providers/index.ts @@ -1,4 +1,5 @@ export { CoinGecko } from './coinGecko'; export { CryptoCompare } from './cryptoCompare'; export { BitPay } from './bitpay'; -export { LiveCoinWatch } from './liveCoinWatch'; \ No newline at end of file +export { LiveCoinWatch } from './liveCoinWatch'; +export { Binance } from './binance'; diff --git a/src/services/providers/liveCoinWatch.ts b/src/services/providers/liveCoinWatch.ts index ed3f02b..6bd4349 100644 --- a/src/services/providers/liveCoinWatch.ts +++ b/src/services/providers/liveCoinWatch.ts @@ -1,8 +1,8 @@ -import AxiosWrapper from "../../lib/axios"; -import config from "../../../config"; -import { makeRequestStrings } from "../../lib/utils"; import { LRUCache as LRU } from 'lru-cache'; -import { LiveCoinWatchMarket } from "../../types"; +import AxiosWrapper from '../../lib/axios'; +import config from '../../../config'; +import { makeRequestStrings } from '../../lib/utils'; +import { LiveCoinWatchMarket } from '../../types'; const MAX_LENGTH_PER_REQUEST = 300; @@ -30,7 +30,7 @@ export class LiveCoinWatch { * The API key for authenticating with the LiveCoinWatch API. * @private */ - private readonly apiKey: string = process.env['LIVE_COIN_WATCH_KEY'] || config.liveCoinWatchApiKey; + private readonly apiKey: string = process.env.LIVE_COIN_WATCH_KEY || config.liveCoinWatchApiKey; /** * The singleton instance of the LiveCoinWatch class. @@ -68,7 +68,7 @@ export class LiveCoinWatch { */ constructor() { if (LiveCoinWatch.instance) { - throw new Error("Use LiveCoinWatch.getInstance()"); + throw new Error('Use LiveCoinWatch.getInstance()'); } LiveCoinWatch.instance = this; LiveCoinWatch.axiosWrapper = new AxiosWrapper(config.liveCoinWatchUrl); @@ -164,10 +164,11 @@ export class LiveCoinWatch { public async getExchangeRates(ids: string[], vsCurrency = 'BTC'): Promise { const newIds = makeRequestStrings(ids, MAX_LENGTH_PER_REQUEST); let allRates: LiveCoinWatchMarket[] = []; - + for (const id of newIds) { + // eslint-disable-next-line no-await-in-loop -- deliberately sequential: one id per request keeps us inside the upstream rate limit. const response = await this._getExchangeRates(id, vsCurrency); - allRates = [ ...allRates, ...response ]; + allRates = [...allRates, ...response]; } return allRates; diff --git a/src/services/zelcoreMarketsUSD.ts b/src/services/zelcoreMarketsUSD.ts index 255f232..0c41c73 100644 --- a/src/services/zelcoreMarketsUSD.ts +++ b/src/services/zelcoreMarketsUSD.ts @@ -19,7 +19,6 @@ import { MarketsData, IErrorObject, CoinGeckoPrice, LiveCoinWatchMarket, Currenc * ``` */ export async function getAll(): Promise { - const markets: MarketsData = [{}, { errors: {} }]; const cmk: CurrencyMap = {}; const errors: IErrorObject = { errors: {} }; @@ -64,7 +63,7 @@ export async function getAll(): Promise { log.error(e); errors.errors.coingecko = true; } - + // Fetch results from LiveCoinWatch try { const livecoinwatch = await LiveCoinWatch.getInstance().getExchangeRates(coinAggregatorIDs.livecoinwatch, 'USD'); @@ -138,4 +137,4 @@ export async function getAll(): Promise { export default { getAll, -}; \ No newline at end of file +}; diff --git a/src/services/zelcoreRatesV2.ts b/src/services/zelcoreRatesV2.ts index f958670..8b49c7a 100644 --- a/src/services/zelcoreRatesV2.ts +++ b/src/services/zelcoreRatesV2.ts @@ -1,8 +1,27 @@ import { coinAggregatorIDs } from './coinAggregatorIDs'; import * as log from '../lib/log'; import { CoinGecko, BitPay, CryptoCompare, LiveCoinWatch } from './providers'; +import { getBstockPrices, isBstocksDegraded, getLastGoodBstockPrices } from './bstocks'; import { PricesResponse, CryptoPrice, ICurrencyRate } from '../types'; +/** + * Resolves after `ms` milliseconds, ignoring the value it's chained onto. + * Used to bound the bStocks fetch below: a total Binance outage can otherwise + * cost a single refresh cycle up to ~92s (the AxiosWrapper retry budget spent + * across `getTokenisedAssets`, `getTradingSymbols`, and both ticker windows), + * which would stall the refresh of all 364 non-bStock assets behind it. + * + * `Promise.race` never cancels the losing branch, so on the normal/healthy + * path (`getBstockPrices()` wins well under 10s) this timer is still live in + * the background for whatever remains of the 10s -- `.unref()` keeps it from + * holding the process open for that tail, since nothing depends on it firing. + * + * @param ms - Milliseconds to wait. + */ +function timeout(ms: number): Promise { + return new Promise((resolve) => { setTimeout(resolve, ms).unref(); }); +} + /** * Fetches and aggregates cryptocurrency prices and fiat rates from multiple providers. * @@ -29,7 +48,7 @@ export async function getAll(): Promise { const processed: CryptoPrice[] = []; const fiat: ICurrencyRate[] = []; const errors: Record = {}; - + // Fetch fiat rates from BitPay try { const bitpayRates = await BitPay.getInstance().getFiatRates(); @@ -71,7 +90,7 @@ export async function getAll(): Promise { log.error(e); errors.coingecko = true; } - + // Fetch cryptocurrency prices from CryptoCompare try { const cryptocompare = await CryptoCompare.getInstance().getMarketData(coinAggregatorIDs.cryptoCompare); @@ -99,7 +118,7 @@ export async function getAll(): Promise { log.error(e); errors.cryptocompare = true; } - + // Fetch cryptocurrency prices from LiveCoinWatch try { const livecoinwatch = await LiveCoinWatch.getInstance().getExchangeRates(coinAggregatorIDs.livecoinwatch); @@ -132,6 +151,38 @@ export async function getAll(): Promise { errors.livecoinwatch = true; } + // Fetch bStock prices from Binance. Bounded to 10s so a hung/slow Binance + // outage cannot stall the refresh of every other provider behind it. + // + // Losing the race must NOT resolve to []. bStock rows carry + // provider: 'coingecko' while their failure is reported under + // errors.binance, so the provider carry-forward in apiServices can never + // protect them -- an empty result would drop every bStock from /v2/rates + // and the wallet would show them at $0 with no error banner. Serve the + // last-known-good snapshot instead, and flag the cycle as degraded, since + // the timeout branch leaves freshPricedLastRun reflecting a previous run. + let raceLost = false; + try { + const bstocks = await Promise.race([ + getBstockPrices(), + timeout(10_000).then((): CryptoPrice[] => { + raceLost = true; + return getLastGoodBstockPrices(); + }), + ]); + processed.push(...bstocks); + // getBstockPrices() never rejects -- every failure inside it is caught + // internally -- so a total Binance outage looks identical to a healthy + // refresh unless we ask it directly whether it degraded this cycle. + if (raceLost || isBstocksDegraded()) { + errors.binance = true; + } + } catch (e) { + log.error('bStocks error'); + log.error(e); + errors.binance = true; + } + return { crypto: processed, fiat, diff --git a/src/types.ts b/src/types.ts index 1c06661..3ef9a2f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -126,6 +126,21 @@ export type CoinGeckoPrice = { price_change_percentage_7d_in_currency: number; }; +export type BinanceTokenisedAsset = { + assetCode: string; + assetName: string; + uq?: string; + logo?: string; + caList?: { network: string; ca: string }[]; +}; + +export type BinanceTicker = { + symbol: string; + lastPrice: string; + priceChangePercent: string; + quoteVolume: string; +}; + export type CryptoComparePrice = { [key: string]: { [key: string]: number; @@ -233,4 +248,4 @@ export type LiveCoinWatchMarket = { quarter: number | null; year: number | null; }; -}; \ No newline at end of file +}; diff --git a/swagger.yaml b/swagger.yaml index 546230f..7c6e92f 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -40,7 +40,29 @@ paths: /v2/rates: get: summary: Get exchange rates (v2) - description: Retrieves version 2 of the exchange rates. + description: > + Retrieves version 2 of the exchange rates. The `crypto` array includes + one synthetic entry per Binance bStock (tokenized US equity on BNB + Smart Chain), alongside the regular CoinGecko/CryptoCompare/LiveCoinWatch + entries. + + + bStock entries are identified by `id` starting with `bstock-` (e.g. + `bstock-tslab` for the Tesla bStock). They are always tagged + `provider: "coingecko"` — never `"binance"` — because the ZelCore + client matches market entries on the literal string + `${provider}-${id}`, and the sibling `api` repo advertises each + bStock's `coinInfo.coingeckoID` as `bstock-`. Using any other + provider value would make the client-side lookup miss silently. + + + `rates.usd` and `rates.btc` are sourced from live Binance Spot + `USDT` tickers (bStocks are quoted in USDT, not USDC) divided by + the same batch's `BTCUSDT` price; `change24h`/`change7d` come from + Binance's 24h and 7d rolling-window tickers. During a CEX trading + halt (e.g. around a stock split) the last known-good price is served + rather than dropping the entry, per the bStocks partner guide's + display-only allowance. responses: '200': description: A list of exchange rates (v2). @@ -48,6 +70,35 @@ paths: application/json: schema: type: object + properties: + crypto: + type: array + items: + type: object + properties: + id: + type: string + example: bstock-tslab + provider: + type: string + example: coingecko + rates: + type: object + properties: + usd: + type: number + btc: + type: number + change24h: + type: number + change7d: + type: number + fiat: + type: array + items: + type: object + errors: + type: object /v2/rates-compressed: get: diff --git a/tests/binanceProvider.spec.ts b/tests/binanceProvider.spec.ts new file mode 100644 index 0000000..8cdb9f8 --- /dev/null +++ b/tests/binanceProvider.spec.ts @@ -0,0 +1,251 @@ +import type { AxiosResponse } from 'axios'; +import { AxiosWrapper } from '../src/lib/axios'; +import { Binance } from '../src/services/providers/binance'; +import type { BinanceTicker } from '../src/types'; + +/** Builds a minimal AxiosResponse-shaped object so mockResolvedValue satisfies AxiosWrapper.get's return type. */ +const axiosResponse = (data: T): AxiosResponse => ({ + data, + status: 200, + statusText: 'OK', + headers: {}, + config: {} as AxiosResponse['config'], +}); + +describe('Binance provider', () => { + const binance = Binance.getInstance(); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('is a singleton', () => { + expect(Binance.getInstance()).toBe(binance); + }); + + it('filters tokenised assets to those with a BSC contract', () => { + const assets = binance.filterBscAssets([ + { assetCode: 'TSLAB', assetName: 'Tesla', caList: [{ network: 'BSC', ca: '0x5b19' }] }, + { assetCode: 'ALABB', assetName: 'Unlaunched', caList: [] }, + ]); + expect(assets.map((a) => a.assetCode)).toEqual(['TSLAB']); + }); + + it('chunks 7d ticker requests to 20 symbols', () => { + const chunks = binance.chunkSymbols(Array.from({ length: 45 }, (_, i) => `S${i}USDT`)); + expect(chunks.length).toBe(3); + expect(chunks[0].length).toBe(20); + expect(chunks[2].length).toBe(5); + }); + + describe('getTokenisedAssets', () => { + it('unwraps the {data:[...]} envelope, keeps only BSC-listed assets, and caches the result', async () => { + const getSpy = jest.spyOn(AxiosWrapper.prototype, 'get').mockResolvedValue(axiosResponse({ + code: '000000', + message: null, + messageDetail: null, + data: [ + { assetCode: 'TSLAB', assetName: 'Tesla (bStocks)', logo: 'https://x/logo.png', uq: 'TSLA', caList: [{ network: 'BSC', ca: '0x5b1910eaad6450e50f816082aa078c41f10c292f' }] }, + { assetCode: 'TEST1B', assetName: 'Bstock TEST1B', uq: 'BNKK', caList: [] as { network: string; ca: string }[] }, + ], + })); + + const result = await binance.getTokenisedAssets(); + expect(result.map((a) => a.assetCode)).toEqual(['TSLAB']); + expect(getSpy).toHaveBeenCalledWith('bapi/asset/v2/public/asset/asset/get-tokenised-asset'); + + // second call within the 1h TTL must be served from cache, not the network + await binance.getTokenisedAssets(); + expect(getSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('getTradingSymbols', () => { + it('returns only symbols whose status is TRADING (BREAK/halted symbols excluded) and caches the result', async () => { + const getSpy = jest.spyOn(AxiosWrapper.prototype, 'get').mockResolvedValue(axiosResponse({ + timezone: 'UTC', + serverTime: 1785921304221, + symbols: [ + { symbol: 'TSLABUSDT', status: 'TRADING', baseAsset: 'TSLAB', quoteAsset: 'USDT' }, + { symbol: 'USDSBUSDT', status: 'BREAK', baseAsset: 'USDSB', quoteAsset: 'USDT' }, + { symbol: 'NVDABUSDT', status: 'TRADING', baseAsset: 'NVDAB', quoteAsset: 'USDT' }, + ], + })); + + const set = await binance.getTradingSymbols(); + expect(set).toEqual(new Set(['TSLABUSDT', 'NVDABUSDT'])); + expect(getSpy).toHaveBeenCalledWith('api/v3/exchangeInfo?permissions=SPOT'); + + await binance.getTradingSymbols(); + expect(getSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('getTicker24h', () => { + it('requests all symbols in a single call and caches the result for 60s', async () => { + const raw: BinanceTicker[] = [ + { symbol: 'TSLABUSDT', lastPrice: '324.92000000', priceChangePercent: '0.247', quoteVolume: '3149396.46686000' }, + { symbol: 'NVDABUSDT', lastPrice: '216.05000000', priceChangePercent: '3.626', quoteVolume: '3539230.84326000' }, + ]; + const getSpy = jest.spyOn(AxiosWrapper.prototype, 'get').mockResolvedValue(axiosResponse(raw)); + + const result = await binance.getTicker24h(['TSLABUSDT', 'NVDABUSDT']); + expect(result).toEqual(raw); + expect(getSpy).toHaveBeenCalledTimes(1); + const [[calledUrl]] = getSpy.mock.calls; + expect(calledUrl).toBe(`api/v3/ticker/24hr?symbols=${encodeURIComponent(JSON.stringify(['TSLABUSDT', 'NVDABUSDT']))}`); + + await binance.getTicker24h(['TSLABUSDT', 'NVDABUSDT']); + expect(getSpy).toHaveBeenCalledTimes(1); + }); + + it('falls back to the last-known-good price for a symbol omitted on refresh (CEX halt)', async () => { + const good: BinanceTicker = { symbol: 'MSTRBUSDT', lastPrice: '400.00000000', priceChangePercent: '1.0', quoteVolume: '1000000' }; + const getSpy = jest.spyOn(AxiosWrapper.prototype, 'get'); + + // first request establishes a known-good price for MSTRBUSDT + getSpy.mockResolvedValueOnce(axiosResponse([good])); + const first = await binance.getTicker24h(['MSTRBUSDT']); + expect(first).toEqual([good]); + + // second request (different symbol set -> new cache key) omits MSTRBUSDT. + // NOTE: omission is NOT what a real halt looks like -- see the zero-price + // test below. This covers the genuinely-absent case only. + const other: BinanceTicker = { symbol: 'AMDBUSDT', lastPrice: '150.00000000', priceChangePercent: '2.0', quoteVolume: '500000' }; + getSpy.mockResolvedValueOnce(axiosResponse([other])); + const second = await binance.getTicker24h(['MSTRBUSDT', 'AMDBUSDT']); + + expect(second).toEqual(expect.arrayContaining([good, other])); + expect(second.find((t) => t.symbol === 'MSTRBUSDT')).toEqual(good); + }); + + it('treats a halted symbol priced at zero as unusable and keeps the last-known-good price', async () => { + // Measured against live Binance data: requesting 20 BREAK-status symbols + // returned all 20 PRESENT, and 9 of them carried lastPrice "0.00000000". + // Binance does not omit a halted symbol -- so a presence check alone never + // triggers the fallback, and accepting the zero would both serve $0 and + // overwrite the real price for good. + const good: BinanceTicker = { symbol: 'GOOGLBUSDT', lastPrice: '180.00000000', priceChangePercent: '1.0', quoteVolume: '900000' }; + const getSpy = jest.spyOn(AxiosWrapper.prototype, 'get'); + + getSpy.mockResolvedValueOnce(axiosResponse([good])); + expect(await binance.getTicker24h(['GOOGLBUSDT'])).toEqual([good]); + + // The halt: symbol present, price zeroed. + const halted: BinanceTicker = { symbol: 'GOOGLBUSDT', lastPrice: '0.00000000', priceChangePercent: '0.0', quoteVolume: '0' }; + const filler: BinanceTicker = { symbol: 'METABUSDT', lastPrice: '500.00000000', priceChangePercent: '0.2', quoteVolume: '100000' }; + getSpy.mockResolvedValueOnce(axiosResponse([halted, filler])); + const during = await binance.getTicker24h(['GOOGLBUSDT', 'METABUSDT']); + expect(during.find((t) => t.symbol === 'GOOGLBUSDT')).toEqual(good); + + // ...and the zero must not have poisoned the store: a later total failure + // still serves the real price rather than $0. + getSpy.mockRejectedValueOnce(new Error('network down')); + const after = await binance.getTicker24h(['GOOGLBUSDT', 'AMZNBUSDT']); + expect(after.find((t) => t.symbol === 'GOOGLBUSDT')).toEqual(good); + }); + + it('reports the age of a last-known-good price, and null for a symbol never priced', async () => { + const good: BinanceTicker = { symbol: 'ORCLBUSDT', lastPrice: '120.00000000', priceChangePercent: '1.0', quoteVolume: '10000' }; + const getSpy = jest.spyOn(AxiosWrapper.prototype, 'get'); + getSpy.mockResolvedValueOnce(axiosResponse([good])); + await binance.getTicker24h(['ORCLBUSDT']); + + expect(binance.lastGoodAgeMs('ORCLBUSDT')).toBeGreaterThanOrEqual(0); + expect(binance.lastGoodAgeMs('NEVERSEENUSDT')).toBeNull(); + }); + + it('serves the last-known-good ticker for requested symbols when the whole refresh request rejects', async () => { + const good: BinanceTicker = { symbol: 'CRCLBUSDT', lastPrice: '90.00000000', priceChangePercent: '0.5', quoteVolume: '2000000' }; + const getSpy = jest.spyOn(AxiosWrapper.prototype, 'get'); + + getSpy.mockResolvedValueOnce(axiosResponse([good])); + await binance.getTicker24h(['CRCLBUSDT']); + + // a different symbol combination forces a fresh network call, which this time fails outright + getSpy.mockRejectedValueOnce(new Error('network blip')); + const result = await binance.getTicker24h(['CRCLBUSDT', 'ZZZUSDT']); + + expect(result).toEqual([good]); + }); + }); + + describe('getTicker7d', () => { + it('chunks requests to <=20 symbols per call, tags windowSize=7d, and merges all chunk results', async () => { + const symbols = Array.from({ length: 25 }, (_, i) => `S${i}USDT`); + const getSpy = jest.spyOn(AxiosWrapper.prototype, 'get').mockImplementation(async (url?: string) => { + const match = (url || '').match(/symbols=([^&]+)/); + const requested: string[] = JSON.parse(decodeURIComponent(match ? match[1] : '[]')); + const data: BinanceTicker[] = requested.map((symbol) => ({ + symbol, lastPrice: '1.00', priceChangePercent: '0.0', quoteVolume: '1', + })); + return axiosResponse(data); + }); + + const result = await binance.getTicker7d(symbols); + expect(getSpy).toHaveBeenCalledTimes(2); + expect(getSpy.mock.calls[0][0]).toContain('windowSize=7d'); + expect(result.length).toBe(25); + expect(result.map((t) => t.symbol).sort()).toEqual([...symbols].sort()); + }); + }); + + // Regression coverage for the shared last-known-good store colliding across + // windows: `mergeTickers` used to key solely on `symbol`, so whichever of + // getTicker24h/getTicker7d ran (and priced successfully) LAST would silently + // overwrite the other window's fallback value -- serving a 7-day change + // and ~7x-inflated volume as the 24-hour figure, or vice versa. + describe('24h/7d last-known-good isolation', () => { + it('does not let a 7d last-known-good leak into a failed 24h fetch, or vice versa', async () => { + const getSpy = jest.spyOn(AxiosWrapper.prototype, 'get'); + + // Establish a 24h last-known-good for NVDAB: small change/volume. + const t24: BinanceTicker = { + symbol: 'NVDABUSDT', lastPrice: '120.00', priceChangePercent: '3.0', quoteVolume: '500000', + }; + getSpy.mockResolvedValueOnce(axiosResponse([t24])); + await binance.getTicker24h(['NVDABUSDT']); + + // Establish a 7d last-known-good for the SAME symbol: much larger + // (window-scoped) change/volume, as Binance's real 7d ticker would report. + const t7d: BinanceTicker = { + symbol: 'NVDABUSDT', lastPrice: '120.00', priceChangePercent: '9.0', quoteVolume: '3500000', + }; + getSpy.mockResolvedValueOnce(axiosResponse([t7d])); + await binance.getTicker7d(['NVDABUSDT']); + + // A fresh 24h fetch (different symbol combo -> new quoteCache key) now + // fails outright and must fall back to the 24h store -- NOT the 7d one. + getSpy.mockRejectedValueOnce(new Error('network down')); + const during24 = await binance.getTicker24h(['NVDABUSDT', 'FILLER1USDT']); + const fallback24 = during24.find((t) => t.symbol === 'NVDABUSDT'); + expect(fallback24?.priceChangePercent).toBe('3.0'); + expect(fallback24?.quoteVolume).toBe('500000'); + + // Symmetric check: a fresh 7d fetch failing must fall back to the 7d + // store, not whatever the 24h store now holds. + getSpy.mockRejectedValueOnce(new Error('network down')); + const during7d = await binance.getTicker7d(['NVDABUSDT', 'FILLER2USDT']); + const fallback7d = during7d.find((t) => t.symbol === 'NVDABUSDT'); + expect(fallback7d?.priceChangePercent).toBe('9.0'); + expect(fallback7d?.quoteVolume).toBe('3500000'); + }); + + it('lastGoodAgeMs reports a per-window age, and the freshest of the two when no window is given', async () => { + const getSpy = jest.spyOn(AxiosWrapper.prototype, 'get'); + + const t24: BinanceTicker = { + symbol: 'AMDBUSDT', lastPrice: '150.00', priceChangePercent: '2.0', quoteVolume: '500000', + }; + getSpy.mockResolvedValueOnce(axiosResponse([t24])); + await binance.getTicker24h(['AMDBUSDT']); + + // No 7d price has ever been recorded for AMDBUSDT. + expect(binance.lastGoodAgeMs('AMDBUSDT', '24h')).toBeGreaterThanOrEqual(0); + expect(binance.lastGoodAgeMs('AMDBUSDT', '7d')).toBeNull(); + // Falls back to whichever window exists when unspecified. + expect(binance.lastGoodAgeMs('AMDBUSDT')).toBeGreaterThanOrEqual(0); + expect(binance.lastGoodAgeMs('NEVERSEENUSDT')).toBeNull(); + }); + }); +}); diff --git a/tests/bstocks.spec.ts b/tests/bstocks.spec.ts new file mode 100644 index 0000000..e31c89a --- /dev/null +++ b/tests/bstocks.spec.ts @@ -0,0 +1,285 @@ +import { Binance } from '../src/services/providers/binance'; +import { + getBstockPrices, getLastGoodBstockPrices, _clearLastGoodForTests, isBstocksDegraded, +} from '../src/services/bstocks'; + +jest.mock('../src/services/providers/binance'); + +const MockedBinance = Binance as jest.Mocked; + +function mockBinance({ assets, trading, t24, t7d }: { + assets: unknown[]; trading: string[]; t24: unknown[]; t7d: unknown[]; +}) { + // Every fixture here is a batch the provider priced live this round: these + // tests hand back raw ticker arrays rather than exercising the provider's + // last-known-good backfill, so a symbol in `t24` is by definition fresh and + // anything else has never priced. The carried-price path -- where the + // provider serves an old ticker that looks identical to a live one -- is + // covered in bstocksStaleness.spec.ts against the real provider. + const live = new Set((t24 as { symbol: string }[]).map((t) => t.symbol)); + MockedBinance.getInstance.mockReturnValue({ + getTokenisedAssets: jest.fn().mockResolvedValue(assets), + getTradingSymbols: jest.fn().mockResolvedValue(new Set(trading)), + getTicker24h: jest.fn().mockResolvedValue(t24), + getTicker7d: jest.fn().mockResolvedValue(t7d), + lastGoodAgeMs: jest.fn((symbol: string) => (live.has(symbol) ? 0 : null)), + pricedFresh: jest.fn((symbol: string) => live.has(symbol)), + } as never); +} + +const TSLAB = { assetCode: 'TSLAB', assetName: 'Tesla', caList: [{ network: 'BSC', ca: '0x5b19' }] }; + +describe('bStocks assembler', () => { + beforeEach(() => _clearLastGoodForTests()); + + it('emits coingecko-provider entries with BTC and USD rates', async () => { + mockBinance({ + assets: [TSLAB], + trading: ['TSLABUSDT', 'BTCUSDT'], + t24: [ + { symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '2.5', quoteVolume: '1000000' }, + { symbol: 'BTCUSDT', lastPrice: '65222.00', priceChangePercent: '1.0', quoteVolume: '9' }, + ], + t7d: [{ symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '7.1', quoteVolume: '0' }], + }); + const prices = await getBstockPrices(); + expect(prices).toHaveLength(1); + expect(prices[0].id).toBe('bstock-tslab'); + expect(prices[0].provider).toBe('coingecko'); + expect(prices[0].rates.usd).toBeCloseTo(326.11); + expect(prices[0].rates.btc).toBeCloseTo(326.11 / 65222); + expect(prices[0].change24h).toBeCloseTo(2.5); + expect(prices[0].change7d).toBeCloseTo(7.1); + // rank must be OMITTED (like CryptoCompare's rows elsewhere in this repo), + // not zeroed -- a literal `rank: 0` would sort every bStock ahead of + // Bitcoin in any ascending rank-ordered wallet list. + expect(prices[0].rank).toBeUndefined(); + }); + + it('skips assets without a TRADING symbol', async () => { + mockBinance({ + assets: [TSLAB], + trading: ['BTCUSDT'], + t24: [ + { symbol: 'BTCUSDT', lastPrice: '65222.00', priceChangePercent: '1.0', quoteVolume: '9' }, + ], + t7d: [], + }); + expect(await getBstockPrices()).toHaveLength(0); + }); + + it('serves last-known-good when a symbol disappears (CEX halt)', async () => { + mockBinance({ + assets: [TSLAB], + trading: ['TSLABUSDT', 'BTCUSDT'], + t24: [ + { symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '2.5', quoteVolume: '1000000' }, + { symbol: 'BTCUSDT', lastPrice: '65222.00', priceChangePercent: '1.0', quoteVolume: '9' }, + ], + t7d: [], + }); + await getBstockPrices(); + // Halt: ticker omits TSLABUSDT this round + mockBinance({ + assets: [TSLAB], + trading: ['TSLABUSDT', 'BTCUSDT'], + t24: [ + { symbol: 'BTCUSDT', lastPrice: '65000.00', priceChangePercent: '0.5', quoteVolume: '9' }, + ], + t7d: [], + }); + const prices = await getBstockPrices(); + expect(prices).toHaveLength(1); + expect(prices[0].rates.usd).toBeCloseTo(326.11); + }); + + // The BTC divisor is the one place an unusable upstream value can turn into + // Infinity/NaN in an emitted rate. A near-identical zero-divisor bug reached + // a transaction-signing path in the sibling `api` repo, so pin both shapes. + it('emits nothing rather than Infinity when BTCUSDT is missing from the batch', async () => { + mockBinance({ + assets: [TSLAB], + trading: ['TSLABUSDT', 'BTCUSDT'], + t24: [{ symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '2.5', quoteVolume: '1000000' }], + t7d: [{ symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '7.1', quoteVolume: '0' }], + }); + expect(await getBstockPrices()).toHaveLength(0); + }); + + it('emits nothing rather than Infinity when BTCUSDT is priced at zero', async () => { + mockBinance({ + assets: [TSLAB], + trading: ['TSLABUSDT', 'BTCUSDT'], + t24: [ + { symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '2.5', quoteVolume: '1000000' }, + { symbol: 'BTCUSDT', lastPrice: '0.00000000', priceChangePercent: '0', quoteVolume: '0' }, + ], + t7d: [{ symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '7.1', quoteVolume: '0' }], + }); + expect(await getBstockPrices()).toHaveLength(0); + }); + + it('isolates a bad ticker to its own asset, leaving siblings in the batch priced', async () => { + const NVDAB = { assetCode: 'NVDAB', assetName: 'Nvidia', caList: [{ network: 'BSC', ca: '0xabcd' }] }; + mockBinance({ + assets: [TSLAB, NVDAB], + trading: ['TSLABUSDT', 'NVDABUSDT', 'BTCUSDT'], + t24: [ + // TSLAB halted (present, zeroed); NVDAB healthy. + { symbol: 'TSLABUSDT', lastPrice: '0.00000000', priceChangePercent: '0', quoteVolume: '0' }, + { symbol: 'NVDABUSDT', lastPrice: '120.00', priceChangePercent: '3.0', quoteVolume: '2000' }, + { symbol: 'BTCUSDT', lastPrice: '65222.00', priceChangePercent: '1.0', quoteVolume: '9' }, + ], + t7d: [{ symbol: 'NVDABUSDT', lastPrice: '120.00', priceChangePercent: '4.0', quoteVolume: '0' }], + }); + const prices = await getBstockPrices(); + expect(prices.map((p) => p.id)).toEqual(['bstock-nvdab']); + expect(prices[0].rates.usd).toBeCloseTo(120); + }); + + it('emits nothing at all when the feature is disabled', async () => { + const config = await import('../config'); + const original = config.default.bStocksEnabled; + config.default.bStocksEnabled = false; + try { + mockBinance({ + assets: [TSLAB], trading: ['TSLABUSDT', 'BTCUSDT'], t24: [], t7d: [], + }); + expect(await getBstockPrices()).toHaveLength(0); + } finally { + config.default.bStocksEnabled = original; + } + }); + + // Finding 3: a total Binance outage must be observable (not reported as a + // healthy service quietly serving frozen prices forever), and getBstockPrices + // must never reject even when every underlying Binance call fails -- every + // failure inside getTokenisedAssets/getTradingSymbols/getTicker24h/getTicker7d + // is already caught internally (see binance.ts), so a total outage looks + // like `{assets: [], trading: new Set(), t24: [], t7d: []}` from here. + it('marks the service degraded without throwing when a total outage prices nothing fresh, while still serving last-known-good within the staleness bound', async () => { + mockBinance({ + assets: [TSLAB], + trading: ['TSLABUSDT', 'BTCUSDT'], + t24: [ + { symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '2.5', quoteVolume: '1000000' }, + { symbol: 'BTCUSDT', lastPrice: '65222.00', priceChangePercent: '1.0', quoteVolume: '9' }, + ], + t7d: [{ symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '7.1', quoteVolume: '0' }], + }); + await getBstockPrices(); + expect(isBstocksDegraded()).toBe(false); + + // Total outage: every Binance call resolves the way the real client does + // after internally catching a network failure -- empty, never rejecting. + mockBinance({ + assets: [], trading: [], t24: [], t7d: [], + }); + await expect(getBstockPrices()).resolves.toHaveLength(1); // stale TSLAB still served + expect(isBstocksDegraded()).toBe(true); + }); + + it('drops a last-known-good entry once it exceeds the configured staleness bound, rather than serving it forever', async () => { + const config = (await import('../config')).default; + const dateSpy = jest.spyOn(Date, 'now'); + try { + dateSpy.mockReturnValue(1_000_000); + mockBinance({ + assets: [TSLAB], + trading: ['TSLABUSDT', 'BTCUSDT'], + t24: [ + { symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '2.5', quoteVolume: '1000000' }, + { symbol: 'BTCUSDT', lastPrice: '65222.00', priceChangePercent: '1.0', quoteVolume: '9' }, + ], + t7d: [{ symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '7.1', quoteVolume: '0' }], + }); + await getBstockPrices(); + + // Still well within the bound: kept. + dateSpy.mockReturnValue(1_000_000 + config.bstocksLastGoodMaxAgeMs - 1); + mockBinance({ + assets: [], trading: [], t24: [], t7d: [], + }); + expect(await getBstockPrices()).toHaveLength(1); + + // Past the bound: a total outage no longer serves the ancient price. + dateSpy.mockReturnValue(1_000_000 + config.bstocksLastGoodMaxAgeMs + 1); + mockBinance({ + assets: [], trading: [], t24: [], t7d: [], + }); + expect(await getBstockPrices()).toHaveLength(0); + } finally { + dateSpy.mockRestore(); + } + }); + + // Finding 7: the cross-repo id/provider contract (see the module doc) was + // only pinned against a single asset. Assert it holds as a property across + // a multi-asset fixture. + it('holds the coingecko-provider / bstock- id contract across a multi-asset fixture', async () => { + const codes = ['TSLAB', 'NVDAB', 'MSTRB', 'GOOGLB', 'AMZNB']; + const assets = codes.map((code) => ({ + assetCode: code, assetName: code, caList: [{ network: 'BSC', ca: '0xabc' }], + })); + const trading = [...codes.map((c) => `${c}USDT`), 'BTCUSDT']; + const t24 = [ + ...codes.map((code, i) => ({ + symbol: `${code}USDT`, lastPrice: `${100 + i}`, priceChangePercent: '1.0', quoteVolume: '1000', + })), + { symbol: 'BTCUSDT', lastPrice: '65222.00', priceChangePercent: '1.0', quoteVolume: '9' }, + ]; + const t7d = codes.map((code) => ({ + symbol: `${code}USDT`, lastPrice: '100', priceChangePercent: '2.0', quoteVolume: '1000', + })); + mockBinance({ + assets, trading, t24, t7d, + }); + + const prices = await getBstockPrices(); + expect(prices).toHaveLength(codes.length); + prices.forEach((p) => { + expect(p.provider).toBe('coingecko'); + expect(p.id).toMatch(/^bstock-[a-z0-9]+$/); + }); + }); +}); + +describe('bStocks degradation signalling and last-good snapshot', () => { + beforeEach(() => _clearLastGoodForTests()); + + // Regression guard: the 10s race in zelcoreRatesV2 used to resolve to [] on + // timeout. bStock rows carry provider "coingecko" but their failure is + // reported under errors.binance, so apiServices' provider carry-forward can + // never protect them -- every bStock would vanish from /v2/rates while the + // wallet showed $0 with no banner. The snapshot below is what the timeout + // branch serves instead. + it('exposes a last-good snapshot without touching Binance', async () => { + mockBinance({ + assets: [TSLAB], + trading: ['TSLABUSDT', 'BTCUSDT'], + t24: [ + { symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '2.5', quoteVolume: '1000000' }, + { symbol: 'BTCUSDT', lastPrice: '65222.00', priceChangePercent: '1.0', quoteVolume: '9' }, + ], + t7d: [{ symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '7.1', quoteVolume: '0' }], + }); + await getBstockPrices(); + + const snapshot = getLastGoodBstockPrices(); + expect(snapshot).toHaveLength(1); + expect(snapshot[0].id).toBe('bstock-tslab'); + expect(snapshot[0].provider).toBe('coingecko'); + expect(snapshot[0].rates.usd).toBeCloseTo(326.11); + }); + + it('reports degraded on a cold start with no last-good data at all', async () => { + // A fresh deploy while Binance is down has nothing cached. Requiring + // lastGood to be non-empty would report a healthy service serving zero + // bStocks -- silent, and exactly when someone needs to know. + mockBinance({ + assets: [], trading: [], t24: [], t7d: [], + }); + expect(await getBstockPrices()).toHaveLength(0); + expect(isBstocksDegraded()).toBe(true); + }); +}); diff --git a/tests/bstocksStaleness.spec.ts b/tests/bstocksStaleness.spec.ts new file mode 100644 index 0000000..e11a947 --- /dev/null +++ b/tests/bstocksStaleness.spec.ts @@ -0,0 +1,109 @@ +import type { AxiosResponse } from 'axios'; + +/** + * Staleness accounting across the provider/assembler seam. + * + * These tests drive the REAL Binance provider and mock only the HTTP layer. + * tests/bstocks.spec.ts mocks the `Binance` class wholesale, which hides the + * interaction pinned here: the provider backfills a symbol's last-known-good + * ticker into the batch it returns, and the assembler cannot tell that from a + * live quote unless it asks how old the price is. + * + * The scenario is a ticker-endpoint outage (Binance rate-limits the heavy + * `/ticker` routes with a 418/429 long before `exchangeInfo` stops answering), + * so the symbol keeps its TRADING status and stays in the assembler's + * `tradable` set while nothing prices live. + */ + +const DAY = 24 * 60 * 60 * 1000; + +const axiosResponse = (data: T): AxiosResponse => ({ + data, + status: 200, + statusText: 'OK', + headers: {}, + config: {} as AxiosResponse['config'], +}); + +const TSLAB = { assetCode: 'TSLAB', assetName: 'Tesla (bStocks)', caList: [{ network: 'BSC', ca: '0x5b19' }] }; + +let tickersDown = false; + +function route(url: string): Promise> { + if (url.includes('get-tokenised-asset')) return Promise.resolve(axiosResponse({ data: [TSLAB] })); + if (url.includes('exchangeInfo')) { + return Promise.resolve(axiosResponse({ + symbols: [ + { symbol: 'TSLABUSDT', status: 'TRADING' }, + { symbol: 'BTCUSDT', status: 'TRADING' }, + ], + })); + } + if (tickersDown) return Promise.reject(new Error('418 rate limited')); + if (url.includes('windowSize=7d')) { + return Promise.resolve(axiosResponse([ + { symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '7.1', quoteVolume: '0' }, + ])); + } + return Promise.resolve(axiosResponse([ + { symbol: 'TSLABUSDT', lastPrice: '326.11', priceChangePercent: '2.5', quoteVolume: '1000000' }, + { symbol: 'BTCUSDT', lastPrice: '65222.00', priceChangePercent: '1.0', quoteVolume: '9' }, + ])); +} + +/** + * Both the Binance singleton's caches and the assembler's last-known-good map + * are module-level state, so each test gets its own module registry. + */ +async function loadIsolated() { + jest.resetModules(); + const { AxiosWrapper } = await import('../src/lib/axios'); + jest.spyOn(AxiosWrapper.prototype, 'get').mockImplementation(route as never); + return import('../src/services/bstocks'); +} + +describe('bStocks staleness accounting', () => { + beforeEach(() => { + // `lru-cache` reads the clock from `performance.now`, which Jest's fake + // timers leave alone; bridge it to the fake `Date` so the provider's 60s + // quote cache and 1h universe cache expire as the test advances time. + jest.useFakeTimers({ doNotFake: ['performance'] }); + jest.spyOn(performance, 'now').mockImplementation(() => Date.now()); + tickersDown = false; + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.useRealTimers(); + }); + + it('reports degraded when every symbol is served from the provider carry-forward', async () => { + const { getBstockPrices, isBstocksDegraded } = await loadIsolated(); + + await getBstockPrices(); + expect(isBstocksDegraded()).toBe(false); + + tickersDown = true; + jest.advanceTimersByTime(10 * 60 * 1000); + const prices = await getBstockPrices(); + + // Still served -- that is what last-known-good is for ... + expect(prices.map((p) => p.id)).toEqual(['bstock-tslab']); + // ... but nothing priced live this run, so the outage has to be visible. + expect(isBstocksDegraded()).toBe(true); + }); + + it('expires a carried price once it passes the staleness bound', async () => { + const { getBstockPrices } = await loadIsolated(); + + await getBstockPrices(); // priced live at 326.11 + + tickersDown = true; + jest.advanceTimersByTime(4 * DAY); + await getBstockPrices(); // carried, four days stale -- must not reset the clock + jest.advanceTimersByTime(4 * DAY); + const prices = await getBstockPrices(); // eight days since the last live quote + + expect(prices).toEqual([]); + }); +}); diff --git a/tests/config.spec.ts b/tests/config.spec.ts new file mode 100644 index 0000000..8e340aa --- /dev/null +++ b/tests/config.spec.ts @@ -0,0 +1,39 @@ +/** + * Finding 5: bStocksEnabled must be overridable via env var (BSTOCKS_ENABLED) + * without a code change + redeploy. Each test re-imports the config module + * fresh (via jest.resetModules) so the env var is picked up at module-load + * time, mirroring how the real process reads it once on startup. + */ +describe('config.bStocksEnabled env override', () => { + const ORIGINAL_ENV = process.env.BSTOCKS_ENABLED; + + afterEach(() => { + if (ORIGINAL_ENV === undefined) { + delete process.env.BSTOCKS_ENABLED; + } else { + process.env.BSTOCKS_ENABLED = ORIGINAL_ENV; + } + jest.resetModules(); + }); + + it('defaults to enabled when BSTOCKS_ENABLED is unset', async () => { + delete process.env.BSTOCKS_ENABLED; + jest.resetModules(); + const config = (await import('../config')).default; + expect(config.bStocksEnabled).toBe(true); + }); + + it('disables via BSTOCKS_ENABLED=false without a code change', async () => { + process.env.BSTOCKS_ENABLED = 'false'; + jest.resetModules(); + const config = (await import('../config')).default; + expect(config.bStocksEnabled).toBe(false); + }); + + it('treats any other value (e.g. a typo) as enabled, matching the !== "false" contract', async () => { + process.env.BSTOCKS_ENABLED = 'no'; + jest.resetModules(); + const config = (await import('../config')).default; + expect(config.bStocksEnabled).toBe(true); + }); +}); diff --git a/tests/mergeCrypto.spec.ts b/tests/mergeCrypto.spec.ts new file mode 100644 index 0000000..ded9850 --- /dev/null +++ b/tests/mergeCrypto.spec.ts @@ -0,0 +1,143 @@ +import { mergeDeep, replaceCryptoByKey } from '../src/lib/objects'; +import { CryptoPrice } from '../src/types'; + +describe('replaceCryptoByKey', () => { + it('replaces entries by provider-id key, not by index', () => { + const source = [ + { id: 'bstock-tslab', provider: 'coingecko', rates: { btc: 0.005 } }, + { id: 'bitcoin', provider: 'coingecko', rates: { btc: 1.0001 } }, + ]; + const merged = replaceCryptoByKey(source); + expect(merged).toHaveLength(2); + expect(merged.find((e) => e.id === 'bitcoin')!.rates.btc).toBe(1.0001); + expect(merged.find((e) => e.id === 'stale')).toBeUndefined(); // anything absent from the fresh array is dropped, not carried + }); +}); + +/** + * Regression coverage for the pre-existing positional-merge bug in + * `mergeDeep`, which `apiServices.ts` used to use for the crypto array too. + * + * `processed` in `zelcoreRatesV2.getAll()` is built by concatenating four + * independent provider blocks (coingecko, cryptocompare, livecoinwatch, and + * now bstocks), each wrapped in its own try/catch. If any block throws or an + * upstream API returns a different number of rows than last cycle -- both + * routine, expected occurrences, not edge cases -- the total array length and + * the identity of "whatever happens to be at index N" shift between refresh + * cycles. `mergeDeep` merges purely by array index, so: + * + * 1. "Frankenstein" records: entry N from the OLD cycle gets deep-merged + * with entry N from the NEW cycle. Fields present on both are correctly + * overwritten (id, provider, rates), but fields present only on the OLD + * entry's shape (e.g. `rank`/`change7d`, which CoinGecko sends but + * CryptoCompare does not) survive untouched -- a CryptoCompare coin ends + * up wearing a stale CoinGecko coin's rank. + * 2. Stale tails: when the NEW array is shorter than the OLD one, + * `mergeDeep`'s `source.forEach` never visits the trailing OLD indices, + * so those entries -- which the new fetch no longer produced at all -- + * persist in the output forever with frozen, increasingly stale data. + */ +describe('positional-merge bug (apiServices.ts crypto merge)', () => { + // Cycle 1 result: three coingecko-shaped entries (has `rank`/`change7d`). + const target: CryptoPrice[] = [ + { + id: 'bitcoin', provider: 'coingecko', rates: { btc: 1, usd: 65000 }, supply: 1, volume: 1, change24h: 1, market: 1, rank: 1, total_supply: 21000000, change7d: 1, + }, + { + id: 'ethereum', provider: 'coingecko', rates: { btc: 0.05, usd: 3200 }, supply: 1, volume: 1, change24h: 1, market: 1, rank: 2, total_supply: 1, change7d: 1, + }, + { + id: 'litecoin', provider: 'coingecko', rates: { btc: 0.002, usd: 130 }, supply: 1, volume: 1, change24h: 1, market: 1, rank: 3, total_supply: 1, change7d: 1, + }, + ]; + // Cycle 2: CoinGecko's block threw this cycle (ethereum/litecoin absent + // entirely), leaving only a single CryptoCompare-shaped entry (no + // `rank`/`change7d`) landing at index 0. + const source: CryptoPrice[] = [ + { + id: 'CONI', provider: 'cryptocompare', rates: { btc: 0.00001, usd: 0.5 }, supply: 1, volume: 1, change24h: 1, market: 1, total_supply: 1, + }, + ]; + + it('mergeDeep (pre-existing, still used for fiat/rates/marketsUSD) corrupts: frankenstein fields + stale tail survive', () => { + const merged = mergeDeep(JSON.parse(JSON.stringify(target)), source) as CryptoPrice[]; + const slot0 = merged.find((e) => e.provider === 'cryptocompare'); + // WRONG: CONI has no rank of its own -- this is bitcoin's stale rank, + // left over because CryptoCompare's shape doesn't carry a `rank` key for + // mergeDeep to overwrite it with. + expect(slot0?.id).toBe('CONI'); + expect(slot0?.rank).toBe(1); + // WRONG: ethereum/litecoin were not part of this cycle's fetch at all, + // but the stale tail (indices 1, 2) was never touched by the positional + // merge, so they survive in the output indefinitely. + expect(merged).toHaveLength(3); + expect(merged.find((e) => e.id === 'litecoin')).toBeDefined(); + }); + + it('replaceCryptoByKey (fixed) replaces wholesale: no stale fields, no stale tail', () => { + const merged = replaceCryptoByKey(source); + expect(merged).toHaveLength(1); + const [only] = merged; + expect(only.id).toBe('CONI'); + expect(only.rank).toBeUndefined(); // no bitcoin leftover + expect(merged.find((e: CryptoPrice) => e.id === 'litecoin')).toBeUndefined(); // dropped, not frozen + }); +}); + +/** + * For the common case the merge runs under every 30s -- a fresh cycle with + * the same providers succeeding, same ids, same order, same shapes -- the + * key-based merge must produce output identical to the old positional one so + * existing consumers see no behavioural change. + */ +describe('replaceCryptoByKey vs mergeDeep -- identical for well-ordered input', () => { + it('produces the same array for a normal, non-corrupting refresh', () => { + const target: CryptoPrice[] = [ + { + id: 'bitcoin', provider: 'coingecko', rates: { btc: 1, usd: 64000 }, supply: 1, volume: 1, change24h: 1, market: 1, rank: 1, total_supply: 21000000, change7d: 1, + }, + { + id: 'ethereum', provider: 'coingecko', rates: { btc: 0.05, usd: 3100 }, supply: 1, volume: 1, change24h: 1, market: 1, rank: 2, total_supply: 1, change7d: 1, + }, + ]; + const source: CryptoPrice[] = [ + { + id: 'bitcoin', provider: 'coingecko', rates: { btc: 1, usd: 65000 }, supply: 1, volume: 1, change24h: 1.2, market: 1, rank: 1, total_supply: 21000000, change7d: 1.2, + }, + { + id: 'ethereum', provider: 'coingecko', rates: { btc: 0.05, usd: 3200 }, supply: 1, volume: 1, change24h: 1.2, market: 1, rank: 2, total_supply: 1, change7d: 1.2, + }, + ]; + const viaMergeDeep = mergeDeep(JSON.parse(JSON.stringify(target)), source); + const viaKeyMerge = replaceCryptoByKey(source); + expect(viaKeyMerge).toEqual(viaMergeDeep); + }); +}); + +describe('replaceCryptoByKey -- the key is provider AND id, last write wins', () => { + // A mutation run showed `return [...source]` passed every earlier test here: + // none of them pinned that provider is part of the key, that duplicates + // collapse, or that the LAST duplicate wins. These two do. + it('keeps the same id under two different providers as separate entries', () => { + const out = replaceCryptoByKey([ + { id: 'bitcoin', provider: 'coingecko', rates: { usd: 100 } }, + { id: 'bitcoin', provider: 'cryptocompare', rates: { usd: 101 } }, + ] as never); + expect(out).toHaveLength(2); + expect(out.map((e) => (e as { provider: string }).provider).sort()) + .toEqual(['coingecko', 'cryptocompare']); + }); + + it('collapses a repeated provider+id to one entry carrying the LAST value', () => { + const out = replaceCryptoByKey([ + { id: 'bitcoin', provider: 'coingecko', rates: { usd: 100 } }, + { id: 'ethereum', provider: 'coingecko', rates: { usd: 5 } }, + { id: 'bitcoin', provider: 'coingecko', rates: { usd: 999 } }, + ] as never); + expect(out).toHaveLength(2); + const btc = out.find((e) => (e as { id: string }).id === 'bitcoin') as unknown as { rates: { usd: number } }; + expect(btc.rates.usd).toBe(999); + // ...and it stays at the first occurrence's position, so ordering is stable. + expect((out[0] as { id: string }).id).toBe('bitcoin'); + }); +}); diff --git a/tests/productionCompareMarkets.spec.ts b/tests/productionCompareMarkets.spec.ts index 86d99ff..f01eefe 100644 --- a/tests/productionCompareMarkets.spec.ts +++ b/tests/productionCompareMarkets.spec.ts @@ -17,17 +17,6 @@ const isWithinRange = (prodMarket: number, localMarket: number): boolean => { return diff <= maxDiff; }; const AVOID = ['TOK', 'GUSD']; -/** - * Helper function to convert the API response to a dictionary with 'code' as key and 'rate' as value. - */ -const convertArrayToMap = (data: Array<{ code: string, name: string, rate: number }>): Record => { - const map: Record = {}; - data.forEach((entry) => { - map[entry.code] = entry.rate; - }); - return map; -}; - describe('Crypto rates comparison between production and localhost', () => { let prodMarkets: Record>; let localMarkets: Record>; @@ -35,12 +24,11 @@ describe('Crypto rates comparison between production and localhost', () => { beforeAll(async () => { // Fetch production rates const prodResponse = await axios.get(PRODUCTION_URL); - prodMarkets = prodResponse.data[0]; // Assuming the rates data is in the first element of the array - + prodMarkets = prodResponse.data[0]; // Assuming the rates data is in the first element of the array + // Fetch localhost rates const localResponse = await axios.get(LOCAL_URL); - localMarkets = localResponse.data[0]; // Assuming the rates data is in the first element of the array - + localMarkets = localResponse.data[0]; // Assuming the rates data is in the first element of the array }); test('All crypto codes should be present in both production and localhost', () => { @@ -57,7 +45,6 @@ describe('Crypto rates comparison between production and localhost', () => { expect(localMarkets[key]).toHaveProperty(prodKey); }); }); - }); test('All rates should be within a reasonable range', () => { @@ -80,7 +67,6 @@ describe('Crypto rates comparison between production and localhost', () => { }); } }); - }); // Ensure the rates are within the allowed range expect(diffs).toEqual([]); diff --git a/tests/productionCompareRates.spec.ts b/tests/productionCompareRates.spec.ts index 6073623..7939ea8 100644 --- a/tests/productionCompareRates.spec.ts +++ b/tests/productionCompareRates.spec.ts @@ -37,11 +37,11 @@ describe('Crypto rates comparison between production and localhost', () => { beforeAll(async () => { // Fetch production rates const prodResponse = await axios.get(PRODUCTION_URL); - const prodData = prodResponse.data[0]; // Assuming the rates data is in the first element of the array - + const [prodData] = prodResponse.data; // the rates data is the first element of the array + // Fetch localhost rates const localResponse = await axios.get(LOCAL_URL); - const localData = localResponse.data[0]; // Assuming the rates data is in the first element of the array + const [localData] = localResponse.data; // the rates data is the first element of the array // Convert the array of rates into a map with 'code' as key and 'rate' as value prodRates = convertArrayToMap(prodData); @@ -76,7 +76,6 @@ describe('Crypto rates comparison between production and localhost', () => { prodKeys.forEach((key) => { const prodRate = prodRates[key]; const localRate = localRates[key]; - const diffs = []; if (!isWithinRange(prodRate, localRate) && !AVOID.includes(key)) { diffs.push({ code: key, prodRate, localRate }); } diff --git a/tests/serviceRefresher.spec.ts b/tests/serviceRefresher.spec.ts new file mode 100644 index 0000000..9966855 --- /dev/null +++ b/tests/serviceRefresher.spec.ts @@ -0,0 +1,148 @@ +import type { Response, Request } from 'express'; +import type { CryptoPrice, PricesResponse } from '../src/types'; + +jest.mock('../src/services/zelcoreRates', () => ({ + __esModule: true, + default: { getAll: jest.fn() }, +})); +jest.mock('../src/services/zelcoreMarketsUSD', () => ({ + __esModule: true, + default: { getAll: jest.fn() }, +})); +jest.mock('../src/services/zelcoreRatesV2', () => ({ + __esModule: true, + default: { getAll: jest.fn() }, +})); + +// eslint-disable-next-line import/first +import zelcoreRates from '../src/services/zelcoreRates'; +// eslint-disable-next-line import/first +import zelcoreMarketsUSD from '../src/services/zelcoreMarketsUSD'; +// eslint-disable-next-line import/first +import zelcoreRatesV2 from '../src/services/zelcoreRatesV2'; +// eslint-disable-next-line import/first +import { serviceRefresher, getRatesV2 } from '../src/services/apiServices'; + +const mockedRates = zelcoreRates as jest.Mocked; +const mockedMarkets = zelcoreMarketsUSD as jest.Mocked; +const mockedRatesV2 = zelcoreRatesV2 as jest.Mocked; + +function makeCoins(provider: string, count: number): CryptoPrice[] { + return Array.from({ length: count }, (_, i) => ({ + id: `${provider}-coin-${i}`, + provider, + rates: { usd: 1, btc: 0.00001 }, + supply: 1, + volume: 1, + change24h: 1, + market: 1, + })); +} + +function makeFiat(count: number) { + return Array.from({ length: count }, (_, i) => ({ code: `C${i}`, name: `Currency ${i}`, rate: 1 })); +} + +/** Flushes a chain of already-resolved microtasks (the sequential `await`s + * inside serviceRefresher) without advancing the fake `delay(30s)` timer that + * would otherwise trigger serviceRefresher's own infinite recursion. */ +async function flush(steps = 25) { + for (let i = 0; i < steps; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Promise.resolve(); + } +} + +function captureRatesV2(): PricesResponse { + const json = jest.fn(); + getRatesV2({} as Request, { json } as unknown as Response); + return json.mock.calls[0][0] as PricesResponse; +} + +// Finding 2 + finding 7 bullet 1: serviceRefresher had zero coverage, so the +// >300 floor's interaction with replaceCryptoByKey (a small-provider outage +// silently truncating /v2/rates) went unverified. +describe('serviceRefresher — small-provider-outage carry-forward (finding 2)', () => { + beforeEach(() => { + jest.useFakeTimers(); + mockedRates.getAll.mockResolvedValue([[], {}, { errors: {} }] as never); + mockedMarkets.getAll.mockResolvedValue([{}, { errors: {} }] as never); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); + }); + + it('carries forward only a failed provider\'s rows and lets fresh data win everywhere else', async () => { + // Cycle 1: every provider healthy. 320 + 39 + 2 = 361 rows, well clear of + // the >300 floor. + mockedRatesV2.getAll.mockResolvedValueOnce({ + crypto: [ + ...makeCoins('coingecko', 320), + ...makeCoins('cryptocompare', 39), + ...makeCoins('livecoinwatch', 2), + ], + fiat: makeFiat(25), + errors: {}, + }); + serviceRefresher(); + await flush(); + + expect(captureRatesV2().crypto).toHaveLength(361); + + // Cycle 2: CryptoCompare fails outright (0 rows), but 320 + 2 = 322 still + // clears the >300 floor -- exactly the scenario the finding describes. + // Under the old positional merge those 39 rows survived as a stale tail; + // under a naive key-rebuild-from-fresh-alone they vanish instead. + mockedRatesV2.getAll.mockResolvedValueOnce({ + crypto: [ + ...makeCoins('coingecko', 320), + ...makeCoins('livecoinwatch', 2), + ], + fiat: makeFiat(25), + errors: { cryptocompare: true }, + }); + serviceRefresher(); + await flush(); + + const payload = captureRatesV2(); + expect(payload.crypto).toHaveLength(361); // 322 fresh + 39 carried + const cryptocompareRows = payload.crypto.filter((c) => c.provider === 'cryptocompare'); + expect(cryptocompareRows).toHaveLength(39); // carried from cycle 1, not dropped + expect(payload.errors?.cryptocompare).toBe(true); // still surfaced as an error + }); + + it('does not carry forward rows from a provider that is healthy this cycle, even if it shrank', async () => { + // Cycle 1: coingecko returns 320 rows including one specific coin. + mockedRatesV2.getAll.mockResolvedValueOnce({ + crypto: [ + { ...makeCoins('coingecko', 1)[0], id: 'delisted-coin' }, + ...makeCoins('coingecko', 319), + ...makeCoins('cryptocompare', 39), + ], + fiat: makeFiat(25), + errors: {}, + }); + serviceRefresher(); + await flush(); + + // Cycle 2: coingecko succeeds again (no error) but legitimately no longer + // returns 'delisted-coin' -- that coin should NOT be carried forward, + // because coingecko did not error this cycle. + mockedRatesV2.getAll.mockResolvedValueOnce({ + crypto: [ + ...makeCoins('coingecko', 319), + ...makeCoins('cryptocompare', 39), + ], + fiat: makeFiat(25), + errors: {}, + }); + serviceRefresher(); + await flush(); + + const payload = captureRatesV2(); + expect(payload.crypto.find((c) => c.id === 'delisted-coin')).toBeUndefined(); + expect(payload.crypto).toHaveLength(358); // 319 + 39, no carry-forward + }); +}); diff --git a/tests/zelcoreRatesV2.spec.ts b/tests/zelcoreRatesV2.spec.ts new file mode 100644 index 0000000..b980a10 --- /dev/null +++ b/tests/zelcoreRatesV2.spec.ts @@ -0,0 +1,105 @@ +// Explicit factories (rather than bare `jest.mock(path)` automocks) so Jest +// never has to load the real provider modules -- each one wires up a real +// AxiosWrapper/axios instance at import time, which is unrelated overhead +// these tests don't need and which was observed to leave the test process +// hanging past its normal exit. +jest.mock('../src/services/providers', () => ({ + __esModule: true, + CoinGecko: { getInstance: jest.fn() }, + BitPay: { getInstance: jest.fn() }, + CryptoCompare: { getInstance: jest.fn() }, + LiveCoinWatch: { getInstance: jest.fn() }, +})); +jest.mock('../src/services/bstocks', () => ({ + __esModule: true, + getBstockPrices: jest.fn(), + isBstocksDegraded: jest.fn(), +})); + +// eslint-disable-next-line import/first +import { CoinGecko, BitPay, CryptoCompare, LiveCoinWatch } from '../src/services/providers'; +// eslint-disable-next-line import/first +import { getBstockPrices, isBstocksDegraded } from '../src/services/bstocks'; +// eslint-disable-next-line import/first +import { getAll } from '../src/services/zelcoreRatesV2'; +// eslint-disable-next-line import/first +import type { CryptoPrice } from '../src/types'; + +const MockedCoinGecko = CoinGecko as jest.Mocked; +const MockedBitPay = BitPay as jest.Mocked; +const MockedCryptoCompare = CryptoCompare as jest.Mocked; +const MockedLiveCoinWatch = LiveCoinWatch as jest.Mocked; +const mockedGetBstockPrices = getBstockPrices as jest.MockedFunction; +const mockedIsBstocksDegraded = isBstocksDegraded as jest.MockedFunction; + +beforeEach(() => { + // All non-bStock providers fail fast (rejected) so these tests exercise + // only the bStocks leg of getAll() without needing real market fixtures. + MockedCoinGecko.getInstance.mockReturnValue({ + getExchangeRates: jest.fn().mockRejectedValue(new Error('down')), + } as never); + MockedBitPay.getInstance.mockReturnValue({ + getFiatRates: jest.fn().mockRejectedValue(new Error('down')), + } as never); + MockedCryptoCompare.getInstance.mockReturnValue({ + getMarketData: jest.fn().mockRejectedValue(new Error('down')), + } as never); + MockedLiveCoinWatch.getInstance.mockReturnValue({ + getExchangeRates: jest.fn().mockRejectedValue(new Error('down')), + } as never); +}); + +afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + jest.clearAllMocks(); +}); + +// Finding 4: a total Binance outage costs up to ~92s inside getBstockPrices() +// (AxiosWrapper's retry budget across two sequential calls plus a 3-chunk +// loop). getAll() awaited that serially, so it -- and therefore the refresh +// of all 364 non-bStock assets behind it -- would stall for just as long. +it('bounds the bStocks fetch to 10s so a hung/slow Binance outage cannot stall the whole refresh', async () => { + jest.useFakeTimers(); + mockedGetBstockPrices.mockImplementation(() => new Promise(() => {})); // never resolves + mockedIsBstocksDegraded.mockReturnValue(false); + + const resultPromise = getAll(); + let resolved: Awaited> | undefined; + resultPromise.then((r) => { resolved = r; }); + + await jest.advanceTimersByTimeAsync(10_000); + expect(resolved).toBeDefined(); + expect(resolved!.crypto.some((c) => c.id.startsWith('bstock-'))).toBe(false); +}); + +// Finding 3a: getBstockPrices() never rejects (every failure is caught +// internally), so a total outage must be surfaced via isBstocksDegraded() +// rather than the (unreachable) catch block. +it('sets errors.binance when bStocks degrades, even though getBstockPrices resolved without throwing', async () => { + const staleRow: CryptoPrice = { + id: 'bstock-tslab', + provider: 'coingecko', + rates: { usd: 300, btc: 0.005 }, + supply: 0, + volume: 0, + change24h: 0, + market: 0, + total_supply: 0, + change7d: 0, + }; + mockedGetBstockPrices.mockResolvedValue([staleRow]); + mockedIsBstocksDegraded.mockReturnValue(true); + + const result = await getAll(); + expect(result.errors?.binance).toBe(true); + expect(result.crypto.some((c) => c.id === 'bstock-tslab')).toBe(true); // stale rows still served +}); + +it('does not set errors.binance when bStocks is healthy', async () => { + mockedGetBstockPrices.mockResolvedValue([]); + mockedIsBstocksDegraded.mockReturnValue(false); + + const result = await getAll(); + expect(result.errors?.binance).toBeUndefined(); +}); diff --git a/typedoc.json b/typedoc.json index 86b52a6..cee1a4e 100644 --- a/typedoc.json +++ b/typedoc.json @@ -10,6 +10,7 @@ "excludePrivate": true, "excludeProtected": true, "includeVersion": true, + "gitRevision": "master", "compilerOptions": { "target": "es2020", "module": "commonjs",