diff --git a/.env.dist b/.env.dist index 5f4b337..129eb32 100644 --- a/.env.dist +++ b/.env.dist @@ -10,3 +10,4 @@ THREE_ROUTE_API_AUTH_TOKEN= REDIS_URL= ADMIN_USERNAME= ADMIN_PASSWORD= +COVALENT_API_KEY= diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 3a43b45..216061d 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -12,7 +12,7 @@ jobs: runs-on: macos-latest steps: - name: Clone repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Repository setup uses: ./.github/workflows/repository-setup diff --git a/.github/workflows/repository-setup/action.yml b/.github/workflows/repository-setup/action.yml index 48c6347..9b410c6 100644 --- a/.github/workflows/repository-setup/action.yml +++ b/.github/workflows/repository-setup/action.yml @@ -1,24 +1,21 @@ name: Repository setup -description: Setups Node 16.x & installs dependencies +description: Setups Node.js & installs dependencies runs: using: 'composite' steps: - - name: Setup Node 16.x - uses: actions/setup-node@v2 + - name: Setup Node 20.x + uses: actions/setup-node@v4 with: - node-version: '16.x' - - - name: Get branch name - uses: EthanSK/git-branch-name-action@v1 + node-version: '20.x' - name: Get yarn cache directory path id: yarn-cache-dir-path shell: bash run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/.gitignore b/.gitignore index 9fb2dbd..8e39db5 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,9 @@ dist # DynamoDB Local files .dynamodb/ +# Redis +*.rdb + # TernJS port file .tern-port diff --git a/Dockerfile b/Dockerfile index e4444be..1702c72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:14 +FROM node:20-alpine3.18 WORKDIR /usr/src/app COPY package.json yarn.lock ./ RUN yarn diff --git a/src/advertising/external-ads.ts b/src/advertising/external-ads.ts index 597ec44..f59a5cf 100644 --- a/src/advertising/external-ads.ts +++ b/src/advertising/external-ads.ts @@ -175,6 +175,7 @@ const PERMANENT_AD_PLACES_RULES_KEY = 'permanent_ad_places_rules'; const PERMANENT_NATIVE_AD_PLACES_RULES_KEY = 'permanent_native_ad_places_rules'; const REPLACE_ADS_URLS_BLACKLIST_KEY = 'replace_ads_urls_blacklist'; const ELEMENTS_TO_HIDE_OR_REMOVE_KEY = 'elements_to_hide_or_remove'; +const AD_PROVIDERS_CATEGORIES_KEY = 'ad_providers_categories'; export const adPlacesRulesMethods = objectStorageMethodsFactory(AD_PLACES_RULES_KEY, []); @@ -205,6 +206,10 @@ export const elementsToHideOrRemoveMethods = objectStorageMethodsFactory(AD_PROVIDERS_CATEGORIES_KEY, [ + 'crypto' +]); + export const getAdProvidersForAllSites = async () => redisClient.smembers(AD_PROVIDERS_ALL_SITES_KEY); export const addAdProvidersForAllSites = async (providers: string[]) => diff --git a/src/index.ts b/src/index.ts index 8b1d9ff..66a1771 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ import { getAliceBobEstimationPayload } from './utils/alice-bob/get-alice-bob-es import { getAliceBobOrderInfo } from './utils/alice-bob/get-alice-bob-order-info'; import { getAliceBobPairInfo } from './utils/alice-bob/get-alice-bob-pair-info'; import { getAliceBobPairsInfo } from './utils/alice-bob/get-alice-bob-pairs-info'; +import { btcExchangeRateProvider, tezExchangeRateProvider } from './utils/coingecko'; import { CodedError } from './utils/errors'; import { coinGeckoTokens } from './utils/gecko-tokens'; import { getExternalApiErrorPayload, isDefined, isNonEmptyString } from './utils/helpers'; @@ -37,7 +38,6 @@ import logger from './utils/logger'; import { getSignedMoonPayUrl } from './utils/moonpay/get-signed-moonpay-url'; import { getSigningNonce } from './utils/signing-nonce'; import SingleQueryDataProvider from './utils/SingleQueryDataProvider'; -import { tezExchangeRateProvider } from './utils/tezos'; import { getExchangeRates } from './utils/tokens'; const PINO_LOGGER = { @@ -175,6 +175,7 @@ app.get('/api/abtest', (_, res) => { }); app.get('/api/exchange-rates/tez', makeProviderDataRequestHandler(tezExchangeRateProvider)); +app.get('/api/exchange-rates/btc', makeProviderDataRequestHandler(btcExchangeRateProvider)); app.get('/api/exchange-rates', async (_req, res) => { const tokensExchangeRates = await getExchangeRates(); diff --git a/src/routers/slise-ad-rules/providers.ts b/src/routers/slise-ad-rules/providers.ts index 253df9d..5567565 100644 --- a/src/routers/slise-ad-rules/providers.ts +++ b/src/routers/slise-ad-rules/providers.ts @@ -9,7 +9,8 @@ import { adProvidersByDomainRulesMethods, AdProviderSelectorsRule, filterRules, - AdProvidersByDomainRule + AdProvidersByDomainRule, + adProvidersCategoriesMethods } from '../../advertising/external-ads'; import { basicAuth } from '../../middlewares/basic-auth.middleware'; import { addObjectStorageMethodsToRouter, withBodyValidation, withExceptionHandler } from '../../utils/express-helpers'; @@ -18,7 +19,8 @@ import { nonEmptyStringsListSchema, hostnamesListSchema, adProvidersByDomainsRulesDictionarySchema, - adProvidersDictionarySchema + adProvidersDictionarySchema, + adProvidersCategoriesDictionarySchema } from '../../utils/schemas'; /** @@ -116,6 +118,12 @@ import { * type: array * items: * $ref: '#/components/schemas/AdProvidersInputValue' + * AdProvidersCategoriesDictionary: + * type: object + * additionalProperties: + * type: array + * items: + * type: string */ export const adProvidersRouter = Router(); @@ -328,6 +336,78 @@ addObjectStorageMethodsToRouter(adProvidersRouter, { objectTransformFn: identity }); +/** + * @swagger + * /api/slise-ad-rules/providers/categories: + * get: + * summary: Get categories for providers + * tags: + * - Known ads providers + * responses: + * '200': + * description: Provider - categories dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AdProvidersCategoriesDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: Upsert categories for providers + * tags: + * - Known ads providers + * security: + * - basicAuth: [] + * requestBody: + * description: Provider - categories dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AdProvidersCategoriesDictionary' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Delete categories for providers + * tags: + * - Known ads providers + * security: + * - basicAuth: [] + * requestBody: + * description: List of provider IDs for which categories should be deleted + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +addObjectStorageMethodsToRouter(adProvidersRouter, { + path: '/categories', + methods: adProvidersCategoriesMethods, + keyName: 'providerId', + objectValidationSchema: adProvidersCategoriesDictionarySchema, + keysArrayValidationSchema: nonEmptyStringsListSchema, + successfulRemovalMessage: entriesCount => `${entriesCount} entries have been removed`, + valueTransformFn: identity, + objectTransformFn: identity +}); + /** * @swagger * /api/slise-ad-rules/providers/negative-selectors: diff --git a/src/utils/coingecko.ts b/src/utils/coingecko.ts index b4abf9c..089a2da 100644 --- a/src/utils/coingecko.ts +++ b/src/utils/coingecko.ts @@ -1,4 +1,7 @@ -import { range } from './helpers'; +import { AxiosError } from 'axios'; + +import { isDefined, range } from './helpers'; +import logger from './logger'; import { makeBuildQueryFn } from './makeBuildQueryFn'; import SingleQueryDataProvider from './SingleQueryDataProvider'; @@ -89,3 +92,29 @@ export const getMarketsBySymbols = async (symbols: string[]) => { return chunks.flat(); }; + +const createCoingeckoExchangeRateProvider = (tokenSymbol: string) => + new SingleQueryDataProvider(60000, async () => { + try { + const [market] = await getMarketsBySymbols([tokenSymbol]); + + return market.current_price; + } catch (e) { + if (!(e instanceof AxiosError)) { + logger.error(`Request for ${tokenSymbol} exchange rate failed with unknown error`); + } else if (isDefined(e.response) && isDefined(e.response.data)) { + logger.error( + `Request for ${tokenSymbol} exchange rate failed with status ${e.response.status} and message ${e.response.data}` + ); + } else if (isDefined(e.response) && isDefined(e.response.status)) { + logger.error(`Request for ${tokenSymbol} exchange rate failed with status ${e.response.status}`); + } else { + logger.error(`Request for ${tokenSymbol} exchange rate failed without response`); + } + + throw e; + } + }); + +export const tezExchangeRateProvider = createCoingeckoExchangeRateProvider('xtz'); +export const btcExchangeRateProvider = createCoingeckoExchangeRateProvider('btc'); diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 1acaa32..1877d31 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -8,7 +8,7 @@ interface CodedErrorForResponse { type StatusCodeNumber = (typeof StatusCodes)[keyof typeof StatusCodes]; export class CodedError extends Error { - constructor(public code: StatusCodeNumber | number, message: string, public errorCode?: string) { + constructor(public code: StatusCodeNumber, message: string, public errorCode?: string) { super(message); } diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index be62bcc..a5a1241 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -278,3 +278,8 @@ export const elementsToHideOrRemoveDictionarySchema = makeDictionarySchema( + nonEmptyStringSchema.clone().required(), + arraySchema().of(nonEmptyStringSchema.clone().required()).required() +).required(); diff --git a/src/utils/tezos.ts b/src/utils/tezos.ts index 2d098c6..5613fde 100644 --- a/src/utils/tezos.ts +++ b/src/utils/tezos.ts @@ -1,13 +1,8 @@ import { compose, MichelCodecPacker, Signer, TezosToolkit } from '@taquito/taquito'; import { tzip12 } from '@taquito/tzip12'; import { tzip16 } from '@taquito/tzip16'; -import { AxiosError } from 'axios'; import memoizee from 'memoizee'; -import { getMarketsBySymbols } from './coingecko'; -import { isDefined } from './helpers'; -import logger from './logger'; -import SingleQueryDataProvider from './SingleQueryDataProvider'; import { BcdTokenData } from './tzkt'; const RPC_URL = process.env.RPC_URL ?? 'https://mainnet-node.madfish.solutions'; @@ -54,30 +49,6 @@ export const getStorage = memoizee( { promise: true, maxAge: 30000 } ); -const getTezExchangeRate = async () => { - try { - const [xtzMarket] = await getMarketsBySymbols(['xtz']); - - return xtzMarket.current_price; - } catch (e) { - if (!(e instanceof AxiosError)) { - logger.error('Request for TEZ exchange rate failed with unknown error'); - } else if (isDefined(e.response) && isDefined(e.response.data)) { - logger.error( - `Request for TEZ exchange rate failed with status ${e.response.status} and message ${e.response.data}` - ); - } else if (isDefined(e.response) && isDefined(e.response.status)) { - logger.error(`Request for TEZ exchange rate failed with status ${e.response.status}`); - } else { - logger.error('Request for TEZ exchange rate failed without response'); - } - - throw e; - } -}; - -export const tezExchangeRateProvider = new SingleQueryDataProvider(60000, getTezExchangeRate); - export class MetadataParseError extends Error {} export const getTokenMetadata = memoizee( diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index c468e6d..64d63df 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -1,10 +1,10 @@ import { BigNumber } from 'bignumber.js'; import { redisClient } from '../redis'; +import { tezExchangeRateProvider } from './coingecko'; import { isDefined } from './helpers'; import logger from './logger'; import SingleQueryDataProvider, { SingleQueryDataProviderState } from './SingleQueryDataProvider'; -import { tezExchangeRateProvider } from './tezos'; import { getThreeRouteExchangeRates, getThreeRouteTokens,