From 4317481f64b83708c4287302c976b7df84d0647f Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Wed, 29 Apr 2026 13:40:57 +0300 Subject: [PATCH] chore: run yarn lint:fix --- apps/api-e2e/src/bff/example.spec.ts | 8 +- .../src/app/affiliateProgramExportPoller.ts | 57 +- apps/api/src/app/app.ts | 29 +- apps/api/src/app/config/affiliate.ts | 2 +- apps/api/src/app/data/poolInfo.ts | 22 +- apps/api/src/app/inversify.config.spec.ts | 85 +- apps/api/src/app/inversify.config.ts | 148 ++- apps/api/src/app/plugins/bffAuth.ts | 18 +- apps/api/src/app/plugins/bffCache.ts | 153 ++-- apps/api/src/app/plugins/caching.ts | 16 +- apps/api/src/app/plugins/cors.ts | 23 +- apps/api/src/app/plugins/env.ts | 16 +- apps/api/src/app/plugins/orm-analytics.ts | 40 +- apps/api/src/app/plugins/orm-repositories.ts | 61 +- apps/api/src/app/plugins/redis.ts | 12 +- apps/api/src/app/plugins/support.ts | 2 +- apps/api/src/app/plugins/swagger.ts | 42 +- .../accounts/__userAddress/balances/index.ts | 206 ++--- .../__chainId/address/__address/balances.ts | 81 +- .../slippageTolerance.ts | 102 +-- .../__chainId/simulation/simulateBundle.ts | 71 +- .../tokens/__tokenAddress/details.ts | 52 +- .../tokens/__tokenAddress/topHolders.ts | 54 +- .../tokens/__tokenAddress/usdPrice.ts | 54 +- .../app/routes/__chainId/tokens/details.ts | 69 +- .../src/app/routes/__chainId/yield/const.ts | 4 +- .../__chainId/yield/getPoolsAverageApr.ts | 67 +- .../routes/__chainId/yield/getPoolsInfo.ts | 65 +- .../src/app/routes/__chainId/yield/schemas.ts | 26 +- .../src/app/routes/__chainId/yield/types.ts | 2 +- .../src/app/routes/__chainId/yield/utils.ts | 2 +- apps/api/src/app/routes/about.ts | 53 +- .../routes/accounts/_account/notifications.ts | 62 +- .../affiliate/_address/affiliate.schemas.ts | 19 +- .../app/routes/affiliate/_address/index.ts | 208 ++--- .../_address/signatureVerification.spec.ts | 151 ++-- .../_address/signatureVerification.ts | 90 +- .../_address/affiliateStats.schemas.ts | 12 +- .../affiliate-stats/_address/index.ts | 51 +- .../affiliate/trader-stats/_address/index.ts | 51 +- .../_address/traderStats.schemas.ts | 12 +- apps/api/src/app/routes/examples/hello.ts | 14 +- apps/api/src/app/routes/hooks.schemas.ts | 10 +- apps/api/src/app/routes/hooks.ts | 75 +- .../src/app/routes/proxies/coingecko/index.ts | 49 +- .../src/app/routes/proxies/socket/index.ts | 55 +- .../src/app/routes/proxies/tokens/index.ts | 18 +- .../src/app/routes/ref-codes/_code/index.ts | 79 +- .../ref-codes/_code/refCodes.schemas.ts | 18 +- apps/api/src/app/routes/root.ts | 11 +- .../src/app/routes/tests/balances/index.ts | 21 +- apps/api/src/app/routes/twap/index.ts | 16 +- apps/api/src/app/schemas.ts | 15 +- apps/api/src/datasource.config.ts | 10 +- apps/api/src/main.ts | 30 +- apps/api/src/types/abstract-cache.d.ts | 4 +- apps/api/src/utils/cache.ts | 85 +- apps/api/src/utils/misc.ts | 2 +- .../notification-producer.spec.ts | 14 +- apps/notification-producer/src/main.ts | 127 ++- .../producers/cms/CmsNotificationProducer.ts | 84 +- .../ExpiredOrdersNotificationProducer.ts | 155 ++-- .../getExpiredOrderNotification.ts | 42 +- .../trade/TradeNotificationProducer.ts | 167 ++-- .../trade/fromTradeToNotification.ts | 62 +- .../producers/trade/getTradeNotifications.ts | 234 +++-- .../src/sendPush.test.ts | 32 +- .../src/utils/getNotificationSummary.ts | 41 +- apps/notification-producer/types.ts | 4 +- .../src/telegram/telegram.spec.ts | 14 +- apps/telegram/src/main.ts | 191 ++-- apps/twap-e2e/src/twap/twap.spec.ts | 12 +- apps/twap/datasource.config.ts | 12 +- apps/twap/src/app/app.spec.ts | 20 +- apps/twap/src/app/app.ts | 14 +- apps/twap/src/app/data/order.ts | 43 +- apps/twap/src/app/data/safeTx.ts | 6 +- apps/twap/src/app/data/wallet.ts | 8 +- apps/twap/src/app/orderbook/order.ts | 55 +- apps/twap/src/app/orderbook/settlement.ts | 21 +- apps/twap/src/app/orderbook/trade.ts | 21 +- apps/twap/src/app/plugins/env.ts | 34 +- apps/twap/src/app/plugins/orderbook.ts | 85 +- apps/twap/src/app/plugins/orm.ts | 34 +- apps/twap/src/app/plugins/sensible.ts | 10 +- apps/twap/src/app/plugins/swagger.ts | 20 +- .../_chainId/_walletAddress/orders/index.ts | 54 +- apps/twap/src/app/routes/root.ts | 11 +- apps/twap/src/app/types/order.ts | 40 +- apps/twap/src/app/utils/getApiBaseUrl.ts | 10 +- .../src/app/utils/getConditionalOrderId.ts | 75 +- apps/twap/src/main.ts | 22 +- .../1688063749511-initial-migration.ts | 2 +- .../src/migrations/1688063878937-migration.ts | 16 +- .../src/migrations/1688066336403-migration.ts | 20 +- .../src/migrations/1688472151788-migration.ts | 34 +- .../src/migrations/1688472763977-migration.ts | 53 +- .../src/migrations/1688554921688-migration.ts | 21 +- .../src/migrations/1688633087604-migration.ts | 16 +- libs/abis/src/index.ts | 4 +- libs/abis/vite.config.ts | 10 +- libs/notifications/src/index.ts | 60 +- libs/repositories/src/const.ts | 16 +- .../src/database/IndexerState.entity.ts | 19 +- libs/repositories/src/datasources/alchemy.ts | 15 +- libs/repositories/src/datasources/cms.ts | 24 +- .../src/datasources/coingecko.test.ts | 45 +- .../repositories/src/datasources/coingecko.ts | 37 +- libs/repositories/src/datasources/cowApi.ts | 26 +- .../repositories/src/datasources/ethplorer.ts | 6 +- libs/repositories/src/datasources/goldRush.ts | 8 +- libs/repositories/src/datasources/moralis.ts | 8 +- .../src/datasources/orderBookDbPool.ts | 46 +- .../src/datasources/orm/datasource.config.ts | 8 +- .../src/datasources/orm/postgresOrm.ts | 20 +- .../src/datasources/postgresPlain.ts | 21 +- libs/repositories/src/datasources/rabbitMq.ts | 10 +- libs/repositories/src/datasources/redis.ts | 6 +- libs/repositories/src/datasources/telegram.ts | 12 +- .../src/datasources/tenderlyApi.ts | 12 +- libs/repositories/src/datasources/viem.ts | 90 +- libs/repositories/src/index.ts | 112 +-- .../1745364046891-initial-migration.ts | 14 +- .../AffiliatesRepository.ts | 54 +- .../AffiliatesRepositoryCms.ts | 250 +++--- .../repos/CacheRepository/CacheRepository.ts | 8 +- .../CacheRepository/CacheRepositoryMemory.ts | 20 +- .../CacheRepository/CacheRepositoryRedis.ts | 16 +- .../DuneRepository/DuneRepository.spec.ts | 339 ++++--- .../repos/DuneRepository/DuneRepository.ts | 96 +- .../DuneRepository/DuneRepositoryImpl.ts | 170 ++-- .../repos/Erc20Repository/Erc20Repository.ts | 14 +- .../Erc20RepositoryCache.spec.ts | 105 +-- .../Erc20Repository/Erc20RepositoryCache.ts | 40 +- .../Erc20RepositoryFallback.spec.ts | 52 +- .../Erc20RepositoryFallback.ts | 17 +- .../Erc20RepositoryNative.spec.ts | 36 +- .../Erc20Repository/Erc20RepositoryNative.ts | 23 +- .../Erc20RepositoryViem.spec.ts | 95 +- .../Erc20Repository/Erc20RepositoryViem.ts | 73 +- .../ExpiredOrdersRepository.ts | 44 +- .../ExpiredOrdersRepositoryPostgres.ts | 45 +- .../expiredOrdersUtils.ts | 10 +- .../IndexerStateRepository.ts | 27 +- .../IndexerStateRepositoryOrm.ts | 41 +- .../IndexerStateRepositoryPostgres.ts | 27 +- .../OnChainPlacedOrdersRepository.ts | 4 +- .../OnChainPlacedOrdersRepositoryPostgres.ts | 49 +- .../OrdersAppDataRepository.ts | 4 +- .../OrdersAppDataRepositoryPostgres.ts | 115 +-- .../PushNotificationsRepository.ts | 155 ++-- .../PushSubscriptionsRepository.ts | 76 +- .../PushSubscriptionsRepositoryCms.ts | 212 ++--- .../SimulationRepository.ts | 35 +- .../SimulationRepositoryTenderly.ts | 187 ++-- .../SimulationrepositoryTenderly.test.ts | 188 ++-- .../SimulationRepository/tenderlyTypes.ts | 842 +++++++++--------- .../TokenBalancesRepository.ts | 16 +- .../TokenBalancesRepositoryAlchemy.ts | 116 ++- .../TokenBalancesRepositoryMoralis.ts | 99 +- .../TokenHolderRepository.ts | 20 +- .../TokenHolderRepositoryCache.spec.ts | 142 ++- .../TokenHolderRepositoryCache.ts | 72 +- .../TokenHolderRepositoryEthplorer.test.ts | 101 +-- .../TokenHolderRepositoryEthplorer.ts | 58 +- .../TokenHolderRepositoryFallback.spec.ts | 91 +- .../TokenHolderRepositoryFallback.ts | 23 +- .../TokenHolderRepositoryGoldRush.test.ts | 61 +- .../TokenHolderRepositoryGoldRush.ts | 85 +- .../TokenHolderRepositoryMoralis.spec.ts | 56 +- .../TokenHolderRepositoryMoralis.ts | 56 +- .../src/repos/UsdRepository/UsdRepository.ts | 50 +- .../UsdRepository/UsdRepositoryCache.spec.ts | 220 +++-- .../repos/UsdRepository/UsdRepositoryCache.ts | 96 +- .../UsdRepositoryCoingecko.test.ts | 202 ++--- .../UsdRepository/UsdRepositoryCoingecko.ts | 135 ++- .../UsdRepository/UsdRepositoryCow.spec.ts | 182 ++-- .../repos/UsdRepository/UsdRepositoryCow.ts | 110 +-- .../UsdRepositoryFallback.spec.ts | 188 ++-- .../UsdRepository/UsdRepositoryFallback.ts | 44 +- .../UserBalanceRepository.ts | 12 +- .../UserBalanceRepositoryCache.ts | 88 +- .../UserBalanceRepositoryViem.ts | 49 +- libs/repositories/src/utils/buildStateDiff.ts | 130 +-- libs/repositories/src/utils/bytesUtils.ts | 2 +- libs/repositories/src/utils/cache.ts | 4 +- libs/repositories/src/utils/chunkArray.ts | 8 +- libs/repositories/src/utils/coingeckoUtils.ts | 55 +- libs/repositories/src/utils/isDbEnabled.ts | 2 +- .../src/utils/throwIfUnsuccessful.ts | 16 +- libs/repositories/test/mock.ts | 44 +- .../AffiliateProgramExportService.config.ts | 6 +- .../AffiliateProgramExportService.ts | 20 +- .../AffiliateProgramExportServiceImpl.ts | 104 +-- .../AffiliateStatsService.config.ts | 26 +- .../AffiliateStatsService.ts | 54 +- .../AffiliateStatsService.types.ts | 17 +- .../AffiliateStatsService.utils.ts | 50 +- .../AffiliateStatsServiceImpl.ts | 149 ++-- .../BalanceTrackingService.ts | 58 +- .../BalanceTrackingServiceMain.ts | 286 +++--- .../services/src/HooksService/HooksService.ts | 65 +- .../src/HooksService/HooksServiceImpl.spec.ts | 180 ++-- .../src/HooksService/HooksServiceImpl.ts | 22 +- .../src/HooksService/utils/isHookData.ts | 42 +- libs/services/src/SSEService/SSEService.ts | 40 +- .../services/src/SSEService/SSEServiceMain.ts | 106 +-- .../SimulationService/SimulationService.ts | 13 +- .../src/SlippageService/SlippageService.ts | 57 +- .../SlippageServiceMain.spec.ts | 314 +++---- .../SlippageServiceMain.test.ts | 67 +- .../SlippageService/SlippageServiceMain.ts | 133 ++- .../TokenBalancesService.ts | 90 +- .../TokenDetailService/TokenDetailService.ts | 40 +- .../TokenHolderService/TokenHolderService.ts | 24 +- libs/services/src/UsdService/UsdService.ts | 18 +- libs/services/src/factories.ts | 117 +-- libs/services/src/index.ts | 36 +- .../services/src/utils/type-checking-utils.ts | 16 +- libs/shared/src/const.ts | 34 +- libs/shared/src/index.ts | 18 +- libs/shared/src/logger.ts | 4 +- libs/shared/src/transformers.ts | 8 +- libs/shared/src/types.ts | 4 +- libs/shared/src/utils/addresses.spec.ts | 45 +- libs/shared/src/utils/addresses.ts | 16 +- libs/shared/src/utils/doForever.ts | 60 +- libs/shared/src/utils/format.ts | 25 +- libs/shared/src/utils/logger.ts | 8 +- libs/shared/src/utils/misc.ts | 47 +- 230 files changed, 6025 insertions(+), 7871 deletions(-) diff --git a/apps/api-e2e/src/bff/example.spec.ts b/apps/api-e2e/src/bff/example.spec.ts index cfc1b155..ae9770db 100644 --- a/apps/api-e2e/src/bff/example.spec.ts +++ b/apps/api-e2e/src/bff/example.spec.ts @@ -1,7 +1,7 @@ -import axios from 'axios'; +import axios from 'axios' test('GET /example', async () => { - const res = await axios.get('/example'); + const res = await axios.get('/example') - expect(res.data).toEqual('this is an example'); -}); + expect(res.data).toEqual('this is an example') +}) diff --git a/apps/api/src/app/affiliateProgramExportPoller.ts b/apps/api/src/app/affiliateProgramExportPoller.ts index 013621f3..866a9ab4 100644 --- a/apps/api/src/app/affiliateProgramExportPoller.ts +++ b/apps/api/src/app/affiliateProgramExportPoller.ts @@ -1,44 +1,33 @@ -import { apiContainer } from './inversify.config'; -import { logger } from '@cowprotocol/shared'; -import { - isCmsEnabled, - isDuneEnabled, -} from '@cowprotocol/repositories'; +import { apiContainer } from './inversify.config' +import { logger } from '@cowprotocol/shared' +import { isCmsEnabled, isDuneEnabled } from '@cowprotocol/repositories' import { AffiliateProgramExportService, AffiliateProgramSignature, affiliateProgramExportServiceSymbol, -} from '@cowprotocol/services'; +} from '@cowprotocol/services' -const POLL_INTERVAL_MS = 5 * 60 * 1000; +const POLL_INTERVAL_MS = 5 * 60 * 1000 -let lastSignature: AffiliateProgramSignature | null = null; -let inFlight = false; +let lastSignature: AffiliateProgramSignature | null = null +let inFlight = false -export function startAffiliateProgramExportPoller(): - | (() => void) - | undefined { +export function startAffiliateProgramExportPoller(): (() => void) | undefined { if (!isCmsEnabled || !isDuneEnabled) { - logger.warn( - 'Affiliate export poller disabled (CMS or Dune not enabled).' - ); - return; + logger.warn('Affiliate export poller disabled (CMS or Dune not enabled).') + return } const run = async () => { if (inFlight) { - return; + return } - inFlight = true; + inFlight = true try { - const exportService = apiContainer.get( - affiliateProgramExportServiceSymbol - ); - const result = await exportService.exportAffiliateProgramDataIfChanged( - lastSignature - ); - lastSignature = result.result.signature; + const exportService = apiContainer.get(affiliateProgramExportServiceSymbol) + const result = await exportService.exportAffiliateProgramDataIfChanged(lastSignature) + lastSignature = result.result.signature if (result.uploaded) { logger.info( @@ -47,7 +36,7 @@ export function startAffiliateProgramExportPoller(): maxUpdatedAt: result.result.signature.maxUpdatedAt, }, 'Affiliate program export poller uploaded data' - ); + ) } else { logger.debug( { @@ -55,16 +44,16 @@ export function startAffiliateProgramExportPoller(): maxUpdatedAt: result.result.signature.maxUpdatedAt, }, 'Affiliate program export poller skipped (no change)' - ); + ) } } catch (error) { - logger.error({ error }, 'Affiliate program export poller failed'); + logger.error({ error }, 'Affiliate program export poller failed') } finally { - inFlight = false; + inFlight = false } - }; + } - void run(); - const intervalId = setInterval(run, POLL_INTERVAL_MS); - return () => clearInterval(intervalId); + void run() + const intervalId = setInterval(run, POLL_INTERVAL_MS) + return () => clearInterval(intervalId) } diff --git a/apps/api/src/app/app.ts b/apps/api/src/app/app.ts index 499f16c7..521490b5 100644 --- a/apps/api/src/app/app.ts +++ b/apps/api/src/app/app.ts @@ -1,25 +1,22 @@ -import 'reflect-metadata'; +import 'reflect-metadata' -import { join } from 'path'; -import AutoLoad, { AutoloadPluginOptions } from '@fastify/autoload'; -import { FastifyPluginAsync } from 'fastify'; +import { join } from 'path' +import AutoLoad, { AutoloadPluginOptions } from '@fastify/autoload' +import { FastifyPluginAsync } from 'fastify' export type AppOptions = { // Place your custom options for app below here. -} & Partial; +} & Partial // Pass --options via CLI arguments in command to enable these options. -const options: AppOptions = {}; +const options: AppOptions = {} -const app: FastifyPluginAsync = async ( - fastify, - opts -): Promise => { +const app: FastifyPluginAsync = async (fastify, opts): Promise => { // Place here your custom code! const appOpts = { ...opts, prefix: '/', - }; + } // Do not touch the following lines // This loads all plugins defined in plugins @@ -28,7 +25,7 @@ const app: FastifyPluginAsync = async ( void fastify.register(AutoLoad, { dir: join(__dirname, 'plugins'), options: appOpts, - }); + }) // This loads all plugins defined in routes // define your routes in one of these @@ -36,8 +33,8 @@ const app: FastifyPluginAsync = async ( dir: join(__dirname, 'routes'), options: appOpts, routeParams: true, - }); -}; + }) +} -export default app; -export { app, options }; +export default app +export { app, options } diff --git a/apps/api/src/app/config/affiliate.ts b/apps/api/src/app/config/affiliate.ts index cd82b4d9..308411d2 100644 --- a/apps/api/src/app/config/affiliate.ts +++ b/apps/api/src/app/config/affiliate.ts @@ -1 +1 @@ -export const AFFILIATE_CODE_REGEX = /^[A-Z0-9_-]{5,20}$/; +export const AFFILIATE_CODE_REGEX = /^[A-Z0-9_-]{5,20}$/ diff --git a/apps/api/src/app/data/poolInfo.ts b/apps/api/src/app/data/poolInfo.ts index d5860b21..8c5926c9 100644 --- a/apps/api/src/app/data/poolInfo.ts +++ b/apps/api/src/app/data/poolInfo.ts @@ -1,32 +1,28 @@ -import { - Column, - Entity, - PrimaryColumn -} from 'typeorm'; -import { bufferToString, stringToBuffer } from '@cowprotocol/shared'; +import { Column, Entity, PrimaryColumn } from 'typeorm' +import { bufferToString, stringToBuffer } from '@cowprotocol/shared' @Entity({ name: 'cow_amm_competitor_info', schema: 'public' }) export class PoolInfo { @PrimaryColumn('bytea', { transformer: { from: bufferToString, to: stringToBuffer }, }) - contract_address: string; + contract_address: string @Column('int') - chain_id: number; + chain_id: number @Column('varchar') - project: string; + project: string @Column('double precision') - apr: number; + apr: number @Column('double precision') - fee: number; + fee: number @Column('double precision') - tvl: number; + tvl: number @Column('double precision') - volume: number; + volume: number } diff --git a/apps/api/src/app/inversify.config.spec.ts b/apps/api/src/app/inversify.config.spec.ts index fad2cb26..8f92d8ee 100644 --- a/apps/api/src/app/inversify.config.spec.ts +++ b/apps/api/src/app/inversify.config.spec.ts @@ -1,12 +1,12 @@ -import 'reflect-metadata'; -import { decorate, injectable } from 'inversify'; +import 'reflect-metadata' +import { decorate, injectable } from 'inversify' -const symbol = (name: string): symbol => Symbol.for(name); -const emptyObject = (): Record => ({}); -const getter = () => jest.fn(emptyObject); +const symbol = (name: string): symbol => Symbol.for(name) +const emptyObject = (): Record => ({}) +const getter = () => jest.fn(emptyObject) -const affiliateStatsServiceSymbol = symbol('AffiliateStatsService'); -const duneRepositorySymbol = symbol('DuneRepository'); +const affiliateStatsServiceSymbol = symbol('AffiliateStatsService') +const duneRepositorySymbol = symbol('DuneRepository') const getters = { getAffiliatesRepository: getter(), @@ -20,7 +20,7 @@ const getters = { getTokenHolderRepository: getter(), getUserBalanceRepository: getter(), getUsdRepository: getter(), -}; +} const plainClasses = Object.fromEntries( [ @@ -49,19 +49,16 @@ const plainClasses = Object.fromEntries( 'UserBalanceRepository', 'BalanceTrackingService', ].map((name) => [name, class {}]) -); +) class InjectableStub {} -decorate(injectable(), InjectableStub); +decorate(injectable(), InjectableStub) class MockAffiliateStatsServiceImpl { - static instances: MockAffiliateStatsServiceImpl[] = []; + static instances: MockAffiliateStatsServiceImpl[] = [] - constructor( - public readonly duneRepository: unknown, - public readonly cacheTtlMs: number - ) { - MockAffiliateStatsServiceImpl.instances.push(this); + constructor(public readonly duneRepository: unknown, public readonly cacheTtlMs: number) { + MockAffiliateStatsServiceImpl.instances.push(this) } } @@ -88,7 +85,7 @@ const symbols = { usdRepositorySymbol: symbol('UsdRepository'), usdServiceSymbol: symbol('UsdService'), userBalanceRepositorySymbol: symbol('UserBalanceRepository'), -}; +} jest.mock('@cowprotocol/repositories', () => ({ ...plainClasses, @@ -96,12 +93,12 @@ jest.mock('@cowprotocol/repositories', () => ({ ...getters, isCmsEnabled: false, isDuneEnabled: true, -})); +})) jest.mock('@cowprotocol/shared', () => ({ Logger: plainClasses.Logger, logger: { warn: jest.fn() }, -})); +})) jest.mock('@cowprotocol/services', () => ({ ...plainClasses, @@ -116,42 +113,36 @@ jest.mock('@cowprotocol/services', () => ({ TokenDetailServiceMain: InjectableStub, TokenHolderServiceMain: InjectableStub, UsdServiceMain: InjectableStub, -})); +})) describe('getApiContainer', () => { - const originalTtl = process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS; + const originalTtl = process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS beforeEach(() => { - MockAffiliateStatsServiceImpl.instances = []; - process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS = '1234'; - jest.resetModules(); - }); + MockAffiliateStatsServiceImpl.instances = [] + process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS = '1234' + jest.resetModules() + }) afterAll(() => { if (originalTtl === undefined) { - delete process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS; - return; + delete process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS + return } - process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS = originalTtl; - }); + process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS = originalTtl + }) it('reuses the same affiliate stats service instance', async () => { - const { getApiContainer } = await import('./inversify.config'); - - const container = getApiContainer(); - const first = container.get( - affiliateStatsServiceSymbol - ); - const second = container.get( - affiliateStatsServiceSymbol - ); - - expect(first).toBe(second); - expect(MockAffiliateStatsServiceImpl.instances).toHaveLength(1); - expect(first.cacheTtlMs).toBe(1234); - expect(first.duneRepository).toBe( - getters.getDuneRepository.mock.results.at(-1)?.value - ); - }); -}); + const { getApiContainer } = await import('./inversify.config') + + const container = getApiContainer() + const first = container.get(affiliateStatsServiceSymbol) + const second = container.get(affiliateStatsServiceSymbol) + + expect(first).toBe(second) + expect(MockAffiliateStatsServiceImpl.instances).toHaveLength(1) + expect(first.cacheTtlMs).toBe(1234) + expect(first.duneRepository).toBe(getters.getDuneRepository.mock.results.at(-1)?.value) + }) +}) diff --git a/apps/api/src/app/inversify.config.ts b/apps/api/src/app/inversify.config.ts index 65c34f2d..4b747d35 100644 --- a/apps/api/src/app/inversify.config.ts +++ b/apps/api/src/app/inversify.config.ts @@ -23,7 +23,7 @@ import { tokenHolderRepositorySymbol, UsdRepository, usdRepositorySymbol, -} from '@cowprotocol/repositories'; +} from '@cowprotocol/repositories' import { getCacheRepository, @@ -69,160 +69,114 @@ import { SSEServiceMain, balanceTrackingServiceSymbol, sseServiceSymbol, -} from '@cowprotocol/services'; +} from '@cowprotocol/services' -import { Container } from 'inversify'; -import { Logger, logger } from '@cowprotocol/shared'; +import { Container } from 'inversify' +import { Logger, logger } from '@cowprotocol/shared' -const DEFAULT_AFFILIATE_STATS_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes +const DEFAULT_AFFILIATE_STATS_CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes function getAffiliateStatsCacheTtlMs(): number { - const rawValue = process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS; + const rawValue = process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS if (!rawValue) { - return DEFAULT_AFFILIATE_STATS_CACHE_TTL_MS; + return DEFAULT_AFFILIATE_STATS_CACHE_TTL_MS } - const parsed = Number(rawValue); + const parsed = Number(rawValue) if (!Number.isFinite(parsed) || parsed < 0) { logger.warn( `Invalid DUNE_AFFILIATE_STATS_CACHE_TTL_MS value: ${rawValue}. Using default ${DEFAULT_AFFILIATE_STATS_CACHE_TTL_MS}ms.` - ); - return DEFAULT_AFFILIATE_STATS_CACHE_TTL_MS; + ) + return DEFAULT_AFFILIATE_STATS_CACHE_TTL_MS } - return parsed; + return parsed } export function getApiContainer(): Container { - const apiContainer = new Container(); + const apiContainer = new Container() // Bind logger - apiContainer.bind('Logger').toConstantValue(logger); + apiContainer.bind('Logger').toConstantValue(logger) // Repositories - const cacheRepository = getCacheRepository(); - const erc20Repository = getErc20Repository(cacheRepository); - const simulationRepository = getSimulationRepository(); - const tokenHolderRepository = getTokenHolderRepository(cacheRepository); - const tokenBalancesRepository = getTokenBalancesRepository(); - const userBalanceRepository = getUserBalanceRepository(cacheRepository); - const usdRepository = getUsdRepository(cacheRepository, erc20Repository); - const pushNotificationsRepository = getPushNotificationsRepository(); - const pushSubscriptionsRepository = getPushSubscriptionsRepository(); - const affiliatesRepository = getAffiliatesRepository(); + const cacheRepository = getCacheRepository() + const erc20Repository = getErc20Repository(cacheRepository) + const simulationRepository = getSimulationRepository() + const tokenHolderRepository = getTokenHolderRepository(cacheRepository) + const tokenBalancesRepository = getTokenBalancesRepository() + const userBalanceRepository = getUserBalanceRepository(cacheRepository) + const usdRepository = getUsdRepository(cacheRepository, erc20Repository) + const pushNotificationsRepository = getPushNotificationsRepository() + const pushSubscriptionsRepository = getPushSubscriptionsRepository() + const affiliatesRepository = getAffiliatesRepository() - apiContainer - .bind(erc20RepositorySymbol) - .toConstantValue(erc20Repository); + apiContainer.bind(erc20RepositorySymbol).toConstantValue(erc20Repository) - apiContainer - .bind(tenderlyRepositorySymbol) - .toConstantValue(simulationRepository); + apiContainer.bind(tenderlyRepositorySymbol).toConstantValue(simulationRepository) - apiContainer - .bind(cacheRepositorySymbol) - .toConstantValue(cacheRepository); + apiContainer.bind(cacheRepositorySymbol).toConstantValue(cacheRepository) - apiContainer - .bind(usdRepositorySymbol) - .toConstantValue(usdRepository); + apiContainer.bind(usdRepositorySymbol).toConstantValue(usdRepository) apiContainer .bind(pushNotificationsRepositorySymbol) - .toConstantValue(pushNotificationsRepository); + .toConstantValue(pushNotificationsRepository) apiContainer .bind(pushSubscriptionsRepositorySymbol) - .toConstantValue(pushSubscriptionsRepository); + .toConstantValue(pushSubscriptionsRepository) - apiContainer - .bind(affiliatesRepositorySymbol) - .toConstantValue(affiliatesRepository); + apiContainer.bind(affiliatesRepositorySymbol).toConstantValue(affiliatesRepository) - apiContainer - .bind(tokenHolderRepositorySymbol) - .toConstantValue(tokenHolderRepository); + apiContainer.bind(tokenHolderRepositorySymbol).toConstantValue(tokenHolderRepository) if (isDuneEnabled) { - const duneRepository = getDuneRepository(); - const affiliateStatsCacheTtlMs = getAffiliateStatsCacheTtlMs(); + const duneRepository = getDuneRepository() + const affiliateStatsCacheTtlMs = getAffiliateStatsCacheTtlMs() - apiContainer - .bind(duneRepositorySymbol) - .toConstantValue(duneRepository); + apiContainer.bind(duneRepositorySymbol).toConstantValue(duneRepository) - apiContainer - .bind(hooksServiceSymbol) - .toDynamicValue(() => new HooksServiceImpl(duneRepository)); + apiContainer.bind(hooksServiceSymbol).toDynamicValue(() => new HooksServiceImpl(duneRepository)) apiContainer .bind(affiliateStatsServiceSymbol) - .toDynamicValue( - () => - new AffiliateStatsServiceImpl( - duneRepository, - affiliateStatsCacheTtlMs - ) - ) - .inSingletonScope(); + .toDynamicValue(() => new AffiliateStatsServiceImpl(duneRepository, affiliateStatsCacheTtlMs)) + .inSingletonScope() } if (isDuneEnabled && isCmsEnabled) { - const duneRepository = - apiContainer.get(duneRepositorySymbol); + const duneRepository = apiContainer.get(duneRepositorySymbol) apiContainer .bind(affiliateProgramExportServiceSymbol) - .toDynamicValue( - () => - new AffiliateProgramExportServiceImpl( - affiliatesRepository, - duneRepository - ) - ); + .toDynamicValue(() => new AffiliateProgramExportServiceImpl(affiliatesRepository, duneRepository)) } - apiContainer - .bind(tokenBalancesRepositorySymbol) - .toConstantValue(tokenBalancesRepository); + apiContainer.bind(tokenBalancesRepositorySymbol).toConstantValue(tokenBalancesRepository) - apiContainer - .bind(userBalanceRepositorySymbol) - .toConstantValue(userBalanceRepository); + apiContainer.bind(userBalanceRepositorySymbol).toConstantValue(userBalanceRepository) // Services - apiContainer - .bind(slippageServiceSymbol) - .to(SlippageServiceMain); + apiContainer.bind(slippageServiceSymbol).to(SlippageServiceMain) - apiContainer - .bind(tokenHolderServiceSymbol) - .to(TokenHolderServiceMain); + apiContainer.bind(tokenHolderServiceSymbol).to(TokenHolderServiceMain) - apiContainer - .bind(tokenBalancesServiceSymbol) - .to(TokenBalancesServiceMain); + apiContainer.bind(tokenBalancesServiceSymbol).to(TokenBalancesServiceMain) - apiContainer.bind(usdServiceSymbol).to(UsdServiceMain); + apiContainer.bind(usdServiceSymbol).to(UsdServiceMain) - apiContainer - .bind(tokenDetailServiceSymbol) - .to(TokenDetailServiceMain); + apiContainer.bind(tokenDetailServiceSymbol).to(TokenDetailServiceMain) - apiContainer - .bind(simulationServiceSymbol) - .to(SimulationService); + apiContainer.bind(simulationServiceSymbol).to(SimulationService) apiContainer .bind(balanceTrackingServiceSymbol) .to(BalanceTrackingServiceMain) - .inSingletonScope(); + .inSingletonScope() - apiContainer - .bind(sseServiceSymbol) - .to(SSEServiceMain) - .inSingletonScope(); + apiContainer.bind(sseServiceSymbol).to(SSEServiceMain).inSingletonScope() - return apiContainer; + return apiContainer } -export const apiContainer = getApiContainer(); +export const apiContainer = getApiContainer() diff --git a/apps/api/src/app/plugins/bffAuth.ts b/apps/api/src/app/plugins/bffAuth.ts index 5b1be836..0bd77248 100644 --- a/apps/api/src/app/plugins/bffAuth.ts +++ b/apps/api/src/app/plugins/bffAuth.ts @@ -1,5 +1,5 @@ -import fp from "fastify-plugin"; -import { FastifyPluginCallback } from "fastify"; +import fp from 'fastify-plugin' +import { FastifyPluginCallback } from 'fastify' const PROTECTED_PATHS = ['/proxies'] @@ -9,14 +9,13 @@ const AUTHORIZED_ORIGINS = (() => { return [] } - return domains.split(',').map(domain => domain.trim()) + return domains.split(',').map((domain) => domain.trim()) })() - export const bffAuth: FastifyPluginCallback = (fastify, opts, next) => { fastify.addHook('onRequest', async (request, reply) => { // Return early if its an unprotected path - if (AUTHORIZED_ORIGINS.length === 0 || !PROTECTED_PATHS.some(path => request.url.startsWith(path))) { + if (AUTHORIZED_ORIGINS.length === 0 || !PROTECTED_PATHS.some((path) => request.url.startsWith(path))) { return } @@ -26,13 +25,10 @@ export const bffAuth: FastifyPluginCallback = (fastify, opts, next) => { if ( // Origin should be present !origin || - ( - // The origin should be explicitly authorized - !AUTHORIZED_ORIGINS.some(authorizedOrigin => origin.endsWith(authorizedOrigin)) && - + // The origin should be explicitly authorized + (!AUTHORIZED_ORIGINS.some((authorizedOrigin) => origin.endsWith(authorizedOrigin)) && // Make an exception for localhost - !isLocalhost(origin) - ) + !isLocalhost(origin)) ) { reply.status(403).send('Unauthorized') return diff --git a/apps/api/src/app/plugins/bffCache.ts b/apps/api/src/app/plugins/bffCache.ts index 4762121a..e33f0aa4 100644 --- a/apps/api/src/app/plugins/bffCache.ts +++ b/apps/api/src/app/plugins/bffCache.ts @@ -1,160 +1,129 @@ -import fp from 'fastify-plugin'; -import { - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue, - parseCacheControlHeaderValue, -} from '../../utils/cache'; -import { FastifyPluginCallback, FastifyReply, FastifyRequest } from 'fastify'; - -const HEADER_NAME = 'x-bff-cache'; -import { - CacheRepository, - cacheRepositorySymbol, - getCacheKey, -} from '@cowprotocol/repositories'; -import { Readable } from 'stream'; -import { apiContainer } from '../inversify.config'; - -const cacheRepository: CacheRepository = apiContainer.get( - cacheRepositorySymbol -); +import fp from 'fastify-plugin' +import { CACHE_CONTROL_HEADER, getCacheControlHeaderValue, parseCacheControlHeaderValue } from '../../utils/cache' +import { FastifyPluginCallback, FastifyReply, FastifyRequest } from 'fastify' + +const HEADER_NAME = 'x-bff-cache' +import { CacheRepository, cacheRepositorySymbol, getCacheKey } from '@cowprotocol/repositories' +import { Readable } from 'stream' +import { apiContainer } from '../inversify.config' + +const cacheRepository: CacheRepository = apiContainer.get(cacheRepositorySymbol) interface BffCacheOptions { - ttl?: number; + ttl?: number } -export const bffCache: FastifyPluginCallback = ( - fastify, - opts, - next -) => { - const { ttl } = opts; +export const bffCache: FastifyPluginCallback = (fastify, opts, next) => { + const { ttl } = opts fastify.addHook('onRequest', async (request, reply) => { // Cache only GET requests if (request.method !== 'GET') { - return; + return } - const key = getKey(request); + const key = getKey(request) // Remove it so we can cache it properly - request.headers['accept-encoding'] = undefined; + request.headers['accept-encoding'] = undefined - const [item, ttl] = await Promise.all([ - cacheRepository.get(key), - cacheRepository.getTtl(key), - ]).catch((e) => { - fastify.log.error(`Error getting key ${key} from cache`, e); - return [null, null]; - }); + const [item, ttl] = await Promise.all([cacheRepository.get(key), cacheRepository.getTtl(key)]).catch((e) => { + fastify.log.error(`Error getting key ${key} from cache`, e) + return [null, null] + }) if (item !== null && ttl !== null) { - fastify.log.debug(`Found cached item "${item}" with TTL ${ttl}`); + fastify.log.debug(`Found cached item "${item}" with TTL ${ttl}`) - const ttlInSeconds = isNaN(ttl) ? undefined : ttl; + const ttlInSeconds = isNaN(ttl) ? undefined : ttl if (ttlInSeconds !== undefined) { - reply.header( - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue(ttlInSeconds) - ); - reply.header(HEADER_NAME, 'HIT'); - reply.type('application/json'); - reply.send(item); - fastify.log.debug('onRequest cacheItem: %s (%d)', item, ttlInSeconds); - - return reply; + reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(ttlInSeconds)) + reply.header(HEADER_NAME, 'HIT') + reply.type('application/json') + reply.send(item) + fastify.log.debug('onRequest cacheItem: %s (%d)', item, ttlInSeconds) + + return reply } } else { - fastify.log.trace(`Request not cached for "${key}`); + fastify.log.trace(`Request not cached for "${key}`) // For now we don't add the cache 'MISS' header, because we only do this for cacheable endpoints (the ones that include cache-control header in the response) // We need to handle this in the "onSend" once we know the actual response } - }); + }) fastify.addHook('onSend', async function (req, reply, payload) { try { - const isCacheHit = reply.getHeader(HEADER_NAME) === 'HIT'; - const cacheTtl: number | undefined = getTtlFromResponse(reply, ttl); - const isStatus200 = reply.statusCode >= 200 && reply.statusCode < 300; + const isCacheHit = reply.getHeader(HEADER_NAME) === 'HIT' + const cacheTtl: number | undefined = getTtlFromResponse(reply, ttl) + const isStatus200 = reply.statusCode >= 200 && reply.statusCode < 300 // If the cache is a hit, or is non-cacheable, we just proceed with the request if (isCacheHit || cacheTtl === undefined || !isStatus200) { - return undefined; + return undefined } // If there is no cached data, then its a cache-miss - reply.header(HEADER_NAME, 'MISS'); + reply.header(HEADER_NAME, 'MISS') // Get content from payload - const content = - typeof payload === 'string' - ? payload - : await getContentFromPayload(payload); + const content = typeof payload === 'string' ? payload : await getContentFromPayload(payload) if (content !== null) { // Cache (fire and forget) - const key = getKey(req); + const key = getKey(req) cacheRepository.set(key, content, cacheTtl).catch((e) => { - fastify.log.error(`Error setting key ${key} to cache`, e); - }); - reply.header( - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue(cacheTtl) - ); - return content; + fastify.log.error(`Error setting key ${key} to cache`, e) + }) + reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(cacheTtl)) + return content } } catch (error) { - fastify.log.error(error, '[bffCache] Error handling the cache'); + fastify.log.error(error, '[bffCache] Error handling the cache') } - }); + }) - next(); -}; + next() +} async function getContentFromPayload(payload: unknown): Promise { if (payload instanceof Buffer) { - return payload.toString(); + return payload.toString() } if (payload instanceof Response) { - return payload.text(); + return payload.text() } - let contents = ''; + let contents = '' if (payload instanceof Readable) { for await (const chunk of payload) { - contents += chunk.toString(); + contents += chunk.toString() } } else if (payload instanceof ReadableStream) { - const reader = payload.getReader(); - let result; + const reader = payload.getReader() + let result while (!(result = await reader.read()).done) { - contents += new TextDecoder().decode(result.value); + contents += new TextDecoder().decode(result.value) } } else { - return null; + return null } - return contents; + return contents } function getKey(req: FastifyRequest) { - return getCacheKey('requests', ...req.url.split('/')); + return getCacheKey('requests', ...req.url.split('/')) } -function getTtlFromResponse( - reply: FastifyReply, - defaultTtl: number | undefined -): number | undefined { - const cacheControl = parseCacheControlHeaderValue( - reply.getHeader(CACHE_CONTROL_HEADER) - ); - const maxAge = cacheControl['max-age']; - return maxAge ? parseInt(maxAge) : defaultTtl; +function getTtlFromResponse(reply: FastifyReply, defaultTtl: number | undefined): number | undefined { + const cacheControl = parseCacheControlHeaderValue(reply.getHeader(CACHE_CONTROL_HEADER)) + const maxAge = cacheControl['max-age'] + return maxAge ? parseInt(maxAge) : defaultTtl } export default fp(bffCache, { fastify: '4.x', name: 'bffCache', -}); +}) diff --git a/apps/api/src/app/plugins/caching.ts b/apps/api/src/app/plugins/caching.ts index 3af1a901..e6f21d30 100644 --- a/apps/api/src/app/plugins/caching.ts +++ b/apps/api/src/app/plugins/caching.ts @@ -1,9 +1,9 @@ -import fastifyCaching, { FastifyCachingPluginOptions } from '@fastify/caching'; -import fp from 'fastify-plugin'; -import abstractCache from 'abstract-cache'; -import { redisClient } from '@cowprotocol/repositories'; +import fastifyCaching, { FastifyCachingPluginOptions } from '@fastify/caching' +import fp from 'fastify-plugin' +import abstractCache from 'abstract-cache' +import { redisClient } from '@cowprotocol/repositories' -import 'abstract-cache-redis'; +import 'abstract-cache-redis' export default fp(async (fastify, opts) => { const options: FastifyCachingPluginOptions = { @@ -22,6 +22,6 @@ export default fp(async (fastify, opts) => { }), } : {}), - }; - fastify.register(fastifyCaching, options); -}); + } + fastify.register(fastifyCaching, options) +}) diff --git a/apps/api/src/app/plugins/cors.ts b/apps/api/src/app/plugins/cors.ts index 25c182b3..b8f16abd 100644 --- a/apps/api/src/app/plugins/cors.ts +++ b/apps/api/src/app/plugins/cors.ts @@ -1,5 +1,5 @@ -import cors, { FastifyCorsOptions } from '@fastify/cors'; -import fp from 'fastify-plugin'; +import cors, { FastifyCorsOptions } from '@fastify/cors' +import fp from 'fastify-plugin' export default fp(async (fastify, opts) => { const options: FastifyCorsOptions = { @@ -9,18 +9,15 @@ export default fp(async (fastify, opts) => { delegator: (req, callback) => { const corsOptions: FastifyCorsOptions = { origin: false, - }; + } - const origin = req.headers.origin as string | undefined; - if ( - origin && - /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(origin) - ) { - corsOptions.origin = true; + const origin = req.headers.origin as string | undefined + if (origin && /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(origin)) { + corsOptions.origin = true } - callback(null, corsOptions); + callback(null, corsOptions) }, - }; - fastify.register(cors, options); -}); + } + fastify.register(cors, options) +}) diff --git a/apps/api/src/app/plugins/env.ts b/apps/api/src/app/plugins/env.ts index 63c273b9..f4c0594c 100644 --- a/apps/api/src/app/plugins/env.ts +++ b/apps/api/src/app/plugins/env.ts @@ -1,5 +1,5 @@ -import fastifyEnv from '@fastify/env'; -import fp from 'fastify-plugin'; +import fastifyEnv from '@fastify/env' +import fp from 'fastify-plugin' const schema = { type: 'object', @@ -71,23 +71,23 @@ const schema = { type: 'string', }, }, -}; +} export default fp(async (fastify, opts) => { const options = { ...opts, schema, dotenv: true, - }; + } - fastify.register(fastifyEnv, options); -}); + fastify.register(fastifyEnv, options) +}) declare module 'fastify' { interface FastifyInstance { config: { // Currently only supports string type like this. - [K in keyof typeof schema.properties]: string; - }; + [K in keyof typeof schema.properties]: string + } } } diff --git a/apps/api/src/app/plugins/orm-analytics.ts b/apps/api/src/app/plugins/orm-analytics.ts index b7c9943e..c4494275 100644 --- a/apps/api/src/app/plugins/orm-analytics.ts +++ b/apps/api/src/app/plugins/orm-analytics.ts @@ -1,16 +1,14 @@ -import 'reflect-metadata'; -import { FastifyInstance } from 'fastify'; -import typeORMPlugin from 'typeorm-fastify-plugin'; -import fp from 'fastify-plugin'; -import { PoolInfo } from '../data/poolInfo'; -import { isDbEnabled } from '@cowprotocol/repositories'; +import 'reflect-metadata' +import { FastifyInstance } from 'fastify' +import typeORMPlugin from 'typeorm-fastify-plugin' +import fp from 'fastify-plugin' +import { PoolInfo } from '../data/poolInfo' +import { isDbEnabled } from '@cowprotocol/repositories' export default fp(async function (fastify: FastifyInstance) { if (!isDbEnabled) { - fastify.log.warn( - 'Database is disabled. ORM for analytics will not be used.' - ); - return; + fastify.log.warn('Database is disabled. ORM for analytics will not be used.') + return } const dbParams = { @@ -19,17 +17,13 @@ export default fp(async function (fastify: FastifyInstance) { database: fastify.config.COW_ANALYTICS_DATABASE_NAME, username: fastify.config.COW_ANALYTICS_DATABASE_USERNAME, password: fastify.config.COW_ANALYTICS_DATABASE_PASSWORD, - }; + } - const dbParamsAreInvalid = Object.values(dbParams).some( - (v) => Number.isNaN(v) || v === undefined - ); + const dbParamsAreInvalid = Object.values(dbParams).some((v) => Number.isNaN(v) || v === undefined) if (dbParamsAreInvalid) { - console.error( - 'Invalid CoW Analytics database parameters, please check COW_ANALYTICS_* env vars' - ); - return; + console.error('Invalid CoW Analytics database parameters, please check COW_ANALYTICS_* env vars') + return } fastify.register(typeORMPlugin, { @@ -43,13 +37,13 @@ export default fp(async function (fastify: FastifyInstance) { rejectUnauthorized: false, }, }, - }); + }) fastify.ready((err) => { if (err) { - throw err; + throw err } - fastify.orm.analytics.runMigrations({ transaction: 'all' }); - }); -}); + fastify.orm.analytics.runMigrations({ transaction: 'all' }) + }) +}) diff --git a/apps/api/src/app/plugins/orm-repositories.ts b/apps/api/src/app/plugins/orm-repositories.ts index 1d35728e..3bfc1db8 100644 --- a/apps/api/src/app/plugins/orm-repositories.ts +++ b/apps/api/src/app/plugins/orm-repositories.ts @@ -1,28 +1,23 @@ -import 'reflect-metadata'; -import { FastifyInstance } from 'fastify'; -import typeORMPlugin from 'typeorm-fastify-plugin'; -import fp from 'fastify-plugin'; -import { resolve } from 'path'; -import { logger } from '@cowprotocol/shared'; +import 'reflect-metadata' +import { FastifyInstance } from 'fastify' +import typeORMPlugin from 'typeorm-fastify-plugin' +import fp from 'fastify-plugin' +import { resolve } from 'path' +import { logger } from '@cowprotocol/shared' -import { getDatabaseParams } from '@cowprotocol/repositories'; -import { readdir } from 'fs/promises'; -import { isDbEnabled } from '@cowprotocol/repositories'; +import { getDatabaseParams } from '@cowprotocol/repositories' +import { readdir } from 'fs/promises' +import { isDbEnabled } from '@cowprotocol/repositories' export default fp(async function (fastify: FastifyInstance) { if (!isDbEnabled) { - fastify.log.warn( - 'Database is disabled. ORM for repositories will not be used.' - ); - return; + fastify.log.warn('Database is disabled. ORM for repositories will not be used.') + return } - const isProduction = process.env.NODE_ENV === 'production'; + const isProduction = process.env.NODE_ENV === 'production' - const migrationsDir = resolve( - __dirname, - '../../../../../libs/repositories/src/migrations' - ); + const migrationsDir = resolve(__dirname, '../../../../../libs/repositories/src/migrations') fastify.register(typeORMPlugin, { ...getDatabaseParams(), @@ -40,36 +35,34 @@ export default fp(async function (fastify: FastifyInstance) { } : undefined, logging: true, // Enable TypeORM logging - }); + }) fastify.ready(async (err) => { if (err) { - throw err; + throw err } try { - logger.info('Starting migrations...'); - logger.info('Migrations dir:', migrationsDir); - await printMigrations(migrationsDir); + logger.info('Starting migrations...') + logger.info('Migrations dir:', migrationsDir) + await printMigrations(migrationsDir) const result = await fastify.orm.repositories.runMigrations({ transaction: 'all', - }); - logger.info(`${result.length} migrations applied`); + }) + logger.info(`${result.length} migrations applied`) } catch (error) { - logger.error('Migration error:', error); - throw error; + logger.error('Migration error:', error) + throw error } - }); -}); + }) +}) async function printMigrations(dirPath: string) { try { - const files = (await readdir(dirPath)).filter((file) => - file.endsWith('.js') - ); + const files = (await readdir(dirPath)).filter((file) => file.endsWith('.js')) for (const file of files) { - logger.info(` - ${file}`); + logger.info(` - ${file}`) } } catch (err) { - logger.error('Error reading directory:', err); + logger.error('Error reading directory:', err) } } diff --git a/apps/api/src/app/plugins/redis.ts b/apps/api/src/app/plugins/redis.ts index c391d53b..e7189813 100644 --- a/apps/api/src/app/plugins/redis.ts +++ b/apps/api/src/app/plugins/redis.ts @@ -1,14 +1,14 @@ -import fastifyRedis, { FastifyRedisPluginOptions } from '@fastify/redis'; +import fastifyRedis, { FastifyRedisPluginOptions } from '@fastify/redis' -import fp from 'fastify-plugin'; -import { redisClient } from '@cowprotocol/repositories'; +import fp from 'fastify-plugin' +import { redisClient } from '@cowprotocol/repositories' export default fp(async (fastify, opts) => { if (redisClient) { const options: FastifyRedisPluginOptions = { ...opts, client: redisClient, - }; - fastify.register(fastifyRedis, options); + } + fastify.register(fastifyRedis, options) } -}); +}) diff --git a/apps/api/src/app/plugins/support.ts b/apps/api/src/app/plugins/support.ts index 562c3b8c..a283a746 100644 --- a/apps/api/src/app/plugins/support.ts +++ b/apps/api/src/app/plugins/support.ts @@ -11,6 +11,6 @@ export default fp(async (fastify) => { // When using .decorate you have to specify added properties for Typescript declare module 'fastify' { export interface FastifyInstance { - someSupport(): string; + someSupport(): string } } diff --git a/apps/api/src/app/plugins/swagger.ts b/apps/api/src/app/plugins/swagger.ts index 81218914..e2d703f2 100644 --- a/apps/api/src/app/plugins/swagger.ts +++ b/apps/api/src/app/plugins/swagger.ts @@ -1,42 +1,40 @@ -import { FastifyInstance } from 'fastify'; -import fp from 'fastify-plugin'; -import fastifySwagger from '@fastify/swagger'; -import fastifySwaggerUi from '@fastify/swagger-ui'; +import { FastifyInstance } from 'fastify' +import fp from 'fastify-plugin' +import fastifySwagger from '@fastify/swagger' +import fastifySwaggerUi from '@fastify/swagger-ui' -const HIDDEN_PATHS = ['/']; -const HIDDEN_BASE_PATHS = ['/proxies', '/proxy', '/twap', '/examples']; +const HIDDEN_PATHS = ['/'] +const HIDDEN_BASE_PATHS = ['/proxies', '/proxy', '/twap', '/examples'] export default fp(async function (fastify: FastifyInstance) { fastify.register(fastifySwagger, { - swagger: { - }, + swagger: {}, transform: (param) => { const { schema = {}, url } = param // can add the hide tag if needed - const modifiedSchema = (isHiddenPath(url)) ? { ...schema, hide: true } : schema + const modifiedSchema = isHiddenPath(url) ? { ...schema, hide: true } : schema return { ...param, schema: modifiedSchema } - } - }); + }, + }) fastify.register(fastifySwaggerUi, { routePrefix: '/docs', - }); + }) fastify.ready((err) => { - if (err) throw err; - fastify.swagger(); - }); - -}); + if (err) throw err + fastify.swagger() + }) +}) function isHiddenPath(url: string) { - if (HIDDEN_BASE_PATHS.some(basePath => url.startsWith(basePath))) { - return true; + if (HIDDEN_BASE_PATHS.some((basePath) => url.startsWith(basePath))) { + return true } - if (HIDDEN_PATHS.some(basePath => url === basePath)) { - return true; + if (HIDDEN_PATHS.some((basePath) => url === basePath)) { + return true } return false -} \ No newline at end of file +} diff --git a/apps/api/src/app/routes/__chainId/accounts/__userAddress/balances/index.ts b/apps/api/src/app/routes/__chainId/accounts/__userAddress/balances/index.ts index b6e7ccb6..2a06d493 100644 --- a/apps/api/src/app/routes/__chainId/accounts/__userAddress/balances/index.ts +++ b/apps/api/src/app/routes/__chainId/accounts/__userAddress/balances/index.ts @@ -1,8 +1,8 @@ -import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { v4 as uuidv4 } from 'uuid'; -import { apiContainer } from '../../../../../inversify.config'; -import { AddressSchema, SupportedChainIdSchema } from '../../../../../schemas'; +import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify' +import { FromSchema, JSONSchema } from 'json-schema-to-ts' +import { v4 as uuidv4 } from 'uuid' +import { apiContainer } from '../../../../../inversify.config' +import { AddressSchema, SupportedChainIdSchema } from '../../../../../schemas' import { SSEService, sseServiceSymbol, @@ -11,12 +11,12 @@ import { SSEClient, TokenBalancesService, tokenBalancesServiceSymbol, -} from '@cowprotocol/services'; -import { parseEthereumAddressList } from '@cowprotocol/shared'; +} from '@cowprotocol/services' +import { parseEthereumAddressList } from '@cowprotocol/shared' const KEEP_ALIVE_INTERVAL_MS = parseInt( process.env.KEEP_ALIVE_INTERVAL_MS || '20000' // 20 seconds -); +) const paramsSchema = { type: 'object', @@ -26,7 +26,7 @@ const paramsSchema = { chainId: SupportedChainIdSchema, userAddress: AddressSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const querySchema = { type: 'object', @@ -38,7 +38,7 @@ const querySchema = { description: 'Comma-separated list of token addresses', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const successSchema = { type: 'array', @@ -80,7 +80,7 @@ const successSchema = { }, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const errorSchema = { type: 'object', @@ -92,37 +92,35 @@ const errorSchema = { description: 'Error message', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -type ParamsSchema = FromSchema; -type QuerySchema = FromSchema; -type SuccessSchema = FromSchema; -type ErrorSchema = FromSchema; +type ParamsSchema = FromSchema +type QuerySchema = FromSchema +type SuccessSchema = FromSchema +type ErrorSchema = FromSchema // TODO: In principle is not nice I use a repository in the API. We should make a service. Here I'm being lazy. Lets fix clean it up later! (hacking mode). Also this service will use 2 repos: ERC20Repo + balanceRepo -const tokenBalancesService: TokenBalancesService = apiContainer.get( - tokenBalancesServiceSymbol -); +const tokenBalancesService: TokenBalancesService = apiContainer.get(tokenBalancesServiceSymbol) function parseTokenAddresses(tokens: string): string[] { - const tokenAddresses = parseEthereumAddressList(tokens.split(',')); + const tokenAddresses = parseEthereumAddressList(tokens.split(',')) if (tokenAddresses.length === 0) { - throw new Error('At least one token address is required'); + throw new Error('At least one token address is required') } - return tokenAddresses; + return tokenAddresses } const root: FastifyPluginAsync = async (fastify): Promise => { // REST endpoint for fetching user token balances // Example: GET /1/accounts/0x123.../balances?tokens=0xabc...,0xdef...&spender=0x456... fastify.get<{ - Params: ParamsSchema; - Querystring: QuerySchema; - Reply: SuccessSchema | ErrorSchema; + Params: ParamsSchema + Querystring: QuerySchema + Reply: SuccessSchema | ErrorSchema }>( '/', - { + { schema: { description: 'Fetches Token balance and allowance for a given user address and token addresses', params: paramsSchema, @@ -136,17 +134,16 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const { chainId, userAddress } = request.params; - const { tokens } = request.query; + const { chainId, userAddress } = request.params + const { tokens } = request.query - let tokenAddresses: string[]; + let tokenAddresses: string[] try { - tokenAddresses = parseTokenAddresses(tokens); + tokenAddresses = parseTokenAddresses(tokens) } catch (error) { - const message = - error instanceof Error ? error.message : 'Invalid token addresses'; - reply.code(400).send({ message }); - return; + const message = error instanceof Error ? error.message : 'Invalid token addresses' + reply.code(400).send({ message }) + return } try { @@ -155,24 +152,22 @@ const root: FastifyPluginAsync = async (fastify): Promise => { chainId, userAddress, tokenAddresses, - }); + }) - reply.send(balances); + reply.send(balances) - fastify.log.info( - `Fetched ${balances.length} token balances for user ${userAddress} on chain ${chainId}` - ); + fastify.log.info(`Fetched ${balances.length} token balances for user ${userAddress} on chain ${chainId}`) } catch (error) { - fastify.log.error('Error fetching user balances:', error); - reply.code(500).send({ message: 'Internal server error' }); + fastify.log.error('Error fetching user balances:', error) + reply.code(500).send({ message: 'Internal server error' }) } } - ); + ) // SSE endpoint for real-time balance updates fastify.get<{ - Params: ParamsSchema; - Querystring: QuerySchema; + Params: ParamsSchema + Querystring: QuerySchema }>( '/sse', { @@ -185,40 +180,37 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, async function ( request: FastifyRequest<{ - Params: ParamsSchema; - Querystring: QuerySchema; + Params: ParamsSchema + Querystring: QuerySchema }>, reply: FastifyReply ) { - const { chainId, userAddress } = request.params; - const { tokens } = request.query; + const { chainId, userAddress } = request.params + const { tokens } = request.query // TODO: This should be done in inversify config, not here. Just quick test - const sseService: SSEService = apiContainer.get(sseServiceSymbol); - const balanceTrackingService: BalanceTrackingService = apiContainer.get( - balanceTrackingServiceSymbol - ); + const sseService: SSEService = apiContainer.get(sseServiceSymbol) + const balanceTrackingService: BalanceTrackingService = apiContainer.get(balanceTrackingServiceSymbol) - let tokenAddresses: string[]; + let tokenAddresses: string[] try { - tokenAddresses = parseTokenAddresses(tokens); + tokenAddresses = parseTokenAddresses(tokens) } catch (error) { - const message = - error instanceof Error ? error.message : 'Invalid token addresses'; - reply.code(400).send({ message }); - return; + const message = error instanceof Error ? error.message : 'Invalid token addresses' + reply.code(400).send({ message }) + return } // Set SSE headers - reply.raw.setHeader('Content-Type', 'text/event-stream'); - reply.raw.setHeader('Cache-Control', 'no-cache'); - reply.raw.setHeader('Connection', 'keep-alive'); - reply.raw.setHeader('Access-Control-Allow-Origin', '*'); - reply.raw.setHeader('Access-Control-Allow-Headers', 'Cache-Control'); - reply.raw.setHeader('X-Accel-Buffering', 'no'); + reply.raw.setHeader('Content-Type', 'text/event-stream') + reply.raw.setHeader('Cache-Control', 'no-cache') + reply.raw.setHeader('Connection', 'keep-alive') + reply.raw.setHeader('Access-Control-Allow-Origin', '*') + reply.raw.setHeader('Access-Control-Allow-Headers', 'Cache-Control') + reply.raw.setHeader('X-Accel-Buffering', 'no') // Create SSE client - const clientId = uuidv4(); + const clientId = uuidv4() const sseClient: SSEClient = { clientId: clientId, @@ -226,23 +218,20 @@ const root: FastifyPluginAsync = async (fastify): Promise => { userAddress, tokenAddresses, send: (data: string) => { - reply.raw.write(data); + reply.raw.write(data) }, close: () => { try { - reply.raw.end(); + reply.raw.end() } catch (error) { - fastify.log.error( - `Error closing SSE connection for client ${clientId}:`, - error - ); + fastify.log.error(`Error closing SSE connection for client ${clientId}:`, error) } }, - }; + } // Add client to SSE service - fastify.log.info(`New client ${clientId} connected to SSE service`); - sseService.addClient(sseClient); + fastify.log.info(`New client ${clientId} connected to SSE service`) + sseService.addClient(sseClient) // Start tracking user balances try { @@ -251,79 +240,66 @@ const root: FastifyPluginAsync = async (fastify): Promise => { chainId, userAddress, tokenAddresses, - }); + }) } catch (error) { - fastify.log.error( - `Error starting balance tracking for clientId=${clientId}:`, - error - ); + fastify.log.error(`Error starting balance tracking for clientId=${clientId}:`, error) } - let isDisconnecting = false; + let isDisconnecting = false const handleDisconnect = async (reason: string) => { if (isDisconnecting) { - return; + return } - isDisconnecting = true; - fastify.log.info(`Client ${clientId} disconnected (${reason})`); - sseService.removeClient(clientId); + isDisconnecting = true + fastify.log.info(`Client ${clientId} disconnected (${reason})`) + sseService.removeClient(clientId) // Stop tracking if no other clients are connected for this user - const remainingClients = sseService.getClientsForUser( - chainId, - userAddress - ); + const remainingClients = sseService.getClientsForUser(chainId, userAddress) if (remainingClients.length === 0) { // No more clients. Don't track this user anymore try { - await balanceTrackingService.stopTrackingUser(chainId, userAddress); + await balanceTrackingService.stopTrackingUser(chainId, userAddress) } catch (error) { - fastify.log.error('Error stopping balance tracking:', error); + fastify.log.error('Error stopping balance tracking:', error) } } else { // There's more clients for this user. Update the tracked tokens - const remainingTokens = new Set(); + const remainingTokens = new Set() remainingClients.forEach((client) => { client.tokenAddresses.forEach((tokenAddress) => { - remainingTokens.add(tokenAddress.toLowerCase()); - }); - }); + remainingTokens.add(tokenAddress.toLowerCase()) + }) + }) try { - await balanceTrackingService.updateTrackedTokens( - chainId, - userAddress, - Array.from(remainingTokens) - ); + await balanceTrackingService.updateTrackedTokens(chainId, userAddress, Array.from(remainingTokens)) } catch (error) { - fastify.log.error(error, 'Error updating tracked tokens'); + fastify.log.error(error, 'Error updating tracked tokens') } } - }; + } // Send keep-alive messages every 30 seconds const keepAliveInterval = setInterval(() => { - const didSend = sseService.sendToClient( - clientId, - 'event: ping\ndata: {}\n\n' - ); + const didSend = sseService.sendToClient(clientId, 'event: ping\ndata: {}\n\n') if (!didSend) { - clearInterval(keepAliveInterval); - void handleDisconnect('ping failed'); + clearInterval(keepAliveInterval) + void handleDisconnect('ping failed') } - }, KEEP_ALIVE_INTERVAL_MS); + }, KEEP_ALIVE_INTERVAL_MS) // Handle client disconnect request.raw.on('close', () => { - clearInterval(keepAliveInterval); - void handleDisconnect('close'); - }); + clearInterval(keepAliveInterval) + void handleDisconnect('close') + }) // Don't end the response - keep it open for SSE - return reply; + return reply } - ); -}; + ) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/__chainId/address/__address/balances.ts b/apps/api/src/app/routes/__chainId/address/__address/balances.ts index 7cbe87d7..c1b6ea24 100644 --- a/apps/api/src/app/routes/__chainId/address/__address/balances.ts +++ b/apps/api/src/app/routes/__chainId/address/__address/balances.ts @@ -1,16 +1,10 @@ -import { AddressSchema, SupportedChainIdSchema } from '../../../../schemas'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { FastifyPluginAsync } from 'fastify'; -import { apiContainer } from '../../../../inversify.config'; -import { - TokenBalancesService, - tokenBalancesServiceSymbol, -} from '@cowprotocol/services'; -import ms from 'ms'; -import { - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue, -} from '../../../../../utils/cache'; +import { AddressSchema, SupportedChainIdSchema } from '../../../../schemas' +import { FromSchema, JSONSchema } from 'json-schema-to-ts' +import { FastifyPluginAsync } from 'fastify' +import { apiContainer } from '../../../../inversify.config' +import { TokenBalancesService, tokenBalancesServiceSymbol } from '@cowprotocol/services' +import ms from 'ms' +import { CACHE_CONTROL_HEADER, getCacheControlHeaderValue } from '../../../../../utils/cache' const querySchema = { type: 'object', @@ -20,9 +14,9 @@ const querySchema = { description: 'Skip cache and fetch fresh data', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -type QuerySchema = FromSchema; +type QuerySchema = FromSchema const paramsSchema = { type: 'object', @@ -32,7 +26,7 @@ const paramsSchema = { chainId: SupportedChainIdSchema, address: AddressSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const successSchema = { type: 'object', @@ -43,7 +37,7 @@ const successSchema = { additionalProperties: { type: 'string' }, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const errorSchema = { type: 'object', @@ -57,30 +51,27 @@ const errorSchema = { examples: ['Balance not found'], }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -type RouteSchema = FromSchema; -type SuccessSchema = FromSchema; -type ErrorSchema = FromSchema; +type RouteSchema = FromSchema +type SuccessSchema = FromSchema +type ErrorSchema = FromSchema -const tokenBalancesService: TokenBalancesService = apiContainer.get( - tokenBalancesServiceSymbol -); +const tokenBalancesService: TokenBalancesService = apiContainer.get(tokenBalancesServiceSymbol) -const CACHE_SECONDS = ms('5s') / 1000; +const CACHE_SECONDS = ms('5s') / 1000 const root: FastifyPluginAsync = async (fastify): Promise => { // example: GET: http://localhost:3010/1/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/balances fastify.get<{ - Params: RouteSchema; - Reply: SuccessSchema | ErrorSchema; - Querystring: QuerySchema; + Params: RouteSchema + Reply: SuccessSchema | ErrorSchema + Querystring: QuerySchema }>( '/balances', { schema: { - description: - 'Get token balances for a given address on a specific chain.', + description: 'Get token balances for a given address on a specific chain.', tags: ['tokens'], params: paramsSchema, querystring: querySchema, @@ -93,37 +84,31 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, async function (request, reply) { if (request.query.ignoreCache) { - reply.header(CACHE_CONTROL_HEADER, 'no-store, max-age=0'); + reply.header(CACHE_CONTROL_HEADER, 'no-store, max-age=0') } else { - reply.header( - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue(CACHE_SECONDS) - ); + reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(CACHE_SECONDS)) } - const { chainId, address } = request.params; + const { chainId, address } = request.params try { const balances = await tokenBalancesService.getTokenBalances({ address, chainId, - }); + }) if (balances) { - reply.send({ balances }); + reply.send({ balances }) } else { - reply.code(404).send({ message: 'Balances not found' }); + reply.code(404).send({ message: 'Balances not found' }) } } catch (e: unknown) { - fastify.log.error( - `Error fetching balances for address ${address} on chain ${chainId}: ${e}` - ); + fastify.log.error(`Error fetching balances for address ${address} on chain ${chainId}: ${e}`) - const errorMessage = - e instanceof Error ? e.message : 'Internal Server Error'; - reply.code(500).send({ message: errorMessage }); + const errorMessage = e instanceof Error ? e.message : 'Internal Server Error' + reply.code(500).send({ message: errorMessage }) } } - ); -}; + ) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/__chainId/markets/__baseTokenAddress-__quoteTokenAddress/slippageTolerance.ts b/apps/api/src/app/routes/__chainId/markets/__baseTokenAddress-__quoteTokenAddress/slippageTolerance.ts index d062e92d..19956c0e 100644 --- a/apps/api/src/app/routes/__chainId/markets/__baseTokenAddress-__quoteTokenAddress/slippageTolerance.ts +++ b/apps/api/src/app/routes/__chainId/markets/__baseTokenAddress-__quoteTokenAddress/slippageTolerance.ts @@ -1,21 +1,11 @@ -import { - SlippageService, - slippageServiceSymbol, - VolatilityDetails, -} from '@cowprotocol/services'; -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue, -} from '../../../../../utils/cache'; -import { apiContainer } from '../../../../inversify.config'; -import { - ETHEREUM_ADDRESS_PATTERN, - SupportedChainIdSchema, -} from '../../../../schemas'; +import { SlippageService, slippageServiceSymbol, VolatilityDetails } from '@cowprotocol/services' +import { FastifyPluginAsync } from 'fastify' +import { FromSchema, JSONSchema } from 'json-schema-to-ts' +import { CACHE_CONTROL_HEADER, getCacheControlHeaderValue } from '../../../../../utils/cache' +import { apiContainer } from '../../../../inversify.config' +import { ETHEREUM_ADDRESS_PATTERN, SupportedChainIdSchema } from '../../../../schemas' -const CACHE_SECONDS = 120; +const CACHE_SECONDS = 120 const routeSchema = { type: 'object', @@ -36,7 +26,7 @@ const routeSchema = { pattern: ETHEREUM_ADDRESS_PATTERN, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const queryStringSchema = { type: 'object', @@ -48,7 +38,7 @@ const queryStringSchema = { expirationTimeInSeconds: { type: 'number' }, feeAmount: { type: 'string' }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const successSchema = { type: 'object', @@ -57,35 +47,31 @@ const successSchema = { properties: { slippageBps: { title: 'Slippage tolerance in basis points', - description: - 'Slippage tolerance in basis points. One basis point is equivalent to 0.01% (1/100th of a percent)', + description: 'Slippage tolerance in basis points. One basis point is equivalent to 0.01% (1/100th of a percent)', type: 'number', examples: [50, 100, 200], minimum: 0, maximum: 10000, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -type RouteSchema = FromSchema; -type SuccessSchema = FromSchema; +type RouteSchema = FromSchema +type SuccessSchema = FromSchema -const slippageService: SlippageService = apiContainer.get( - slippageServiceSymbol -); +const slippageService: SlippageService = apiContainer.get(slippageServiceSymbol) const root: FastifyPluginAsync = async (fastify): Promise => { // example (basic): http://localhost:3010/1/markets/0x6b175474e89094c44da98b954eedeac495271d0f-0x2260fac5e5542a773aa44fbcfedf7c193bc2c599/slippageTolerance // example (with optional params): http://localhost:3010/1/markets/0x6b175474e89094c44da98b954eedeac495271d0f-0x2260fac5e5542a773aa44fbcfedf7c193bc2c599/slippageTolerance?orderKind=sell&partiallyFillable=false&sellAmount=123456&expirationTimeInSeconds=1800 fastify.get<{ - Params: RouteSchema; - Reply: SuccessSchema; + Params: RouteSchema + Reply: SuccessSchema }>( '/slippageTolerance', { schema: { - description: - 'Retrieve a proposed slippage tolerance for a given market', + description: 'Retrieve a proposed slippage tolerance for a given market', tags: ['markets'], params: routeSchema, response: { @@ -94,31 +80,28 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const { chainId, baseTokenAddress, quoteTokenAddress } = request.params; + const { chainId, baseTokenAddress, quoteTokenAddress } = request.params - const queryString = JSON.stringify(request.query); + const queryString = JSON.stringify(request.query) fastify.log.info( `Get default slippage for market ${baseTokenAddress}-${quoteTokenAddress} on chain ${chainId}. Query: ${queryString}` - ); + ) const slippageBps = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); - reply.header( - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue(CACHE_SECONDS) - ); - reply.send({ slippageBps }); + }) + reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(CACHE_SECONDS)) + reply.send({ slippageBps }) } - ); + ) fastify.get<{ - Params: RouteSchema; + Params: RouteSchema Reply: { - baseToken: VolatilityDetails | null; - quoteToken: VolatilityDetails | null; - }; + baseToken: VolatilityDetails | null + quoteToken: VolatilityDetails | null + } }>( '/volatilityDetails', { @@ -129,33 +112,24 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const { chainId, baseTokenAddress, quoteTokenAddress } = request.params; + const { chainId, baseTokenAddress, quoteTokenAddress } = request.params - const queryString = JSON.stringify(request.query); + const queryString = JSON.stringify(request.query) fastify.log.info( `Get volatility details for market ${baseTokenAddress}-${quoteTokenAddress} on chain ${chainId}. Query: ${queryString}`, JSON.stringify(request.query) - ); - const volatilityDetailsBase = await slippageService.getVolatilityDetails( - chainId, - baseTokenAddress - ); + ) + const volatilityDetailsBase = await slippageService.getVolatilityDetails(chainId, baseTokenAddress) - const volatilityDetailsQuote = await slippageService.getVolatilityDetails( - chainId, - quoteTokenAddress - ); + const volatilityDetailsQuote = await slippageService.getVolatilityDetails(chainId, quoteTokenAddress) - reply.header( - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue(CACHE_SECONDS) - ); + reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(CACHE_SECONDS)) reply.send({ baseToken: volatilityDetailsBase, quoteToken: volatilityDetailsQuote, - }); + }) } - ); -}; + ) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/__chainId/simulation/simulateBundle.ts b/apps/api/src/app/routes/__chainId/simulation/simulateBundle.ts index 9da503d2..3fd778d9 100644 --- a/apps/api/src/app/routes/__chainId/simulation/simulateBundle.ts +++ b/apps/api/src/app/routes/__chainId/simulation/simulateBundle.ts @@ -1,11 +1,8 @@ -import { - SimulationService, - simulationServiceSymbol, -} from '@cowprotocol/services'; -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { apiContainer } from '../../../inversify.config'; -import { AddressSchema, SupportedChainIdSchema } from '../../../schemas'; +import { SimulationService, simulationServiceSymbol } from '@cowprotocol/services' +import { FastifyPluginAsync } from 'fastify' +import { FromSchema, JSONSchema } from 'json-schema-to-ts' +import { apiContainer } from '../../../inversify.config' +import { AddressSchema, SupportedChainIdSchema } from '../../../schemas' const paramsSchema = { type: 'object', @@ -14,7 +11,7 @@ const paramsSchema = { properties: { chainId: SupportedChainIdSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const successSchema = { type: 'array', @@ -117,7 +114,7 @@ const successSchema = { }, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const bodySchema = { type: 'array', @@ -150,7 +147,7 @@ const bodySchema = { }, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const errorSchema = { type: 'object', @@ -163,22 +160,20 @@ const errorSchema = { type: 'string', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -type RouteSchema = FromSchema; -type SuccessSchema = FromSchema; -type ErrorSchema = FromSchema; -type BodySchema = FromSchema; +type RouteSchema = FromSchema +type SuccessSchema = FromSchema +type ErrorSchema = FromSchema +type BodySchema = FromSchema -const tenderlyService: SimulationService = apiContainer.get( - simulationServiceSymbol -); +const tenderlyService: SimulationService = apiContainer.get(simulationServiceSymbol) const root: FastifyPluginAsync = async (fastify): Promise => { fastify.post<{ - Params: RouteSchema; - Reply: SuccessSchema | ErrorSchema; - Body: BodySchema; + Params: RouteSchema + Reply: SuccessSchema | ErrorSchema + Body: BodySchema }>( '/simulateBundle', { @@ -193,33 +188,25 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, async function (request, reply) { try { - const { chainId } = request.params; + const { chainId } = request.params - fastify.log.info( - `Starting simulation of ${request.body.length} transactions on chain ${chainId}` - ); + fastify.log.info(`Starting simulation of ${request.body.length} transactions on chain ${chainId}`) - const simulationResult = - await tenderlyService.postTenderlyBundleSimulation( - chainId, - request.body - ); + const simulationResult = await tenderlyService.postTenderlyBundleSimulation(chainId, request.body) if (simulationResult === null) { - reply.code(400).send({ message: 'Build simulation error' }); - return; + reply.code(400).send({ message: 'Build simulation error' }) + return } - fastify.log.info( - `Post bundle of ${request.body.length} simulation on chain ${chainId}` - ); + fastify.log.info(`Post bundle of ${request.body.length} simulation on chain ${chainId}`) - reply.send(simulationResult); + reply.send(simulationResult) } catch (e) { - fastify.log.error('Error in /simulateBundle', e); - reply.code(500).send({ message: 'Error in /simulateBundle' }); + fastify.log.error('Error in /simulateBundle', e) + reply.code(500).send({ message: 'Error in /simulateBundle' }) } } - ); -}; + ) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/details.ts b/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/details.ts index 560b1331..2554fa77 100644 --- a/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/details.ts +++ b/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/details.ts @@ -1,11 +1,8 @@ -import { - TokenDetailService, - tokenDetailServiceSymbol, -} from '@cowprotocol/services'; -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { apiContainer } from '../../../../inversify.config'; -import { AddressSchema, SupportedChainIdSchema } from '../../../../schemas'; +import { TokenDetailService, tokenDetailServiceSymbol } from '@cowprotocol/services' +import { FastifyPluginAsync } from 'fastify' +import { FromSchema, JSONSchema } from 'json-schema-to-ts' +import { apiContainer } from '../../../../inversify.config' +import { AddressSchema, SupportedChainIdSchema } from '../../../../schemas' const paramsSchema = { type: 'object', @@ -15,7 +12,7 @@ const paramsSchema = { chainId: SupportedChainIdSchema, tokenAddress: AddressSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const successSchema = { type: 'object', @@ -43,7 +40,7 @@ const successSchema = { type: 'integer', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const errorSchema = { type: 'object', @@ -56,21 +53,19 @@ const errorSchema = { type: 'string', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -type ParamsSchema = FromSchema; -type SuccessSchema = FromSchema; -type ErrorSchema = FromSchema; +type ParamsSchema = FromSchema +type SuccessSchema = FromSchema +type ErrorSchema = FromSchema -const tokenDetailService: TokenDetailService = apiContainer.get( - tokenDetailServiceSymbol -); +const tokenDetailService: TokenDetailService = apiContainer.get(tokenDetailServiceSymbol) const root: FastifyPluginAsync = async (fastify): Promise => { // example: http://localhost:3010/1/tokens/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/details fastify.get<{ - Params: ParamsSchema; - Reply: SuccessSchema | ErrorSchema; + Params: ParamsSchema + Reply: SuccessSchema | ErrorSchema }>( '/details', { @@ -85,21 +80,18 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const { chainId, tokenAddress } = request.params; + const { chainId, tokenAddress } = request.params - const token = await tokenDetailService.getTokenDetails( - chainId, - tokenAddress - ); + const token = await tokenDetailService.getTokenDetails(chainId, tokenAddress) if (token === null) { - reply.code(404).send({ message: 'Token not found' }); - return; + reply.code(404).send({ message: 'Token not found' }) + return } - reply.send(token); + reply.send(token) } - ); -}; + ) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/topHolders.ts b/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/topHolders.ts index 07b51f50..2e0431e5 100644 --- a/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/topHolders.ts +++ b/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/topHolders.ts @@ -1,11 +1,8 @@ -import { - TokenHolderService, - tokenHolderServiceSymbol, -} from '@cowprotocol/services'; -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { apiContainer } from '../../../../inversify.config'; -import { AddressSchema, SupportedChainIdSchema } from '../../../../schemas'; +import { TokenHolderService, tokenHolderServiceSymbol } from '@cowprotocol/services' +import { FastifyPluginAsync } from 'fastify' +import { FromSchema, JSONSchema } from 'json-schema-to-ts' +import { apiContainer } from '../../../../inversify.config' +import { AddressSchema, SupportedChainIdSchema } from '../../../../schemas' const paramsSchema = { type: 'object', @@ -15,7 +12,7 @@ const paramsSchema = { chainId: SupportedChainIdSchema, tokenAddress: AddressSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const successSchema = { type: 'array', @@ -36,7 +33,7 @@ const successSchema = { }, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const errorSchema = { type: 'object', @@ -49,21 +46,19 @@ const errorSchema = { type: 'string', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -type RouteSchema = FromSchema; -type SuccessSchema = FromSchema; -type ErrorSchema = FromSchema; +type RouteSchema = FromSchema +type SuccessSchema = FromSchema +type ErrorSchema = FromSchema -const tokenHolderService: TokenHolderService = apiContainer.get( - tokenHolderServiceSymbol -); +const tokenHolderService: TokenHolderService = apiContainer.get(tokenHolderServiceSymbol) const root: FastifyPluginAsync = async (fastify): Promise => { // example: http://localhost:3010/1/tokens/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/topHolders fastify.get<{ - Params: RouteSchema; - Reply: SuccessSchema | ErrorSchema; + Params: RouteSchema + Reply: SuccessSchema | ErrorSchema }>( '/topHolders', { @@ -77,23 +72,20 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const { chainId, tokenAddress } = request.params; + const { chainId, tokenAddress } = request.params - const tokenHolders = await tokenHolderService.getTopTokenHolders( - chainId, - tokenAddress - ); + const tokenHolders = await tokenHolderService.getTopTokenHolders(chainId, tokenAddress) fastify.log.info( `Get token holders for ${tokenAddress} on chain ${chainId}: ${tokenHolders?.length} holder found` - ); + ) if (tokenHolders === null) { - reply.code(404).send({ message: 'Token holders not found' }); - return; + reply.code(404).send({ message: 'Token holders not found' }) + return } - reply.send(tokenHolders); + reply.send(tokenHolders) } - ); -}; + ) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/usdPrice.ts b/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/usdPrice.ts index ae4adf34..11346e31 100644 --- a/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/usdPrice.ts +++ b/apps/api/src/app/routes/__chainId/tokens/__tokenAddress/usdPrice.ts @@ -1,11 +1,8 @@ -import { UsdService, usdServiceSymbol } from '@cowprotocol/services'; -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { apiContainer } from '../../../../inversify.config'; -import { - ChainIdOrSlugSchema, - OptionalAddressSchema, -} from '../../../../schemas'; +import { UsdService, usdServiceSymbol } from '@cowprotocol/services' +import { FastifyPluginAsync } from 'fastify' +import { FromSchema, JSONSchema } from 'json-schema-to-ts' +import { apiContainer } from '../../../../inversify.config' +import { ChainIdOrSlugSchema, OptionalAddressSchema } from '../../../../schemas' const paramsSchema = { type: 'object', @@ -15,7 +12,7 @@ const paramsSchema = { chainId: ChainIdOrSlugSchema, tokenAddress: OptionalAddressSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const successSchema = { type: 'object', @@ -29,7 +26,7 @@ const successSchema = { examples: [3561.1267842], }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const errorSchema = { type: 'object', @@ -43,19 +40,19 @@ const errorSchema = { examples: ['Price not found'], }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -type RouteSchema = FromSchema; -type SuccessSchema = FromSchema; -type ErrorSchema = FromSchema; +type RouteSchema = FromSchema +type SuccessSchema = FromSchema +type ErrorSchema = FromSchema -const usdService: UsdService = apiContainer.get(usdServiceSymbol); +const usdService: UsdService = apiContainer.get(usdServiceSymbol) const root: FastifyPluginAsync = async (fastify): Promise => { // example: http://localhost:3010/1/tokens/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/usdPrice fastify.get<{ - Params: RouteSchema; - Reply: SuccessSchema | ErrorSchema; + Params: RouteSchema + Reply: SuccessSchema | ErrorSchema }>( '/usdPrice', { @@ -70,28 +67,23 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const { chainId, tokenAddress: _tokenAddress } = request.params; + const { chainId, tokenAddress: _tokenAddress } = request.params /** * The token address is optional. If it is not provided, it should be '-'. * @see {@link OptionalAddressSchema} */ const tokenAddress = _tokenAddress === '-' ? undefined : _tokenAddress - const price = await usdService.getUsdPrice( - chainId, - tokenAddress - ); - fastify.log.info( - `Get USD value for ${tokenAddress} on chain ${chainId}: ${price}` - ); + const price = await usdService.getUsdPrice(chainId, tokenAddress) + fastify.log.info(`Get USD value for ${tokenAddress} on chain ${chainId}: ${price}`) if (price === null) { - reply.code(404).send({ message: 'Price not found' }); - return; + reply.code(404).send({ message: 'Price not found' }) + return } - reply.send({ price }); + reply.send({ price }) } - ); -}; + ) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/__chainId/tokens/details.ts b/apps/api/src/app/routes/__chainId/tokens/details.ts index a33d8723..a44b2c8d 100644 --- a/apps/api/src/app/routes/__chainId/tokens/details.ts +++ b/apps/api/src/app/routes/__chainId/tokens/details.ts @@ -1,11 +1,8 @@ -import { - TokenDetailService, - tokenDetailServiceSymbol, -} from '@cowprotocol/services'; -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { apiContainer } from '../../../inversify.config'; -import { SupportedChainIdSchema, ETHEREUM_ADDRESS_PATTERN } from '../../../schemas'; +import { TokenDetailService, tokenDetailServiceSymbol } from '@cowprotocol/services' +import { FastifyPluginAsync } from 'fastify' +import { FromSchema, JSONSchema } from 'json-schema-to-ts' +import { apiContainer } from '../../../inversify.config' +import { SupportedChainIdSchema, ETHEREUM_ADDRESS_PATTERN } from '../../../schemas' const paramsSchema = { type: 'object', @@ -14,7 +11,7 @@ const paramsSchema = { properties: { chainId: SupportedChainIdSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const bodySchema = { type: 'object', @@ -33,7 +30,7 @@ const bodySchema = { maxItems: 100, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const tokenSchema = { type: 'object', @@ -61,12 +58,12 @@ const tokenSchema = { type: 'integer', }, }, -} as const; +} as const const successSchema = { type: 'array', items: tokenSchema, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema const errorSchema = { type: 'object', @@ -79,29 +76,26 @@ const errorSchema = { type: 'string', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -type ParamsSchema = FromSchema; -type BodySchema = FromSchema; -type SuccessSchema = FromSchema; -type ErrorSchema = FromSchema; +type ParamsSchema = FromSchema +type BodySchema = FromSchema +type SuccessSchema = FromSchema +type ErrorSchema = FromSchema -const tokenDetailService: TokenDetailService = apiContainer.get( - tokenDetailServiceSymbol -); +const tokenDetailService: TokenDetailService = apiContainer.get(tokenDetailServiceSymbol) const root: FastifyPluginAsync = async (fastify): Promise => { // example: POST http://localhost:3010/1/tokens/details { "tokenAddresses": ["0xC02...", "0xA0b..."] } fastify.post<{ - Params: ParamsSchema; - Body: BodySchema; - Reply: SuccessSchema | ErrorSchema; + Params: ParamsSchema + Body: BodySchema + Reply: SuccessSchema | ErrorSchema }>( '/details', { schema: { - description: - 'Get details (name, symbol, decimals) for multiple tokens', + description: 'Get details (name, symbol, decimals) for multiple tokens', tags: ['tokens'], params: paramsSchema, body: bodySchema, @@ -112,26 +106,21 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const { chainId } = request.params; - const { tokenAddresses } = request.body; + const { chainId } = request.params + const { tokenAddresses } = request.body - const tokens = await tokenDetailService.getTokensDetails( - chainId, - tokenAddresses - ); + const tokens = await tokenDetailService.getTokensDetails(chainId, tokenAddresses) - const result = tokens.filter( - (token): token is NonNullable => token !== null - ); + const result = tokens.filter((token): token is NonNullable => token !== null) if (result.length === 0) { - reply.code(404).send({ message: 'No tokens found' }); - return; + reply.code(404).send({ message: 'No tokens found' }) + return } - reply.send(result); + reply.send(result) } - ); -}; + ) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/__chainId/yield/const.ts b/apps/api/src/app/routes/__chainId/yield/const.ts index 37ad1b4a..5f0d7bb6 100644 --- a/apps/api/src/app/routes/__chainId/yield/const.ts +++ b/apps/api/src/app/routes/__chainId/yield/const.ts @@ -1,4 +1,4 @@ -import ms from 'ms'; +import ms from 'ms' export const POOLS_RESULT_LIMIT = 500 -export const POOLS_QUERY_CACHE = ms('12h') \ No newline at end of file +export const POOLS_QUERY_CACHE = ms('12h') diff --git a/apps/api/src/app/routes/__chainId/yield/getPoolsAverageApr.ts b/apps/api/src/app/routes/__chainId/yield/getPoolsAverageApr.ts index 593ffbe6..6dbac7a5 100644 --- a/apps/api/src/app/routes/__chainId/yield/getPoolsAverageApr.ts +++ b/apps/api/src/app/routes/__chainId/yield/getPoolsAverageApr.ts @@ -1,32 +1,25 @@ -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema } from 'json-schema-to-ts'; -import { PoolInfo } from '../../../data/poolInfo'; -import { - errorSchema, - paramsSchema, - poolsAverageAprBodySchema, -} from './schemas'; -import { trimDoubleQuotes } from './utils'; -import { - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue, -} from '../../../../utils/cache'; +import { FastifyPluginAsync } from 'fastify' +import { FromSchema } from 'json-schema-to-ts' +import { PoolInfo } from '../../../data/poolInfo' +import { errorSchema, paramsSchema, poolsAverageAprBodySchema } from './schemas' +import { trimDoubleQuotes } from './utils' +import { CACHE_CONTROL_HEADER, getCacheControlHeaderValue } from '../../../../utils/cache' -type RouteSchema = FromSchema; -type SuccessSchema = FromSchema; -type ErrorSchema = FromSchema; +type RouteSchema = FromSchema +type SuccessSchema = FromSchema +type ErrorSchema = FromSchema interface PoolInfoResult { - project: string; - average_apr: number; + project: string + average_apr: number } -const CACHE_SECONDS = 21600; // 6 hours +const CACHE_SECONDS = 21600 // 6 hours const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get<{ - Params: RouteSchema; - Reply: SuccessSchema | ErrorSchema; + Params: RouteSchema + Reply: SuccessSchema | ErrorSchema }>( '/pools-average-apr', { @@ -40,9 +33,9 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const { chainId } = request.params; + const { chainId } = request.params - const poolInfoRepository = fastify.orm.analytics.getRepository(PoolInfo); + const poolInfoRepository = fastify.orm.analytics.getRepository(PoolInfo) const result = await poolInfoRepository.query(` SELECT project, @@ -50,26 +43,20 @@ const root: FastifyPluginAsync = async (fastify): Promise => { FROM cow_amm_competitor_info WHERE chain_id = ${chainId} GROUP BY project; - `); + `) - const averageApr = result.reduce( - (acc: Record, val: PoolInfoResult) => { - const projectName = trimDoubleQuotes(val.project); + const averageApr = result.reduce((acc: Record, val: PoolInfoResult) => { + const projectName = trimDoubleQuotes(val.project) - acc[projectName] = +val.average_apr.toFixed(6); + acc[projectName] = +val.average_apr.toFixed(6) - return acc; - }, - {} - ); + return acc + }, {}) - reply.header( - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue(CACHE_SECONDS) - ); - reply.status(200).send(averageApr); + reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(CACHE_SECONDS)) + reply.status(200).send(averageApr) } - ); -}; + ) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/__chainId/yield/getPoolsInfo.ts b/apps/api/src/app/routes/__chainId/yield/getPoolsInfo.ts index ffb2bae0..372fed15 100644 --- a/apps/api/src/app/routes/__chainId/yield/getPoolsInfo.ts +++ b/apps/api/src/app/routes/__chainId/yield/getPoolsInfo.ts @@ -1,34 +1,27 @@ -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema } from 'json-schema-to-ts'; -import { PoolInfo } from '../../../data/poolInfo'; -import { In } from 'typeorm'; -import { - poolsInfoBodySchema, - errorSchema, - paramsSchema, - poolsInfoSuccessSchema, -} from './schemas'; -import { POOLS_QUERY_CACHE, POOLS_RESULT_LIMIT } from './const'; -import { trimDoubleQuotes } from './utils'; -import { isDbEnabled } from '@cowprotocol/repositories'; +import { FastifyPluginAsync } from 'fastify' +import { FromSchema } from 'json-schema-to-ts' +import { PoolInfo } from '../../../data/poolInfo' +import { In } from 'typeorm' +import { poolsInfoBodySchema, errorSchema, paramsSchema, poolsInfoSuccessSchema } from './schemas' +import { POOLS_QUERY_CACHE, POOLS_RESULT_LIMIT } from './const' +import { trimDoubleQuotes } from './utils' +import { isDbEnabled } from '@cowprotocol/repositories' -type RouteSchema = FromSchema; -type SuccessSchema = FromSchema; -type ErrorSchema = FromSchema; -type BodySchema = FromSchema; +type RouteSchema = FromSchema +type SuccessSchema = FromSchema +type ErrorSchema = FromSchema +type BodySchema = FromSchema const root: FastifyPluginAsync = async (fastify): Promise => { if (!isDbEnabled) { - fastify.log.warn( - 'Database is disabled. /pools endpoint will not be available' - ); - return; + fastify.log.warn('Database is disabled. /pools endpoint will not be available') + return } fastify.post<{ - Params: RouteSchema; - Reply: SuccessSchema | ErrorSchema; - Body: BodySchema; + Params: RouteSchema + Reply: SuccessSchema | ErrorSchema + Body: BodySchema }>( '/pools', { @@ -43,32 +36,30 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const { chainId } = request.params; - const poolsAddresses = request.body; + const { chainId } = request.params + const poolsAddresses = request.body - const poolInfoRepository = fastify.orm.analytics.getRepository(PoolInfo); + const poolInfoRepository = fastify.orm.analytics.getRepository(PoolInfo) const results = await poolInfoRepository.find({ take: POOLS_RESULT_LIMIT, where: { - ...(poolsAddresses.length > 0 - ? { contract_address: In(poolsAddresses) } - : null), + ...(poolsAddresses.length > 0 ? { contract_address: In(poolsAddresses) } : null), chain_id: chainId, }, cache: POOLS_QUERY_CACHE, - }); + }) const mappedResults = results.map((res) => { return { ...res, project: trimDoubleQuotes(res.project), - }; - }); + } + }) - reply.status(200).send(mappedResults); + reply.status(200).send(mappedResults) } - ); -}; + ) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/__chainId/yield/schemas.ts b/apps/api/src/app/routes/__chainId/yield/schemas.ts index cc4b4273..7112771d 100644 --- a/apps/api/src/app/routes/__chainId/yield/schemas.ts +++ b/apps/api/src/app/routes/__chainId/yield/schemas.ts @@ -1,6 +1,6 @@ -import { JSONSchema } from 'json-schema-to-ts'; -import { AddressSchema, SupportedChainIdSchema } from '../../../schemas'; -import { POOLS_RESULT_LIMIT } from './const'; +import { JSONSchema } from 'json-schema-to-ts' +import { AddressSchema, SupportedChainIdSchema } from '../../../schemas' +import { POOLS_RESULT_LIMIT } from './const' export const paramsSchema = { type: 'object', @@ -9,21 +9,13 @@ export const paramsSchema = { properties: { chainId: SupportedChainIdSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const poolsInfoSuccessSchema = { type: 'array', items: { type: 'object', - required: [ - 'contract_address', - 'chain_id', - 'project', - 'apr', - 'fee', - 'tvl', - 'volume', - ], + required: ['contract_address', 'chain_id', 'project', 'apr', 'fee', 'tvl', 'volume'], additionalProperties: false, properties: { contract_address: { @@ -58,7 +50,7 @@ export const poolsInfoSuccessSchema = { }, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const poolsInfoBodySchema = { type: 'array', @@ -69,13 +61,13 @@ export const poolsInfoBodySchema = { pattern: AddressSchema.pattern, }, maxItems: POOLS_RESULT_LIMIT, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const poolsAverageAprBodySchema = { type: 'object', title: 'Liquidity provider - apr', additionalProperties: true, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const errorSchema = { type: 'object', @@ -88,4 +80,4 @@ export const errorSchema = { type: 'string', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema diff --git a/apps/api/src/app/routes/__chainId/yield/types.ts b/apps/api/src/app/routes/__chainId/yield/types.ts index 4e22d63f..286abda3 100644 --- a/apps/api/src/app/routes/__chainId/yield/types.ts +++ b/apps/api/src/app/routes/__chainId/yield/types.ts @@ -3,4 +3,4 @@ export interface PoolInfo { tvl: number feeTier: number volume24h: number -} \ No newline at end of file +} diff --git a/apps/api/src/app/routes/__chainId/yield/utils.ts b/apps/api/src/app/routes/__chainId/yield/utils.ts index 7b039229..39446ce8 100644 --- a/apps/api/src/app/routes/__chainId/yield/utils.ts +++ b/apps/api/src/app/routes/__chainId/yield/utils.ts @@ -8,4 +8,4 @@ export function trimDoubleQuotes(value: string): string { } return value -} \ No newline at end of file +} diff --git a/apps/api/src/app/routes/about.ts b/apps/api/src/app/routes/about.ts index 48a84ca6..bb8c94b1 100644 --- a/apps/api/src/app/routes/about.ts +++ b/apps/api/src/app/routes/about.ts @@ -1,25 +1,21 @@ -import { log } from 'console'; -import { FastifyPluginAsync } from 'fastify'; +import { log } from 'console' +import { FastifyPluginAsync } from 'fastify' -import { readFileSync } from 'fs'; -const GIT_COMMIT_HASH_FILE = 'git-commit-hash.txt'; -const VERSION = - process.env.VERSION || 'UNKNOWN, please set the environment variable'; -const COMMIT_HASH = getCommitHash(); -import { join } from 'path'; -import { server } from '../../main'; -import { - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue, -} from '../../utils/cache'; -import ms from 'ms'; +import { readFileSync } from 'fs' +const GIT_COMMIT_HASH_FILE = 'git-commit-hash.txt' +const VERSION = process.env.VERSION || 'UNKNOWN, please set the environment variable' +const COMMIT_HASH = getCommitHash() +import { join } from 'path' +import { server } from '../../main' +import { CACHE_CONTROL_HEADER, getCacheControlHeaderValue } from '../../utils/cache' +import ms from 'ms' -const CACHE_SECONDS = ms('10m') / 1000; +const CACHE_SECONDS = ms('10m') / 1000 interface AboutResponse { - name: string; - version: string; - gitCommitHash?: string | undefined; + name: string + version: string + gitCommitHash?: string | undefined } const about: FastifyPluginAsync = async (fastify): Promise => { @@ -32,34 +28,31 @@ const about: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (_request, reply) { - reply.header( - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue(CACHE_SECONDS) - ); + reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(CACHE_SECONDS)) return reply.send({ name: 'BFF API', version: VERSION, gitCommitHash: COMMIT_HASH, - }); + }) } - ); -}; + ) +} /** * Read a file with the git commit hash (generated for example using github actions) */ function getCommitHash(): string | undefined { - const filePath = join(__dirname, '../..', GIT_COMMIT_HASH_FILE); + const filePath = join(__dirname, '../..', GIT_COMMIT_HASH_FILE) try { - return readFileSync(filePath, 'utf-8'); + return readFileSync(filePath, 'utf-8') } catch (error) { // Not a big deal, if the file is not present, the about won't server.log.warn( `Unable to read the file with the git commit hash: ${filePath}. /about endpoint won't export the 'gitCommitHash'` - ); - return undefined; + ) + return undefined } } -export default about; +export default about diff --git a/apps/api/src/app/routes/accounts/_account/notifications.ts b/apps/api/src/app/routes/accounts/_account/notifications.ts index 8705fa6d..f1ed1397 100644 --- a/apps/api/src/app/routes/accounts/_account/notifications.ts +++ b/apps/api/src/app/routes/accounts/_account/notifications.ts @@ -1,21 +1,18 @@ -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { ETHEREUM_ADDRESS_PATTERN } from '../../../schemas'; -import { - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue, -} from '../../../../utils/cache'; -import ms from 'ms'; +import { FastifyPluginAsync } from 'fastify' +import { FromSchema, JSONSchema } from 'json-schema-to-ts' +import { ETHEREUM_ADDRESS_PATTERN } from '../../../schemas' +import { CACHE_CONTROL_HEADER, getCacheControlHeaderValue } from '../../../../utils/cache' +import ms from 'ms' import { isCmsEnabled, NotificationModel, PushSubscriptionsRepository, pushSubscriptionsRepositorySymbol, -} from '@cowprotocol/repositories'; -import { apiContainer } from '../../../inversify.config'; -import { logger } from '@cowprotocol/shared'; +} from '@cowprotocol/repositories' +import { apiContainer } from '../../../inversify.config' +import { logger } from '@cowprotocol/shared' -const CACHE_SECONDS = ms('5m') / 1000; +const CACHE_SECONDS = ms('5m') / 1000 const routeSchema = { type: 'object', @@ -28,28 +25,25 @@ const routeSchema = { pattern: ETHEREUM_ADDRESS_PATTERN, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -type RouteSchema = FromSchema; +type RouteSchema = FromSchema -type GetNotificationsSchema = RouteSchema; +type GetNotificationsSchema = RouteSchema const accounts: FastifyPluginAsync = async (fastify): Promise => { if (!isCmsEnabled) { - logger.warn( - 'CMS is not enabled. Please check CMS_ENABLED and CMS_API_KEY environment variables' - ); + logger.warn('CMS is not enabled. Please check CMS_ENABLED and CMS_API_KEY environment variables') - return; + return } - const pushSubscriptionsRepository: PushSubscriptionsRepository = - apiContainer.get(pushSubscriptionsRepositorySymbol); + const pushSubscriptionsRepository: PushSubscriptionsRepository = apiContainer.get(pushSubscriptionsRepositorySymbol) // GET /accounts/:account/notifications fastify.get<{ - Params: GetNotificationsSchema; - Reply: NotificationModel[]; + Params: GetNotificationsSchema + Reply: NotificationModel[] }>( '/notifications', { @@ -60,19 +54,15 @@ const accounts: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - reply.header( - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue(CACHE_SECONDS) - ); + reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(CACHE_SECONDS)) - const account = request.params.account; - const notifications = - await pushSubscriptionsRepository.getNotificationsByAccount({ - account, - }); - reply.send(notifications); + const account = request.params.account + const notifications = await pushSubscriptionsRepository.getNotificationsByAccount({ + account, + }) + reply.send(notifications) } - ); -}; + ) +} -export default accounts; +export default accounts diff --git a/apps/api/src/app/routes/affiliate/_address/affiliate.schemas.ts b/apps/api/src/app/routes/affiliate/_address/affiliate.schemas.ts index e0f5ac5c..f557d15e 100644 --- a/apps/api/src/app/routes/affiliate/_address/affiliate.schemas.ts +++ b/apps/api/src/app/routes/affiliate/_address/affiliate.schemas.ts @@ -1,6 +1,6 @@ -import { JSONSchema } from 'json-schema-to-ts'; -import { AddressSchema } from '../../../schemas'; -import { AFFILIATE_CODE_REGEX } from '../../../config/affiliate'; +import { JSONSchema } from 'json-schema-to-ts' +import { AddressSchema } from '../../../schemas' +import { AFFILIATE_CODE_REGEX } from '../../../config/affiliate' export const paramsSchema = { type: 'object', @@ -8,7 +8,7 @@ export const paramsSchema = { properties: { address: AddressSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const bodySchema = { type: 'object', @@ -17,8 +17,7 @@ export const bodySchema = { properties: { code: { title: 'Affiliate code', - description: - 'Affiliate code to bind to the wallet. Format: 5-20 uppercase chars (A-Z, 0-9, -, _).', + description: 'Affiliate code to bind to the wallet. Format: 5-20 uppercase chars (A-Z, 0-9, -, _).', type: 'string', minLength: 5, maxLength: 20, @@ -32,7 +31,7 @@ export const bodySchema = { minLength: 1, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const affiliateGetResponseSchema = { type: 'object', @@ -63,7 +62,7 @@ export const affiliateGetResponseSchema = { revenueSplitTraderPct: { type: 'number' }, revenueSplitDaoPct: { type: 'number' }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const affiliateCreateResponseSchema = { type: 'object', @@ -77,7 +76,7 @@ export const affiliateCreateResponseSchema = { type: 'string', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const errorSchema = { type: 'object', @@ -88,4 +87,4 @@ export const errorSchema = { type: 'string', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema diff --git a/apps/api/src/app/routes/affiliate/_address/index.ts b/apps/api/src/app/routes/affiliate/_address/index.ts index 124d687a..400a5599 100644 --- a/apps/api/src/app/routes/affiliate/_address/index.ts +++ b/apps/api/src/app/routes/affiliate/_address/index.ts @@ -1,5 +1,5 @@ -import { FastifyPluginAsync, FastifyReply } from 'fastify'; -import { FromSchema } from 'json-schema-to-ts'; +import { FastifyPluginAsync, FastifyReply } from 'fastify' +import { FromSchema } from 'json-schema-to-ts' import { AffiliatesRepository, affiliatesRepositorySymbol, @@ -7,42 +7,33 @@ import { isCmsEnabled, isCmsRequestError, isDuneEnabled, -} from '@cowprotocol/repositories'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { apiContainer } from '../../../inversify.config'; -import { logger } from '@cowprotocol/shared'; -import { - AffiliateProgramExportService, - affiliateProgramExportServiceSymbol, -} from '@cowprotocol/services'; -import { AFFILIATE_CODE_REGEX } from '../../../config/affiliate'; +} from '@cowprotocol/repositories' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { apiContainer } from '../../../inversify.config' +import { logger } from '@cowprotocol/shared' +import { AffiliateProgramExportService, affiliateProgramExportServiceSymbol } from '@cowprotocol/services' +import { AFFILIATE_CODE_REGEX } from '../../../config/affiliate' import { affiliateCreateResponseSchema, affiliateGetResponseSchema, bodySchema, errorSchema, paramsSchema, -} from './affiliate.schemas'; -import { - SignatureCheckResult, - verifyAffiliateSignature, - type AffiliateTypedData, -} from './signatureVerification'; +} from './affiliate.schemas' +import { SignatureCheckResult, verifyAffiliateSignature, type AffiliateTypedData } from './signatureVerification' -type ParamsSchema = FromSchema; -type BodySchema = FromSchema; -type GetSuccessSchema = FromSchema; -type CreateSuccessSchema = FromSchema; -type ErrorSchema = FromSchema; +type ParamsSchema = FromSchema +type BodySchema = FromSchema +type GetSuccessSchema = FromSchema +type CreateSuccessSchema = FromSchema +type ErrorSchema = FromSchema -const affiliatesRepository: AffiliatesRepository = apiContainer.get( - affiliatesRepositorySymbol -); +const affiliatesRepository: AffiliatesRepository = apiContainer.get(affiliatesRepositorySymbol) const AFFILIATE_TYPED_DATA_DOMAIN = { name: 'CoW Swap Affiliate', version: '1', -}; +} const AFFILIATE_TYPED_DATA_TYPES: AffiliateTypedData['types'] = { AffiliateCode: [ @@ -50,21 +41,19 @@ const AFFILIATE_TYPED_DATA_TYPES: AffiliateTypedData['types'] = { { name: 'code', type: 'string' }, { name: 'chainId', type: 'uint256' }, ], -}; +} -const PAYOUTS_CHAIN_ID = 1; +const PAYOUTS_CHAIN_ID = 1 const affiliate: FastifyPluginAsync = async (fastify): Promise => { if (!isCmsEnabled) { - logger.warn( - 'CMS is not enabled. Please check CMS_ENABLED and CMS_API_KEY environment variables' - ); - return; + logger.warn('CMS is not enabled. Please check CMS_ENABLED and CMS_API_KEY environment variables') + return } // GET /affiliate/:address fastify.get<{ - Params: ParamsSchema; - Reply: GetSuccessSchema | ErrorSchema; + Params: ParamsSchema + Reply: GetSuccessSchema | ErrorSchema }>( '/', { @@ -81,17 +70,16 @@ const affiliate: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const address = normalizeAddress(request.params.address); + const address = normalizeAddress(request.params.address) try { - const affiliateEntry = - await affiliatesRepository.getAffiliateByWalletAddress({ - walletAddress: address, - }); + const affiliateEntry = await affiliatesRepository.getAffiliateByWalletAddress({ + walletAddress: address, + }) if (!affiliateEntry) { - reply.code(404).send({ message: 'Affiliate not found' }); - return; + reply.code(404).send({ message: 'Affiliate not found' }) + return } reply.send({ @@ -104,18 +92,18 @@ const affiliate: FastifyPluginAsync = async (fastify): Promise => { revenueSplitAffiliatePct: affiliateEntry.revenueSplitAffiliatePct, revenueSplitTraderPct: affiliateEntry.revenueSplitTraderPct, revenueSplitDaoPct: affiliateEntry.revenueSplitDaoPct, - }); + }) } catch (error) { - handleCmsError(error, reply); + handleCmsError(error, reply) } } - ); + ) // POST /affiliate/:address fastify.post<{ - Params: ParamsSchema; - Body: BodySchema; - Reply: CreateSuccessSchema | ErrorSchema; + Params: ParamsSchema + Body: BodySchema + Reply: CreateSuccessSchema | ErrorSchema }>( '/', { @@ -132,31 +120,31 @@ const affiliate: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const address = normalizeAddress(request.params.address); - const walletAddress = normalizeAddress(request.body.walletAddress); - const code = normalizeCode(request.body.code); - const signedMessage = request.body.signedMessage; + const address = normalizeAddress(request.params.address) + const walletAddress = normalizeAddress(request.body.walletAddress) + const code = normalizeCode(request.body.code) + const signedMessage = request.body.signedMessage if (address !== walletAddress) { - reply.code(400).send({ message: 'Affiliate wallet address mismatch' }); - return; + reply.code(400).send({ message: 'Affiliate wallet address mismatch' }) + return } if (!code) { - reply.code(400).send({ message: 'Affiliate code is required' }); - return; + reply.code(400).send({ message: 'Affiliate code is required' }) + return } if (!isValidCode(code)) { - reply.code(400).send({ message: 'Affiliate code format is invalid' }); - return; + reply.code(400).send({ message: 'Affiliate code format is invalid' }) + return } const typedData = buildAffiliateTypedData({ walletAddress, code, chainId: PAYOUTS_CHAIN_ID, - }); + }) try { const res = await verifyAffiliateSignature({ @@ -164,50 +152,42 @@ const affiliate: FastifyPluginAsync = async (fastify): Promise => { signedMessage, typedData, client: getViemClients()[SupportedChainId.MAINNET], - }); - - if ( - res === SignatureCheckResult.invalidAddress || - res === SignatureCheckResult.addressIsNotSmartContract - ) { - reply - .code(401) - .send({ message: 'Affiliate signature has invalid address' }); - return; + }) + + if (res === SignatureCheckResult.invalidAddress || res === SignatureCheckResult.addressIsNotSmartContract) { + reply.code(401).send({ message: 'Affiliate signature has invalid address' }) + return } if (res === SignatureCheckResult.invalidSignature) { - reply.code(401).send({ message: 'Affiliate has invalid signature' }); - return; + reply.code(401).send({ message: 'Affiliate has invalid signature' }) + return } } catch (error) { - fastify.log.warn({ error }, 'Affiliate signature verification failed'); - reply.code(401).send({ message: 'Affiliate has invalid signature' }); - return; + fastify.log.warn({ error }, 'Affiliate signature verification failed') + reply.code(401).send({ message: 'Affiliate has invalid signature' }) + return } try { - const existingByWallet = - await affiliatesRepository.getAffiliateByWalletAddress({ - walletAddress, - }); + const existingByWallet = await affiliatesRepository.getAffiliateByWalletAddress({ + walletAddress, + }) if (existingByWallet) { - reply - .code(409) - .send({ - message: 'Affiliate wallet address already bound to a code', - }); - return; + reply.code(409).send({ + message: 'Affiliate wallet address already bound to a code', + }) + return } const existingByCode = await affiliatesRepository.getAffiliateByCode({ code, - }); + }) if (existingByCode) { - reply.code(409).send({ message: 'Affiliate code already taken' }); - return; + reply.code(409).send({ message: 'Affiliate code already taken' }) + return } const affiliateEntry = await affiliatesRepository.createAffiliate({ @@ -215,17 +195,12 @@ const affiliate: FastifyPluginAsync = async (fastify): Promise => { walletAddress, signedMessage: request.body.signedMessage, enabled: true, - }); + }) - fastify.log.info( - { walletAddress, code: affiliateEntry.code }, - 'Affiliate code created' - ); + fastify.log.info({ walletAddress, code: affiliateEntry.code }, 'Affiliate code created') if (isDuneEnabled) { - const exportService = apiContainer.get( - affiliateProgramExportServiceSymbol - ); + const exportService = apiContainer.get(affiliateProgramExportServiceSymbol) void exportService .exportAffiliateProgramData() .then((result) => { @@ -235,32 +210,25 @@ const affiliate: FastifyPluginAsync = async (fastify): Promise => { maxUpdatedAt: result.signature.maxUpdatedAt, }, 'Affiliate program export after create' - ); + ) }) .catch((error) => { - fastify.log.error( - { error }, - 'Affiliate program export after create failed' - ); - }); + fastify.log.error({ error }, 'Affiliate program export after create failed') + }) } reply.code(201).send({ code: affiliateEntry.code, createdAt: affiliateEntry.createdAt, - }); + }) } catch (error) { - handleCmsError(error, reply); + handleCmsError(error, reply) } } - ); -}; - -function buildAffiliateTypedData(params: { - walletAddress: string; - code: string; - chainId: number; -}): AffiliateTypedData { + ) +} + +function buildAffiliateTypedData(params: { walletAddress: string; code: string; chainId: number }): AffiliateTypedData { return { domain: { ...AFFILIATE_TYPED_DATA_DOMAIN, @@ -271,33 +239,33 @@ function buildAffiliateTypedData(params: { code: params.code, chainId: params.chainId, }, - } as const; + } as const } function normalizeAddress(value: string): string { - return value.toLowerCase(); + return value.toLowerCase() } function normalizeCode(value: string): string { - return value.trim().toUpperCase(); + return value.trim().toUpperCase() } function isValidCode(value: string): boolean { - return AFFILIATE_CODE_REGEX.test(value); + return AFFILIATE_CODE_REGEX.test(value) } function handleCmsError(error: unknown, reply: FastifyReply) { if (isCmsRequestError(error)) { if (error.status === 400 || error.status === 409) { - reply.code(409).send({ message: 'Affiliate already exists' }); - return; + reply.code(409).send({ message: 'Affiliate already exists' }) + return } - reply.code(502).send({ message: 'CMS request failed' }); - return; + reply.code(502).send({ message: 'CMS request failed' }) + return } - reply.code(500).send({ message: 'Unexpected error' }); + reply.code(500).send({ message: 'Unexpected error' }) } -export default affiliate; +export default affiliate diff --git a/apps/api/src/app/routes/affiliate/_address/signatureVerification.spec.ts b/apps/api/src/app/routes/affiliate/_address/signatureVerification.spec.ts index 4ed469af..8eb1bb84 100644 --- a/apps/api/src/app/routes/affiliate/_address/signatureVerification.spec.ts +++ b/apps/api/src/app/routes/affiliate/_address/signatureVerification.spec.ts @@ -1,10 +1,6 @@ -import { Wallet } from 'ethers'; +import { Wallet } from 'ethers' -import { - SignatureCheckResult, - AffiliateTypedData, - verifyAffiliateSignature, -} from './signatureVerification'; +import { SignatureCheckResult, AffiliateTypedData, verifyAffiliateSignature } from './signatureVerification' function buildTypedData(walletAddress: string): AffiliateTypedData { return { @@ -24,170 +20,147 @@ function buildTypedData(walletAddress: string): AffiliateTypedData { code: 'COW-12345', chainId: 1, }, - }; + } } describe('verifyAffiliateSignature', () => { it('returns valid for a matching EOA signature', async () => { - const wallet = Wallet.createRandom(); - const typedData = buildTypedData(wallet.address); - const signedMessage = await wallet._signTypedData( - typedData.domain, - typedData.types, - typedData.message - ); + const wallet = Wallet.createRandom() + const typedData = buildTypedData(wallet.address) + const signedMessage = await wallet._signTypedData(typedData.domain, typedData.types, typedData.message) const client = { getBytecode: jest.fn(), readContract: jest.fn(), - }; + } const result = await verifyAffiliateSignature({ walletAddress: wallet.address, signedMessage, typedData, client, - }); + }) - expect(result).toBe(SignatureCheckResult.valid); - expect(client.getBytecode).not.toHaveBeenCalled(); - }); + expect(result).toBe(SignatureCheckResult.valid) + expect(client.getBytecode).not.toHaveBeenCalled() + }) it('returns invalidAddress for mismatched EOA signer', async () => { - const signer = Wallet.createRandom(); - const walletAddress = Wallet.createRandom().address; - const typedData = buildTypedData(walletAddress); - const signedMessage = await signer._signTypedData( - typedData.domain, - typedData.types, - typedData.message - ); + const signer = Wallet.createRandom() + const walletAddress = Wallet.createRandom().address + const typedData = buildTypedData(walletAddress) + const signedMessage = await signer._signTypedData(typedData.domain, typedData.types, typedData.message) const client = { getBytecode: jest.fn().mockResolvedValue(null), readContract: jest.fn(), - }; + } const result = await verifyAffiliateSignature({ walletAddress, signedMessage, typedData, client, - }); + }) - expect(result).toBe(SignatureCheckResult.invalidAddress); - expect(client.readContract).not.toHaveBeenCalled(); - }); + expect(result).toBe(SignatureCheckResult.invalidAddress) + expect(client.readContract).not.toHaveBeenCalled() + }) it('returns valid for contract wallets via EIP-1271', async () => { - const signer = Wallet.createRandom(); - const walletAddress = Wallet.createRandom().address; - const typedData = buildTypedData(walletAddress); - const signedMessage = await signer._signTypedData( - typedData.domain, - typedData.types, - typedData.message - ); + const signer = Wallet.createRandom() + const walletAddress = Wallet.createRandom().address + const typedData = buildTypedData(walletAddress) + const signedMessage = await signer._signTypedData(typedData.domain, typedData.types, typedData.message) const client = { getBytecode: jest.fn().mockResolvedValue('0x1234'), readContract: jest.fn().mockResolvedValue('0x1626ba7e00000000000000000000000000000000000000000000000000000000'), - }; + } const result = await verifyAffiliateSignature({ walletAddress, signedMessage, typedData, client, - }); + }) - expect(result).toBe(SignatureCheckResult.valid); - expect(client.readContract).toHaveBeenCalledTimes(1); - }); + expect(result).toBe(SignatureCheckResult.valid) + expect(client.readContract).toHaveBeenCalledTimes(1) + }) it('returns invalidAddress when EIP-1271 magic value is not returned', async () => { - const signer = Wallet.createRandom(); - const walletAddress = Wallet.createRandom().address; - const typedData = buildTypedData(walletAddress); - const signedMessage = await signer._signTypedData( - typedData.domain, - typedData.types, - typedData.message - ); + const signer = Wallet.createRandom() + const walletAddress = Wallet.createRandom().address + const typedData = buildTypedData(walletAddress) + const signedMessage = await signer._signTypedData(typedData.domain, typedData.types, typedData.message) const client = { getBytecode: jest.fn().mockResolvedValue('0x1234'), readContract: jest.fn().mockResolvedValue('0xffffffff'), - }; + } const result = await verifyAffiliateSignature({ walletAddress, signedMessage, typedData, client, - }); + }) - expect(result).toBe(SignatureCheckResult.invalidAddress); - }); + expect(result).toBe(SignatureCheckResult.invalidAddress) + }) it('returns invalidSignature for malformed signatures', async () => { - const walletAddress = Wallet.createRandom().address; - const typedData = buildTypedData(walletAddress); + const walletAddress = Wallet.createRandom().address + const typedData = buildTypedData(walletAddress) const client = { getBytecode: jest.fn().mockResolvedValue('0x1234'), readContract: jest.fn(), - }; + } const result = await verifyAffiliateSignature({ walletAddress, signedMessage: 'not-a-hex-signature', typedData, client, - }); + }) - expect(result).toBe(SignatureCheckResult.invalidSignature); - expect(client.getBytecode).not.toHaveBeenCalled(); - }); + expect(result).toBe(SignatureCheckResult.invalidSignature) + expect(client.getBytecode).not.toHaveBeenCalled() + }) it('returns invalidSignature when EIP-1271 contract call fails', async () => { - const signer = Wallet.createRandom(); - const walletAddress = Wallet.createRandom().address; - const typedData = buildTypedData(walletAddress); - const signedMessage = await signer._signTypedData( - typedData.domain, - typedData.types, - typedData.message - ); + const signer = Wallet.createRandom() + const walletAddress = Wallet.createRandom().address + const typedData = buildTypedData(walletAddress) + const signedMessage = await signer._signTypedData(typedData.domain, typedData.types, typedData.message) const client = { getBytecode: jest.fn().mockResolvedValue('0x1234'), readContract: jest.fn().mockRejectedValue(new Error('rpc failed')), - }; + } const result = await verifyAffiliateSignature({ walletAddress, signedMessage, typedData, client, - }); + }) - expect(result).toBe(SignatureCheckResult.invalidSignature); - }); + expect(result).toBe(SignatureCheckResult.invalidSignature) + }) it('returns addressIsNotSmartContract when recovery fails for an EOA address', async () => { - const walletAddress = Wallet.createRandom().address; - const typedData = buildTypedData(walletAddress); + const walletAddress = Wallet.createRandom().address + const typedData = buildTypedData(walletAddress) const client = { getBytecode: jest.fn().mockResolvedValue('0x'), readContract: jest.fn(), - }; + } const result = await verifyAffiliateSignature({ walletAddress, - signedMessage: - '0x11111111111111111111111111111111111111111111111111111111111111111b', + signedMessage: '0x11111111111111111111111111111111111111111111111111111111111111111b', typedData, client, - }); - - expect(result).toBe( - SignatureCheckResult.addressIsNotSmartContract - ); - expect(client.readContract).not.toHaveBeenCalled(); - }); -}); + }) + + expect(result).toBe(SignatureCheckResult.addressIsNotSmartContract) + expect(client.readContract).not.toHaveBeenCalled() + }) +}) diff --git a/apps/api/src/app/routes/affiliate/_address/signatureVerification.ts b/apps/api/src/app/routes/affiliate/_address/signatureVerification.ts index c994e1a3..8fbc3aa7 100644 --- a/apps/api/src/app/routes/affiliate/_address/signatureVerification.ts +++ b/apps/api/src/app/routes/affiliate/_address/signatureVerification.ts @@ -1,13 +1,7 @@ -import { logger } from '@cowprotocol/shared'; -import { - Address, - hashTypedData, - isHex, - PublicClient, - verifyTypedData, -} from 'viem'; +import { logger } from '@cowprotocol/shared' +import { Address, hashTypedData, isHex, PublicClient, verifyTypedData } from 'viem' -const EIP1271_MAGIC_VALUE = '0x1626ba7e'; +const EIP1271_MAGIC_VALUE = '0x1626ba7e' const EIP1271_ABI = [ { @@ -20,25 +14,25 @@ const EIP1271_ABI = [ ], outputs: [{ name: '', type: 'bytes4' }], }, -] as const; +] as const export type AffiliateTypedData = { domain: { - name: string; - version: string; - }; - types: Record; + name: string + version: string + } + types: Record message: { - walletAddress: string; - code: string; - chainId: number; - }; -}; + walletAddress: string + code: string + chainId: number + } +} export type AffiliateTypedDataField = { - name: string; - type: string; -}; + name: string + type: string +} export enum SignatureCheckResult { valid = 'valid', @@ -47,7 +41,7 @@ export enum SignatureCheckResult { addressIsNotSmartContract = 'addressIsNotSmartContract', } -type Eip1271Client = Pick; +type Eip1271Client = Pick /** * Verifies affiliate typed-data signatures for both EOAs and contract wallets. @@ -66,16 +60,16 @@ type Eip1271Client = Pick; * - `addressIsNotSmartContract`: recovery failed and the address has no bytecode. */ export async function verifyAffiliateSignature(params: { - walletAddress: string; - signedMessage: string; - typedData: AffiliateTypedData; - client: Eip1271Client; + walletAddress: string + signedMessage: string + typedData: AffiliateTypedData + client: Eip1271Client }): Promise { - const { walletAddress, signedMessage, typedData } = params; - const normalizedWalletAddress = walletAddress.toLowerCase(); - const primaryType = 'AffiliateCode'; + const { walletAddress, signedMessage, typedData } = params + const normalizedWalletAddress = walletAddress.toLowerCase() + const primaryType = 'AffiliateCode' - let hasRecoverError = false; + let hasRecoverError = false try { const isValidEoaSignature = await verifyTypedData({ @@ -85,32 +79,27 @@ export async function verifyAffiliateSignature(params: { primaryType, message: typedData.message, signature: signedMessage as `0x${string}`, - }); + }) if (isValidEoaSignature) { - return SignatureCheckResult.valid; + return SignatureCheckResult.valid } } catch (error) { - hasRecoverError = true; - logger.warn( - { error, walletAddress: normalizedWalletAddress }, - 'Affiliate typed data recovery failed' - ); + hasRecoverError = true + logger.warn({ error, walletAddress: normalizedWalletAddress }, 'Affiliate typed data recovery failed') } if (!isHex(signedMessage)) { - return SignatureCheckResult.invalidSignature; + return SignatureCheckResult.invalidSignature } try { const bytecode = await params.client.getBytecode({ address: walletAddress as Address, - }); + }) if (!bytecode || bytecode === '0x') { - return hasRecoverError - ? SignatureCheckResult.addressIsNotSmartContract - : SignatureCheckResult.invalidAddress; + return hasRecoverError ? SignatureCheckResult.addressIsNotSmartContract : SignatureCheckResult.invalidAddress } const digest = hashTypedData({ @@ -118,28 +107,25 @@ export async function verifyAffiliateSignature(params: { types: typedData.types, primaryType, message: typedData.message, - }); + }) const isValidSignatureResult = await params.client.readContract({ address: walletAddress as Address, abi: EIP1271_ABI, functionName: 'isValidSignature', args: [digest as `0x${string}`, signedMessage as `0x${string}`], - }); + }) if ( typeof isValidSignatureResult === 'string' && isValidSignatureResult.slice(0, 10).toLowerCase() === EIP1271_MAGIC_VALUE ) { - return SignatureCheckResult.valid; + return SignatureCheckResult.valid } - return SignatureCheckResult.invalidAddress; + return SignatureCheckResult.invalidAddress } catch (error) { - logger.warn( - { error, walletAddress: normalizedWalletAddress }, - 'Affiliate EIP-1271 verification failed' - ); - return SignatureCheckResult.invalidSignature; + logger.warn({ error, walletAddress: normalizedWalletAddress }, 'Affiliate EIP-1271 verification failed') + return SignatureCheckResult.invalidSignature } } diff --git a/apps/api/src/app/routes/affiliate/affiliate-stats/_address/affiliateStats.schemas.ts b/apps/api/src/app/routes/affiliate/affiliate-stats/_address/affiliateStats.schemas.ts index ff94edeb..6d33bfcf 100644 --- a/apps/api/src/app/routes/affiliate/affiliate-stats/_address/affiliateStats.schemas.ts +++ b/apps/api/src/app/routes/affiliate/affiliate-stats/_address/affiliateStats.schemas.ts @@ -1,5 +1,5 @@ -import { JSONSchema } from 'json-schema-to-ts'; -import { AddressSchema } from '../../../../schemas'; +import { JSONSchema } from 'json-schema-to-ts' +import { AddressSchema } from '../../../../schemas' export const paramsSchema = { type: 'object', @@ -8,7 +8,7 @@ export const paramsSchema = { properties: { address: AddressSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const affiliateStatsSchema = { type: 'object', @@ -39,9 +39,9 @@ export const affiliateStatsSchema = { total_traders: { type: 'number' }, lastUpdatedAt: { type: 'string' }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -export const responseSchema = affiliateStatsSchema; +export const responseSchema = affiliateStatsSchema export const errorSchema = { type: 'object', @@ -50,4 +50,4 @@ export const errorSchema = { properties: { message: { type: 'string' }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema diff --git a/apps/api/src/app/routes/affiliate/affiliate-stats/_address/index.ts b/apps/api/src/app/routes/affiliate/affiliate-stats/_address/index.ts index 839e9574..7a91cef0 100644 --- a/apps/api/src/app/routes/affiliate/affiliate-stats/_address/index.ts +++ b/apps/api/src/app/routes/affiliate/affiliate-stats/_address/index.ts @@ -1,26 +1,17 @@ -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema } from 'json-schema-to-ts'; -import { apiContainer } from '../../../../inversify.config'; -import { - AffiliateStatsService, - affiliateStatsServiceSymbol, -} from '@cowprotocol/services'; -import { isDuneEnabled } from '@cowprotocol/repositories'; -import { - errorSchema, - paramsSchema, - responseSchema, -} from './affiliateStats.schemas'; +import { FastifyPluginAsync } from 'fastify' +import { FromSchema } from 'json-schema-to-ts' +import { apiContainer } from '../../../../inversify.config' +import { AffiliateStatsService, affiliateStatsServiceSymbol } from '@cowprotocol/services' +import { isDuneEnabled } from '@cowprotocol/repositories' +import { errorSchema, paramsSchema, responseSchema } from './affiliateStats.schemas' -type ParamsSchema = FromSchema; -type ResponseSchema = FromSchema | FromSchema; +type ParamsSchema = FromSchema +type ResponseSchema = FromSchema | FromSchema const affiliateStats: FastifyPluginAsync = async (fastify): Promise => { if (!isDuneEnabled) { - fastify.log.warn( - 'DUNE_API_KEY is not set. Skipping affiliate stats endpoint.' - ); - return; + fastify.log.warn('DUNE_API_KEY is not set. Skipping affiliate stats endpoint.') + return } fastify.get<{ Params: ParamsSchema; Reply: ResponseSchema }>( @@ -38,27 +29,23 @@ const affiliateStats: FastifyPluginAsync = async (fastify): Promise => { }, async function (request, reply) { try { - const affiliateStatsService = apiContainer.get( - affiliateStatsServiceSymbol - ); - const result = await affiliateStatsService.getAffiliateStats( - request.params.address - ); + const affiliateStatsService = apiContainer.get(affiliateStatsServiceSymbol) + const result = await affiliateStatsService.getAffiliateStats(request.params.address) if (result.rows.length === 0) { - return reply.status(404).send({ message: 'Affiliate stats not found' }); + return reply.status(404).send({ message: 'Affiliate stats not found' }) } return reply.send({ ...result.rows[0], lastUpdatedAt: result.lastUpdatedAt, - }); + }) } catch (error) { - fastify.log.error({ err: error }, 'Error fetching affiliate stats'); - return reply.status(500).send({ message: 'Unexpected error' }); + fastify.log.error({ err: error }, 'Error fetching affiliate stats') + return reply.status(500).send({ message: 'Unexpected error' }) } } - ); -}; + ) +} -export default affiliateStats; +export default affiliateStats diff --git a/apps/api/src/app/routes/affiliate/trader-stats/_address/index.ts b/apps/api/src/app/routes/affiliate/trader-stats/_address/index.ts index 213f20ad..b32f650c 100644 --- a/apps/api/src/app/routes/affiliate/trader-stats/_address/index.ts +++ b/apps/api/src/app/routes/affiliate/trader-stats/_address/index.ts @@ -1,26 +1,17 @@ -import { FastifyPluginAsync } from 'fastify'; -import { FromSchema } from 'json-schema-to-ts'; -import { apiContainer } from '../../../../inversify.config'; -import { - AffiliateStatsService, - affiliateStatsServiceSymbol, -} from '@cowprotocol/services'; -import { isDuneEnabled } from '@cowprotocol/repositories'; -import { - errorSchema, - paramsSchema, - responseSchema, -} from './traderStats.schemas'; +import { FastifyPluginAsync } from 'fastify' +import { FromSchema } from 'json-schema-to-ts' +import { apiContainer } from '../../../../inversify.config' +import { AffiliateStatsService, affiliateStatsServiceSymbol } from '@cowprotocol/services' +import { isDuneEnabled } from '@cowprotocol/repositories' +import { errorSchema, paramsSchema, responseSchema } from './traderStats.schemas' -type ParamsSchema = FromSchema; -type ResponseSchema = FromSchema | FromSchema; +type ParamsSchema = FromSchema +type ResponseSchema = FromSchema | FromSchema const traderStats: FastifyPluginAsync = async (fastify): Promise => { if (!isDuneEnabled) { - fastify.log.warn( - 'DUNE_API_KEY is not set. Skipping affiliate trader stats endpoint.' - ); - return; + fastify.log.warn('DUNE_API_KEY is not set. Skipping affiliate trader stats endpoint.') + return } fastify.get<{ Params: ParamsSchema; Reply: ResponseSchema }>( @@ -38,27 +29,23 @@ const traderStats: FastifyPluginAsync = async (fastify): Promise => { }, async function (request, reply) { try { - const affiliateStatsService = apiContainer.get( - affiliateStatsServiceSymbol - ); - const result = await affiliateStatsService.getTraderStats( - request.params.address - ); + const affiliateStatsService = apiContainer.get(affiliateStatsServiceSymbol) + const result = await affiliateStatsService.getTraderStats(request.params.address) if (result.rows.length === 0) { - return reply.status(404).send({ message: 'Trader stats not found' }); + return reply.status(404).send({ message: 'Trader stats not found' }) } return reply.send({ ...result.rows[0], lastUpdatedAt: result.lastUpdatedAt, - }); + }) } catch (error) { - fastify.log.error({ err: error }, 'Error fetching affiliate trader stats'); - return reply.status(500).send({ message: 'Unexpected error' }); + fastify.log.error({ err: error }, 'Error fetching affiliate trader stats') + return reply.status(500).send({ message: 'Unexpected error' }) } } - ); -}; + ) +} -export default traderStats; +export default traderStats diff --git a/apps/api/src/app/routes/affiliate/trader-stats/_address/traderStats.schemas.ts b/apps/api/src/app/routes/affiliate/trader-stats/_address/traderStats.schemas.ts index de241d5b..8d85c146 100644 --- a/apps/api/src/app/routes/affiliate/trader-stats/_address/traderStats.schemas.ts +++ b/apps/api/src/app/routes/affiliate/trader-stats/_address/traderStats.schemas.ts @@ -1,5 +1,5 @@ -import { JSONSchema } from 'json-schema-to-ts'; -import { AddressSchema } from '../../../../schemas'; +import { JSONSchema } from 'json-schema-to-ts' +import { AddressSchema } from '../../../../schemas' export const paramsSchema = { type: 'object', @@ -8,7 +8,7 @@ export const paramsSchema = { properties: { address: AddressSchema, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const traderStatsSchema = { type: 'object', @@ -39,9 +39,9 @@ export const traderStatsSchema = { next_payout: { type: 'number' }, lastUpdatedAt: { type: 'string' }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema -export const responseSchema = traderStatsSchema; +export const responseSchema = traderStatsSchema export const errorSchema = { type: 'object', @@ -50,4 +50,4 @@ export const errorSchema = { properties: { message: { type: 'string' }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema diff --git a/apps/api/src/app/routes/examples/hello.ts b/apps/api/src/app/routes/examples/hello.ts index dd1a29e5..c39ddf22 100644 --- a/apps/api/src/app/routes/examples/hello.ts +++ b/apps/api/src/app/routes/examples/hello.ts @@ -1,18 +1,18 @@ -import { FastifyPluginAsync } from 'fastify'; -import { CACHE_CONTROL_HEADER as CACHE_CONTROL_HEADER, getCacheControlHeaderValue } from '../../../utils/cache'; -import { delay } from '../../../utils/misc'; +import { FastifyPluginAsync } from 'fastify' +import { CACHE_CONTROL_HEADER as CACHE_CONTROL_HEADER, getCacheControlHeaderValue } from '../../../utils/cache' +import { delay } from '../../../utils/misc' const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/hello', async function (request, reply) { await delay(2000) reply.send({ hello: 'world' }) - }); + }) fastify.get('/hello-cached', async function (request, reply) { await delay(2000) reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(10)) reply.send({ hello: 'world' }) - }); -}; + }) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/hooks.schemas.ts b/apps/api/src/app/routes/hooks.schemas.ts index 3299336f..52e22772 100644 --- a/apps/api/src/app/routes/hooks.schemas.ts +++ b/apps/api/src/app/routes/hooks.schemas.ts @@ -1,6 +1,6 @@ -import { BLOCKCHAIN_VALUES, PERIOD_VALUES } from '@cowprotocol/services'; +import { BLOCKCHAIN_VALUES, PERIOD_VALUES } from '@cowprotocol/services' -const HOOKS_QUERY_REQUIRED = ['blockchain', 'period'] as const; +const HOOKS_QUERY_REQUIRED = ['blockchain', 'period'] as const export const hooksQuerySchema = { type: 'object', @@ -31,7 +31,7 @@ export const hooksQuerySchema = { description: 'Number of hooks to skip', }, }, -} as const; +} as const const hookItemSchema = { type: 'object', @@ -50,7 +50,7 @@ const hookItemSchema = { app_hash: { type: 'string' }, tx_hash: { type: 'string' }, }, -} as const; +} as const export const hooksResponseSchema = { type: 'object', @@ -59,4 +59,4 @@ export const hooksResponseSchema = { count: { type: 'number' }, error: { type: 'string' }, }, -} as const; +} as const diff --git a/apps/api/src/app/routes/hooks.ts b/apps/api/src/app/routes/hooks.ts index b8a7f2c2..5d85b4a9 100644 --- a/apps/api/src/app/routes/hooks.ts +++ b/apps/api/src/app/routes/hooks.ts @@ -1,45 +1,34 @@ -import { FastifyPluginAsync } from 'fastify'; -import { apiContainer } from '../inversify.config'; -import { hooksServiceSymbol } from '@cowprotocol/services'; -import { - HooksService, - Blockchain, - Period, - HookData, -} from '@cowprotocol/services'; -import { - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue, -} from '../../utils/cache'; -import ms from 'ms'; -import { isDuneEnabled } from '@cowprotocol/repositories'; -import { - hooksQuerySchema, - hooksResponseSchema, -} from './hooks.schemas'; +import { FastifyPluginAsync } from 'fastify' +import { apiContainer } from '../inversify.config' +import { hooksServiceSymbol } from '@cowprotocol/services' +import { HooksService, Blockchain, Period, HookData } from '@cowprotocol/services' +import { CACHE_CONTROL_HEADER, getCacheControlHeaderValue } from '../../utils/cache' +import ms from 'ms' +import { isDuneEnabled } from '@cowprotocol/repositories' +import { hooksQuerySchema, hooksResponseSchema } from './hooks.schemas' -const CACHE_SECONDS = ms('5m') / 1000; // Cache for 5 minutes +const CACHE_SECONDS = ms('5m') / 1000 // Cache for 5 minutes interface HooksQuery { - blockchain: Blockchain; - period: Period; - maxWaitTimeMs?: number; - limit?: number; - offset?: number; + blockchain: Blockchain + period: Period + maxWaitTimeMs?: number + limit?: number + offset?: number } interface HooksResponse { - hooks: HookData[]; - count: number; - error?: string; + hooks: HookData[] + count: number + error?: string } -const HOOKS_TAGS = ['hooks'] as const; +const HOOKS_TAGS = ['hooks'] as const const hooks: FastifyPluginAsync = async (fastify): Promise => { if (!isDuneEnabled) { - fastify.log.warn('DUNE_API_KEY is not set. Skipping hooks endpoint.'); - return; + fastify.log.warn('DUNE_API_KEY is not set. Skipping hooks endpoint.') + return } fastify.get<{ Querystring: HooksQuery; Reply: HooksResponse }>( @@ -56,36 +45,32 @@ const hooks: FastifyPluginAsync = async (fastify): Promise => { }, async function (request, reply) { try { - const hooksService = apiContainer.get(hooksServiceSymbol); + const hooksService = apiContainer.get(hooksServiceSymbol) const hooks = await hooksService.getHooks({ blockchain: request.query.blockchain, period: request.query.period, maxWaitTimeMs: request.query.maxWaitTimeMs, limit: request.query.limit, offset: request.query.offset, - }); + }) - reply.header( - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue(CACHE_SECONDS) - ); + reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(CACHE_SECONDS)) return reply.send({ hooks, count: hooks.length, - }); + }) } catch (error) { - fastify.log.error('Error fetching hooks:', error); - reply.header(CACHE_CONTROL_HEADER, 'no-store'); + fastify.log.error('Error fetching hooks:', error) + reply.header(CACHE_CONTROL_HEADER, 'no-store') return reply.status(500).send({ hooks: [], count: 0, error: 'Internal server error while fetching hooks', - }); + }) } } - ); - -}; + ) +} -export default hooks; +export default hooks diff --git a/apps/api/src/app/routes/proxies/coingecko/index.ts b/apps/api/src/app/routes/proxies/coingecko/index.ts index 22280705..56be523a 100644 --- a/apps/api/src/app/routes/proxies/coingecko/index.ts +++ b/apps/api/src/app/routes/proxies/coingecko/index.ts @@ -1,28 +1,20 @@ -import httpProxy from '@fastify/http-proxy'; -import { - CACHE_CONTROL_HEADER, - getCacheControlHeaderValue, -} from '../../../../utils/cache'; -import { FastifyPluginAsync } from 'fastify'; -import { COINGECKO_PRO_BASE_URL } from '@cowprotocol/repositories'; -import { KeysOf } from 'fastify/types/type-provider'; -import { IncomingHttpHeaders } from 'http2'; +import httpProxy from '@fastify/http-proxy' +import { CACHE_CONTROL_HEADER, getCacheControlHeaderValue } from '../../../../utils/cache' +import { FastifyPluginAsync } from 'fastify' +import { COINGECKO_PRO_BASE_URL } from '@cowprotocol/repositories' +import { KeysOf } from 'fastify/types/type-provider' +import { IncomingHttpHeaders } from 'http2' -const DROP_HEADERS: KeysOf[] = [ - 'cf-ray', - 'cf-cache-status', - 'set-cookie', - 'server', -]; +const DROP_HEADERS: KeysOf[] = ['cf-ray', 'cf-cache-status', 'set-cookie', 'server'] -const CACHE_TTL = parseInt(process.env.COINGECKO_CACHING_TIME || '150'); // Defaults to 2.5 minutes (150 seconds) +const CACHE_TTL = parseInt(process.env.COINGECKO_CACHING_TIME || '150') // Defaults to 2.5 minutes (150 seconds) const coingeckoProxy: FastifyPluginAsync = async (fastify): Promise => { - const coingeckoApiKey = fastify.config.COINGECKO_API_KEY; + const coingeckoApiKey = fastify.config.COINGECKO_API_KEY if (!coingeckoApiKey) { - fastify.log.warn('COINGECKO_API_KEY is not set. Skipping proxy.'); - return; + fastify.log.warn('COINGECKO_API_KEY is not set. Skipping proxy.') + return } fastify.register(httpProxy, { @@ -38,8 +30,8 @@ const coingeckoProxy: FastifyPluginAsync = async (fastify): Promise => { // Drop some headers const newHeaders = DROP_HEADERS.reduce( (acc, header) => { - delete acc[header]; - return acc; + delete acc[header] + return acc }, { ...headers, @@ -49,21 +41,18 @@ const coingeckoProxy: FastifyPluginAsync = async (fastify): Promise => { } : undefined), } - ); + ) - return newHeaders; + return newHeaders }, }, preHandler: async (request) => { - fastify.log.debug( - { url: request.url, method: request.method }, - `Request coingecko proxy` - ); + fastify.log.debug({ url: request.url, method: request.method }, `Request coingecko proxy`) }, undici: { strictContentLength: false, // Prevent errors when content-length header mismatches }, - }); -}; + }) +} -export default coingeckoProxy; +export default coingeckoProxy diff --git a/apps/api/src/app/routes/proxies/socket/index.ts b/apps/api/src/app/routes/proxies/socket/index.ts index 77a83605..2f452924 100644 --- a/apps/api/src/app/routes/proxies/socket/index.ts +++ b/apps/api/src/app/routes/proxies/socket/index.ts @@ -1,47 +1,36 @@ -import httpProxy from '@fastify/http-proxy'; -import { FastifyPluginAsync } from 'fastify'; +import httpProxy from '@fastify/http-proxy' +import { FastifyPluginAsync } from 'fastify' -const DEFAULT_SOCKET_BASE_URL = 'https://dedicated-backend.bungee.exchange'; +const DEFAULT_SOCKET_BASE_URL = 'https://dedicated-backend.bungee.exchange' const proxy: FastifyPluginAsync = async (fastify, opts): Promise => { - const upstream = fastify.config.SOCKET_BASE_URL || DEFAULT_SOCKET_BASE_URL; - const apiKey = fastify.config.SOCKET_API_KEY; - const affiliateCode = fastify.config.SOCKET_AFFILIATE_CODE; + const upstream = fastify.config.SOCKET_BASE_URL || DEFAULT_SOCKET_BASE_URL + const apiKey = fastify.config.SOCKET_API_KEY + const affiliateCode = fastify.config.SOCKET_AFFILIATE_CODE if (!apiKey) { - fastify.log.warn('SOCKET_API_KEY is not set. Skipping proxy.'); - return; + fastify.log.warn('SOCKET_API_KEY is not set. Skipping proxy.') + return } if (!affiliateCode) { - fastify.log.warn('SOCKET_AFFILIATE_CODE is not set. Skipping proxy.'); - return; + fastify.log.warn('SOCKET_AFFILIATE_CODE is not set. Skipping proxy.') + return } // Handle CORS preflight locally for socket routes fastify.options('/*', async (request, reply) => { - const origin = (request.headers.origin as string) || '*'; - const acrm = - (request.headers['access-control-request-method'] as string) || ''; - const acrh = - (request.headers['access-control-request-headers'] as string) || ''; + const origin = (request.headers.origin as string) || '*' + const acrm = (request.headers['access-control-request-method'] as string) || '' + const acrh = (request.headers['access-control-request-headers'] as string) || '' reply .header('Access-Control-Allow-Origin', origin) - .header( - 'Vary', - 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers' - ) - .header( - 'Access-Control-Allow-Methods', - acrm || 'GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD' - ) - .header( - 'Access-Control-Allow-Headers', - acrh || 'authorization, content-type, x-requested-with' - ) + .header('Vary', 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers') + .header('Access-Control-Allow-Methods', acrm || 'GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD') + .header('Access-Control-Allow-Headers', acrh || 'authorization, content-type, x-requested-with') .header('Access-Control-Max-Age', '600') .status(204) - .send(); - }); + .send() + }) fastify.register(httpProxy, { upstream, @@ -59,9 +48,9 @@ const proxy: FastifyPluginAsync = async (fastify, opts): Promise => { fastify.log.info( { url: request.url, method: request.method, headers: request.headers }, `Proxying request to socket ${upstream}` - ); + ) }, - }); -}; + }) +} -export default proxy; +export default proxy diff --git a/apps/api/src/app/routes/proxies/tokens/index.ts b/apps/api/src/app/routes/proxies/tokens/index.ts index fef323cf..e9a693a5 100644 --- a/apps/api/src/app/routes/proxies/tokens/index.ts +++ b/apps/api/src/app/routes/proxies/tokens/index.ts @@ -1,11 +1,11 @@ -import { FastifyPluginAsync } from 'fastify'; -import httpProxy from '@fastify/http-proxy'; +import { FastifyPluginAsync } from 'fastify' +import httpProxy from '@fastify/http-proxy' const proxy: FastifyPluginAsync = async (fastify, opts): Promise => { - const upstream = fastify.config.PROXY_UPSTREAM; + const upstream = fastify.config.PROXY_UPSTREAM if (!upstream) { - fastify.log.warn('PROXY_UPSTREAM is not set. Skipping proxy.'); - return; + fastify.log.warn('PROXY_UPSTREAM is not set. Skipping proxy.') + return } fastify.register(httpProxy, { @@ -16,10 +16,10 @@ const proxy: FastifyPluginAsync = async (fastify, opts): Promise => { ...headers, Origin: fastify.config.PROXY_ORIGIN, Host: fastify.config.PROXY_HOST, - }; + } }, }, - }); -}; + }) +} -export default proxy; +export default proxy diff --git a/apps/api/src/app/routes/ref-codes/_code/index.ts b/apps/api/src/app/routes/ref-codes/_code/index.ts index c922fd14..cca0f835 100644 --- a/apps/api/src/app/routes/ref-codes/_code/index.ts +++ b/apps/api/src/app/routes/ref-codes/_code/index.ts @@ -1,36 +1,32 @@ -import { FastifyPluginAsync, FastifyReply } from 'fastify'; -import { FromSchema } from 'json-schema-to-ts'; +import { FastifyPluginAsync, FastifyReply } from 'fastify' +import { FromSchema } from 'json-schema-to-ts' import { AffiliatesRepository, affiliatesRepositorySymbol, isCmsEnabled, isCmsRequestError, -} from '@cowprotocol/repositories'; -import { apiContainer } from '../../../inversify.config'; -import { logger } from '@cowprotocol/shared'; -import { errorSchema, paramsSchema, responseSchema } from './refCodes.schemas'; -import { AFFILIATE_CODE_REGEX } from '../../../config/affiliate'; +} from '@cowprotocol/repositories' +import { apiContainer } from '../../../inversify.config' +import { logger } from '@cowprotocol/shared' +import { errorSchema, paramsSchema, responseSchema } from './refCodes.schemas' +import { AFFILIATE_CODE_REGEX } from '../../../config/affiliate' -type ParamsSchema = FromSchema; -type SuccessSchema = FromSchema; -type ErrorSchema = FromSchema; +type ParamsSchema = FromSchema +type SuccessSchema = FromSchema +type ErrorSchema = FromSchema const refCodes: FastifyPluginAsync = async (fastify): Promise => { if (!isCmsEnabled) { - logger.warn( - 'CMS is not enabled. Please check CMS_ENABLED and CMS_API_KEY environment variables' - ); - return; + logger.warn('CMS is not enabled. Please check CMS_ENABLED and CMS_API_KEY environment variables') + return } - const affiliatesRepository: AffiliatesRepository = apiContainer.get( - affiliatesRepositorySymbol - ); + const affiliatesRepository: AffiliatesRepository = apiContainer.get(affiliatesRepositorySymbol) // GET /ref-codes/:code fastify.get<{ - Params: ParamsSchema; - Reply: SuccessSchema | ErrorSchema; + Params: ParamsSchema + Reply: SuccessSchema | ErrorSchema }>( '/', { @@ -49,65 +45,62 @@ const refCodes: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const code = normalizeCode(request.params.code); + const code = normalizeCode(request.params.code) if (!code) { - reply.code(400).send({ message: 'Affiliate code is required' }); - return; + reply.code(400).send({ message: 'Affiliate code is required' }) + return } if (!isValidCode(code)) { - reply.code(400).send({ message: 'Affiliate code format is invalid' }); - return; + reply.code(400).send({ message: 'Affiliate code format is invalid' }) + return } try { const affiliateEntry = await affiliatesRepository.getAffiliateByCode({ code, - }); + }) if (!affiliateEntry) { - reply.code(404).send({ message: 'Affiliate code not found' }); - return; + reply.code(404).send({ message: 'Affiliate code not found' }) + return } if (!affiliateEntry.enabled) { - reply.code(403).send({ message: 'Affiliate code disabled' }); - return; + reply.code(403).send({ message: 'Affiliate code disabled' }) + return } reply.send({ code: affiliateEntry.code, - traderRewardAmount: - (affiliateEntry.rewardAmount * - affiliateEntry.revenueSplitTraderPct) / - 100, + traderRewardAmount: (affiliateEntry.rewardAmount * affiliateEntry.revenueSplitTraderPct) / 100, triggerVolume: affiliateEntry.triggerVolume, timeCapDays: affiliateEntry.timeCapDays, volumeCap: affiliateEntry.volumeCap, - }); + }) } catch (error) { - handleCmsError(error, reply); + handleCmsError(error, reply) } } - ); -}; + ) +} function normalizeCode(value: string): string { - return value.trim().toUpperCase(); + return value.trim().toUpperCase() } function isValidCode(value: string): boolean { - return AFFILIATE_CODE_REGEX.test(value); + return AFFILIATE_CODE_REGEX.test(value) } function handleCmsError(error: unknown, reply: FastifyReply) { if (isCmsRequestError(error)) { - reply.code(502).send({ message: 'CMS request failed' }); - return; + reply.code(502).send({ message: 'CMS request failed' }) + return } - reply.code(500).send({ message: 'Unexpected error' }); + reply.code(500).send({ message: 'Unexpected error' }) } -export default refCodes; +export default refCodes diff --git a/apps/api/src/app/routes/ref-codes/_code/refCodes.schemas.ts b/apps/api/src/app/routes/ref-codes/_code/refCodes.schemas.ts index 0327d7ce..50384ad7 100644 --- a/apps/api/src/app/routes/ref-codes/_code/refCodes.schemas.ts +++ b/apps/api/src/app/routes/ref-codes/_code/refCodes.schemas.ts @@ -1,5 +1,5 @@ -import { JSONSchema } from 'json-schema-to-ts'; -import { AFFILIATE_CODE_REGEX } from '../../../config/affiliate'; +import { JSONSchema } from 'json-schema-to-ts' +import { AFFILIATE_CODE_REGEX } from '../../../config/affiliate' export const paramsSchema = { type: 'object', @@ -14,17 +14,11 @@ export const paramsSchema = { pattern: AFFILIATE_CODE_REGEX.source, }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const responseSchema = { type: 'object', - required: [ - 'code', - 'traderRewardAmount', - 'triggerVolume', - 'timeCapDays', - 'volumeCap', - ], + required: ['code', 'traderRewardAmount', 'triggerVolume', 'timeCapDays', 'volumeCap'], additionalProperties: false, properties: { code: { @@ -35,7 +29,7 @@ export const responseSchema = { timeCapDays: { type: 'number' }, volumeCap: { type: 'number' }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema export const errorSchema = { type: 'object', @@ -46,4 +40,4 @@ export const errorSchema = { type: 'string', }, }, -} as const satisfies JSONSchema; +} as const satisfies JSONSchema diff --git a/apps/api/src/app/routes/root.ts b/apps/api/src/app/routes/root.ts index 0d1da3f3..769cb653 100644 --- a/apps/api/src/app/routes/root.ts +++ b/apps/api/src/app/routes/root.ts @@ -1,10 +1,9 @@ -import { FastifyPluginAsync } from 'fastify'; +import { FastifyPluginAsync } from 'fastify' const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/', async function (request, reply) { - reply.redirect(307, '/docs/static/index.html'); - }); + reply.redirect(307, '/docs/static/index.html') + }) +} -}; - -export default root; +export default root diff --git a/apps/api/src/app/routes/tests/balances/index.ts b/apps/api/src/app/routes/tests/balances/index.ts index 19ae1c51..a939f6ee 100644 --- a/apps/api/src/app/routes/tests/balances/index.ts +++ b/apps/api/src/app/routes/tests/balances/index.ts @@ -1,19 +1,16 @@ -import { FastifyPluginAsync } from 'fastify'; -import { readFileSync } from 'fs'; -import { join } from 'path'; +import { FastifyPluginAsync } from 'fastify' +import { readFileSync } from 'fs' +import { join } from 'path' const root: FastifyPluginAsync = async (fastify): Promise => { // Serve the SSE test HTML page fastify.get('/', { schema: { tags: ['tests'] } }, async (request, reply) => { // Resolve from the copied assets folder (shared build rule). - const htmlPath = join( - process.cwd(), - 'apps/api/src/assets/tests/balances/sse-test.html' - ); - const htmlContent = readFileSync(htmlPath, 'utf8'); + const htmlPath = join(process.cwd(), 'apps/api/src/assets/tests/balances/sse-test.html') + const htmlContent = readFileSync(htmlPath, 'utf8') - return reply.type('text/html').send(htmlContent); - }); -}; + return reply.type('text/html').send(htmlContent) + }) +} -export default root; +export default root diff --git a/apps/api/src/app/routes/twap/index.ts b/apps/api/src/app/routes/twap/index.ts index 67045f3e..aa44f9d7 100644 --- a/apps/api/src/app/routes/twap/index.ts +++ b/apps/api/src/app/routes/twap/index.ts @@ -1,16 +1,16 @@ -import { FastifyPluginAsync } from 'fastify'; -import httpProxy from '@fastify/http-proxy'; +import { FastifyPluginAsync } from 'fastify' +import httpProxy from '@fastify/http-proxy' const proxy: FastifyPluginAsync = async (fastify, opts): Promise => { - const upstream = fastify.config.TWAP_BASE_URL; + const upstream = fastify.config.TWAP_BASE_URL if (!upstream) { - fastify.log.warn('TWAP_BASE_URL is not set. Skipping proxy.'); - return; + fastify.log.warn('TWAP_BASE_URL is not set. Skipping proxy.') + return } fastify.register(httpProxy, { upstream, - }); -}; + }) +} -export default proxy; +export default proxy diff --git a/apps/api/src/app/schemas.ts b/apps/api/src/app/schemas.ts index 7a7b9e5e..7b4dfb39 100644 --- a/apps/api/src/app/schemas.ts +++ b/apps/api/src/app/schemas.ts @@ -1,31 +1,30 @@ -import { AllChainIds } from '@cowprotocol/shared'; +import { AllChainIds } from '@cowprotocol/shared' export const SupportedChainIdSchema = { title: 'Supported Chain ID', description: 'Supported Chain ID', enum: AllChainIds, type: 'integer', -} as const; +} as const export const ChainIdOrSlugSchema = { title: 'Chain ID or Slug', description: 'Chain ID (integer) or chain slug (string)', type: 'string', pattern: '^(\\d{1,20})|([0-9a-z\\-]{3,30})$', -} as const; +} as const export const AddressSchema = { title: 'Address', description: 'Ethereum address.', type: 'string', pattern: '^0x[a-fA-F0-9]{40}$', -} as const; +} as const export const OptionalAddressSchema = { title: 'Optional Address', // Since the token address is part of the path, we can't leave it empty - description: - 'Either provide a token address or a dash (-) to indicate no address.', + description: 'Either provide a token address or a dash (-) to indicate no address.', oneOf: [ { type: 'string', @@ -57,6 +56,6 @@ export const OptionalAddressSchema = { pattern: '^-$', }, ], -} as const; +} as const -export const ETHEREUM_ADDRESS_PATTERN = '^0x[a-fA-F0-9]{40}$'; +export const ETHEREUM_ADDRESS_PATTERN = '^0x[a-fA-F0-9]{40}$' diff --git a/apps/api/src/datasource.config.ts b/apps/api/src/datasource.config.ts index 4d38ba6a..6d53b364 100644 --- a/apps/api/src/datasource.config.ts +++ b/apps/api/src/datasource.config.ts @@ -1,7 +1,7 @@ -import * as dotenv from 'dotenv'; -import { DataSource } from 'typeorm'; +import * as dotenv from 'dotenv' +import { DataSource } from 'typeorm' -dotenv.config(); +dotenv.config() export const cowAnalyticsDb = new DataSource({ type: 'postgres', @@ -11,6 +11,6 @@ export const cowAnalyticsDb = new DataSource({ password: process.env.COW_ANALYTICS_DATABASE_PASSWORD, database: process.env.COW_ANALYTICS_DATABASE_NAME, entities: ['src/app/data/*.ts'], -}); +}) -cowAnalyticsDb.initialize(); +cowAnalyticsDb.initialize() diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 042a0a6c..5824e098 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,30 +1,30 @@ -import Fastify from 'fastify'; -import { app } from './app/app'; -import { logger } from '@cowprotocol/shared'; -import { startAffiliateProgramExportPoller } from './app/affiliateProgramExportPoller'; +import Fastify from 'fastify' +import { app } from './app/app' +import { logger } from '@cowprotocol/shared' +import { startAffiliateProgramExportPoller } from './app/affiliateProgramExportPoller' -const host = process.env.HOST ?? 'localhost'; -const port = process.env.PORT ? Number(process.env.PORT) : 3001; +const host = process.env.HOST ?? 'localhost' +const port = process.env.PORT ? Number(process.env.PORT) : 3001 // Instantiate Fastify with some config export const server = Fastify({ logger, -}); +}) // Register your application as a normal plugin. -server.register(app); +server.register(app) -const stopAffiliateProgramExportPoller = startAffiliateProgramExportPoller(); +const stopAffiliateProgramExportPoller = startAffiliateProgramExportPoller() server.addHook('onClose', async () => { - await stopAffiliateProgramExportPoller?.(); -}); + await stopAffiliateProgramExportPoller?.() +}) // Start listening. server.listen({ port, host }, (err) => { if (err) { - server.log.error(err); - process.exit(1); + server.log.error(err) + process.exit(1) } else { - server.log.info(`[ ready ] http://${host}:${port}`); + server.log.info(`[ ready ] http://${host}:${port}`) } -}); +}) diff --git a/apps/api/src/types/abstract-cache.d.ts b/apps/api/src/types/abstract-cache.d.ts index 2bbaad96..386c6daa 100644 --- a/apps/api/src/types/abstract-cache.d.ts +++ b/apps/api/src/types/abstract-cache.d.ts @@ -1,4 +1,4 @@ declare module 'abstract-cache' { - const abstractCache: any; - export default abstractCache; + const abstractCache: any + export default abstractCache } diff --git a/apps/api/src/utils/cache.ts b/apps/api/src/utils/cache.ts index 1838e6af..a8faa4bc 100644 --- a/apps/api/src/utils/cache.ts +++ b/apps/api/src/utils/cache.ts @@ -1,33 +1,30 @@ -import { FastifyInstance } from 'fastify'; +import { FastifyInstance } from 'fastify' -export const CACHE_CONTROL_HEADER = 'cache-control'; +export const CACHE_CONTROL_HEADER = 'cache-control' export interface CachedItem { - stored: number; - ttl: number; - item: unknown; + stored: number + ttl: number + item: unknown } // TODO: Implement using decorators instead of utility functions -export async function getCache( - key: string, - fastify: FastifyInstance -): Promise { +export async function getCache(key: string, fastify: FastifyInstance): Promise { return new Promise((resolve, reject) => { fastify.cache.get(key, (err: unknown, value: unknown) => { if (err) { - reject(err); + reject(err) } if (!value) { - resolve(undefined); + resolve(undefined) } else if (!isCachedItem(value)) { - reject(new Error('Value is not a CachedItem')); + reject(new Error('Value is not a CachedItem')) } else { - resolve(value); + resolve(value) } - }); - }); + }) + }) } // TODO: Implement using decorators instead of utility functions @@ -38,59 +35,43 @@ export async function setCache( fastify: FastifyInstance ): Promise { return new Promise((resolve, reject) => { - fastify.cache.set( - key, - value, - timeToLive * 1000, - (err: unknown, value: unknown) => { - if (err) { - reject(err); - } else { - resolve(); - } + fastify.cache.set(key, value, timeToLive * 1000, (err: unknown, value: unknown) => { + if (err) { + reject(err) + } else { + resolve() } - ); - }); + }) + }) } function isCachedItem(value: unknown): value is CachedItem { - return ( - typeof value === 'object' && - value !== null && - 'stored' in value && - 'ttl' in value && - 'item' in value - ); + return typeof value === 'object' && value !== null && 'stored' in value && 'ttl' in value && 'item' in value } -export function getCacheControlHeaderValue( - ttl: number, - ttlSharedCache?: number -) { - return `max-age=${ttl}, public, s-maxage=${ttlSharedCache || ttl}`; +export function getCacheControlHeaderValue(ttl: number, ttlSharedCache?: number) { + return `max-age=${ttl}, public, s-maxage=${ttlSharedCache || ttl}` } -type Directives = { [key: string]: string }; +type Directives = { [key: string]: string } -export function parseCacheControlHeaderValue( - headerValue?: string | number | string[] -): Directives { +export function parseCacheControlHeaderValue(headerValue?: string | number | string[]): Directives { if (!headerValue || typeof headerValue !== 'string') { - return {}; + return {} } - const directivesResult: Directives = {}; + const directivesResult: Directives = {} - const parts = headerValue.split(','); + const parts = headerValue.split(',') parts.forEach((part) => { - const trimmedPart = part.trim(); + const trimmedPart = part.trim() if (trimmedPart.includes('=')) { - const [key, value] = trimmedPart.split('='); - directivesResult[key] = value; + const [key, value] = trimmedPart.split('=') + directivesResult[key] = value } else { - directivesResult[trimmedPart] = 'true'; + directivesResult[trimmedPart] = 'true' } - }); + }) - return directivesResult; + return directivesResult } diff --git a/apps/api/src/utils/misc.ts b/apps/api/src/utils/misc.ts index c040966c..23cf7bfd 100644 --- a/apps/api/src/utils/misc.ts +++ b/apps/api/src/utils/misc.ts @@ -1 +1 @@ -export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); \ No newline at end of file +export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/apps/notification-producer-e2e/src/notification-producer/notification-producer.spec.ts b/apps/notification-producer-e2e/src/notification-producer/notification-producer.spec.ts index 49998d4c..568fb881 100644 --- a/apps/notification-producer-e2e/src/notification-producer/notification-producer.spec.ts +++ b/apps/notification-producer-e2e/src/notification-producer/notification-producer.spec.ts @@ -1,12 +1,12 @@ -import { execSync } from 'child_process'; -import { join } from 'path'; +import { execSync } from 'child_process' +import { join } from 'path' describe('CLI tests', () => { it('should print a message', () => { - const cliPath = join(process.cwd(), 'dist/apps/notification-producer'); + const cliPath = join(process.cwd(), 'dist/apps/notification-producer') - const output = execSync(`node ${cliPath}`).toString(); + const output = execSync(`node ${cliPath}`).toString() - expect(output).toMatch(/Hello World/); - }); -}); + expect(output).toMatch(/Hello World/) + }) +}) diff --git a/apps/notification-producer/src/main.ts b/apps/notification-producer/src/main.ts index c6a36abd..d0e1f874 100644 --- a/apps/notification-producer/src/main.ts +++ b/apps/notification-producer/src/main.ts @@ -1,4 +1,4 @@ -import 'reflect-metadata'; +import 'reflect-metadata' import { getCacheRepository, @@ -8,41 +8,35 @@ import { getPushNotificationsRepository, getPushSubscriptionsRepository, getExpiredOrdersRepository, - getOrdersAppDataRepository -} from '@cowprotocol/services'; + getOrdersAppDataRepository, +} from '@cowprotocol/services' -import { Runnable } from '../types'; -import { TradeNotificationProducer } from './producers/trade/TradeNotificationProducer'; -import { - ExpiredOrdersNotificationProducer -} from './producers/expired-orders/ExpiredOrdersNotificationProducer'; -import { ALL_SUPPORTED_CHAIN_IDS } from '@cowprotocol/cow-sdk'; -import ms from 'ms'; -import { CmsNotificationProducer } from './producers/cms/CmsNotificationProducer'; -import { logger } from '@cowprotocol/shared'; +import { Runnable } from '../types' +import { TradeNotificationProducer } from './producers/trade/TradeNotificationProducer' +import { ExpiredOrdersNotificationProducer } from './producers/expired-orders/ExpiredOrdersNotificationProducer' +import { ALL_SUPPORTED_CHAIN_IDS } from '@cowprotocol/cow-sdk' +import ms from 'ms' +import { CmsNotificationProducer } from './producers/cms/CmsNotificationProducer' +import { logger } from '@cowprotocol/shared' -const TIMEOUT_STOP_PRODUCERS = ms(`30s`); +const TIMEOUT_STOP_PRODUCERS = ms(`30s`) -let shuttingDown = false; +let shuttingDown = false /** * Main loop: Run and re-attempt on error */ async function mainLoop() { - const chainIds = getProducerChains(); - logger.info( - `[notification-producer:main] Start notification producer for networks: ${chainIds.join( - ', ' - )}` - ); - - const cacheRepository = getCacheRepository(); - const erc20Repository = getErc20Repository(cacheRepository); - const pushNotificationsRepository = getPushNotificationsRepository(); - const pushSubscriptionsRepository = getPushSubscriptionsRepository(); - const indexerStateRepository = getIndexerStateRepository(); - const onChainPlacedOrdersRepository = getOnChainPlacedOrdersRepository(); - const expiredOrdersRepository = getExpiredOrdersRepository(); - const ordersAppDataRepository = getOrdersAppDataRepository(); + const chainIds = getProducerChains() + logger.info(`[notification-producer:main] Start notification producer for networks: ${chainIds.join(', ')}`) + + const cacheRepository = getCacheRepository() + const erc20Repository = getErc20Repository(cacheRepository) + const pushNotificationsRepository = getPushNotificationsRepository() + const pushSubscriptionsRepository = getPushSubscriptionsRepository() + const indexerStateRepository = getIndexerStateRepository() + const onChainPlacedOrdersRepository = getOnChainPlacedOrdersRepository() + const expiredOrdersRepository = getExpiredOrdersRepository() + const ordersAppDataRepository = getOrdersAppDataRepository() const repositories = { pushNotificationsRepository, @@ -50,7 +44,7 @@ async function mainLoop() { indexerStateRepository, erc20Repository, onChainPlacedOrdersRepository, - }; + } // Create all producers const producers: Runnable[] = [ @@ -63,7 +57,7 @@ async function mainLoop() { ...repositories, ordersAppDataRepository, chainId, - }); + }) }), // Expired order producer @@ -71,84 +65,69 @@ async function mainLoop() { return new ExpiredOrdersNotificationProducer({ chainId, ...repositories, - expiredOrdersRepository - }); + expiredOrdersRepository, + }) }), - ]; + ] // Run all producers in the background - const promises = producers.map((producer) => producer.start()); + const promises = producers.map((producer) => producer.start()) // Wrap all producers in a promise - const producersPromise = Promise.all(promises); + const producersPromise = Promise.all(promises) // Cleanup resources on application termination const shutdown = () => { gracefulShutdown(producers, producersPromise).catch((error) => { - logger.error(error, 'Error during shutdown'); - process.exit(1); - }); - }; + logger.error(error, 'Error during shutdown') + process.exit(1) + }) + } - process.on('SIGTERM', shutdown); - process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown) + process.on('SIGINT', shutdown) - await producersPromise; + await producersPromise } function getProducerChains() { // Comma-separated list of chain IDs to run the notification producer on const producerNetworks = - process.env.NOTIFICATIONS_PRODUCER_CHAINS?.split(',').map((chain) => - Number(chain.trim()) - ) || []; + process.env.NOTIFICATIONS_PRODUCER_CHAINS?.split(',').map((chain) => Number(chain.trim())) || [] // If no producer networks are specified, use all supported chain ids if (producerNetworks.length === 0) { - return ALL_SUPPORTED_CHAIN_IDS; + return ALL_SUPPORTED_CHAIN_IDS } - return ALL_SUPPORTED_CHAIN_IDS.filter((chain) => - producerNetworks.includes(chain) - ); + return ALL_SUPPORTED_CHAIN_IDS.filter((chain) => producerNetworks.includes(chain)) } -async function gracefulShutdown( - producers: Runnable[], - producersPromise: Promise -) { - if (shuttingDown) return; - shuttingDown = true; +async function gracefulShutdown(producers: Runnable[], producersPromise: Promise) { + if (shuttingDown) return + shuttingDown = true // Command all producers to stop - logger.info(`Stopping ${producers.length} producers...`); + logger.info(`Stopping ${producers.length} producers...`) - const stopProducersPromise = Promise.all( - producers.map((producer) => producer.stop()) - ); + const stopProducersPromise = Promise.all(producers.map((producer) => producer.stop())) const timeoutInGracePeriod = new Promise((resolve) => setTimeout(() => { - logger.info( - `Some of the producers did not stop in time (${ - TIMEOUT_STOP_PRODUCERS / 1000 - }s), forcing exit` - ); - resolve(true); + logger.info(`Some of the producers did not stop in time (${TIMEOUT_STOP_PRODUCERS / 1000}s), forcing exit`) + resolve(true) }, TIMEOUT_STOP_PRODUCERS) - ); + ) await Promise.race([ // Wait for all producers to actually stop - stopProducersPromise - .then(() => producersPromise) - .then(() => logger.info('All producers have been stopped')), + stopProducersPromise.then(() => producersPromise).then(() => logger.info('All producers have been stopped')), // Give some grace period (otherwise timeout) timeoutInGracePeriod, - ]); + ]) - logger.info('Bye!'); - process.exit(0); + logger.info('Bye!') + process.exit(0) } // Start the main loop -mainLoop().catch((error) => logger.error(error, 'Unhandled error in producer')); +mainLoop().catch((error) => logger.error(error, 'Unhandled error in producer')) diff --git a/apps/notification-producer/src/producers/cms/CmsNotificationProducer.ts b/apps/notification-producer/src/producers/cms/CmsNotificationProducer.ts index 0c0d78a8..cfa1147c 100644 --- a/apps/notification-producer/src/producers/cms/CmsNotificationProducer.ts +++ b/apps/notification-producer/src/producers/cms/CmsNotificationProducer.ts @@ -1,22 +1,19 @@ -import { PushNotification } from '@cowprotocol/notifications'; -import { - CmsPushNotification, - PushNotificationsRepository, -} from '@cowprotocol/repositories'; -import Mustache from 'mustache'; -import { Runnable } from '../../../types'; -import { PushSubscriptionsRepository } from '@cowprotocol/repositories'; -import { logger, doForever } from '@cowprotocol/shared'; - -const WAIT_TIME = 30000; +import { PushNotification } from '@cowprotocol/notifications' +import { CmsPushNotification, PushNotificationsRepository } from '@cowprotocol/repositories' +import Mustache from 'mustache' +import { Runnable } from '../../../types' +import { PushSubscriptionsRepository } from '@cowprotocol/repositories' +import { logger, doForever } from '@cowprotocol/shared' + +const WAIT_TIME = 30000 export type CmsNotificationProducerProps = { - pushNotificationsRepository: PushNotificationsRepository; - pushSubscriptionsRepository: PushSubscriptionsRepository; -}; + pushNotificationsRepository: PushNotificationsRepository + pushSubscriptionsRepository: PushSubscriptionsRepository +} export class CmsNotificationProducer implements Runnable { - isStopping = false; + isStopping = false /** * This in-memory state just adds some resilience in case there's an error posting the message. @@ -24,7 +21,7 @@ export class CmsNotificationProducer implements Runnable { * * This solution is a patch until we properly implement a more reliable consumption */ - pendingNotifications = new Map(); + pendingNotifications = new Map() constructor(private props: CmsNotificationProducerProps) {} @@ -39,72 +36,61 @@ export class CmsNotificationProducer implements Runnable { name: 'CmsNotificationProducer', callback: async (stop) => { if (this.isStopping) { - stop(); - return; + stop() + return } - await this.fetchAndSend(); + await this.fetchAndSend() }, waitTimeMilliseconds: WAIT_TIME, logger, - }); + }) - logger.info('CmsNotificationProducer', 'stopped'); + logger.info('CmsNotificationProducer', 'stopped') } async stop(): Promise { - this.isStopping = true; + this.isStopping = true } async fetchAndSend(): Promise { - const accounts = - await this.props.pushSubscriptionsRepository.getAllSubscribedAccounts(); + const accounts = await this.props.pushSubscriptionsRepository.getAllSubscribedAccounts() // Get PUSH notifications - const cmsPushNotifications = ( - await this.props.pushSubscriptionsRepository.getPushNotifications() - ).filter( + const cmsPushNotifications = (await this.props.pushSubscriptionsRepository.getPushNotifications()).filter( // Include only the notifications for subscribed accounts ({ account }) => accounts.includes(account) - ); + ) - const pendingNotifications = Array.from(this.pendingNotifications.values()); - const pushNotifications = cmsPushNotifications - .map(fromCmsToNotifications) - .concat(pendingNotifications); + const pendingNotifications = Array.from(this.pendingNotifications.values()) + const pushNotifications = cmsPushNotifications.map(fromCmsToNotifications).concat(pendingNotifications) if (pushNotifications.length === 0) { - return; + return } - logger.debug( - `[notification-producer:main] ${pushNotifications.length} new PUSH notifications` - ); + logger.debug(`[notification-producer:main] ${pushNotifications.length} new PUSH notifications`) // Save notifications in-memory, so they are not lost if there's an issue with the queue - pushNotifications.forEach((notification) => - this.pendingNotifications.set(notification.id, notification) - ); + pushNotifications.forEach((notification) => this.pendingNotifications.set(notification.id, notification)) // Connect - await this.props.pushNotificationsRepository.connect(); + await this.props.pushNotificationsRepository.connect() // Post notifications to queue - this.props.pushNotificationsRepository.send(pushNotifications); - this.pendingNotifications.clear(); + this.props.pushNotificationsRepository.send(pushNotifications) + this.pendingNotifications.clear() } } -function fromCmsToNotifications( - cmsNotification: CmsPushNotification -): PushNotification { +function fromCmsToNotifications(cmsNotification: CmsPushNotification): PushNotification { const { id, account, data, notification_template: { title, description, url }, - } = cmsNotification; - const message = Mustache.render(description, data); - const cmsNotificationId = id.toString(); + } = cmsNotification + const message = Mustache.render(description, data) + const cmsNotificationId = id.toString() return { id: cmsNotificationId, @@ -115,5 +101,5 @@ function fromCmsToNotifications( context: { cmsId: cmsNotificationId, }, - }; + } } diff --git a/apps/notification-producer/src/producers/expired-orders/ExpiredOrdersNotificationProducer.ts b/apps/notification-producer/src/producers/expired-orders/ExpiredOrdersNotificationProducer.ts index a9df6c01..7df31762 100644 --- a/apps/notification-producer/src/producers/expired-orders/ExpiredOrdersNotificationProducer.ts +++ b/apps/notification-producer/src/producers/expired-orders/ExpiredOrdersNotificationProducer.ts @@ -1,4 +1,4 @@ -import { BARN_ETH_FLOW_ADDRESSES, ETH_FLOW_ADDRESSES, SupportedChainId } from '@cowprotocol/cow-sdk'; +import { BARN_ETH_FLOW_ADDRESSES, ETH_FLOW_ADDRESSES, SupportedChainId } from '@cowprotocol/cow-sdk' import { Erc20Repository, ExpiredOrdersRepository, @@ -6,42 +6,42 @@ import { IndexerStateValue, OnChainPlacedOrdersRepository, PushNotificationsRepository, - PushSubscriptionsRepository -} from '@cowprotocol/repositories'; + PushSubscriptionsRepository, +} from '@cowprotocol/repositories' -import { Runnable } from '../../../types'; -import { doForever, logger } from '@cowprotocol/shared'; -import { getExpiredOrderNotification } from './getExpiredOrderNotification'; -import { isTruthy } from '../../utils/commonUtils'; +import { Runnable } from '../../../types' +import { doForever, logger } from '@cowprotocol/shared' +import { getExpiredOrderNotification } from './getExpiredOrderNotification' +import { isTruthy } from '../../utils/commonUtils' async function wait(time: number) { return new Promise((res) => setTimeout(res, time)) } -const WAIT_TIME = 10_000; -const POLLING_INTERVAL = 120_000; // 2 minutes -const PRODUCER_NAME = 'expired_orders_notification_producer'; +const WAIT_TIME = 10_000 +const POLLING_INTERVAL = 120_000 // 2 minutes +const PRODUCER_NAME = 'expired_orders_notification_producer' export type ExpiredOrdersNotificationProducerProps = { - chainId: SupportedChainId; - erc20Repository: Erc20Repository; - indexerStateRepository: IndexerStateRepository; - pushSubscriptionsRepository: PushSubscriptionsRepository; - expiredOrdersRepository: ExpiredOrdersRepository; - pushNotificationsRepository: PushNotificationsRepository; - onChainPlacedOrdersRepository: OnChainPlacedOrdersRepository; -}; + chainId: SupportedChainId + erc20Repository: Erc20Repository + indexerStateRepository: IndexerStateRepository + pushSubscriptionsRepository: PushSubscriptionsRepository + expiredOrdersRepository: ExpiredOrdersRepository + pushNotificationsRepository: PushNotificationsRepository + onChainPlacedOrdersRepository: OnChainPlacedOrdersRepository +} export interface ExpiredOrdersNotificationProducerState extends IndexerStateValue { - lastCheckTimestamp: string; + lastCheckTimestamp: string } export class ExpiredOrdersNotificationProducer implements Runnable { - isStopping = false; - prefix: string; + isStopping = false + prefix: string constructor(private props: ExpiredOrdersNotificationProducerProps) { - this.prefix = '[ExpiredOrdersNotificationProducer:' + this.props.chainId + ']'; + this.prefix = '[ExpiredOrdersNotificationProducer:' + this.props.chainId + ']' } /** @@ -55,28 +55,30 @@ export class ExpiredOrdersNotificationProducer implements Runnable { name: 'ExpiredOrdersNotificationProducer:' + this.props.chainId, callback: async (stop) => { if (this.isStopping) { - stop(); - return; + stop() + return } - await this.processExpiredOrders(); + await this.processExpiredOrders() }, waitTimeMilliseconds: WAIT_TIME, - logger - }); + logger, + }) } async stop(): Promise { - this.isStopping = true; + this.isStopping = true } async processExpiredOrders(): Promise { - return this.pollExpiredOrders().then(() => { - return wait(POLLING_INTERVAL); - }).then(() => { - if (this.isStopping) return - - return this.processExpiredOrders(); - }); + return this.pollExpiredOrders() + .then(() => { + return wait(POLLING_INTERVAL) + }) + .then(() => { + if (this.isStopping) return + + return this.processExpiredOrders() + }) } async pollExpiredOrders() { @@ -87,73 +89,78 @@ export class ExpiredOrdersNotificationProducer implements Runnable { pushSubscriptionsRepository, expiredOrdersRepository, pushNotificationsRepository, - onChainPlacedOrdersRepository - } = this.props; + onChainPlacedOrdersRepository, + } = this.props - const nowTimestamp = Math.ceil(Date.now() / 1000); + const nowTimestamp = Math.ceil(Date.now() / 1000) - const stateRegistry = - await indexerStateRepository.get( - PRODUCER_NAME, - chainId - ); + const stateRegistry = await indexerStateRepository.get( + PRODUCER_NAME, + chainId + ) - const lastCheckTimestampRaw = stateRegistry?.state.lastCheckTimestamp; + const lastCheckTimestampRaw = stateRegistry?.state.lastCheckTimestamp if (lastCheckTimestampRaw) { - const lastCheckTimestamp = Number(lastCheckTimestampRaw); + const lastCheckTimestamp = Number(lastCheckTimestampRaw) - const ethFlowAddresses = [ETH_FLOW_ADDRESSES[chainId], BARN_ETH_FLOW_ADDRESSES[chainId]].map(t => t.toLowerCase()); + const ethFlowAddresses = [ETH_FLOW_ADDRESSES[chainId], BARN_ETH_FLOW_ADDRESSES[chainId]].map((t) => + t.toLowerCase() + ) - const accounts = - await pushSubscriptionsRepository.getAllSubscribedAccounts(); + const accounts = await pushSubscriptionsRepository.getAllSubscribedAccounts() const expiredOrders = await expiredOrdersRepository.fetchExpiredOrdersForAccounts({ chainId, accounts: [...accounts, ...ethFlowAddresses], lastCheckTimestamp, - nowTimestamp - }); + nowTimestamp, + }) const ethFlowOrderOwners = expiredOrders.length - ? await onChainPlacedOrdersRepository.getAccountsForOrders(chainId, expiredOrders.map(o => o.uid)) - : {}; + ? await onChainPlacedOrdersRepository.getAccountsForOrders( + chainId, + expiredOrders.map((o) => o.uid) + ) + : {} logger.debug( `${this.prefix} got ${expiredOrders.length} expired orders of ${accounts.length} accounts, lastCheckTimestamp=${lastCheckTimestamp}` - ); + ) - const notifications = await Promise.all(expiredOrders.map(order => { - const isEthFlowOrder = ethFlowAddresses.includes(order.owner.toLowerCase()); + const notifications = await Promise.all( + expiredOrders.map((order) => { + const isEthFlowOrder = ethFlowAddresses.includes(order.owner.toLowerCase()) - const orderOwner = isEthFlowOrder - ? Object.keys(ethFlowOrderOwners).find(key => { - const orderUids = ethFlowOrderOwners[key]; + const orderOwner = isEthFlowOrder + ? Object.keys(ethFlowOrderOwners).find((key) => { + const orderUids = ethFlowOrderOwners[key] - return orderUids.includes(order.uid.toLowerCase()); - }) - : order.owner.toLowerCase(); + return orderUids.includes(order.uid.toLowerCase()) + }) + : order.owner.toLowerCase() - if (!orderOwner) return Promise.resolve(undefined); + if (!orderOwner) return Promise.resolve(undefined) - return getExpiredOrderNotification(order, { - chainId, - nowTimestamp, - lastCheckTimestamp, - isEthFlowOrder, - owner: orderOwner, - erc20Repository - }); - })); + return getExpiredOrderNotification(order, { + chainId, + nowTimestamp, + lastCheckTimestamp, + isEthFlowOrder, + owner: orderOwner, + erc20Repository, + }) + }) + ) if (notifications.length > 0) { logger.info( `${this.prefix} Sending ${notifications.length} notifications`, JSON.stringify(notifications, null, 2) - ); + ) // Post notifications to queue - pushNotificationsRepository.send(notifications.filter(isTruthy)); + pushNotificationsRepository.send(notifications.filter(isTruthy)) } } @@ -161,6 +168,6 @@ export class ExpiredOrdersNotificationProducer implements Runnable { PRODUCER_NAME, { lastCheckTimestamp: nowTimestamp.toString() }, chainId - ); + ) } } diff --git a/apps/notification-producer/src/producers/expired-orders/getExpiredOrderNotification.ts b/apps/notification-producer/src/producers/expired-orders/getExpiredOrderNotification.ts index ee93ab4d..617d318b 100644 --- a/apps/notification-producer/src/producers/expired-orders/getExpiredOrderNotification.ts +++ b/apps/notification-producer/src/producers/expired-orders/getExpiredOrderNotification.ts @@ -1,23 +1,23 @@ -import { PushNotification } from '@cowprotocol/notifications'; -import { Erc20Repository, ParsedExpiredOrder } from '@cowprotocol/repositories'; -import { getExplorerUrl } from '@cowprotocol/shared'; -import { type SupportedChainId } from '@cowprotocol/cow-sdk'; -import { getNotificationSummary } from '../../utils/getNotificationSummary'; +import { PushNotification } from '@cowprotocol/notifications' +import { Erc20Repository, ParsedExpiredOrder } from '@cowprotocol/repositories' +import { getExplorerUrl } from '@cowprotocol/shared' +import { type SupportedChainId } from '@cowprotocol/cow-sdk' +import { getNotificationSummary } from '../../utils/getNotificationSummary' export interface ExpiredOrderNotificationContext { - chainId: SupportedChainId; - nowTimestamp: number; - lastCheckTimestamp: number; - isEthFlowOrder: boolean; - owner: string; - erc20Repository: Erc20Repository; + chainId: SupportedChainId + nowTimestamp: number + lastCheckTimestamp: number + isEthFlowOrder: boolean + owner: string + erc20Repository: Erc20Repository } export async function getExpiredOrderNotification( expiredOrder: ParsedExpiredOrder, notificationContext: ExpiredOrderNotificationContext ): Promise { - const { chainId, lastCheckTimestamp, nowTimestamp, isEthFlowOrder, owner, erc20Repository } = notificationContext; + const { chainId, lastCheckTimestamp, nowTimestamp, isEthFlowOrder, owner, erc20Repository } = notificationContext const summary = await getNotificationSummary({ chainId, @@ -26,16 +26,16 @@ export async function getExpiredOrderNotification( sellAmount: expiredOrder.sellAmount, buyAmount: expiredOrder.buyAmount, sellTokenAddress: expiredOrder.sellTokenAddress, - buyTokenAddress: expiredOrder.buyTokenAddress - }); + buyTokenAddress: expiredOrder.buyTokenAddress, + }) - const title = `🕐 Order ${summary} has expired`; + const title = `🕐 Order ${summary} has expired` const message = ` Expiration time: ${new Date(expiredOrder.validTo * 1000).toISOString()}. Account: ${owner}. - `.trim(); + `.trim() - const url = getExplorerUrl(chainId, expiredOrder.uid); + const url = getExplorerUrl(chainId, expiredOrder.uid) return { id: 'OrderExpired-' + expiredOrder.uid + '-' + expiredOrder.validTo + '-' + lastCheckTimestamp, @@ -45,7 +45,7 @@ export async function getExpiredOrderNotification( url, context: { chainId: chainId.toString(), - nowTimestamp: nowTimestamp.toString() - } - }; -} \ No newline at end of file + nowTimestamp: nowTimestamp.toString(), + }, + } +} diff --git a/apps/notification-producer/src/producers/trade/TradeNotificationProducer.ts b/apps/notification-producer/src/producers/trade/TradeNotificationProducer.ts index 84acd4b1..46cd760e 100644 --- a/apps/notification-producer/src/producers/trade/TradeNotificationProducer.ts +++ b/apps/notification-producer/src/producers/trade/TradeNotificationProducer.ts @@ -1,49 +1,49 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Erc20Repository, getViemClients, IndexerStateValue, PushNotificationsRepository, OnChainPlacedOrdersRepository, - OrdersAppDataRepository -} from '@cowprotocol/repositories'; -import { BlockNotFoundError } from 'viem'; + OrdersAppDataRepository, +} from '@cowprotocol/repositories' +import { BlockNotFoundError } from 'viem' -import { Runnable } from '../../../types'; -import { PushSubscriptionsRepository } from '@cowprotocol/repositories'; -import { IndexerStateRepository } from '@cowprotocol/repositories'; -import { doForever, logger } from '@cowprotocol/shared'; -import { getTradeNotifications } from './getTradeNotifications'; +import { Runnable } from '../../../types' +import { PushSubscriptionsRepository } from '@cowprotocol/repositories' +import { IndexerStateRepository } from '@cowprotocol/repositories' +import { doForever, logger } from '@cowprotocol/shared' +import { getTradeNotifications } from './getTradeNotifications' -const WAIT_TIME = 10000; -const PRODUCER_NAME = 'trade_notification_producer'; -const MAX_BLOCKS_PER_BATCH = 5000n; +const WAIT_TIME = 10000 +const PRODUCER_NAME = 'trade_notification_producer' +const MAX_BLOCKS_PER_BATCH = 5000n -const NO_PENDING_BLOCKS = { hasPendingBlocks: false }; -const HAS_PENDING_BLOCKS = { hasPendingBlocks: true }; +const NO_PENDING_BLOCKS = { hasPendingBlocks: false } +const HAS_PENDING_BLOCKS = { hasPendingBlocks: true } export type TradeNotificationProducerProps = { - chainId: SupportedChainId; - pushNotificationsRepository: PushNotificationsRepository; - pushSubscriptionsRepository: PushSubscriptionsRepository; - indexerStateRepository: IndexerStateRepository; - erc20Repository: Erc20Repository; - onChainPlacedOrdersRepository: OnChainPlacedOrdersRepository; - ordersAppDataRepository: OrdersAppDataRepository; -}; + chainId: SupportedChainId + pushNotificationsRepository: PushNotificationsRepository + pushSubscriptionsRepository: PushSubscriptionsRepository + indexerStateRepository: IndexerStateRepository + erc20Repository: Erc20Repository + onChainPlacedOrdersRepository: OnChainPlacedOrdersRepository + ordersAppDataRepository: OrdersAppDataRepository +} export interface TradeNotificationProducerState extends IndexerStateValue { - lastBlock: string; - lastBlockTimestamp: string; - lastBlockHash: string; + lastBlock: string + lastBlockTimestamp: string + lastBlockHash: string } export class TradeNotificationProducer implements Runnable { - isStopping = false; - prefix: string; + isStopping = false + prefix: string constructor(private props: TradeNotificationProducerProps) { - this.prefix = '[TradeNotificationProducer:' + this.props.chainId + ']'; + this.prefix = '[TradeNotificationProducer:' + this.props.chainId + ']' } /** @@ -57,124 +57,108 @@ export class TradeNotificationProducer implements Runnable { name: 'TradeNotificationProducer:' + this.props.chainId, callback: async (stop) => { if (this.isStopping) { - stop(); - return; + stop() + return } - await this.fetchAndSend(); + await this.fetchAndSend() }, waitTimeMilliseconds: WAIT_TIME, logger, - }); + }) } async stop(): Promise { - this.isStopping = true; + this.isStopping = true } async fetchAndSend(): Promise { - let hasPendingBlocks = true; + let hasPendingBlocks = true // Keep processing blocks until there are no more pending blocks while (hasPendingBlocks) { - ({ hasPendingBlocks } = await this.processAllPendingBlocks()); + ;({ hasPendingBlocks } = await this.processAllPendingBlocks()) } } async processAllPendingBlocks(): Promise<{ hasPendingBlocks: boolean }> { - const { chainId, indexerStateRepository } = this.props; + const { chainId, indexerStateRepository } = this.props // Get last indexed block - const stateRegistry = - await indexerStateRepository.get( - PRODUCER_NAME, - chainId - ); + const stateRegistry = await indexerStateRepository.get(PRODUCER_NAME, chainId) // Get last block - const client = getViemClients()[chainId]; - const lastBlock = await client.getBlock(); - const toBlockFinal = lastBlock.number; + const client = getViemClients()[chainId] + const lastBlock = await client.getBlock() + const toBlockFinal = lastBlock.number // Get starting block - let fromBlock = stateRegistry?.state - ? BigInt(stateRegistry.state.lastBlock) + 1n - : toBlockFinal; + let fromBlock = stateRegistry?.state ? BigInt(stateRegistry.state.lastBlock) + 1n : toBlockFinal - const totalBlocksToIndex = toBlockFinal - fromBlock + 1n; + const totalBlocksToIndex = toBlockFinal - fromBlock + 1n // Print debug message if (totalBlocksToIndex < 1n) { // We are up to date. Nothing to index - logger.trace(`${this.prefix} No new blocks to index`); - return NO_PENDING_BLOCKS; + logger.trace(`${this.prefix} No new blocks to index`) + return NO_PENDING_BLOCKS } else { - logger.debug( - `${this.prefix} Indexing from block ${fromBlock} to ${toBlockFinal}: ${totalBlocksToIndex} blocks` - ); + logger.debug(`${this.prefix} Indexing from block ${fromBlock} to ${toBlockFinal}: ${totalBlocksToIndex} blocks`) } // Process blocks in batches - let page = 1; - const totalPages = Math.ceil( - Number(totalBlocksToIndex) / Number(MAX_BLOCKS_PER_BATCH) - ); + let page = 1 + const totalPages = Math.ceil(Number(totalBlocksToIndex) / Number(MAX_BLOCKS_PER_BATCH)) while (fromBlock <= toBlockFinal) { // Calculate toBlock for this batch const toBlock = - fromBlock + MAX_BLOCKS_PER_BATCH - 1n > toBlockFinal - ? toBlockFinal - : fromBlock + MAX_BLOCKS_PER_BATCH - 1n; + fromBlock + MAX_BLOCKS_PER_BATCH - 1n > toBlockFinal ? toBlockFinal : fromBlock + MAX_BLOCKS_PER_BATCH - 1n // Process this batch of blocks // Print debug message only if there's more than one page (otherwise is too spammy) if (totalPages !== 1) { logger.debug( - `${ - this.prefix - } Processing batch ${page} of ${totalPages}: From block ${fromBlock} to ${toBlock}: ${ + `${this.prefix} Processing batch ${page} of ${totalPages}: From block ${fromBlock} to ${toBlock}: ${ toBlock - fromBlock + 1n } blocks` - ); + ) } - let toBlockInfo; + let toBlockInfo try { - toBlockInfo = await client.getBlock({ blockNumber: toBlock }); + toBlockInfo = await client.getBlock({ blockNumber: toBlock }) } catch (e) { if (e instanceof BlockNotFoundError) { logger.warn( `${this.prefix} Block ${toBlock} not found on RPC node (chainId: ${chainId}) (possible load balancer lag). Stopping batch processing.` - ); - break; + ) + break } - throw e; + throw e } const producerState: TradeNotificationProducerState = { lastBlock: toBlock.toString(), lastBlockTimestamp: toBlockInfo.timestamp.toString(), lastBlockHash: toBlockInfo.hash, - }; - await this.processBlocks(fromBlock, toBlock, producerState); + } + await this.processBlocks(fromBlock, toBlock, producerState) // Move to next batch - fromBlock = toBlock + 1n; - page++; + fromBlock = toBlock + 1n + page++ } // Check if during the process time there were some new blocks - const newLastBlock = await client.getBlock(); + const newLastBlock = await client.getBlock() if (newLastBlock.number > toBlockFinal) { logger.debug( - `${this.prefix} New blocks were indexed during the process: ${ - newLastBlock.number - toBlockFinal - } blocks` - ); + `${this.prefix} New blocks were indexed during the process: ${newLastBlock.number - toBlockFinal} blocks` + ) // Recursive call to process the new blocks - return HAS_PENDING_BLOCKS; + return HAS_PENDING_BLOCKS } else { - return NO_PENDING_BLOCKS; + return NO_PENDING_BLOCKS } } @@ -189,12 +173,11 @@ export class TradeNotificationProducer implements Runnable { indexerStateRepository, erc20Repository, onChainPlacedOrdersRepository, - ordersAppDataRepository - } = this.props; + ordersAppDataRepository, + } = this.props // Get all accounts subscribed to PUSH notifications - const accounts = - await pushSubscriptionsRepository.getAllSubscribedAccounts(); + const accounts = await pushSubscriptionsRepository.getAllSubscribedAccounts() // Get all trade notifications for the block range const notificationPromises = getTradeNotifications({ @@ -206,30 +189,26 @@ export class TradeNotificationProducer implements Runnable { onChainPlacedOrdersRepository, ordersAppDataRepository, prefix: this.prefix, - }); + }) // Connect to PUSH repository - await this.props.pushNotificationsRepository.connect(); + await this.props.pushNotificationsRepository.connect() // Await to resolve all notifications - const notifications = await notificationPromises; + const notifications = await notificationPromises // Return early if there are no notifications if (notifications.length > 0) { logger.info( `${this.prefix} Sending ${notifications.length} notifications`, JSON.stringify(notifications, null, 2) - ); + ) // Post notifications to queue - this.props.pushNotificationsRepository.send(notifications); + this.props.pushNotificationsRepository.send(notifications) } // Update state - await indexerStateRepository.upsert( - PRODUCER_NAME, - producerState, - chainId - ); + await indexerStateRepository.upsert(PRODUCER_NAME, producerState, chainId) } } diff --git a/apps/notification-producer/src/producers/trade/fromTradeToNotification.ts b/apps/notification-producer/src/producers/trade/fromTradeToNotification.ts index 36b25180..97e89e64 100644 --- a/apps/notification-producer/src/producers/trade/fromTradeToNotification.ts +++ b/apps/notification-producer/src/producers/trade/fromTradeToNotification.ts @@ -1,24 +1,24 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { PushNotification } from '@cowprotocol/notifications'; -import { Erc20Repository } from '@cowprotocol/repositories'; -import { getExplorerUrl, logger } from '@cowprotocol/shared'; -import { getNotificationSummary } from '../../utils/getNotificationSummary'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { PushNotification } from '@cowprotocol/notifications' +import { Erc20Repository } from '@cowprotocol/repositories' +import { getExplorerUrl, logger } from '@cowprotocol/shared' +import { getNotificationSummary } from '../../utils/getNotificationSummary' export async function fromTradeToNotification(props: { - prefix: string; - id: string; - isEthFlowOrder: boolean; - chainId: SupportedChainId; - orderUid: string; - owner: string; - sellTokenAddress: string; - buyTokenAddress: string; - sellAmount: bigint; - buyAmount: bigint; - feeAmount: bigint; - erc20Repository: Erc20Repository; - transactionHash: string; - logIndex: number; + prefix: string + id: string + isEthFlowOrder: boolean + chainId: SupportedChainId + orderUid: string + owner: string + sellTokenAddress: string + buyTokenAddress: string + sellAmount: bigint + buyAmount: bigint + feeAmount: bigint + erc20Repository: Erc20Repository + transactionHash: string + logIndex: number }): Promise { const { id, @@ -33,8 +33,8 @@ export async function fromTradeToNotification(props: { prefix, orderUid, transactionHash, - logIndex - } = props; + logIndex, + } = props const summary = await getNotificationSummary({ chainId, @@ -43,16 +43,14 @@ export async function fromTradeToNotification(props: { sellTokenAddress, buyTokenAddress, sellAmount, - buyAmount - }); + buyAmount, + }) - const title = `Trade ${summary}`; - const message = `Account: ${owner}`; + const title = `Trade ${summary}` + const message = `Account: ${owner}` - const url = orderUid ? getExplorerUrl(chainId, orderUid) : undefined; - logger.info( - `${prefix} New ${message} for ${owner}. Tx=${transactionHash}, logIndex=${logIndex}` - ); + const url = orderUid ? getExplorerUrl(chainId, orderUid) : undefined + logger.info(`${prefix} New ${message} for ${owner}. Tx=${transactionHash}, logIndex=${logIndex}`) return { id, account: owner, @@ -61,7 +59,7 @@ export async function fromTradeToNotification(props: { url, context: { transactionHash, - logIndex: logIndex.toString() - } - }; + logIndex: logIndex.toString(), + }, + } } diff --git a/apps/notification-producer/src/producers/trade/getTradeNotifications.ts b/apps/notification-producer/src/producers/trade/getTradeNotifications.ts index f7eadc3c..3e61f3dd 100644 --- a/apps/notification-producer/src/producers/trade/getTradeNotifications.ts +++ b/apps/notification-producer/src/producers/trade/getTradeNotifications.ts @@ -3,38 +3,36 @@ import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, ETH_FLOW_ADDRESSES, SupportedChainId, - LatestAppDataDocVersion -} from '@cowprotocol/cow-sdk'; -import { PushNotification } from '@cowprotocol/notifications'; + LatestAppDataDocVersion, +} from '@cowprotocol/cow-sdk' +import { PushNotification } from '@cowprotocol/notifications' import { Erc20Repository, getViemClients, OnChainPlacedOrdersRepository, - OrdersAppDataRepository -} from '@cowprotocol/repositories'; -import { bigIntReplacer, logger } from '@cowprotocol/shared'; -import { getAddress, parseAbi } from 'viem'; -import { fromTradeToNotification } from './fromTradeToNotification'; + OrdersAppDataRepository, +} from '@cowprotocol/repositories' +import { bigIntReplacer, logger } from '@cowprotocol/shared' +import { getAddress, parseAbi } from 'viem' +import { fromTradeToNotification } from './fromTradeToNotification' const EVENTS = parseAbi([ // 'event OrderInvalidated(address indexed owner, bytes orderUid)', // Do not index this event - 'event Trade(address indexed owner, address sellToken, address buyToken, uint256 sellAmount, uint256 buyAmount, uint256 feeAmount, bytes orderUid)' -]); + 'event Trade(address indexed owner, address sellToken, address buyToken, uint256 sellAmount, uint256 buyAmount, uint256 feeAmount, bytes orderUid)', +]) export interface GetTradeNotificationParams { - accounts: string[]; - fromBlock: bigint; - toBlock: bigint; - chainId: SupportedChainId; - erc20Repository: Erc20Repository; - onChainPlacedOrdersRepository: OnChainPlacedOrdersRepository; - ordersAppDataRepository: OrdersAppDataRepository; - prefix: string; + accounts: string[] + fromBlock: bigint + toBlock: bigint + chainId: SupportedChainId + erc20Repository: Erc20Repository + onChainPlacedOrdersRepository: OnChainPlacedOrdersRepository + ordersAppDataRepository: OrdersAppDataRepository + prefix: string } -export async function getTradeNotifications( - params: GetTradeNotificationParams -) { +export async function getTradeNotifications(params: GetTradeNotificationParams) { const { accounts, fromBlock, @@ -43,13 +41,12 @@ export async function getTradeNotifications( erc20Repository, onChainPlacedOrdersRepository, ordersAppDataRepository, - prefix - } = - params; + prefix, + } = params - const client = getViemClients()[chainId]; + const client = getViemClients()[chainId] - const ethFlowAddresses = [ETH_FLOW_ADDRESSES[chainId], BARN_ETH_FLOW_ADDRESSES[chainId]].map(t => t.toLowerCase()); + const ethFlowAddresses = [ETH_FLOW_ADDRESSES[chainId], BARN_ETH_FLOW_ADDRESSES[chainId]].map((t) => t.toLowerCase()) const logs = await client.getLogs({ events: EVENTS, @@ -57,127 +54,118 @@ export async function getTradeNotifications( toBlock, address: getAddress(COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId]), args: { - owner: [...accounts, ...ethFlowAddresses] - } as any - }); + owner: [...accounts, ...ethFlowAddresses], + } as any, + }) // Return empty array if no events if (logs.length === 0) { - return []; + return [] } - logger.debug(`${prefix} Found ${logs.length} events`); + logger.debug(`${prefix} Found ${logs.length} events`) const orderUids = logs.reduce((acc, log) => { - const { orderUid } = log.args; + const { orderUid } = log.args - if (log.eventName !== 'Trade' || !orderUid) return acc; + if (log.eventName !== 'Trade' || !orderUid) return acc - acc.push(orderUid.toLowerCase()); + acc.push(orderUid.toLowerCase()) - return acc; - }, []); + return acc + }, []) const ethFlowOrderIds = logs.reduce((acc, log) => { - const { owner, orderUid } = log.args; + const { owner, orderUid } = log.args - if (log.eventName !== 'Trade') return acc; - if (!orderUid) return acc; - if (!owner || !ethFlowAddresses.includes(owner.toLowerCase())) return acc; + if (log.eventName !== 'Trade') return acc + if (!orderUid) return acc + if (!owner || !ethFlowAddresses.includes(owner.toLowerCase())) return acc - acc.push(orderUid); + acc.push(orderUid) - return acc; - }, []); + return acc + }, []) const ethFlowOrderOwners = ethFlowOrderIds.length ? await onChainPlacedOrdersRepository.getAccountsForOrders(chainId, ethFlowOrderIds) - : {}; - - const ordersAppData = await ordersAppDataRepository.getAppDataForOrders(chainId, orderUids); - - const notificationPromises = logs.reduce[]>( - (acc, log) => { - switch (log.eventName) { - case 'Trade': { - const { - owner, - orderUid, - sellToken: sellTokenAddress, - buyToken: buyTokenAddress, - sellAmount, - buyAmount, - feeAmount - } = log.args; - - if ( - owner === undefined || - sellTokenAddress === undefined || - buyTokenAddress === undefined || - orderUid === undefined || - sellAmount === undefined || - buyAmount === undefined || - feeAmount === undefined - ) { - logger.error( - `${prefix} Invalid Trade event ${JSON.stringify( - log, - bigIntReplacer, - 2 - )}` - ); - break; - } - - const orderUidLower = orderUid.toLowerCase(); - const isEthFlowOrder = ethFlowAddresses.includes(owner.toLowerCase()); - const appData = ordersAppData.get(orderUidLower); - const isBridgingOrder = !!(appData as LatestAppDataDocVersion)?.metadata?.bridging - - const orderOwner = isEthFlowOrder - ? Object.keys(ethFlowOrderOwners).find(key => { - const orderUids = ethFlowOrderOwners[key]; - - return orderUids.includes(orderUidLower); - }) - : owner.toLowerCase(); - - if (orderOwner && !isBridgingOrder) { - acc.push( - fromTradeToNotification({ - prefix, - isEthFlowOrder, - id: 'Trade-' + log.transactionHash + '-' + log.logIndex, - chainId, - orderUid, - owner: orderOwner, - sellTokenAddress, - buyTokenAddress, - sellAmount, - buyAmount, - feeAmount, - erc20Repository, - transactionHash: log.transactionHash, - logIndex: log.logIndex - }) - ); - } - break; + : {} + + const ordersAppData = await ordersAppDataRepository.getAppDataForOrders(chainId, orderUids) + + const notificationPromises = logs.reduce[]>((acc, log) => { + switch (log.eventName) { + case 'Trade': { + const { + owner, + orderUid, + sellToken: sellTokenAddress, + buyToken: buyTokenAddress, + sellAmount, + buyAmount, + feeAmount, + } = log.args + + if ( + owner === undefined || + sellTokenAddress === undefined || + buyTokenAddress === undefined || + orderUid === undefined || + sellAmount === undefined || + buyAmount === undefined || + feeAmount === undefined + ) { + logger.error(`${prefix} Invalid Trade event ${JSON.stringify(log, bigIntReplacer, 2)}`) + break } - default: - logger.info(`${prefix} Unknown event ${log}`); - break; + const orderUidLower = orderUid.toLowerCase() + const isEthFlowOrder = ethFlowAddresses.includes(owner.toLowerCase()) + const appData = ordersAppData.get(orderUidLower) + const isBridgingOrder = !!(appData as LatestAppDataDocVersion)?.metadata?.bridging + + const orderOwner = isEthFlowOrder + ? Object.keys(ethFlowOrderOwners).find((key) => { + const orderUids = ethFlowOrderOwners[key] + + return orderUids.includes(orderUidLower) + }) + : owner.toLowerCase() + + if (orderOwner && !isBridgingOrder) { + acc.push( + fromTradeToNotification({ + prefix, + isEthFlowOrder, + id: 'Trade-' + log.transactionHash + '-' + log.logIndex, + chainId, + orderUid, + owner: orderOwner, + sellTokenAddress, + buyTokenAddress, + sellAmount, + buyAmount, + feeAmount, + erc20Repository, + transactionHash: log.transactionHash, + logIndex: log.logIndex, + }) + ) + } + break } - return acc; - }, - [] - ); + + default: + logger.info(`${prefix} Unknown event ${log}`) + break + } + return acc + }, []) // Return empty array if no notifications if (notificationPromises.length === 0) { - return []; + return [] } - return Promise.all(notificationPromises); + return Promise.all(notificationPromises) } diff --git a/apps/notification-producer/src/sendPush.test.ts b/apps/notification-producer/src/sendPush.test.ts index 5b21bc78..ecc25d24 100644 --- a/apps/notification-producer/src/sendPush.test.ts +++ b/apps/notification-producer/src/sendPush.test.ts @@ -1,23 +1,23 @@ -import { PushNotification } from '@cowprotocol/notifications'; -import { getPushNotificationsRepository } from '@cowprotocol/services'; +import { PushNotification } from '@cowprotocol/notifications' +import { getPushNotificationsRepository } from '@cowprotocol/services' -const POST_TO_QUEUE_ACCOUNT = process.env.POST_TO_QUEUE_ACCOUNT; +const POST_TO_QUEUE_ACCOUNT = process.env.POST_TO_QUEUE_ACCOUNT it('Post to queue', async () => { - console.log('POST_TO_QUEUE_ACCOUNT', POST_TO_QUEUE_ACCOUNT); + console.log('POST_TO_QUEUE_ACCOUNT', POST_TO_QUEUE_ACCOUNT) if (!POST_TO_QUEUE_ACCOUNT) { - return; + return } - const pushNotificationsRepository = getPushNotificationsRepository(); - await pushNotificationsRepository.connect(); + const pushNotificationsRepository = getPushNotificationsRepository() + await pushNotificationsRepository.connect() const markdown = `\ **Testing PUSH notifications** This \`message\` has been generated by [sendPush.test.ts](https://github.com/cowprotocol/bff/blob/main/apps/notification-producer/src/sendPush.test.ts) (in [BFF's](https://github.com/cowprotocol/bff) [apps/notification-producer](https://github.com/cowprotocol/bff/tree/main/apps/notification-producer)). -🐮 Have a nice day!`; +🐮 Have a nice day!` const message: PushNotification = { id: '1', @@ -25,14 +25,8 @@ This \`message\` has been generated by [sendPush.test.ts](https://github.com/cow message: markdown, account: POST_TO_QUEUE_ACCOUNT, url: 'https://swap.cow.fi/#/1/limit/WETH/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48?tab=all&page=1', - }; - - await pushNotificationsRepository.send([message]); - console.log( - `✅ Posted a message to queue for account ${POST_TO_QUEUE_ACCOUNT}:\n${JSON.stringify( - message, - null, - 2 - )}` - ); -}); + } + + await pushNotificationsRepository.send([message]) + console.log(`✅ Posted a message to queue for account ${POST_TO_QUEUE_ACCOUNT}:\n${JSON.stringify(message, null, 2)}`) +}) diff --git a/apps/notification-producer/src/utils/getNotificationSummary.ts b/apps/notification-producer/src/utils/getNotificationSummary.ts index c17553bc..e46bca90 100644 --- a/apps/notification-producer/src/utils/getNotificationSummary.ts +++ b/apps/notification-producer/src/utils/getNotificationSummary.ts @@ -1,36 +1,33 @@ -import { EVM_NATIVE_CURRENCY_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk'; -import { getAddress } from 'viem'; -import { ChainNames, formatAmount, formatTokenName } from '@cowprotocol/shared'; -import { Erc20Repository } from '@cowprotocol/repositories'; +import { EVM_NATIVE_CURRENCY_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' +import { getAddress } from 'viem' +import { ChainNames, formatAmount, formatTokenName } from '@cowprotocol/shared' +import { Erc20Repository } from '@cowprotocol/repositories' interface OrderInfoForNotificationParams { - chainId: SupportedChainId; - isEthFlowOrder: boolean; - erc20Repository: Erc20Repository; - sellTokenAddress: string; - buyTokenAddress: string; - sellAmount: string | bigint; - buyAmount: string | bigint; + chainId: SupportedChainId + isEthFlowOrder: boolean + erc20Repository: Erc20Repository + sellTokenAddress: string + buyTokenAddress: string + sellAmount: string | bigint + buyAmount: string | bigint } export async function getNotificationSummary(params: OrderInfoForNotificationParams): Promise { - const { chainId, isEthFlowOrder, erc20Repository } = params; + const { chainId, isEthFlowOrder, erc20Repository } = params const sellToken = await erc20Repository.get( chainId, isEthFlowOrder ? EVM_NATIVE_CURRENCY_ADDRESS : getAddress(params.sellTokenAddress) - ); + ) - const buyToken = await erc20Repository.get( - chainId, - getAddress(params.buyTokenAddress) - ); + const buyToken = await erc20Repository.get(chainId, getAddress(params.buyTokenAddress)) - const sellAmountFormatted = formatAmount(BigInt(params.sellAmount), sellToken?.decimals); - const buyAmountFormatted = formatAmount(BigInt(params.buyAmount), buyToken?.decimals); + const sellAmountFormatted = formatAmount(BigInt(params.sellAmount), sellToken?.decimals) + const buyAmountFormatted = formatAmount(BigInt(params.buyAmount), buyToken?.decimals) - const sellTokenName = formatTokenName(sellToken); - const buyTokenName = formatTokenName(buyToken); + const sellTokenName = formatTokenName(sellToken) + const buyTokenName = formatTokenName(buyToken) return `${sellAmountFormatted} ${sellTokenName} for ${buyAmountFormatted} ${buyTokenName} in ${ChainNames[chainId]}` -} \ No newline at end of file +} diff --git a/apps/notification-producer/types.ts b/apps/notification-producer/types.ts index c4fd0b7b..a1696586 100644 --- a/apps/notification-producer/types.ts +++ b/apps/notification-producer/types.ts @@ -5,10 +5,10 @@ export interface Runnable { /** * Start the program, this method should not throw or finish. */ - start(): Promise; + start(): Promise /** * Stop the program, this method should not throw or finish. */ - stop(): Promise; + stop(): Promise } diff --git a/apps/telegram-e2e/src/telegram/telegram.spec.ts b/apps/telegram-e2e/src/telegram/telegram.spec.ts index 47b5c3f0..2704a1c8 100644 --- a/apps/telegram-e2e/src/telegram/telegram.spec.ts +++ b/apps/telegram-e2e/src/telegram/telegram.spec.ts @@ -1,12 +1,12 @@ -import { execSync } from 'child_process'; -import { join } from 'path'; +import { execSync } from 'child_process' +import { join } from 'path' describe('CLI tests', () => { it('should print a message', () => { - const cliPath = join(process.cwd(), 'dist/apps/telegram'); + const cliPath = join(process.cwd(), 'dist/apps/telegram') - const output = execSync(`node ${cliPath}`).toString(); + const output = execSync(`node ${cliPath}`).toString() - expect(output).toMatch(/Hello World/); - }); -}); + expect(output).toMatch(/Hello World/) + }) +}) diff --git a/apps/telegram/src/main.ts b/apps/telegram/src/main.ts index 2a1056e4..432e1121 100644 --- a/apps/telegram/src/main.ts +++ b/apps/telegram/src/main.ts @@ -1,46 +1,39 @@ -import 'reflect-metadata'; - -import ms from 'ms'; -import { marked } from 'marked'; -import { JSDOM } from 'jsdom'; -import createDOMPurify from 'dompurify'; - -import { doForever, logger, sleep } from '@cowprotocol/shared'; -import { - getPushNotificationsRepository, - getPushSubscriptionsRepository, - getTelegramBot, -} from '@cowprotocol/services'; -import { PushNotification } from '@cowprotocol/notifications'; -import TelegramBot from 'node-telegram-bot-api'; -import { - CmsTelegramSubscription, - PushSubscriptionsRepository, -} from '@cowprotocol/repositories'; - -const WAIT_TIME = ms(`10s`); -const SUBSCRIPTION_CACHE_TIME = ms(`5m`); - -const SUBSCRIPTION_CACHE = new Map(); -const LAST_SUBSCRIPTION_CHECK = new Map(); - -let telegramBot: TelegramBot; +import 'reflect-metadata' + +import ms from 'ms' +import { marked } from 'marked' +import { JSDOM } from 'jsdom' +import createDOMPurify from 'dompurify' + +import { doForever, logger, sleep } from '@cowprotocol/shared' +import { getPushNotificationsRepository, getPushSubscriptionsRepository, getTelegramBot } from '@cowprotocol/services' +import { PushNotification } from '@cowprotocol/notifications' +import TelegramBot from 'node-telegram-bot-api' +import { CmsTelegramSubscription, PushSubscriptionsRepository } from '@cowprotocol/repositories' + +const WAIT_TIME = ms(`10s`) +const SUBSCRIPTION_CACHE_TIME = ms(`5m`) + +const SUBSCRIPTION_CACHE = new Map() +const LAST_SUBSCRIPTION_CHECK = new Map() + +let telegramBot: TelegramBot /** * Main loop: Run and re-attempt on error */ async function mainLoop() { // Create telegram bot - telegramBot = getTelegramBot(); + telegramBot = getTelegramBot() // Subscribe to notifications - logger.info('[telegram] Start telegram consumer'); + logger.info('[telegram] Start telegram consumer') await doForever({ name: 'telegram', callback: subscribeToNotifications, waitTimeMilliseconds: WAIT_TIME, logger, - }); + }) } async function getSubscriptions( @@ -48,72 +41,63 @@ async function getSubscriptions( pushSubscriptionsRepository: PushSubscriptionsRepository ): Promise { // Get the subscriptions for this account - const lastCheck = LAST_SUBSCRIPTION_CHECK.get(account); - if ( - !lastCheck || - lastCheck.getTime() + SUBSCRIPTION_CACHE_TIME < Date.now() - ) { + const lastCheck = LAST_SUBSCRIPTION_CHECK.get(account) + if (!lastCheck || lastCheck.getTime() + SUBSCRIPTION_CACHE_TIME < Date.now()) { // Get the subscriptions for this account (if we haven't checked in a while) - const subscriptionForAccount = - await pushSubscriptionsRepository.getAllTelegramSubscriptionsForAccounts([ - account, - ]); - SUBSCRIPTION_CACHE.set(account, subscriptionForAccount); + const subscriptionForAccount = await pushSubscriptionsRepository.getAllTelegramSubscriptionsForAccounts([account]) + SUBSCRIPTION_CACHE.set(account, subscriptionForAccount) } - return SUBSCRIPTION_CACHE.get(account) || []; + return SUBSCRIPTION_CACHE.get(account) || [] } async function sendNotificationToTelegram( notification: PushNotification, pushSubscriptionsRepository: PushSubscriptionsRepository ): Promise { - const { id, message, account, title, url, context } = notification; + const { id, message, account, title, url, context } = notification logger.debug( `[telegram] New PushNotification ${id} for ${account}. ${title}: ${message}. URL=${url}. Context=${JSON.stringify( context )}` - ); + ) // Get the subscriptions for this account - const telegramSubscriptions = await getSubscriptions( - account, - pushSubscriptionsRepository - ).catch((error) => { - logger.error(error, `Error getting subscriptions for account ${account}`); - return null; - }); + const telegramSubscriptions = await getSubscriptions(account, pushSubscriptionsRepository).catch((error) => { + logger.error(error, `Error getting subscriptions for account ${account}`) + return null + }) if (!telegramSubscriptions) { - return false; + return false } - let consumeMessage = false; + let consumeMessage = false try { if (telegramSubscriptions.length > 0) { // Send the message to all subscribers for (const { chatId } of telegramSubscriptions) { logger.info( `[telegram] Sending message ${id} to chatId ${chatId}. Title: ${title}. Message: ${message}. URL=${url}` - ); + ) // Send message to Telegram - await sendMessageTelegram(chatId, notification); + await sendMessageTelegram(chatId, notification) // Acknowledge the message once its been sent to at least one subscriber for this account - consumeMessage = true; + consumeMessage = true } } else { // No telegram subscriptions found for this account - consumeMessage = true; - logger.debug(`[telegram] No subscriptions found for account ${account}`); + consumeMessage = true + logger.debug(`[telegram] No subscriptions found for account ${account}`) } } catch (error) { - logger.error(error, `Error sending notification`); + logger.error(error, `Error sending notification`) } // We return whether we notified at least one consumer - return consumeMessage; + return consumeMessage } /** @@ -122,77 +106,64 @@ async function sendNotificationToTelegram( * This function will not resolved until the connection is closed or an error occurs. */ async function subscribeToNotifications() { - const pushNotificationsRepository = getPushNotificationsRepository(); - const pushSubscriptionsRepository = getPushSubscriptionsRepository(); + const pushNotificationsRepository = getPushNotificationsRepository() + const pushSubscriptionsRepository = getPushSubscriptionsRepository() // Connect to push notifications - const { connection } = await pushNotificationsRepository.connect(); + const { connection } = await pushNotificationsRepository.connect() // Subscribe to new notifications - const { subscriptionId, cancelSubscription } = - await pushNotificationsRepository.subscribe(async (notification) => { - return sendNotificationToTelegram( - notification, - pushSubscriptionsRepository - ); - }); - - logger.info( - `[telegram] Subscribed to notifications with ID ${subscriptionId}` - ); + const { subscriptionId, cancelSubscription } = await pushNotificationsRepository.subscribe(async (notification) => { + return sendNotificationToTelegram(notification, pushSubscriptionsRepository) + }) + + logger.info(`[telegram] Subscribed to notifications with ID ${subscriptionId}`) // Watch for connection close - let connectionOpen = true; + let connectionOpen = true connection.on('close', () => { - logger.error( - `[telegram] Queue connection closed! Reconnecting in ${WAIT_TIME / 1000}s` - ); - connectionOpen = false; + logger.error(`[telegram] Queue connection closed! Reconnecting in ${WAIT_TIME / 1000}s`) + connectionOpen = false // Cancel the subscription - cancelSubscription(); - }); + cancelSubscription() + }) // Wait while we have an open connection while (connectionOpen) { - await sleep(WAIT_TIME); + await sleep(WAIT_TIME) } } -async function sendMessageTelegram( - chatId: string, - notification: PushNotification -) { - const markdown = formatMessageMarkdown(notification); - const html = await markdownToTelegramHTMLSafe(markdown); +async function sendMessageTelegram(chatId: string, notification: PushNotification) { + const markdown = formatMessageMarkdown(notification) + const html = await markdownToTelegramHTMLSafe(markdown) try { telegramBot.sendMessage(chatId, html, { parse_mode: 'HTML', disable_web_page_preview: true, - }); + }) } catch (error) { - console.error( - `Error sending message to telegram.\nMarkdown:\n${markdown}\n\nOffending. HTML:\n${html}\n\n` - ); + console.error(`Error sending message to telegram.\nMarkdown:\n${markdown}\n\nOffending. HTML:\n${html}\n\n`) - throw error; + throw error } } function formatMessageMarkdown({ title, message, url }: PushNotification) { - const moreInfo = url ? `\n\nMore info in [Explorer](${url})` : ''; + const moreInfo = url ? `\n\nMore info in [Explorer](${url})` : '' return `\ **${title}**. -${message}${moreInfo}`; +${message}${moreInfo}` } async function markdownToTelegramHTMLSafe(markdown: string): Promise { - const html = await marked(markdown); + const html = await marked(markdown) - const window = new JSDOM('').window; - const DOMPurify = createDOMPurify(window); + const window = new JSDOM('').window + const DOMPurify = createDOMPurify(window) // Replace paragraph tags with double line breaks const withLineBreaks = html @@ -200,32 +171,18 @@ async function markdownToTelegramHTMLSafe(markdown: string): Promise { .replace(/^

/, '') // opening

at the start .replace(/<\/p>$/, '') // closing

at the end .replace(/<\/p>/g, '

') // fallback: closing p - .replace(/

/g, ''); // fallback: opening p + .replace(/

/g, '') // fallback: opening p // Only allow Telegram-supported tags const cleanHtml = DOMPurify.sanitize(withLineBreaks, { - ALLOWED_TAGS: [ - 'b', - 'strong', - 'i', - 'em', - 'u', - 'ins', - 's', - 'strike', - 'del', - 'a', - 'code', - 'pre', - 'br', - ], + ALLOWED_TAGS: ['b', 'strong', 'i', 'em', 'u', 'ins', 's', 'strike', 'del', 'a', 'code', 'pre', 'br'], ALLOWED_ATTR: ['href'], - }); + }) // Replace
tags with newlines - const finalHtml = cleanHtml.replace(//g, '\n'); + const finalHtml = cleanHtml.replace(//g, '\n') - return finalHtml; + return finalHtml } -mainLoop().catch((error) => logger.error(error, 'Unhandled error in telegram')); +mainLoop().catch((error) => logger.error(error, 'Unhandled error in telegram')) diff --git a/apps/twap-e2e/src/twap/twap.spec.ts b/apps/twap-e2e/src/twap/twap.spec.ts index 183d774d..45b571dd 100644 --- a/apps/twap-e2e/src/twap/twap.spec.ts +++ b/apps/twap-e2e/src/twap/twap.spec.ts @@ -1,10 +1,10 @@ -import axios from 'axios'; +import axios from 'axios' describe('GET /', () => { it('should be healthy', async () => { - const res = await axios.get(`/health-check`); + const res = await axios.get(`/health-check`) - expect(res.status).toBe(200); - expect(res.data).toEqual({ status: 'ok' }); - }); -}); + expect(res.status).toBe(200) + expect(res.data).toEqual({ status: 'ok' }) + }) +}) diff --git a/apps/twap/datasource.config.ts b/apps/twap/datasource.config.ts index 0665331a..c3b42043 100644 --- a/apps/twap/datasource.config.ts +++ b/apps/twap/datasource.config.ts @@ -1,7 +1,7 @@ -import * as dotenv from 'dotenv'; -import { DataSource } from 'typeorm'; +import * as dotenv from 'dotenv' +import { DataSource } from 'typeorm' -dotenv.config(); +dotenv.config() const dataSource = new DataSource({ type: 'postgres', @@ -12,8 +12,8 @@ const dataSource = new DataSource({ database: process.env.DATABASE_NAME, entities: ['src/app/data/*.ts'], migrations: ['src/migrations/*.ts'], -}); +}) -dataSource.initialize(); +dataSource.initialize() -export default dataSource; +export default dataSource diff --git a/apps/twap/src/app/app.spec.ts b/apps/twap/src/app/app.spec.ts index cc0352e0..2a42e0b1 100644 --- a/apps/twap/src/app/app.spec.ts +++ b/apps/twap/src/app/app.spec.ts @@ -1,20 +1,20 @@ -import Fastify, { FastifyInstance } from 'fastify'; -import { app } from './app'; +import Fastify, { FastifyInstance } from 'fastify' +import { app } from './app' describe('GET /', () => { - let server: FastifyInstance; + let server: FastifyInstance beforeEach(() => { - server = Fastify(); - server.register(app); - }); + server = Fastify() + server.register(app) + }) it.skip('should respond with a message', async () => { const response = await server.inject({ method: 'GET', url: '/', - }); + }) - expect(response.json()).toEqual({ message: 'Hello API' }); - }); -}); + expect(response.json()).toEqual({ message: 'Hello API' }) + }) +}) diff --git a/apps/twap/src/app/app.ts b/apps/twap/src/app/app.ts index f8ad6a71..c3082ef0 100644 --- a/apps/twap/src/app/app.ts +++ b/apps/twap/src/app/app.ts @@ -1,13 +1,13 @@ -import * as path from 'path'; -import { FastifyInstance } from 'fastify'; -import AutoLoad from '@fastify/autoload'; -import EnvPlugin from './plugins/env'; +import * as path from 'path' +import { FastifyInstance } from 'fastify' +import AutoLoad from '@fastify/autoload' +import EnvPlugin from './plugins/env' /* eslint-disable-next-line */ export interface AppOptions {} export async function app(fastify: FastifyInstance, opts: AppOptions) { - await fastify.register(EnvPlugin); + await fastify.register(EnvPlugin) // Do not touch the following lines @@ -17,7 +17,7 @@ export async function app(fastify: FastifyInstance, opts: AppOptions) { fastify.register(AutoLoad, { dir: path.join(__dirname, 'plugins'), options: { ...opts }, - }); + }) // This loads all plugins defined in routes // define your routes in one of these @@ -25,5 +25,5 @@ export async function app(fastify: FastifyInstance, opts: AppOptions) { dir: path.join(__dirname, 'routes'), routeParams: true, options: { ...opts }, - }); + }) } diff --git a/apps/twap/src/app/data/order.ts b/apps/twap/src/app/data/order.ts index 828f588e..5c096dca 100644 --- a/apps/twap/src/app/data/order.ts +++ b/apps/twap/src/app/data/order.ts @@ -1,20 +1,11 @@ -import { - BeforeInsert, - Column, - Entity, - ManyToOne, - PrimaryColumn, -} from 'typeorm'; -import { Wallet } from './wallet'; -import { - buildTwapOrderParamsStruct, - getConditionalOrderId, -} from '../utils/getConditionalOrderId'; +import { BeforeInsert, Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm' +import { Wallet } from './wallet' +import { buildTwapOrderParamsStruct, getConditionalOrderId } from '../utils/getConditionalOrderId' @Entity({ name: 'order' }) export class Order { @PrimaryColumn('varchar') - id: string; + id: string @BeforeInsert() createOrderId() { @@ -31,45 +22,45 @@ export class Order { span: this.span, appData: this.appData, }) - ); + ) } @ManyToOne(() => Wallet, (wallet) => wallet.orders, { createForeignKeyConstraints: true, eager: true, }) - wallet: Wallet; + wallet: Wallet @Column('varchar') - sellToken: string; + sellToken: string @Column('varchar') - buyToken: string; + buyToken: string @Column('varchar') - appData: string; + appData: string @Column('varchar') - receiver: string; + receiver: string @Column('int') - chainId: number; + chainId: number @Column('varchar') - partSellAmount: string; + partSellAmount: string @Column('varchar') - minPartLimit: string; + minPartLimit: string @Column('int') - t0: number; + t0: number @Column('int') - n: number; + n: number @Column('int') - t: number; + t: number @Column('int') - span: number; + span: number } diff --git a/apps/twap/src/app/data/safeTx.ts b/apps/twap/src/app/data/safeTx.ts index ebf2e666..2cfa731b 100644 --- a/apps/twap/src/app/data/safeTx.ts +++ b/apps/twap/src/app/data/safeTx.ts @@ -1,10 +1,10 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; +import { Column, Entity, PrimaryColumn } from 'typeorm' @Entity({ name: 'safe_tx' }) export class SafeTx { @PrimaryColumn('varchar') - safeTxHash: string; + safeTxHash: string @Column('int') - nonce: number; + nonce: number } diff --git a/apps/twap/src/app/data/wallet.ts b/apps/twap/src/app/data/wallet.ts index b216a9e3..422ee452 100644 --- a/apps/twap/src/app/data/wallet.ts +++ b/apps/twap/src/app/data/wallet.ts @@ -1,13 +1,13 @@ -import { Entity, OneToMany, PrimaryColumn } from 'typeorm'; -import { Order } from './order'; +import { Entity, OneToMany, PrimaryColumn } from 'typeorm' +import { Order } from './order' @Entity({ name: 'wallet' }) export class Wallet { @PrimaryColumn('varchar') - address: string; + address: string @OneToMany(() => Order, (order) => order.wallet, { createForeignKeyConstraints: true, }) - orders: Order[]; + orders: Order[] } diff --git a/apps/twap/src/app/orderbook/order.ts b/apps/twap/src/app/orderbook/order.ts index 6fb6cf7f..b5368d47 100644 --- a/apps/twap/src/app/orderbook/order.ts +++ b/apps/twap/src/app/orderbook/order.ts @@ -1,10 +1,5 @@ -import { Column, PrimaryColumn, Entity } from 'typeorm'; -import { - bigIntToString, - bufferToString, - stringToBigInt, - stringToBuffer, -} from '@cowprotocol/shared'; +import { Column, PrimaryColumn, Entity } from 'typeorm' +import { bigIntToString, bufferToString, stringToBigInt, stringToBuffer } from '@cowprotocol/shared' @Entity({ name: 'orders' }) export class Order { @@ -12,115 +7,115 @@ export class Order { name: 'uid', transformer: { from: bufferToString, to: stringToBuffer }, }) - uid: string; + uid: string @Column('bytea', { name: 'owner', transformer: { from: bufferToString, to: stringToBuffer }, }) - owner: string; + owner: string @Column('timestamp with time zone', { name: 'creation_timestamp' }) - creationTimestamp: Date; + creationTimestamp: Date @Column('bytea', { name: 'sell_token', transformer: { from: bufferToString, to: stringToBuffer }, }) - sellToken: string; + sellToken: string @Column('bytea', { name: 'buy_token', transformer: { from: bufferToString, to: stringToBuffer }, }) - buyToken: string; + buyToken: string @Column('numeric', { name: 'sell_amount' }) - sellAmount: number; + sellAmount: number @Column('numeric', { name: 'buy_amount' }) - buyAmount: number; + buyAmount: number @Column('bigint', { name: 'valid_to', transformer: { from: stringToBigInt, to: bigIntToString }, }) - validTo: bigint; + validTo: bigint @Column('numeric', { name: 'fee_amount' }) - feeAmount: number; + feeAmount: number @Column('enum', { name: 'kind', enumName: 'orderkind', enum: ['sell', 'buy'], }) - kind: 'sell' | 'buy'; + kind: 'sell' | 'buy' @Column('boolean', { name: 'partially_fillable' }) - partiallyFillable: boolean; + partiallyFillable: boolean @Column('bytea', { name: 'signature', transformer: { from: bufferToString, to: stringToBuffer }, }) - signature: string; + signature: string @Column('timestamp with time zone', { name: 'cancellation_timestamp' }) - cancellationTimestamp: Date; + cancellationTimestamp: Date @Column('bytea', { name: 'receiver', transformer: { from: bufferToString, to: stringToBuffer }, }) - receiver: string; + receiver: string @Column('bytea', { name: 'app_data', transformer: { from: bufferToString, to: stringToBuffer }, }) - appData: string; + appData: string @Column('enum', { name: 'signing_scheme', enumName: 'signingscheme', enum: ['presign', 'eip712', 'eip1271', 'ethsign'], }) - signingScheme: 'presign' | 'eip712' | 'eip1271' | 'ethsign'; + signingScheme: 'presign' | 'eip712' | 'eip1271' | 'ethsign' @Column('bytea', { name: 'settlement_contract', transformer: { from: bufferToString, to: stringToBuffer }, }) - settlementContract: string; + settlementContract: string @Column('enum', { name: 'sell_token_balance', enumName: 'selltokensource', enum: ['erc20', 'internal', 'external'], }) - sellTokenBalance: 'erc20' | 'internal' | 'external'; + sellTokenBalance: 'erc20' | 'internal' | 'external' @Column('enum', { name: 'buy_token_balance', enumName: 'buytokendestination', enum: ['erc20', 'internal'], }) - buyTokenBalance: 'erc20' | 'internal'; + buyTokenBalance: 'erc20' | 'internal' @Column('numeric', { name: 'full_fee_amount' }) - fullFeeAmount: number; + fullFeeAmount: number @Column('enum', { name: 'class', enumName: 'orderclass', enum: ['market', 'liquidity', 'limit'], }) - class: 'market' | 'liquidity' | 'limit'; + class: 'market' | 'liquidity' | 'limit' @Column('numeric', { name: 'surplus_fee' }) - surplusFee: number; + surplusFee: number @Column('timestamp with time zone', { name: 'surplus_fee_timestamp' }) - surplusFeeTimestamp: Date; + surplusFeeTimestamp: Date } diff --git a/apps/twap/src/app/orderbook/settlement.ts b/apps/twap/src/app/orderbook/settlement.ts index f2d117ff..6f7c6f45 100644 --- a/apps/twap/src/app/orderbook/settlement.ts +++ b/apps/twap/src/app/orderbook/settlement.ts @@ -1,10 +1,5 @@ -import { Column, PrimaryColumn, Entity } from 'typeorm'; -import { - bigIntToString, - bufferToString, - stringToBigInt, - stringToBuffer, -} from '@cowprotocol/shared'; +import { Column, PrimaryColumn, Entity } from 'typeorm' +import { bigIntToString, bufferToString, stringToBigInt, stringToBuffer } from '@cowprotocol/shared' @Entity({ name: 'settlements' }) export class Settlement { @@ -12,34 +7,34 @@ export class Settlement { name: 'block_number', transformer: { from: stringToBigInt, to: bigIntToString }, }) - blockNumber: bigint; + blockNumber: bigint @PrimaryColumn('bigint', { name: 'log_index', transformer: { from: stringToBigInt, to: bigIntToString }, }) - logIndex: bigint; + logIndex: bigint @Column('bytea', { transformer: { from: bufferToString, to: stringToBuffer }, }) - solver: string; + solver: string @Column('bytea', { name: 'tx_hash', transformer: { from: bufferToString, to: stringToBuffer }, }) - txHash: string; + txHash: string @Column('bytea', { name: 'tx_from', transformer: { from: bufferToString, to: stringToBuffer }, }) - txFrom: string; + txFrom: string @Column('bigint', { name: 'tx_nonce', transformer: { from: stringToBigInt, to: bigIntToString }, }) - txNonce: bigint; + txNonce: bigint } diff --git a/apps/twap/src/app/orderbook/trade.ts b/apps/twap/src/app/orderbook/trade.ts index 849b8c30..2d0f970f 100644 --- a/apps/twap/src/app/orderbook/trade.ts +++ b/apps/twap/src/app/orderbook/trade.ts @@ -1,10 +1,5 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; -import { - bigIntToString, - bufferToString, - stringToBigInt, - stringToBuffer, -} from '@cowprotocol/shared'; +import { Column, Entity, PrimaryColumn } from 'typeorm' +import { bigIntToString, bufferToString, stringToBigInt, stringToBuffer } from '@cowprotocol/shared' @Entity({ name: 'trades' }) export class Trade { @@ -12,26 +7,26 @@ export class Trade { name: 'block_number', transformer: { from: stringToBigInt, to: bigIntToString }, }) - blockNumber: bigint; + blockNumber: bigint @PrimaryColumn('bigint', { name: 'log_index', transformer: { from: stringToBigInt, to: bigIntToString }, }) - logIndex: bigint; + logIndex: bigint @Column('bytea', { name: 'order_uid', transformer: { from: bufferToString, to: stringToBuffer }, }) - orderUid: string; + orderUid: string @Column('numeric', { name: 'sell_amount' }) - sellAmount: number; + sellAmount: number @Column('numeric', { name: 'buy_amount' }) - buyAmount: number; + buyAmount: number @Column('numeric', { name: 'fee_amount' }) - feeAmount: number; + feeAmount: number } diff --git a/apps/twap/src/app/plugins/env.ts b/apps/twap/src/app/plugins/env.ts index 113ff1ec..4be11d97 100644 --- a/apps/twap/src/app/plugins/env.ts +++ b/apps/twap/src/app/plugins/env.ts @@ -1,5 +1,5 @@ -import fp from 'fastify-plugin'; -import fastifyEnv from '@fastify/env'; +import fp from 'fastify-plugin' +import fastifyEnv from '@fastify/env' const schema = { type: 'object', @@ -43,31 +43,31 @@ const schema = { type: 'string', }, }, -}; +} export default fp(async (fastify, opts) => { const options = { ...opts, schema, - }; + } - fastify.register(fastifyEnv, options); -}); + fastify.register(fastifyEnv, options) +}) declare module 'fastify' { interface FastifyInstance { config: { - DATABASE_NAME: string; - DATABASE_USERNAME: string; - DATABASE_PASSWORD: string; - DATABASE_HOST: string; - DATABASE_PORT: number; - ORDERBOOK_DATABASE_HOST: string; - ORDERBOOK_DATABASE_PORT: number; - ORDERBOOK_DATABASE_USERNAME: string; - ORDERBOOK_DATABASE_PASSWORD: string; - }; + DATABASE_NAME: string + DATABASE_USERNAME: string + DATABASE_PASSWORD: string + DATABASE_HOST: string + DATABASE_PORT: number + ORDERBOOK_DATABASE_HOST: string + ORDERBOOK_DATABASE_PORT: number + ORDERBOOK_DATABASE_USERNAME: string + ORDERBOOK_DATABASE_PASSWORD: string + } } } -module.exports.autoload = false; +module.exports.autoload = false diff --git a/apps/twap/src/app/plugins/orderbook.ts b/apps/twap/src/app/plugins/orderbook.ts index caff1966..b909f42a 100644 --- a/apps/twap/src/app/plugins/orderbook.ts +++ b/apps/twap/src/app/plugins/orderbook.ts @@ -1,24 +1,21 @@ -import { FastifyInstance } from 'fastify'; -import fp from 'fastify-plugin'; +import { FastifyInstance } from 'fastify' +import fp from 'fastify-plugin' -import { Settlement } from '../orderbook/settlement'; -import { DataSource, Repository } from 'typeorm'; -import { Trade } from '../orderbook/trade'; -import { Order } from '../orderbook/order'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { getApiBaseUrl } from '../utils/getApiBaseUrl'; -import { ExecutionInfo } from '../types/order'; +import { Settlement } from '../orderbook/settlement' +import { DataSource, Repository } from 'typeorm' +import { Trade } from '../orderbook/trade' +import { Order } from '../orderbook/order' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { getApiBaseUrl } from '../utils/getApiBaseUrl' +import { ExecutionInfo } from '../types/order' const DEFAULT_TWAP_EXECUTION_INFO = { executedSellAmount: BigInt(0), executedBuyAmount: BigInt(0), -}; +} function getExecutionInfoFactory(dataSource: DataSource, apiBaseUrl: string) { - return async function getExecutionInfo( - orderId: string, - partIds: string[] - ): Promise { + return async function getExecutionInfo(orderId: string, partIds: string[]): Promise { // TODO: We need to create a query here to derive the execution info from the // read only db. // However, because staging db does not have any of the data we need, we can't rely on this. @@ -35,37 +32,32 @@ function getExecutionInfoFactory(dataSource: DataSource, apiBaseUrl: string) { })) .catch(() => DEFAULT_TWAP_EXECUTION_INFO) ) - ); + ) return orders.reduce( (accumulator, order) => { return { - executedBuyAmount: - accumulator.executedBuyAmount + BigInt(order.executedBuyAmount), - executedSellAmount: - accumulator.executedSellAmount + BigInt(order.executedSellAmount), - }; + executedBuyAmount: accumulator.executedBuyAmount + BigInt(order.executedBuyAmount), + executedSellAmount: accumulator.executedSellAmount + BigInt(order.executedSellAmount), + } }, { ...DEFAULT_TWAP_EXECUTION_INFO, } - ); + ) } catch (err) { - return DEFAULT_TWAP_EXECUTION_INFO; + return DEFAULT_TWAP_EXECUTION_INFO } - }; + } } -function orderbookFactory( - dataSource: DataSource, - apiBaseUrl: string -): Orderbook { +function orderbookFactory(dataSource: DataSource, apiBaseUrl: string): Orderbook { return { settlement: dataSource.getRepository(Settlement), trade: dataSource.getRepository(Trade), order: dataSource.getRepository(Order), getExecutionInfo: getExecutionInfoFactory(dataSource, apiBaseUrl), - }; + } } export default fp(async function (fastify: FastifyInstance) { @@ -74,10 +66,7 @@ export default fp(async function (fastify: FastifyInstance) { // fastify.orm['gnosis'], // getApiBaseUrl(SupportedChainId.GNOSIS_CHAIN) // ); - const mainnet = orderbookFactory( - fastify.orm['mainnet'], - getApiBaseUrl(SupportedChainId.MAINNET) - ); + const mainnet = orderbookFactory(fastify.orm['mainnet'], getApiBaseUrl(SupportedChainId.MAINNET)) // const sepolia = orderbookFactory( // fastify.orm['sepolia'], // getApiBaseUrl(SupportedChainId.SEPOLIA) @@ -89,28 +78,28 @@ export default fp(async function (fastify: FastifyInstance) { [SupportedChainId.MAINNET]: mainnet, // [SupportedChainId.SEPOLIA]: sepolia, // [SupportedChainId.GNOSIS_CHAIN]: gnosis, - }); - }); -}); + }) + }) +}) interface Orderbook { - settlement: Repository; - trade: Repository; - order: Repository; - getExecutionInfo: ReturnType; + settlement: Repository + trade: Repository + order: Repository + getExecutionInfo: ReturnType } declare module 'fastify' { interface FastifyInstance { orderbook: { - gnosis: Orderbook; - mainnet: Orderbook; - sepolia: Orderbook; - [SupportedChainId.SEPOLIA]: Orderbook; - [SupportedChainId.GNOSIS_CHAIN]: Orderbook; - [SupportedChainId.MAINNET]: Orderbook; - [SupportedChainId.POLYGON]: Orderbook; - [SupportedChainId.AVALANCHE]: Orderbook; - }; + gnosis: Orderbook + mainnet: Orderbook + sepolia: Orderbook + [SupportedChainId.SEPOLIA]: Orderbook + [SupportedChainId.GNOSIS_CHAIN]: Orderbook + [SupportedChainId.MAINNET]: Orderbook + [SupportedChainId.POLYGON]: Orderbook + [SupportedChainId.AVALANCHE]: Orderbook + } } } diff --git a/apps/twap/src/app/plugins/orm.ts b/apps/twap/src/app/plugins/orm.ts index 486f5408..fbaa38be 100644 --- a/apps/twap/src/app/plugins/orm.ts +++ b/apps/twap/src/app/plugins/orm.ts @@ -1,13 +1,13 @@ -import 'reflect-metadata'; -import { FastifyInstance } from 'fastify'; -import typeORMPlugin from 'typeorm-fastify-plugin'; -import fp from 'fastify-plugin'; -import { Order } from '../data/order'; -import { Wallet } from '../data/wallet'; -import { SafeTx } from '../data/safeTx'; -import { Order as OrderbookOrder } from '../orderbook/order'; -import { Settlement } from '../orderbook/settlement'; -import { Trade } from '../orderbook/trade'; +import 'reflect-metadata' +import { FastifyInstance } from 'fastify' +import typeORMPlugin from 'typeorm-fastify-plugin' +import fp from 'fastify-plugin' +import { Order } from '../data/order' +import { Wallet } from '../data/wallet' +import { SafeTx } from '../data/safeTx' +import { Order as OrderbookOrder } from '../orderbook/order' +import { Settlement } from '../orderbook/settlement' +import { Trade } from '../orderbook/trade' export default fp(async function (fastify: FastifyInstance) { fastify.register(typeORMPlugin, { @@ -19,7 +19,7 @@ export default fp(async function (fastify: FastifyInstance) { password: fastify.config.DATABASE_PASSWORD, entities: [Wallet, Order, SafeTx], migrations: ['twap/apps/twap/src/migrations/*.js'], - }); + }) fastify.register(typeORMPlugin, { namespace: 'goerli', @@ -30,7 +30,7 @@ export default fp(async function (fastify: FastifyInstance) { username: fastify.config.ORDERBOOK_DATABASE_USERNAME, password: fastify.config.ORDERBOOK_DATABASE_PASSWORD, entities: [Settlement, Trade, OrderbookOrder], - }); + }) fastify.register(typeORMPlugin, { namespace: 'mainnet', @@ -41,13 +41,13 @@ export default fp(async function (fastify: FastifyInstance) { username: fastify.config.ORDERBOOK_DATABASE_USERNAME, password: fastify.config.ORDERBOOK_DATABASE_PASSWORD, entities: [Settlement, Trade, OrderbookOrder], - }); + }) fastify.ready((err) => { if (err) { - throw err; + throw err } - fastify.orm.runMigrations({ transaction: 'all' }); - }); -}); + fastify.orm.runMigrations({ transaction: 'all' }) + }) +}) diff --git a/apps/twap/src/app/plugins/sensible.ts b/apps/twap/src/app/plugins/sensible.ts index 3e78bcd4..7388f2a8 100644 --- a/apps/twap/src/app/plugins/sensible.ts +++ b/apps/twap/src/app/plugins/sensible.ts @@ -1,6 +1,6 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import fp from 'fastify-plugin'; -import sensible from '@fastify/sensible'; +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify' +import fp from 'fastify-plugin' +import sensible from '@fastify/sensible' /** * This plugins adds some utilities to handle http errors @@ -8,5 +8,5 @@ import sensible from '@fastify/sensible'; * @see https://github.com/fastify/fastify-sensible */ export default fp(async function (fastify: FastifyInstance) { - fastify.register(sensible); -}); + fastify.register(sensible) +}) diff --git a/apps/twap/src/app/plugins/swagger.ts b/apps/twap/src/app/plugins/swagger.ts index 46e43570..ea4b4f10 100644 --- a/apps/twap/src/app/plugins/swagger.ts +++ b/apps/twap/src/app/plugins/swagger.ts @@ -1,17 +1,17 @@ -import { FastifyInstance } from 'fastify'; -import fp from 'fastify-plugin'; -import fastifySwagger from '@fastify/swagger'; -import fastifySwaggerUi from '@fastify/swagger-ui'; +import { FastifyInstance } from 'fastify' +import fp from 'fastify-plugin' +import fastifySwagger from '@fastify/swagger' +import fastifySwaggerUi from '@fastify/swagger-ui' export default fp(async function (fastify: FastifyInstance) { fastify.register(fastifySwagger, { swagger: {}, - }); + }) fastify.register(fastifySwaggerUi, { routePrefix: '/docs', - }); + }) fastify.ready((err) => { - if (err) throw err; - fastify.swagger(); - }); -}); + if (err) throw err + fastify.swagger() + }) +}) diff --git a/apps/twap/src/app/routes/_chainId/_walletAddress/orders/index.ts b/apps/twap/src/app/routes/_chainId/_walletAddress/orders/index.ts index ac13d155..955b102f 100644 --- a/apps/twap/src/app/routes/_chainId/_walletAddress/orders/index.ts +++ b/apps/twap/src/app/routes/_chainId/_walletAddress/orders/index.ts @@ -1,9 +1,9 @@ -import { FastifyInstance } from 'fastify'; -import { Wallet } from '../../../../data/wallet'; -import { Order } from '../../../../data/order'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import { FastifyInstance } from 'fastify' +import { Wallet } from '../../../../data/wallet' +import { Order } from '../../../../data/order' +import { FromSchema, JSONSchema } from 'json-schema-to-ts' -const ADDRESS_LENGTH = 42; +const ADDRESS_LENGTH = 42 const routeSchema = { type: 'object', @@ -19,8 +19,8 @@ const routeSchema = { maxLength: ADDRESS_LENGTH, }, }, -} as const satisfies JSONSchema; -type RouteSchema = FromSchema; +} as const satisfies JSONSchema +type RouteSchema = FromSchema const postOrderBodySchema = { type: 'object', @@ -85,15 +85,15 @@ const postOrderBodySchema = { type: 'string', }, }, -} as const satisfies JSONSchema; -type PostOrderBodySchema = FromSchema; -type PostOrderParamsSchema = RouteSchema; +} as const satisfies JSONSchema +type PostOrderBodySchema = FromSchema +type PostOrderParamsSchema = RouteSchema -type GetOrdersParamsSchema = RouteSchema; +type GetOrdersParamsSchema = RouteSchema export default async function (fastify: FastifyInstance) { fastify.get<{ - Params: GetOrdersParamsSchema; + Params: GetOrdersParamsSchema }>( '/', { @@ -102,8 +102,8 @@ export default async function (fastify: FastifyInstance) { }, }, async function (request, reply) { - const { chainId, walletAddress } = request.params; - const orderRepository = fastify.orm.getRepository(Order); + const { chainId, walletAddress } = request.params + const orderRepository = fastify.orm.getRepository(Order) const orders = await orderRepository.find({ where: { @@ -115,11 +115,11 @@ export default async function (fastify: FastifyInstance) { relations: { wallet: true, }, - }); + }) - reply.status(200).send(orders); + reply.status(200).send(orders) } - ); + ) fastify.post<{ Body: PostOrderBodySchema; Params: PostOrderParamsSchema }>( '/', @@ -130,33 +130,33 @@ export default async function (fastify: FastifyInstance) { }, }, async function (request, reply) { - const { chainId, walletAddress } = request.params; - const walletRepository = fastify.orm.getRepository(Wallet); - const orderRepository = fastify.orm.getRepository(Order); + const { chainId, walletAddress } = request.params + const walletRepository = fastify.orm.getRepository(Wallet) + const orderRepository = fastify.orm.getRepository(Order) let wallet = await walletRepository.findOne({ where: { address: walletAddress, }, - }); + }) if (wallet === null) { wallet = walletRepository.create({ address: walletAddress, - }); + }) - await walletRepository.save(wallet); + await walletRepository.save(wallet) } const order = orderRepository.create({ ...request.body.order, wallet, chainId: Number(chainId), - }); + }) - await orderRepository.save(order); + await orderRepository.save(order) - reply.status(200).send(order); + reply.status(200).send(order) } - ); + ) } diff --git a/apps/twap/src/app/routes/root.ts b/apps/twap/src/app/routes/root.ts index 59cec2fa..aaac3496 100644 --- a/apps/twap/src/app/routes/root.ts +++ b/apps/twap/src/app/routes/root.ts @@ -1,10 +1,7 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify' export default async function (fastify: FastifyInstance) { - fastify.get( - '/health-check', - async function (request: FastifyRequest, reply: FastifyReply) { - reply.status(200).send({ status: 'ok' }); - } - ); + fastify.get('/health-check', async function (request: FastifyRequest, reply: FastifyReply) { + reply.status(200).send({ status: 'ok' }) + }) } diff --git a/apps/twap/src/app/types/order.ts b/apps/twap/src/app/types/order.ts index 1d8b2414..f0999192 100644 --- a/apps/twap/src/app/types/order.ts +++ b/apps/twap/src/app/types/order.ts @@ -1,4 +1,4 @@ -import { OrderParameters, SupportedChainId } from '@cowprotocol/cow-sdk'; +import { OrderParameters, SupportedChainId } from '@cowprotocol/cow-sdk' export enum OrderStatus { WaitSigning = 'WaitSigning', @@ -11,35 +11,35 @@ export enum OrderStatus { } export interface OrderStruct { - sellToken: string; - buyToken: string; - receiver: string; - partSellAmount: string; - minPartLimit: string; + sellToken: string + buyToken: string + receiver: string + partSellAmount: string + minPartLimit: string // timeStart - t0: number; + t0: number // numOfParts - n: number; + n: number // timeInterval - t: number; - span: number; - appData: string; + t: number + span: number + appData: string } export interface OrderPart { - uid: string; - index: number; - chainId: SupportedChainId; - safeAddress: string; - orderId: string; - order: OrderParameters; + uid: string + index: number + chainId: SupportedChainId + safeAddress: string + orderId: string + order: OrderParameters } export interface ExecutionInfo { - executedSellAmount: bigint; - executedBuyAmount: bigint; + executedSellAmount: bigint + executedBuyAmount: bigint } export function returnNever(): never { - throw new Error('This should never happen.'); + throw new Error('This should never happen.') } diff --git a/apps/twap/src/app/utils/getApiBaseUrl.ts b/apps/twap/src/app/utils/getApiBaseUrl.ts index b517075c..3b3775c2 100644 --- a/apps/twap/src/app/utils/getApiBaseUrl.ts +++ b/apps/twap/src/app/utils/getApiBaseUrl.ts @@ -1,10 +1,10 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { COW_API_NETWORK_NAMES } from '@cowprotocol/shared'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { COW_API_NETWORK_NAMES } from '@cowprotocol/shared' -const COW_API_BASE_URL = 'https://api.cow.fi'; +const COW_API_BASE_URL = 'https://api.cow.fi' export function getApiBaseUrl(chainId: SupportedChainId): string { - const chainName = COW_API_NETWORK_NAMES[chainId]; + const chainName = COW_API_NETWORK_NAMES[chainId] - return `${COW_API_BASE_URL}/${chainName}/v1`; + return `${COW_API_BASE_URL}/${chainName}/v1` } diff --git a/apps/twap/src/app/utils/getConditionalOrderId.ts b/apps/twap/src/app/utils/getConditionalOrderId.ts index 360604f1..38e9c233 100644 --- a/apps/twap/src/app/utils/getConditionalOrderId.ts +++ b/apps/twap/src/app/utils/getConditionalOrderId.ts @@ -1,66 +1,60 @@ -import { - mapAddressToSupportedNetworks, - SupportedChainId, -} from '@cowprotocol/cow-sdk'; -import { defaultAbiCoder } from '@ethersproject/abi'; -import { keccak256 } from '@ethersproject/keccak256'; -import { CurrencyAmount, Token } from '@uniswap/sdk-core'; +import { mapAddressToSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' +import { defaultAbiCoder } from '@ethersproject/abi' +import { keccak256 } from '@ethersproject/keccak256' +import { CurrencyAmount, Token } from '@uniswap/sdk-core' interface ConditionalOrderParams { - staticInput: string; - salt: string; - handler: string; + staticInput: string + salt: string + handler: string } export interface TWAPOrder { - sellAmount: CurrencyAmount; - buyAmount: CurrencyAmount; - receiver: string; - numOfParts: number; - startTime: number; - timeInterval: number; - span: number; - appData: string; + sellAmount: CurrencyAmount + buyAmount: CurrencyAmount + receiver: string + numOfParts: number + startTime: number + timeInterval: number + span: number + appData: string } export interface TWAPOrderStruct { - sellToken: string; - buyToken: string; - receiver: string; - partSellAmount: string; - minPartLimit: string; + sellToken: string + buyToken: string + receiver: string + partSellAmount: string + minPartLimit: string // timeStart - t0: number; + t0: number // numOfParts - n: number; + n: number // timeInterval - t: number; - span: number; - appData: string; + t: number + span: number + appData: string } export const TWAP_ORDER_STRUCT = - 'tuple(address sellToken,address buyToken,address receiver,uint256 partSellAmount,uint256 minPartLimit,uint256 t0,uint256 n,uint256 t,uint256 span,bytes32 appData)'; + 'tuple(address sellToken,address buyToken,address receiver,uint256 partSellAmount,uint256 minPartLimit,uint256 t0,uint256 n,uint256 t,uint256 span,bytes32 appData)' -const twapHandlerAddress = '0x910d00a310f7Dc5B29FE73458F47f519be547D3d'; -export const TWAP_HANDLER_ADDRESS: Record = - mapAddressToSupportedNetworks(twapHandlerAddress); +const twapHandlerAddress = '0x910d00a310f7Dc5B29FE73458F47f519be547D3d' +export const TWAP_HANDLER_ADDRESS: Record = mapAddressToSupportedNetworks(twapHandlerAddress) export function twapOrderToStruct(order: TWAPOrder): TWAPOrderStruct { return { sellToken: order.sellAmount.currency.address, buyToken: order.buyAmount.currency.address, receiver: order.receiver, - partSellAmount: order.sellAmount - .divide(order.numOfParts) - .quotient.toString(), + partSellAmount: order.sellAmount.divide(order.numOfParts).quotient.toString(), minPartLimit: order.buyAmount.divide(order.numOfParts).quotient.toString(), t0: order.startTime, n: order.numOfParts, t: order.timeInterval, span: order.span, appData: order.appData, - }; + } } export function buildTwapOrderParamsStruct( @@ -71,14 +65,11 @@ export function buildTwapOrderParamsStruct( handler: TWAP_HANDLER_ADDRESS[chainId], salt: '0x00000000000000000000000000000000000000000000000000000018920d8ce7', // hexZeroPad(Buffer.from(Date.now().toString(16), 'hex'), 32), staticInput: defaultAbiCoder.encode([TWAP_ORDER_STRUCT], [twapOrderData]), - }; + } } -const CONDITIONAL_ORDER_PARAMS_STRUCT = - 'tuple(address handler, bytes32 salt, bytes staticInput)'; +const CONDITIONAL_ORDER_PARAMS_STRUCT = 'tuple(address handler, bytes32 salt, bytes staticInput)' export function getConditionalOrderId(params: ConditionalOrderParams): string { - return keccak256( - defaultAbiCoder.encode([CONDITIONAL_ORDER_PARAMS_STRUCT], [params]) - ); + return keccak256(defaultAbiCoder.encode([CONDITIONAL_ORDER_PARAMS_STRUCT], [params])) } diff --git a/apps/twap/src/main.ts b/apps/twap/src/main.ts index ee844e47..b3a32a0f 100644 --- a/apps/twap/src/main.ts +++ b/apps/twap/src/main.ts @@ -1,24 +1,24 @@ -import Fastify from 'fastify'; -import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; -import { app } from './app/app'; +import Fastify from 'fastify' +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts' +import { app } from './app/app' -const host = process.env.HOST ?? 'localhost'; -const port = process.env.PORT ? Number(process.env.PORT) : 3001; +const host = process.env.HOST ?? 'localhost' +const port = process.env.PORT ? Number(process.env.PORT) : 3001 // Instantiate Fastify with some config const server = Fastify({ logger: true, -}).withTypeProvider(); +}).withTypeProvider() // Register your application as a normal plugin. -server.register(app); +server.register(app) // Start listening. server.listen({ port, host }, (err) => { if (err) { - server.log.error(err); - process.exit(1); + server.log.error(err) + process.exit(1) } else { - console.log(`[ ready ] http://${host}:${port}`); + console.log(`[ ready ] http://${host}:${port}`) } -}); +}) diff --git a/apps/twap/src/migrations/1688063749511-initial-migration.ts b/apps/twap/src/migrations/1688063749511-initial-migration.ts index f20166d6..3a884e4a 100644 --- a/apps/twap/src/migrations/1688063749511-initial-migration.ts +++ b/apps/twap/src/migrations/1688063749511-initial-migration.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { MigrationInterface, QueryRunner } from 'typeorm' export class InitialMigration1688063749511 implements MigrationInterface { // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/apps/twap/src/migrations/1688063878937-migration.ts b/apps/twap/src/migrations/1688063878937-migration.ts index 3183079f..454e1bcc 100644 --- a/apps/twap/src/migrations/1688063878937-migration.ts +++ b/apps/twap/src/migrations/1688063878937-migration.ts @@ -1,7 +1,7 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { MigrationInterface, QueryRunner } from 'typeorm' export class Migration1688063878937 implements MigrationInterface { - name = 'Migration1688063878937'; + name = 'Migration1688063878937' public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` @@ -10,7 +10,7 @@ export class Migration1688063878937 implements MigrationInterface { "ordersId" character varying, CONSTRAINT "PK_1dcc9f5fd49e3dc52c6d2393c53" PRIMARY KEY ("address") ) - `); + `) await queryRunner.query(` CREATE TABLE "order" ( "id" character varying NOT NULL, @@ -26,22 +26,22 @@ export class Migration1688063878937 implements MigrationInterface { "submissionDate" date NOT NULL, CONSTRAINT "PK_1031171c13130102495201e3e20" PRIMARY KEY ("id") ) - `); + `) await queryRunner.query(` ALTER TABLE "wallet" ADD CONSTRAINT "FK_43396129d4329e711c4b92e8a99" FOREIGN KEY ("ordersId") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION - `); + `) } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "wallet" DROP CONSTRAINT "FK_43396129d4329e711c4b92e8a99" - `); + `) await queryRunner.query(` DROP TABLE "order" - `); + `) await queryRunner.query(` DROP TABLE "wallet" - `); + `) } } diff --git a/apps/twap/src/migrations/1688066336403-migration.ts b/apps/twap/src/migrations/1688066336403-migration.ts index 45d31c8c..fa48b73a 100644 --- a/apps/twap/src/migrations/1688066336403-migration.ts +++ b/apps/twap/src/migrations/1688066336403-migration.ts @@ -1,39 +1,39 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { MigrationInterface, QueryRunner } from 'typeorm' export class Migration1688066336403 implements MigrationInterface { - name = 'Migration1688066336403'; + name = 'Migration1688066336403' public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "wallet" DROP CONSTRAINT "FK_43396129d4329e711c4b92e8a99" - `); + `) await queryRunner.query(` ALTER TABLE "wallet" DROP COLUMN "ordersId" - `); + `) await queryRunner.query(` ALTER TABLE "order" ADD "walletAddress" character varying - `); + `) await queryRunner.query(` ALTER TABLE "order" ADD CONSTRAINT "FK_b9f446b7cd2f92b160780f75296" FOREIGN KEY ("walletAddress") REFERENCES "wallet"("address") ON DELETE NO ACTION ON UPDATE NO ACTION - `); + `) } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "order" DROP CONSTRAINT "FK_b9f446b7cd2f92b160780f75296" - `); + `) await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "walletAddress" - `); + `) await queryRunner.query(` ALTER TABLE "wallet" ADD "ordersId" character varying - `); + `) await queryRunner.query(` ALTER TABLE "wallet" ADD CONSTRAINT "FK_43396129d4329e711c4b92e8a99" FOREIGN KEY ("ordersId") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION - `); + `) } } diff --git a/apps/twap/src/migrations/1688472151788-migration.ts b/apps/twap/src/migrations/1688472151788-migration.ts index 434b376d..5c7425e0 100644 --- a/apps/twap/src/migrations/1688472151788-migration.ts +++ b/apps/twap/src/migrations/1688472151788-migration.ts @@ -1,7 +1,7 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { MigrationInterface, QueryRunner } from 'typeorm' export class Migration1688472151788 implements MigrationInterface { - name = 'Migration1688472151788'; + name = 'Migration1688472151788' public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` @@ -9,55 +9,55 @@ export class Migration1688472151788 implements MigrationInterface { "safeTxHash" character varying NOT NULL, CONSTRAINT "PK_cfe8c20950562a40e71887f5a42" PRIMARY KEY ("safeTxHash") ) - `); - await queryRunner.query(`TRUNCATE TABLE "order"`); + `) + await queryRunner.query(`TRUNCATE TABLE "order"`) await queryRunner.query(` ALTER TABLE "order" ADD "partSellAmount" character varying NOT NULL - `); + `) await queryRunner.query(` ALTER TABLE "order" ADD "minPartLimit" character varying NOT NULL - `); + `) await queryRunner.query(` ALTER TABLE "order" ADD "t0" integer NOT NULL - `); + `) await queryRunner.query(` ALTER TABLE "order" ADD "n" integer NOT NULL - `); + `) await queryRunner.query(` ALTER TABLE "order" ADD "t" integer NOT NULL - `); + `) await queryRunner.query(` ALTER TABLE "order" ADD "span" integer NOT NULL - `); + `) } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "span" - `); + `) await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "t" - `); + `) await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "n" - `); + `) await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "t0" - `); + `) await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "minPartLimit" - `); + `) await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "partSellAmount" - `); + `) await queryRunner.query(` DROP TABLE "safe_tx" - `); + `) } } diff --git a/apps/twap/src/migrations/1688472763977-migration.ts b/apps/twap/src/migrations/1688472763977-migration.ts index 7cdba702..56ee7db2 100644 --- a/apps/twap/src/migrations/1688472763977-migration.ts +++ b/apps/twap/src/migrations/1688472763977-migration.ts @@ -1,47 +1,46 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm' export class Migration1688472763977 implements MigrationInterface { - name = 'Migration1688472763977' + name = 'Migration1688472763977' - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "safeTxHash" - `); - await queryRunner.query(` + `) + await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "nonce" - `); - await queryRunner.query(` + `) + await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "confirmations" - `); - await queryRunner.query(` + `) + await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "confirmationsRequired" - `); - await queryRunner.query(` + `) + await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "submissionDate" - `); - } + `) + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` ALTER TABLE "order" ADD "submissionDate" date NOT NULL - `); - await queryRunner.query(` + `) + await queryRunner.query(` ALTER TABLE "order" ADD "confirmationsRequired" integer NOT NULL - `); - await queryRunner.query(` + `) + await queryRunner.query(` ALTER TABLE "order" ADD "confirmations" integer NOT NULL - `); - await queryRunner.query(` + `) + await queryRunner.query(` ALTER TABLE "order" ADD "nonce" integer NOT NULL - `); - await queryRunner.query(` + `) + await queryRunner.query(` ALTER TABLE "order" ADD "safeTxHash" character varying NOT NULL - `); - } - + `) + } } diff --git a/apps/twap/src/migrations/1688554921688-migration.ts b/apps/twap/src/migrations/1688554921688-migration.ts index 464c4de8..c6e4987a 100644 --- a/apps/twap/src/migrations/1688554921688-migration.ts +++ b/apps/twap/src/migrations/1688554921688-migration.ts @@ -1,19 +1,18 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm' export class Migration1688554921688 implements MigrationInterface { - name = 'Migration1688554921688' + name = 'Migration1688554921688' - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` ALTER TABLE "safe_tx" ADD "nonce" integer NOT NULL - `); - } + `) + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` ALTER TABLE "safe_tx" DROP COLUMN "nonce" - `); - } - + `) + } } diff --git a/apps/twap/src/migrations/1688633087604-migration.ts b/apps/twap/src/migrations/1688633087604-migration.ts index f3897a87..2ca2edd8 100644 --- a/apps/twap/src/migrations/1688633087604-migration.ts +++ b/apps/twap/src/migrations/1688633087604-migration.ts @@ -1,31 +1,31 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { MigrationInterface, QueryRunner } from 'typeorm' export class Migration1688633087604 implements MigrationInterface { - name = 'Migration1688633087604'; + name = 'Migration1688633087604' public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "chainId" - `); + `) await queryRunner.query(` TRUNCATE TABLE "order" - `); + `) await queryRunner.query(` ALTER TABLE "order" ADD "chainId" integer NOT NULL - `); + `) } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "order" DROP COLUMN "chainId" - `); + `) await queryRunner.query(` ALTER TABLE "order" ADD "chainId" character varying NOT NULL - `); + `) await queryRunner.query(` ALTER TABLE "safe_tx" DROP COLUMN "nonce" - `); + `) } } diff --git a/libs/abis/src/index.ts b/libs/abis/src/index.ts index a3d856a4..69f18989 100644 --- a/libs/abis/src/index.ts +++ b/libs/abis/src/index.ts @@ -1,3 +1,3 @@ // Custom -export * from './generated/custom'; -export type { GPv2Order } from './generated/custom/ComposableCoW'; +export * from './generated/custom' +export type { GPv2Order } from './generated/custom/ComposableCoW' diff --git a/libs/abis/vite.config.ts b/libs/abis/vite.config.ts index 3e4dca37..a62e2ac0 100644 --- a/libs/abis/vite.config.ts +++ b/libs/abis/vite.config.ts @@ -1,8 +1,8 @@ /// -import { joinPathFragments } from '@nx/devkit'; -import { defineConfig } from 'vite'; -import dts from 'vite-plugin-dts'; -import viteTsConfigPaths from 'vite-tsconfig-paths'; +import { joinPathFragments } from '@nx/devkit' +import { defineConfig } from 'vite' +import dts from 'vite-plugin-dts' +import viteTsConfigPaths from 'vite-tsconfig-paths' export default defineConfig({ cacheDir: '../../node_modules/.vite/abis', @@ -44,4 +44,4 @@ export default defineConfig({ external: [], }, }, -}); +}) diff --git a/libs/notifications/src/index.ts b/libs/notifications/src/index.ts index e82280d7..806e1f37 100644 --- a/libs/notifications/src/index.ts +++ b/libs/notifications/src/index.ts @@ -1,67 +1,57 @@ -import { Channel, Connection } from 'amqplib'; +import { Channel, Connection } from 'amqplib' export interface PushNotification { - id: string; - account: string; - title: string; - message: string; - url?: string; - context?: Record; + id: string + account: string + title: string + message: string + url?: string + context?: Record } export interface ConnectToQueueParams { - channel?: string; + channel?: string } export interface ConnectToChannelResponse { - connection: Connection; - channel: Channel; + connection: Connection + channel: Channel } export interface SendToQueueParams { - channel: Channel; - queue: string; - notifications: PushNotification[]; + channel: Channel + queue: string + notifications: PushNotification[] } -export function parseNotifications( - notificationsString: string -): PushNotification[] { - const notifications = JSON.parse(notificationsString); +export function parseNotifications(notificationsString: string): PushNotification[] { + const notifications = JSON.parse(notificationsString) if (!isNotificationArray(notifications)) { - throw new Error( - `The parsed message is not a valid notification array. Message: ${notificationsString}` - ); + throw new Error(`The parsed message is not a valid notification array. Message: ${notificationsString}`) } - return notifications; + return notifications } -export function stringifyNotifications( - notifications: PushNotification[] -): string { - return JSON.stringify(notifications); +export function stringifyNotifications(notifications: PushNotification[]): string { + return JSON.stringify(notifications) } -export function isNotificationArray( - notifications: unknown -): notifications is PushNotification[] { - return Array.isArray(notifications) && notifications.every(isNotification); +export function isNotificationArray(notifications: unknown): notifications is PushNotification[] { + return Array.isArray(notifications) && notifications.every(isNotification) } -export function isNotification( - notification: unknown -): notification is PushNotification { +export function isNotification(notification: unknown): notification is PushNotification { if (typeof notification !== 'object' || notification === null) { - return false; + return false } - const record = notification as Record; + const record = notification as Record return ( typeof record.id === 'string' && typeof record.account === 'string' && typeof record.title === 'string' && typeof record.message === 'string' - ); + ) } diff --git a/libs/repositories/src/const.ts b/libs/repositories/src/const.ts index 803e7b6a..31e11d27 100644 --- a/libs/repositories/src/const.ts +++ b/libs/repositories/src/const.ts @@ -1,9 +1,9 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import BigNumber from 'bignumber.js'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import BigNumber from 'bignumber.js' interface TokenAddressAndDecimals { - address: string; - decimals: number; + address: string + decimals: number } export const USDC = { @@ -54,8 +54,8 @@ export const USDC = { address: '0xbe72E441BF55620febc26715db68d3494213D8Cb', decimals: 18, }, -} as const satisfies Record; +} as const satisfies Record -export const ZeroBigNumber = new BigNumber(0); -export const OneBigNumber = new BigNumber(1); -export const TenBigNumber = new BigNumber(10); +export const ZeroBigNumber = new BigNumber(0) +export const OneBigNumber = new BigNumber(1) +export const TenBigNumber = new BigNumber(10) diff --git a/libs/repositories/src/database/IndexerState.entity.ts b/libs/repositories/src/database/IndexerState.entity.ts index 73be2d24..4329b12b 100644 --- a/libs/repositories/src/database/IndexerState.entity.ts +++ b/libs/repositories/src/database/IndexerState.entity.ts @@ -1,27 +1,20 @@ -import { - Column, - Entity, - PrimaryColumn, - CreateDateColumn, - UpdateDateColumn, - Unique, -} from 'typeorm'; +import { Column, Entity, PrimaryColumn, CreateDateColumn, UpdateDateColumn, Unique } from 'typeorm' @Entity({ name: 'indexer_state' }) @Unique(['key', 'chainId']) export class IndexerState { @PrimaryColumn('text') - key!: string; + key!: string @Column('integer', { name: 'chain_id', nullable: true }) - chainId!: number | null; + chainId!: number | null @Column('jsonb') - state!: Record; + state!: Record @CreateDateColumn({ name: 'created_at' }) - createdAt!: Date; + createdAt!: Date @UpdateDateColumn({ name: 'updated_at' }) - updatedAt!: Date; + updatedAt!: Date } diff --git a/libs/repositories/src/datasources/alchemy.ts b/libs/repositories/src/datasources/alchemy.ts index d4808c95..d17a9b98 100644 --- a/libs/repositories/src/datasources/alchemy.ts +++ b/libs/repositories/src/datasources/alchemy.ts @@ -1,17 +1,14 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' -export const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY; +export const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY /** * Alchemy API base URL format: https://{network}.g.alchemy.com/v2/{apiKey} * From https://www.alchemy.com/docs/data/token-api/token-api-endpoints/alchemy-get-token-balances */ -export const ALCHEMY_CLIENT_NETWORK_MAPPING: Record< - SupportedChainId, - string | null -> = { +export const ALCHEMY_CLIENT_NETWORK_MAPPING: Record = { [SupportedChainId.MAINNET]: 'eth-mainnet', - [SupportedChainId.SEPOLIA]: null, // it's actually supported, but we don't use it for bff + [SupportedChainId.SEPOLIA]: null, // it's actually supported, but we don't use it for bff [SupportedChainId.GNOSIS_CHAIN]: 'gnosis-mainnet', [SupportedChainId.ARBITRUM_ONE]: 'arb-mainnet', [SupportedChainId.POLYGON]: 'polygon-mainnet', @@ -21,8 +18,8 @@ export const ALCHEMY_CLIENT_NETWORK_MAPPING: Record< [SupportedChainId.LINEA]: 'linea-mainnet', [SupportedChainId.INK]: 'ink-mainnet', [SupportedChainId.PLASMA]: null, // todo add when alchemy supports plasma -}; +} export function getAlchemyApiUrl(network: string, apiKey: string): string { - return `https://${network}.g.alchemy.com/v2/${apiKey}`; + return `https://${network}.g.alchemy.com/v2/${apiKey}` } diff --git a/libs/repositories/src/datasources/cms.ts b/libs/repositories/src/datasources/cms.ts index d9606b7f..d30a0a95 100644 --- a/libs/repositories/src/datasources/cms.ts +++ b/libs/repositories/src/datasources/cms.ts @@ -1,34 +1,32 @@ -import { CmsClient } from '@cowprotocol/cms'; +import { CmsClient } from '@cowprotocol/cms' -export type CmsClientType = ReturnType; +export type CmsClientType = ReturnType -let cmsClient: CmsClientType | undefined = undefined; +let cmsClient: CmsClientType | undefined = undefined export const isCmsEnabled = - process.env.CMS_ENABLED !== undefined - ? process.env.CMS_ENABLED.toLowerCase() === 'true' - : !!process.env.CMS_API_KEY; + process.env.CMS_ENABLED !== undefined ? process.env.CMS_ENABLED.toLowerCase() === 'true' : !!process.env.CMS_API_KEY export function getCmsClient(): CmsClientType { if (cmsClient) { - return cmsClient; + return cmsClient } - const cmsBaseUrl = process.env.CMS_BASE_URL; + const cmsBaseUrl = process.env.CMS_BASE_URL - const cmsApiKey = process.env.CMS_API_KEY; + const cmsApiKey = process.env.CMS_API_KEY if (!cmsApiKey) { - throw new Error('CMS_API_KEY is not set'); + throw new Error('CMS_API_KEY is not set') } if (!isCmsEnabled) { - throw new Error('CMS is not enabled'); + throw new Error('CMS is not enabled') } cmsClient = CmsClient({ url: cmsBaseUrl, apiKey: cmsApiKey, - }); + }) - return cmsClient; + return cmsClient } diff --git a/libs/repositories/src/datasources/coingecko.test.ts b/libs/repositories/src/datasources/coingecko.test.ts index 28c7b639..b179066f 100644 --- a/libs/repositories/src/datasources/coingecko.test.ts +++ b/libs/repositories/src/datasources/coingecko.test.ts @@ -1,40 +1,39 @@ -import { getCoingeckoProClient } from './coingecko'; +import { getCoingeckoProClient } from './coingecko' describe('getCoingeckoProClient', () => { beforeEach(() => { - jest.resetModules(); + jest.resetModules() // Clear the singleton instance by re-requiring the module - jest.clearAllMocks(); - - }); + jest.clearAllMocks() + }) it('should create and return a client when COINGECKO_API_KEY is set', () => { - const client = getCoingeckoProClient('test-api-key'); + const client = getCoingeckoProClient('test-api-key') - expect(client).toBeDefined(); - expect(typeof client.GET).toBe('function'); - expect(typeof client.POST).toBe('function'); - expect(typeof client.PUT).toBe('function'); - expect(typeof client.DELETE).toBe('function'); - }); + expect(client).toBeDefined() + expect(typeof client.GET).toBe('function') + expect(typeof client.POST).toBe('function') + expect(typeof client.PUT).toBe('function') + expect(typeof client.DELETE).toBe('function') + }) it('should return the same client instance on subsequent calls (singleton pattern)', () => { - const client1 = getCoingeckoProClient('test-api-key'); - const client2 = getCoingeckoProClient('test-api-key'); + const client1 = getCoingeckoProClient('test-api-key') + const client2 = getCoingeckoProClient('test-api-key') - expect(client1).toBe(client2); - }); + expect(client1).toBe(client2) + }) it('should throw an error when COINGECKO_API_KEY is not set', () => { expect(() => { - getCoingeckoProClient(); - }).toThrow('COINGECKO_API_KEY is not set'); - }); + getCoingeckoProClient() + }).toThrow('COINGECKO_API_KEY is not set') + }) it('should throw an error when COINGECKO_API_KEY is empty string', () => { expect(() => { - getCoingeckoProClient(''); - }).toThrow('COINGECKO_API_KEY is not set'); - }); -}); \ No newline at end of file + getCoingeckoProClient('') + }).toThrow('COINGECKO_API_KEY is not set') + }) +}) diff --git a/libs/repositories/src/datasources/coingecko.ts b/libs/repositories/src/datasources/coingecko.ts index dd03f6d7..01c72727 100644 --- a/libs/repositories/src/datasources/coingecko.ts +++ b/libs/repositories/src/datasources/coingecko.ts @@ -1,9 +1,9 @@ -import { AdditionalTargetChainId, SupportedChainId, TargetChainId } from '@cowprotocol/cow-sdk'; +import { AdditionalTargetChainId, SupportedChainId, TargetChainId } from '@cowprotocol/cow-sdk' -import createClient from 'openapi-fetch'; -import type { components, paths } from '../gen/coingecko/coingecko-pro-types'; +import createClient from 'openapi-fetch' +import type { components, paths } from '../gen/coingecko/coingecko-pro-types' -export const COINGECKO_PRO_BASE_URL = 'https://pro-api.coingecko.com'; +export const COINGECKO_PRO_BASE_URL = 'https://pro-api.coingecko.com' export const SUPPORTED_COINGECKO_PLATFORMS = { [SupportedChainId.SEPOLIA]: undefined, @@ -20,7 +20,7 @@ export const SUPPORTED_COINGECKO_PLATFORMS = { [AdditionalTargetChainId.OPTIMISM]: 'optimistic-ethereum', [AdditionalTargetChainId.BITCOIN]: 'bitcoin', [AdditionalTargetChainId.SOLANA]: 'solana', -} as const satisfies Record; +} as const satisfies Record /** * Map of chain IDs to Coingecko platform IDs, for every platform that has a network id. @@ -224,35 +224,32 @@ export const COINGECKO_PLATFORMS: Record = { [9898]: 'larissa', [9980]: 'combo', [999]: 'hyperevm', -}; +} -export type CoingeckoProClient = ReturnType>; +export type CoingeckoProClient = ReturnType> -const coingeckoProClientCache: Record = - {}; +const coingeckoProClientCache: Record = {} -export function getCoingeckoProClient( - apiKey = process.env.COINGECKO_API_KEY -): CoingeckoProClient { +export function getCoingeckoProClient(apiKey = process.env.COINGECKO_API_KEY): CoingeckoProClient { if (!apiKey) { - throw new Error('COINGECKO_API_KEY is not set'); + throw new Error('COINGECKO_API_KEY is not set') } - const cached = coingeckoProClientCache[apiKey]; + const cached = coingeckoProClientCache[apiKey] - if (cached) return cached; + if (cached) return cached const coingeckoProClient = createClient({ baseUrl: COINGECKO_PRO_BASE_URL + '/api/v3', headers: { 'x-cg-pro-api-key': apiKey, }, - }); + }) - coingeckoProClientCache[apiKey] = coingeckoProClient; + coingeckoProClientCache[apiKey] = coingeckoProClient - return coingeckoProClient; + return coingeckoProClient } -export type SimplePriceItem = components['schemas']['SimplePrice']; -export type SimplePriceResponse = Record; +export type SimplePriceItem = components['schemas']['SimplePrice'] +export type SimplePriceResponse = Record diff --git a/libs/repositories/src/datasources/cowApi.ts b/libs/repositories/src/datasources/cowApi.ts index 8b6963bb..6f0fb81a 100644 --- a/libs/repositories/src/datasources/cowApi.ts +++ b/libs/repositories/src/datasources/cowApi.ts @@ -1,23 +1,19 @@ -import createClient from 'openapi-fetch'; +import createClient from 'openapi-fetch' -const COW_API_BASE_URL = process.env.COW_API_BASE_URL || 'https://api.cow.fi'; +const COW_API_BASE_URL = process.env.COW_API_BASE_URL || 'https://api.cow.fi' -import { AllChainIds, COW_API_NETWORK_NAMES } from '@cowprotocol/shared'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import type { paths } from '../gen/cow/cow-api-types'; +import { AllChainIds, COW_API_NETWORK_NAMES } from '@cowprotocol/shared' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import type { paths } from '../gen/cow/cow-api-types' -export type CowApiClient = ReturnType>; +export type CowApiClient = ReturnType> -export const cowApiClients = AllChainIds.reduce< - Record ->((acc, chainId) => { - const cowApiUrl = - process.env[`COW_API_URL_${chainId}`] || - COW_API_BASE_URL + '/' + COW_API_NETWORK_NAMES[chainId]; +export const cowApiClients = AllChainIds.reduce>((acc, chainId) => { + const cowApiUrl = process.env[`COW_API_URL_${chainId}`] || COW_API_BASE_URL + '/' + COW_API_NETWORK_NAMES[chainId] acc[chainId] = createClient({ baseUrl: cowApiUrl, - }); + }) - return acc; -}, {} as Record); + return acc +}, {} as Record) diff --git a/libs/repositories/src/datasources/ethplorer.ts b/libs/repositories/src/datasources/ethplorer.ts index 31cc1d52..51edd661 100644 --- a/libs/repositories/src/datasources/ethplorer.ts +++ b/libs/repositories/src/datasources/ethplorer.ts @@ -1,6 +1,6 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' -export const ETHPLORER_API_KEY = process.env.ETHPLORER_API_KEY as string; +export const ETHPLORER_API_KEY = process.env.ETHPLORER_API_KEY as string /** * From https://github.com/EverexIO/Ethplorer/wiki/Ethplorer-API#supported-chains @@ -17,4 +17,4 @@ export const ETHPLORER_BASE_URL = { [SupportedChainId.LINEA]: 'https://api.lineaplorer.build', [SupportedChainId.PLASMA]: null, [SupportedChainId.INK]: null, -} as const satisfies Record; +} as const satisfies Record diff --git a/libs/repositories/src/datasources/goldRush.ts b/libs/repositories/src/datasources/goldRush.ts index 52c291dd..026d1e15 100644 --- a/libs/repositories/src/datasources/goldRush.ts +++ b/libs/repositories/src/datasources/goldRush.ts @@ -1,7 +1,7 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' -export const GOLD_RUSH_API_KEY = process.env.GOLD_RUSH_API_KEY; -export const GOLD_RUSH_API_BASE_URL = 'https://api.covalenthq.com'; +export const GOLD_RUSH_API_KEY = process.env.GOLD_RUSH_API_KEY +export const GOLD_RUSH_API_BASE_URL = 'https://api.covalenthq.com' /** * From https://goldrush.dev/docs/chains/overview @@ -18,4 +18,4 @@ export const GOLD_RUSH_CLIENT_NETWORK_MAPPING = { [SupportedChainId.LINEA]: 'linea-mainnet', [SupportedChainId.PLASMA]: 'plasma-mainnet', [SupportedChainId.INK]: 'ink-mainnet', -} as const satisfies Record; +} as const satisfies Record diff --git a/libs/repositories/src/datasources/moralis.ts b/libs/repositories/src/datasources/moralis.ts index d9756e94..c5f451c2 100644 --- a/libs/repositories/src/datasources/moralis.ts +++ b/libs/repositories/src/datasources/moralis.ts @@ -1,7 +1,7 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' -export const MORALIS_API_KEY = process.env.MORALIS_API_KEY; -export const MORALIS_API_BASE_URL = 'https://deep-index.moralis.io/api'; +export const MORALIS_API_KEY = process.env.MORALIS_API_KEY +export const MORALIS_API_BASE_URL = 'https://deep-index.moralis.io/api' /** * From https://docs.moralis.com/supported-web3data-apis @@ -18,4 +18,4 @@ export const MORALIS_CLIENT_NETWORK_MAPPING = { [SupportedChainId.LINEA]: 'linea', [SupportedChainId.PLASMA]: null, [SupportedChainId.INK]: null, -} as const satisfies Record; +} as const satisfies Record diff --git a/libs/repositories/src/datasources/orderBookDbPool.ts b/libs/repositories/src/datasources/orderBookDbPool.ts index ac6e724e..765cd551 100644 --- a/libs/repositories/src/datasources/orderBookDbPool.ts +++ b/libs/repositories/src/datasources/orderBookDbPool.ts @@ -1,13 +1,13 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { ensureEnvs } from '@cowprotocol/shared'; -import { Pool } from 'pg'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { ensureEnvs } from '@cowprotocol/shared' +import { Pool } from 'pg' const REQUIRED_ENVS = [ 'ORDERBOOK_DATABASE_HOST', 'ORDERBOOK_DATABASE_PORT', 'ORDERBOOK_DATABASE_USERNAME', 'ORDERBOOK_DATABASE_PASSWORD', -]; +] const chainToDbNameMap = { [SupportedChainId.MAINNET]: 'mainnet', @@ -21,15 +21,12 @@ const chainToDbNameMap = { [SupportedChainId.LINEA]: 'linea', [SupportedChainId.PLASMA]: 'plasma', [SupportedChainId.INK]: 'ink', -} as const satisfies Record; +} as const satisfies Record -function createNewOrderBookDbPool( - env: 'prod' | 'barn', - chainId: SupportedChainId -): Pool { - const ENV_PREFIX = env.toUpperCase(); +function createNewOrderBookDbPool(env: 'prod' | 'barn', chainId: SupportedChainId): Pool { + const ENV_PREFIX = env.toUpperCase() - ensureEnvs(REQUIRED_ENVS.map((name) => `${ENV_PREFIX}_${name}`)); + ensureEnvs(REQUIRED_ENVS.map((name) => `${ENV_PREFIX}_${name}`)) const pool = new Pool({ user: process.env[`${ENV_PREFIX}_ORDERBOOK_DATABASE_USERNAME`], @@ -38,30 +35,27 @@ function createNewOrderBookDbPool( password: process.env[`${ENV_PREFIX}_ORDERBOOK_DATABASE_PASSWORD`], port: Number(process.env[`${ENV_PREFIX}_ORDERBOOK_DATABASE_PORT`]), keepAlive: true, - }); + }) // Handle connection errors pool.on('error', (err) => { - console.error('Unexpected error on idle database client', err); - }); + console.error('Unexpected error on idle database client', err) + }) - return pool; + return pool } -const orderBookDbCache = new Map(); +const orderBookDbCache = new Map() -export function getOrderBookDbPool( - env: 'prod' | 'barn', - chainId: SupportedChainId -) { - const key = `${env}|${chainId}`; - const cached = orderBookDbCache.get(key); +export function getOrderBookDbPool(env: 'prod' | 'barn', chainId: SupportedChainId) { + const key = `${env}|${chainId}` + const cached = orderBookDbCache.get(key) - if (cached) return cached; + if (cached) return cached - const db = createNewOrderBookDbPool(env, chainId); + const db = createNewOrderBookDbPool(env, chainId) - orderBookDbCache.set(key, db); + orderBookDbCache.set(key, db) - return db; + return db } diff --git a/libs/repositories/src/datasources/orm/datasource.config.ts b/libs/repositories/src/datasources/orm/datasource.config.ts index 176e065c..3c8d4bfb 100644 --- a/libs/repositories/src/datasources/orm/datasource.config.ts +++ b/libs/repositories/src/datasources/orm/datasource.config.ts @@ -1,6 +1,6 @@ -import { createNewPostgresOrm } from './postgresOrm'; +import { createNewPostgresOrm } from './postgresOrm' -const dataSource = createNewPostgresOrm(); -dataSource.initialize(); +const dataSource = createNewPostgresOrm() +dataSource.initialize() -export default dataSource; +export default dataSource diff --git a/libs/repositories/src/datasources/orm/postgresOrm.ts b/libs/repositories/src/datasources/orm/postgresOrm.ts index bb2b9bc1..01e608bf 100644 --- a/libs/repositories/src/datasources/orm/postgresOrm.ts +++ b/libs/repositories/src/datasources/orm/postgresOrm.ts @@ -1,15 +1,15 @@ -import { DataSource } from 'typeorm'; +import { DataSource } from 'typeorm' -import assert from 'assert'; -import { IndexerState } from '../../database/IndexerState.entity'; +import assert from 'assert' +import { IndexerState } from '../../database/IndexerState.entity' export function getDatabaseParams() { // Note: not using the `ensureEnvs` util function because it causes issues with the migrations) - assert(process.env.DATABASE_HOST, 'DATABASE_HOST is not set'); - assert(process.env.DATABASE_PORT, 'DATABASE_PORT is not set'); - assert(process.env.DATABASE_USERNAME, 'DATABASE_USERNAME is not set'); - assert(process.env.DATABASE_PASSWORD, 'DATABASE_PASSWORD is not set'); - assert(process.env.DATABASE_NAME, 'DATABASE_NAME is not set'); + assert(process.env.DATABASE_HOST, 'DATABASE_HOST is not set') + assert(process.env.DATABASE_PORT, 'DATABASE_PORT is not set') + assert(process.env.DATABASE_USERNAME, 'DATABASE_USERNAME is not set') + assert(process.env.DATABASE_PASSWORD, 'DATABASE_PASSWORD is not set') + assert(process.env.DATABASE_NAME, 'DATABASE_NAME is not set') return { host: process.env.DATABASE_HOST, @@ -17,7 +17,7 @@ export function getDatabaseParams() { username: process.env.DATABASE_USERNAME, password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_NAME, - }; + } } export function createNewPostgresOrm(): DataSource { @@ -27,5 +27,5 @@ export function createNewPostgresOrm(): DataSource { entities: [IndexerState], migrations: ['src/migrations/*.ts'], migrationsTableName: 'migrations_repositories', - }); + }) } diff --git a/libs/repositories/src/datasources/postgresPlain.ts b/libs/repositories/src/datasources/postgresPlain.ts index 5a3ef84a..649c4c00 100644 --- a/libs/repositories/src/datasources/postgresPlain.ts +++ b/libs/repositories/src/datasources/postgresPlain.ts @@ -1,15 +1,10 @@ -import { ensureEnvs } from '@cowprotocol/shared'; -import { Pool } from 'pg'; +import { ensureEnvs } from '@cowprotocol/shared' +import { Pool } from 'pg' -const REQUIRED_ENVS = [ - 'DATABASE_USERNAME', - 'DATABASE_HOST', - 'DATABASE_NAME', - 'DATABASE_PASSWORD', -]; +const REQUIRED_ENVS = ['DATABASE_USERNAME', 'DATABASE_HOST', 'DATABASE_NAME', 'DATABASE_PASSWORD'] export function createNewPostgresPool(): Pool { - ensureEnvs(REQUIRED_ENVS); + ensureEnvs(REQUIRED_ENVS) const pool = new Pool({ user: process.env.DATABASE_USERNAME, @@ -18,12 +13,12 @@ export function createNewPostgresPool(): Pool { password: process.env.DATABASE_PASSWORD, port: Number(process.env.DATABASE_PORT) || 5432, keepAlive: true, - }); + }) // Handle connection errors pool.on('error', (err) => { - console.error('Unexpected error on idle database client', err); - }); + console.error('Unexpected error on idle database client', err) + }) - return pool; + return pool } diff --git a/libs/repositories/src/datasources/rabbitMq.ts b/libs/repositories/src/datasources/rabbitMq.ts index 570e383c..21076499 100644 --- a/libs/repositories/src/datasources/rabbitMq.ts +++ b/libs/repositories/src/datasources/rabbitMq.ts @@ -1,15 +1,15 @@ -import { ensureEnvs } from '@cowprotocol/shared'; -import amqp from 'amqplib'; +import { ensureEnvs } from '@cowprotocol/shared' +import amqp from 'amqplib' -const REQUIRED_ENVS = ['QUEUE_HOST', 'QUEUE_USER', 'QUEUE_PASSWORD']; +const REQUIRED_ENVS = ['QUEUE_HOST', 'QUEUE_USER', 'QUEUE_PASSWORD'] export async function createRabbitMqConnection() { - ensureEnvs(REQUIRED_ENVS); + ensureEnvs(REQUIRED_ENVS) return amqp.connect({ hostname: process.env.QUEUE_HOST, port: Number(process.env.QUEUE_PORT || '5672'), username: process.env.QUEUE_USER, password: process.env.QUEUE_PASSWORD, - }); + }) } diff --git a/libs/repositories/src/datasources/redis.ts b/libs/repositories/src/datasources/redis.ts index 662ffe14..3d5d9ad0 100644 --- a/libs/repositories/src/datasources/redis.ts +++ b/libs/repositories/src/datasources/redis.ts @@ -1,10 +1,10 @@ -import IORedis from 'ioredis'; +import IORedis from 'ioredis' // Check if redis is enable REDIS_ENABLED env takes precence. Otherwise enable if we provide REDIS_HOST const isRedisEnabled = process.env.REDIS_ENABLED !== undefined ? process.env.REDIS_ENABLED.toLowerCase() === 'true' - : !!process.env.REDIS_HOST; + : !!process.env.REDIS_HOST export const redisClient = isRedisEnabled ? new IORedis({ @@ -14,4 +14,4 @@ export const redisClient = isRedisEnabled username: process.env.REDIS_USER, password: process.env.REDIS_PASSWORD, }) - : null; + : null diff --git a/libs/repositories/src/datasources/telegram.ts b/libs/repositories/src/datasources/telegram.ts index 8e3b916b..401d5c8d 100644 --- a/libs/repositories/src/datasources/telegram.ts +++ b/libs/repositories/src/datasources/telegram.ts @@ -1,11 +1,11 @@ -import assert from 'assert'; +import assert from 'assert' -import TelegramBotClass from 'node-telegram-bot-api'; +import TelegramBotClass from 'node-telegram-bot-api' -export type TelegramBot = TelegramBotClass; +export type TelegramBot = TelegramBotClass export function createTelegramBot(): TelegramBot { - const token = process.env.TELEGRAM_SECRET; - assert(token, 'TELEGRAM_SECRET is required'); - return new TelegramBotClass(token, { polling: true }); + const token = process.env.TELEGRAM_SECRET + assert(token, 'TELEGRAM_SECRET is required') + return new TelegramBotClass(token, { polling: true }) } diff --git a/libs/repositories/src/datasources/tenderlyApi.ts b/libs/repositories/src/datasources/tenderlyApi.ts index 63d0934c..b8f26e45 100644 --- a/libs/repositories/src/datasources/tenderlyApi.ts +++ b/libs/repositories/src/datasources/tenderlyApi.ts @@ -1,11 +1,11 @@ -export const TENDERLY_API_KEY = process.env.TENDERLY_API_KEY as string; +export const TENDERLY_API_KEY = process.env.TENDERLY_API_KEY as string -export const TENDERLY_ORG_NAME = process.env.TENDERLY_ORG_NAME; -export const TENDERLY_PROJECT_NAME = process.env.TENDERLY_PROJECT_NAME; +export const TENDERLY_ORG_NAME = process.env.TENDERLY_ORG_NAME +export const TENDERLY_PROJECT_NAME = process.env.TENDERLY_PROJECT_NAME -export const TENDERLY_API_BASE_ENDPOINT = `https://api.tenderly.co/api/v1/account/${TENDERLY_ORG_NAME}/project/${TENDERLY_PROJECT_NAME}`; +export const TENDERLY_API_BASE_ENDPOINT = `https://api.tenderly.co/api/v1/account/${TENDERLY_ORG_NAME}/project/${TENDERLY_PROJECT_NAME}` export const getTenderlySimulationLink = (simulationId: string): string => { // This link only work for public projects, if the project is private remove the "/public" from the link - return `https://dashboard.tenderly.co/public/${TENDERLY_ORG_NAME}/${TENDERLY_PROJECT_NAME}/simulator/${simulationId}`; -}; + return `https://dashboard.tenderly.co/public/${TENDERLY_ORG_NAME}/${TENDERLY_PROJECT_NAME}/simulator/${simulationId}` +} diff --git a/libs/repositories/src/datasources/viem.ts b/libs/repositories/src/datasources/viem.ts index f6d80c1c..3a7db42f 100644 --- a/libs/repositories/src/datasources/viem.ts +++ b/libs/repositories/src/datasources/viem.ts @@ -1,25 +1,7 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { AllChainIds, logger } from '@cowprotocol/shared'; -import { - Chain, - createPublicClient, - http, - PublicClient, - webSocket, -} from 'viem'; -import { - arbitrum, - avalanche, - base, - bsc, - gnosis, - linea, - mainnet, - plasma, - polygon, - sepolia, - ink, -} from 'viem/chains'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { AllChainIds, logger } from '@cowprotocol/shared' +import { Chain, createPublicClient, http, PublicClient, webSocket } from 'viem' +import { arbitrum, avalanche, base, bsc, gnosis, linea, mainnet, plasma, polygon, sepolia, ink } from 'viem/chains' const NETWORKS = { [SupportedChainId.MAINNET]: mainnet, @@ -33,67 +15,59 @@ const NETWORKS = { [SupportedChainId.LINEA]: linea, [SupportedChainId.PLASMA]: plasma, [SupportedChainId.INK]: ink, -} as const satisfies Record; +} as const satisfies Record -let viemClients: Record | undefined; +let viemClients: Record | undefined export function getViemClients(): Record { if (viemClients) { - return viemClients; + return viemClients } - viemClients = AllChainIds.reduce>( - (acc, chainId) => { - const chain = NETWORKS[chainId]; - const envVarName = `RPC_URL_${chainId}`; - const rpcEndpoint = process.env[envVarName]; - if (!rpcEndpoint) { - logger.warn( - `RPC_URL_${chainId} is not set. Using default RPC URL for ${chain.name}` - ); - } - const defaultRpcUrls = getDefaultRpcUrl(chain, rpcEndpoint); + viemClients = AllChainIds.reduce>((acc, chainId) => { + const chain = NETWORKS[chainId] + const envVarName = `RPC_URL_${chainId}` + const rpcEndpoint = process.env[envVarName] + if (!rpcEndpoint) { + logger.warn(`RPC_URL_${chainId} is not set. Using default RPC URL for ${chain.name}`) + } + const defaultRpcUrls = getDefaultRpcUrl(chain, rpcEndpoint) - acc[chainId] = createPublicClient({ - chain: { - ...chain, - rpcUrls: { - default: defaultRpcUrls, - }, + acc[chainId] = createPublicClient({ + chain: { + ...chain, + rpcUrls: { + default: defaultRpcUrls, }, - transport: defaultRpcUrls.webSocket - ? webSocket(undefined, { + }, + transport: defaultRpcUrls.webSocket + ? webSocket(undefined, { retryDelay: 5_000, // 5sec retryCount: 3, reconnect: true, }) - : http(), - }) as PublicClient; + : http(), + }) as PublicClient - return acc; - }, - {} as Record - ); + return acc + }, {} as Record) - return viemClients; + return viemClients } -function getDefaultRpcUrl( - chain: Chain, - rpcEndpoint?: string -): Chain['rpcUrls']['default'] { +function getDefaultRpcUrl(chain: Chain, rpcEndpoint?: string): Chain['rpcUrls']['default'] { if (!rpcEndpoint) { - return chain.rpcUrls.default; + return chain.rpcUrls.default } if (rpcEndpoint.startsWith('http')) { return { http: [rpcEndpoint], - }; + } } return { http: chain.rpcUrls.default.http, webSocket: [rpcEndpoint], - }; + } } diff --git a/libs/repositories/src/index.ts b/libs/repositories/src/index.ts index b4383b3e..4cf35221 100644 --- a/libs/repositories/src/index.ts +++ b/libs/repositories/src/index.ts @@ -1,88 +1,88 @@ // Utils -export * from './utils/cache'; -export * from './utils/isDbEnabled'; +export * from './utils/cache' +export * from './utils/isDbEnabled' // Data-sources -export * from './datasources/cms'; -export * from './datasources/cowApi'; -export * from './datasources/orm/postgresOrm'; -export * from './datasources/postgresPlain'; -export * from './datasources/rabbitMq'; -export * from './datasources/redis'; -export * from './datasources/telegram'; -export * from './datasources/viem'; -export * from './datasources/orderBookDbPool'; +export * from './datasources/cms' +export * from './datasources/cowApi' +export * from './datasources/orm/postgresOrm' +export * from './datasources/postgresPlain' +export * from './datasources/rabbitMq' +export * from './datasources/redis' +export * from './datasources/telegram' +export * from './datasources/viem' +export * from './datasources/orderBookDbPool' // Data sources -export { COINGECKO_PRO_BASE_URL } from './datasources/coingecko'; +export { COINGECKO_PRO_BASE_URL } from './datasources/coingecko' // Cache repositories -export * from './repos/CacheRepository/CacheRepository'; -export * from './repos/CacheRepository/CacheRepositoryMemory'; -export * from './repos/CacheRepository/CacheRepositoryRedis'; +export * from './repos/CacheRepository/CacheRepository' +export * from './repos/CacheRepository/CacheRepositoryMemory' +export * from './repos/CacheRepository/CacheRepositoryRedis' // Erc20Repository -export * from './repos/Erc20Repository/Erc20Repository'; -export * from './repos/Erc20Repository/Erc20RepositoryCache'; -export * from './repos/Erc20Repository/Erc20RepositoryFallback'; -export * from './repos/Erc20Repository/Erc20RepositoryNative'; -export * from './repos/Erc20Repository/Erc20RepositoryViem'; +export * from './repos/Erc20Repository/Erc20Repository' +export * from './repos/Erc20Repository/Erc20RepositoryCache' +export * from './repos/Erc20Repository/Erc20RepositoryFallback' +export * from './repos/Erc20Repository/Erc20RepositoryNative' +export * from './repos/Erc20Repository/Erc20RepositoryViem' // USD repositories -export * from './repos/UsdRepository/UsdRepository'; -export * from './repos/UsdRepository/UsdRepositoryCache'; -export * from './repos/UsdRepository/UsdRepositoryCoingecko'; -export * from './repos/UsdRepository/UsdRepositoryCow'; -export * from './repos/UsdRepository/UsdRepositoryFallback'; +export * from './repos/UsdRepository/UsdRepository' +export * from './repos/UsdRepository/UsdRepositoryCache' +export * from './repos/UsdRepository/UsdRepositoryCoingecko' +export * from './repos/UsdRepository/UsdRepositoryCow' +export * from './repos/UsdRepository/UsdRepositoryFallback' // Token holder repositories -export * from './repos/TokenHolderRepository/TokenHolderRepository'; -export * from './repos/TokenHolderRepository/TokenHolderRepositoryCache'; -export * from './repos/TokenHolderRepository/TokenHolderRepositoryEthplorer'; -export * from './repos/TokenHolderRepository/TokenHolderRepositoryFallback'; -export * from './repos/TokenHolderRepository/TokenHolderRepositoryGoldRush'; -export * from './repos/TokenHolderRepository/TokenHolderRepositoryMoralis'; +export * from './repos/TokenHolderRepository/TokenHolderRepository' +export * from './repos/TokenHolderRepository/TokenHolderRepositoryCache' +export * from './repos/TokenHolderRepository/TokenHolderRepositoryEthplorer' +export * from './repos/TokenHolderRepository/TokenHolderRepositoryFallback' +export * from './repos/TokenHolderRepository/TokenHolderRepositoryGoldRush' +export * from './repos/TokenHolderRepository/TokenHolderRepositoryMoralis' // Token balances repositories -export * from './repos/TokenBalancesRepository/TokenBalancesRepository'; -export * from './repos/TokenBalancesRepository/TokenBalancesRepositoryMoralis'; -export * from './repos/TokenBalancesRepository/TokenBalancesRepositoryAlchemy'; +export * from './repos/TokenBalancesRepository/TokenBalancesRepository' +export * from './repos/TokenBalancesRepository/TokenBalancesRepositoryMoralis' +export * from './repos/TokenBalancesRepository/TokenBalancesRepositoryAlchemy' // User balance repositories -export * from './repos/UserBalanceRepository/UserBalanceRepository'; -export * from './repos/UserBalanceRepository/UserBalanceRepositoryCache'; -export * from './repos/UserBalanceRepository/UserBalanceRepositoryViem'; +export * from './repos/UserBalanceRepository/UserBalanceRepository' +export * from './repos/UserBalanceRepository/UserBalanceRepositoryCache' +export * from './repos/UserBalanceRepository/UserBalanceRepositoryViem' // Simulation repositories -export * from './repos/SimulationRepository/SimulationRepository'; -export * from './repos/SimulationRepository/SimulationRepositoryTenderly'; -export * from './repos/SimulationRepository/tenderlyTypes'; +export * from './repos/SimulationRepository/SimulationRepository' +export * from './repos/SimulationRepository/SimulationRepositoryTenderly' +export * from './repos/SimulationRepository/tenderlyTypes' // Indexer state repository -export * from './repos/IndexerStateRepository/IndexerStateRepository'; -export * from './repos/IndexerStateRepository/IndexerStateRepositoryOrm'; -export * from './repos/IndexerStateRepository/IndexerStateRepositoryPostgres'; +export * from './repos/IndexerStateRepository/IndexerStateRepository' +export * from './repos/IndexerStateRepository/IndexerStateRepositoryOrm' +export * from './repos/IndexerStateRepository/IndexerStateRepositoryPostgres' // OnChainPlacedOrdersRepository -export * from './repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepository'; -export * from './repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepositoryPostgres'; +export * from './repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepository' +export * from './repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepositoryPostgres' // ExpiredOrdersRepository -export * from './repos/ExpiredOrdersRepository/ExpiredOrdersRepository'; -export * from './repos/ExpiredOrdersRepository/ExpiredOrdersRepositoryPostgres'; +export * from './repos/ExpiredOrdersRepository/ExpiredOrdersRepository' +export * from './repos/ExpiredOrdersRepository/ExpiredOrdersRepositoryPostgres' // OrdersAppDataRepository -export * from './repos/OrdersAppDataRepository/OrdersAppDataRepository'; -export * from './repos/OrdersAppDataRepository/OrdersAppDataRepositoryPostgres'; +export * from './repos/OrdersAppDataRepository/OrdersAppDataRepository' +export * from './repos/OrdersAppDataRepository/OrdersAppDataRepositoryPostgres' // Notifications repositories -export * from './repos/PushNotificationsRepository/PushNotificationsRepository'; -export * from './repos/PushSubscriptionsRepository/PushSubscriptionsRepository'; -export * from './repos/PushSubscriptionsRepository/PushSubscriptionsRepositoryCms'; +export * from './repos/PushNotificationsRepository/PushNotificationsRepository' +export * from './repos/PushSubscriptionsRepository/PushSubscriptionsRepository' +export * from './repos/PushSubscriptionsRepository/PushSubscriptionsRepositoryCms' // Dune repositories -export * from './repos/DuneRepository/DuneRepository'; -export * from './repos/DuneRepository/DuneRepositoryImpl'; +export * from './repos/DuneRepository/DuneRepository' +export * from './repos/DuneRepository/DuneRepositoryImpl' // Affiliates repositories -export * from './repos/AffiliatesRepository/AffiliatesRepository'; -export * from './repos/AffiliatesRepository/AffiliatesRepositoryCms'; +export * from './repos/AffiliatesRepository/AffiliatesRepository' +export * from './repos/AffiliatesRepository/AffiliatesRepositoryCms' diff --git a/libs/repositories/src/migrations/1745364046891-initial-migration.ts b/libs/repositories/src/migrations/1745364046891-initial-migration.ts index 93317ad8..ee69d099 100644 --- a/libs/repositories/src/migrations/1745364046891-initial-migration.ts +++ b/libs/repositories/src/migrations/1745364046891-initial-migration.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { MigrationInterface, QueryRunner } from 'typeorm' export class InitialMigration1745364046891 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { @@ -13,7 +13,7 @@ export class InitialMigration1745364046891 implements MigrationInterface { updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (key, chain_id) ) - `); + `) //Update the updated_at column with the current timestamp await queryRunner.query(` @@ -24,7 +24,7 @@ export class InitialMigration1745364046891 implements MigrationInterface { RETURN NEW; END; $$ LANGUAGE plpgsql; -`); +`) // Create a trigger to automatically update the updated_at column await queryRunner.query(` @@ -32,23 +32,23 @@ export class InitialMigration1745364046891 implements MigrationInterface { BEFORE UPDATE ON indexer_state FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - `); + `) } public async down(queryRunner: QueryRunner): Promise { // Drop the trigger await queryRunner.query(` DROP TRIGGER IF EXISTS trigger_set_updated_at ON indexer_state - `); + `) // Drop the function await queryRunner.query(` DROP FUNCTION IF EXISTS set_updated_at - `); + `) // Drop the table await queryRunner.query(` DROP TABLE IF EXISTS indexer_state - `); + `) } } diff --git a/libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepository.ts b/libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepository.ts index b21a00df..9b312f79 100644 --- a/libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepository.ts +++ b/libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepository.ts @@ -1,41 +1,39 @@ -export const affiliatesRepositorySymbol = Symbol.for('AffiliatesRepository'); +export const affiliatesRepositorySymbol = Symbol.for('AffiliatesRepository') export type AffiliateRecord = { - id: number; - code: string; - walletAddress: string; - signedMessage: string | null; - enabled: boolean; - rewardAmount: number; - triggerVolume: number; - timeCapDays: number; - volumeCap: number; - revenueSplitAffiliatePct: number; - revenueSplitTraderPct: number; - revenueSplitDaoPct: number; - createdAt: string; - updatedAt: string; - publishedAt: string | null; -}; + id: number + code: string + walletAddress: string + signedMessage: string | null + enabled: boolean + rewardAmount: number + triggerVolume: number + timeCapDays: number + volumeCap: number + revenueSplitAffiliatePct: number + revenueSplitTraderPct: number + revenueSplitDaoPct: number + createdAt: string + updatedAt: string + publishedAt: string | null +} export type CreateAffiliateInput = { - code: string; - walletAddress: string; - signedMessage?: string | null; - enabled?: boolean; -}; + code: string + walletAddress: string + signedMessage?: string | null + enabled?: boolean +} /** * Repository for affiliate codes stored in the CMS. */ export interface AffiliatesRepository { - getAffiliateByWalletAddress(params: { - walletAddress: string; - }): Promise; + getAffiliateByWalletAddress(params: { walletAddress: string }): Promise - getAffiliateByCode(params: { code: string }): Promise; + getAffiliateByCode(params: { code: string }): Promise - createAffiliate(params: CreateAffiliateInput): Promise; + createAffiliate(params: CreateAffiliateInput): Promise - listAffiliates(): Promise; + listAffiliates(): Promise } diff --git a/libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepositoryCms.ts b/libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepositoryCms.ts index a33c300b..c43daa3f 100644 --- a/libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepositoryCms.ts +++ b/libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepositoryCms.ts @@ -1,124 +1,107 @@ -import { getCmsClient } from '../../datasources/cms'; -import { - AffiliateRecord, - AffiliatesRepository, - CreateAffiliateInput, -} from './AffiliatesRepository'; +import { getCmsClient } from '../../datasources/cms' +import { AffiliateRecord, AffiliatesRepository, CreateAffiliateInput } from './AffiliatesRepository' -const AFFILIATE_COLLECTION_PATH = '/affiliates'; -const PAGE_SIZE = 100; +const AFFILIATE_COLLECTION_PATH = '/affiliates' +const PAGE_SIZE = 100 type AffiliateAttributes = { - code: string; - walletAddress: string; - signedMessage?: string | null; - enabled: boolean; - rewardAmount: number; - triggerVolume: number; - timeCapDays: number; - volumeCap: number; - revenueSplitAffiliatePct: number; - revenueSplitTraderPct: number; - revenueSplitDaoPct: number; - createdAt: string; - updatedAt: string; - publishedAt?: string | null; -}; + code: string + walletAddress: string + signedMessage?: string | null + enabled: boolean + rewardAmount: number + triggerVolume: number + timeCapDays: number + volumeCap: number + revenueSplitAffiliatePct: number + revenueSplitTraderPct: number + revenueSplitDaoPct: number + createdAt: string + updatedAt: string + publishedAt?: string | null +} type StrapiData = { - id: number; - attributes: T; -}; + id: number + attributes: T +} type StrapiPagination = { - page: number; - pageSize: number; - pageCount: number; - total: number; -}; + page: number + pageSize: number + pageCount: number + total: number +} type StrapiListResponse = { - data: Array>; + data: Array> meta?: { - pagination?: StrapiPagination; - }; -}; + pagination?: StrapiPagination + } +} type StrapiSingleResponse = { - data: StrapiData; -}; + data: StrapiData +} class CmsRequestError extends Error { - readonly status: number; - readonly url: string; - readonly payload: unknown; + readonly status: number + readonly url: string + readonly payload: unknown constructor(params: { status: number; url: string; payload: unknown }) { - super(`CMS request failed (${params.status})`); - this.status = params.status; - this.url = params.url; - this.payload = params.payload; + super(`CMS request failed (${params.status})`) + this.status = params.status + this.url = params.url + this.payload = params.payload } } export class AffiliatesRepositoryCms implements AffiliatesRepository { - async getAffiliateByWalletAddress(params: { - walletAddress: string; - }): Promise { - const { walletAddress } = params; - const normalizedWallet = walletAddress.toLowerCase(); - const response = await cmsGet>( - AFFILIATE_COLLECTION_PATH, - { - 'filters[walletAddress][$eq]': normalizedWallet, - 'pagination[pageSize]': 1, - } - ); - - return response.data.length > 0 ? mapAffiliate(response.data[0]) : null; + async getAffiliateByWalletAddress(params: { walletAddress: string }): Promise { + const { walletAddress } = params + const normalizedWallet = walletAddress.toLowerCase() + const response = await cmsGet>(AFFILIATE_COLLECTION_PATH, { + 'filters[walletAddress][$eq]': normalizedWallet, + 'pagination[pageSize]': 1, + }) + + return response.data.length > 0 ? mapAffiliate(response.data[0]) : null } - async getAffiliateByCode(params: { - code: string; - }): Promise { - const { code } = params; - const normalizedCode = code.trim().toUpperCase(); - const response = await cmsGet>( - AFFILIATE_COLLECTION_PATH, - { - 'filters[code][$eq]': normalizedCode, - 'pagination[pageSize]': 1, - } - ); + async getAffiliateByCode(params: { code: string }): Promise { + const { code } = params + const normalizedCode = code.trim().toUpperCase() + const response = await cmsGet>(AFFILIATE_COLLECTION_PATH, { + 'filters[code][$eq]': normalizedCode, + 'pagination[pageSize]': 1, + }) - return response.data.length > 0 ? mapAffiliate(response.data[0]) : null; + return response.data.length > 0 ? mapAffiliate(response.data[0]) : null } async createAffiliate(params: CreateAffiliateInput): Promise { - const { code, walletAddress, signedMessage, enabled } = params; - - const response = await cmsPost>( - AFFILIATE_COLLECTION_PATH, - { - data: { - code, - walletAddress, - signedMessage: signedMessage ?? null, - enabled: enabled ?? true, - }, - } - ); - - return mapAffiliate(response.data); + const { code, walletAddress, signedMessage, enabled } = params + + const response = await cmsPost>(AFFILIATE_COLLECTION_PATH, { + data: { + code, + walletAddress, + signedMessage: signedMessage ?? null, + enabled: enabled ?? true, + }, + }) + + return mapAffiliate(response.data) } async listAffiliates(): Promise { const entries = await getAllPages({ pageSize: PAGE_SIZE, getPage: (params) => getAffiliatePage(params), - }); + }) - return entries.map(mapAffiliate); + return entries.map(mapAffiliate) } } @@ -139,113 +122,106 @@ function mapAffiliate(data: StrapiData): AffiliateRecord { createdAt: data.attributes.createdAt, updatedAt: data.attributes.updatedAt, publishedAt: data.attributes.publishedAt ?? null, - }; + } } -async function cmsGet( - path: string, - query?: Record -): Promise { - const cmsClient = getCmsClient() as any; +async function cmsGet(path: string, query?: Record): Promise { + const cmsClient = getCmsClient() as any const { data, error, response } = await cmsClient.GET(path, { params: query ? { query } : undefined, - }); + }) if (error) { throw new CmsRequestError({ status: response.status, url: response.url, payload: error, - }); + }) } - return data as T; + return data as T } async function cmsPost(path: string, body: unknown): Promise { - const cmsClient = getCmsClient() as any; - const { data, error, response } = await cmsClient.POST(path, { body }); + const cmsClient = getCmsClient() as any + const { data, error, response } = await cmsClient.POST(path, { body }) if (error) { throw new CmsRequestError({ status: response.status, url: response.url, payload: error, - }); + }) } - return data as T; + return data as T } export function isCmsRequestError(error: unknown): error is CmsRequestError { - return error instanceof CmsRequestError; + return error instanceof CmsRequestError } /** * Strapi REST pagination docs: https://docs.strapi.io/cms/api/rest/sort-pagination */ -async function getAffiliatePage({ - page = 1, - pageSize = PAGE_SIZE, -}: PaginationParam = {}): Promise> { - return cmsGet>( - AFFILIATE_COLLECTION_PATH, - { - 'pagination[withCount]': true, - 'pagination[page]': page, - 'pagination[pageSize]': pageSize, - } - ); +async function getAffiliatePage({ page = 1, pageSize = PAGE_SIZE }: PaginationParam = {}): Promise< + StrapiListResponse +> { + return cmsGet>(AFFILIATE_COLLECTION_PATH, { + 'pagination[withCount]': true, + 'pagination[page]': page, + 'pagination[pageSize]': pageSize, + }) } type PaginationParam = { - page?: number; - pageSize?: number; -}; + page?: number + pageSize?: number +} async function getAllPages({ pageSize = PAGE_SIZE, getPage, }: PaginationParam & { - getPage: (params: PaginationParam) => Promise>; + getPage: (params: PaginationParam) => Promise> }): Promise>> { - const allEntries: Array> = []; - let page = 1; - let total: number | null = null; - let fetched = 0; + const allEntries: Array> = [] + let page = 1 + let total: number | null = null + let fetched = 0 - let hasMore = true; + let hasMore = true while (hasMore) { - const response = await getPage({ page, pageSize }); - const entries = response.data; + const response = await getPage({ page, pageSize }) + const entries = response.data if (total === null) { - const metaTotal = response.meta?.pagination?.total; + const metaTotal = response.meta?.pagination?.total if (typeof metaTotal === 'number') { - total = metaTotal; + total = metaTotal } } if (entries.length === 0) { - hasMore = false; - continue; + hasMore = false + continue } - allEntries.push(...entries); - fetched += entries.length; + allEntries.push(...entries) + fetched += entries.length if (total !== null && fetched >= total) { - hasMore = false; - continue; + hasMore = false + continue } if (entries.length < pageSize) { - hasMore = false; - continue; + hasMore = false + continue } - page++; + page++ } - return allEntries; + return allEntries } diff --git a/libs/repositories/src/repos/CacheRepository/CacheRepository.ts b/libs/repositories/src/repos/CacheRepository/CacheRepository.ts index 78107c44..c4b55b6d 100644 --- a/libs/repositories/src/repos/CacheRepository/CacheRepository.ts +++ b/libs/repositories/src/repos/CacheRepository/CacheRepository.ts @@ -1,7 +1,7 @@ -export const cacheRepositorySymbol = Symbol.for('CacheRepository'); +export const cacheRepositorySymbol = Symbol.for('CacheRepository') export interface CacheRepository { - get(key: string): Promise; - getTtl(key: string): Promise; - set(key: string, value: string, ttl: number): Promise; + get(key: string): Promise + getTtl(key: string): Promise + set(key: string, value: string, ttl: number): Promise } diff --git a/libs/repositories/src/repos/CacheRepository/CacheRepositoryMemory.ts b/libs/repositories/src/repos/CacheRepository/CacheRepositoryMemory.ts index 501fadce..3aceda50 100644 --- a/libs/repositories/src/repos/CacheRepository/CacheRepositoryMemory.ts +++ b/libs/repositories/src/repos/CacheRepository/CacheRepositoryMemory.ts @@ -1,28 +1,28 @@ -import { injectable } from 'inversify'; -import NodeCache from 'node-cache'; -import { CacheRepository } from './CacheRepository'; +import { injectable } from 'inversify' +import NodeCache from 'node-cache' +import { CacheRepository } from './CacheRepository' @injectable() export class CacheRepositoryMemory implements CacheRepository { - static cache: NodeCache = new NodeCache(); + static cache: NodeCache = new NodeCache() async get(key: string): Promise { - const value = await CacheRepositoryMemory.cache.get(key); + const value = await CacheRepositoryMemory.cache.get(key) - return value ?? null; + return value ?? null } async getTtl(key: string): Promise { - const ttlEpoch = (await CacheRepositoryMemory.cache.getTtl(key)) || null; + const ttlEpoch = (await CacheRepositoryMemory.cache.getTtl(key)) || null if (ttlEpoch === null) { - return null; + return null } - return Math.floor((ttlEpoch - Date.now()) / 1000); + return Math.floor((ttlEpoch - Date.now()) / 1000) } async set(key: string, value: string, ttl: number): Promise { - await CacheRepositoryMemory.cache.set(key, value, ttl); + await CacheRepositoryMemory.cache.set(key, value, ttl) } } diff --git a/libs/repositories/src/repos/CacheRepository/CacheRepositoryRedis.ts b/libs/repositories/src/repos/CacheRepository/CacheRepositoryRedis.ts index f9c250b6..683bb3c7 100644 --- a/libs/repositories/src/repos/CacheRepository/CacheRepositoryRedis.ts +++ b/libs/repositories/src/repos/CacheRepository/CacheRepositoryRedis.ts @@ -1,26 +1,26 @@ -import { injectable } from 'inversify'; -import { CacheRepository } from './CacheRepository'; -import { Redis } from 'ioredis'; +import { injectable } from 'inversify' +import { CacheRepository } from './CacheRepository' +import { Redis } from 'ioredis' @injectable() export class CacheRepositoryRedis implements CacheRepository { constructor(private redisClient: Redis) {} async get(key: string): Promise { - return this.redisClient.get(key); + return this.redisClient.get(key) } async getTtl(key: string): Promise { - const ttl = await this.redisClient.ttl(key); + const ttl = await this.redisClient.ttl(key) if (ttl < 0) { - return null; + return null } - return ttl; + return ttl } async set(key: string, value: string, ttl: number): Promise { - await this.redisClient.set(key, value, 'EX', ttl); + await this.redisClient.set(key, value, 'EX', ttl) } } diff --git a/libs/repositories/src/repos/DuneRepository/DuneRepository.spec.ts b/libs/repositories/src/repos/DuneRepository/DuneRepository.spec.ts index 15f6abba..c3991006 100644 --- a/libs/repositories/src/repos/DuneRepository/DuneRepository.spec.ts +++ b/libs/repositories/src/repos/DuneRepository/DuneRepository.spec.ts @@ -1,170 +1,158 @@ -import { DuneExecutionResponse, DuneResultResponse } from './DuneRepository'; -import { DuneRepositoryImpl } from './DuneRepositoryImpl'; +import { DuneExecutionResponse, DuneResultResponse } from './DuneRepository' +import { DuneRepositoryImpl } from './DuneRepositoryImpl' // Mock fetch globally -global.fetch = jest.fn(); +global.fetch = jest.fn() describe('DuneRepositoryImpl', () => { - let repository: DuneRepositoryImpl; - const mockApiKey = 'test-api-key'; + let repository: DuneRepositoryImpl + const mockApiKey = 'test-api-key' beforeEach(() => { - repository = new DuneRepositoryImpl(mockApiKey); - jest.clearAllMocks(); - }); + repository = new DuneRepositoryImpl(mockApiKey) + jest.clearAllMocks() + }) describe('executeQuery', () => { it('should execute a query successfully with parameters', async () => { const mockResponse: DuneExecutionResponse = { execution_id: 'test-execution-123', state: 'QUERY_STATE_PENDING', - }; + } - (fetch as jest.Mock).mockResolvedValueOnce({ + ;(fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, - }); + }) const result = await repository.executeQuery({ queryId: 12345, parameters: { param1: 'value1', param2: 42 }, - }); + }) - expect(fetch).toHaveBeenCalledWith( - 'https://api.dune.com/api/v1/query/12345/execute', - { - method: 'POST', - headers: { - 'X-DUNE-API-KEY': mockApiKey, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query_parameters: { param1: 'value1', param2: 42 }, - }), - } - ); + expect(fetch).toHaveBeenCalledWith('https://api.dune.com/api/v1/query/12345/execute', { + method: 'POST', + headers: { + 'X-DUNE-API-KEY': mockApiKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query_parameters: { param1: 'value1', param2: 42 }, + }), + }) - expect(result).toEqual(mockResponse); - }); + expect(result).toEqual(mockResponse) + }) it('should execute a query with performance parameter', async () => { const mockResponse: DuneExecutionResponse = { execution_id: 'test-execution-456', state: 'QUERY_STATE_PENDING', - }; + } - (fetch as jest.Mock).mockResolvedValueOnce({ + ;(fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, - }); + }) const result = await repository.executeQuery({ queryId: 12345, performance: 'large', - }); + }) - expect(fetch).toHaveBeenCalledWith( - 'https://api.dune.com/api/v1/query/12345/execute', - { - method: 'POST', - headers: { - 'X-DUNE-API-KEY': mockApiKey, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ performance: 'large' }), - } - ); + expect(fetch).toHaveBeenCalledWith('https://api.dune.com/api/v1/query/12345/execute', { + method: 'POST', + headers: { + 'X-DUNE-API-KEY': mockApiKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ performance: 'large' }), + }) - expect(result).toEqual(mockResponse); - }); + expect(result).toEqual(mockResponse) + }) it('should execute a query with both parameters and performance', async () => { const mockResponse: DuneExecutionResponse = { execution_id: 'test-execution-789', state: 'QUERY_STATE_PENDING', - }; + } - (fetch as jest.Mock).mockResolvedValueOnce({ + ;(fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, - }); + }) const result = await repository.executeQuery({ queryId: 12345, parameters: { param1: 'value1' }, performance: 'large', - }); + }) - expect(fetch).toHaveBeenCalledWith( - 'https://api.dune.com/api/v1/query/12345/execute', - { - method: 'POST', - headers: { - 'X-DUNE-API-KEY': mockApiKey, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query_parameters: { param1: 'value1' }, - performance: 'large', - }), - } - ); + expect(fetch).toHaveBeenCalledWith('https://api.dune.com/api/v1/query/12345/execute', { + method: 'POST', + headers: { + 'X-DUNE-API-KEY': mockApiKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query_parameters: { param1: 'value1' }, + performance: 'large', + }), + }) - expect(result).toEqual(mockResponse); - }); + expect(result).toEqual(mockResponse) + }) it('should execute a query without parameters', async () => { const mockResponse: DuneExecutionResponse = { execution_id: 'test-execution-456', state: 'QUERY_STATE_PENDING', - }; + } - (fetch as jest.Mock).mockResolvedValueOnce({ + ;(fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, - }); + }) const result = await repository.executeQuery({ queryId: 12345, - }); + }) - expect(fetch).toHaveBeenCalledWith( - 'https://api.dune.com/api/v1/query/12345/execute', - { - method: 'POST', - headers: { - 'X-DUNE-API-KEY': mockApiKey, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({}), - } - ); + expect(fetch).toHaveBeenCalledWith('https://api.dune.com/api/v1/query/12345/execute', { + method: 'POST', + headers: { + 'X-DUNE-API-KEY': mockApiKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }) - expect(result).toEqual(mockResponse); - }); + expect(result).toEqual(mockResponse) + }) it('should handle API errors', async () => { - (fetch as jest.Mock).mockResolvedValueOnce({ + ;(fetch as jest.Mock).mockResolvedValueOnce({ ok: false, status: 400, statusText: 'Bad Request', - }); + }) await expect( repository.executeQuery({ queryId: 12345, }) - ).rejects.toThrow('Dune API request failed: 400 Bad Request'); - }); - }); + ).rejects.toThrow('Dune API request failed: 400 Bad Request') + }) + }) describe('getExecutionResults', () => { it('should get execution results successfully', async () => { @@ -191,45 +179,42 @@ describe('DuneRepositoryImpl', () => { execution_time_millis: 500, }, }, - }; + } - (fetch as jest.Mock).mockResolvedValueOnce({ + ;(fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, - }); + }) const result = await repository.getExecutionResults({ executionId: 'test-execution-123', - }); + }) - expect(fetch).toHaveBeenCalledWith( - 'https://api.dune.com/api/v1/execution/test-execution-123/results', - { - headers: { - 'X-DUNE-API-KEY': mockApiKey, - }, - } - ); + expect(fetch).toHaveBeenCalledWith('https://api.dune.com/api/v1/execution/test-execution-123/results', { + headers: { + 'X-DUNE-API-KEY': mockApiKey, + }, + }) - expect(result).toEqual(mockResponse); - }); + expect(result).toEqual(mockResponse) + }) it('should handle API errors', async () => { - (fetch as jest.Mock).mockResolvedValueOnce({ + ;(fetch as jest.Mock).mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found', - }); + }) await expect( repository.getExecutionResults({ executionId: 'test-execution-123', }) - ).rejects.toThrow('Dune API request failed: 404 Not Found'); - }); - }); + ).rejects.toThrow('Dune API request failed: 404 Not Found') + }) + }) describe('waitForExecution', () => { it('should wait for execution to complete successfully', async () => { @@ -256,21 +241,21 @@ describe('DuneRepositoryImpl', () => { execution_time_millis: 500, }, }, - }; + } - (fetch as jest.Mock).mockResolvedValue({ + ;(fetch as jest.Mock).mockResolvedValue({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, - }); + }) const result = await repository.waitForExecution({ executionId: 'test-execution-123', - }); + }) - expect(result).toEqual(mockResponse); - }); + expect(result).toEqual(mockResponse) + }) it('should validate data with type assertion function', async () => { const validData = [ @@ -286,12 +271,10 @@ describe('DuneRepositoryImpl', () => { app_id: null, target: '0x1234567890123456789012345678901234567890', gas_limit: 250000, - app_hash: - '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', - tx_hash: - '0x9876543210987654321098765432109876543210987654321098765432109876', + app_hash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + tx_hash: '0x9876543210987654321098765432109876543210987654321098765432109876', }, - ]; + ] const mockResponse: DuneResultResponse = { execution_id: 'test-execution-123', @@ -344,44 +327,42 @@ describe('DuneRepositoryImpl', () => { execution_time_millis: 500, }, }, - }; + } - (fetch as jest.Mock).mockResolvedValue({ + ;(fetch as jest.Mock).mockResolvedValue({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, - }); + }) const isHookData = ( data: unknown ): data is { - environment: string; - block_time: string; - is_bridging: boolean; - success: boolean; - app_code: string; - destination_chain_id: number | null; - destination_token_address: string | null; - hook_type: string; - app_id: string | null; - target: string; - gas_limit: number; - app_hash: string; - tx_hash: string; + environment: string + block_time: string + is_bridging: boolean + success: boolean + app_code: string + destination_chain_id: number | null + destination_token_address: string | null + hook_type: string + app_id: string | null + target: string + gas_limit: number + app_hash: string + tx_hash: string } => { - return ( - typeof data === 'object' && data !== null && 'environment' in data - ); - }; + return typeof data === 'object' && data !== null && 'environment' in data + } const result = await repository.waitForExecution({ executionId: 'test-execution-123', typeAssertion: isHookData, - }); + }) - expect(result).toEqual(mockResponse); - }); + expect(result).toEqual(mockResponse) + }) it('should throw error when data validation fails', async () => { const invalidData = [ @@ -392,7 +373,7 @@ describe('DuneRepositoryImpl', () => { success: true, app_code: 'https://example.com/', }, - ]; + ] const mockResponse: DuneResultResponse = { execution_id: 'test-execution-123', @@ -406,20 +387,8 @@ describe('DuneRepositoryImpl', () => { result: { rows: invalidData, metadata: { - column_names: [ - 'environment', - 'block_time', - 'is_bridging', - 'success', - 'app_code', - ], - column_types: [ - 'varchar', - 'timestamp', - 'boolean', - 'boolean', - 'varchar', - ], + column_names: ['environment', 'block_time', 'is_bridging', 'success', 'app_code'], + column_types: ['varchar', 'timestamp', 'boolean', 'boolean', 'varchar'], row_count: 1, result_set_bytes: 100, total_row_count: 1, @@ -429,52 +398,50 @@ describe('DuneRepositoryImpl', () => { execution_time_millis: 500, }, }, - }; + } - (fetch as jest.Mock).mockResolvedValue({ + ;(fetch as jest.Mock).mockResolvedValue({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, - }); + }) const isHookData = ( data: unknown ): data is { - environment: string; - block_time: string; - is_bridging: boolean; - success: boolean; - app_code: string; - destination_chain_id: number | null; - destination_token_address: string | null; - hook_type: string; - app_id: string | null; - target: string; - gas_limit: number; - app_hash: string; - tx_hash: string; + environment: string + block_time: string + is_bridging: boolean + success: boolean + app_code: string + destination_chain_id: number | null + destination_token_address: string | null + hook_type: string + app_id: string | null + target: string + gas_limit: number + app_hash: string + tx_hash: string } => { - if (typeof data !== 'object' || data === null) return false; - const d = data as Record; + if (typeof data !== 'object' || data === null) return false + const d = data as Record return ( typeof d.environment === 'string' && typeof d.block_time === 'string' && typeof d.is_bridging === 'boolean' && typeof d.success === 'boolean' && typeof d.app_code === 'string' - ); - }; + ) + } await expect( repository.waitForExecution({ executionId: 'test-execution-123', typeAssertion: isHookData, }) - ).rejects.toThrow( - 'Data validation failed for execution test-execution-123' - ); - }); + ).rejects.toThrow('Data validation failed for execution test-execution-123') + }) it('should timeout if execution does not complete', async () => { const pendingResponse: DuneResultResponse = { @@ -500,23 +467,21 @@ describe('DuneRepositoryImpl', () => { execution_time_millis: 0, }, }, - }; + } - (fetch as jest.Mock).mockResolvedValue({ + ;(fetch as jest.Mock).mockResolvedValue({ ok: true, status: 200, statusText: 'OK', json: async () => pendingResponse, - }); + }) await expect( repository.waitForExecution({ executionId: 'test-execution-123', maxWaitTimeMs: 1000, // 1 second timeout }) - ).rejects.toThrow( - 'Execution test-execution-123 did not complete within 1000ms' - ); - }); - }); -}); + ).rejects.toThrow('Execution test-execution-123 did not complete within 1000ms') + }) + }) +}) diff --git a/libs/repositories/src/repos/DuneRepository/DuneRepository.ts b/libs/repositories/src/repos/DuneRepository/DuneRepository.ts index 83100fc1..41581dc2 100644 --- a/libs/repositories/src/repos/DuneRepository/DuneRepository.ts +++ b/libs/repositories/src/repos/DuneRepository/DuneRepository.ts @@ -1,88 +1,82 @@ -export const isDuneEnabled = !!process.env.DUNE_API_KEY; +export const isDuneEnabled = !!process.env.DUNE_API_KEY export interface DuneExecutionResponse { - execution_id: string; - state: string; + execution_id: string + state: string } export interface DuneResultResponse { - execution_id: string; - query_id: number; - is_execution_finished: boolean; - state: string; - submitted_at: string; - expires_at: string; - execution_started_at: string; - execution_ended_at: string; + execution_id: string + query_id: number + is_execution_finished: boolean + state: string + submitted_at: string + expires_at: string + execution_started_at: string + execution_ended_at: string result: { - rows: T[]; + rows: T[] metadata: { - column_names: string[]; - column_types: string[]; - row_count: number; - result_set_bytes: number; - total_row_count: number; - total_result_set_bytes: number; - datapoint_count: number; - pending_time_millis: number; - execution_time_millis: number; - }; - }; + column_names: string[] + column_types: string[] + row_count: number + result_set_bytes: number + total_row_count: number + total_result_set_bytes: number + datapoint_count: number + pending_time_millis: number + execution_time_millis: number + } + } } -export type PerformanceTier = 'medium' | 'large'; +export type PerformanceTier = 'medium' | 'large' export interface UploadCsvParams { - tableName: string; - data: string; - description?: string; - isPrivate?: boolean; + tableName: string + data: string + description?: string + isPrivate?: boolean } export interface UploadCsvResponse { - success: boolean; - message?: string; + success: boolean + message?: string } export interface ExecuteQueryParams { - queryId: number; - parameters?: Record; - performance?: PerformanceTier; + queryId: number + parameters?: Record + performance?: PerformanceTier } export interface GetExecutionResultsParams { - executionId: string; + executionId: string } export interface WaitForExecutionParams extends WithTypeAssertion { - executionId: string; - maxWaitTimeMs?: number; + executionId: string + maxWaitTimeMs?: number } export interface WithTypeAssertion { - typeAssertion?: (data: unknown) => data is T; + typeAssertion?: (data: unknown) => data is T } export interface GetQueryResultsParams extends WithTypeAssertion { - queryId: number; - limit?: number; - offset?: number; + queryId: number + limit?: number + offset?: number } export interface DuneRepository { - getQueryResults( - params: GetQueryResultsParams - ): Promise>; + getQueryResults(params: GetQueryResultsParams): Promise> - executeQuery(params: ExecuteQueryParams): Promise; + executeQuery(params: ExecuteQueryParams): Promise - getExecutionResults( - params: GetExecutionResultsParams - ): Promise>; + getExecutionResults(params: GetExecutionResultsParams): Promise> - waitForExecution( - params: WaitForExecutionParams - ): Promise>; + waitForExecution(params: WaitForExecutionParams): Promise> - uploadCsv(params: UploadCsvParams): Promise; + uploadCsv(params: UploadCsvParams): Promise } diff --git a/libs/repositories/src/repos/DuneRepository/DuneRepositoryImpl.ts b/libs/repositories/src/repos/DuneRepository/DuneRepositoryImpl.ts index c69139ce..d6aa340d 100644 --- a/libs/repositories/src/repos/DuneRepository/DuneRepositoryImpl.ts +++ b/libs/repositories/src/repos/DuneRepository/DuneRepositoryImpl.ts @@ -1,4 +1,4 @@ -import { logger } from '@cowprotocol/shared'; +import { logger } from '@cowprotocol/shared' import { DuneExecutionResponse, DuneRepository, @@ -9,139 +9,121 @@ import { UploadCsvParams, UploadCsvResponse, WaitForExecutionParams, -} from './DuneRepository'; +} from './DuneRepository' -export const duneRepositorySymbol = Symbol.for('DuneRepository'); +export const duneRepositorySymbol = Symbol.for('DuneRepository') -const POLL_TIME = 2000; -const MAX_WAIT_TIME = 300000; -const DEFAULT_LIMIT = 1000; +const POLL_TIME = 2000 +const MAX_WAIT_TIME = 300000 +const DEFAULT_LIMIT = 1000 export class DuneRepositoryImpl implements DuneRepository { - private readonly apiKey: string; - private readonly baseUrl: string; + private readonly apiKey: string + private readonly baseUrl: string constructor(apiKey: string, baseUrl = 'https://api.dune.com/api/v1') { - this.apiKey = apiKey; - this.baseUrl = baseUrl; + this.apiKey = apiKey + this.baseUrl = baseUrl } - async executeQuery( - params: ExecuteQueryParams - ): Promise { - const { queryId, parameters, performance } = params; + async executeQuery(params: ExecuteQueryParams): Promise { + const { queryId, parameters, performance } = params - const url = `/query/${queryId}/execute`; + const url = `/query/${queryId}/execute` const body = { ...(parameters ? { query_parameters: parameters } : {}), ...(performance ? { performance } : {}), - }; + } logger.info( - `Executing Dune query ${queryId} with parameters: ${JSON.stringify( - parameters - )} and performance: ${performance || 'medium'}` - ); + `Executing Dune query ${queryId} with parameters: ${JSON.stringify(parameters)} and performance: ${ + performance || 'medium' + }` + ) - return this.makeRequest(url, {}, body); + return this.makeRequest(url, {}, body) } - async getExecutionResults( - params: GetExecutionResultsParams - ): Promise> { - const { executionId } = params; - return this.makeRequest>( - `/execution/${executionId}/results` - ); + async getExecutionResults(params: GetExecutionResultsParams): Promise> { + const { executionId } = params + return this.makeRequest>(`/execution/${executionId}/results`) } - async waitForExecution( - params: WaitForExecutionParams - ): Promise> { - const { - executionId, - maxWaitTimeMs = MAX_WAIT_TIME, - typeAssertion, - } = params; - const startTime = Date.now(); + async waitForExecution(params: WaitForExecutionParams): Promise> { + const { executionId, maxWaitTimeMs = MAX_WAIT_TIME, typeAssertion } = params + const startTime = Date.now() while (Date.now() - startTime < maxWaitTimeMs) { - const result = await this.getExecutionResults({ executionId }); + const result = await this.getExecutionResults({ executionId }) if (result.is_execution_finished) { // If type assertion is provided, validate the data if (typeAssertion && result.result.rows.length > 0) { - const invalidRows: Array<{ index: number; data: unknown }> = []; + const invalidRows: Array<{ index: number; data: unknown }> = [] result.result.rows.forEach((row, index) => { if (!typeAssertion(row)) { - invalidRows.push({ index, data: row }); + invalidRows.push({ index, data: row }) } - }); - const isValid = invalidRows.length === 0; + }) + const isValid = invalidRows.length === 0 if (!isValid) { - const errorMessage = `Data validation failed for execution ${executionId}. Some rows do not match the expected type.`; - const debugInfo = `\nInvalid rows found: ${invalidRows.length}/${result.result.rows.length}`; + const errorMessage = `Data validation failed for execution ${executionId}. Some rows do not match the expected type.` + const debugInfo = `\nInvalid rows found: ${invalidRows.length}/${result.result.rows.length}` const exampleData = invalidRows.length > 0 - ? `\nExample invalid row (index ${ - invalidRows[0].index - }):\n${JSON.stringify(invalidRows[0].data, null, 2)}` - : ''; + ? `\nExample invalid row (index ${invalidRows[0].index}):\n${JSON.stringify( + invalidRows[0].data, + null, + 2 + )}` + : '' const expectedColumns = result.result.metadata?.column_names - ? `\nExpected columns from Dune: ${result.result.metadata.column_names.join( - ', ' - )}` - : ''; - - throw new Error( - errorMessage + debugInfo + exampleData + expectedColumns - ); + ? `\nExpected columns from Dune: ${result.result.metadata.column_names.join(', ')}` + : '' + + throw new Error(errorMessage + debugInfo + exampleData + expectedColumns) } } - return result; + return result } // Wait before polling again - await new Promise((resolve) => setTimeout(resolve, POLL_TIME)); + await new Promise((resolve) => setTimeout(resolve, POLL_TIME)) } - throw new Error( - `Execution ${executionId} did not complete within ${maxWaitTimeMs}ms` - ); + throw new Error(`Execution ${executionId} did not complete within ${maxWaitTimeMs}ms`) } - async getQueryResults( - params: GetQueryResultsParams - ): Promise> { - const { queryId, limit = DEFAULT_LIMIT, offset = 0 } = params; + async getQueryResults(params: GetQueryResultsParams): Promise> { + const { queryId, limit = DEFAULT_LIMIT, offset = 0 } = params - const queryParams = new URLSearchParams(); - queryParams.append('limit', limit.toString()); - queryParams.append('offset', offset.toString()); + const queryParams = new URLSearchParams() + queryParams.append('limit', limit.toString()) + queryParams.append('offset', offset.toString()) - const url = `/query/${queryId}/results?${queryParams.toString()}`; + const url = `/query/${queryId}/results?${queryParams.toString()}` return this.makeRequest>(url, { method: 'GET', - }); + }) } async uploadCsv(params: UploadCsvParams): Promise { - const { tableName, data, description, isPrivate } = params; + const { tableName, data, description, isPrivate } = params const payload: Record = { table_name: tableName, data, - }; + } if (description) { - payload.description = description; + payload.description = description } if (typeof isPrivate === 'boolean') { - payload.is_private = isPrivate; + payload.is_private = isPrivate } - return this.makeRequest('/uploads/csv', {}, payload); + return this.makeRequest('/uploads/csv', {}, payload) } private async makeRequest( @@ -149,10 +131,10 @@ export class DuneRepositoryImpl implements DuneRepository { options: RequestInit = {}, body?: Record ): Promise { - const url = `${this.baseUrl}${endpoint}`; + const url = `${this.baseUrl}${endpoint}` const defaultHeaders = { 'X-DUNE-API-KEY': this.apiKey, - }; + } const requestOptions: RequestInit = { ...options, @@ -160,49 +142,45 @@ export class DuneRepositoryImpl implements DuneRepository { ...defaultHeaders, ...options.headers, }, - }; + } if (body) { - requestOptions.method = 'POST'; - requestOptions.body = JSON.stringify(body); + requestOptions.method = 'POST' + requestOptions.body = JSON.stringify(body) requestOptions.headers = { ...requestOptions.headers, 'Content-Type': 'application/json', - }; + } } logger.debug( - `Making Dune API request: ${ - requestOptions.method || 'GET' - } ${url}${ + `Making Dune API request: ${requestOptions.method || 'GET'} ${url}${ requestOptions.body ? ` with body: ${requestOptions.body}` : '' }` - ); + ) - const response = await fetch(url, requestOptions); + const response = await fetch(url, requestOptions) - logger.info(`Dune API response: ${response.status} ${response.statusText}`); + logger.info(`Dune API response: ${response.status} ${response.statusText}`) if (!response.ok) { - let errorBody = 'Unable to read error body'; - const responseText = (response as Partial).text; + let errorBody = 'Unable to read error body' + const responseText = (response as Partial).text if (typeof responseText === 'function') { try { - errorBody = await responseText.call(response); + errorBody = await responseText.call(response) } catch { // keep default } } - throw new Error( - `Dune API request failed: ${response.status} ${response.statusText}. Body: ${errorBody}` - ); + throw new Error(`Dune API request failed: ${response.status} ${response.statusText}. Body: ${errorBody}`) } - const json = await response.json(); + const json = await response.json() if (logger.isLevelEnabled('debug')) { - logger.debug(`Dune API response:\n${JSON.stringify(json, null, 2)}`); + logger.debug(`Dune API response:\n${JSON.stringify(json, null, 2)}`) } - return json; + return json } } diff --git a/libs/repositories/src/repos/Erc20Repository/Erc20Repository.ts b/libs/repositories/src/repos/Erc20Repository/Erc20Repository.ts index d61b7540..a66c1c88 100644 --- a/libs/repositories/src/repos/Erc20Repository/Erc20Repository.ts +++ b/libs/repositories/src/repos/Erc20Repository/Erc20Repository.ts @@ -1,12 +1,12 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' -export const erc20RepositorySymbol = Symbol.for('Erc20Repository'); +export const erc20RepositorySymbol = Symbol.for('Erc20Repository') export interface Erc20 { - address: string; - name?: string; - symbol?: string; - decimals?: number; + address: string + name?: string + symbol?: string + decimals?: number } export interface Erc20Repository { @@ -15,5 +15,5 @@ export interface Erc20Repository { * @param chainId * @param tokenAddress */ - get(chainId: SupportedChainId, tokenAddress: string): Promise; + get(chainId: SupportedChainId, tokenAddress: string): Promise } diff --git a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryCache.spec.ts b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryCache.spec.ts index 8b3acffa..ea5798e5 100644 --- a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryCache.spec.ts +++ b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryCache.spec.ts @@ -1,129 +1,116 @@ -import { Erc20RepositoryCache } from './Erc20RepositoryCache'; -import { Erc20, Erc20Repository } from './Erc20Repository'; -import { CacheRepository } from '../CacheRepository/CacheRepository'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { Erc20RepositoryCache } from './Erc20RepositoryCache' +import { Erc20, Erc20Repository } from './Erc20Repository' +import { CacheRepository } from '../CacheRepository/CacheRepository' +import { SupportedChainId } from '@cowprotocol/cow-sdk' describe('Erc20RepositoryCache', () => { - let erc20RepositoryCache: Erc20RepositoryCache; - let mockProxy: jest.Mocked; - let mockCache: jest.Mocked; + let erc20RepositoryCache: Erc20RepositoryCache + let mockProxy: jest.Mocked + let mockCache: jest.Mocked - const chainId: SupportedChainId = 1; - const tokenAddress = '0xTokenAddress'; - const cacheName = 'erc20'; - const cacheTimeSeconds = 60; - const cacheKey = `repos:${cacheName}:get:${chainId}:${tokenAddress.toLocaleLowerCase()}`; + const chainId: SupportedChainId = 1 + const tokenAddress = '0xTokenAddress' + const cacheName = 'erc20' + const cacheTimeSeconds = 60 + const cacheKey = `repos:${cacheName}:get:${chainId}:${tokenAddress.toLocaleLowerCase()}` const erc20Data: Erc20 = { address: '0x1111111111111111111111111111111111111111', name: 'Token Name', symbol: 'TKN', decimals: 18, - }; + } beforeEach(() => { mockProxy = { get: jest.fn(), - } as unknown as jest.Mocked; + } as unknown as jest.Mocked mockCache = { get: jest.fn(), set: jest.fn(), - } as unknown as jest.Mocked; + } as unknown as jest.Mocked - erc20RepositoryCache = new Erc20RepositoryCache( - mockProxy, - mockCache, - cacheName, - cacheTimeSeconds - ); - }); + erc20RepositoryCache = new Erc20RepositoryCache(mockProxy, mockCache, cacheName, cacheTimeSeconds) + }) it('should return cached value if available', async () => { // GIVEN: Cached value is available - mockCache.get.mockResolvedValue(JSON.stringify(erc20Data)); + mockCache.get.mockResolvedValue(JSON.stringify(erc20Data)) // WHEN: get is called - const result = await erc20RepositoryCache.get(chainId, tokenAddress); + const result = await erc20RepositoryCache.get(chainId, tokenAddress) // THEN: The cache is called - expect(mockCache.get).toHaveBeenCalledWith(cacheKey); + expect(mockCache.get).toHaveBeenCalledWith(cacheKey) // THEN: The proxy is not called - expect(mockProxy.get).not.toHaveBeenCalled(); + expect(mockProxy.get).not.toHaveBeenCalled() // THEN: The cached value is returned - expect(result).toEqual(erc20Data); - }); + expect(result).toEqual(erc20Data) + }) it('should return null if cached value is NULL_VALUE', async () => { // GIVEN: Cached value is null - mockCache.get.mockResolvedValue('null'); + mockCache.get.mockResolvedValue('null') // WHEN: get is called - const result = await erc20RepositoryCache.get(chainId, tokenAddress); + const result = await erc20RepositoryCache.get(chainId, tokenAddress) // THEN: The cache is called - expect(mockCache.get).toHaveBeenCalledWith(cacheKey); + expect(mockCache.get).toHaveBeenCalledWith(cacheKey) // THEN: The proxy is not called - expect(mockProxy.get).not.toHaveBeenCalled(); + expect(mockProxy.get).not.toHaveBeenCalled() // THEN: null is returned - expect(result).toBeNull(); - }); + expect(result).toBeNull() + }) it('should fetch from proxy and cache the result if not cached', async () => { // GIVEN: Cached value is not available - mockCache.get.mockResolvedValue(null); + mockCache.get.mockResolvedValue(null) // GIVEN: Proxy returns the value - mockProxy.get.mockResolvedValue(erc20Data); + mockProxy.get.mockResolvedValue(erc20Data) // WHEN: get is called - const result = await erc20RepositoryCache.get(chainId, tokenAddress); + const result = await erc20RepositoryCache.get(chainId, tokenAddress) // THEN: The cache is called - expect(mockCache.get).toHaveBeenCalledWith(cacheKey); + expect(mockCache.get).toHaveBeenCalledWith(cacheKey) // THEN: The proxy is called - expect(mockProxy.get).toHaveBeenCalledWith(chainId, tokenAddress); + expect(mockProxy.get).toHaveBeenCalledWith(chainId, tokenAddress) // THEN: The value is cached - expect(mockCache.set).toHaveBeenCalledWith( - cacheKey, - JSON.stringify(erc20Data), - cacheTimeSeconds - ); + expect(mockCache.set).toHaveBeenCalledWith(cacheKey, JSON.stringify(erc20Data), cacheTimeSeconds) // THEN: The value is returned - expect(result).toEqual(erc20Data); - }); + expect(result).toEqual(erc20Data) + }) it('should cache NULL_VALUE if proxy returns null', async () => { // GIVEN: Cached value is not available - mockCache.get.mockResolvedValue(null); + mockCache.get.mockResolvedValue(null) // GIVEN: Proxy returns null - mockProxy.get.mockResolvedValue(null); + mockProxy.get.mockResolvedValue(null) // WHEN: get is called - const result = await erc20RepositoryCache.get(chainId, tokenAddress); + const result = await erc20RepositoryCache.get(chainId, tokenAddress) // THEN: The cache is called - expect(mockCache.get).toHaveBeenCalledWith(cacheKey); + expect(mockCache.get).toHaveBeenCalledWith(cacheKey) // THEN: The proxy is called - expect(mockProxy.get).toHaveBeenCalledWith(chainId, tokenAddress); + expect(mockProxy.get).toHaveBeenCalledWith(chainId, tokenAddress) // THEN: The null value is cached - expect(mockCache.set).toHaveBeenCalledWith( - cacheKey, - 'null', - cacheTimeSeconds - ); + expect(mockCache.set).toHaveBeenCalledWith(cacheKey, 'null', cacheTimeSeconds) // THEN: The result is null - expect(result).toBeNull(); - }); -}); + expect(result).toBeNull() + }) +}) diff --git a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryCache.ts b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryCache.ts index c8ee4feb..9448567d 100644 --- a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryCache.ts +++ b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryCache.ts @@ -1,14 +1,14 @@ -import { injectable } from 'inversify'; -import { Erc20, Erc20Repository } from './Erc20Repository'; -import { CacheRepository } from '../CacheRepository/CacheRepository'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { getCacheKey, PartialCacheKey } from '../../utils/cache'; +import { injectable } from 'inversify' +import { Erc20, Erc20Repository } from './Erc20Repository' +import { CacheRepository } from '../CacheRepository/CacheRepository' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { getCacheKey, PartialCacheKey } from '../../utils/cache' -const NULL_VALUE = 'null'; +const NULL_VALUE = 'null' @injectable() export class Erc20RepositoryCache implements Erc20Repository { - private baseCacheKey: PartialCacheKey[]; + private baseCacheKey: PartialCacheKey[] constructor( private proxy: Erc20Repository, @@ -16,32 +16,24 @@ export class Erc20RepositoryCache implements Erc20Repository { cacheName: string, private cacheTimeSeconds: number ) { - this.baseCacheKey = ['repos', cacheName]; + this.baseCacheKey = ['repos', cacheName] } - async get( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { + async get(chainId: SupportedChainId, tokenAddress: string): Promise { // Get cached value - const cacheKey = getCacheKey( - ...this.baseCacheKey, - 'get', - chainId, - tokenAddress - ); - const valueString = await this.cache.get(cacheKey); + const cacheKey = getCacheKey(...this.baseCacheKey, 'get', chainId, tokenAddress) + const valueString = await this.cache.get(cacheKey) if (valueString) { - return valueString === NULL_VALUE ? null : JSON.parse(valueString); + return valueString === NULL_VALUE ? null : JSON.parse(valueString) } // Get fresh value from proxy - const value = await this.proxy.get(chainId, tokenAddress); + const value = await this.proxy.get(chainId, tokenAddress) // Cache value - const cacheValue = value === null ? NULL_VALUE : JSON.stringify(value); - await this.cache.set(cacheKey, cacheValue, this.cacheTimeSeconds); + const cacheValue = value === null ? NULL_VALUE : JSON.stringify(value) + await this.cache.set(cacheKey, cacheValue, this.cacheTimeSeconds) - return value; + return value } } diff --git a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryFallback.spec.ts b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryFallback.spec.ts index aa83a77a..07bb7ce3 100644 --- a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryFallback.spec.ts +++ b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryFallback.spec.ts @@ -1,42 +1,42 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { Erc20, Erc20Repository } from './Erc20Repository'; -import { Erc20RepositoryFallback } from './Erc20RepositoryFallback'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { Erc20, Erc20Repository } from './Erc20Repository' +import { Erc20RepositoryFallback } from './Erc20RepositoryFallback' class RepoReturns implements Erc20Repository { constructor(private value: Erc20 | null) {} async get(): Promise { - return this.value; + return this.value } } describe('Erc20RepositoryFallback', () => { - const chainId = SupportedChainId.MAINNET; - const token = '0x0000000000000000000000000000000000000001'; + const chainId = SupportedChainId.MAINNET + const token = '0x0000000000000000000000000000000000000001' it('returns first non-null result', async () => { - const repo1 = new RepoReturns({ address: '0x1', name: 'ONE' }); - const repo2 = new RepoReturns({ address: '0x2', name: 'TWO' }); - const fallback = new Erc20RepositoryFallback([repo1, repo2]); + const repo1 = new RepoReturns({ address: '0x1', name: 'ONE' }) + const repo2 = new RepoReturns({ address: '0x2', name: 'TWO' }) + const fallback = new Erc20RepositoryFallback([repo1, repo2]) - const result = await fallback.get(chainId, token); - expect(result?.address).toEqual('0x1'); - }); + const result = await fallback.get(chainId, token) + expect(result?.address).toEqual('0x1') + }) it('falls back to second when first returns null', async () => { - const repo1 = new RepoReturns(null); - const repo2 = new RepoReturns({ address: '0x2', name: 'TWO' }); - const fallback = new Erc20RepositoryFallback([repo1, repo2]); + const repo1 = new RepoReturns(null) + const repo2 = new RepoReturns({ address: '0x2', name: 'TWO' }) + const fallback = new Erc20RepositoryFallback([repo1, repo2]) - const result = await fallback.get(chainId, token); - expect(result?.address).toEqual('0x2'); - }); + const result = await fallback.get(chainId, token) + expect(result?.address).toEqual('0x2') + }) it('returns null when all repositories return null', async () => { - const repo1 = new RepoReturns(null); - const repo2 = new RepoReturns(null); - const fallback = new Erc20RepositoryFallback([repo1, repo2]); - - const result = await fallback.get(chainId, token); - expect(result).toBeNull(); - }); -}); + const repo1 = new RepoReturns(null) + const repo2 = new RepoReturns(null) + const fallback = new Erc20RepositoryFallback([repo1, repo2]) + + const result = await fallback.get(chainId, token) + expect(result).toBeNull() + }) +}) diff --git a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryFallback.ts b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryFallback.ts index 82fcf952..587a8a71 100644 --- a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryFallback.ts +++ b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryFallback.ts @@ -1,21 +1,18 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { injectable } from 'inversify'; -import { Erc20, Erc20Repository } from './Erc20Repository'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { injectable } from 'inversify' +import { Erc20, Erc20Repository } from './Erc20Repository' @injectable() export class Erc20RepositoryFallback implements Erc20Repository { constructor(private readonly repositories: Erc20Repository[]) {} - async get( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { + async get(chainId: SupportedChainId, tokenAddress: string): Promise { for (const repository of this.repositories) { - const result = await repository.get(chainId, tokenAddress); + const result = await repository.get(chainId, tokenAddress) if (result !== null) { - return result; + return result } } - return null; + return null } } diff --git a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryNative.spec.ts b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryNative.spec.ts index 56bee88a..cbff3457 100644 --- a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryNative.spec.ts +++ b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryNative.spec.ts @@ -1,27 +1,23 @@ -import { - EVM_NATIVE_CURRENCY_ADDRESS, - SupportedChainId, - WRAPPED_NATIVE_CURRENCIES, -} from '@cowprotocol/cow-sdk'; -import { Erc20RepositoryNative } from './Erc20RepositoryNative'; +import { EVM_NATIVE_CURRENCY_ADDRESS, SupportedChainId, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk' +import { Erc20RepositoryNative } from './Erc20RepositoryNative' describe('Erc20RepositoryNative', () => { - const repo = new Erc20RepositoryNative(); + const repo = new Erc20RepositoryNative() it('returns wrapped native token info when given native sentinel address', async () => { - const chainId = SupportedChainId.MAINNET; - const result = await repo.get(chainId, EVM_NATIVE_CURRENCY_ADDRESS); + const chainId = SupportedChainId.MAINNET + const result = await repo.get(chainId, EVM_NATIVE_CURRENCY_ADDRESS) - expect(result).not.toBeNull(); - expect(result?.address).toEqual(EVM_NATIVE_CURRENCY_ADDRESS); - expect(result?.symbol).toEqual('ETH'); - expect(result?.decimals).toEqual(18); - }); + expect(result).not.toBeNull() + expect(result?.address).toEqual(EVM_NATIVE_CURRENCY_ADDRESS) + expect(result?.symbol).toEqual('ETH') + expect(result?.decimals).toEqual(18) + }) it('returns null for non-native addresses', async () => { - const chainId = SupportedChainId.MAINNET; - const nonNativeAddress = WRAPPED_NATIVE_CURRENCIES[chainId].address; // e.g. WETH address - const result = await repo.get(chainId, nonNativeAddress); - expect(result).toBeNull(); - }); -}); + const chainId = SupportedChainId.MAINNET + const nonNativeAddress = WRAPPED_NATIVE_CURRENCIES[chainId].address // e.g. WETH address + const result = await repo.get(chainId, nonNativeAddress) + expect(result).toBeNull() + }) +}) diff --git a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryNative.ts b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryNative.ts index 05479b40..05e63860 100644 --- a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryNative.ts +++ b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryNative.ts @@ -1,26 +1,19 @@ -import { - ALL_SUPPORTED_CHAINS_MAP, - EVM_NATIVE_CURRENCY_ADDRESS, - SupportedChainId, -} from '@cowprotocol/cow-sdk'; -import { injectable } from 'inversify'; -import { Erc20, Erc20Repository } from './Erc20Repository'; +import { ALL_SUPPORTED_CHAINS_MAP, EVM_NATIVE_CURRENCY_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' +import { injectable } from 'inversify' +import { Erc20, Erc20Repository } from './Erc20Repository' @injectable() export class Erc20RepositoryNative implements Erc20Repository { - async get( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { + async get(chainId: SupportedChainId, tokenAddress: string): Promise { if (tokenAddress.toLowerCase() !== EVM_NATIVE_CURRENCY_ADDRESS.toLowerCase()) { - return null; + return null } - const chainInfo = ALL_SUPPORTED_CHAINS_MAP[chainId]; + const chainInfo = ALL_SUPPORTED_CHAINS_MAP[chainId] if (!chainInfo?.nativeCurrency) { - return null; + return null } - return chainInfo.nativeCurrency; + return chainInfo.nativeCurrency } } diff --git a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryViem.spec.ts b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryViem.spec.ts index c98d990d..887b3e12 100644 --- a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryViem.spec.ts +++ b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryViem.spec.ts @@ -1,31 +1,31 @@ -import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk'; -import { erc20Abi, PublicClient } from 'viem'; -import { Erc20 } from './Erc20Repository'; -import { Erc20RepositoryViem } from './Erc20RepositoryViem'; +import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' +import { erc20Abi, PublicClient } from 'viem' +import { Erc20 } from './Erc20Repository' +import { Erc20RepositoryViem } from './Erc20RepositoryViem' -const multicallMock = jest.fn(); +const multicallMock = jest.fn() // Mock implementation for PublicClient const mockPublicClient: PublicClient = { multicall: multicallMock, // Add other methods of PublicClient if needed -} as unknown as jest.Mocked; +} as unknown as jest.Mocked -export default mockPublicClient; +export default mockPublicClient describe('Erc20RepositoryViem', () => { - let erc20RepositoryViem: Erc20RepositoryViem; + let erc20RepositoryViem: Erc20RepositoryViem - const chainId = SupportedChainId.MAINNET; - const tokenAddress = '0x1111111111111111111111111111111111111111'; - const error = new Error('Multicall error'); + const chainId = SupportedChainId.MAINNET + const tokenAddress = '0x1111111111111111111111111111111111111111' + const error = new Error('Multicall error') const erc20: Erc20 = { address: tokenAddress, name: 'Token Name', symbol: 'TKN', decimals: 18, - }; + } const expectedMulticallParams = { contracts: [ { address: tokenAddress, abi: erc20Abi, functionName: 'totalSupply' }, @@ -33,13 +33,12 @@ describe('Erc20RepositoryViem', () => { { address: tokenAddress, abi: erc20Abi, functionName: 'symbol' }, { address: tokenAddress, abi: erc20Abi, functionName: 'decimals' }, ], - }; - const viemClients: Record = - mapSupportedNetworks(mockPublicClient); + } + const viemClients: Record = mapSupportedNetworks(mockPublicClient) beforeEach(() => { - erc20RepositoryViem = new Erc20RepositoryViem(viemClients); - }); + erc20RepositoryViem = new Erc20RepositoryViem(viemClients) + }) it('should return null if the address has no totalSupply (is not a ERC20)', async () => { // GIVEN: The address is not a ERC20. The multicall fails for totalSupply (and all other calls) @@ -48,19 +47,17 @@ describe('Erc20RepositoryViem', () => { { error, status: 'failure' }, // name { error, status: 'failure' }, // symbol { error, status: 'failure' }, // decimals - ]); + ]) // WHEN: get is called - const result = await erc20RepositoryViem.get(chainId, tokenAddress); + const result = await erc20RepositoryViem.get(chainId, tokenAddress) // THEN: Multicall called with the correct parameters - expect(viemClients[chainId].multicall).toHaveBeenCalledWith( - expectedMulticallParams - ); + expect(viemClients[chainId].multicall).toHaveBeenCalledWith(expectedMulticallParams) // THEN: The result is null - expect(result).toBeNull(); - }); + expect(result).toBeNull() + }) it('should return Erc20 token details if address is an ERC20', async () => { // GIVEN: The address is a ERC20, but has no symbol or any other details @@ -69,15 +66,13 @@ describe('Erc20RepositoryViem', () => { { error, status: 'failure' }, // name { error, status: 'failure' }, // symbol { error, status: 'failure' }, // decimals - ]); + ]) // WHEN: get is called - const result = await erc20RepositoryViem.get(chainId, tokenAddress); + const result = await erc20RepositoryViem.get(chainId, tokenAddress) // THEN: Multicall called with the correct parameters - expect(viemClients[chainId].multicall).toHaveBeenCalledWith( - expectedMulticallParams - ); + expect(viemClients[chainId].multicall).toHaveBeenCalledWith(expectedMulticallParams) // THEN: The result is the token details expect(result).toEqual({ @@ -85,8 +80,8 @@ describe('Erc20RepositoryViem', () => { name: undefined, symbol: undefined, decimals: undefined, - }); - }); + }) + }) it('should return symbol if its the only optional method implemented', async () => { // GIVEN: The address is a ERC20, but has no symbol or any other details @@ -95,15 +90,13 @@ describe('Erc20RepositoryViem', () => { { error, status: 'failure' }, // name { result: erc20.symbol, status: 'success' }, // symbol { error, status: 'failure' }, // decimals - ]); + ]) // WHEN: get is called - const result = await erc20RepositoryViem.get(chainId, tokenAddress); + const result = await erc20RepositoryViem.get(chainId, tokenAddress) // THEN: Multicall called with the correct parameters - expect(viemClients[chainId].multicall).toHaveBeenCalledWith( - expectedMulticallParams - ); + expect(viemClients[chainId].multicall).toHaveBeenCalledWith(expectedMulticallParams) // THEN: The result is the token details expect(result).toEqual({ @@ -111,8 +104,8 @@ describe('Erc20RepositoryViem', () => { symbol: erc20.symbol, name: undefined, decimals: undefined, - }); - }); + }) + }) it('should return all ERC20 fields', async () => { // GIVEN: The address is a ERC20, but has no symbol or any other details @@ -121,33 +114,29 @@ describe('Erc20RepositoryViem', () => { { result: erc20.name, status: 'success' }, // name { result: erc20.symbol, status: 'success' }, // symbol { result: erc20.decimals, status: 'success' }, // decimals - ]); + ]) // WHEN: get is called - const result = await erc20RepositoryViem.get(chainId, tokenAddress); + const result = await erc20RepositoryViem.get(chainId, tokenAddress) // THEN: Multicall called with the correct parameters - expect(viemClients[chainId].multicall).toHaveBeenCalledWith( - expectedMulticallParams - ); + expect(viemClients[chainId].multicall).toHaveBeenCalledWith(expectedMulticallParams) // THEN: The result is the token details - expect(result).toEqual(erc20); - }); + expect(result).toEqual(erc20) + }) it('should handle multicall errors', async () => { // GIVEN: The multicall throws an error - multicallMock.mockRejectedValue(error); + multicallMock.mockRejectedValue(error) // WHEN: get is called - const resultPromise = erc20RepositoryViem.get(chainId, tokenAddress); + const resultPromise = erc20RepositoryViem.get(chainId, tokenAddress) // THEN: Multicall called with the correct parameters - expect(viemClients[chainId].multicall).toHaveBeenCalledWith( - expectedMulticallParams - ); + expect(viemClients[chainId].multicall).toHaveBeenCalledWith(expectedMulticallParams) // THEN: The result should rethrow the original error - expect(resultPromise).rejects.toThrowError(error); - }); -}); + expect(resultPromise).rejects.toThrowError(error) + }) +}) diff --git a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryViem.ts b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryViem.ts index 4c82591a..b506474e 100644 --- a/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryViem.ts +++ b/libs/repositories/src/repos/Erc20Repository/Erc20RepositoryViem.ts @@ -1,65 +1,58 @@ -import { injectable } from 'inversify'; -import { Erc20, Erc20Repository } from './Erc20Repository'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { erc20Abi, getAddress, PublicClient } from 'viem'; +import { injectable } from 'inversify' +import { Erc20, Erc20Repository } from './Erc20Repository' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { erc20Abi, getAddress, PublicClient } from 'viem' @injectable() export class Erc20RepositoryViem implements Erc20Repository { constructor(private viemClients: Record) {} - async get( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { - const viemClient = this.viemClients[chainId]; - const tokenAddressHex = getAddress(tokenAddress); + async get(chainId: SupportedChainId, tokenAddress: string): Promise { + const viemClient = this.viemClients[chainId] + const tokenAddressHex = getAddress(tokenAddress) const ercTokenParams = { address: tokenAddressHex, abi: erc20Abi, - }; + } - const [totalSupplyResult, nameResult, symbolResult, decimalsResult] = - await viemClient.multicall({ - contracts: [ - { - ...ercTokenParams, - functionName: 'totalSupply', - }, - { - ...ercTokenParams, - functionName: 'name', - }, - { - ...ercTokenParams, - functionName: 'symbol', - }, - { - ...ercTokenParams, - functionName: 'decimals', - }, - ], - }); + const [totalSupplyResult, nameResult, symbolResult, decimalsResult] = await viemClient.multicall({ + contracts: [ + { + ...ercTokenParams, + functionName: 'totalSupply', + }, + { + ...ercTokenParams, + functionName: 'name', + }, + { + ...ercTokenParams, + functionName: 'symbol', + }, + { + ...ercTokenParams, + functionName: 'decimals', + }, + ], + }) // If the total supply fails, the token is not an ERC20 if (totalSupplyResult.status === 'failure') { - return null; + return null } - const name = - nameResult.status === 'success' ? nameResult.result : undefined; + const name = nameResult.status === 'success' ? nameResult.result : undefined - const symbol = - symbolResult.status === 'success' ? symbolResult.result : undefined; + const symbol = symbolResult.status === 'success' ? symbolResult.result : undefined - const decimals = - decimalsResult.status === 'success' ? decimalsResult.result : undefined; + const decimals = decimalsResult.status === 'success' ? decimalsResult.result : undefined return { address: tokenAddress, name, symbol, decimals, - }; + } } } diff --git a/libs/repositories/src/repos/ExpiredOrdersRepository/ExpiredOrdersRepository.ts b/libs/repositories/src/repos/ExpiredOrdersRepository/ExpiredOrdersRepository.ts index 23ff3a0e..cf91268d 100644 --- a/libs/repositories/src/repos/ExpiredOrdersRepository/ExpiredOrdersRepository.ts +++ b/libs/repositories/src/repos/ExpiredOrdersRepository/ExpiredOrdersRepository.ts @@ -1,34 +1,34 @@ -import type { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk'; +import type { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' export interface ExpiredOrdersContext { - chainId: SupportedChainId; - accounts: string[]; - nowTimestamp: number; - lastCheckTimestamp: number; + chainId: SupportedChainId + accounts: string[] + nowTimestamp: number + lastCheckTimestamp: number } export interface ExpiredOrder { - uid: T; - owner: T; - valid_to: number; - sell_token: T; - buy_token: T; - sell_amount: string; - buy_amount: string; - kind: OrderKind; + uid: T + owner: T + valid_to: number + sell_token: T + buy_token: T + sell_amount: string + buy_amount: string + kind: OrderKind } export interface ParsedExpiredOrder { - uid: string; - owner: string; - validTo: number; - sellTokenAddress: string; - buyTokenAddress: string; - sellAmount: string; - buyAmount: string; - kind: OrderKind; + uid: string + owner: string + validTo: number + sellTokenAddress: string + buyTokenAddress: string + sellAmount: string + buyAmount: string + kind: OrderKind } export interface ExpiredOrdersRepository { - fetchExpiredOrdersForAccounts(context: ExpiredOrdersContext): Promise; + fetchExpiredOrdersForAccounts(context: ExpiredOrdersContext): Promise } diff --git a/libs/repositories/src/repos/ExpiredOrdersRepository/ExpiredOrdersRepositoryPostgres.ts b/libs/repositories/src/repos/ExpiredOrdersRepository/ExpiredOrdersRepositoryPostgres.ts index 4bc90ca5..6f9f758f 100644 --- a/libs/repositories/src/repos/ExpiredOrdersRepository/ExpiredOrdersRepositoryPostgres.ts +++ b/libs/repositories/src/repos/ExpiredOrdersRepository/ExpiredOrdersRepositoryPostgres.ts @@ -1,31 +1,31 @@ -import { Pool, QueryResult } from 'pg'; +import { Pool, QueryResult } from 'pg' import { ExpiredOrder, ExpiredOrdersContext, ExpiredOrdersRepository, - ParsedExpiredOrder -} from './ExpiredOrdersRepository'; -import { getOrderBookDbPool } from '../../datasources/orderBookDbPool'; -import { bytesToHexString } from '../../utils/bytesUtils'; -import { parseExpiredOrder } from './expiredOrdersUtils'; + ParsedExpiredOrder, +} from './ExpiredOrdersRepository' +import { getOrderBookDbPool } from '../../datasources/orderBookDbPool' +import { bytesToHexString } from '../../utils/bytesUtils' +import { parseExpiredOrder } from './expiredOrdersUtils' -const LIMIT = 1000; -const ORDER_EXPIRATION_THRESHOLD = 60; // 1 minute +const LIMIT = 1000 +const ORDER_EXPIRATION_THRESHOLD = 60 // 1 minute export class ExpiredOrdersRepositoryPostgres implements ExpiredOrdersRepository { async fetchExpiredOrdersForAccounts(context: ExpiredOrdersContext): Promise { - const { chainId, accounts } = context; + const { chainId, accounts } = context - const prodDb = getOrderBookDbPool('prod', chainId); - const barnDb = getOrderBookDbPool('barn', chainId); + const prodDb = getOrderBookDbPool('prod', chainId) + const barnDb = getOrderBookDbPool('barn', chainId) - const prodExpiredOrdersResult = await this.fetchExpiredOrdersFromDb(context, prodDb); - const barnExpiredOrdersResult = await this.fetchExpiredOrdersFromDb(context, barnDb); + const prodExpiredOrdersResult = await this.fetchExpiredOrdersFromDb(context, prodDb) + const barnExpiredOrdersResult = await this.fetchExpiredOrdersFromDb(context, barnDb) - const allExpiredOrders = [...(prodExpiredOrdersResult?.rows || []), ...(barnExpiredOrdersResult?.rows || [])]; + const allExpiredOrders = [...(prodExpiredOrdersResult?.rows || []), ...(barnExpiredOrdersResult?.rows || [])] const accountsMap = accounts.reduce>((acc, account) => { - acc.add(account.toLowerCase()); + acc.add(account.toLowerCase()) return acc }, new Set()) @@ -38,10 +38,13 @@ export class ExpiredOrdersRepositoryPostgres implements ExpiredOrdersRepository }, []) } - private async fetchExpiredOrdersFromDb(context: ExpiredOrdersContext, db: Pool): Promise | null> { - const { accounts, lastCheckTimestamp, nowTimestamp } = context; + private async fetchExpiredOrdersFromDb( + context: ExpiredOrdersContext, + db: Pool + ): Promise | null> { + const { accounts, lastCheckTimestamp, nowTimestamp } = context - if (accounts.length === 0) return null; + if (accounts.length === 0) return null const query = ` WITH filtered_orders AS ( @@ -84,10 +87,6 @@ export class ExpiredOrdersRepositoryPostgres implements ExpiredOrdersRepository LIMIT $3; ` - return db.query(query, [ - lastCheckTimestamp, - nowTimestamp - ORDER_EXPIRATION_THRESHOLD, - LIMIT - ]); + return db.query(query, [lastCheckTimestamp, nowTimestamp - ORDER_EXPIRATION_THRESHOLD, LIMIT]) } } diff --git a/libs/repositories/src/repos/ExpiredOrdersRepository/expiredOrdersUtils.ts b/libs/repositories/src/repos/ExpiredOrdersRepository/expiredOrdersUtils.ts index 7a03df47..020f5da2 100644 --- a/libs/repositories/src/repos/ExpiredOrdersRepository/expiredOrdersUtils.ts +++ b/libs/repositories/src/repos/ExpiredOrdersRepository/expiredOrdersUtils.ts @@ -1,5 +1,5 @@ -import { ExpiredOrder, ParsedExpiredOrder } from './ExpiredOrdersRepository'; -import { bytesToHexString } from '../../utils/bytesUtils'; +import { ExpiredOrder, ParsedExpiredOrder } from './ExpiredOrdersRepository' +import { bytesToHexString } from '../../utils/bytesUtils' export function parseExpiredOrder(order: ExpiredOrder): ParsedExpiredOrder { return { @@ -8,8 +8,8 @@ export function parseExpiredOrder(order: ExpiredOrder): ParsedExpiredOrder { validTo: order.valid_to, owner: bytesToHexString(order.owner), sellTokenAddress: bytesToHexString(order.sell_token), - sellAmount: (order.sell_amount), + sellAmount: order.sell_amount, buyTokenAddress: bytesToHexString(order.buy_token), - buyAmount: (order.buy_amount), + buyAmount: order.buy_amount, } -} \ No newline at end of file +} diff --git a/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepository.ts b/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepository.ts index 7beeecef..33ae1d70 100644 --- a/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepository.ts +++ b/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepository.ts @@ -1,4 +1,4 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' /** * Indexer state. @@ -8,14 +8,14 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk'; * The state is stored in a JSON object, so each indexer can define its own state schema. */ export type IndexerState = { - key: string; - chainId: number | null; - state: T; - createdAt: Date; - updatedAt: Date; -}; + key: string + chainId: number | null + state: T + createdAt: Date + updatedAt: Date +} -export type IndexerStateValue = Record; +export type IndexerStateValue = Record /** * Indexer state repository. @@ -23,14 +23,7 @@ export type IndexerStateValue = Record; * This repository allows to store and retrieve the state of an indexer. */ export interface IndexerStateRepository { - get( - key: string, - chainId?: SupportedChainId - ): Promise | null>; + get(key: string, chainId?: SupportedChainId): Promise | null> - upsert( - key: string, - state: T, - chainId?: number - ): Promise; + upsert(key: string, state: T, chainId?: number): Promise } diff --git a/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepositoryOrm.ts b/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepositoryOrm.ts index e484779c..c34f2ff2 100644 --- a/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepositoryOrm.ts +++ b/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepositoryOrm.ts @@ -1,40 +1,33 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { DataSource, Repository, FindOptionsWhere } from 'typeorm'; -import { IndexerState as IndexerStateEntity } from '../../database/IndexerState.entity'; -import { injectable } from 'inversify'; -import { - IndexerState, - IndexerStateRepository, - IndexerStateValue, -} from './IndexerStateRepository'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { DataSource, Repository, FindOptionsWhere } from 'typeorm' +import { IndexerState as IndexerStateEntity } from '../../database/IndexerState.entity' +import { injectable } from 'inversify' +import { IndexerState, IndexerStateRepository, IndexerStateValue } from './IndexerStateRepository' @injectable() export class IndexerStateRepositoryTypeOrm implements IndexerStateRepository { - private repository: Repository; + private repository: Repository constructor(dataSource: DataSource) { - this.repository = dataSource.getRepository(IndexerStateEntity); + this.repository = dataSource.getRepository(IndexerStateEntity) } /** * Get indexer state by key and optional chainId */ - async get( - key: string, - chainId?: SupportedChainId - ): Promise | null> { + async get(key: string, chainId?: SupportedChainId): Promise | null> { const result = await this.repository.findOne({ where: { key, chainId: chainId === undefined ? null : chainId, } as FindOptionsWhere, - }); + }) if (!result) { - return null; + return null } - const { state, createdAt, updatedAt } = result; + const { state, createdAt, updatedAt } = result return { key, @@ -43,23 +36,19 @@ export class IndexerStateRepositoryTypeOrm implements IndexerStateRepository { state: state as any as T, createdAt, updatedAt, - }; + } } /** * Update or insert indexer state */ - async upsert( - key: string, - state: T, - chainId?: number - ): Promise { + async upsert(key: string, state: T, chainId?: number): Promise { const entity = this.repository.create({ key, chainId: chainId ?? null, state: state as unknown as Record, - }); + }) - await this.repository.save(entity); + await this.repository.save(entity) } } diff --git a/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepositoryPostgres.ts b/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepositoryPostgres.ts index e3c1ff02..53b95301 100644 --- a/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepositoryPostgres.ts +++ b/libs/repositories/src/repos/IndexerStateRepository/IndexerStateRepositoryPostgres.ts @@ -1,10 +1,6 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { Pool } from 'pg'; -import { - IndexerState, - IndexerStateRepository, - IndexerStateValue, -} from './IndexerStateRepository'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { Pool } from 'pg' +import { IndexerState, IndexerStateRepository, IndexerStateValue } from './IndexerStateRepository' export class IndexerStateRepositoryPostgres implements IndexerStateRepository { constructor(readonly db: Pool) {} @@ -12,21 +8,18 @@ export class IndexerStateRepositoryPostgres implements IndexerStateRepository { /** * Get indexer state by key and optional chainId */ - async get( - key: string, - chainId?: SupportedChainId - ): Promise | null> { + async get(key: string, chainId?: SupportedChainId): Promise | null> { const query = ` SELECT state FROM indexer_state WHERE key = $1 ${chainId !== undefined ? 'AND chain_id = $2' : ''} LIMIT 1 - `; - const params = chainId !== undefined ? [key, chainId] : [key]; + ` + const params = chainId !== undefined ? [key, chainId] : [key] - const result = await this.db.query(query, params); - return result.rows[0] || null; + const result = await this.db.query(query, params) + return result.rows[0] || null } /** @@ -38,8 +31,8 @@ export class IndexerStateRepositoryPostgres implements IndexerStateRepository { VALUES ($1, $2, $3) ON CONFLICT (key, chain_id) DO UPDATE SET state = $3 - `; + ` - await this.db.query(query, [key, chainId, state]); + await this.db.query(query, [key, chainId, state]) } } diff --git a/libs/repositories/src/repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepository.ts b/libs/repositories/src/repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepository.ts index 81a9d94d..5b4d5bea 100644 --- a/libs/repositories/src/repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepository.ts +++ b/libs/repositories/src/repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepository.ts @@ -1,5 +1,5 @@ -import type { SupportedChainId } from '@cowprotocol/cow-sdk'; +import type { SupportedChainId } from '@cowprotocol/cow-sdk' export interface OnChainPlacedOrdersRepository { - getAccountsForOrders(chainId: SupportedChainId, uids: string[]): Promise<{[account: string]: string[]}>; + getAccountsForOrders(chainId: SupportedChainId, uids: string[]): Promise<{ [account: string]: string[] }> } diff --git a/libs/repositories/src/repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepositoryPostgres.ts b/libs/repositories/src/repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepositoryPostgres.ts index 9149b740..1c4c1c58 100644 --- a/libs/repositories/src/repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepositoryPostgres.ts +++ b/libs/repositories/src/repos/OnchainPlacedOrdersRepository/OnChainPlacedOrdersRepositoryPostgres.ts @@ -1,57 +1,60 @@ -import { Pool, QueryResult } from 'pg'; -import { OnChainPlacedOrdersRepository } from './OnChainPlacedOrdersRepository'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { getOrderBookDbPool } from '../../datasources/orderBookDbPool'; -import { bytesToHexString, hexStringToBytes } from '../../utils/bytesUtils'; +import { Pool, QueryResult } from 'pg' +import { OnChainPlacedOrdersRepository } from './OnChainPlacedOrdersRepository' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { getOrderBookDbPool } from '../../datasources/orderBookDbPool' +import { bytesToHexString, hexStringToBytes } from '../../utils/bytesUtils' interface OnChainPlacedOrder { - sender: Buffer; - uid: Buffer; + sender: Buffer + uid: Buffer } type AccountsForOrders = { [account: string]: string[] } export class OnChainPlacedOrdersRepositoryPostgres implements OnChainPlacedOrdersRepository { async getAccountsForOrders(chainId: SupportedChainId, uids: string[]): Promise { - const orders: OnChainPlacedOrder[] = []; + const orders: OnChainPlacedOrder[] = [] - const prodDb = getOrderBookDbPool('prod', chainId); - const prodOrders = await this.fetchOnChainPlacedOrdersFromDb(uids, prodDb); + const prodDb = getOrderBookDbPool('prod', chainId) + const prodOrders = await this.fetchOnChainPlacedOrdersFromDb(uids, prodDb) if (!prodOrders?.rowCount) { - const barnDb = getOrderBookDbPool('barn', chainId); - const barnOrders = await this.fetchOnChainPlacedOrdersFromDb(uids, barnDb); + const barnDb = getOrderBookDbPool('barn', chainId) + const barnOrders = await this.fetchOnChainPlacedOrdersFromDb(uids, barnDb) if (barnOrders?.rowCount) { - orders.push(...barnOrders.rows); + orders.push(...barnOrders.rows) } } else { - orders.push(...prodOrders.rows); + orders.push(...prodOrders.rows) } return orders.reduce((acc, row) => { - const owner = bytesToHexString(row.sender).toLowerCase(); + const owner = bytesToHexString(row.sender).toLowerCase() - acc[owner] = acc[owner] || []; + acc[owner] = acc[owner] || [] - acc[owner].push(bytesToHexString(row.uid).toLowerCase()); + acc[owner].push(bytesToHexString(row.uid).toLowerCase()) - return acc; - }, {}); + return acc + }, {}) } - private async fetchOnChainPlacedOrdersFromDb(uids: string[], db: Pool): Promise | null> { + private async fetchOnChainPlacedOrdersFromDb( + uids: string[], + db: Pool + ): Promise | null> { if (uids.length === 0) return null const query = ` SELECT sender, uid FROM onchain_placed_orders WHERE uid = ANY($1) LIMIT 1000 - `; + ` // TODO: do we need batching for uids? What if there are 20000000 uids? - const params = uids.map(hexStringToBytes); + const params = uids.map(hexStringToBytes) - return db.query(query, [params]); + return db.query(query, [params]) } } diff --git a/libs/repositories/src/repos/OrdersAppDataRepository/OrdersAppDataRepository.ts b/libs/repositories/src/repos/OrdersAppDataRepository/OrdersAppDataRepository.ts index 8d79d4ce..f69999a6 100644 --- a/libs/repositories/src/repos/OrdersAppDataRepository/OrdersAppDataRepository.ts +++ b/libs/repositories/src/repos/OrdersAppDataRepository/OrdersAppDataRepository.ts @@ -1,5 +1,5 @@ -import { AnyAppDataDocVersion, SupportedChainId } from '@cowprotocol/cow-sdk'; +import { AnyAppDataDocVersion, SupportedChainId } from '@cowprotocol/cow-sdk' export interface OrdersAppDataRepository { - getAppDataForOrders(chainId: SupportedChainId, uids: string[]): Promise>; + getAppDataForOrders(chainId: SupportedChainId, uids: string[]): Promise> } diff --git a/libs/repositories/src/repos/OrdersAppDataRepository/OrdersAppDataRepositoryPostgres.ts b/libs/repositories/src/repos/OrdersAppDataRepository/OrdersAppDataRepositoryPostgres.ts index 1cfe37cf..6cee9548 100644 --- a/libs/repositories/src/repos/OrdersAppDataRepository/OrdersAppDataRepositoryPostgres.ts +++ b/libs/repositories/src/repos/OrdersAppDataRepository/OrdersAppDataRepositoryPostgres.ts @@ -1,118 +1,119 @@ -import { Pool } from 'pg'; -import { OrdersAppDataRepository } from './OrdersAppDataRepository'; -import { AnyAppDataDocVersion, SupportedChainId } from '@cowprotocol/cow-sdk'; -import { getOrderBookDbPool } from '../../datasources/orderBookDbPool'; -import { bytesToHexString, hexStringToBytes } from '../../utils/bytesUtils'; -import { logger } from '@cowprotocol/shared'; -import { chunkArray } from '../../utils/chunkArray'; +import { Pool } from 'pg' +import { OrdersAppDataRepository } from './OrdersAppDataRepository' +import { AnyAppDataDocVersion, SupportedChainId } from '@cowprotocol/cow-sdk' +import { getOrderBookDbPool } from '../../datasources/orderBookDbPool' +import { bytesToHexString, hexStringToBytes } from '../../utils/bytesUtils' +import { logger } from '@cowprotocol/shared' +import { chunkArray } from '../../utils/chunkArray' -const LIMIT = 100; +const LIMIT = 100 -type UidToAppData = Map; +type UidToAppData = Map interface AppDataFromDbResult { - uidToAppData: UidToAppData; - missingAppDataUids: string[]; + uidToAppData: UidToAppData + missingAppDataUids: string[] } -const uidToAppDataCache = new Map(); +const uidToAppDataCache = new Map() export class OrdersAppDataRepositoryPostgres implements OrdersAppDataRepository { async getAppDataForOrders(chainId: SupportedChainId, uids: string[]): Promise { const cachedResults = uids.reduce((acc: UidToAppData, _uid: string) => { - const uid = _uid.toLowerCase(); - const cached = uidToAppDataCache.get(uid); + const uid = _uid.toLowerCase() + const cached = uidToAppDataCache.get(uid) - if (cached) acc.set(uid, cached); + if (cached) acc.set(uid, cached) - return acc; - }, new Map()); + return acc + }, new Map()) - if (cachedResults.size === uids.length) return cachedResults; + if (cachedResults.size === uids.length) return cachedResults - const prodDb = getOrderBookDbPool('prod', chainId); + const prodDb = getOrderBookDbPool('prod', chainId) - const uidsToFetch = uids.filter(uid => !cachedResults.has(uid.toLowerCase())); - const prodChunks = chunkArray(uidsToFetch, LIMIT); + const uidsToFetch = uids.filter((uid) => !cachedResults.has(uid.toLowerCase())) + const prodChunks = chunkArray(uidsToFetch, LIMIT) - const prodResults = await Promise.all(prodChunks.map(chunk => { - return this.fetchAppDataFromDb(chunk, prodDb); - })); + const prodResults = await Promise.all( + prodChunks.map((chunk) => { + return this.fetchAppDataFromDb(chunk, prodDb) + }) + ) const missingAppDataUidsOnProd = prodResults.reduce((acc, result) => { - acc.push(...result.missingAppDataUids); + acc.push(...result.missingAppDataUids) - return acc; - }, []); + return acc + }, []) const prodUidToAppData = prodResults.reduce((acc, result) => { - return this.mergeUidToAppDataMaps(acc, result.uidToAppData); - }, new Map()); + return this.mergeUidToAppDataMaps(acc, result.uidToAppData) + }, new Map()) - const totalUidToAppData = this.mergeUidToAppDataMaps(cachedResults, prodUidToAppData); + const totalUidToAppData = this.mergeUidToAppDataMaps(cachedResults, prodUidToAppData) if (!missingAppDataUidsOnProd.length) { - return totalUidToAppData; + return totalUidToAppData } - const barnDb = getOrderBookDbPool('barn', chainId); - const barnChunks = chunkArray(missingAppDataUidsOnProd, LIMIT); + const barnDb = getOrderBookDbPool('barn', chainId) + const barnChunks = chunkArray(missingAppDataUidsOnProd, LIMIT) - const barnResults = await Promise.all(barnChunks.map(chunk => { - return this.fetchAppDataFromDb(chunk, barnDb); - })); + const barnResults = await Promise.all( + barnChunks.map((chunk) => { + return this.fetchAppDataFromDb(chunk, barnDb) + }) + ) const barnUidToAppData = barnResults.reduce((acc, result) => { - return this.mergeUidToAppDataMaps(acc, result.uidToAppData); - }, new Map()); + return this.mergeUidToAppDataMaps(acc, result.uidToAppData) + }, new Map()) - const results = this.mergeUidToAppDataMaps(totalUidToAppData, barnUidToAppData); + const results = this.mergeUidToAppDataMaps(totalUidToAppData, barnUidToAppData) - this.mergeUidToAppDataMaps(uidToAppDataCache, results); + this.mergeUidToAppDataMaps(uidToAppDataCache, results) - return results; + return results } private async fetchAppDataFromDb(uids: string[], db: Pool): Promise { - if (!uids.length) return { missingAppDataUids: [], uidToAppData: new Map() }; + if (!uids.length) return { missingAppDataUids: [], uidToAppData: new Map() } - const byteaUids = uids.map(hexStringToBytes); + const byteaUids = uids.map(hexStringToBytes) const query = ` SELECT o.uid, a.full_app_data FROM orders o JOIN app_data a ON o.app_data = a.contract_app_data WHERE o.uid = ANY($1) LIMIT $2 - `; + ` - const result = await db.query(query, [byteaUids, LIMIT]); + const result = await db.query(query, [byteaUids, LIMIT]) - const uidToAppData = new Map(); + const uidToAppData = new Map() for (const row of result.rows) { - const uidHex = bytesToHexString(row.uid).toLowerCase(); + const uidHex = bytesToHexString(row.uid).toLowerCase() try { - const fullAppDataHex = JSON.parse(row.full_app_data.toString()); - uidToAppData.set(uidHex, fullAppDataHex); + const fullAppDataHex = JSON.parse(row.full_app_data.toString()) + uidToAppData.set(uidHex, fullAppDataHex) } catch (error) { - logger.error( - error, - `Could not parse app data from DB` - ); + logger.error(error, `Could not parse app data from DB`) } } - const missingAppDataUids = uids.filter(id => !uidToAppData.has(id.toLowerCase())); + const missingAppDataUids = uids.filter((id) => !uidToAppData.has(id.toLowerCase())) - return { uidToAppData, missingAppDataUids }; + return { uidToAppData, missingAppDataUids } } private mergeUidToAppDataMaps(map1: UidToAppData, map2: UidToAppData): UidToAppData { for (const [key, value] of map2) { - map1.set(key, value); + map1.set(key, value) } - return map1; + return map1 } } diff --git a/libs/repositories/src/repos/PushNotificationsRepository/PushNotificationsRepository.ts b/libs/repositories/src/repos/PushNotificationsRepository/PushNotificationsRepository.ts index a33266d8..aa22b510 100644 --- a/libs/repositories/src/repos/PushNotificationsRepository/PushNotificationsRepository.ts +++ b/libs/repositories/src/repos/PushNotificationsRepository/PushNotificationsRepository.ts @@ -4,21 +4,19 @@ import { parseNotifications, PushNotification, stringifyNotifications, -} from '@cowprotocol/notifications'; -import { createRabbitMqConnection } from '../../datasources/rabbitMq'; -import { logger } from '@cowprotocol/shared'; -import crypto from 'node:crypto'; +} from '@cowprotocol/notifications' +import { createRabbitMqConnection } from '../../datasources/rabbitMq' +import { logger } from '@cowprotocol/shared' +import crypto from 'node:crypto' -const MAX_RETRIES = 3; // Maximum number of retry attempts before dropping a message -export const NOTIFICATIONS_QUEUE = 'notifications'; +const MAX_RETRIES = 3 // Maximum number of retry attempts before dropping a message +export const NOTIFICATIONS_QUEUE = 'notifications' -export const pushNotificationsRepositorySymbol = Symbol.for( - 'PushNotificationsRepository' -); +export const pushNotificationsRepositorySymbol = Symbol.for('PushNotificationsRepository') export interface QueueSubscription { - subscriptionId: string; - cancelSubscription: () => void; + subscriptionId: string + cancelSubscription: () => void } /** @@ -27,136 +25,121 @@ export interface QueueSubscription { * This repository allows to send notifications to a queue. */ export interface PushNotificationsRepository { - connection: ConnectToChannelResponse | null; + connection: ConnectToChannelResponse | null - connect(): Promise; - close(): Promise; + connect(): Promise + close(): Promise - send(notifications: PushNotification[]): Promise; - subscribe( - callback: (notification: PushNotification) => Promise - ): Promise; - ping(): Promise; + send(notifications: PushNotification[]): Promise + subscribe(callback: (notification: PushNotification) => Promise): Promise + ping(): Promise } -export class PushNotificationsRepositoryRabbit - implements PushNotificationsRepository { - connection: ConnectToChannelResponse | null = null; +export class PushNotificationsRepositoryRabbit implements PushNotificationsRepository { + connection: ConnectToChannelResponse | null = null - messageRetries = new Map(); // Track message retries by message ID + messageRetries = new Map() // Track message retries by message ID - constructor( - private readonly queueName = NOTIFICATIONS_QUEUE, - private readonly maxRetries = MAX_RETRIES - ) { } + constructor(private readonly queueName = NOTIFICATIONS_QUEUE, private readonly maxRetries = MAX_RETRIES) {} async connect(): Promise { if (!this.connection || !(await this.ping())) { // Connect to the queue this.connection = await connectToChannel({ channel: this.queueName, - }); + }) // Watch for connection close - this.connection.connection.on('close', () => this.close()); + this.connection.connection.on('close', () => this.close()) - return this.connection; + return this.connection } // Return connection - return this.connection; + return this.connection } async close() { if (this.connection) { try { - await this.connection.connection.close(); + await this.connection.connection.close() } finally { - this.connection = null; + this.connection = null } } } async send(notifications: PushNotification[]) { - const connection = await this.connect(); - const { channel } = connection; + const connection = await this.connect() + const { channel } = connection // If there are no notifications, do nothing if (notifications.length === 0) { - return; + return } - const message = stringifyNotifications(notifications); + const message = stringifyNotifications(notifications) channel.sendToQueue(this.queueName, Buffer.from(message), { messageId: crypto.randomUUID(), - }); + }) } - async subscribe( - callback: (notification: PushNotification) => Promise - ): Promise { - logger.info( - `[PushNotificationsRepository] Waiting for messages in "${this.queueName}" queue` - ); + async subscribe(callback: (notification: PushNotification) => Promise): Promise { + logger.info(`[PushNotificationsRepository] Waiting for messages in "${this.queueName}" queue`) - const { channel } = await this.connect(); + const { channel } = await this.connect() const consumer = await channel.consume( this.queueName, async (msg) => { if (msg !== null) { - const messageId = msg.properties.messageId || msg.content.toString(); + const messageId = msg.properties.messageId || msg.content.toString() - let consumeMessage = false; - const clearRetryCount = () => this.messageRetries.delete(messageId); - logger.debug( - `[PushNotificationsRepository] Received message ${messageId}` - ); + let consumeMessage = false + const clearRetryCount = () => this.messageRetries.delete(messageId) + logger.debug(`[PushNotificationsRepository] Received message ${messageId}`) try { // Parse the message into a notifications array (or throw if invalid) - const notifications = parseNotifications(msg.content.toString()); + const notifications = parseNotifications(msg.content.toString()) for (const notification of notifications) { - const sent = await callback(notification); + const sent = await callback(notification) if (!sent && !consumeMessage) { // If we didn't send any notification, we just throw. Otherwise we try to send the next notification - throw new Error(`Failed to send the notifications`); + throw new Error(`Failed to send the notifications`) } - consumeMessage ||= sent; + consumeMessage ||= sent } // All notifications handled. Acknowledge & clear the retry counter - clearRetryCount(); - channel.ack(msg); + clearRetryCount() + channel.ack(msg) } catch (error) { - const retryCount = this.messageRetries.get(messageId) || 0; + const retryCount = this.messageRetries.get(messageId) || 0 if (consumeMessage) { // If we sent at least one notification, clear retry count and acknowledge the message - clearRetryCount(); - channel.ack(msg); // Acknowledge to remove from queue + clearRetryCount() + channel.ack(msg) // Acknowledge to remove from queue } else if (retryCount >= this.maxRetries) { // Max retries reached, drop the message logger.error( error, `[PushNotificationsRepository] Max retries (${this.maxRetries}) reached for message ${messageId}, dropping it` - ); - clearRetryCount(); - channel.nack(msg, false, false); // Negative acknowledge and remove from queue + ) + clearRetryCount() + channel.nack(msg, false, false) // Negative acknowledge and remove from queue } else { // Increment retry count and NACK the message - const newRetryCount = retryCount + 1; - this.messageRetries.set(messageId, newRetryCount); - logger.error( - error, - `[PushNotificationsRepository] Error processing message. Retrying later` - ); + const newRetryCount = retryCount + 1 + this.messageRetries.set(messageId, newRetryCount) + logger.error(error, `[PushNotificationsRepository] Error processing message. Retrying later`) logger.warn( `[PushNotificationsRepository] Retry attempt ${newRetryCount}/${this.maxRetries} for message ${messageId}` - ); - channel.nack(msg, false, true); // Negative acknowledge but keep in queue + ) + channel.nack(msg, false, true) // Negative acknowledge but keep in queue } } } @@ -164,15 +147,15 @@ export class PushNotificationsRepositoryRabbit { noAck: false, } - ); + ) // Return a subscription ID and a function to cancel the subscription return { subscriptionId: consumer.consumerTag, cancelSubscription: () => { - channel.cancel(consumer.consumerTag); + channel.cancel(consumer.consumerTag) }, - }; + } } /** @@ -180,34 +163,32 @@ export class PushNotificationsRepositoryRabbit * @returns true if the connection is alive, false otherwise */ async ping(): Promise { - if (!this.connection) return false; + if (!this.connection) return false try { - const ch = await this.connection.connection.createChannel(); - await ch.close(); - return true; + const ch = await this.connection.connection.createChannel() + await ch.close() + return true } catch (error) { - return false; + return false } } } -async function connectToChannel( - params: ConnectToQueueParams -): Promise { +async function connectToChannel(params: ConnectToQueueParams): Promise { // Connect to RabbitMQ server - const { channel: channelName } = params; + const { channel: channelName } = params - const connection = await createRabbitMqConnection(); + const connection = await createRabbitMqConnection() - const channel = await connection.createChannel(); + const channel = await connection.createChannel() if (channelName) { // This makes sure the queue is declared channel.assertQueue(channelName, { durable: false, - }); + }) } - return { connection, channel }; + return { connection, channel } } diff --git a/libs/repositories/src/repos/PushSubscriptionsRepository/PushSubscriptionsRepository.ts b/libs/repositories/src/repos/PushSubscriptionsRepository/PushSubscriptionsRepository.ts index e46a08ae..c6319830 100644 --- a/libs/repositories/src/repos/PushSubscriptionsRepository/PushSubscriptionsRepository.ts +++ b/libs/repositories/src/repos/PushSubscriptionsRepository/PushSubscriptionsRepository.ts @@ -1,58 +1,50 @@ -import { components } from '@cowprotocol/cms'; +import { components } from '@cowprotocol/cms' -export const pushSubscriptionsRepositorySymbol = Symbol.for( - 'PushSubscriptionsRepository' -); +export const pushSubscriptionsRepositorySymbol = Symbol.for('PushSubscriptionsRepository') -type Schemas = components['schemas']; -export type CmsNotification = Schemas['NotificationListResponseDataItem']; -export type CmsNotificationResponse = Schemas['NotificationListResponse']; +type Schemas = components['schemas'] +export type CmsNotification = Schemas['NotificationListResponseDataItem'] +export type CmsNotificationResponse = Schemas['NotificationListResponse'] export type CmsTelegramSubscription = { - account: string; - chatId: string; -}; -export type CmsTelegramSubscriptionsResponse = - Schemas['TelegramSubscriptionResponse']; + account: string + chatId: string +} +export type CmsTelegramSubscriptionsResponse = Schemas['TelegramSubscriptionResponse'] -export type CmsTelegramSubscriptions = - Schemas['TelegramSubscriptionResponse']['data']; +export type CmsTelegramSubscriptions = Schemas['TelegramSubscriptionResponse']['data'] export type CmsPushNotification = { - id: number; - account: string; - data: Record; - createdAt: string; - updatedAt: string; + id: number + account: string + data: Record + createdAt: string + updatedAt: string notification_template: { - id: number; - title: string; - description: string; - url: null | string; - push: boolean; - thumbnail: null | string; - }; -}; + id: number + title: string + description: string + url: null | string + push: boolean + thumbnail: null | string + } +} export interface NotificationModel { - id: number; - account: string; - title: string; - description: string; - createdAt: string; - url: string | null; - thumbnail: string | null; + id: number + account: string + title: string + description: string + createdAt: string + url: string | null + thumbnail: string | null } /** * Repository to keep track of subscribed accounts for push notifications. */ export interface PushSubscriptionsRepository { - getAllSubscribedAccounts(): Promise; - getAllTelegramSubscriptionsForAccounts( - accounts: string[] - ): Promise; - getPushNotifications(): Promise; - getNotificationsByAccount(params: { - account: string; - }): Promise; + getAllSubscribedAccounts(): Promise + getAllTelegramSubscriptionsForAccounts(accounts: string[]): Promise + getPushNotifications(): Promise + getNotificationsByAccount(params: { account: string }): Promise } diff --git a/libs/repositories/src/repos/PushSubscriptionsRepository/PushSubscriptionsRepositoryCms.ts b/libs/repositories/src/repos/PushSubscriptionsRepository/PushSubscriptionsRepositoryCms.ts index 7e2ab557..286e8857 100644 --- a/libs/repositories/src/repos/PushSubscriptionsRepository/PushSubscriptionsRepositoryCms.ts +++ b/libs/repositories/src/repos/PushSubscriptionsRepository/PushSubscriptionsRepositoryCms.ts @@ -1,50 +1,42 @@ -import { getCmsClient } from '../../datasources/cms'; +import { getCmsClient } from '../../datasources/cms' import { CmsNotification, CmsPushNotification, CmsTelegramSubscription, CmsTelegramSubscriptions, NotificationModel, -} from './PushSubscriptionsRepository'; +} from './PushSubscriptionsRepository' -import { PushSubscriptionsRepository } from './PushSubscriptionsRepository'; +import { PushSubscriptionsRepository } from './PushSubscriptionsRepository' -const PAGE_SIZE = 50; -const CACHE_TIME = 30000; +const PAGE_SIZE = 50 +const CACHE_TIME = 30000 type PaginationParam = { - page?: number; - pageSize?: number; -}; + page?: number + pageSize?: number +} /** * Repository to keep track of subscribed accounts for push notifications. * * Uses the CMS to retrieve the subscriptions */ -export class PushSubscriptionsRepositoryCms - implements PushSubscriptionsRepository -{ - private lastCheck: number | null = null; - private cachedAccounts: string[] | null = null; +export class PushSubscriptionsRepositoryCms implements PushSubscriptionsRepository { + private lastCheck: number | null = null + private cachedAccounts: string[] | null = null async getAllSubscribedAccounts(): Promise { - const now = Date.now(); - if ( - !this.cachedAccounts || - !this.lastCheck || - now - this.lastCheck > CACHE_TIME - ) { - this.cachedAccounts = uniqueLowercase(await getAllSubscribedAccounts()); - this.lastCheck = now; - return this.cachedAccounts; + const now = Date.now() + if (!this.cachedAccounts || !this.lastCheck || now - this.lastCheck > CACHE_TIME) { + this.cachedAccounts = uniqueLowercase(await getAllSubscribedAccounts()) + this.lastCheck = now + return this.cachedAccounts } - return this.cachedAccounts || []; + return this.cachedAccounts || [] } - async getAllTelegramSubscriptionsForAccounts( - accounts: string[] - ): Promise { + async getAllTelegramSubscriptionsForAccounts(accounts: string[]): Promise { return getAllPages({ pageSize: PAGE_SIZE, getPage: (params) => @@ -52,83 +44,65 @@ export class PushSubscriptionsRepositoryCms ...params, accounts, }), - }); + }) } async getPushNotifications(): Promise { - const { data, error, response } = await getCmsClient().GET( - '/push-notifications' - ); + const { data, error, response } = await getCmsClient().GET('/push-notifications') if (error) { - console.error( - `Error ${response.status} getting push-notifications: ${response.url}`, - error - ); - throw error; + console.error(`Error ${response.status} getting push-notifications: ${response.url}`, error) + throw error } - return data; + return data } - async getNotificationsByAccount(params: { - account: string; - }): Promise { - const { account } = params; - const { data, error, response } = await getCmsClient().GET( - '/notification-list/' + account - ); + async getNotificationsByAccount(params: { account: string }): Promise { + const { account } = params + const { data, error, response } = await getCmsClient().GET('/notification-list/' + account) if (error) { - console.error( - `Error ${response.status} getting notifications: ${response.url}`, - error - ); - throw error; + console.error(`Error ${response.status} getting notifications: ${response.url}`, error) + throw error } - return data; + return data } } function uniqueLowercase(items: string[]): string[] { - return Array.from(new Set(items.map((item) => item.toLowerCase()))); + return Array.from(new Set(items.map((item) => item.toLowerCase()))) } export async function getAllNotifications(): Promise { - const allNotifications = []; - let page = 0; + const allNotifications = [] + let page = 0 let notifications = await getNotificationsPage({ page, pageSize: PAGE_SIZE + 1, - }); // Get one extra to check if there's more pages + }) // Get one extra to check if there's more pages - allNotifications.push( - notifications.length > PAGE_SIZE - ? notifications.slice(0, -1) - : notifications - ); + allNotifications.push(notifications.length > PAGE_SIZE ? notifications.slice(0, -1) : notifications) while (notifications.length > PAGE_SIZE) { notifications = await getNotificationsPage({ page, pageSize: PAGE_SIZE + 1, - }); // Get one extra to check if there's more pages - const hasMorePages = notifications.length > PAGE_SIZE; - allNotifications.push( - hasMorePages ? notifications.slice(0, -1) : notifications - ); + }) // Get one extra to check if there's more pages + const hasMorePages = notifications.length > PAGE_SIZE + allNotifications.push(hasMorePages ? notifications.slice(0, -1) : notifications) if (!hasMorePages) { - break; + break } // Keep fetching while there's more pages - page++; + page++ } - return allNotifications.flat(); + return allNotifications.flat() } /** @@ -137,129 +111,105 @@ export async function getAllNotifications(): Promise { * @param params - The pagination parameters * @returns The notifications */ -async function getNotificationsPage({ - page = 0, - pageSize = PAGE_SIZE, -}: PaginationParam = {}): Promise { +async function getNotificationsPage({ page = 0, pageSize = PAGE_SIZE }: PaginationParam = {}): Promise< + CmsNotification[] +> { const { data, error, response } = await getCmsClient().GET('/notifications', { 'populate[0]': 'notification_template', // Pagination 'pagination[page]': page, 'pagination[pageSize]': pageSize, - }); + }) if (error) { - console.error( - `Error ${response.status} getting notifications: ${response.url}. Page${page}`, - error - ); - throw error; + console.error(`Error ${response.status} getting notifications: ${response.url}. Page${page}`, error) + throw error } - return data.data; + return data.data } export async function getAllSubscribedAccounts(): Promise { return getAllPages({ pageSize: PAGE_SIZE, getPage: (params) => getSubscribedAccounts(params), - }); + }) } async function getAllPages({ pageSize = PAGE_SIZE, getPage, }: PaginationParam & { - getPage: (params: PaginationParam) => Promise; + getPage: (params: PaginationParam) => Promise }): Promise { - const allSubscriptions = []; - let page = 0; + const allSubscriptions = [] + let page = 0 let subscriptions = await getPage({ page, pageSize: pageSize + 1, - }); // Get one extra to check if there's more pages + }) // Get one extra to check if there's more pages - allSubscriptions.push( - subscriptions.length > pageSize ? subscriptions.slice(0, -1) : subscriptions - ); + allSubscriptions.push(subscriptions.length > pageSize ? subscriptions.slice(0, -1) : subscriptions) while (subscriptions.length > pageSize) { subscriptions = await getPage({ page, pageSize: pageSize + 1, - }); // Get one extra to check if there's more pages - const hasMorePages = subscriptions.length > pageSize; - allSubscriptions.push( - hasMorePages ? subscriptions.slice(0, -1) : subscriptions - ); + }) // Get one extra to check if there's more pages + const hasMorePages = subscriptions.length > pageSize + allSubscriptions.push(hasMorePages ? subscriptions.slice(0, -1) : subscriptions) if (!hasMorePages) { - break; + break } // Keep fetching while there's more pages - page++; + page++ } - return allSubscriptions.flat(); + return allSubscriptions.flat() } async function getTelegramSubscriptionsForAccounts({ page = 0, pageSize = PAGE_SIZE, accounts, -}: PaginationParam & { accounts: string[] }): Promise< - CmsTelegramSubscription[] -> { - const { data, error, response } = await getCmsClient().GET( - `/accounts/${accounts.join(',')}/subscriptions/telegram`, - { - // Pagination - 'pagination[page]': page, - 'pagination[pageSize]': pageSize, - } - ); +}: PaginationParam & { accounts: string[] }): Promise { + const { data, error, response } = await getCmsClient().GET(`/accounts/${accounts.join(',')}/subscriptions/telegram`, { + // Pagination + 'pagination[page]': page, + 'pagination[pageSize]': pageSize, + }) if (error) { - console.error( - `Error ${response.status} getting telegram subscriptions: ${response.url}. Page${page}` - ); - throw error; + console.error(`Error ${response.status} getting telegram subscriptions: ${response.url}. Page${page}`) + throw error } - return data; + return data } -async function getSubscribedAccounts({ - page = 0, - pageSize = PAGE_SIZE, -}: PaginationParam): Promise { - const { data, error, response } = await getCmsClient().GET( - `/telegram-subscriptions`, - { - // Pagination - 'pagination[page]': page, - 'pagination[pageSize]': pageSize, - } - ); +async function getSubscribedAccounts({ page = 0, pageSize = PAGE_SIZE }: PaginationParam): Promise { + const { data, error, response } = await getCmsClient().GET(`/telegram-subscriptions`, { + // Pagination + 'pagination[page]': page, + 'pagination[pageSize]': pageSize, + }) if (error) { - console.error( - `Error ${response.status} getting telegram subscriptions: ${response.url}. Page${page}`, - error - ); - throw error; + console.error(`Error ${response.status} getting telegram subscriptions: ${response.url}. Page${page}`, error) + throw error } - const subscriptions = data.data as CmsTelegramSubscriptions[]; + const subscriptions = data.data as CmsTelegramSubscriptions[] return subscriptions.reduce((acc, subscription) => { - const account = subscription?.attributes?.account; + const account = subscription?.attributes?.account if (account) { - acc.push(account); + acc.push(account) } - return acc; - }, []); + return acc + }, []) } diff --git a/libs/repositories/src/repos/SimulationRepository/SimulationRepository.ts b/libs/repositories/src/repos/SimulationRepository/SimulationRepository.ts index 23ec7ac7..e8e3c936 100644 --- a/libs/repositories/src/repos/SimulationRepository/SimulationRepository.ts +++ b/libs/repositories/src/repos/SimulationRepository/SimulationRepository.ts @@ -1,31 +1,28 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { StateDiff } from './tenderlyTypes'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { StateDiff } from './tenderlyTypes' export interface SimulationInput { - input: string; - from: string; - to: string; - value?: string; - gas?: number; - gas_price?: string; + input: string + from: string + to: string + value?: string + gas?: number + gas_price?: string } export interface SimulationData { - link: string; - status: boolean; - id: string; + link: string + status: boolean + id: string // { [address: string]: { [token: string]: balanceDiff: string } } // example: { '0x123': { '0x456': '100', '0xabc': '-100' } } - cumulativeBalancesDiff: Record>; - stateDiff: StateDiff[]; - gasUsed?: string; + cumulativeBalancesDiff: Record> + stateDiff: StateDiff[] + gasUsed?: string } -export const simulationRepositorySymbol = Symbol.for('SimulationRepository'); +export const simulationRepositorySymbol = Symbol.for('SimulationRepository') export interface SimulationRepository { - postBundleSimulation( - chainId: SupportedChainId, - simulationsInput: SimulationInput[] - ): Promise; + postBundleSimulation(chainId: SupportedChainId, simulationsInput: SimulationInput[]): Promise } diff --git a/libs/repositories/src/repos/SimulationRepository/SimulationRepositoryTenderly.ts b/libs/repositories/src/repos/SimulationRepository/SimulationRepositoryTenderly.ts index 8b5b09a9..580974f4 100644 --- a/libs/repositories/src/repos/SimulationRepository/SimulationRepositoryTenderly.ts +++ b/libs/repositories/src/repos/SimulationRepository/SimulationRepositoryTenderly.ts @@ -1,55 +1,44 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { logger } from '@cowprotocol/shared'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { logger } from '@cowprotocol/shared' import { AssetChange, SimulationError, StateDiff, TenderlyBundleSimulationResponse, TenderlySimulatePayload, -} from './tenderlyTypes'; -import { - getTenderlySimulationLink, - TENDERLY_API_BASE_ENDPOINT, - TENDERLY_API_KEY, -} from '../../datasources/tenderlyApi'; -import { injectable } from 'inversify'; -import { - SimulationData, - SimulationInput, - SimulationRepository, -} from './SimulationRepository'; -import { BigNumber } from 'ethers'; -import { buildStateDiff } from '../../utils/buildStateDiff'; +} from './tenderlyTypes' +import { getTenderlySimulationLink, TENDERLY_API_BASE_ENDPOINT, TENDERLY_API_KEY } from '../../datasources/tenderlyApi' +import { injectable } from 'inversify' +import { SimulationData, SimulationInput, SimulationRepository } from './SimulationRepository' +import { BigNumber } from 'ethers' +import { buildStateDiff } from '../../utils/buildStateDiff' interface TenderlyRequestLog { - timestamp: string; - chainId: SupportedChainId; - endpoint: string; - method: string; - simulationsCount: number; - simulations: TenderlySimulatePayload[]; + timestamp: string + chainId: SupportedChainId + endpoint: string + method: string + simulationsCount: number + simulations: TenderlySimulatePayload[] } interface TenderlyResponseLog { - timestamp: string; - duration: number; - status: 'success' | 'error'; - error?: string; + timestamp: string + duration: number + status: 'success' | 'error' + error?: string simulationResults?: { - id: string; - status: boolean; - gasUsed?: string; - }[]; + id: string + status: boolean + gasUsed?: string + }[] } -export const tenderlyRepositorySymbol = Symbol.for('TenderlyRepository'); +export const tenderlyRepositorySymbol = Symbol.for('TenderlyRepository') @injectable() export class SimulationRepositoryTenderly implements SimulationRepository { - private logRequest( - chainId: SupportedChainId, - simulations: TenderlySimulatePayload[] - ): TenderlyRequestLog { + private logRequest(chainId: SupportedChainId, simulations: TenderlySimulatePayload[]): TenderlyRequestLog { const requestLog: TenderlyRequestLog = { timestamp: new Date().toISOString(), chainId, @@ -57,45 +46,43 @@ export class SimulationRepositoryTenderly implements SimulationRepository { method: 'POST', simulationsCount: simulations.length, simulations, - }; + } logger.info({ msg: 'Tenderly simulation request', ...requestLog, - }); + }) - return requestLog; + return requestLog } private logResponse( startTime: number, response: TenderlyBundleSimulationResponse | SimulationError ): TenderlyResponseLog { - const duration = Date.now() - startTime; + const duration = Date.now() - startTime const responseLog: TenderlyResponseLog = { timestamp: new Date().toISOString(), duration, status: this.checkBundleSimulationError(response) ? 'error' : 'success', - }; + } if (this.checkBundleSimulationError(response)) { - responseLog.error = response.error.message; + responseLog.error = response.error.message } else { - responseLog.simulationResults = response.simulation_results.map( - (result) => ({ - id: result.simulation.id, - status: result.simulation.status, - gasUsed: result.transaction?.gas_used.toString(), - }) - ); + responseLog.simulationResults = response.simulation_results.map((result) => ({ + id: result.simulation.id, + status: result.simulation.status, + gasUsed: result.transaction?.gas_used.toString(), + })) } logger.info({ msg: 'Tenderly simulation response', ...responseLog, - }); + }) - return responseLog; + return responseLog } async postBundleSimulation( @@ -108,43 +95,33 @@ export class SimulationRepositoryTenderly implements SimulationRepository { gas_price: '0', save: true, save_if_fails: true, - })) as TenderlySimulatePayload[]; + })) as TenderlySimulatePayload[] - const startTime = Date.now(); - this.logRequest(chainId, simulations); + const startTime = Date.now() + this.logRequest(chainId, simulations) try { - const response = (await fetch( - `${TENDERLY_API_BASE_ENDPOINT}/simulate-bundle`, - { - method: 'POST', - body: JSON.stringify({ simulations }), - headers: { - 'X-Access-Key': TENDERLY_API_KEY, - }, - } - ).then((res) => res.json())) as - | TenderlyBundleSimulationResponse - | SimulationError; + const response = (await fetch(`${TENDERLY_API_BASE_ENDPOINT}/simulate-bundle`, { + method: 'POST', + body: JSON.stringify({ simulations }), + headers: { + 'X-Access-Key': TENDERLY_API_KEY, + }, + }).then((res) => res.json())) as TenderlyBundleSimulationResponse | SimulationError - this.logResponse(startTime, response); + this.logResponse(startTime, response) if (this.checkBundleSimulationError(response)) { - return null; + return null } const balancesDiff = this.buildBalancesDiff( - response.simulation_results.map( - (result) => result.transaction?.transaction_info.asset_changes || [] - ) - ); + response.simulation_results.map((result) => result.transaction?.transaction_info.asset_changes || []) + ) const stateDiff = this.buildStateDiff( - response.simulation_results.map( - (result) => - result.transaction?.transaction_info.call_trace?.state_diff ?? [] - ) - ); + response.simulation_results.map((result) => result.transaction?.transaction_info.call_trace?.state_diff ?? []) + ) return response.simulation_results.map((simulation_result, i) => { return { @@ -154,8 +131,8 @@ export class SimulationRepositoryTenderly implements SimulationRepository { cumulativeBalancesDiff: balancesDiff[i], stateDiff: stateDiff[i], gasUsed: simulation_result.transaction?.gas_used.toString(), - }; - }); + } + }) } catch (error) { logger.error({ msg: 'Tenderly simulation unexpected error', @@ -163,59 +140,49 @@ export class SimulationRepositoryTenderly implements SimulationRepository { chainId, simulationsCount: simulations.length, duration: Date.now() - startTime, - }); - throw error; + }) + throw error } } checkBundleSimulationError( response: TenderlyBundleSimulationResponse | SimulationError ): response is SimulationError { - return (response as SimulationError).error !== undefined; + return (response as SimulationError).error !== undefined } - buildBalancesDiff( - assetChangesList: AssetChange[][] - ): Record>[] { - const cumulativeBalancesDiff: Record> = {}; + buildBalancesDiff(assetChangesList: AssetChange[][]): Record>[] { + const cumulativeBalancesDiff: Record> = {} return assetChangesList.map((assetChanges) => { assetChanges.forEach((change) => { - const { token_info, from, to, raw_amount } = change; - const { contract_address } = token_info; - - const updateBalance = ( - address: string, - tokenSymbol: string, - changeAmount: string - ) => { + const { token_info, from, to, raw_amount } = change + const { contract_address } = token_info + + const updateBalance = (address: string, tokenSymbol: string, changeAmount: string) => { if (!cumulativeBalancesDiff[address]) { - cumulativeBalancesDiff[address] = {}; + cumulativeBalancesDiff[address] = {} } if (!cumulativeBalancesDiff[address][tokenSymbol]) { - cumulativeBalancesDiff[address][tokenSymbol] = '0'; + cumulativeBalancesDiff[address][tokenSymbol] = '0' } - const currentBalance = BigNumber.from( - cumulativeBalancesDiff[address][tokenSymbol] - ); - const changeValue = BigNumber.from(changeAmount); - const newBalance = currentBalance.add(changeValue); - cumulativeBalancesDiff[address][tokenSymbol] = newBalance.toString(); - }; + const currentBalance = BigNumber.from(cumulativeBalancesDiff[address][tokenSymbol]) + const changeValue = BigNumber.from(changeAmount) + const newBalance = currentBalance.add(changeValue) + cumulativeBalancesDiff[address][tokenSymbol] = newBalance.toString() + } if (from) { - updateBalance(from, contract_address, `-${raw_amount}`); + updateBalance(from, contract_address, `-${raw_amount}`) } if (to) { - updateBalance(to, contract_address, raw_amount); + updateBalance(to, contract_address, raw_amount) } - }); - return structuredClone( - cumulativeBalancesDiff - ); - }); + }) + return structuredClone(cumulativeBalancesDiff) + }) } buildStateDiff(stateDiffList: StateDiff[][]): StateDiff[][] { - return buildStateDiff(stateDiffList); + return buildStateDiff(stateDiffList) } } diff --git a/libs/repositories/src/repos/SimulationRepository/SimulationrepositoryTenderly.test.ts b/libs/repositories/src/repos/SimulationRepository/SimulationrepositoryTenderly.test.ts index bd8ee1a4..87ef2ea3 100644 --- a/libs/repositories/src/repos/SimulationRepository/SimulationrepositoryTenderly.test.ts +++ b/libs/repositories/src/repos/SimulationRepository/SimulationrepositoryTenderly.test.ts @@ -1,13 +1,9 @@ -import { Container } from 'inversify'; -import { SimulationRepositoryTenderly } from './SimulationRepositoryTenderly'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { WETH, NULL_ADDRESS } from '../../../test/mock'; -import { - TENDERLY_API_KEY, - TENDERLY_ORG_NAME, - TENDERLY_PROJECT_NAME, -} from '../../datasources/tenderlyApi'; -import { AssetChange, StateDiff } from './tenderlyTypes'; +import { Container } from 'inversify' +import { SimulationRepositoryTenderly } from './SimulationRepositoryTenderly' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { WETH, NULL_ADDRESS } from '../../../test/mock' +import { TENDERLY_API_KEY, TENDERLY_ORG_NAME, TENDERLY_PROJECT_NAME } from '../../datasources/tenderlyApi' +import { AssetChange, StateDiff } from './tenderlyTypes' // Transfering ETH from WETH to NULL ADDRESS const TENDERLY_SIMULATION = { @@ -15,14 +11,14 @@ const TENDERLY_SIMULATION = { to: NULL_ADDRESS, value: '1000000000000000000', input: '0x', -}; +} const INVALID_TENDERLY_SIMULATION = { from: NULL_ADDRESS, to: WETH, value: '0', input: 'wrong input', -}; +} const FAILED_TENDERLY_SIMULATION = { from: NULL_ADDRESS, @@ -30,60 +26,52 @@ const FAILED_TENDERLY_SIMULATION = { value: '0', input: '0x23b872dd000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000a', -}; +} // The tests are integration tests and require ENV variables describe.skip('SimulationRepositoryTenderly', () => { - let tenderlyRepository: SimulationRepositoryTenderly; + let tenderlyRepository: SimulationRepositoryTenderly beforeAll(() => { - const container = new Container(); - container - .bind(SimulationRepositoryTenderly) - .to(SimulationRepositoryTenderly); - tenderlyRepository = container.get(SimulationRepositoryTenderly); - expect(TENDERLY_API_KEY).toBeDefined(); - expect(TENDERLY_ORG_NAME).toBeDefined(); - expect(TENDERLY_PROJECT_NAME).toBeDefined(); - }); + const container = new Container() + container.bind(SimulationRepositoryTenderly).to(SimulationRepositoryTenderly) + tenderlyRepository = container.get(SimulationRepositoryTenderly) + expect(TENDERLY_API_KEY).toBeDefined() + expect(TENDERLY_ORG_NAME).toBeDefined() + expect(TENDERLY_PROJECT_NAME).toBeDefined() + }) describe('postBundleSimulation', () => { it('should return simulation data for success simulation', async () => { - const tenderlySimulationResult = - await tenderlyRepository.postBundleSimulation( - SupportedChainId.MAINNET, - [TENDERLY_SIMULATION] - ); - - expect(tenderlySimulationResult).toBeDefined(); - expect(tenderlySimulationResult?.length).toBe(1); - expect(tenderlySimulationResult?.[0].status).toBeTruthy(); - expect(Number(tenderlySimulationResult?.[0].gasUsed)).toBeGreaterThan(0); - }, 100000); + const tenderlySimulationResult = await tenderlyRepository.postBundleSimulation(SupportedChainId.MAINNET, [ + TENDERLY_SIMULATION, + ]) + + expect(tenderlySimulationResult).toBeDefined() + expect(tenderlySimulationResult?.length).toBe(1) + expect(tenderlySimulationResult?.[0].status).toBeTruthy() + expect(Number(tenderlySimulationResult?.[0].gasUsed)).toBeGreaterThan(0) + }, 100000) it('should return null for invalid simulation', async () => { - const tenderlySimulationResult = - await tenderlyRepository.postBundleSimulation( - SupportedChainId.MAINNET, - [INVALID_TENDERLY_SIMULATION] - ); + const tenderlySimulationResult = await tenderlyRepository.postBundleSimulation(SupportedChainId.MAINNET, [ + INVALID_TENDERLY_SIMULATION, + ]) - expect(tenderlySimulationResult).toBeNull(); - }, 100000); + expect(tenderlySimulationResult).toBeNull() + }, 100000) it('should return simulation data for failed simulation', async () => { - const tenderlySimulationResult = - await tenderlyRepository.postBundleSimulation( - SupportedChainId.MAINNET, - [FAILED_TENDERLY_SIMULATION] - ); - - expect(tenderlySimulationResult).toBeDefined(); - expect(tenderlySimulationResult?.length).toBe(1); - expect(tenderlySimulationResult?.[0].status).toBeFalsy(); - expect(Number(tenderlySimulationResult?.[0].gasUsed)).toBeGreaterThan(0); - }, 100000); - }); + const tenderlySimulationResult = await tenderlyRepository.postBundleSimulation(SupportedChainId.MAINNET, [ + FAILED_TENDERLY_SIMULATION, + ]) + + expect(tenderlySimulationResult).toBeDefined() + expect(tenderlySimulationResult?.length).toBe(1) + expect(tenderlySimulationResult?.[0].status).toBeFalsy() + expect(Number(tenderlySimulationResult?.[0].gasUsed)).toBeGreaterThan(0) + }, 100000) + }) describe('buildBalancesDiff', () => { it('should correctly process a single asset change', () => { const input: AssetChange[][] = [ @@ -106,7 +94,7 @@ describe.skip('SimulationRepositoryTenderly', () => { raw_amount: '100000000000000000000', }, ], - ]; + ] const expected = [ { @@ -117,10 +105,10 @@ describe.skip('SimulationRepositoryTenderly', () => { '0x123': '100000000000000000000', }, }, - ]; + ] - expect(tenderlyRepository.buildBalancesDiff(input)).toEqual(expected); - }); + expect(tenderlyRepository.buildBalancesDiff(input)).toEqual(expected) + }) it('should correctly process multiple asset changes', () => { const input: AssetChange[][] = [ @@ -162,7 +150,7 @@ describe.skip('SimulationRepositoryTenderly', () => { raw_amount: '50000000000000000000', }, ], - ]; + ] const expected = [ { @@ -185,10 +173,10 @@ describe.skip('SimulationRepositoryTenderly', () => { '0x456': '50000000000000000000', }, }, - ]; + ] - expect(tenderlyRepository.buildBalancesDiff(input)).toEqual(expected); - }); + expect(tenderlyRepository.buildBalancesDiff(input)).toEqual(expected) + }) it('should handle diffs with missing soltype', () => { const input: StateDiff[][] = [ @@ -208,7 +196,7 @@ describe.skip('SimulationRepositoryTenderly', () => { ], }, ], - ]; + ] const expected: StateDiff[][] = [ [ @@ -227,20 +215,20 @@ describe.skip('SimulationRepositoryTenderly', () => { ], }, ], - ]; + ] - expect(tenderlyRepository.buildStateDiff(input)).toEqual(expected); - }); + expect(tenderlyRepository.buildStateDiff(input)).toEqual(expected) + }) it('should handle empty input', () => { - const input: AssetChange[][] = []; - expect(tenderlyRepository.buildBalancesDiff(input)).toEqual([]); - }); + const input: AssetChange[][] = [] + expect(tenderlyRepository.buildBalancesDiff(input)).toEqual([]) + }) it('should handle input with empty asset changes', () => { - const input: AssetChange[][] = [[], []]; - expect(tenderlyRepository.buildBalancesDiff(input)).toEqual([{}, {}]); - }); + const input: AssetChange[][] = [[], []] + expect(tenderlyRepository.buildBalancesDiff(input)).toEqual([{}, {}]) + }) it('should correctly handle cumulative changes', () => { const input: AssetChange[][] = [ @@ -282,7 +270,7 @@ describe.skip('SimulationRepositoryTenderly', () => { raw_amount: '50000000000000000000', }, ], - ]; + ] const expected = [ { @@ -301,11 +289,11 @@ describe.skip('SimulationRepositoryTenderly', () => { '0x123': '50000000000000000000', }, }, - ]; + ] - expect(tenderlyRepository.buildBalancesDiff(input)).toEqual(expected); - }); - }); + expect(tenderlyRepository.buildBalancesDiff(input)).toEqual(expected) + }) + }) describe('buildStateDiff', () => { it('should correctly process a single state diff', () => { const input: StateDiff[][] = [ @@ -344,7 +332,7 @@ describe.skip('SimulationRepositoryTenderly', () => { ], }, ], - ]; + ] const expected: StateDiff[][] = [ [ @@ -382,10 +370,10 @@ describe.skip('SimulationRepositoryTenderly', () => { ], }, ], - ]; + ] - expect(tenderlyRepository.buildStateDiff(input)).toEqual(expected); - }); + expect(tenderlyRepository.buildStateDiff(input)).toEqual(expected) + }) it('should accumulate state changes across multiple simulations', () => { const input: StateDiff[][] = [ @@ -459,7 +447,7 @@ describe.skip('SimulationRepositoryTenderly', () => { ], }, ], - ]; + ] const expected: StateDiff[][] = [ [ @@ -532,10 +520,10 @@ describe.skip('SimulationRepositoryTenderly', () => { ], }, ], - ]; + ] - expect(tenderlyRepository.buildStateDiff(input)).toEqual(expected); - }); + expect(tenderlyRepository.buildStateDiff(input)).toEqual(expected) + }) it('should process multiple addresses with different properties', () => { const input: StateDiff[][] = [ @@ -606,7 +594,7 @@ describe.skip('SimulationRepositoryTenderly', () => { ], }, ], - ]; + ] const expected: StateDiff[][] = [ [ @@ -697,20 +685,20 @@ describe.skip('SimulationRepositoryTenderly', () => { ], }, ], - ]; + ] - expect(tenderlyRepository.buildStateDiff(input)).toEqual(expected); - }); + expect(tenderlyRepository.buildStateDiff(input)).toEqual(expected) + }) it('should handle empty input', () => { - const input: StateDiff[][] = []; - expect(tenderlyRepository.buildStateDiff(input)).toEqual([]); - }); + const input: StateDiff[][] = [] + expect(tenderlyRepository.buildStateDiff(input)).toEqual([]) + }) it('should handle input with empty state diffs', () => { - const input: StateDiff[][] = [[], []]; - expect(tenderlyRepository.buildStateDiff(input)).toEqual([[], []]); - }); + const input: StateDiff[][] = [[], []] + expect(tenderlyRepository.buildStateDiff(input)).toEqual([[], []]) + }) it('should correctly update raw elements when address and key match', () => { const input: StateDiff[][] = [ @@ -753,7 +741,7 @@ describe.skip('SimulationRepositoryTenderly', () => { ], }, ], - ]; + ] // In the expected result, the accumulated state should maintain the original "original" value // but update the "dirty" value @@ -804,9 +792,9 @@ describe.skip('SimulationRepositoryTenderly', () => { ], }, ], - ]; + ] - expect(tenderlyRepository.buildStateDiff(input)).toEqual(expected); - }); - }); -}); + expect(tenderlyRepository.buildStateDiff(input)).toEqual(expected) + }) + }) +}) diff --git a/libs/repositories/src/repos/SimulationRepository/tenderlyTypes.ts b/libs/repositories/src/repos/SimulationRepository/tenderlyTypes.ts index 59d76c70..04877412 100644 --- a/libs/repositories/src/repos/SimulationRepository/tenderlyTypes.ts +++ b/libs/repositories/src/repos/SimulationRepository/tenderlyTypes.ts @@ -1,129 +1,129 @@ export interface TenderlyBundleSimulationResponse { - simulation_results: TenderlySimulation[]; + simulation_results: TenderlySimulation[] } // types were found in Uniswap repository // https://github.com/Uniswap/governance-seatbelt/blob/e2c6a0b11d1660f3bd934dab0d9df3ca6f90a1a0/types.d.ts#L123 type StateObject = { - balance?: string; - code?: string; - storage?: Record; -}; + balance?: string + code?: string + storage?: Record +} type ContractObject = { - contractName: string; - source: string; - sourcePath: string; + contractName: string + source: string + sourcePath: string compiler: { - name: 'solc'; - version: string; - }; + name: 'solc' + version: string + } networks: Record< string, { - events?: Record; - links?: Record; - address: string; - transactionHash?: string; + events?: Record + links?: Record + address: string + transactionHash?: string } - >; -}; + > +} export type TenderlySimulatePayload = { - network_id: string; - block_number?: number; - transaction_index?: number; - from: string; - to: string; - input: string; - gas: number; - gas_price?: string; - value?: string; - simulation_type?: 'full' | 'quick'; - save?: boolean; - save_if_fails?: boolean; - state_objects?: Record; - contracts?: ContractObject[]; + network_id: string + block_number?: number + transaction_index?: number + from: string + to: string + input: string + gas: number + gas_price?: string + value?: string + simulation_type?: 'full' | 'quick' + save?: boolean + save_if_fails?: boolean + state_objects?: Record + contracts?: ContractObject[] block_header?: { - number?: string; - timestamp?: string; - }; - generate_access_list?: boolean; -}; + number?: string + timestamp?: string + } + generate_access_list?: boolean +} // --- Tenderly types, Response --- // NOTE: These type definitions were autogenerated using https://app.quicktype.io/, so are almost // certainly not entirely accurate (and they have some interesting type names) export interface TenderlySimulation { - transaction?: Transaction; - simulation: Simulation; - contracts: TenderlyContract[]; - generated_access_list: GeneratedAccessList[]; + transaction?: Transaction + simulation: Simulation + contracts: TenderlyContract[] + generated_access_list: GeneratedAccessList[] } interface TenderlyContract { - id: string; - contract_id: string; - balance: string; - network_id: string; - public: boolean; - export: boolean; - verified_by: string; - verification_date: null; - address: string; - contract_name: string; - ens_domain: null; - type: string; - evm_version: string; - compiler_version: string; - optimizations_used: boolean; - optimization_runs: number; - libraries: null; - data: Data; - creation_block: number; - creation_tx: string; - creator_address: string; - created_at: Date; - number_of_watches: null; - language: string; - in_project: boolean; - number_of_files: number; - standard?: string; - standards?: string[]; - token_data?: TokenData; + id: string + contract_id: string + balance: string + network_id: string + public: boolean + export: boolean + verified_by: string + verification_date: null + address: string + contract_name: string + ens_domain: null + type: string + evm_version: string + compiler_version: string + optimizations_used: boolean + optimization_runs: number + libraries: null + data: Data + creation_block: number + creation_tx: string + creator_address: string + created_at: Date + number_of_watches: null + language: string + in_project: boolean + number_of_files: number + standard?: string + standards?: string[] + token_data?: TokenData } interface Data { - main_contract: number; - contract_info: ContractInfo[]; - abi: ABI[]; - raw_abi: null; + main_contract: number + contract_info: ContractInfo[] + abi: ABI[] + raw_abi: null } interface ABI { - type: ABIType; - name: string; - constant: boolean; - anonymous: boolean; - inputs: SoltypeElement[]; - outputs: Output[] | null; + type: ABIType + name: string + constant: boolean + anonymous: boolean + inputs: SoltypeElement[] + outputs: Output[] | null } interface SoltypeElement { - name: string; - type: SoltypeType; - storage_location: StorageLocation; - components?: Record[] | null; - offset: number; - index: string; - indexed: boolean; - simple_type?: Type; + name: string + type: SoltypeType + storage_location: StorageLocation + components?: Record[] | null + offset: number + index: string + indexed: boolean + simple_type?: Type } interface Type { - type: SimpleTypeType; + type: SimpleTypeType } enum SimpleTypeType { @@ -135,7 +135,7 @@ enum SimpleTypeType { Uint = 'uint', } -type StorageLocation = 'calldata' | 'default' | 'memory' | 'storage'; +type StorageLocation = 'calldata' | 'default' | 'memory' | 'storage' type SoltypeType = | 'address' @@ -152,22 +152,22 @@ type SoltypeType = | 'uint256' | 'uint48' | 'uint56' - | 'uint8'; + | 'uint8' interface Output { - name: string; - type: SoltypeType; - storage_location: StorageLocation; - components?: SoltypeElement[] | null; - offset: number; - index: string; - indexed: boolean; - simple_type?: SimpleType; + name: string + type: SoltypeType + storage_location: StorageLocation + components?: SoltypeElement[] | null + offset: number + index: string + indexed: boolean + simple_type?: SimpleType } interface SimpleType { - type: SimpleTypeType; - nested_type?: Type; + type: SimpleTypeType + nested_type?: Type } enum ABIType { @@ -177,415 +177,415 @@ enum ABIType { } interface ContractInfo { - id: number; - path: string; - name: string; - source: string; + id: number + path: string + name: string + source: string } interface TokenData { - symbol: string; - name: string; - decimals: number; + symbol: string + name: string + decimals: number } interface GeneratedAccessList { - address: string; - storage_keys: string[]; + address: string + storage_keys: string[] } interface Simulation { - id: string; - project_id: string; - owner_id: string; - network_id: string; - block_number: number; - transaction_index: number; - from: string; - to: string; - input: string; - gas: number; - gas_price: string; - value: string; - method: string; - status: boolean; - access_list: null; - queue_origin: string; - created_at: Date; + id: string + project_id: string + owner_id: string + network_id: string + block_number: number + transaction_index: number + from: string + to: string + input: string + gas: number + gas_price: string + value: string + method: string + status: boolean + access_list: null + queue_origin: string + created_at: Date } interface ErrorInfo { - error_message: string; - address: string; + error_message: string + address: string } export interface SimulationError { error: { - id: string; - message: string; - slug: string; - }; + id: string + message: string + slug: string + } } interface Transaction { - hash: string; - block_hash: string; - block_number: number; - from: string; - gas: number; - gas_price: number; - gas_fee_cap: number; - gas_tip_cap: number; - cumulative_gas_used: number; - gas_used: number; - effective_gas_price: number; - input: string; - nonce: number; - to: string; - index: number; - error_message?: string; - error_info?: ErrorInfo; - value: string; - access_list: null; - status: boolean; - addresses: string[]; - contract_ids: string[]; - network_id: string; - function_selector: string; - transaction_info: TransactionInfo; - timestamp: Date; - method: string; - decoded_input: null; + hash: string + block_hash: string + block_number: number + from: string + gas: number + gas_price: number + gas_fee_cap: number + gas_tip_cap: number + cumulative_gas_used: number + gas_used: number + effective_gas_price: number + input: string + nonce: number + to: string + index: number + error_message?: string + error_info?: ErrorInfo + value: string + access_list: null + status: boolean + addresses: string[] + contract_ids: string[] + network_id: string + function_selector: string + transaction_info: TransactionInfo + timestamp: Date + method: string + decoded_input: null // Note: manually added (partial keys of `call_trace`) call_trace: Array<{ - error?: string; - input: string; - }>; + error?: string + input: string + }> } interface TransactionInfo { - contract_id: string; - block_number: number; - transaction_id: string; - contract_address: string; - method: string; - parameters: null; - intrinsic_gas: number; - refund_gas: number; - call_trace: CallTrace; - stack_trace: null | StackTrace[]; - logs: Log[] | null; - state_diff: StateDiff[]; - raw_state_diff: null; - console_logs: null; - created_at: Date; + contract_id: string + block_number: number + transaction_id: string + contract_address: string + method: string + parameters: null + intrinsic_gas: number + refund_gas: number + call_trace: CallTrace + stack_trace: null | StackTrace[] + logs: Log[] | null + state_diff: StateDiff[] + raw_state_diff: null + console_logs: null + created_at: Date // Note: manually added - asset_changes?: AssetChange[]; + asset_changes?: AssetChange[] } // Note: manually added export interface AssetChange { token_info: { - standard: string; - type: string; - contract_address: string; - symbol: string; - name: string; - logo: string; - decimals: number; - dollar_value: string; - }; - type: string; - from?: string; - to?: string; - amount: string; - raw_amount: string; + standard: string + type: string + contract_address: string + symbol: string + name: string + logo: string + decimals: number + dollar_value: string + } + type: string + from?: string + to?: string + amount: string + raw_amount: string } interface StackTrace { - file_index: number; - contract: string; - name: string; - line: number; - error: string; - error_reason: string; - code: string; - op: string; - length: number; + file_index: number + contract: string + name: string + line: number + error: string + error_reason: string + code: string + op: string + length: number } interface CallTrace { - hash: string; - contract_name: string; - function_name: string; - function_pc: number; - function_op: string; - function_file_index: number; - function_code_start: number; - function_line_number: number; - function_code_length: number; - function_states: CallTraceFunctionState[]; - caller_pc: number; - caller_op: string; - call_type: string; - from: string; - from_balance: string; - to: string; - to_balance: string; - value: string; - caller: Caller; - block_timestamp: Date; - gas: number; - gas_used: number; - intrinsic_gas: number; - input: string; - decoded_input: Input[]; - state_diff: StateDiff[]; - logs: Log[]; - output: string; - decoded_output: FunctionVariableElement[]; - network_id: string; - calls: CallTraceCall[]; + hash: string + contract_name: string + function_name: string + function_pc: number + function_op: string + function_file_index: number + function_code_start: number + function_line_number: number + function_code_length: number + function_states: CallTraceFunctionState[] + caller_pc: number + caller_op: string + call_type: string + from: string + from_balance: string + to: string + to_balance: string + value: string + caller: Caller + block_timestamp: Date + gas: number + gas_used: number + intrinsic_gas: number + input: string + decoded_input: Input[] + state_diff: StateDiff[] + logs: Log[] + output: string + decoded_output: FunctionVariableElement[] + network_id: string + calls: CallTraceCall[] } interface Caller { - address: string; - balance: string; + address: string + balance: string } interface CallTraceCall { - hash: string; - contract_name: string; - function_name: string; - function_pc: number; - function_op: string; - function_file_index: number; - function_code_start: number; - function_line_number: number; - function_code_length: number; - function_states: CallTraceFunctionState[]; - function_variables: FunctionVariableElement[]; - caller_pc: number; - caller_op: string; - caller_file_index: number; - caller_line_number: number; - caller_code_start: number; - caller_code_length: number; - call_type: string; - from: string; - from_balance: null; - to: string; - to_balance: null; - value: null; - caller: Caller; - block_timestamp: Date; - gas: number; - gas_used: number; - input: string; - decoded_input: Input[]; - output: string; - decoded_output: FunctionVariableElement[]; - network_id: string; - calls: PurpleCall[]; + hash: string + contract_name: string + function_name: string + function_pc: number + function_op: string + function_file_index: number + function_code_start: number + function_line_number: number + function_code_length: number + function_states: CallTraceFunctionState[] + function_variables: FunctionVariableElement[] + caller_pc: number + caller_op: string + caller_file_index: number + caller_line_number: number + caller_code_start: number + caller_code_length: number + call_type: string + from: string + from_balance: null + to: string + to_balance: null + value: null + caller: Caller + block_timestamp: Date + gas: number + gas_used: number + input: string + decoded_input: Input[] + output: string + decoded_output: FunctionVariableElement[] + network_id: string + calls: PurpleCall[] } interface PurpleCall { - hash: string; - contract_name: string; - function_name: string; - function_pc: number; - function_op: string; - function_file_index: number; - function_code_start: number; - function_line_number: number; - function_code_length: number; - function_states?: FluffyFunctionState[]; - function_variables?: FunctionVariable[]; - caller_pc: number; - caller_op: string; - caller_file_index: number; - caller_line_number: number; - caller_code_start: number; - caller_code_length: number; - call_type: string; - from: string; - from_balance: null | string; - to: string; - to_balance: null | string; - value: null | string; - caller: Caller; - block_timestamp: Date; - gas: number; - gas_used: number; - refund_gas?: number; - input: string; - decoded_input: Input[]; - output: string; - decoded_output: FunctionVariable[] | null; - network_id: string; - calls: FluffyCall[] | null; + hash: string + contract_name: string + function_name: string + function_pc: number + function_op: string + function_file_index: number + function_code_start: number + function_line_number: number + function_code_length: number + function_states?: FluffyFunctionState[] + function_variables?: FunctionVariable[] + caller_pc: number + caller_op: string + caller_file_index: number + caller_line_number: number + caller_code_start: number + caller_code_length: number + call_type: string + from: string + from_balance: null | string + to: string + to_balance: null | string + value: null | string + caller: Caller + block_timestamp: Date + gas: number + gas_used: number + refund_gas?: number + input: string + decoded_input: Input[] + output: string + decoded_output: FunctionVariable[] | null + network_id: string + calls: FluffyCall[] | null } interface FluffyCall { - hash: string; - contract_name: string; - function_name?: string; - function_pc: number; - function_op: string; - function_file_index?: number; - function_code_start?: number; - function_line_number?: number; - function_code_length?: number; - function_states?: FluffyFunctionState[]; - function_variables?: FunctionVariable[]; - caller_pc: number; - caller_op: string; - caller_file_index: number; - caller_line_number: number; - caller_code_start: number; - caller_code_length: number; - call_type: string; - from: string; - from_balance: null | string; - to: string; - to_balance: null | string; - value: null | string; - caller?: Caller; - block_timestamp: Date; - gas: number; - gas_used: number; - input: string; - decoded_input?: FunctionVariable[]; - output: string; - decoded_output: PurpleDecodedOutput[] | null; - network_id: string; - calls: TentacledCall[] | null; - refund_gas?: number; + hash: string + contract_name: string + function_name?: string + function_pc: number + function_op: string + function_file_index?: number + function_code_start?: number + function_line_number?: number + function_code_length?: number + function_states?: FluffyFunctionState[] + function_variables?: FunctionVariable[] + caller_pc: number + caller_op: string + caller_file_index: number + caller_line_number: number + caller_code_start: number + caller_code_length: number + call_type: string + from: string + from_balance: null | string + to: string + to_balance: null | string + value: null | string + caller?: Caller + block_timestamp: Date + gas: number + gas_used: number + input: string + decoded_input?: FunctionVariable[] + output: string + decoded_output: PurpleDecodedOutput[] | null + network_id: string + calls: TentacledCall[] | null + refund_gas?: number } interface TentacledCall { - hash: string; - contract_name: string; - function_name: string; - function_pc: number; - function_op: string; - function_file_index: number; - function_code_start: number; - function_line_number: number; - function_code_length: number; - function_states: PurpleFunctionState[]; - caller_pc: number; - caller_op: string; - caller_file_index: number; - caller_line_number: number; - caller_code_start: number; - caller_code_length: number; - call_type: string; - from: string; - from_balance: null; - to: string; - to_balance: null; - value: null; - caller: Caller; - block_timestamp: Date; - gas: number; - gas_used: number; - input: string; - decoded_input: FunctionVariableElement[]; - output: string; - decoded_output: FunctionVariable[]; - network_id: string; - calls: null; + hash: string + contract_name: string + function_name: string + function_pc: number + function_op: string + function_file_index: number + function_code_start: number + function_line_number: number + function_code_length: number + function_states: PurpleFunctionState[] + caller_pc: number + caller_op: string + caller_file_index: number + caller_line_number: number + caller_code_start: number + caller_code_length: number + call_type: string + from: string + from_balance: null + to: string + to_balance: null + value: null + caller: Caller + block_timestamp: Date + gas: number + gas_used: number + input: string + decoded_input: FunctionVariableElement[] + output: string + decoded_output: FunctionVariable[] + network_id: string + calls: null } interface FunctionVariableElement { - soltype: SoltypeElement; - value: string; + soltype: SoltypeElement + value: string } interface FunctionVariable { - soltype: SoltypeElement; - value: PurpleValue | string; + soltype: SoltypeElement + value: PurpleValue | string } interface PurpleValue { - ballot: string; - basedOn: string; - configured: string; - currency: string; - cycleLimit: string; - discountRate: string; - duration: string; - fee: string; - id: string; - metadata: string; - number: string; - projectId: string; - start: string; - tapped: string; - target: string; - weight: string; + ballot: string + basedOn: string + configured: string + currency: string + cycleLimit: string + discountRate: string + duration: string + fee: string + id: string + metadata: string + number: string + projectId: string + start: string + tapped: string + target: string + weight: string } interface PurpleFunctionState { - soltype: SoltypeElement; - value: Record; + soltype: SoltypeElement + value: Record } interface PurpleDecodedOutput { - soltype: SoltypeElement; - value: boolean | PurpleValue | string; + soltype: SoltypeElement + value: boolean | PurpleValue | string } interface FluffyFunctionState { - soltype: PurpleSoltype; - value: Record; + soltype: PurpleSoltype + value: Record } interface PurpleSoltype { - name: string; - type: SoltypeType; - storage_location: StorageLocation; - components?: null; - offset: number; - index: string; - indexed: boolean; + name: string + type: SoltypeType + storage_location: StorageLocation + components?: null + offset: number + index: string + indexed: boolean } interface Input { - soltype: SoltypeElement | null; - value: boolean | string; + soltype: SoltypeElement | null + value: boolean | string } interface CallTraceFunctionState { - soltype: PurpleSoltype; - value: Record; + soltype: PurpleSoltype + value: Record } interface Log { - name: string | null; - anonymous: boolean; - inputs: Input[]; - raw: LogRaw; + name: string | null + anonymous: boolean + inputs: Input[] + raw: LogRaw } interface LogRaw { - address: string; - topics: string[]; - data: string; + address: string + topics: string[] + data: string } export interface StateDiff { - address: string; - soltype: SoltypeElement | null; - original: string | number | boolean | Record | unknown[] | null; - dirty: string | number | boolean | Record | unknown[] | null; - raw?: RawElement[]; + address: string + soltype: SoltypeElement | null + original: string | number | boolean | Record | unknown[] | null + dirty: string | number | boolean | Record | unknown[] | null + raw?: RawElement[] } export interface RawElement { - address: string; - key: string; - original: string; - dirty: string; + address: string + key: string + original: string + dirty: string } diff --git a/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepository.ts b/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepository.ts index 0c5a4db7..aef4d021 100644 --- a/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepository.ts +++ b/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepository.ts @@ -1,20 +1,20 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' -export const tokenBalancesRepositorySymbol = Symbol.for('TokenBalancesRepository'); +export const tokenBalancesRepositorySymbol = Symbol.for('TokenBalancesRepository') export interface TokenBalanceParams { - address: string; - chainId: SupportedChainId, + address: string + chainId: SupportedChainId } -export type TokenBalancesResponse = Record | null; +export type TokenBalancesResponse = Record | null export interface TokenBalancesRepository { - getTokenBalances({ address, chainId }: TokenBalanceParams): Promise; + getTokenBalances({ address, chainId }: TokenBalanceParams): Promise } export class TokenBalancesNoop implements TokenBalancesRepository { async getTokenBalances(_params: TokenBalanceParams): Promise { - return null; + return null } -} \ No newline at end of file +} diff --git a/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepositoryAlchemy.ts b/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepositoryAlchemy.ts index 5b1d66f8..9cf4ae9c 100644 --- a/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepositoryAlchemy.ts +++ b/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepositoryAlchemy.ts @@ -1,38 +1,36 @@ -import { injectable } from 'inversify'; -import { TokenBalanceParams, TokenBalancesRepository, TokenBalancesResponse } from './TokenBalancesRepository'; -import { ALCHEMY_API_KEY, ALCHEMY_CLIENT_NETWORK_MAPPING, getAlchemyApiUrl } from '../../datasources/alchemy'; -import { EVM_NATIVE_CURRENCY_ADDRESS, ZERO_ADDRESS } from '@cowprotocol/cow-sdk'; -import { logger } from '@cowprotocol/shared'; +import { injectable } from 'inversify' +import { TokenBalanceParams, TokenBalancesRepository, TokenBalancesResponse } from './TokenBalancesRepository' +import { ALCHEMY_API_KEY, ALCHEMY_CLIENT_NETWORK_MAPPING, getAlchemyApiUrl } from '../../datasources/alchemy' +import { EVM_NATIVE_CURRENCY_ADDRESS, ZERO_ADDRESS } from '@cowprotocol/cow-sdk' +import { logger } from '@cowprotocol/shared' -const JSON_RPC_VERSION = '2.0'; -const JSON_RPC_REQUEST_ID = 1; -const REQUEST_TIMEOUT_MS = 10_000; -const TOKEN_SPEC = ['erc20', 'NATIVE_TOKEN'] as const; +const JSON_RPC_VERSION = '2.0' +const JSON_RPC_REQUEST_ID = 1 +const REQUEST_TIMEOUT_MS = 10_000 +const TOKEN_SPEC = ['erc20', 'NATIVE_TOKEN'] as const type AlchemyTokenBalance = { - contractAddress: string | null; - tokenBalance: string; - error?: string; -}; + contractAddress: string | null + tokenBalance: string + error?: string +} type AlchemyGetTokenBalancesResponse = { - jsonrpc: string; - id: number; + jsonrpc: string + id: number result: { - address: string; - tokenBalances: AlchemyTokenBalance[]; - pageKey?: string; - }; -}; - -function isAlchemyGetTokenBalancesResponse( - data: unknown -): data is AlchemyGetTokenBalancesResponse { + address: string + tokenBalances: AlchemyTokenBalance[] + pageKey?: string + } +} + +function isAlchemyGetTokenBalancesResponse(data: unknown): data is AlchemyGetTokenBalancesResponse { if (!data || typeof data !== 'object') { - return false; + return false } - const response = data as AlchemyGetTokenBalancesResponse; + const response = data as AlchemyGetTokenBalancesResponse if ( response.jsonrpc !== JSON_RPC_VERSION || @@ -40,90 +38,82 @@ function isAlchemyGetTokenBalancesResponse( !response.result || typeof response.result !== 'object' ) { - return false; + return false } - return Array.isArray(response.result.tokenBalances); + return Array.isArray(response.result.tokenBalances) } @injectable() export class TokenBalancesRepositoryAlchemy implements TokenBalancesRepository { - async getTokenBalances({ - address, - chainId, - }: TokenBalanceParams): Promise { - const network = ALCHEMY_CLIENT_NETWORK_MAPPING[chainId]; + async getTokenBalances({ address, chainId }: TokenBalanceParams): Promise { + const network = ALCHEMY_CLIENT_NETWORK_MAPPING[chainId] if (!network) { - throw new Error('Unsupported chain'); + throw new Error('Unsupported chain') } - const response = await this.requestBalanceData(address, network); + const response = await this.requestBalanceData(address, network) if (!response.ok) { - throw new Error( - `Alchemy API error: ${response.status} ${response.statusText}` - ); + throw new Error(`Alchemy API error: ${response.status} ${response.statusText}`) } - const asJson = await response.json(); + const asJson = await response.json() if (!isAlchemyGetTokenBalancesResponse(asJson)) { - throw new Error('Invalid Alchemy response'); + throw new Error('Invalid Alchemy response') } return asJson.result.tokenBalances.reduce((acc, tokenBalance) => { if (tokenBalance.error) { - return acc; + return acc } const contractAddress = // alchemy return null for native token tokenBalance.contractAddress === 'null' || - tokenBalance.contractAddress === null || - tokenBalance.contractAddress === ZERO_ADDRESS + tokenBalance.contractAddress === null || + tokenBalance.contractAddress === ZERO_ADDRESS ? EVM_NATIVE_CURRENCY_ADDRESS.toLowerCase() - : tokenBalance.contractAddress.toLowerCase(); + : tokenBalance.contractAddress.toLowerCase() // Convert hex balance to decimal string // Alchemy returns hex string - const balanceHex = tokenBalance.tokenBalance; + const balanceHex = tokenBalance.tokenBalance if (balanceHex && balanceHex !== '0x') { try { - const balanceBigInt = BigInt(balanceHex); + const balanceBigInt = BigInt(balanceHex) if (balanceBigInt !== 0n) { - acc[contractAddress] = balanceBigInt.toString(); + acc[contractAddress] = balanceBigInt.toString() } } catch (error) { logger.error( error, `[TokenBalancesRepository] Error processing balance for token ${contractAddress} on chain ${chainId}. Value is ${balanceHex}.` - ); - return acc; + ) + return acc } } - return acc; - }, {} as Record); + return acc + }, {} as Record) } - private async requestBalanceData( - address: string, - network: string - ): Promise { + private async requestBalanceData(address: string, network: string): Promise { if (!ALCHEMY_API_KEY) { - throw new Error('ALCHEMY_API_KEY is not set'); + throw new Error('ALCHEMY_API_KEY is not set') } - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS) - const apiUrl = getAlchemyApiUrl(network, ALCHEMY_API_KEY); + const apiUrl = getAlchemyApiUrl(network, ALCHEMY_API_KEY) const requestBody = { jsonrpc: JSON_RPC_VERSION, method: 'alchemy_getTokenBalances', params: [address, TOKEN_SPEC], id: JSON_RPC_REQUEST_ID, - }; + } try { const response = await fetch(apiUrl, { @@ -133,11 +123,11 @@ export class TokenBalancesRepositoryAlchemy implements TokenBalancesRepository { }, body: JSON.stringify(requestBody), signal: controller.signal, - }); + }) - return response; + return response } finally { - clearTimeout(timeoutId); + clearTimeout(timeoutId) } } } diff --git a/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepositoryMoralis.ts b/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepositoryMoralis.ts index 9241917b..8f3ad7c7 100644 --- a/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepositoryMoralis.ts +++ b/libs/repositories/src/repos/TokenBalancesRepository/TokenBalancesRepositoryMoralis.ts @@ -1,96 +1,77 @@ -import { injectable } from 'inversify'; -import { - TokenBalanceParams, - TokenBalancesRepository, - TokenBalancesResponse, -} from './TokenBalancesRepository'; -import { - MORALIS_API_BASE_URL, - MORALIS_API_KEY, - MORALIS_CLIENT_NETWORK_MAPPING, -} from '../../datasources/moralis'; +import { injectable } from 'inversify' +import { TokenBalanceParams, TokenBalancesRepository, TokenBalancesResponse } from './TokenBalancesRepository' +import { MORALIS_API_BASE_URL, MORALIS_API_KEY, MORALIS_CLIENT_NETWORK_MAPPING } from '../../datasources/moralis' type MoralisBalanceTokenResponse = { - token_address: string; - symbol: string; - name: string; - logo: string; - thumbnail: string; - decimals: number; - balance: string; - possible_spam: boolean; - verified_contract: boolean; - total_supply: string; - total_supply_formatted: string; - percentage_relative_to_total_supply: number; - security_score: number; -}; + token_address: string + symbol: string + name: string + logo: string + thumbnail: string + decimals: number + balance: string + possible_spam: boolean + verified_contract: boolean + total_supply: string + total_supply_formatted: string + percentage_relative_to_total_supply: number + security_score: number +} type MoralisBalanceResponse = { - result: MoralisBalanceTokenResponse[]; -}; + result: MoralisBalanceTokenResponse[] +} -function isMoralisBalanceResponse( - data: unknown -): data is MoralisBalanceResponse { +function isMoralisBalanceResponse(data: unknown): data is MoralisBalanceResponse { if (!data || typeof data !== 'object') { - return false; + return false } - const response = data as MoralisBalanceResponse; + const response = data as MoralisBalanceResponse if (!Array.isArray(response.result)) { - return false; + return false } - return !!response.result; + return !!response.result } @injectable() export class TokenBalancesRepositoryMoralis implements TokenBalancesRepository { - async getTokenBalances({ - address, - chainId, - }: TokenBalanceParams): Promise { + async getTokenBalances({ address, chainId }: TokenBalanceParams): Promise { // todo // Right now this mapping only has the supported chains. // Would be great to implement it like the usd estimation endpoint, // where chains other than supported ones are supported. // This will allow us to enable balance checking for target chains that we don't support as swap chains, // such as BTC, Solana etc - const network = MORALIS_CLIENT_NETWORK_MAPPING[chainId]; + const network = MORALIS_CLIENT_NETWORK_MAPPING[chainId] if (!network) { - throw new Error('Unsupported chain'); + throw new Error('Unsupported chain') } - const response = await this.requestBalanceData(address, network); + const response = await this.requestBalanceData(address, network) if (!response.ok) { - throw new Error( - `Moralis API error: ${response.status} ${response.statusText}` - ); + throw new Error(`Moralis API error: ${response.status} ${response.statusText}`) } - const asJson = await response.json(); + const asJson = await response.json() if (!isMoralisBalanceResponse(asJson)) { - throw new Error('Invalid Moralis response'); + throw new Error('Invalid Moralis response') } return asJson.result.reduce((acc, tokenBalanceItem) => { - acc[tokenBalanceItem.token_address.toLowerCase()] = - tokenBalanceItem.balance; - return acc; - }, {} as Record); + acc[tokenBalanceItem.token_address.toLowerCase()] = tokenBalanceItem.balance + return acc + }, {} as Record) } - private async requestBalanceData( - address: string, - network: string - ): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10_000); + private async requestBalanceData(address: string, network: string): Promise { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 10_000) - const url = `${MORALIS_API_BASE_URL}/v2.2/wallets/${address}/tokens?chain=${network}&order=DESC`; + const url = `${MORALIS_API_BASE_URL}/v2.2/wallets/${address}/tokens?chain=${network}&order=DESC` try { return fetch(url, { method: 'GET', @@ -99,9 +80,9 @@ export class TokenBalancesRepositoryMoralis implements TokenBalancesRepository { 'X-API-Key': `${MORALIS_API_KEY}`, }, signal: controller.signal, - }); + }) } finally { - clearTimeout(timeoutId); + clearTimeout(timeoutId) } } } diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepository.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepository.ts index df7a54a9..5acd741b 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepository.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepository.ts @@ -1,25 +1,19 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' -export const tokenHolderRepositorySymbol = Symbol.for('TokenHolderRepository'); +export const tokenHolderRepositorySymbol = Symbol.for('TokenHolderRepository') export interface TokenHolderPoint { - address: string; - balance: string; + address: string + balance: string } export interface TokenHolderRepository { - getTopTokenHolders( - chainId: SupportedChainId, - tokenAddress: string - ): Promise; + getTopTokenHolders(chainId: SupportedChainId, tokenAddress: string): Promise } export class TokenHolderRepositoryNoop implements TokenHolderRepository { - async getTopTokenHolders( - _chainId: SupportedChainId, - _tokenAddress: string - ): Promise { - return null; + async getTopTokenHolders(_chainId: SupportedChainId, _tokenAddress: string): Promise { + return null } } diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryCache.spec.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryCache.spec.ts index 71ed6f82..1f55377d 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryCache.spec.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryCache.spec.ts @@ -1,108 +1,105 @@ -import { TokenHolderRepositoryCache } from './TokenHolderRepositoryCache'; -import IORedis from 'ioredis'; -import { TokenHolderRepository } from './TokenHolderRepository'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { NULL_ADDRESS, WETH } from '../../../test/mock'; -import { CacheRepositoryRedis } from '../CacheRepository/CacheRepositoryRedis'; +import { TokenHolderRepositoryCache } from './TokenHolderRepositoryCache' +import IORedis from 'ioredis' +import { TokenHolderRepository } from './TokenHolderRepository' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { NULL_ADDRESS, WETH } from '../../../test/mock' +import { CacheRepositoryRedis } from '../CacheRepository/CacheRepositoryRedis' -const CACHE_VALUE_SECONDS = 10; -const CACHE_NULL_SECONDS = 20; +const CACHE_VALUE_SECONDS = 10 +const CACHE_NULL_SECONDS = 20 -const wethLowercase = WETH.toLocaleLowerCase(); -const chainId = SupportedChainId.MAINNET; +const wethLowercase = WETH.toLocaleLowerCase() +const chainId = SupportedChainId.MAINNET jest.mock('ioredis', () => { return jest.fn().mockImplementation(() => ({ get: jest.fn(), set: jest.fn(), - })); -}); + })) +}) describe('TokenHolderRepositoryCache', () => { - let tokenHoldersRepositoryCache: TokenHolderRepositoryCache; - let redisMock: jest.Mocked; - let proxyMock: jest.Mocked; + let tokenHoldersRepositoryCache: TokenHolderRepositoryCache + let redisMock: jest.Mocked + let proxyMock: jest.Mocked beforeEach(() => { - redisMock = new IORedis() as jest.Mocked; + redisMock = new IORedis() as jest.Mocked proxyMock = { getTopTokenHolders: jest.fn(), - }; - const cacheRepository = new CacheRepositoryRedis(redisMock); + } + const cacheRepository = new CacheRepositoryRedis(redisMock) tokenHoldersRepositoryCache = new TokenHolderRepositoryCache( proxyMock, cacheRepository, 'test-cache', CACHE_VALUE_SECONDS, CACHE_NULL_SECONDS - ); - }); + ) + }) const HOLDERS_1 = [ { address: NULL_ADDRESS, balance: '1', }, - ]; + ] const HOLDERS_2 = [ { address: NULL_ADDRESS, balance: '2', }, - ]; + ] - const HOLDERS_1_STRING = JSON.stringify(HOLDERS_1); - const HOLDERS_2_STRING = JSON.stringify(HOLDERS_2); + const HOLDERS_1_STRING = JSON.stringify(HOLDERS_1) + const HOLDERS_2_STRING = JSON.stringify(HOLDERS_2) describe('getTopTokenHolders', () => { it('should return token holders from cache', async () => { // GIVEN: Cached value HOLDERS_1 - redisMock.get.mockResolvedValue(HOLDERS_1_STRING); + redisMock.get.mockResolvedValue(HOLDERS_1_STRING) // GIVEN: proxy returns HOLDERS_2 - proxyMock.getTopTokenHolders.mockResolvedValue(HOLDERS_2); + proxyMock.getTopTokenHolders.mockResolvedValue(HOLDERS_2) // WHEN: Get Top Token Holders - const topTokenHolder = - await tokenHoldersRepositoryCache.getTopTokenHolders(chainId, WETH); + const topTokenHolder = await tokenHoldersRepositoryCache.getTopTokenHolders(chainId, WETH) - expect(topTokenHolder).toStrictEqual(HOLDERS_1); - expect(proxyMock.getTopTokenHolders).not.toHaveBeenCalled(); - }); + expect(topTokenHolder).toStrictEqual(HOLDERS_1) + expect(proxyMock.getTopTokenHolders).not.toHaveBeenCalled() + }) it('should return NULL from cache', async () => { // GIVEN: Cached value 'null' - redisMock.get.mockResolvedValue('null'); + redisMock.get.mockResolvedValue('null') // GIVEN: proxy returns HOLDERS_2 - proxyMock.getTopTokenHolders.mockResolvedValue(HOLDERS_2); + proxyMock.getTopTokenHolders.mockResolvedValue(HOLDERS_2) // WHEN: Get Top Token Holders - const topTokenHolder = - await tokenHoldersRepositoryCache.getTopTokenHolders(chainId, WETH); + const topTokenHolder = await tokenHoldersRepositoryCache.getTopTokenHolders(chainId, WETH) // THEN: We get the cached value - expect(topTokenHolder).toEqual(null); - expect(proxyMock.getTopTokenHolders).not.toHaveBeenCalled(); - }); + expect(topTokenHolder).toEqual(null) + expect(proxyMock.getTopTokenHolders).not.toHaveBeenCalled() + }) it('should call the proxy if no cache, then cache the value', async () => { // GIVEN: The value is not cached - redisMock.get.mockResolvedValue(null); + redisMock.get.mockResolvedValue(null) // GIVEN: proxy returns HOLDERS_2 - proxyMock.getTopTokenHolders.mockResolvedValue(HOLDERS_2); + proxyMock.getTopTokenHolders.mockResolvedValue(HOLDERS_2) // WHEN: Get Top Token Holders - const topTokenHolder = - await tokenHoldersRepositoryCache.getTopTokenHolders(chainId, WETH); + const topTokenHolder = await tokenHoldersRepositoryCache.getTopTokenHolders(chainId, WETH) // THEN: The holders matches the result from the proxy - expect(topTokenHolder).toStrictEqual(HOLDERS_2); + expect(topTokenHolder).toStrictEqual(HOLDERS_2) // THEN: The proxy has been called once - expect(proxyMock.getTopTokenHolders).toHaveBeenCalledWith(chainId, WETH); + expect(proxyMock.getTopTokenHolders).toHaveBeenCalledWith(chainId, WETH) // THEN: The value returned by the proxy is cached expect(redisMock.set).toHaveBeenCalledWith( @@ -110,27 +107,24 @@ describe('TokenHolderRepositoryCache', () => { HOLDERS_2_STRING, 'EX', CACHE_VALUE_SECONDS - ); - }); + ) + }) it('should call the proxy if no cache, then cache the NULL', async () => { // GIVEN: The value is not cached - redisMock.get.mockResolvedValue(null); + redisMock.get.mockResolvedValue(null) // GIVEN: proxy returns null - proxyMock.getTopTokenHolders.mockResolvedValue(null); + proxyMock.getTopTokenHolders.mockResolvedValue(null) // WHEN: Get Top Token Holders - const price = await tokenHoldersRepositoryCache.getTopTokenHolders( - chainId, - WETH - ); + const price = await tokenHoldersRepositoryCache.getTopTokenHolders(chainId, WETH) // THEN: The price matches the result from the proxy - expect(price).toEqual(null); + expect(price).toEqual(null) // THEN: The proxy has been called once - expect(proxyMock.getTopTokenHolders).toHaveBeenCalledWith(chainId, WETH); + expect(proxyMock.getTopTokenHolders).toHaveBeenCalledWith(chainId, WETH) // THEN: The value returned by the proxy is cached expect(redisMock.set).toHaveBeenCalledWith( @@ -138,46 +132,40 @@ describe('TokenHolderRepositoryCache', () => { 'null', 'EX', CACHE_NULL_SECONDS - ); - }); + ) + }) it('should return the cached value, even if the proxy throws', async () => { // GIVEN: Cached value HOLDERS_1_STRING - redisMock.get.mockResolvedValue(HOLDERS_1_STRING); + redisMock.get.mockResolvedValue(HOLDERS_1_STRING) // GIVEN: The proxy throws an awful error proxyMock.getTopTokenHolders.mockImplementation(() => { - throw new Error('💥 Booom!'); - }); + throw new Error('💥 Booom!') + }) // WHEN: Get Top Token Holders - const tokenHolders = await tokenHoldersRepositoryCache.getTopTokenHolders( - chainId, - WETH - ); + const tokenHolders = await tokenHoldersRepositoryCache.getTopTokenHolders(chainId, WETH) // THEN: The holders matches the result from the proxy - expect(tokenHolders).toStrictEqual(HOLDERS_1); - expect(proxyMock.getTopTokenHolders).not.toHaveBeenCalled(); - }); + expect(tokenHolders).toStrictEqual(HOLDERS_1) + expect(proxyMock.getTopTokenHolders).not.toHaveBeenCalled() + }) it('should throw if the proxy throws and there is no cache available', async () => { // GIVEN: The value is not cached - redisMock.get.mockResolvedValue(null); + redisMock.get.mockResolvedValue(null) // GIVEN: The proxy throws an awful error proxyMock.getTopTokenHolders.mockImplementation(async () => { - throw new Error('💥 Booom!'); - }); + throw new Error('💥 Booom!') + }) // WHEN: Get Top Token Holders - const tokenHolderPromise = tokenHoldersRepositoryCache.getTopTokenHolders( - chainId, - WETH - ); + const tokenHolderPromise = tokenHoldersRepositoryCache.getTopTokenHolders(chainId, WETH) // THEN: The call throws an awful error - expect(tokenHolderPromise).rejects.toThrow('💥 Booom!'); - }); - }); -}); + expect(tokenHolderPromise).rejects.toThrow('💥 Booom!') + }) + }) +}) diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryCache.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryCache.ts index 0111ee60..07358bf6 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryCache.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryCache.ts @@ -1,17 +1,14 @@ -import { injectable } from 'inversify'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { CacheRepository } from '../CacheRepository/CacheRepository'; -import { getCacheKey, PartialCacheKey } from '../../utils/cache'; -import { - TokenHolderPoint, - TokenHolderRepository, -} from './TokenHolderRepository'; +import { injectable } from 'inversify' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { CacheRepository } from '../CacheRepository/CacheRepository' +import { getCacheKey, PartialCacheKey } from '../../utils/cache' +import { TokenHolderPoint, TokenHolderRepository } from './TokenHolderRepository' -const NULL_VALUE = 'null'; +const NULL_VALUE = 'null' @injectable() export class TokenHolderRepositoryCache implements TokenHolderRepository { - private baseCacheKey: PartialCacheKey[]; + private baseCacheKey: PartialCacheKey[] constructor( private proxy: TokenHolderRepository, @@ -20,68 +17,47 @@ export class TokenHolderRepositoryCache implements TokenHolderRepository { private cacheTimeValueSeconds: number, private cacheTimeNullSeconds: number ) { - this.baseCacheKey = ['repos', this.cacheName]; + this.baseCacheKey = ['repos', this.cacheName] } - async getTopTokenHolders( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { + async getTopTokenHolders(chainId: SupportedChainId, tokenAddress: string): Promise { // Get price from cache - const key = getCacheKey( - ...this.baseCacheKey, - 'usd-price', - chainId, - tokenAddress - ); + const key = getCacheKey(...this.baseCacheKey, 'usd-price', chainId, tokenAddress) const holdersCache = await this.getValueFromCache({ key, - }); + }) if (holdersCache !== undefined) { - return holdersCache; + return holdersCache } - const tokenHolders = await this.proxy.getTopTokenHolders( - chainId, - tokenAddress - ); + const tokenHolders = await this.proxy.getTopTokenHolders(chainId, tokenAddress) // Cache price (or absence of it) this.cacheValue({ key, value: tokenHolders || null, - }); + }) - return tokenHolders; + return tokenHolders } - private async getValueFromCache(props: { - key: string; - }): Promise { - const { key } = props; + private async getValueFromCache(props: { key: string }): Promise { + const { key } = props - const valueString = await this.cache.get(key); + const valueString = await this.cache.get(key) if (valueString) { - return valueString === NULL_VALUE ? null : JSON.parse(valueString); + return valueString === NULL_VALUE ? null : JSON.parse(valueString) } - return undefined; + return undefined } - private async cacheValue(props: { - key: string; - value: TokenHolderPoint[] | null; - }): Promise { - const { key, value } = props; + private async cacheValue(props: { key: string; value: TokenHolderPoint[] | null }): Promise { + const { key, value } = props - const cacheTimeSeconds = - value === null ? this.cacheTimeNullSeconds : this.cacheTimeValueSeconds; + const cacheTimeSeconds = value === null ? this.cacheTimeNullSeconds : this.cacheTimeValueSeconds - await this.cache.set( - key, - value === null ? NULL_VALUE : JSON.stringify(value), - cacheTimeSeconds - ); + await this.cache.set(key, value === null ? NULL_VALUE : JSON.stringify(value), cacheTimeSeconds) } } diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryEthplorer.test.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryEthplorer.test.ts index 82a9010a..dc05de0c 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryEthplorer.test.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryEthplorer.test.ts @@ -1,88 +1,61 @@ -import { Container } from 'inversify'; -import { TokenHolderRepositoryEthplorer } from './TokenHolderRepositoryEthplorer'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { WETH, NULL_ADDRESS } from '../../../test/mock'; -import { ETHPLORER_API_KEY } from '../../datasources/ethplorer'; +import { Container } from 'inversify' +import { TokenHolderRepositoryEthplorer } from './TokenHolderRepositoryEthplorer' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { WETH, NULL_ADDRESS } from '../../../test/mock' +import { ETHPLORER_API_KEY } from '../../datasources/ethplorer' // The tests are integration tests and require ENV variables describe.skip('TokenHolderRepositoryEthplorer', () => { - let tokenHolderRepositoryEthplorer: TokenHolderRepositoryEthplorer; + let tokenHolderRepositoryEthplorer: TokenHolderRepositoryEthplorer beforeAll(() => { - const container = new Container(); - container - .bind(TokenHolderRepositoryEthplorer) - .to(TokenHolderRepositoryEthplorer); - tokenHolderRepositoryEthplorer = container.get( - TokenHolderRepositoryEthplorer - ); - expect(ETHPLORER_API_KEY).toBeDefined(); - }); + const container = new Container() + container.bind(TokenHolderRepositoryEthplorer).to(TokenHolderRepositoryEthplorer) + tokenHolderRepositoryEthplorer = container.get(TokenHolderRepositoryEthplorer) + expect(ETHPLORER_API_KEY).toBeDefined() + }) describe('getTopTokenHolders', () => { it('should return the top token holders of WETH', async () => { - const tokenHolders = - await tokenHolderRepositoryEthplorer.getTopTokenHolders( - SupportedChainId.MAINNET, - WETH - ); + const tokenHolders = await tokenHolderRepositoryEthplorer.getTopTokenHolders(SupportedChainId.MAINNET, WETH) - expect(tokenHolders?.length).toBeGreaterThan(0); - expect(tokenHolders?.[0].address).toBeDefined(); - expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(0); - expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan( - Number(tokenHolders?.[1].balance) - ); - }, 100000); + expect(tokenHolders?.length).toBeGreaterThan(0) + expect(tokenHolders?.[0].address).toBeDefined() + expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(0) + expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(Number(tokenHolders?.[1].balance)) + }, 100000) it('should return null for an unknown token', async () => { - const tokenHolders = - await tokenHolderRepositoryEthplorer.getTopTokenHolders( - SupportedChainId.MAINNET, - NULL_ADDRESS - ); + const tokenHolders = await tokenHolderRepositoryEthplorer.getTopTokenHolders( + SupportedChainId.MAINNET, + NULL_ADDRESS + ) - expect(tokenHolders).toBeNull(); - }, 100000); + expect(tokenHolders).toBeNull() + }, 100000) it('should return null for gnosis chain', async () => { - const tokenHolders = - await tokenHolderRepositoryEthplorer.getTopTokenHolders( - SupportedChainId.GNOSIS_CHAIN, - WETH - ); + const tokenHolders = await tokenHolderRepositoryEthplorer.getTopTokenHolders(SupportedChainId.GNOSIS_CHAIN, WETH) - expect(tokenHolders).toBeNull(); - }, 100000); + expect(tokenHolders).toBeNull() + }, 100000) it('should return null for arbitrum one', async () => { - const tokenHolders = - await tokenHolderRepositoryEthplorer.getTopTokenHolders( - SupportedChainId.ARBITRUM_ONE, - WETH - ); + const tokenHolders = await tokenHolderRepositoryEthplorer.getTopTokenHolders(SupportedChainId.ARBITRUM_ONE, WETH) - expect(tokenHolders).toBeNull(); - }, 100000); + expect(tokenHolders).toBeNull() + }, 100000) it('should return null for polygon', async () => { - const tokenHolders = - await tokenHolderRepositoryEthplorer.getTopTokenHolders( - SupportedChainId.POLYGON, - WETH - ); + const tokenHolders = await tokenHolderRepositoryEthplorer.getTopTokenHolders(SupportedChainId.POLYGON, WETH) - expect(tokenHolders).toBeNull(); - }, 100000); + expect(tokenHolders).toBeNull() + }, 100000) it('should return null for avalanche', async () => { - const tokenHolders = - await tokenHolderRepositoryEthplorer.getTopTokenHolders( - SupportedChainId.AVALANCHE, - WETH - ); + const tokenHolders = await tokenHolderRepositoryEthplorer.getTopTokenHolders(SupportedChainId.AVALANCHE, WETH) - expect(tokenHolders).toBeNull(); - }, 100000); - }); -}); + expect(tokenHolders).toBeNull() + }, 100000) + }) +}) diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryEthplorer.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryEthplorer.ts index 4bd396d3..068f5a52 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryEthplorer.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryEthplorer.ts @@ -1,63 +1,51 @@ -import { injectable } from 'inversify'; -import { - TokenHolderPoint, - TokenHolderRepository, -} from './TokenHolderRepository'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { - ETHPLORER_API_KEY, - ETHPLORER_BASE_URL, -} from '../../datasources/ethplorer'; +import { injectable } from 'inversify' +import { TokenHolderPoint, TokenHolderRepository } from './TokenHolderRepository' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { ETHPLORER_API_KEY, ETHPLORER_BASE_URL } from '../../datasources/ethplorer' interface EthplorerSuccess { holders: { - address: string; - balance: number; - share: number; - rawBalance: string; - }[]; + address: string + balance: number + share: number + rawBalance: string + }[] } interface EthplorerError { error: { - message: string; - code: number; - }; + message: string + code: number + } } @injectable() export class TokenHolderRepositoryEthplorer implements TokenHolderRepository { - async getTopTokenHolders( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { - const baseAPI = ETHPLORER_BASE_URL[chainId]; + async getTopTokenHolders(chainId: SupportedChainId, tokenAddress: string): Promise { + const baseAPI = ETHPLORER_BASE_URL[chainId] if (!baseAPI) { - return null; + return null } const searchParams = new URLSearchParams({ apiKey: ETHPLORER_API_KEY, limit: '100', - }); - - const response = await fetch( - `${baseAPI}/getTopTokenHolders/${tokenAddress}?${searchParams}`, - { - method: 'GET', - } - ) + }) + + const response = await fetch(`${baseAPI}/getTopTokenHolders/${tokenAddress}?${searchParams}`, { + method: 'GET', + }) .then((res) => res.json() as Promise) - .catch((e) => e as EthplorerError); + .catch((e) => e as EthplorerError) if ('error' in response || !response.holders.length) { - return null; + return null } return response.holders.map((item) => ({ address: item.address, balance: item.rawBalance, - })); + })) } } diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryFallback.spec.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryFallback.spec.ts index c53daf48..aec808fc 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryFallback.spec.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryFallback.spec.ts @@ -1,45 +1,45 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { TokenHolderRepository } from './TokenHolderRepository'; -import { TokenHolderRepositoryFallback } from './TokenHolderRepositoryFallback'; -import { NULL_ADDRESS, WETH } from '../../../test/mock'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { TokenHolderRepository } from './TokenHolderRepository' +import { TokenHolderRepositoryFallback } from './TokenHolderRepositoryFallback' +import { NULL_ADDRESS, WETH } from '../../../test/mock' const firstRepositoryResult = [ { address: NULL_ADDRESS, balance: '1', }, -]; +] const secondRepositoryResult = [ { address: NULL_ADDRESS, balance: '2', }, -]; +] class TokenHolderRepositoryMock_1 implements TokenHolderRepository { async getTopTokenHolders() { - return firstRepositoryResult; + return firstRepositoryResult } } class TokenHolderRepositoryMock_2 implements TokenHolderRepository { async getTopTokenHolders() { - return secondRepositoryResult; + return secondRepositoryResult } } class TokenHolderRepositoryMock_null implements TokenHolderRepository { async getTopTokenHolders() { - return null; + return null } } -const PARAMS_PRICE = [SupportedChainId.MAINNET, WETH] as const; +const PARAMS_PRICE = [SupportedChainId.MAINNET, WETH] as const -const tokenHoldersRepositoryMock_1 = new TokenHolderRepositoryMock_1(); -const tokenHoldersRepositoryMock_2 = new TokenHolderRepositoryMock_2(); -const tokenHoldersRepositoryMock_null = new TokenHolderRepositoryMock_null(); +const tokenHoldersRepositoryMock_1 = new TokenHolderRepositoryMock_1() +const tokenHoldersRepositoryMock_2 = new TokenHolderRepositoryMock_2() +const tokenHoldersRepositoryMock_null = new TokenHolderRepositoryMock_null() describe('TokenHolderRepositoryCoingecko', () => { describe('getTopTokenHolders', () => { @@ -47,70 +47,53 @@ describe('TokenHolderRepositoryCoingecko', () => { let tokenHoldersRepositoryFallback = new TokenHolderRepositoryFallback([ tokenHoldersRepositoryMock_1, tokenHoldersRepositoryMock_2, - ]); + ]) - let tokenHolders = - await tokenHoldersRepositoryFallback.getTopTokenHolders( - ...PARAMS_PRICE - ); + let tokenHolders = await tokenHoldersRepositoryFallback.getTopTokenHolders(...PARAMS_PRICE) - expect(tokenHolders).toStrictEqual(firstRepositoryResult); + expect(tokenHolders).toStrictEqual(firstRepositoryResult) tokenHoldersRepositoryFallback = new TokenHolderRepositoryFallback([ tokenHoldersRepositoryMock_2, tokenHoldersRepositoryMock_1, - ]); + ]) - tokenHolders = await tokenHoldersRepositoryFallback.getTopTokenHolders( - ...PARAMS_PRICE - ); + tokenHolders = await tokenHoldersRepositoryFallback.getTopTokenHolders(...PARAMS_PRICE) - expect(tokenHolders).toStrictEqual(secondRepositoryResult); + expect(tokenHolders).toStrictEqual(secondRepositoryResult) tokenHoldersRepositoryFallback = new TokenHolderRepositoryFallback([ tokenHoldersRepositoryMock_1, tokenHoldersRepositoryMock_null, - ]); + ]) - tokenHolders = await tokenHoldersRepositoryFallback.getTopTokenHolders( - ...PARAMS_PRICE - ); - expect(tokenHolders).toStrictEqual(firstRepositoryResult); - }); + tokenHolders = await tokenHoldersRepositoryFallback.getTopTokenHolders(...PARAMS_PRICE) + expect(tokenHolders).toStrictEqual(firstRepositoryResult) + }) it('Returns second repo holders when null', async () => { const tokenHoldersRepositoryFallback = new TokenHolderRepositoryFallback([ tokenHoldersRepositoryMock_null, tokenHoldersRepositoryMock_1, - ]); + ]) - const tokenHolders = - await tokenHoldersRepositoryFallback.getTopTokenHolders( - ...PARAMS_PRICE - ); - expect(tokenHolders).toStrictEqual(firstRepositoryResult); - }); + const tokenHolders = await tokenHoldersRepositoryFallback.getTopTokenHolders(...PARAMS_PRICE) + expect(tokenHolders).toStrictEqual(firstRepositoryResult) + }) it('Returns null when configured with no repositories', async () => { - const tokenHoldersRepositoryFallback = new TokenHolderRepositoryFallback( - [] - ); - const tokenHolders = - await tokenHoldersRepositoryFallback.getTopTokenHolders( - ...PARAMS_PRICE - ); - expect(tokenHolders).toEqual(null); - }); + const tokenHoldersRepositoryFallback = new TokenHolderRepositoryFallback([]) + const tokenHolders = await tokenHoldersRepositoryFallback.getTopTokenHolders(...PARAMS_PRICE) + expect(tokenHolders).toEqual(null) + }) it('Returns null when no repo return holders', async () => { const tokenHoldersRepositoryFallback = new TokenHolderRepositoryFallback([ tokenHoldersRepositoryMock_null, tokenHoldersRepositoryMock_null, - ]); - const price = await tokenHoldersRepositoryFallback.getTopTokenHolders( - ...PARAMS_PRICE - ); - expect(price).toEqual(null); - }); - }); -}); + ]) + const price = await tokenHoldersRepositoryFallback.getTopTokenHolders(...PARAMS_PRICE) + expect(price).toEqual(null) + }) + }) +}) diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryFallback.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryFallback.ts index 039b5c6d..7987092d 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryFallback.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryFallback.ts @@ -1,27 +1,18 @@ -import { injectable } from 'inversify'; -import { - TokenHolderPoint, - TokenHolderRepository, -} from './TokenHolderRepository'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { injectable } from 'inversify' +import { TokenHolderPoint, TokenHolderRepository } from './TokenHolderRepository' +import { SupportedChainId } from '@cowprotocol/cow-sdk' @injectable() export class TokenHolderRepositoryFallback implements TokenHolderRepository { constructor(private tokenHolderRepositories: TokenHolderRepository[]) {} - async getTopTokenHolders( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { + async getTopTokenHolders(chainId: SupportedChainId, tokenAddress: string): Promise { for (const tokenHolderRepository of this.tokenHolderRepositories) { - const tokenHolders = await tokenHolderRepository.getTopTokenHolders( - chainId, - tokenAddress - ); + const tokenHolders = await tokenHolderRepository.getTopTokenHolders(chainId, tokenAddress) if (tokenHolders !== null) { - return tokenHolders; + return tokenHolders } } - return null; + return null } } diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryGoldRush.test.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryGoldRush.test.ts index 7e4f17c4..2fed88d6 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryGoldRush.test.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryGoldRush.test.ts @@ -1,48 +1,37 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { Container } from 'inversify'; -import { NULL_ADDRESS, WETH } from '../../../test/mock'; -import { GOLD_RUSH_API_KEY } from '../../datasources/goldRush'; -import { TokenHolderRepositoryGoldRush } from './TokenHolderRepositoryGoldRush'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { Container } from 'inversify' +import { NULL_ADDRESS, WETH } from '../../../test/mock' +import { GOLD_RUSH_API_KEY } from '../../datasources/goldRush' +import { TokenHolderRepositoryGoldRush } from './TokenHolderRepositoryGoldRush' // Skip this test as it requires an https://goldrush.dev API key. Enable it locally when needed describe.skip('TokenHolderRepositoryGoldRush', () => { - let tokenHolderRepositoryGoldRush: TokenHolderRepositoryGoldRush; + let tokenHolderRepositoryGoldRush: TokenHolderRepositoryGoldRush beforeAll(() => { - const container = new Container(); - container - .bind(TokenHolderRepositoryGoldRush) - .to(TokenHolderRepositoryGoldRush); - tokenHolderRepositoryGoldRush = container.get( - TokenHolderRepositoryGoldRush - ); - expect(GOLD_RUSH_API_KEY).toBeDefined(); - }); + const container = new Container() + container.bind(TokenHolderRepositoryGoldRush).to(TokenHolderRepositoryGoldRush) + tokenHolderRepositoryGoldRush = container.get(TokenHolderRepositoryGoldRush) + expect(GOLD_RUSH_API_KEY).toBeDefined() + }) describe('getTopTokenHolders', () => { it('should return the top token holders of WETH', async () => { - const tokenHolders = - await tokenHolderRepositoryGoldRush.getTopTokenHolders( - SupportedChainId.MAINNET, - WETH - ); + const tokenHolders = await tokenHolderRepositoryGoldRush.getTopTokenHolders(SupportedChainId.MAINNET, WETH) - expect(tokenHolders?.length).toBeGreaterThan(0); - expect(tokenHolders?.[0].address).toBeDefined(); - expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(0); - expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan( - Number(tokenHolders?.[1].balance) - ); - }, 100000); + expect(tokenHolders?.length).toBeGreaterThan(0) + expect(tokenHolders?.[0].address).toBeDefined() + expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(0) + expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(Number(tokenHolders?.[1].balance)) + }, 100000) it('should return null for an unknown token', async () => { - const tokenHolders = - await tokenHolderRepositoryGoldRush.getTopTokenHolders( - SupportedChainId.MAINNET, - NULL_ADDRESS - ); + const tokenHolders = await tokenHolderRepositoryGoldRush.getTopTokenHolders( + SupportedChainId.MAINNET, + NULL_ADDRESS + ) - expect(tokenHolders).toBeNull(); - }, 100000); - }); -}); + expect(tokenHolders).toBeNull() + }, 100000) + }) +}) diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryGoldRush.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryGoldRush.ts index 6c40c338..86428b9c 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryGoldRush.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryGoldRush.ts @@ -1,72 +1,59 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { injectable } from 'inversify'; -import { - GOLD_RUSH_API_BASE_URL, - GOLD_RUSH_API_KEY, - GOLD_RUSH_CLIENT_NETWORK_MAPPING, -} from '../../datasources/goldRush'; -import { - TokenHolderPoint, - TokenHolderRepository, -} from './TokenHolderRepository'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { injectable } from 'inversify' +import { GOLD_RUSH_API_BASE_URL, GOLD_RUSH_API_KEY, GOLD_RUSH_CLIENT_NETWORK_MAPPING } from '../../datasources/goldRush' +import { TokenHolderPoint, TokenHolderRepository } from './TokenHolderRepository' interface GoldRushTokenHolderItem { - contract_decimals: number; - contract_name: string; - contract_ticker_symbol: string; - contract_address: string; - supports_erc: string[]; - logo_url: string; - address: string; - balance: string; - total_supply: string; - block_height: number; + contract_decimals: number + contract_name: string + contract_ticker_symbol: string + contract_address: string + supports_erc: string[] + logo_url: string + address: string + balance: string + total_supply: string + block_height: number } interface GoldRushTokenHoldersResponse { data: { - updated_at: string; - chain_id: number; - chain_name: string; - items: GoldRushTokenHolderItem[]; + updated_at: string + chain_id: number + chain_name: string + items: GoldRushTokenHolderItem[] pagination: { - has_more: boolean; - page_number: number; - page_size: number; - total_count: number; - }; - }; - error: boolean; - error_message: null | string; - error_code: null | number; + has_more: boolean + page_number: number + page_size: number + total_count: number + } + } + error: boolean + error_message: null | string + error_code: null | number } @injectable() export class TokenHolderRepositoryGoldRush implements TokenHolderRepository { - async getTopTokenHolders( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { - const network = GOLD_RUSH_CLIENT_NETWORK_MAPPING[chainId]; + async getTopTokenHolders(chainId: SupportedChainId, tokenAddress: string): Promise { + const network = GOLD_RUSH_CLIENT_NETWORK_MAPPING[chainId] if (!network) { - return null; + return null } - const response = (await fetch( - `${GOLD_RUSH_API_BASE_URL}/v1/${network}/tokens/${tokenAddress}/token_holders_v2/`, - { - method: 'GET', - headers: { Authorization: `Bearer ${GOLD_RUSH_API_KEY}` }, - } - ).then((res) => res.json())) as GoldRushTokenHoldersResponse; + const response = (await fetch(`${GOLD_RUSH_API_BASE_URL}/v1/${network}/tokens/${tokenAddress}/token_holders_v2/`, { + method: 'GET', + headers: { Authorization: `Bearer ${GOLD_RUSH_API_KEY}` }, + }).then((res) => res.json())) as GoldRushTokenHoldersResponse if (response.error) { - return null; + return null } return response.data.items.map((item) => ({ address: item.address, balance: item.balance, - })); + })) } } diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryMoralis.spec.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryMoralis.spec.ts index 8bf142f5..8d854e79 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryMoralis.spec.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryMoralis.spec.ts @@ -1,46 +1,34 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { Container } from 'inversify'; -import { NULL_ADDRESS, WETH } from '../../../test/mock'; -import { MORALIS_API_KEY } from '../../datasources/moralis'; -import { TokenHolderRepositoryMoralis } from './TokenHolderRepositoryMoralis'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { Container } from 'inversify' +import { NULL_ADDRESS, WETH } from '../../../test/mock' +import { MORALIS_API_KEY } from '../../datasources/moralis' +import { TokenHolderRepositoryMoralis } from './TokenHolderRepositoryMoralis' // The tests are integration tests and require ENV variables describe.skip('TokenHolderRepositoryMoralis', () => { - let tokenHolderRepositoryMoralis: TokenHolderRepositoryMoralis; + let tokenHolderRepositoryMoralis: TokenHolderRepositoryMoralis beforeAll(() => { - const container = new Container(); - container - .bind(TokenHolderRepositoryMoralis) - .to(TokenHolderRepositoryMoralis); - tokenHolderRepositoryMoralis = container.get(TokenHolderRepositoryMoralis); - expect(MORALIS_API_KEY).toBeDefined(); - }); + const container = new Container() + container.bind(TokenHolderRepositoryMoralis).to(TokenHolderRepositoryMoralis) + tokenHolderRepositoryMoralis = container.get(TokenHolderRepositoryMoralis) + expect(MORALIS_API_KEY).toBeDefined() + }) describe('getTopTokenHolders', () => { it('should return the top token holders of WETH', async () => { - const tokenHolders = - await tokenHolderRepositoryMoralis.getTopTokenHolders( - SupportedChainId.MAINNET, - WETH - ); + const tokenHolders = await tokenHolderRepositoryMoralis.getTopTokenHolders(SupportedChainId.MAINNET, WETH) - expect(tokenHolders?.length).toBeGreaterThan(0); - expect(tokenHolders?.[0].address).toBeDefined(); - expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(0); - expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan( - Number(tokenHolders?.[1].balance) - ); - }, 100000); + expect(tokenHolders?.length).toBeGreaterThan(0) + expect(tokenHolders?.[0].address).toBeDefined() + expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(0) + expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(Number(tokenHolders?.[1].balance)) + }, 100000) it('should return null for an unknown token', async () => { - const tokenHolders = - await tokenHolderRepositoryMoralis.getTopTokenHolders( - SupportedChainId.MAINNET, - NULL_ADDRESS - ); + const tokenHolders = await tokenHolderRepositoryMoralis.getTopTokenHolders(SupportedChainId.MAINNET, NULL_ADDRESS) - expect(tokenHolders).toBeNull(); - }, 100000); - }); -}); + expect(tokenHolders).toBeNull() + }, 100000) + }) +}) diff --git a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryMoralis.ts b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryMoralis.ts index f1be2363..24d6ef58 100644 --- a/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryMoralis.ts +++ b/libs/repositories/src/repos/TokenHolderRepository/TokenHolderRepositoryMoralis.ts @@ -1,43 +1,33 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { injectable } from 'inversify'; -import { - MORALIS_API_BASE_URL, - MORALIS_API_KEY, - MORALIS_CLIENT_NETWORK_MAPPING, -} from '../../datasources/moralis'; -import { - TokenHolderPoint, - TokenHolderRepository, -} from './TokenHolderRepository'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { injectable } from 'inversify' +import { MORALIS_API_BASE_URL, MORALIS_API_KEY, MORALIS_CLIENT_NETWORK_MAPPING } from '../../datasources/moralis' +import { TokenHolderPoint, TokenHolderRepository } from './TokenHolderRepository' interface MoralisTokenHolderItem { - balance: string; - balance_formated: string; - is_contract: boolean; - owner_address: string; - owner_address_label: string; - entity: string; - entity_logo: string; - usd_value: string; - percentage_relative_to_total_supply: number; + balance: string + balance_formated: string + is_contract: boolean + owner_address: string + owner_address_label: string + entity: string + entity_logo: string + usd_value: string + percentage_relative_to_total_supply: number } interface MoralisTokenHoldersResponse { - result: MoralisTokenHolderItem[]; - cursor: string; - page: number; - page_size: number; + result: MoralisTokenHolderItem[] + cursor: string + page: number + page_size: number } @injectable() export class TokenHolderRepositoryMoralis implements TokenHolderRepository { - async getTopTokenHolders( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { - const network = MORALIS_CLIENT_NETWORK_MAPPING[chainId]; + async getTopTokenHolders(chainId: SupportedChainId, tokenAddress: string): Promise { + const network = MORALIS_CLIENT_NETWORK_MAPPING[chainId] if (!network) { - return null; + return null } const response = (await fetch( @@ -49,15 +39,15 @@ export class TokenHolderRepositoryMoralis implements TokenHolderRepository { 'X-API-Key': `${MORALIS_API_KEY}`, }, } - ).then((res) => res.json())) as MoralisTokenHoldersResponse; + ).then((res) => res.json())) as MoralisTokenHoldersResponse if (response.result.length === 0) { - return null; + return null } return response.result.map((item) => ({ address: item.owner_address, balance: item.balance, - })); + })) } } diff --git a/libs/repositories/src/repos/UsdRepository/UsdRepository.ts b/libs/repositories/src/repos/UsdRepository/UsdRepository.ts index cb2850af..08e77472 100644 --- a/libs/repositories/src/repos/UsdRepository/UsdRepository.ts +++ b/libs/repositories/src/repos/UsdRepository/UsdRepository.ts @@ -1,54 +1,48 @@ -export const usdRepositorySymbol = Symbol.for('UsdRepository'); +export const usdRepositorySymbol = Symbol.for('UsdRepository') -export type PriceStrategy = '5m' | 'hourly' | 'daily'; +export type PriceStrategy = '5m' | 'hourly' | 'daily' export interface PricePoint { /** * Date and time of the price point */ - date: Date; + date: Date /** * Price */ - price: number; + price: number /** * Volume traded at that price */ - volume: number; + volume: number } export interface UsdRepository { - name: string; + name: string - getUsdPrice( - chainIdOrSlug: string, - tokenAddress?: string | undefined - ): Promise; + getUsdPrice(chainIdOrSlug: string, tokenAddress?: string | undefined): Promise getUsdPrices( chainIdOrSlug: string, tokenAddress: string | undefined, priceStrategy: PriceStrategy - ): Promise; + ): Promise } export class UsdRepositoryNoop implements UsdRepository { - name = 'Noop'; + name = 'Noop' - async getUsdPrice( - chainIdOrSlug: string, - tokenAddress?: string | undefined - ): Promise { - return null; + async getUsdPrice(chainIdOrSlug: string, tokenAddress?: string | undefined): Promise { + return null } async getUsdPrices( chainIdOrSlug: string, tokenAddress: string | undefined, priceStrategy: PriceStrategy ): Promise { - return null; + return null } } @@ -56,20 +50,18 @@ export const serializePricePoints = (pricePoints: PricePoint[]): string => { const serialized = pricePoints.map((point) => ({ ...point, date: point.date.toISOString(), - })); - return JSON.stringify(serialized); -}; + })) + return JSON.stringify(serialized) +} export type PricePointSerializable = Omit & { - date: string; -}; + date: string +} -export const deserializePricePoints = ( - serializedPricePoints: string -): PricePoint[] => { - const parsed: PricePointSerializable[] = JSON.parse(serializedPricePoints); +export const deserializePricePoints = (serializedPricePoints: string): PricePoint[] => { + const parsed: PricePointSerializable[] = JSON.parse(serializedPricePoints) return parsed.map((point) => ({ ...point, date: new Date(point.date), - })); -}; + })) +} diff --git a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCache.spec.ts b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCache.spec.ts index e0b8bd06..0d6194ae 100644 --- a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCache.spec.ts +++ b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCache.spec.ts @@ -1,98 +1,98 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import IORedis from 'ioredis'; -import { WETH } from '../../../test/mock'; -import { CacheRepositoryRedis } from '../CacheRepository/CacheRepositoryRedis'; -import type { PricePoint } from './UsdRepository'; -import { UsdRepository } from './UsdRepository'; -import { UsdRepositoryCache } from './UsdRepositoryCache'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import IORedis from 'ioredis' +import { WETH } from '../../../test/mock' +import { CacheRepositoryRedis } from '../CacheRepository/CacheRepositoryRedis' +import type { PricePoint } from './UsdRepository' +import { UsdRepository } from './UsdRepository' +import { UsdRepositoryCache } from './UsdRepositoryCache' -const CACHE_VALUE_SECONDS = 10; -const CACHE_NULL_SECONDS = 20; +const CACHE_VALUE_SECONDS = 10 +const CACHE_NULL_SECONDS = 20 -const wethLowercase = WETH.toLocaleLowerCase(); -const chainId = SupportedChainId.MAINNET.toString(); +const wethLowercase = WETH.toLocaleLowerCase() +const chainId = SupportedChainId.MAINNET.toString() jest.mock('ioredis', () => { return jest.fn().mockImplementation(() => ({ get: jest.fn(), set: jest.fn(), - })); -}); + })) +}) describe('UsdRepositoryCache', () => { - let usdRepositoryCache: UsdRepositoryCache; - let redisMock: jest.Mocked; - let proxyMock: jest.Mocked; + let usdRepositoryCache: UsdRepositoryCache + let redisMock: jest.Mocked + let proxyMock: jest.Mocked beforeEach(() => { - redisMock = new IORedis() as jest.Mocked; + redisMock = new IORedis() as jest.Mocked proxyMock = { name: 'ProxyMock', getUsdPrice: jest.fn(), getUsdPrices: jest.fn(), - }; - const cacheRepository = new CacheRepositoryRedis(redisMock); + } + const cacheRepository = new CacheRepositoryRedis(redisMock) usdRepositoryCache = new UsdRepositoryCache( proxyMock, cacheRepository, 'test-cache', CACHE_VALUE_SECONDS, CACHE_NULL_SECONDS - ); - }); + ) + }) describe('name', () => { it('should delegate name to proxy', () => { - expect(usdRepositoryCache.name).toEqual('ProxyMock'); - }); - }); + expect(usdRepositoryCache.name).toEqual('ProxyMock') + }) + }) describe('getUsdPrice', () => { it('should return price from cache', async () => { // GIVEN: Cached value '100' - redisMock.get.mockResolvedValue('100'); + redisMock.get.mockResolvedValue('100') // GIVEN: proxy returns 200 - proxyMock.getUsdPrice.mockResolvedValue(200); + proxyMock.getUsdPrice.mockResolvedValue(200) // WHEN: Get USD price - const price = await usdRepositoryCache.getUsdPrice(chainId, WETH); + const price = await usdRepositoryCache.getUsdPrice(chainId, WETH) // THEN: We get the cached value - expect(price).toEqual(100); - expect(proxyMock.getUsdPrice).not.toHaveBeenCalled(); - }); + expect(price).toEqual(100) + expect(proxyMock.getUsdPrice).not.toHaveBeenCalled() + }) it('should return NULL from cache', async () => { // GIVEN: Cached value 'null' - redisMock.get.mockResolvedValue('null'); + redisMock.get.mockResolvedValue('null') // GIVEN: proxy returns 200 - proxyMock.getUsdPrice.mockResolvedValue(200); + proxyMock.getUsdPrice.mockResolvedValue(200) // WHEN: Get USD price - const price = await usdRepositoryCache.getUsdPrice(chainId, WETH); + const price = await usdRepositoryCache.getUsdPrice(chainId, WETH) // THEN: We get the cached value - expect(price).toEqual(null); - expect(proxyMock.getUsdPrice).not.toHaveBeenCalled(); - }); + expect(price).toEqual(null) + expect(proxyMock.getUsdPrice).not.toHaveBeenCalled() + }) it('should call the proxy if no cache, then cache the value', async () => { // GIVEN: The value is not cached - redisMock.get.mockResolvedValue(null); + redisMock.get.mockResolvedValue(null) // GIVEN: proxy returns 200 - proxyMock.getUsdPrice.mockResolvedValue(200); + proxyMock.getUsdPrice.mockResolvedValue(200) // When: Get USD price - const price = await usdRepositoryCache.getUsdPrice(chainId, WETH); + const price = await usdRepositoryCache.getUsdPrice(chainId, WETH) // THEN: The price matches the result from the proxy - expect(price).toEqual(200); + expect(price).toEqual(200) // THEN: The proxy has been called once - expect(proxyMock.getUsdPrice).toHaveBeenCalledWith(chainId, WETH); + expect(proxyMock.getUsdPrice).toHaveBeenCalledWith(chainId, WETH) // THEN: The value returned by the proxy is cached expect(redisMock.set).toHaveBeenCalledWith( @@ -100,24 +100,24 @@ describe('UsdRepositoryCache', () => { '200', 'EX', CACHE_VALUE_SECONDS - ); - }); + ) + }) it('should call the proxy if no cache, then cache the NULL', async () => { // GIVEN: The value is not cached - redisMock.get.mockResolvedValue(null); + redisMock.get.mockResolvedValue(null) // GIVEN: proxy returns 200 - proxyMock.getUsdPrice.mockResolvedValue(null); + proxyMock.getUsdPrice.mockResolvedValue(null) // When: Get USD price - const price = await usdRepositoryCache.getUsdPrice(chainId, WETH); + const price = await usdRepositoryCache.getUsdPrice(chainId, WETH) // THEN: The price matches the result from the proxy - expect(price).toEqual(null); + expect(price).toEqual(null) // THEN: The proxy has been called once - expect(proxyMock.getUsdPrice).toHaveBeenCalledWith(chainId, WETH); + expect(proxyMock.getUsdPrice).toHaveBeenCalledWith(chainId, WETH) // THEN: The value returned by the proxy is cached expect(redisMock.set).toHaveBeenCalledWith( @@ -125,93 +125,93 @@ describe('UsdRepositoryCache', () => { 'null', 'EX', CACHE_NULL_SECONDS - ); - }); + ) + }) it('should return the cached value, even if the proxy throws', async () => { // GIVEN: Cached value '100' - redisMock.get.mockResolvedValue('100'); + redisMock.get.mockResolvedValue('100') // GIVEN: The proxy throws an awful error proxyMock.getUsdPrice.mockImplementation(() => { - throw new Error('💥 Booom!'); - }); + throw new Error('💥 Booom!') + }) // When: Get USD price - const price = await usdRepositoryCache.getUsdPrice(chainId, WETH); + const price = await usdRepositoryCache.getUsdPrice(chainId, WETH) // THEN: The price matches the result from the proxy - expect(price).toEqual(100); - expect(proxyMock.getUsdPrice).not.toHaveBeenCalled(); - }); + expect(price).toEqual(100) + expect(proxyMock.getUsdPrice).not.toHaveBeenCalled() + }) it('should throw if the proxy throws and there is no cache available', async () => { // GIVEN: The value is not cached - redisMock.get.mockResolvedValue(null); + redisMock.get.mockResolvedValue(null) // GIVEN: The proxy throws an awful error proxyMock.getUsdPrice.mockImplementation(async () => { - throw new Error('💥 Booom!'); - }); + throw new Error('💥 Booom!') + }) // When: Get USD price - const pricePromise = usdRepositoryCache.getUsdPrice(chainId, WETH); + const pricePromise = usdRepositoryCache.getUsdPrice(chainId, WETH) // THEN: The call throws an awful error - expect(pricePromise).rejects.toThrow('💥 Booom!'); - }); - }); + expect(pricePromise).rejects.toThrow('💥 Booom!') + }) + }) describe('getUsdPrices', () => { - let pricePoint100: PricePoint; - let pricePoint200: PricePoint; + let pricePoint100: PricePoint + let pricePoint200: PricePoint - let pricePoints100String: string; - let pricePoints200String: string; + let pricePoints100String: string + let pricePoints200String: string beforeAll(() => { pricePoint100 = { date: new Date('2024-01-01T00:00:00Z'), price: 100, volume: 12345, - }; + } pricePoint200 = { date: new Date('2024-12-31T11:59:59Z'), price: 200, volume: 67890, - }; - pricePoints100String = JSON.stringify([pricePoint100]); - pricePoints200String = JSON.stringify([pricePoint200]); - }); + } + pricePoints100String = JSON.stringify([pricePoint100]) + pricePoints200String = JSON.stringify([pricePoint200]) + }) it('should return prices from cache', async () => { // GIVEN: cached prices - redisMock.get.mockResolvedValue(pricePoints100String); - proxyMock.getUsdPrices.mockResolvedValue([pricePoint200]); + redisMock.get.mockResolvedValue(pricePoints100String) + proxyMock.getUsdPrices.mockResolvedValue([pricePoint200]) // WHEN: Get USD prices - const prices = await usdRepositoryCache.getUsdPrices(chainId, WETH, '5m'); + const prices = await usdRepositoryCache.getUsdPrices(chainId, WETH, '5m') // THEN: We get the cached value - expect(prices).toEqual([pricePoint100]); - expect(proxyMock.getUsdPrices).not.toHaveBeenCalled(); - }); + expect(prices).toEqual([pricePoint100]) + expect(proxyMock.getUsdPrices).not.toHaveBeenCalled() + }) it('should call the proxy if no cache, then cache the value', async () => { // GIVEN: The value is not cached - redisMock.get.mockResolvedValue(null); + redisMock.get.mockResolvedValue(null) // GIVEN: proxy returns 200 - proxyMock.getUsdPrices.mockResolvedValue([pricePoint200]); + proxyMock.getUsdPrices.mockResolvedValue([pricePoint200]) // When: Get USD prices - const prices = await usdRepositoryCache.getUsdPrices(chainId, WETH, '5m'); + const prices = await usdRepositoryCache.getUsdPrices(chainId, WETH, '5m') // THEN: The price matches the result from the proxy - expect(prices).toEqual([pricePoint200]); + expect(prices).toEqual([pricePoint200]) // THEN: The proxy has been called once - expect(proxyMock.getUsdPrices).toHaveBeenCalledWith(chainId, WETH, '5m'); + expect(proxyMock.getUsdPrices).toHaveBeenCalledWith(chainId, WETH, '5m') // THEN: The value returned by the proxy is cached expect(redisMock.set).toHaveBeenCalledWith( @@ -219,24 +219,24 @@ describe('UsdRepositoryCache', () => { pricePoints200String, 'EX', CACHE_VALUE_SECONDS - ); - }); + ) + }) it('should call the proxy if no cache, then cache the NULL', async () => { // GIVEN: The value is not cached - redisMock.get.mockResolvedValue(null); + redisMock.get.mockResolvedValue(null) // GIVEN: proxy returns 200 - proxyMock.getUsdPrices.mockResolvedValue(null); + proxyMock.getUsdPrices.mockResolvedValue(null) // When: Get USD prices - const prices = await usdRepositoryCache.getUsdPrices(chainId, WETH, '5m'); + const prices = await usdRepositoryCache.getUsdPrices(chainId, WETH, '5m') // THEN: The price matches the result from the proxy - expect(prices).toEqual(null); + expect(prices).toEqual(null) // THEN: The proxy has been called once - expect(proxyMock.getUsdPrices).toHaveBeenCalledWith(chainId, WETH, '5m'); + expect(proxyMock.getUsdPrices).toHaveBeenCalledWith(chainId, WETH, '5m') // THEN: The value returned by the proxy is cached expect(redisMock.set).toHaveBeenCalledWith( @@ -244,44 +244,40 @@ describe('UsdRepositoryCache', () => { 'null', 'EX', CACHE_NULL_SECONDS - ); - }); + ) + }) it('should return the cached value, even if the proxy throws', async () => { // GIVEN: Cached value '100' - redisMock.get.mockResolvedValue(pricePoints100String); + redisMock.get.mockResolvedValue(pricePoints100String) // GIVEN: The proxy throws an awful error proxyMock.getUsdPrices.mockImplementation(() => { - throw new Error('💥 Booom!'); - }); + throw new Error('💥 Booom!') + }) // When: Get USD price - const prices = await usdRepositoryCache.getUsdPrices(chainId, WETH, '5m'); + const prices = await usdRepositoryCache.getUsdPrices(chainId, WETH, '5m') // THEN: The price matches the result from the proxy - expect(prices).toEqual([pricePoint100]); - expect(proxyMock.getUsdPrice).not.toHaveBeenCalled(); - }); + expect(prices).toEqual([pricePoint100]) + expect(proxyMock.getUsdPrice).not.toHaveBeenCalled() + }) it('should throw if the proxy throws and there is no cache available', async () => { // GIVEN: The value is not cached - redisMock.get.mockResolvedValue(null); + redisMock.get.mockResolvedValue(null) // GIVEN: The proxy throws an awful error proxyMock.getUsdPrices.mockImplementation(async () => { - throw new Error('💥 Booom!'); - }); + throw new Error('💥 Booom!') + }) // When: Get USD prices - const pricesPromise = usdRepositoryCache.getUsdPrices( - chainId, - WETH, - '5m' - ); + const pricesPromise = usdRepositoryCache.getUsdPrices(chainId, WETH, '5m') // THEN: The call throws an awful error - expect(pricesPromise).rejects.toThrow('💥 Booom!'); - }); - }); -}); + expect(pricesPromise).rejects.toThrow('💥 Booom!') + }) + }) +}) diff --git a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCache.ts b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCache.ts index 44e784dc..8eea5ea1 100644 --- a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCache.ts +++ b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCache.ts @@ -1,19 +1,13 @@ -import { injectable } from 'inversify'; -import { getCacheKey, PartialCacheKey } from '../../utils/cache'; -import { CacheRepository } from '../CacheRepository/CacheRepository'; -import { - deserializePricePoints, - PricePoint, - PriceStrategy, - serializePricePoints, - UsdRepository, -} from './UsdRepository'; - -const NULL_VALUE = 'null'; +import { injectable } from 'inversify' +import { getCacheKey, PartialCacheKey } from '../../utils/cache' +import { CacheRepository } from '../CacheRepository/CacheRepository' +import { deserializePricePoints, PricePoint, PriceStrategy, serializePricePoints, UsdRepository } from './UsdRepository' + +const NULL_VALUE = 'null' @injectable() export class UsdRepositoryCache implements UsdRepository { - private baseCacheKey: PartialCacheKey[]; + private baseCacheKey: PartialCacheKey[] constructor( private proxy: UsdRepository, @@ -22,112 +16,86 @@ export class UsdRepositoryCache implements UsdRepository { private cacheTimeValueSeconds: number, private cacheTimeNullSeconds: number ) { - this.baseCacheKey = ['repos', this.cacheName]; + this.baseCacheKey = ['repos', this.cacheName] } get name(): string { - return this.proxy.name; + return this.proxy.name } - async getUsdPrice( - chainIdOrSlug: string, - tokenAddress?: string | undefined - ): Promise { + async getUsdPrice(chainIdOrSlug: string, tokenAddress?: string | undefined): Promise { // Get price from cache - const key = getCacheKey( - ...this.baseCacheKey, - 'usd-price', - chainIdOrSlug, - tokenAddress || '' - ); + const key = getCacheKey(...this.baseCacheKey, 'usd-price', chainIdOrSlug, tokenAddress || '') const usdPriceCached = await this.getValueFromCache({ key, convertFn: parseFloat, - }); + }) if (usdPriceCached !== undefined) { // Return cached price (if available) - return usdPriceCached; + return usdPriceCached } // Get the usd Price (delegate call) - const usdPrice = await this.proxy.getUsdPrice(chainIdOrSlug, tokenAddress); + const usdPrice = await this.proxy.getUsdPrice(chainIdOrSlug, tokenAddress) // Cache price (or absence of it) this.cacheValue({ key, value: usdPrice?.toString() || null, - }); + }) - return usdPrice; + return usdPrice } async getUsdPrices( chainIdOrSlug: string, tokenAddress: string | undefined, priceStrategy: PriceStrategy ): Promise { - const key = getCacheKey( - ...this.baseCacheKey, - 'usd-prices', - chainIdOrSlug, - tokenAddress || '', - priceStrategy - ); + const key = getCacheKey(...this.baseCacheKey, 'usd-prices', chainIdOrSlug, tokenAddress || '', priceStrategy) // Get price from cache const usdPriceCached = await this.getValueFromCache({ key, convertFn: deserializePricePoints, - }); + }) if (usdPriceCached !== undefined) { // Return cached prices (if available) - return usdPriceCached; + return usdPriceCached } // Get the usd Prices (delegate call) - const usdPrices = await this.proxy.getUsdPrices( - chainIdOrSlug, - tokenAddress, - priceStrategy - ); + const usdPrices = await this.proxy.getUsdPrices(chainIdOrSlug, tokenAddress, priceStrategy) // Cache prices (or absence of it) this.cacheValue({ key, value: usdPrices ? serializePricePoints(usdPrices) : null, - }); + }) - return usdPrices; + return usdPrices } private async getValueFromCache(props: { - key: string; - convertFn: (value: string) => T; + key: string + convertFn: (value: string) => T }): Promise { - const { key, convertFn } = props; + const { key, convertFn } = props - const valueString = await this.cache.get(key); + const valueString = await this.cache.get(key) if (valueString) { - return valueString === NULL_VALUE ? null : convertFn(valueString); + return valueString === NULL_VALUE ? null : convertFn(valueString) } - return undefined; + return undefined } - private async cacheValue(props: { - key: string; - value: string | null; - }): Promise { - const { key, value } = props; + private async cacheValue(props: { key: string; value: string | null }): Promise { + const { key, value } = props - const cacheTimeSeconds = - value === null ? this.cacheTimeNullSeconds : this.cacheTimeValueSeconds; + const cacheTimeSeconds = value === null ? this.cacheTimeNullSeconds : this.cacheTimeValueSeconds - await this.cache.set( - key, - value === null ? NULL_VALUE : value, - cacheTimeSeconds - ); + await this.cache.set(key, value === null ? NULL_VALUE : value, cacheTimeSeconds) } } diff --git a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCoingecko.test.ts b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCoingecko.test.ts index 5b04b4ba..81f1cda2 100644 --- a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCoingecko.test.ts +++ b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCoingecko.test.ts @@ -1,206 +1,172 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { Container } from 'inversify'; -import ms from 'ms'; -import { NULL_ADDRESS, WETH } from '../../../test/mock'; -import { UsdRepositoryCoingecko } from './UsdRepositoryCoingecko'; - -const FIVE_MINUTES = ms('5m'); -const ONE_HOUR = ms('1h'); -const ONE_DAY = ms('1d'); -const BUFFER_ERROR_TOLERANCE = 1.5; // 50% error tolerance -const CHAIN_ID = SupportedChainId.MAINNET.toString(); +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { Container } from 'inversify' +import ms from 'ms' +import { NULL_ADDRESS, WETH } from '../../../test/mock' +import { UsdRepositoryCoingecko } from './UsdRepositoryCoingecko' + +const FIVE_MINUTES = ms('5m') +const ONE_HOUR = ms('1h') +const ONE_DAY = ms('1d') +const BUFFER_ERROR_TOLERANCE = 1.5 // 50% error tolerance +const CHAIN_ID = SupportedChainId.MAINNET.toString() // The tests are not mocked and use real HTTP resources describe.skip('UsdRepositoryCoingecko', () => { - let usdRepositoryCoingecko: UsdRepositoryCoingecko; + let usdRepositoryCoingecko: UsdRepositoryCoingecko beforeAll(() => { - const container = new Container(); - container - .bind(UsdRepositoryCoingecko) - .to(UsdRepositoryCoingecko); - usdRepositoryCoingecko = container.get(UsdRepositoryCoingecko); - }); + const container = new Container() + container.bind(UsdRepositoryCoingecko).to(UsdRepositoryCoingecko) + usdRepositoryCoingecko = container.get(UsdRepositoryCoingecko) + }) describe('getUsdPrice', () => { it('should return the current price of WETH', async () => { - const price = await usdRepositoryCoingecko.getUsdPrice(CHAIN_ID, WETH); + const price = await usdRepositoryCoingecko.getUsdPrice(CHAIN_ID, WETH) - expect(price).toBeGreaterThan(0); - }); + expect(price).toBeGreaterThan(0) + }) it('should return the current price for a chain by slug', async () => { - const price = await usdRepositoryCoingecko.getUsdPrice('ethereum', WETH); + const price = await usdRepositoryCoingecko.getUsdPrice('ethereum', WETH) - expect(price).toBeGreaterThan(0); - }); + expect(price).toBeGreaterThan(0) + }) it('should return the current price for a unsupported chain by id', async () => { const price = await usdRepositoryCoingecko.getUsdPrice( '369', // pulsechain '0x2b591e99afe9f32eaa6214f7b7629768c40eeb39' // Hex on pulsechain - ); + ) - expect(price).toBeGreaterThan(0); - }); + expect(price).toBeGreaterThan(0) + }) it('should return the current price for a unsupported chain by id for a non-evm chain', async () => { const price = await usdRepositoryCoingecko.getUsdPrice( 'solana', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC on Solana - ); + ) - expect(price).toBeGreaterThan(0); - }); + expect(price).toBeGreaterThan(0) + }) it('should return the current price without token address', async () => { - const price = await usdRepositoryCoingecko.getUsdPrice('bitcoin'); + const price = await usdRepositoryCoingecko.getUsdPrice('bitcoin') - expect(price).toBeGreaterThan(0); - }); + expect(price).toBeGreaterThan(0) + }) it('should return NULL for an unknown token', async () => { - const price = await usdRepositoryCoingecko.getUsdPrice( - CHAIN_ID, - NULL_ADDRESS - ); + const price = await usdRepositoryCoingecko.getUsdPrice(CHAIN_ID, NULL_ADDRESS) // Price should be null (no data available) - expect(price).toBeNull(); - }); + expect(price).toBeNull() + }) it('should return NULL for an unknown chain', async () => { - const price = await usdRepositoryCoingecko.getUsdPrice( - 'unknown-chain', - WETH - ); + const price = await usdRepositoryCoingecko.getUsdPrice('unknown-chain', WETH) // Price should be null (no data available) - expect(price).toBeNull(); - }); - }); + expect(price).toBeNull() + }) + }) describe('getUsdPrices', () => { it('[5m] should return ~288 prices of WETH (~5min apart)', async () => { - const prices = await usdRepositoryCoingecko.getUsdPrices( - CHAIN_ID, - WETH, - '5m' - ); + const prices = await usdRepositoryCoingecko.getUsdPrices(CHAIN_ID, WETH, '5m') if (prices === null) { - throw new Error('Prices should not be null'); + throw new Error('Prices should not be null') } // We expect around 288 prices. We just assert we receive between 250 and 300 prices - expect(prices.length).toBeGreaterThan(250); - expect(prices.length).toBeLessThan(300); + expect(prices.length).toBeGreaterThan(250) + expect(prices.length).toBeLessThan(300) for (let i = 1; i < prices.length; i++) { - const price = prices[i]; - const previousPrice = prices[i - 1]; + const price = prices[i] + const previousPrice = prices[i - 1] // Check the time difference between the two prices is around 5 minutes // logger.info('Price', price, previousPrice); - expect( - Math.abs( - price.date.getTime() - previousPrice.date.getTime() - FIVE_MINUTES - ) - ).toBeLessThanOrEqual(FIVE_MINUTES * BUFFER_ERROR_TOLERANCE); // 5 min of error tolerance (we don't need to be super precise, but we also want to assert the points are kind of 5min apart) - expect(price.price).toBeGreaterThan(0); + expect(Math.abs(price.date.getTime() - previousPrice.date.getTime() - FIVE_MINUTES)).toBeLessThanOrEqual( + FIVE_MINUTES * BUFFER_ERROR_TOLERANCE + ) // 5 min of error tolerance (we don't need to be super precise, but we also want to assert the points are kind of 5min apart) + expect(price.price).toBeGreaterThan(0) } - }); + }) it('should return prices without token address', async () => { - const prices = await usdRepositoryCoingecko.getUsdPrices( - 'bitcoin', - undefined, - '5m' - ); + const prices = await usdRepositoryCoingecko.getUsdPrices('bitcoin', undefined, '5m') if (prices === null) { - throw new Error('Prices should not be null'); + throw new Error('Prices should not be null') } // We expect around 288 prices. We just assert we receive between 250 and 300 prices - expect(prices.length).toBeGreaterThan(250); - expect(prices.length).toBeLessThan(300); - }); + expect(prices.length).toBeGreaterThan(250) + expect(prices.length).toBeLessThan(300) + }) it('[5m] should return NULL for an unknown token', async () => { - const prices = await usdRepositoryCoingecko.getUsdPrices( - CHAIN_ID, - NULL_ADDRESS, - '5m' - ); + const prices = await usdRepositoryCoingecko.getUsdPrices(CHAIN_ID, NULL_ADDRESS, '5m') // Prices should be null (no data available) - expect(prices).toBeNull(); - }); + expect(prices).toBeNull() + }) it('[5m] should return NULL for an unknown chain', async () => { const prices = await usdRepositoryCoingecko.getUsdPrices( '', // unknown-chain WETH, '5m' - ); + ) // Prices should be null (no data available) - expect(prices).toBeNull(); - }); + expect(prices).toBeNull() + }) it('[hourly] should return ~120 prices of WETH (~60min apart)', async () => { - const prices = await usdRepositoryCoingecko.getUsdPrices( - CHAIN_ID, - WETH, - 'hourly' - ); + const prices = await usdRepositoryCoingecko.getUsdPrices(CHAIN_ID, WETH, 'hourly') if (prices === null) { - throw new Error('Prices should not be null'); + throw new Error('Prices should not be null') } // We expect around 120 prices. We just assert we receive between 100 and 150 prices - expect(prices.length).toBeGreaterThan(100); - expect(prices.length).toBeLessThan(150); + expect(prices.length).toBeGreaterThan(100) + expect(prices.length).toBeLessThan(150) for (let i = 1; i < prices.length; i++) { - const price = prices[i]; - const previousPrice = prices[i - 1]; + const price = prices[i] + const previousPrice = prices[i - 1] // Check the time difference between the two prices is around 5 minutes - expect( - Math.abs( - price.date.getTime() - previousPrice.date.getTime() - ONE_HOUR - ) - ).toBeLessThanOrEqual(ONE_HOUR * BUFFER_ERROR_TOLERANCE); // 1 hour of error tolerance (we don't need to be super precise, but we also want to assert the points are kind of 1 hour apart) - expect(price.price).toBeGreaterThan(0); + expect(Math.abs(price.date.getTime() - previousPrice.date.getTime() - ONE_HOUR)).toBeLessThanOrEqual( + ONE_HOUR * BUFFER_ERROR_TOLERANCE + ) // 1 hour of error tolerance (we don't need to be super precise, but we also want to assert the points are kind of 1 hour apart) + expect(price.price).toBeGreaterThan(0) } - }); + }) it('[daily] should return ~90 prices of WETH (~24h apart)', async () => { - const prices = await usdRepositoryCoingecko.getUsdPrices( - CHAIN_ID, - WETH, - 'daily' - ); + const prices = await usdRepositoryCoingecko.getUsdPrices(CHAIN_ID, WETH, 'daily') if (prices === null) { - throw new Error('Prices should not be null'); + throw new Error('Prices should not be null') } // We expect around ~90 prices. We just assert we receive between 80 and 100 prices - expect(prices.length).toBeGreaterThan(80); - expect(prices.length).toBeLessThan(100); + expect(prices.length).toBeGreaterThan(80) + expect(prices.length).toBeLessThan(100) for (let i = 1; i < prices.length; i++) { - const price = prices[i]; - const previousPrice = prices[i - 1]; + const price = prices[i] + const previousPrice = prices[i - 1] // Check the time difference between the two prices is around 5 minutes - expect( - Math.abs( - price.date.getTime() - previousPrice.date.getTime() - ONE_DAY - ) - ).toBeLessThanOrEqual(ONE_DAY * BUFFER_ERROR_TOLERANCE); // 1 day of error tolerance (we don't need to be super precise, but we also want to assert the points are kind of 1 day apart) - expect(price.price).toBeGreaterThan(0); + expect(Math.abs(price.date.getTime() - previousPrice.date.getTime() - ONE_DAY)).toBeLessThanOrEqual( + ONE_DAY * BUFFER_ERROR_TOLERANCE + ) // 1 day of error tolerance (we don't need to be super precise, but we also want to assert the points are kind of 1 day apart) + expect(price.price).toBeGreaterThan(0) } - }); - }); -}); + }) + }) +}) diff --git a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCoingecko.ts b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCoingecko.ts index 4172356b..1850f2fd 100644 --- a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCoingecko.ts +++ b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCoingecko.ts @@ -1,9 +1,9 @@ -import { injectable } from 'inversify'; -import { getAddressKey } from '@cowprotocol/cow-sdk'; -import { getCoingeckoProClient, SimplePriceResponse } from '../../datasources/coingecko'; -import { getAddressOrPlatform, getCoingeckoPlatform } from '../../utils/coingeckoUtils'; -import { throwIfUnsuccessful } from '../../utils/throwIfUnsuccessful'; -import { PricePoint, PriceStrategy, UsdRepository } from './UsdRepository'; +import { injectable } from 'inversify' +import { getAddressKey } from '@cowprotocol/cow-sdk' +import { getCoingeckoProClient, SimplePriceResponse } from '../../datasources/coingecko' +import { getAddressOrPlatform, getCoingeckoPlatform } from '../../utils/coingeckoUtils' +import { throwIfUnsuccessful } from '../../utils/throwIfUnsuccessful' +import { PricePoint, PriceStrategy, UsdRepository } from './UsdRepository' /** * Number of days of data to fetch for each price strategy @@ -18,29 +18,26 @@ const DAYS_PER_PRICE_STRATEGY: Record = { '5m': 1, // 1 day (~288 points) hourly: 5, // 5 Days of hourly data (~120 points) daily: 90, // 90 Days of daily data (~90 points) -}; +} @injectable() export class UsdRepositoryCoingecko implements UsdRepository { - name = 'Coingecko'; + name = 'Coingecko' - async getUsdPrice( - chainIdOrSlug: string, - tokenAddress?: string | undefined - ): Promise { - const platform = getCoingeckoPlatform(chainIdOrSlug); + async getUsdPrice(chainIdOrSlug: string, tokenAddress?: string | undefined): Promise { + const platform = getCoingeckoPlatform(chainIdOrSlug) if (!platform) { - return null; + return null } - const addressOrPlatform = getAddressOrPlatform(tokenAddress, platform); + const addressOrPlatform = getAddressOrPlatform(tokenAddress, platform) const fetchPromise = tokenAddress && addressOrPlatform !== platform ? this.getSinglePriceByContractAddress(platform, addressOrPlatform) - : this.getSinglePriceByPlatformId(platform); + : this.getSinglePriceByPlatformId(platform) - return this.handleSinglePriceResponse(fetchPromise, addressOrPlatform); + return this.handleSinglePriceResponse(fetchPromise, addressOrPlatform) } async getUsdPrices( @@ -48,58 +45,47 @@ export class UsdRepositoryCoingecko implements UsdRepository { tokenAddress: string | undefined, priceStrategy: PriceStrategy ): Promise { - const platform = getCoingeckoPlatform(chainIdOrSlug); + const platform = getCoingeckoPlatform(chainIdOrSlug) if (!platform) { - return null; + return null } - const days = DAYS_PER_PRICE_STRATEGY[priceStrategy].toString(); - const interval = priceStrategy === 'daily' ? 'daily' : undefined; + const days = DAYS_PER_PRICE_STRATEGY[priceStrategy].toString() + const interval = priceStrategy === 'daily' ? 'daily' : undefined - const addressOrPlatform = getAddressOrPlatform(tokenAddress, platform); + const addressOrPlatform = getAddressOrPlatform(tokenAddress, platform) const { data, response } = tokenAddress && addressOrPlatform !== platform - ? await this.getMarketDataByTokenAddress( - platform, - days, - interval, - addressOrPlatform - ) - : await this.getMarketDataByPlatformId(platform, days, interval); + ? await this.getMarketDataByTokenAddress(platform, days, interval, addressOrPlatform) + : await this.getMarketDataByPlatformId(platform, days, interval) if (response.status === 404 || !data) { - return null; + return null } - await throwIfUnsuccessful( - 'Error getting USD prices from Coingecko', - response - ); + await throwIfUnsuccessful('Error getting USD prices from Coingecko', response) const volumesMap = data.total_volumes?.reduce((acc, [timestamp, volume]) => { - acc.set(timestamp, volume); - return acc; - }, new Map()) || undefined; + acc.set(timestamp, volume) + return acc + }, new Map()) || undefined - const prices = data.prices; + const prices = data.prices if (!prices) { - return null; + return null } const pricePoints = prices.map(([timestamp, price]) => ({ date: new Date(timestamp), price, volume: volumesMap?.get(timestamp) ?? 0, - })); + })) - return pricePoints; + return pricePoints } - private async getSinglePriceByContractAddress( - platform: string, - tokenAddress: string - ) { + private async getSinglePriceByContractAddress(platform: string, tokenAddress: string) { // Get USD price: https://docs.coingecko.com/reference/simple-token-price return getCoingeckoProClient().GET(`/simple/token_price/{id}`, { params: { @@ -111,7 +97,7 @@ export class UsdRepositoryCoingecko implements UsdRepository { vs_currencies: 'usd', }, }, - }); + }) } private async getSinglePriceByPlatformId(platform: string) { @@ -123,28 +109,22 @@ export class UsdRepositoryCoingecko implements UsdRepository { vs_currencies: 'usd', }, }, - }); + }) } - private async handleSinglePriceResponse( - fetchPromise: Promise, - key: string - ): Promise { + private async handleSinglePriceResponse(fetchPromise: Promise, key: string): Promise { const { data, response } = (await fetchPromise) as { - data: SimplePriceResponse; - response: Response; - }; + data: SimplePriceResponse + response: Response + } if (response.status === 404 || !data?.[key]?.usd) { - return null; + return null } - await throwIfUnsuccessful( - 'Error getting USD price from Coingecko', - response - ); + await throwIfUnsuccessful('Error getting USD price from Coingecko', response) - return data[key].usd; + return data[key].usd } private async getMarketDataByTokenAddress( @@ -154,29 +134,22 @@ export class UsdRepositoryCoingecko implements UsdRepository { tokenAddress: string ) { // Get prices: See https://docs.coingecko.com/reference/contract-address-market-chart - return getCoingeckoProClient().GET( - `/coins/{id}/contract/{contract_address}/market_chart`, - { - params: { - path: { - id: platform, - contract_address: getAddressKey(tokenAddress), - }, - query: { - vs_currency: 'usd', - days, - interval, // Coingecko will auto-choose the granularity based on the number of days (but days, its required in our case). However, is not good to specify it for the other because it will throw an error (saying that the PRO account is not enough) - }, + return getCoingeckoProClient().GET(`/coins/{id}/contract/{contract_address}/market_chart`, { + params: { + path: { + id: platform, + contract_address: getAddressKey(tokenAddress), }, - } - ); + query: { + vs_currency: 'usd', + days, + interval, // Coingecko will auto-choose the granularity based on the number of days (but days, its required in our case). However, is not good to specify it for the other because it will throw an error (saying that the PRO account is not enough) + }, + }, + }) } - private async getMarketDataByPlatformId( - platform: string, - days: string, - interval: 'daily' | undefined - ) { + private async getMarketDataByPlatformId(platform: string, days: string, interval: 'daily' | undefined) { // Get prices: See https://docs.coingecko.com/reference/contract-address-market-chart return getCoingeckoProClient().GET(`/coins/{id}/market_chart`, { params: { @@ -189,6 +162,6 @@ export class UsdRepositoryCoingecko implements UsdRepository { interval, // Coingecko will auto-choose the granularity based on the number of days (but days, its required in our case). However, is not good to specify it for the other because it will throw an error (saying that the PRO account is not enough) }, }, - }); + }) } } diff --git a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCow.spec.ts b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCow.spec.ts index c5a7ce50..22bfa422 100644 --- a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCow.spec.ts +++ b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCow.spec.ts @@ -1,49 +1,37 @@ -import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk'; -import { UsdRepositoryCow } from './UsdRepositoryCow'; +import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' +import { UsdRepositoryCow } from './UsdRepositoryCow' -import { - errorResponse, - NULL_ADDRESS, - okResponse, - WETH, -} from '../../../test/mock'; -import { USDC } from '../../const'; -import { CowApiClient } from '../../datasources/cowApi'; -import { Erc20, Erc20Repository } from '../Erc20Repository/Erc20Repository'; +import { errorResponse, NULL_ADDRESS, okResponse, WETH } from '../../../test/mock' +import { USDC } from '../../const' +import { CowApiClient } from '../../datasources/cowApi' +import { Erc20, Erc20Repository } from '../Erc20Repository/Erc20Repository' -const mockApiGet = jest.fn(); +const mockApiGet = jest.fn() // Mock implementation for PublicClient const mockApi: CowApiClient = { GET: mockApiGet, // Add other methods of PublicClient if needed -} as unknown as jest.Mocked; +} as unknown as jest.Mocked -const NATIVE_PRICE_ENDPOINT = '/api/v1/token/{token}/native_price'; -const WETH_NATIVE_PRICE = 1; // See https://api.cow.fi/mainnet/api/v1/token/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/native_price -const USDC_PRICE = 288778763.042292; // USD price: 3,462.8585200136 (calculated 1e12 / 288778763.042292). See https://api.cow.fi/mainnet/api/v1/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/native_price -const CHAIN_ID = SupportedChainId.MAINNET.toString(); +const NATIVE_PRICE_ENDPOINT = '/api/v1/token/{token}/native_price' +const WETH_NATIVE_PRICE = 1 // See https://api.cow.fi/mainnet/api/v1/token/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/native_price +const USDC_PRICE = 288778763.042292 // USD price: 3,462.8585200136 (calculated 1e12 / 288778763.042292). See https://api.cow.fi/mainnet/api/v1/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/native_price +const CHAIN_ID = SupportedChainId.MAINNET.toString() const mockErc20Repository = { - async get( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { - const decimals = tokenAddress === WETH ? 18 : 6; + async get(chainId: SupportedChainId, tokenAddress: string): Promise { + const decimals = tokenAddress === WETH ? 18 : 6 return { address: tokenAddress, decimals, - }; + } }, -} as jest.Mocked; +} as jest.Mocked -const cowApiClients: Record = - mapSupportedNetworks(mockApi); +const cowApiClients: Record = mapSupportedNetworks(mockApi) -const usdRepositoryCow = new UsdRepositoryCow( - cowApiClients, - mockErc20Repository -); +const usdRepositoryCow = new UsdRepositoryCow(cowApiClients, mockErc20Repository) // const cowApiMock = jest.spyOn(cowApiClientMainnet, 'GET'); @@ -51,32 +39,32 @@ describe('UsdRepositoryCow', () => { describe('getUsdPrice', () => { describe('success cases', () => { beforeEach(() => { - jest.clearAllMocks(); + jest.clearAllMocks() mockApiGet.mockImplementation(async (url, params) => { - const token = (params as any).params.path.token || undefined; + const token = (params as any).params.path.token || undefined switch (token) { case WETH: // Return WETH native price return okResponse({ data: { price: WETH_NATIVE_PRICE }, - }); + }) case USDC[SupportedChainId.MAINNET].address: // Return USDC native price return okResponse({ data: { price: USDC_PRICE }, - }); + }) default: - throw new Error('Unexpected token: ' + token); + throw new Error('Unexpected token: ' + token) } - }); - }); + }) + }) it('USD price calculation is correct', async () => { // Get USD price for WETH - const price = await usdRepositoryCow.getUsdPrice(CHAIN_ID, WETH); + const price = await usdRepositoryCow.getUsdPrice(CHAIN_ID, WETH) // Assert that the implementation did the right calls to the API - expect(mockApiGet).toHaveBeenCalledTimes(2); + expect(mockApiGet).toHaveBeenCalledTimes(2) expect(mockApiGet.mock.calls).toEqual([ [NATIVE_PRICE_ENDPOINT, { params: { path: { token: WETH } } }], [ @@ -87,18 +75,18 @@ describe('UsdRepositoryCow', () => { }, }, ], - ]); + ]) // USD calculation based on native price is correct - expect(price).toEqual(3_462.8585200136367); - }); + expect(price).toEqual(3_462.8585200136367) + }) it('Handles receiving SupportedChainId as platform', async () => { // Get USD price for WETH - const price = await usdRepositoryCow.getUsdPrice('ethereum', WETH); + const price = await usdRepositoryCow.getUsdPrice('ethereum', WETH) // Assert that the implementation did the right calls to the API - expect(mockApiGet).toHaveBeenCalledTimes(2); + expect(mockApiGet).toHaveBeenCalledTimes(2) expect(mockApiGet.mock.calls).toEqual([ [NATIVE_PRICE_ENDPOINT, { params: { path: { token: WETH } } }], [ @@ -109,12 +97,12 @@ describe('UsdRepositoryCow', () => { }, }, ], - ]); + ]) // USD calculation based on native price is correct - expect(price).toEqual(3_462.8585200136367); - }); - }); + expect(price).toEqual(3_462.8585200136367) + }) + }) describe('error cases', () => { it('Handles UnsupportedToken(400) errors', async () => { @@ -128,17 +116,17 @@ describe('UsdRepositoryCow', () => { description: 'Token not supported', }, }) - ); + ) // Get USD price for a not supported token const price = await usdRepositoryCow.getUsdPrice( CHAIN_ID, NULL_ADDRESS // See https://api.cow.fi/mainnet/api/v1/token/0x0000000000000000000000000000000000000000/native_price - ); + ) // USD calculation based on native price is correct - expect(price).toEqual(null); - }); + expect(price).toEqual(null) + }) it('Handles NewErrorTypeWeDontHandleYet(400) errors', async () => { // Mock native price @@ -148,20 +136,19 @@ describe('UsdRepositoryCow', () => { statusText: 'Bad Request', error: { errorType: 'NewErrorTypeWeDontHandleYet', - description: - "This is a new error type we don't, so we expect the repository to throw", + description: "This is a new error type we don't, so we expect the repository to throw", }, }) - ); + ) // Get USD price for a not supported token - const pricePromise = usdRepositoryCow.getUsdPrice(CHAIN_ID, WETH); + const pricePromise = usdRepositoryCow.getUsdPrice(CHAIN_ID, WETH) // USD calculation based on native price is correct expect(pricePromise).rejects.toThrow( "Error getting native prices. 400 (Bad Request): Mock response text. NewErrorTypeWeDontHandleYet: This is a new error type we don't, so we expect the repository to throw URL: http://mocked-url.mock" - ); - }); + ) + }) it('Handles NotFound(404) errors', async () => { // Mock native price @@ -171,17 +158,17 @@ describe('UsdRepositoryCow', () => { statusText: 'Not Found', error: undefined, }) - ); + ) // Get USD price for something is not even an address const price = await usdRepositoryCow.getUsdPrice( CHAIN_ID, 'this-is-not-a-token' // See https://api.cow.fi/mainnet/api/v1/token/this-is-not-a-token/native_price - ); + ) // USD calculation based on native price is correct - expect(price).toEqual(null); - }); + expect(price).toEqual(null) + }) it('Handles un-expected errors (I_AM_A_TEA_POT)', async () => { // Mock native price @@ -190,23 +177,19 @@ describe('UsdRepositoryCow', () => { status: 418, statusText: "I'm a teapot", url: 'http://calling-a-teapot.com', - text: async () => - 'This server is a teapot, and it cannot brew coffee', + text: async () => 'This server is a teapot, and it cannot brew coffee', error: undefined, }) - ); + ) // Get USD price for something is not even an address - const priceResult = usdRepositoryCow.getUsdPrice( - CHAIN_ID, - 'this-is-not-a-token' - ); + const priceResult = usdRepositoryCow.getUsdPrice(CHAIN_ID, 'this-is-not-a-token') // USD calculation based on native price is correct expect(priceResult).rejects.toThrow( "Error getting native prices. 418 (I'm a teapot): This server is a teapot, and it cannot brew coffee. URL: http://calling-a-teapot.com" - ); - }); + ) + }) it('Handles not finding token decimals', async () => { // Mock native price @@ -214,61 +197,52 @@ describe('UsdRepositoryCow', () => { okResponse({ data: { price: WETH_NATIVE_PRICE }, }) - ); + ) const mockErc20Repository = { - async get( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { - const decimals = undefined; // Simulate not finding the token decimals + async get(chainId: SupportedChainId, tokenAddress: string): Promise { + const decimals = undefined // Simulate not finding the token decimals return { address: tokenAddress, decimals, - }; + } }, - } as jest.Mocked; + } as jest.Mocked - const usdRepositoryCow = new UsdRepositoryCow( - cowApiClients, - mockErc20Repository - ); + const usdRepositoryCow = new UsdRepositoryCow(cowApiClients, mockErc20Repository) // Get USD price for a token without decimals - const price = await usdRepositoryCow.getUsdPrice(CHAIN_ID, WETH); + const price = await usdRepositoryCow.getUsdPrice(CHAIN_ID, WETH) // Should return null when missing decimals - expect(price).toBe(null); - }); + expect(price).toBe(null) + }) it('Handles receiving an unsupported chainId', async () => { - let price = await usdRepositoryCow.getUsdPrice( - 'this-is-not-a-chain-id', - WETH - ); + let price = await usdRepositoryCow.getUsdPrice('this-is-not-a-chain-id', WETH) // Should return null when received an unsupported chainId as string - expect(price).toBe(null); + expect(price).toBe(null) - price = await usdRepositoryCow.getUsdPrice('8931273', WETH); + price = await usdRepositoryCow.getUsdPrice('8931273', WETH) // Should return null when received an unsupported chainId as number - expect(price).toBe(null); - }); + expect(price).toBe(null) + }) it('Handles receiving no token address', async () => { - const price = await usdRepositoryCow.getUsdPrice(CHAIN_ID); + const price = await usdRepositoryCow.getUsdPrice(CHAIN_ID) // Should return null when received an empty token address - expect(price).toBe(null); - }); - }); - }); + expect(price).toBe(null) + }) + }) + }) describe('getUsdPrices', () => { it('Returns null', async () => { - const price = await usdRepositoryCow.getUsdPrices(CHAIN_ID, WETH, '5m'); - expect(price).toEqual(null); - }); - }); -}); + const price = await usdRepositoryCow.getUsdPrices(CHAIN_ID, WETH, '5m') + expect(price).toEqual(null) + }) + }) +}) diff --git a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCow.ts b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCow.ts index 6b289de1..8998a5d4 100644 --- a/libs/repositories/src/repos/UsdRepository/UsdRepositoryCow.ts +++ b/libs/repositories/src/repos/UsdRepository/UsdRepositoryCow.ts @@ -1,98 +1,83 @@ -import { isSupportedChain, SupportedChainId } from '@cowprotocol/cow-sdk'; -import { logger } from '@cowprotocol/shared'; -import { BigNumber } from 'bignumber.js'; -import { injectable } from 'inversify'; +import { isSupportedChain, SupportedChainId } from '@cowprotocol/cow-sdk' +import { logger } from '@cowprotocol/shared' +import { BigNumber } from 'bignumber.js' +import { injectable } from 'inversify' -import { OneBigNumber, TenBigNumber, USDC, ZeroBigNumber } from '../../const'; -import { CowApiClient } from '../../datasources/cowApi'; -import { throwIfUnsuccessful } from '../../utils/throwIfUnsuccessful'; -import { Erc20Repository } from '../Erc20Repository/Erc20Repository'; -import { UsdRepositoryNoop } from './UsdRepository'; +import { OneBigNumber, TenBigNumber, USDC, ZeroBigNumber } from '../../const' +import { CowApiClient } from '../../datasources/cowApi' +import { throwIfUnsuccessful } from '../../utils/throwIfUnsuccessful' +import { Erc20Repository } from '../Erc20Repository/Erc20Repository' +import { UsdRepositoryNoop } from './UsdRepository' -import { getSupportedCoingeckoChainId } from '../../utils/coingeckoUtils'; +import { getSupportedCoingeckoChainId } from '../../utils/coingeckoUtils' @injectable() export class UsdRepositoryCow extends UsdRepositoryNoop { - override name = 'Cow'; + override name = 'Cow' - constructor( - private cowApiClients: Record, - private erc20Repository: Erc20Repository - ) { - super(); + constructor(private cowApiClients: Record, private erc20Repository: Erc20Repository) { + super() } - async getUsdPrice( - chainIdOrSlug: string, - tokenAddress?: string | undefined - ): Promise { - const chainId = getSupportedCoingeckoChainId(chainIdOrSlug); + async getUsdPrice(chainIdOrSlug: string, tokenAddress?: string | undefined): Promise { + const chainId = getSupportedCoingeckoChainId(chainIdOrSlug) if (!chainId || !isSupportedChain(chainId)) { - return null; + return null } if (!tokenAddress) { logger.debug({ msg: `Token address is required for UsdRepositoryCow`, - }); - return null; + }) + return null } // Get native price for token (in ETH/xDAI) - const tokenNativePrice = await this.getNativePrice(chainId, tokenAddress); + const tokenNativePrice = await this.getNativePrice(chainId, tokenAddress) if (!tokenNativePrice) { logger.info({ msg: `Native price not found for ${tokenAddress} on chain ${chainId}`, - }); - return null; + }) + return null } - const erc20 = await this.erc20Repository.get(chainId, tokenAddress); - const tokenDecimals = erc20?.decimals; + const erc20 = await this.erc20Repository.get(chainId, tokenAddress) + const tokenDecimals = erc20?.decimals if (tokenDecimals === undefined) { logger.info({ msg: `Token decimals not found for ${tokenAddress} on chain ${chainId}`, - }); - return null; + }) + return null } // Get native price for USDC (in ETH/xDAI) - const { address: usdAddress, decimals: usdDecimals } = USDC[chainId]; - const usdcNativePrice = await this.getNativePrice(chainId, usdAddress); + const { address: usdAddress, decimals: usdDecimals } = USDC[chainId] + const usdcNativePrice = await this.getNativePrice(chainId, usdAddress) if (!usdcNativePrice) { logger.info({ msg: `Usd native price not found for ${usdAddress} on chain ${chainId}`, - }); - return null; + }) + return null } - const usdcPrice = invertNativeToTokenPrice( - new BigNumber(usdcNativePrice), - usdDecimals - ); - const tokenPrice = invertNativeToTokenPrice( - new BigNumber(tokenNativePrice), - tokenDecimals - ); + const usdcPrice = invertNativeToTokenPrice(new BigNumber(usdcNativePrice), usdDecimals) + const tokenPrice = invertNativeToTokenPrice(new BigNumber(tokenNativePrice), tokenDecimals) if (tokenPrice.eq(ZeroBigNumber)) { logger.info({ msg: `Token price is zero for ${tokenAddress} on chain ${chainId}`, - }); - return null; + }) + return null } - return usdcPrice.div(tokenPrice).toNumber(); + return usdcPrice.div(tokenPrice).toNumber() } - private async getNativePrice( - chainId: SupportedChainId, - tokenAddress: string - ) { - const cowApiClient = this.cowApiClients[chainId]; + private async getNativePrice(chainId: SupportedChainId, tokenAddress: string) { + const cowApiClient = this.cowApiClients[chainId] const { data: priceResult = {}, response, @@ -103,31 +88,31 @@ export class UsdRepositoryCow extends UsdRepositoryNoop { token: tokenAddress, }, }, - }); + }) // If tokens is not found, return null. See See https://api.cow.fi/mainnet/api/v1/token/this-is-not-a-token/native_price if (response.status === 404) { - return null; + return null } // Unsupported tokens return undefined. See https://api.cow.fi/mainnet/api/v1/token/0x0000000000000000000000000000000000000000/native_price if (response.status === 400) { - const errorType = (error as any)?.errorType; - const description = (error as any)?.description; + const errorType = (error as any)?.errorType + const description = (error as any)?.description if (errorType === 'UnsupportedToken') { - return null; + return null } else { await throwIfUnsuccessful( `Error getting native prices`, response, errorType && description ? `${errorType}: ${description}` : undefined - ); + ) } } - await throwIfUnsuccessful('Error getting native prices', response); + await throwIfUnsuccessful('Error getting native prices', response) - return priceResult.price || null; + return priceResult.price || null } } @@ -135,9 +120,6 @@ export class UsdRepositoryCow extends UsdRepositoryNoop { * API response value represents the amount of native token atoms needed to buy 1 atom of the specified token * This function inverts the price to represent the amount of specified token atoms needed to buy 1 atom of the native token */ -function invertNativeToTokenPrice( - value: BigNumber, - decimals: number -): BigNumber { - return OneBigNumber.times(TenBigNumber.pow(18 - decimals)).div(value); +function invertNativeToTokenPrice(value: BigNumber, decimals: number): BigNumber { + return OneBigNumber.times(TenBigNumber.pow(18 - decimals)).div(value) } diff --git a/libs/repositories/src/repos/UsdRepository/UsdRepositoryFallback.spec.ts b/libs/repositories/src/repos/UsdRepository/UsdRepositoryFallback.spec.ts index ca7af06e..d8a6390c 100644 --- a/libs/repositories/src/repos/UsdRepository/UsdRepositoryFallback.spec.ts +++ b/libs/repositories/src/repos/UsdRepository/UsdRepositoryFallback.spec.ts @@ -1,187 +1,157 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { logger } from '@cowprotocol/shared'; -import { WETH } from '../../../test/mock'; -import { PricePoint, UsdRepository } from './UsdRepository'; -import { UsdRepositoryFallback } from './UsdRepositoryFallback'; -const mockDate = new Date('2024-01-01T00:00:00Z'); +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { logger } from '@cowprotocol/shared' +import { WETH } from '../../../test/mock' +import { PricePoint, UsdRepository } from './UsdRepository' +import { UsdRepositoryFallback } from './UsdRepositoryFallback' +const mockDate = new Date('2024-01-01T00:00:00Z') class UsdRepositoryMock_1_1 implements UsdRepository { - name = 'Mock_1_1'; + name = 'Mock_1_1' async getUsdPrice() { - return 1; + return 1 } async getUsdPrices(): Promise { - return [{ date: mockDate, price: 1, volume: 1 }]; + return [{ date: mockDate, price: 1, volume: 1 }] } } class UsdRepositoryMock_2_2 implements UsdRepository { - name = 'Mock_2_2'; + name = 'Mock_2_2' async getUsdPrice(): Promise { - return 2; + return 2 } async getUsdPrices(): Promise { - return [{ date: mockDate, price: 2, volume: 2 }]; + return [{ date: mockDate, price: 2, volume: 2 }] } } class UsdRepositoryMock_null_3 implements UsdRepository { - name = 'Mock_null_3'; + name = 'Mock_null_3' async getUsdPrice() { - return null; + return null } async getUsdPrices(): Promise { - return [{ date: mockDate, price: 3, volume: 3 }]; + return [{ date: mockDate, price: 3, volume: 3 }] } } class UsdRepositoryMock_3_null implements UsdRepository { - name = 'Mock_3_null'; + name = 'Mock_3_null' async getUsdPrice() { - return 3; + return 3 } async getUsdPrices(): Promise { - return null; + return null } } class UsdRepositoryMock_null_null implements UsdRepository { - name = 'Mock_null_null'; + name = 'Mock_null_null' async getUsdPrice() { - return null; + return null } async getUsdPrices(): Promise { - return null; + return null } } -const CHAIN_ID = SupportedChainId.MAINNET.toString(); +const CHAIN_ID = SupportedChainId.MAINNET.toString() -const PARAMS_PRICE = [CHAIN_ID, WETH] as const; -const PARAMS_PRICES = [CHAIN_ID, WETH, '5m'] as const; +const PARAMS_PRICE = [CHAIN_ID, WETH] as const +const PARAMS_PRICES = [CHAIN_ID, WETH, '5m'] as const -const usdRepositoryMock_1_1 = new UsdRepositoryMock_1_1(); -const usdRepositoryMock_2_2 = new UsdRepositoryMock_2_2(); -const usdRepositoryMock_null_3 = new UsdRepositoryMock_null_3(); -const usdRepositoryMock_3_null = new UsdRepositoryMock_3_null(); -const usdRepositoryMock_null_null = new UsdRepositoryMock_null_null(); +const usdRepositoryMock_1_1 = new UsdRepositoryMock_1_1() +const usdRepositoryMock_2_2 = new UsdRepositoryMock_2_2() +const usdRepositoryMock_null_3 = new UsdRepositoryMock_null_3() +const usdRepositoryMock_3_null = new UsdRepositoryMock_3_null() +const usdRepositoryMock_null_null = new UsdRepositoryMock_null_null() describe('UsdRepositoryCoingecko', () => { describe('getUsdPrice', () => { it('Returns first repo price when is not null', async () => { - let usdRepositoryFallback = new UsdRepositoryFallback([ - usdRepositoryMock_1_1, - usdRepositoryMock_2_2, - ]); + let usdRepositoryFallback = new UsdRepositoryFallback([usdRepositoryMock_1_1, usdRepositoryMock_2_2]) - let price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE); + let price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE) - expect(price).toEqual(1); + expect(price).toEqual(1) - usdRepositoryFallback = new UsdRepositoryFallback([ - usdRepositoryMock_2_2, - usdRepositoryMock_1_1, - ]); + usdRepositoryFallback = new UsdRepositoryFallback([usdRepositoryMock_2_2, usdRepositoryMock_1_1]) - price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE); + price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE) - expect(price).toEqual(2); + expect(price).toEqual(2) - usdRepositoryFallback = new UsdRepositoryFallback([ - usdRepositoryMock_1_1, - usdRepositoryMock_null_3, - ]); + usdRepositoryFallback = new UsdRepositoryFallback([usdRepositoryMock_1_1, usdRepositoryMock_null_3]) - price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE); - expect(price).toEqual(1); - }); + price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE) + expect(price).toEqual(1) + }) it('Returns second repo price when null, and logs the name', async () => { - const loggerSpy = jest.spyOn(logger, 'info'); - const usdRepositoryFallback = new UsdRepositoryFallback([ - usdRepositoryMock_null_3, - usdRepositoryMock_1_1, - ]); - - const price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE); - expect(price).toEqual(1); + const loggerSpy = jest.spyOn(logger, 'info') + const usdRepositoryFallback = new UsdRepositoryFallback([usdRepositoryMock_null_3, usdRepositoryMock_1_1]) + + const price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE) + expect(price).toEqual(1) expect(loggerSpy).toHaveBeenCalledWith( `UsdRepositoryFallback: ${usdRepositoryMock_null_3.name} returned null for ${PARAMS_PRICE[0]}/${PARAMS_PRICE[1]}, falling back to ${usdRepositoryMock_1_1.name}` - ); - }); + ) + }) it('Returns null when configured with no repositories', async () => { - const usdRepositoryFallback = new UsdRepositoryFallback([]); - const price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE); - expect(price).toEqual(null); - }); + const usdRepositoryFallback = new UsdRepositoryFallback([]) + const price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE) + expect(price).toEqual(null) + }) it('Returns null when no repo return a price', async () => { - const usdRepositoryFallback = new UsdRepositoryFallback([ - usdRepositoryMock_null_3, - usdRepositoryMock_null_null, - ]); - const price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE); - expect(price).toEqual(null); - }); - }); + const usdRepositoryFallback = new UsdRepositoryFallback([usdRepositoryMock_null_3, usdRepositoryMock_null_null]) + const price = await usdRepositoryFallback.getUsdPrice(...PARAMS_PRICE) + expect(price).toEqual(null) + }) + }) describe('getUsdPrices', () => { it('Returns first repo prices when is not null', async () => { - let usdRepositoryFallback = new UsdRepositoryFallback([ - usdRepositoryMock_1_1, - usdRepositoryMock_2_2, - ]); + let usdRepositoryFallback = new UsdRepositoryFallback([usdRepositoryMock_1_1, usdRepositoryMock_2_2]) - let price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES); - expect(price).toEqual([{ date: mockDate, price: 1, volume: 1 }]); + let price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES) + expect(price).toEqual([{ date: mockDate, price: 1, volume: 1 }]) - usdRepositoryFallback = new UsdRepositoryFallback([ - usdRepositoryMock_2_2, - usdRepositoryMock_1_1, - ]); + usdRepositoryFallback = new UsdRepositoryFallback([usdRepositoryMock_2_2, usdRepositoryMock_1_1]) - price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES); + price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES) - expect(price).toEqual([{ date: mockDate, price: 2, volume: 2 }]); + expect(price).toEqual([{ date: mockDate, price: 2, volume: 2 }]) - usdRepositoryFallback = new UsdRepositoryFallback([ - usdRepositoryMock_1_1, - usdRepositoryMock_null_3, - ]); + usdRepositoryFallback = new UsdRepositoryFallback([usdRepositoryMock_1_1, usdRepositoryMock_null_3]) - price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES); - expect(price).toEqual([{ date: mockDate, price: 1, volume: 1 }]); - }); + price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES) + expect(price).toEqual([{ date: mockDate, price: 1, volume: 1 }]) + }) it('Returns second repo prices when null', async () => { - const usdRepositoryFallback = new UsdRepositoryFallback([ - usdRepositoryMock_null_null, - usdRepositoryMock_1_1, - ]); + const usdRepositoryFallback = new UsdRepositoryFallback([usdRepositoryMock_null_null, usdRepositoryMock_1_1]) - const price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES); - expect(price).toEqual([{ date: mockDate, price: 1, volume: 1 }]); - }); + const price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES) + expect(price).toEqual([{ date: mockDate, price: 1, volume: 1 }]) + }) it('Returns null when configured with no repositories', async () => { // When no repo is provided, it returns null - const usdRepositoryFallback = new UsdRepositoryFallback([]); - const price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES); - expect(price).toEqual(null); - }); + const usdRepositoryFallback = new UsdRepositoryFallback([]) + const price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES) + expect(price).toEqual(null) + }) it('Returns null when no repo return prices', async () => { - const usdRepositoryFallback = new UsdRepositoryFallback([ - usdRepositoryMock_3_null, - usdRepositoryMock_null_null, - ]); - const price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES); - expect(price).toEqual(null); - }); - }); -}); + const usdRepositoryFallback = new UsdRepositoryFallback([usdRepositoryMock_3_null, usdRepositoryMock_null_null]) + const price = await usdRepositoryFallback.getUsdPrices(...PARAMS_PRICES) + expect(price).toEqual(null) + }) + }) +}) diff --git a/libs/repositories/src/repos/UsdRepository/UsdRepositoryFallback.ts b/libs/repositories/src/repos/UsdRepository/UsdRepositoryFallback.ts index 2b01f44c..e1f95ac7 100644 --- a/libs/repositories/src/repos/UsdRepository/UsdRepositoryFallback.ts +++ b/libs/repositories/src/repos/UsdRepository/UsdRepositoryFallback.ts @@ -1,35 +1,29 @@ -import { logger } from '@cowprotocol/shared'; -import { injectable } from 'inversify'; -import { PricePoint, PriceStrategy, UsdRepository } from './UsdRepository'; +import { logger } from '@cowprotocol/shared' +import { injectable } from 'inversify' +import { PricePoint, PriceStrategy, UsdRepository } from './UsdRepository' @injectable() export class UsdRepositoryFallback implements UsdRepository { - name = 'Fallback'; + name = 'Fallback' constructor(private usdRepositories: UsdRepository[]) {} - async getUsdPrice( - chainIdOrSlug: string, - tokenAddress: string - ): Promise { + async getUsdPrice(chainIdOrSlug: string, tokenAddress: string): Promise { for (let i = 0; i < this.usdRepositories.length; i++) { - const usdRepository = this.usdRepositories[i]; - const price = await usdRepository.getUsdPrice( - chainIdOrSlug, - tokenAddress - ); + const usdRepository = this.usdRepositories[i] + const price = await usdRepository.getUsdPrice(chainIdOrSlug, tokenAddress) if (price !== null) { - return price; + return price } if (i < this.usdRepositories.length - 1) { - const nextRepository = this.usdRepositories[i + 1]; + const nextRepository = this.usdRepositories[i + 1] logger.info( `UsdRepositoryFallback: ${usdRepository.name} returned null for ${chainIdOrSlug}/${tokenAddress}, falling back to ${nextRepository.name}` - ); + ) } } - return null; + return null } async getUsdPrices( @@ -38,23 +32,19 @@ export class UsdRepositoryFallback implements UsdRepository { priceStrategy: PriceStrategy ): Promise { for (let i = 0; i < this.usdRepositories.length; i++) { - const usdRepository = this.usdRepositories[i]; - const prices = await usdRepository.getUsdPrices( - chainIdOrSlug, - tokenAddress, - priceStrategy - ); + const usdRepository = this.usdRepositories[i] + const prices = await usdRepository.getUsdPrices(chainIdOrSlug, tokenAddress, priceStrategy) if (prices !== null) { - return prices; + return prices } if (i < this.usdRepositories.length - 1) { - const nextRepository = this.usdRepositories[i + 1]; + const nextRepository = this.usdRepositories[i + 1] logger.info( `UsdRepositoryFallback: ${usdRepository.name} returned null for ${chainIdOrSlug}/${tokenAddress}, falling back to ${nextRepository.name}` - ); + ) } } - return null; + return null } } diff --git a/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepository.ts b/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepository.ts index 20b3d07a..6dd76b2a 100644 --- a/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepository.ts +++ b/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepository.ts @@ -1,11 +1,11 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' -export const userBalanceRepositorySymbol = Symbol.for('UserBalanceRepository'); +export const userBalanceRepositorySymbol = Symbol.for('UserBalanceRepository') export interface UserTokenBalance { - tokenAddress: string; - balance: string; - allowance: string; + tokenAddress: string + balance: string + allowance: string } export interface UserBalanceRepository { @@ -13,5 +13,5 @@ export interface UserBalanceRepository { chainId: SupportedChainId, userAddress: string, tokenAddresses: string[] - ): Promise; + ): Promise } diff --git a/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepositoryCache.ts b/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepositoryCache.ts index eedef445..51e5c7ba 100644 --- a/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepositoryCache.ts +++ b/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepositoryCache.ts @@ -1,11 +1,8 @@ -import { injectable } from 'inversify'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { CacheRepository } from '../CacheRepository/CacheRepository'; -import { - UserBalanceRepository, - UserTokenBalance, -} from './UserBalanceRepository'; -import { logger } from '@cowprotocol/shared'; +import { injectable } from 'inversify' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { CacheRepository } from '../CacheRepository/CacheRepository' +import { UserBalanceRepository, UserTokenBalance } from './UserBalanceRepository' +import { logger } from '@cowprotocol/shared' @injectable() export class UserBalanceRepositoryCache implements UserBalanceRepository { @@ -16,18 +13,12 @@ export class UserBalanceRepositoryCache implements UserBalanceRepository { private cacheTimeSeconds: number ) {} - private getCacheKey( - chainId: SupportedChainId, - userAddress: string, - tokenAddress?: string - ): string { - const baseKey = `${ - this.cachePrefix - }:${chainId}:${userAddress.toLowerCase()}`; + private getCacheKey(chainId: SupportedChainId, userAddress: string, tokenAddress?: string): string { + const baseKey = `${this.cachePrefix}:${chainId}:${userAddress.toLowerCase()}` if (tokenAddress) { - return `${baseKey}:${tokenAddress.toLowerCase()}`; + return `${baseKey}:${tokenAddress.toLowerCase()}` } - return baseKey; + return baseKey } private async getCachedBalances( @@ -35,30 +26,25 @@ export class UserBalanceRepositoryCache implements UserBalanceRepository { userAddress: string, tokenAddresses: string[] ): Promise { - const cacheKey = this.getCacheKey(chainId, userAddress); - const cached = await this.cacheRepository.get(cacheKey); + const cacheKey = this.getCacheKey(chainId, userAddress) + const cached = await this.cacheRepository.get(cacheKey) if (!cached) { - return []; + return [] } // Get the cached balances try { - const cachedBalances: UserTokenBalance[] = JSON.parse(cached); + const cachedBalances: UserTokenBalance[] = JSON.parse(cached) // Return only the relevant tokens return cachedBalances.filter((balance) => { return tokenAddresses.some((tokenAddress) => { - return ( - tokenAddress.toLowerCase() === balance.tokenAddress.toLowerCase() - ); - }); - }); + return tokenAddress.toLowerCase() === balance.tokenAddress.toLowerCase() + }) + }) } catch (error) { - logger.warn( - 'Error parsing cached balances. Proceeding to fetch fresh data.', - error - ); - return []; + logger.warn('Error parsing cached balances. Proceeding to fetch fresh data.', error) + return [] } } @@ -70,52 +56,42 @@ export class UserBalanceRepositoryCache implements UserBalanceRepository { // const cacheKey = this.getCacheKey(chainId, userAddress); // const cached = await this.cacheRepository.get(cacheKey); - const cachedBalances = await this.getCachedBalances( - chainId, - userAddress, - tokenAddresses - ); + const cachedBalances = await this.getCachedBalances(chainId, userAddress, tokenAddresses) // Group the cached balances by token address - const cachedByToken = new Map( - cachedBalances.map((balance) => [ - balance.tokenAddress.toLowerCase(), - balance, - ]) - ); + const cachedByToken = new Map(cachedBalances.map((balance) => [balance.tokenAddress.toLowerCase(), balance])) // Get the tokens that we don't have cached const missingTokenAddresses = tokenAddresses.filter((tokenAddress) => { - return !cachedByToken.has(tokenAddress.toLowerCase()); - }); + return !cachedByToken.has(tokenAddress.toLowerCase()) + }) // Return early if we have all the balances cached if (missingTokenAddresses.length == 0) { - return cachedBalances; + return cachedBalances } // Fetch the missing balances - const fetchedBalances = - await this.userBalanceRepository.getUserTokenBalances( - chainId, - userAddress, - missingTokenAddresses - ); + const fetchedBalances = await this.userBalanceRepository.getUserTokenBalances( + chainId, + userAddress, + missingTokenAddresses + ) for (const balance of fetchedBalances) { - cachedByToken.set(balance.tokenAddress.toLowerCase(), balance); + cachedByToken.set(balance.tokenAddress.toLowerCase(), balance) } // Combine cached balances and fetched balances - const mergedBalances = Array.from(cachedByToken.values()); + const mergedBalances = Array.from(cachedByToken.values()) // Cache the results await this.cacheRepository.set( this.getCacheKey(chainId, userAddress), JSON.stringify(mergedBalances), this.cacheTimeSeconds - ); + ) - return mergedBalances; + return mergedBalances } } diff --git a/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepositoryViem.ts b/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepositoryViem.ts index f5e113b4..ac636666 100644 --- a/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepositoryViem.ts +++ b/libs/repositories/src/repos/UserBalanceRepository/UserBalanceRepositoryViem.ts @@ -1,14 +1,8 @@ -import { injectable } from 'inversify'; -import { - COW_PROTOCOL_VAULT_RELAYER_ADDRESS, - SupportedChainId, -} from '@cowprotocol/cow-sdk'; -import { erc20Abi, getAddress, PublicClient } from 'viem'; +import { injectable } from 'inversify' +import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' +import { erc20Abi, getAddress, PublicClient } from 'viem' -import { - UserBalanceRepository, - UserTokenBalance, -} from './UserBalanceRepository'; +import { UserBalanceRepository, UserTokenBalance } from './UserBalanceRepository' @injectable() export class UserBalanceRepositoryViem implements UserBalanceRepository { @@ -19,14 +13,12 @@ export class UserBalanceRepositoryViem implements UserBalanceRepository { userAddress: string, tokenAddresses: string[] ): Promise { - const viemClient = this.viemClients[chainId]; - const userAddressHex = getAddress(userAddress); - const vaultRelayerAddress = getAddress( - COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId] - ); + const viemClient = this.viemClients[chainId] + const userAddressHex = getAddress(userAddress) + const vaultRelayerAddress = getAddress(COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId]) const contracts = tokenAddresses.flatMap((tokenAddress) => { - const tokenAddressHex = getAddress(tokenAddress); + const tokenAddressHex = getAddress(tokenAddress) return [ { address: tokenAddressHex, @@ -40,35 +32,32 @@ export class UserBalanceRepositoryViem implements UserBalanceRepository { functionName: 'allowance', args: [userAddressHex, vaultRelayerAddress], }, - ]; - }); + ] + }) // TODO: We need to batch the calls (it might be a loooong list of tokens) const results = await viemClient.multicall({ contracts, - }); + }) - const balances: UserTokenBalance[] = []; + const balances: UserTokenBalance[] = [] for (let i = 0; i < tokenAddresses.length; i++) { - const baseIndex = i * 2; - const balanceResult = results[baseIndex]; - const allowanceResult = results[baseIndex + 1]; - const tokenAddress = getAddress(tokenAddresses[i]); + const baseIndex = i * 2 + const balanceResult = results[baseIndex] + const allowanceResult = results[baseIndex + 1] + const tokenAddress = getAddress(tokenAddresses[i]) // TODO: Improve the error handling. This implementation drops from the result tokens where the RPC fails, which can happen. It should re-attempt or/and return the errors - if ( - balanceResult.status === 'success' && - allowanceResult.status === 'success' - ) { + if (balanceResult.status === 'success' && allowanceResult.status === 'success') { balances.push({ tokenAddress, balance: balanceResult.result.toString(), allowance: allowanceResult.result.toString(), - }); + }) } } - return balances; + return balances } } diff --git a/libs/repositories/src/utils/buildStateDiff.ts b/libs/repositories/src/utils/buildStateDiff.ts index 1e1a9a27..1f8f77a9 100644 --- a/libs/repositories/src/utils/buildStateDiff.ts +++ b/libs/repositories/src/utils/buildStateDiff.ts @@ -1,81 +1,65 @@ -import { - RawElement, - StateDiff, -} from '../repos/SimulationRepository/tenderlyTypes'; +import { RawElement, StateDiff } from '../repos/SimulationRepository/tenderlyTypes' // Helper function to find existing diff by key -function findExistingDiffIndex( - accumulatedStateDiff: StateDiff[], - diff: StateDiff -): number { - if (!diff.soltype) return -1; +function findExistingDiffIndex(accumulatedStateDiff: StateDiff[], diff: StateDiff): number { + if (!diff.soltype) return -1 - const diffKey = `${diff.address}-${diff.soltype.name}`; + const diffKey = `${diff.address}-${diff.soltype.name}` - return accumulatedStateDiff.findIndex( - (item) => item.soltype && `${item.address}-${item.soltype.name}` === diffKey - ); + return accumulatedStateDiff.findIndex((item) => item.soltype && `${item.address}-${item.soltype.name}` === diffKey) } // Helper function to process regular diffs (with complete soltype, original, dirty data) -function processRegularDiff( - accumulatedStateDiff: StateDiff[], - diff: StateDiff -): StateDiff[] { - const existingIndex = findExistingDiffIndex(accumulatedStateDiff, diff); +function processRegularDiff(accumulatedStateDiff: StateDiff[], diff: StateDiff): StateDiff[] { + const existingIndex = findExistingDiffIndex(accumulatedStateDiff, diff) if (existingIndex === -1) { // New entry - add it to our accumulated state - accumulatedStateDiff.push(structuredClone(diff)); + accumulatedStateDiff.push(structuredClone(diff)) } else { // Update existing entry - keep original values, update dirty values // Update dirty values - accumulatedStateDiff[existingIndex].dirty = structuredClone< - typeof diff.dirty - >(diff.dirty); + accumulatedStateDiff[existingIndex].dirty = structuredClone(diff.dirty) // Handle raw updates if present if (diff.raw) { - accumulatedStateDiff[existingIndex] = updateRawElements( - accumulatedStateDiff[existingIndex], - diff - ); + accumulatedStateDiff[existingIndex] = updateRawElements(accumulatedStateDiff[existingIndex], diff) } } - return accumulatedStateDiff; + return accumulatedStateDiff } // Helper function to update raw elements of a state diff function updateRawElements(stateDiff: StateDiff, diff: StateDiff): StateDiff { - if (!diff.raw) return stateDiff; + if (!diff.raw) return stateDiff if (!stateDiff.raw) { - stateDiff.raw = structuredClone(diff.raw); + stateDiff.raw = structuredClone(diff.raw) } else { // Update each raw element, preserving original values - const updatedRaw = [...stateDiff.raw]; + const updatedRaw = [...stateDiff.raw] diff.raw.forEach((rawElement) => { - const idx = updatedRaw.findIndex((item) => item.key === rawElement.key); + const idx = updatedRaw.findIndex((item) => item.key === rawElement.key) if (idx === -1) { // New raw element - updatedRaw.push(structuredClone(rawElement)); + updatedRaw.push(structuredClone(rawElement)) } else { // Update existing raw element - keep original, update dirty updatedRaw[idx] = { ...updatedRaw[idx], dirty: rawElement.dirty, - }; + } } - }); + }) - stateDiff.raw = updatedRaw; + stateDiff.raw = updatedRaw } - return stateDiff; + return stateDiff } // Helper function to find and update a raw element in accumulated state diffs @@ -86,49 +70,38 @@ function updateRawElementInAccumulated( ): boolean { const foundIndex = accumulatedStateDiff.findIndex( (stateDiff) => - stateDiff.address === diff.address && - stateDiff.raw && - stateDiff.raw.some((raw) => raw.key === rawElement.key) - ); + stateDiff.address === diff.address && stateDiff.raw && stateDiff.raw.some((raw) => raw.key === rawElement.key) + ) if (foundIndex !== -1) { - const stateDiff = accumulatedStateDiff[foundIndex]; - if (!stateDiff?.raw) return false; - const rawIndex = stateDiff.raw.findIndex( - (raw) => raw.key === rawElement.key - ); + const stateDiff = accumulatedStateDiff[foundIndex] + if (!stateDiff?.raw) return false + const rawIndex = stateDiff.raw.findIndex((raw) => raw.key === rawElement.key) // Found matching raw element - update only dirty value - const newRaw = [...stateDiff.raw]; + const newRaw = [...stateDiff.raw] newRaw[rawIndex] = { ...newRaw[rawIndex], dirty: rawElement.dirty, - }; + } accumulatedStateDiff[foundIndex] = { ...accumulatedStateDiff[foundIndex], raw: newRaw, - }; + } - return true; + return true } - return false; + return false } // Helper function to process raw-only diffs -function processRawOnlyDiff( - accumulatedStateDiff: StateDiff[], - diff: StateDiff -): StateDiff[] { - if (!diff?.raw || diff.raw.length === 0) return accumulatedStateDiff; +function processRawOnlyDiff(accumulatedStateDiff: StateDiff[], diff: StateDiff): StateDiff[] { + if (!diff?.raw || diff.raw.length === 0) return accumulatedStateDiff diff.raw.forEach((rawElement) => { - const updated = updateRawElementInAccumulated( - accumulatedStateDiff, - diff, - rawElement - ); + const updated = updateRawElementInAccumulated(accumulatedStateDiff, diff, rawElement) // If no existing entry was updated, create a new one if (!updated) { @@ -138,31 +111,28 @@ function processRawOnlyDiff( original: diff.original ?? null, dirty: diff.dirty ?? null, raw: [structuredClone(rawElement)], - }; + } - accumulatedStateDiff.push(newDiff); + accumulatedStateDiff.push(newDiff) } - }); + }) - return accumulatedStateDiff; + return accumulatedStateDiff } // Helper function to process a single diff -function processSingleDiff( - accumulatedStateDiff: StateDiff[], - diff: StateDiff -): StateDiff[] { +function processSingleDiff(accumulatedStateDiff: StateDiff[], diff: StateDiff): StateDiff[] { // Handle regular diffs (with complete soltype, original, dirty data) // Using != null (loose check) intentionally to catch both null and undefined if (diff?.soltype != null && diff?.original != null && diff?.dirty != null) { - return processRegularDiff(accumulatedStateDiff, diff); + return processRegularDiff(accumulatedStateDiff, diff) } // Handle raw-only diffs (missing soltype, original, or dirty) else if (diff?.raw && diff.raw.length > 0) { - return processRawOnlyDiff(accumulatedStateDiff, diff); + return processRawOnlyDiff(accumulatedStateDiff, diff) } - return accumulatedStateDiff; + return accumulatedStateDiff } /** @@ -183,23 +153,21 @@ function processSingleDiff( * @returns {StateDiff[][]} Array of cumulative states after each simulation */ export function buildStateDiff(stateDiffList: StateDiff[][]): StateDiff[][] { - if (stateDiffList.length === 0) return []; + if (stateDiffList.length === 0) return [] - const cumulativeStateDiff: StateDiff[][] = []; + const cumulativeStateDiff: StateDiff[][] = [] // This will store our accumulated state across all simulations - const accumulatedStateDiff: StateDiff[] = []; + const accumulatedStateDiff: StateDiff[] = [] stateDiffList.forEach((stateDiffs) => { // Process each diff in the current simulation stateDiffs.forEach((diff) => { - processSingleDiff(accumulatedStateDiff, diff); - }); + processSingleDiff(accumulatedStateDiff, diff) + }) // Add a deep copy of the current accumulated state to our results - cumulativeStateDiff.push( - structuredClone(accumulatedStateDiff) - ); - }); + cumulativeStateDiff.push(structuredClone(accumulatedStateDiff)) + }) - return cumulativeStateDiff; + return cumulativeStateDiff } diff --git a/libs/repositories/src/utils/bytesUtils.ts b/libs/repositories/src/utils/bytesUtils.ts index b6c90af2..51f00035 100644 --- a/libs/repositories/src/utils/bytesUtils.ts +++ b/libs/repositories/src/utils/bytesUtils.ts @@ -4,4 +4,4 @@ export function bytesToHexString(bytes: Buffer): string { export function hexStringToBytes(hex: string): Buffer { return Buffer.from(hex.slice(2), 'hex') -} \ No newline at end of file +} diff --git a/libs/repositories/src/utils/cache.ts b/libs/repositories/src/utils/cache.ts index bc3bc2a6..188ed2ef 100644 --- a/libs/repositories/src/utils/cache.ts +++ b/libs/repositories/src/utils/cache.ts @@ -1,8 +1,8 @@ -export type PartialCacheKey = string | number | boolean; +export type PartialCacheKey = string | number | boolean export function getCacheKey(...params: PartialCacheKey[]) { return params .filter((item) => item !== '') .map((param) => param.toString().toLowerCase()) - .join(':'); + .join(':') } diff --git a/libs/repositories/src/utils/chunkArray.ts b/libs/repositories/src/utils/chunkArray.ts index 2f1cb9ce..846ccd44 100644 --- a/libs/repositories/src/utils/chunkArray.ts +++ b/libs/repositories/src/utils/chunkArray.ts @@ -1,9 +1,9 @@ export function chunkArray(arr: T[], size: number): T[][] { - const result: T[][] = []; + const result: T[][] = [] for (let i = 0; i < arr.length; i += size) { - result.push(arr.slice(i, i + size)); + result.push(arr.slice(i, i + size)) } - return result; -} \ No newline at end of file + return result +} diff --git a/libs/repositories/src/utils/coingeckoUtils.ts b/libs/repositories/src/utils/coingeckoUtils.ts index 01ec04c3..45875522 100644 --- a/libs/repositories/src/utils/coingeckoUtils.ts +++ b/libs/repositories/src/utils/coingeckoUtils.ts @@ -1,7 +1,4 @@ -import { - COINGECKO_PLATFORMS, - SUPPORTED_COINGECKO_PLATFORMS, -} from '../datasources/coingecko'; +import { COINGECKO_PLATFORMS, SUPPORTED_COINGECKO_PLATFORMS } from '../datasources/coingecko' import { AdditionalTargetChainId, BTC_CURRENCY_ADDRESS, @@ -9,62 +6,52 @@ import { SOL_NATIVE_CURRENCY_ADDRESS, SupportedChainId, TargetChainId, -} from '@cowprotocol/cow-sdk'; +} from '@cowprotocol/cow-sdk' // for sol/btc we use our internal convention of the native address // for coingecko we should just replace the address by platform -const NON_EVM_NATIVE_TOKENS = new Set([ - getAddressKey(SOL_NATIVE_CURRENCY_ADDRESS), - getAddressKey(BTC_CURRENCY_ADDRESS), -]); +const NON_EVM_NATIVE_TOKENS = new Set([getAddressKey(SOL_NATIVE_CURRENCY_ADDRESS), getAddressKey(BTC_CURRENCY_ADDRESS)]) // Invert number→slug map to slug→SupportedChainId -const SUPPORTED_CHAIN_SLUG_TO_ID: Record = - Object.entries(SUPPORTED_COINGECKO_PLATFORMS).reduce((map, [id, slug]) => { +const SUPPORTED_CHAIN_SLUG_TO_ID: Record = Object.entries(SUPPORTED_COINGECKO_PLATFORMS).reduce( + (map, [id, slug]) => { if (slug) { - map[slug as string] = +id as TargetChainId; + map[slug as string] = +id as TargetChainId } - return map; - }, {} as Record); + return map + }, + {} as Record +) -export function getAddressOrPlatform( - tokenAddress: string | undefined, - platform: string -): string { +export function getAddressOrPlatform(tokenAddress: string | undefined, platform: string): string { if (!tokenAddress) { - return platform; + return platform } // Native currency addresses are conventions, not real contracts. // CoinGecko expects platform-level lookup for native tokens. - const addressKey = getAddressKey(tokenAddress); + const addressKey = getAddressKey(tokenAddress) if (NON_EVM_NATIVE_TOKENS.has(addressKey)) { - return platform; + return platform } // getAddressKey lowercases EVM addresses (as CoinGecko expects) // and preserves case for non-EVM addresses - return addressKey; + return addressKey } -export function getCoingeckoPlatform( - chainIdOrSlug: string -): string | undefined { +export function getCoingeckoPlatform(chainIdOrSlug: string): string | undefined { // If the chainIdOrSlug is a number, it is a chainId and should match an existing platform on Coingecko - return COINGECKO_PLATFORMS[+chainIdOrSlug] || chainIdOrSlug; + return COINGECKO_PLATFORMS[+chainIdOrSlug] || chainIdOrSlug } -export function getSupportedCoingeckoChainId( - chainIdOrSlug: string -): TargetChainId | null { - const chainIdAsNumber = +chainIdOrSlug; +export function getSupportedCoingeckoChainId(chainIdOrSlug: string): TargetChainId | null { + const chainIdAsNumber = +chainIdOrSlug // Only SupportedChainIds are supported const numericId = isNaN(chainIdAsNumber) ? SUPPORTED_CHAIN_SLUG_TO_ID[chainIdOrSlug] - : (chainIdAsNumber as TargetChainId); + : (chainIdAsNumber as TargetChainId) - return SupportedChainId[numericId] || AdditionalTargetChainId[numericId] - ? numericId - : null; + return SupportedChainId[numericId] || AdditionalTargetChainId[numericId] ? numericId : null } diff --git a/libs/repositories/src/utils/isDbEnabled.ts b/libs/repositories/src/utils/isDbEnabled.ts index 6ed67809..1350307a 100644 --- a/libs/repositories/src/utils/isDbEnabled.ts +++ b/libs/repositories/src/utils/isDbEnabled.ts @@ -1,4 +1,4 @@ export const isDbEnabled = process.env.DATABASE_ENABLED !== undefined ? process.env.DATABASE_ENABLED.toLowerCase() === 'true' - : !!process.env.DATABASE_HOST; + : !!process.env.DATABASE_HOST diff --git a/libs/repositories/src/utils/throwIfUnsuccessful.ts b/libs/repositories/src/utils/throwIfUnsuccessful.ts index 9ca217ac..09d9d6f2 100644 --- a/libs/repositories/src/utils/throwIfUnsuccessful.ts +++ b/libs/repositories/src/utils/throwIfUnsuccessful.ts @@ -1,14 +1,10 @@ -export async function throwIfUnsuccessful( - errorMessage: string, - response: Response, - context?: string -) { +export async function throwIfUnsuccessful(errorMessage: string, response: Response, context?: string) { if (!response.ok || response.status !== 200) { - const text = await response.text().catch(() => undefined); + const text = await response.text().catch(() => undefined) throw new Error( - `${errorMessage}. ${response.status} (${response.statusText})${ - text ? ': ' + text : '' - }. ${context ? context + ' ' : ''}URL: ${response.url}` - ); + `${errorMessage}. ${response.status} (${response.statusText})${text ? ': ' + text : ''}. ${ + context ? context + ' ' : '' + }URL: ${response.url}` + ) } } diff --git a/libs/repositories/test/mock.ts b/libs/repositories/test/mock.ts index 09d1a080..4ad63944 100644 --- a/libs/repositories/test/mock.ts +++ b/libs/repositories/test/mock.ts @@ -1,7 +1,7 @@ -import type { Headers, Response } from 'node-fetch'; +import type { Headers, Response } from 'node-fetch' -export const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; -export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; +export const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' +export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' const MOCK_RESPONSE: Response = { status: 200, @@ -15,37 +15,37 @@ const MOCK_RESPONSE: Response = { bodyUsed: false, size: 0, buffer(): Promise { - throw new Error('Function not implemented.'); + throw new Error('Function not implemented.') }, async text() { - return 'Mock response text'; + return 'Mock response text' }, arrayBuffer(): Promise { - throw new Error('Function not implemented.'); + throw new Error('Function not implemented.') }, clone(): Response { - throw new Error('Function not implemented.'); + throw new Error('Function not implemented.') }, blob(): Promise { - throw new Error('Function not implemented.'); + throw new Error('Function not implemented.') }, formData(): Promise { - throw new Error('Function not implemented.'); + throw new Error('Function not implemented.') }, json(): Promise { - throw new Error('Function not implemented.'); + throw new Error('Function not implemented.') }, -}; +} interface OkResponseParams extends Partial> { - data: unknown; + data: unknown } export function okResponse(params: OkResponseParams): { - data: unknown; - response: Response; + data: unknown + response: Response } { - const { status, data, ...overrides } = params; + const { status, data, ...overrides } = params return { response: { ...MOCK_RESPONSE, @@ -54,19 +54,19 @@ export function okResponse(params: OkResponseParams): { ok: true, } as Response, data, - }; + } } interface ErrorResponseParams extends Partial> { - status: number; - error: unknown; + status: number + error: unknown } export function errorResponse(params: ErrorResponseParams): { - response: Response; - error?: unknown; + response: Response + error?: unknown } { - const { status, error, ...overrides } = params; + const { status, error, ...overrides } = params return { response: { ...MOCK_RESPONSE, @@ -75,5 +75,5 @@ export function errorResponse(params: ErrorResponseParams): { ok: false, } as Response, error, - }; + } } diff --git a/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportService.config.ts b/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportService.config.ts index dfef38eb..f86778c5 100644 --- a/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportService.config.ts +++ b/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportService.config.ts @@ -1,7 +1,7 @@ export function getAffiliateProgramTableName(): string { - const value = process.env.DUNE_AFFILIATE_PROGRAM_TABLE_NAME; + const value = process.env.DUNE_AFFILIATE_PROGRAM_TABLE_NAME if (!value) { - throw new Error('DUNE_AFFILIATE_PROGRAM_TABLE_NAME is not set'); + throw new Error('DUNE_AFFILIATE_PROGRAM_TABLE_NAME is not set') } - return value; + return value } diff --git a/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportService.ts b/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportService.ts index c0b1b06a..915335ec 100644 --- a/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportService.ts +++ b/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportService.ts @@ -1,20 +1,18 @@ -export const affiliateProgramExportServiceSymbol = Symbol.for( - 'AffiliateProgramExportService' -); +export const affiliateProgramExportServiceSymbol = Symbol.for('AffiliateProgramExportService') export type AffiliateProgramSignature = { - maxUpdatedAt: string | null; - rowCount: number; -}; + maxUpdatedAt: string | null + rowCount: number +} export type AffiliateProgramExportResult = { - rows: number; - signature: AffiliateProgramSignature; -}; + rows: number + signature: AffiliateProgramSignature +} export interface AffiliateProgramExportService { - exportAffiliateProgramData(): Promise; + exportAffiliateProgramData(): Promise exportAffiliateProgramDataIfChanged( lastSignature: AffiliateProgramSignature | null - ): Promise<{ uploaded: boolean; result: AffiliateProgramExportResult }>; + ): Promise<{ uploaded: boolean; result: AffiliateProgramExportResult }> } diff --git a/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportServiceImpl.ts b/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportServiceImpl.ts index ec541475..93d96595 100644 --- a/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportServiceImpl.ts +++ b/libs/services/src/AffiliateProgramExportService/AffiliateProgramExportServiceImpl.ts @@ -1,59 +1,57 @@ -import { AffiliatesRepository, DuneRepository } from '@cowprotocol/repositories'; +import { AffiliatesRepository, DuneRepository } from '@cowprotocol/repositories' import { AffiliateProgramExportResult, AffiliateProgramExportService, AffiliateProgramSignature, -} from './AffiliateProgramExportService'; -import { getAffiliateProgramTableName } from './AffiliateProgramExportService.config'; +} from './AffiliateProgramExportService' +import { getAffiliateProgramTableName } from './AffiliateProgramExportService.config' type AffiliateProgramRow = { - code: string; - affiliate_address: string; - enabled: boolean; - reward_amount: number; - trigger_volume: number; - time_cap_days: number; - volume_cap: number; - revenue_split_affiliate_pct: number; - revenue_split_trader_pct: number; - revenue_split_dao_pct: number; - created_at: string; - updated_at: string; -}; + code: string + affiliate_address: string + enabled: boolean + reward_amount: number + trigger_volume: number + time_cap_days: number + volume_cap: number + revenue_split_affiliate_pct: number + revenue_split_trader_pct: number + revenue_split_dao_pct: number + created_at: string + updated_at: string +} -export class AffiliateProgramExportServiceImpl - implements AffiliateProgramExportService -{ +export class AffiliateProgramExportServiceImpl implements AffiliateProgramExportService { constructor( private readonly affiliatesRepository: AffiliatesRepository, private readonly duneRepository: DuneRepository ) {} async exportAffiliateProgramData(): Promise { - const { rows, signature } = await this.buildAffiliateProgramData(); - await this.upload(rows); - return { rows: rows.length, signature }; + const { rows, signature } = await this.buildAffiliateProgramData() + await this.upload(rows) + return { rows: rows.length, signature } } async exportAffiliateProgramDataIfChanged( lastSignature: AffiliateProgramSignature | null ): Promise<{ uploaded: boolean; result: AffiliateProgramExportResult }> { - const { rows, signature } = await this.buildAffiliateProgramData(); - const shouldUpload = !lastSignature || !isSameSignature(lastSignature, signature); + const { rows, signature } = await this.buildAffiliateProgramData() + const shouldUpload = !lastSignature || !isSameSignature(lastSignature, signature) if (!shouldUpload) { - return { uploaded: false, result: { rows: rows.length, signature } }; + return { uploaded: false, result: { rows: rows.length, signature } } } - await this.upload(rows); - return { uploaded: true, result: { rows: rows.length, signature } }; + await this.upload(rows) + return { uploaded: true, result: { rows: rows.length, signature } } } private async buildAffiliateProgramData(): Promise<{ - rows: AffiliateProgramRow[]; - signature: AffiliateProgramSignature; + rows: AffiliateProgramRow[] + signature: AffiliateProgramSignature }> { - const affiliates = await this.affiliatesRepository.listAffiliates(); + const affiliates = await this.affiliatesRepository.listAffiliates() const rows = affiliates.map((affiliate) => ({ code: affiliate.code.trim().toUpperCase(), affiliate_address: affiliate.walletAddress.toLowerCase(), @@ -67,17 +65,17 @@ export class AffiliateProgramExportServiceImpl revenue_split_dao_pct: affiliate.revenueSplitDaoPct, created_at: affiliate.createdAt, updated_at: affiliate.updatedAt, - })); + })) const maxUpdatedAt = rows.reduce((max, row) => { if (!row.updated_at) { - return max; + return max } if (!max || row.updated_at > max) { - return row.updated_at; + return row.updated_at } - return max; - }, null); + return max + }, null) return { rows, @@ -85,32 +83,26 @@ export class AffiliateProgramExportServiceImpl maxUpdatedAt, rowCount: rows.length, }, - }; + } } private async upload(rows: AffiliateProgramRow[]): Promise { - const csv = buildCsv(rows); - const tableName = getAffiliateProgramTableName(); + const csv = buildCsv(rows) + const tableName = getAffiliateProgramTableName() const response = await this.duneRepository.uploadCsv({ tableName, data: csv, isPrivate: true, - }); + }) if (!response.success) { - const message = response.message ? `: ${response.message}` : ''; - throw new Error(`Dune CSV upload failed for ${tableName}${message}`); + const message = response.message ? `: ${response.message}` : '' + throw new Error(`Dune CSV upload failed for ${tableName}${message}`) } } } -function isSameSignature( - left: AffiliateProgramSignature, - right: AffiliateProgramSignature -): boolean { - return ( - left.rowCount === right.rowCount && - left.maxUpdatedAt === right.maxUpdatedAt - ); +function isSameSignature(left: AffiliateProgramSignature, right: AffiliateProgramSignature): boolean { + return left.rowCount === right.rowCount && left.maxUpdatedAt === right.maxUpdatedAt } const CSV_HEADERS = [ @@ -125,10 +117,10 @@ const CSV_HEADERS = [ 'revenue_split_trader_pct', 'revenue_split_dao_pct', 'created_at', -]; +] function buildCsv(rows: AffiliateProgramRow[]): string { - const header = CSV_HEADERS.join(','); + const header = CSV_HEADERS.join(',') const lines = rows.map((row) => [ row.code, @@ -145,15 +137,15 @@ function buildCsv(rows: AffiliateProgramRow[]): string { ] .map(csvEscape) .join(',') - ); + ) - return [header, ...lines].join('\n'); + return [header, ...lines].join('\n') } function csvEscape(value: unknown): string { - const stringValue = String(value ?? ''); + const stringValue = String(value ?? '') if (/[",\n]/.test(stringValue)) { - return `"${stringValue.replace(/"/g, '""')}"`; + return `"${stringValue.replace(/"/g, '""')}"` } - return stringValue; + return stringValue } diff --git a/libs/services/src/AffiliateStatsService/AffiliateStatsService.config.ts b/libs/services/src/AffiliateStatsService/AffiliateStatsService.config.ts index bb82086b..dd040399 100644 --- a/libs/services/src/AffiliateStatsService/AffiliateStatsService.config.ts +++ b/libs/services/src/AffiliateStatsService/AffiliateStatsService.config.ts @@ -1,27 +1,27 @@ export function getDuneQueryIds(): { - traderStats: number; - affiliateStats: number; + traderStats: number + affiliateStats: number } { - const traderRaw = process.env.DUNE_QUERY_ID_TRADER_STATS; + const traderRaw = process.env.DUNE_QUERY_ID_TRADER_STATS if (!traderRaw) { - throw new Error('DUNE_QUERY_ID_TRADER_STATS is not set'); + throw new Error('DUNE_QUERY_ID_TRADER_STATS is not set') } - const traderStats = Number.parseInt(traderRaw, 10); + const traderStats = Number.parseInt(traderRaw, 10) if (Number.isNaN(traderStats)) { - throw new Error('DUNE_QUERY_ID_TRADER_STATS must be an integer'); + throw new Error('DUNE_QUERY_ID_TRADER_STATS must be an integer') } - const affiliateRaw = process.env.DUNE_QUERY_ID_AFFILIATE_STATS; + const affiliateRaw = process.env.DUNE_QUERY_ID_AFFILIATE_STATS if (!affiliateRaw) { - throw new Error('DUNE_QUERY_ID_AFFILIATE_STATS is not set'); + throw new Error('DUNE_QUERY_ID_AFFILIATE_STATS is not set') } - const affiliateStats = Number.parseInt(affiliateRaw, 10); + const affiliateStats = Number.parseInt(affiliateRaw, 10) if (Number.isNaN(affiliateStats)) { - throw new Error('DUNE_QUERY_ID_AFFILIATE_STATS must be an integer'); + throw new Error('DUNE_QUERY_ID_AFFILIATE_STATS must be an integer') } - return { traderStats, affiliateStats }; + return { traderStats, affiliateStats } } -export const DUNE_PAGE_SIZE = 1000 as const; -export const DUNE_MAX_ROWS = 1_000_000 as const; +export const DUNE_PAGE_SIZE = 1000 as const +export const DUNE_MAX_ROWS = 1_000_000 as const diff --git a/libs/services/src/AffiliateStatsService/AffiliateStatsService.ts b/libs/services/src/AffiliateStatsService/AffiliateStatsService.ts index 1b49056f..7e71588d 100644 --- a/libs/services/src/AffiliateStatsService/AffiliateStatsService.ts +++ b/libs/services/src/AffiliateStatsService/AffiliateStatsService.ts @@ -1,42 +1,42 @@ -export const affiliateStatsServiceSymbol = Symbol.for('AffiliateStatsService'); +export const affiliateStatsServiceSymbol = Symbol.for('AffiliateStatsService') export interface TraderStatsRow { - trader_address: string; - bound_referrer_code: string; - linked_since: string; - rewards_end: string; - eligible_volume: T; - left_to_next_rewards: T; - trigger_volume: T; - total_earned: T; - paid_out: T; - next_payout: T; + trader_address: string + bound_referrer_code: string + linked_since: string + rewards_end: string + eligible_volume: T + left_to_next_rewards: T + trigger_volume: T + total_earned: T + paid_out: T + next_payout: T } export interface AffiliateStatsRow { - affiliate_address: string; - referrer_code: string; - total_volume: T; - trigger_volume: T; - total_earned: T; - paid_out: T; - next_payout: T; - left_to_next_reward: T; - active_traders: T; - total_traders: T; + affiliate_address: string + referrer_code: string + total_volume: T + trigger_volume: T + total_earned: T + paid_out: T + next_payout: T + left_to_next_reward: T + active_traders: T + total_traders: T } export interface AffiliateStatsResult { - rows: AffiliateStatsRow[]; - lastUpdatedAt: string; + rows: AffiliateStatsRow[] + lastUpdatedAt: string } export interface TraderStatsResult { - rows: TraderStatsRow[]; - lastUpdatedAt: string; + rows: TraderStatsRow[] + lastUpdatedAt: string } export interface AffiliateStatsService { - getTraderStats(address: string): Promise; - getAffiliateStats(address: string): Promise; + getTraderStats(address: string): Promise + getAffiliateStats(address: string): Promise } diff --git a/libs/services/src/AffiliateStatsService/AffiliateStatsService.types.ts b/libs/services/src/AffiliateStatsService/AffiliateStatsService.types.ts index f31167d0..c15e3397 100644 --- a/libs/services/src/AffiliateStatsService/AffiliateStatsService.types.ts +++ b/libs/services/src/AffiliateStatsService/AffiliateStatsService.types.ts @@ -1,16 +1,13 @@ -import type { - AffiliateStatsRow, - TraderStatsRow, -} from './AffiliateStatsService'; +import type { AffiliateStatsRow, TraderStatsRow } from './AffiliateStatsService' export interface CacheEntry { - expiresAt: number; - rows: T[]; - lastUpdatedAt: string; + expiresAt: number + rows: T[] + lastUpdatedAt: string } -export type NumericValue = number | string; +export type NumericValue = number | string -export type TraderStatsRowRaw = TraderStatsRow; +export type TraderStatsRowRaw = TraderStatsRow -export type AffiliateStatsRowRaw = AffiliateStatsRow; +export type AffiliateStatsRowRaw = AffiliateStatsRow diff --git a/libs/services/src/AffiliateStatsService/AffiliateStatsService.utils.ts b/libs/services/src/AffiliateStatsService/AffiliateStatsService.utils.ts index 510b6d14..183fc9c3 100644 --- a/libs/services/src/AffiliateStatsService/AffiliateStatsService.utils.ts +++ b/libs/services/src/AffiliateStatsService/AffiliateStatsService.utils.ts @@ -1,18 +1,10 @@ -import type { - AffiliateStatsRow, - TraderStatsRow, -} from './AffiliateStatsService'; -import type { - AffiliateStatsRowRaw, - TraderStatsRowRaw, -} from './AffiliateStatsService.types'; -import { isNumeric, isRecord, isString, toNumber } from '../utils/type-checking-utils'; +import type { AffiliateStatsRow, TraderStatsRow } from './AffiliateStatsService' +import type { AffiliateStatsRowRaw, TraderStatsRowRaw } from './AffiliateStatsService.types' +import { isNumeric, isRecord, isString, toNumber } from '../utils/type-checking-utils' -export function isTraderStatsRowRaw( - data: unknown -): data is TraderStatsRowRaw { +export function isTraderStatsRowRaw(data: unknown): data is TraderStatsRowRaw { if (!isRecord(data)) { - return false; + return false } return ( @@ -26,14 +18,12 @@ export function isTraderStatsRowRaw( isNumeric(data.total_earned) && isNumeric(data.paid_out) && isNumeric(data.next_payout) - ); + ) } -export function isAffiliateStatsRowRaw( - data: unknown -): data is AffiliateStatsRowRaw { +export function isAffiliateStatsRowRaw(data: unknown): data is AffiliateStatsRowRaw { if (!isRecord(data)) { - return false; + return false } return ( @@ -47,32 +37,25 @@ export function isAffiliateStatsRowRaw( isNumeric(data.left_to_next_reward) && isNumeric(data.active_traders) && isNumeric(data.total_traders) - ); + ) } -export function normalizeTraderStatsRow( - row: TraderStatsRowRaw -): TraderStatsRow { +export function normalizeTraderStatsRow(row: TraderStatsRowRaw): TraderStatsRow { return { trader_address: row.trader_address, bound_referrer_code: row.bound_referrer_code, linked_since: row.linked_since, rewards_end: row.rewards_end, eligible_volume: toNumber(row.eligible_volume, 'eligible_volume'), - left_to_next_rewards: toNumber( - row.left_to_next_rewards, - 'left_to_next_rewards' - ), + left_to_next_rewards: toNumber(row.left_to_next_rewards, 'left_to_next_rewards'), trigger_volume: toNumber(row.trigger_volume, 'trigger_volume'), total_earned: toNumber(row.total_earned, 'total_earned'), paid_out: toNumber(row.paid_out, 'paid_out'), next_payout: toNumber(row.next_payout, 'next_payout'), - }; + } } -export function normalizeAffiliateStatsRow( - row: AffiliateStatsRowRaw -): AffiliateStatsRow { +export function normalizeAffiliateStatsRow(row: AffiliateStatsRowRaw): AffiliateStatsRow { return { affiliate_address: row.affiliate_address, referrer_code: row.referrer_code, @@ -81,11 +64,8 @@ export function normalizeAffiliateStatsRow( total_earned: toNumber(row.total_earned, 'total_earned'), paid_out: toNumber(row.paid_out, 'paid_out'), next_payout: toNumber(row.next_payout, 'next_payout'), - left_to_next_reward: toNumber( - row.left_to_next_reward, - 'left_to_next_reward' - ), + left_to_next_reward: toNumber(row.left_to_next_reward, 'left_to_next_reward'), active_traders: toNumber(row.active_traders, 'active_traders'), total_traders: toNumber(row.total_traders, 'total_traders'), - }; + } } diff --git a/libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts b/libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts index e82a7d4a..c6fe8660 100644 --- a/libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts +++ b/libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts @@ -1,185 +1,168 @@ -import { logger } from '@cowprotocol/shared'; -import { DuneRepository } from '@cowprotocol/repositories'; +import { logger } from '@cowprotocol/shared' +import { DuneRepository } from '@cowprotocol/repositories' import { AffiliateStatsResult, AffiliateStatsRow, AffiliateStatsService, TraderStatsResult, TraderStatsRow, -} from './AffiliateStatsService'; -import { - DUNE_MAX_ROWS, - DUNE_PAGE_SIZE, - getDuneQueryIds, -} from './AffiliateStatsService.config'; -import type { AffiliateStatsRowRaw, CacheEntry, TraderStatsRowRaw } from './AffiliateStatsService.types'; +} from './AffiliateStatsService' +import { DUNE_MAX_ROWS, DUNE_PAGE_SIZE, getDuneQueryIds } from './AffiliateStatsService.config' +import type { AffiliateStatsRowRaw, CacheEntry, TraderStatsRowRaw } from './AffiliateStatsService.types' import { isAffiliateStatsRowRaw, isTraderStatsRowRaw, normalizeAffiliateStatsRow, normalizeTraderStatsRow, -} from './AffiliateStatsService.utils'; +} from './AffiliateStatsService.utils' export class AffiliateStatsServiceImpl implements AffiliateStatsService { - private readonly duneRepository: DuneRepository; - private readonly cacheTtlMs: number; - private readonly cache = new Map>(); + private readonly duneRepository: DuneRepository + private readonly cacheTtlMs: number + private readonly cache = new Map>() constructor(duneRepository: DuneRepository, cacheTtlMs: number) { - this.duneRepository = duneRepository; - this.cacheTtlMs = cacheTtlMs; + this.duneRepository = duneRepository + this.cacheTtlMs = cacheTtlMs } async getTraderStats(address: string): Promise { - const normalizedAddress = address.toLowerCase(); - const { rows, lastUpdatedAt } = await this.getCachedQuery< - TraderStatsRowRaw, - TraderStatsRow - >({ + const normalizedAddress = address.toLowerCase() + const { rows, lastUpdatedAt } = await this.getCachedQuery({ cacheKey: 'affiliate-trader-stats', queryId: getDuneQueryIds().traderStats, typeAssertion: isTraderStatsRowRaw, mapRow: normalizeTraderStatsRow, - }); + }) - const filtered = rows.filter( - (row) => row.trader_address.toLowerCase() === normalizedAddress - ); + const filtered = rows.filter((row) => row.trader_address.toLowerCase() === normalizedAddress) - return { rows: filtered, lastUpdatedAt }; + return { rows: filtered, lastUpdatedAt } } async getAffiliateStats(address: string): Promise { - const normalizedAddress = address.toLowerCase(); - const { rows, lastUpdatedAt } = await this.getCachedQuery< - AffiliateStatsRowRaw, - AffiliateStatsRow - >({ + const normalizedAddress = address.toLowerCase() + const { rows, lastUpdatedAt } = await this.getCachedQuery({ cacheKey: 'affiliate-stats', queryId: getDuneQueryIds().affiliateStats, typeAssertion: isAffiliateStatsRowRaw, mapRow: normalizeAffiliateStatsRow, - }); + }) - const filtered = rows.filter( - (row) => row.affiliate_address.toLowerCase() === normalizedAddress - ); + const filtered = rows.filter((row) => row.affiliate_address.toLowerCase() === normalizedAddress) - return { rows: filtered, lastUpdatedAt }; + return { rows: filtered, lastUpdatedAt } } private async getCachedQuery(params: { - cacheKey: string; - queryId: number; - typeAssertion: (data: unknown) => data is T; - mapRow: (row: T) => U; + cacheKey: string + queryId: number + typeAssertion: (data: unknown) => data is T + mapRow: (row: T) => U }): Promise<{ rows: U[]; lastUpdatedAt: string }> { - const cached = this.getCache(params.cacheKey); + const cached = this.getCache(params.cacheKey) if (cached) { - return { rows: cached.rows as U[], lastUpdatedAt: cached.lastUpdatedAt }; + return { rows: cached.rows as U[], lastUpdatedAt: cached.lastUpdatedAt } } - logger.debug(`Affiliate stats cache miss for ${params.cacheKey}.`); + logger.debug(`Affiliate stats cache miss for ${params.cacheKey}.`) try { - const limit = DUNE_PAGE_SIZE; - let offset = 0; - let total: number | null = null; - const rows: U[] = []; - let lastUpdatedAt: string | undefined = undefined; + const limit = DUNE_PAGE_SIZE + let offset = 0 + let total: number | null = null + const rows: U[] = [] + let lastUpdatedAt: string | undefined = undefined - let hasMore = true; + let hasMore = true while (hasMore) { const result = await this.duneRepository.getQueryResults({ queryId: params.queryId, typeAssertion: params.typeAssertion, limit, offset, - }); + }) if (!lastUpdatedAt) { - lastUpdatedAt = - result.execution_started_at || - result.execution_ended_at || - result.submitted_at; + lastUpdatedAt = result.execution_started_at || result.execution_ended_at || result.submitted_at } - const pageRows = result.result.rows.map(params.mapRow); - rows.push(...pageRows); + const pageRows = result.result.rows.map(params.mapRow) + rows.push(...pageRows) if (total === null) { - const metaTotal = result.result.metadata?.total_row_count; + const metaTotal = result.result.metadata?.total_row_count if (typeof metaTotal === 'number') { - total = metaTotal; + total = metaTotal } } if (result.result.rows.length === 0) { - hasMore = false; - continue; + hasMore = false + continue } - offset += result.result.rows.length; + offset += result.result.rows.length if (total !== null && offset >= total) { - hasMore = false; - continue; + hasMore = false + continue } if (result.result.rows.length < limit) { - hasMore = false; - continue; + hasMore = false + continue } if (offset >= DUNE_MAX_ROWS) { logger.warn( { cacheKey: params.cacheKey, maxRows: DUNE_MAX_ROWS }, 'Affiliate stats row limit reached. Stopping pagination.' - ); - hasMore = false; - continue; + ) + hasMore = false + continue } } - const resolvedLastUpdatedAt = lastUpdatedAt ?? new Date().toISOString(); - this.setCache(params.cacheKey, rows, resolvedLastUpdatedAt); - return { rows, lastUpdatedAt: resolvedLastUpdatedAt }; + const resolvedLastUpdatedAt = lastUpdatedAt ?? new Date().toISOString() + this.setCache(params.cacheKey, rows, resolvedLastUpdatedAt) + return { rows, lastUpdatedAt: resolvedLastUpdatedAt } } catch (error) { - logger.error({ error }, `Affiliate stats Dune query failed (${params.cacheKey}).`); - throw error; + logger.error({ error }, `Affiliate stats Dune query failed (${params.cacheKey}).`) + throw error } } private getCache(key: string): CacheEntry | null { - const cached = this.cache.get(key); + const cached = this.cache.get(key) if (!cached) { - return null; + return null } if (Date.now() >= cached.expiresAt) { - this.cache.delete(key); - return null; + this.cache.delete(key) + return null } if (!cached.lastUpdatedAt) { - this.cache.delete(key); - return null; + this.cache.delete(key) + return null } - logger.debug(`Affiliate stats cache hit for ${key}.`); - return cached as CacheEntry; + logger.debug(`Affiliate stats cache hit for ${key}.`) + return cached as CacheEntry } private setCache(key: string, rows: T[], lastUpdatedAt: string): void { if (this.cacheTtlMs <= 0) { - return; + return } - logger.debug(`Affiliate stats cache set for ${key}.`); + logger.debug(`Affiliate stats cache set for ${key}.`) this.cache.set(key, { expiresAt: Date.now() + this.cacheTtlMs, rows, lastUpdatedAt, - }); + }) } } diff --git a/libs/services/src/BalanceTrackingService/BalanceTrackingService.ts b/libs/services/src/BalanceTrackingService/BalanceTrackingService.ts index 4723e4a2..ba3daf81 100644 --- a/libs/services/src/BalanceTrackingService/BalanceTrackingService.ts +++ b/libs/services/src/BalanceTrackingService/BalanceTrackingService.ts @@ -1,42 +1,35 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { SSEClient } from '../SSEService/SSEService'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { SSEClient } from '../SSEService/SSEService' -export const balanceTrackingServiceSymbol = Symbol.for( - 'BalanceTrackingService' -); +export const balanceTrackingServiceSymbol = Symbol.for('BalanceTrackingService') /** * Balance or allowance change event */ -export type BalanceAllowanceChangeEvent = - | BalanceChangeEvent - | AllowanceChangeEvent; +export type BalanceAllowanceChangeEvent = BalanceChangeEvent | AllowanceChangeEvent export interface BalanceAllowanceChangeEventCommon { - chainId: SupportedChainId; - timestamp: number; - userAddress: string; - tokenAddress: string; + chainId: SupportedChainId + timestamp: number + userAddress: string + tokenAddress: string } export interface BalanceChangeEvent extends BalanceAllowanceChangeEventCommon { - type: 'balance_change'; - oldBalance: string; - newBalance: string; + type: 'balance_change' + oldBalance: string + newBalance: string } -export interface AllowanceChangeEvent - extends BalanceAllowanceChangeEventCommon { - type: 'allowance_change'; - oldAllowance: string; - newAllowance: string; +export interface AllowanceChangeEvent extends BalanceAllowanceChangeEventCommon { + type: 'allowance_change' + oldAllowance: string + newAllowance: string } -export type BalanceChangeCallback = ( - event: BalanceAllowanceChangeEvent -) => void; +export type BalanceChangeCallback = (event: BalanceAllowanceChangeEvent) => void -export type BalanceTrackingRequest = Omit; +export type BalanceTrackingRequest = Omit /** * Service that keeps track of some user's balance and allowances, and notifies changes @@ -48,17 +41,14 @@ export interface BalanceTrackingService { * @param userAddress * @param tokenAddresses List of tokens to monitor */ - startTrackingUser(request: BalanceTrackingRequest): Promise; + startTrackingUser(request: BalanceTrackingRequest): Promise /** * Stops tracking some user's balances and allowances * @param chainId * @param userAddress */ - stopTrackingUser( - chainId: SupportedChainId, - userAddress: string - ): Promise; + stopTrackingUser(chainId: SupportedChainId, userAddress: string): Promise /** * Updates the list of tracked tokens for a user. @@ -66,20 +56,16 @@ export interface BalanceTrackingService { * @param userAddress * @param tokenAddresses List of tokens to monitor */ - updateTrackedTokens( - chainId: SupportedChainId, - userAddress: string, - tokenAddresses: string[] - ): Promise; + updateTrackedTokens(chainId: SupportedChainId, userAddress: string, tokenAddresses: string[]): Promise /** * Get all users being tracked */ - getTrackedUsers(): Promise>; + getTrackedUsers(): Promise> /** * Event triggered when there's a change on balance or allowance for a tracked user * @param callback */ - onBalanceChange(callback: BalanceChangeCallback): void; + onBalanceChange(callback: BalanceChangeCallback): void } diff --git a/libs/services/src/BalanceTrackingService/BalanceTrackingServiceMain.ts b/libs/services/src/BalanceTrackingService/BalanceTrackingServiceMain.ts index 2c77c1ed..459f1857 100644 --- a/libs/services/src/BalanceTrackingService/BalanceTrackingServiceMain.ts +++ b/libs/services/src/BalanceTrackingService/BalanceTrackingServiceMain.ts @@ -1,42 +1,39 @@ -import { injectable, inject } from 'inversify'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { logger } from '@cowprotocol/shared'; -import { UserTokenBalanceWithToken } from '../TokenBalancesService/TokenBalancesService'; +import { injectable, inject } from 'inversify' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { logger } from '@cowprotocol/shared' +import { UserTokenBalanceWithToken } from '../TokenBalancesService/TokenBalancesService' import { BalanceTrackingService, BalanceAllowanceChangeEvent, BalanceChangeCallback, BalanceTrackingRequest, -} from './BalanceTrackingService'; -import { - TokenBalancesService, - tokenBalancesServiceSymbol, -} from '../TokenBalancesService/TokenBalancesService'; -import { SSEService, sseServiceSymbol } from '../SSEService/SSEService'; +} from './BalanceTrackingService' +import { TokenBalancesService, tokenBalancesServiceSymbol } from '../TokenBalancesService/TokenBalancesService' +import { SSEService, sseServiceSymbol } from '../SSEService/SSEService' -const POLLING_INTERVAL_MS = 5000; // 5 seconds // TODO: We should do this service more sophisticated. When a client subscribes, we poll more often, but because we want to use this is notifications, we might want to have other lower prio checking logics +const POLLING_INTERVAL_MS = 5000 // 5 seconds // TODO: We should do this service more sophisticated. When a client subscribes, we poll more often, but because we want to use this is notifications, we might want to have other lower prio checking logics export interface TrackedUser extends BalanceTrackingRequest { /** * Keep in memory the latest balances */ - lastBalances: Map; + lastBalances: Map /** * Interval ID to poll for changes */ - intervalId: NodeJS.Timeout; + intervalId: NodeJS.Timeout /** * Flag to indicate if the user is currently being checked */ - isChecking: boolean; + isChecking: boolean } @injectable() export class BalanceTrackingServiceMain implements BalanceTrackingService { - private trackedUsers = new Map(); - private balanceChangeCallbacks: Array = []; + private trackedUsers = new Map() + private balanceChangeCallbacks: Array = [] constructor( @inject(tokenBalancesServiceSymbol) @@ -45,10 +42,10 @@ export class BalanceTrackingServiceMain implements BalanceTrackingService { ) { // Auto-connect to SSE service if available if (this.sseService) { - const sseService = this.sseService; + const sseService = this.sseService this.onBalanceChange((event) => { - sseService.broadcastBalanceUpdate(event); - }); + sseService.broadcastBalanceUpdate(event) + }) } } @@ -56,40 +53,33 @@ export class BalanceTrackingServiceMain implements BalanceTrackingService { request: BalanceTrackingRequest, balancesForTokens: UserTokenBalanceWithToken[] ): { - trackedUser: TrackedUser; - isNewTracking: boolean; + trackedUser: TrackedUser + isNewTracking: boolean } { - const { clientId, chainId, tokenAddresses, userAddress } = request; - const key = this.getUserKey(chainId, userAddress); - const existingTrackedUser = this.trackedUsers.get(key); - const normalizedTokenAddresses = - this.normalizeTokenAddresses(tokenAddresses); + const { clientId, chainId, tokenAddresses, userAddress } = request + const key = this.getUserKey(chainId, userAddress) + const existingTrackedUser = this.trackedUsers.get(key) + const normalizedTokenAddresses = this.normalizeTokenAddresses(tokenAddresses) if (existingTrackedUser) { // Merge token addresses (old tokens + new tokens) - const mergedTokensToTrack = this.mergeTokenAddresses( - existingTrackedUser.tokenAddresses, - normalizedTokenAddresses - ); + const mergedTokensToTrack = this.mergeTokenAddresses(existingTrackedUser.tokenAddresses, normalizedTokenAddresses) // Update the last known balances balancesForTokens.forEach((balance) => { - existingTrackedUser.lastBalances.set( - balance.token.address.toLowerCase(), - balance - ); - }); - existingTrackedUser.tokenAddresses = mergedTokensToTrack; + existingTrackedUser.lastBalances.set(balance.token.address.toLowerCase(), balance) + }) + existingTrackedUser.tokenAddresses = mergedTokensToTrack return { trackedUser: existingTrackedUser, isNewTracking: false, - }; + } } else { - const updatedLastBalances = new Map(); + const updatedLastBalances = new Map() balancesForTokens.forEach((balance) => { - updatedLastBalances.set(balance.token.address.toLowerCase(), balance); - }); + updatedLastBalances.set(balance.token.address.toLowerCase(), balance) + }) const trackedUser: TrackedUser = { clientId, @@ -99,60 +89,51 @@ export class BalanceTrackingServiceMain implements BalanceTrackingService { lastBalances: updatedLastBalances, intervalId: undefined as unknown as NodeJS.Timeout, isChecking: false, - }; + } return { trackedUser, isNewTracking: true, - }; + } } } async startTrackingUser(request: BalanceTrackingRequest): Promise { - const { clientId, chainId, tokenAddresses, userAddress } = request; + const { clientId, chainId, tokenAddresses, userAddress } = request - const normalizedTokenAddresses = - this.normalizeTokenAddresses(tokenAddresses); + const normalizedTokenAddresses = this.normalizeTokenAddresses(tokenAddresses) // Get initial balances for this client // TODO: It is not great that if this fails, the subscription is not done. This should be refined (hackathon mode) - const balancesForTokens = - await this.tokenBalancesService.getUserTokenBalances({ - chainId, - userAddress, - tokenAddresses: normalizedTokenAddresses, - }); + const balancesForTokens = await this.tokenBalancesService.getUserTokenBalances({ + chainId, + userAddress, + tokenAddresses: normalizedTokenAddresses, + }) // Update the tracked user - const { trackedUser, isNewTracking } = this.mergeBalanceTrackingRequest( - request, - balancesForTokens - ); + const { trackedUser, isNewTracking } = this.mergeBalanceTrackingRequest(request, balancesForTokens) // Broadcast the initial balances to the client if (this.sseService) { - this.sseService.broadcastInitialBalances(clientId, balancesForTokens); + this.sseService.broadcastInitialBalances(clientId, balancesForTokens) } - const key = this.getUserKey(chainId, userAddress); - this.trackedUsers.set(key, trackedUser); + const key = this.getUserKey(chainId, userAddress) + this.trackedUsers.set(key, trackedUser) if (isNewTracking) { logger.info( `Created new tracking for user ${userAddress} on chain ${chainId}. Tracking ${normalizedTokenAddresses.length} tokens` - ); + ) // Start polling for changes - const intervalId = this.startPollingForChanges( - chainId, - userAddress, - trackedUser - ); - trackedUser.intervalId = intervalId; + const intervalId = this.startPollingForChanges(chainId, userAddress, trackedUser) + trackedUser.intervalId = intervalId } else { logger.info( `Updated tracking user ${userAddress} on chain ${chainId}. Tracking ${normalizedTokenAddresses.length} new tokens. Total tokens: ${trackedUser.tokenAddresses.length}` - ); + ) } } @@ -164,123 +145,98 @@ export class BalanceTrackingServiceMain implements BalanceTrackingService { // TODO: This is too simplistic implementation. Ideally we don't do an interval per subscription, but instead we have a general periodic check that updates all subscriptions. For now, lets keep simple, but would be nice to refine. const intervalId = setInterval(async () => { if (trackedUser.isChecking) { - return; + return } - trackedUser.isChecking = true; + trackedUser.isChecking = true try { - await this.checkBalanceChanges( - chainId, - userAddress, - trackedUser.tokenAddresses, - trackedUser.lastBalances - ); + await this.checkBalanceChanges(chainId, userAddress, trackedUser.tokenAddresses, trackedUser.lastBalances) } catch (error) { logger.error( `[${intervalId}] Error checking balance changes for chainId=${chainId}, userAddress=${userAddress}, lastBalances=${trackedUser.lastBalances}:\n`, error - ); + ) } finally { - trackedUser.isChecking = false; + trackedUser.isChecking = false } - }, POLLING_INTERVAL_MS); + }, POLLING_INTERVAL_MS) logger.info( `Started tracking user ${userAddress} on chain ${chainId} for ${trackedUser.tokenAddresses.length} tokens. IntervalId=${intervalId}` - ); + ) - return intervalId; + return intervalId } - async stopTrackingUser( - chainId: SupportedChainId, - userAddress: string - ): Promise { - const key = this.getUserKey(chainId, userAddress); - const trackedUser = this.trackedUsers.get(key); + async stopTrackingUser(chainId: SupportedChainId, userAddress: string): Promise { + const key = this.getUserKey(chainId, userAddress) + const trackedUser = this.trackedUsers.get(key) if (trackedUser) { - clearInterval(trackedUser.intervalId); - this.trackedUsers.delete(key); - logger.info(`Stopped tracking user ${userAddress} on chain ${chainId}`); + clearInterval(trackedUser.intervalId) + this.trackedUsers.delete(key) + logger.info(`Stopped tracking user ${userAddress} on chain ${chainId}`) } } /** * Updates the list of tracked tokens for a user */ - async updateTrackedTokens( - chainId: SupportedChainId, - userAddress: string, - tokenAddresses: string[] - ): Promise { + async updateTrackedTokens(chainId: SupportedChainId, userAddress: string, tokenAddresses: string[]): Promise { if (tokenAddresses.length === 0) { - await this.stopTrackingUser(chainId, userAddress); - return; + await this.stopTrackingUser(chainId, userAddress) + return } - const key = this.getUserKey(chainId, userAddress); - const trackedUser = this.trackedUsers.get(key); + const key = this.getUserKey(chainId, userAddress) + const trackedUser = this.trackedUsers.get(key) if (!trackedUser) { - return; + return } - const normalizedTokenAddresses = - this.normalizeTokenAddresses(tokenAddresses); + const normalizedTokenAddresses = this.normalizeTokenAddresses(tokenAddresses) - const currentTokens = new Set( - trackedUser.tokenAddresses.map((tokenAddress) => - tokenAddress.toLowerCase() - ) - ); - const desiredTokens = new Set(normalizedTokenAddresses); - const tokensToAdd = normalizedTokenAddresses.filter( - (tokenAddress) => !currentTokens.has(tokenAddress) - ); + const currentTokens = new Set(trackedUser.tokenAddresses.map((tokenAddress) => tokenAddress.toLowerCase())) + const desiredTokens = new Set(normalizedTokenAddresses) + const tokensToAdd = normalizedTokenAddresses.filter((tokenAddress) => !currentTokens.has(tokenAddress)) // Set the new list of tracked tokens - trackedUser.tokenAddresses = normalizedTokenAddresses; + trackedUser.tokenAddresses = normalizedTokenAddresses // Delete balances for untracked tokens for (const tokenAddress of trackedUser.lastBalances.keys()) { if (!desiredTokens.has(tokenAddress)) { - logger.info(`Stopping balance tracking for token: ${tokenAddress}`); - trackedUser.lastBalances.delete(tokenAddress); + logger.info(`Stopping balance tracking for token: ${tokenAddress}`) + trackedUser.lastBalances.delete(tokenAddress) } } // Update balances for new tokens if (tokensToAdd.length > 0) { - logger.info(`New tracked token: ${tokensToAdd.join(',')}`); - const balancesForTokens = - await this.tokenBalancesService.getUserTokenBalances({ - chainId, - userAddress, - tokenAddresses: tokensToAdd, - }); + logger.info(`New tracked token: ${tokensToAdd.join(',')}`) + const balancesForTokens = await this.tokenBalancesService.getUserTokenBalances({ + chainId, + userAddress, + tokenAddresses: tokensToAdd, + }) balancesForTokens.forEach((balance) => { - trackedUser.lastBalances.set( - balance.token.address.toLowerCase(), - balance - ); - }); + trackedUser.lastBalances.set(balance.token.address.toLowerCase(), balance) + }) } } async getTrackedUsers(): Promise> { - return Array.from(this.trackedUsers.values()).map( - ({ clientId, chainId, userAddress, tokenAddresses }) => ({ - clientId, - chainId, - userAddress, - tokenAddresses, - }) - ); + return Array.from(this.trackedUsers.values()).map(({ clientId, chainId, userAddress, tokenAddresses }) => ({ + clientId, + chainId, + userAddress, + tokenAddresses, + })) } onBalanceChange(callback: BalanceChangeCallback): void { - this.balanceChangeCallbacks.push(callback); + this.balanceChangeCallbacks.push(callback) } private async checkBalanceChanges( @@ -290,25 +246,24 @@ export class BalanceTrackingServiceMain implements BalanceTrackingService { lastBalances: Map ): Promise { if (tokenAddresses.length === 0) { - return; + return } - const currentBalances = - await this.tokenBalancesService.getUserTokenBalances({ - chainId, - userAddress, - tokenAddresses, - }); + const currentBalances = await this.tokenBalancesService.getUserTokenBalances({ + chainId, + userAddress, + tokenAddresses, + }) for (const currentBalance of currentBalances) { - const tokenAddress = currentBalance.token.address; - const tokenKey = tokenAddress.toLowerCase(); - const lastBalance = lastBalances.get(tokenKey); + const tokenAddress = currentBalance.token.address + const tokenKey = tokenAddress.toLowerCase() + const lastBalance = lastBalances.get(tokenKey) if (!lastBalance) { // New token balance - lastBalances.set(tokenKey, currentBalance); - continue; + lastBalances.set(tokenKey, currentBalance) + continue } // Check for balance changes @@ -321,9 +276,9 @@ export class BalanceTrackingServiceMain implements BalanceTrackingService { oldBalance: lastBalance.balance, newBalance: currentBalance.balance, timestamp: Date.now(), - }; + } - this.emitBalanceChange(event); + this.emitBalanceChange(event) } // Check for allowance changes (if both have allowance data) @@ -336,28 +291,28 @@ export class BalanceTrackingServiceMain implements BalanceTrackingService { oldAllowance: lastBalance.allowance, newAllowance: currentBalance.allowance, timestamp: Date.now(), - }; + } - this.emitBalanceChange(event); + this.emitBalanceChange(event) } // Update the last known balance - lastBalances.set(tokenKey, currentBalance); + lastBalances.set(tokenKey, currentBalance) } } private emitBalanceChange(event: BalanceAllowanceChangeEvent): void { this.balanceChangeCallbacks.forEach((callback) => { try { - callback(event); + callback(event) } catch (error) { - logger.error('Error in balance change callback:', error); + logger.error('Error in balance change callback:', error) } - }); + }) } private getUserKey(chainId: SupportedChainId, userAddress: string): string { - return `${chainId}:${userAddress.toLowerCase()}`; + return `${chainId}:${userAddress.toLowerCase()}` } /** @@ -366,23 +321,18 @@ export class BalanceTrackingServiceMain implements BalanceTrackingService { * @returns */ private normalizeTokenAddresses(tokenAddresses: string[]): string[] { - const unique = new Set(); + const unique = new Set() tokenAddresses.forEach((address) => { - unique.add(address.toLowerCase()); - }); - return Array.from(unique); + unique.add(address.toLowerCase()) + }) + return Array.from(unique) } - private mergeTokenAddresses( - existing: string[], - incoming: string[] - ): string[] { - const merged = new Set( - existing.map((address) => address.toLowerCase()) - ); + private mergeTokenAddresses(existing: string[], incoming: string[]): string[] { + const merged = new Set(existing.map((address) => address.toLowerCase())) incoming.forEach((address) => { - merged.add(address.toLowerCase()); - }); - return Array.from(merged); + merged.add(address.toLowerCase()) + }) + return Array.from(merged) } } diff --git a/libs/services/src/HooksService/HooksService.ts b/libs/services/src/HooksService/HooksService.ts index 618f001e..f3ac66f0 100644 --- a/libs/services/src/HooksService/HooksService.ts +++ b/libs/services/src/HooksService/HooksService.ts @@ -1,55 +1,40 @@ -export const hooksServiceSymbol = Symbol.for('HooksService'); +export const hooksServiceSymbol = Symbol.for('HooksService') // Single source of truth - define the values once -const BLOCKCHAIN_VALUES = [ - 'mainnet', - 'arbitrum', - 'avalanche', - 'base', - 'gnosis', - 'polygon', -] as const; -const PERIOD_VALUES = [ - 'last 3h', - 'last 1d', - 'last 7d', - 'last 30d', - 'last 3m', - 'last 6m', - 'last 12m', -] as const; +const BLOCKCHAIN_VALUES = ['mainnet', 'arbitrum', 'avalanche', 'base', 'gnosis', 'polygon'] as const +const PERIOD_VALUES = ['last 3h', 'last 1d', 'last 7d', 'last 30d', 'last 3m', 'last 6m', 'last 12m'] as const // Derive types from the arrays -export type Blockchain = (typeof BLOCKCHAIN_VALUES)[number]; -export type Period = (typeof PERIOD_VALUES)[number]; +export type Blockchain = (typeof BLOCKCHAIN_VALUES)[number] +export type Period = (typeof PERIOD_VALUES)[number] // Export the arrays for runtime use -export { BLOCKCHAIN_VALUES, PERIOD_VALUES }; +export { BLOCKCHAIN_VALUES, PERIOD_VALUES } export interface HookData { - environment: string; - block_time: string; - is_bridging: boolean; - success: boolean; - app_code: string; - destination_chain_id: number | null; - destination_token_address: string | null; - hook_type: string; - app_id: string | null; - target: string; - gas_limit: number; - app_hash: string; - tx_hash: string; + environment: string + block_time: string + is_bridging: boolean + success: boolean + app_code: string + destination_chain_id: number | null + destination_token_address: string | null + hook_type: string + app_id: string | null + target: string + gas_limit: number + app_hash: string + tx_hash: string } export interface GetHooksParams { - blockchain: Blockchain; - period: Period; - maxWaitTimeMs?: number; - limit?: number; - offset?: number; + blockchain: Blockchain + period: Period + maxWaitTimeMs?: number + limit?: number + offset?: number } export interface HooksService { - getHooks(params: GetHooksParams): Promise; + getHooks(params: GetHooksParams): Promise } diff --git a/libs/services/src/HooksService/HooksServiceImpl.spec.ts b/libs/services/src/HooksService/HooksServiceImpl.spec.ts index 8777321d..38a48fa6 100644 --- a/libs/services/src/HooksService/HooksServiceImpl.spec.ts +++ b/libs/services/src/HooksService/HooksServiceImpl.spec.ts @@ -4,64 +4,62 @@ import { DuneResultResponse, UploadCsvParams, UploadCsvResponse, -} from '@cowprotocol/repositories'; -import { HookData, Blockchain, Period } from './HooksService'; -import { HooksServiceImpl } from './HooksServiceImpl'; +} from '@cowprotocol/repositories' +import { HookData, Blockchain, Period } from './HooksService' +import { HooksServiceImpl } from './HooksServiceImpl' // Mock DuneRepository for testing class MockDuneRepository implements DuneRepository { - private mockExecutionId = 'test-execution-123'; - private mockResult: DuneResultResponse; + private mockExecutionId = 'test-execution-123' + private mockResult: DuneResultResponse constructor(mockResult?: DuneResultResponse) { - this.mockResult = mockResult || this.getDefaultMockResult(); + this.mockResult = mockResult || this.getDefaultMockResult() } async executeQuery(): Promise { return { execution_id: this.mockExecutionId, state: 'QUERY_STATE_PENDING', - }; + } } async getExecutionResults(): Promise> { - return this.mockResult as DuneResultResponse; + return this.mockResult as DuneResultResponse } async getQueryResults(): Promise> { - return this.mockResult as DuneResultResponse; + return this.mockResult as DuneResultResponse } async waitForExecution(params: { - executionId: string; - maxWaitTimeMs?: number; - typeAssertion?: (data: unknown) => data is T; + executionId: string + maxWaitTimeMs?: number + typeAssertion?: (data: unknown) => data is T }): Promise> { - const { typeAssertion } = params; + const { typeAssertion } = params // Simulate type validation if provided if (typeAssertion && this.mockResult.result.rows.length > 0) { - const invalidRows: Array<{ index: number; data: unknown }> = []; + const invalidRows: Array<{ index: number; data: unknown }> = [] const isValid = this.mockResult.result.rows.every((row, index) => { if (!typeAssertion(row)) { - invalidRows.push({ index, data: row }); - return false; + invalidRows.push({ index, data: row }) + return false } - return true; - }); + return true + }) if (!isValid) { - throw new Error( - `Data validation failed for execution ${params.executionId}` - ); + throw new Error(`Data validation failed for execution ${params.executionId}`) } } - return this.mockResult as DuneResultResponse; + return this.mockResult as DuneResultResponse } async uploadCsv(_params: UploadCsvParams): Promise { - return { success: true }; + return { success: true } } private getDefaultMockResult(): DuneResultResponse { @@ -88,10 +86,8 @@ class MockDuneRepository implements DuneRepository { app_id: null, target: '0x1234567890123456789012345678901234567890', gas_limit: 250000, - app_hash: - '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', - tx_hash: - '0x9876543210987654321098765432109876543210987654321098765432109876', + app_hash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + tx_hash: '0x9876543210987654321098765432109876543210987654321098765432109876', }, { environment: 'prod', @@ -100,16 +96,13 @@ class MockDuneRepository implements DuneRepository { success: true, app_code: 'https://bridge.example.com/', destination_chain_id: 137, - destination_token_address: - '0x1234567890123456789012345678901234567890', + destination_token_address: '0x1234567890123456789012345678901234567890', hook_type: 'pre', app_id: 'bridge-app', target: '0xabcdef1234567890abcdef1234567890abcdef1234', gas_limit: 300000, - app_hash: - '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', - tx_hash: - '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + app_hash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + tx_hash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', }, ], metadata: { @@ -152,31 +145,31 @@ class MockDuneRepository implements DuneRepository { execution_time_millis: 500, }, }, - }; + } } setMockResult(result: DuneResultResponse) { - this.mockResult = result; + this.mockResult = result } } describe('HooksService', () => { - let mockRepository: MockDuneRepository; - let hooksService: HooksServiceImpl; + let mockRepository: MockDuneRepository + let hooksService: HooksServiceImpl beforeEach(() => { - mockRepository = new MockDuneRepository(); - hooksService = new HooksServiceImpl(mockRepository); - }); + mockRepository = new MockDuneRepository() + hooksService = new HooksServiceImpl(mockRepository) + }) describe('getHooks', () => { it('should return valid hook data when Dune query succeeds', async () => { const hooks = await hooksService.getHooks({ blockchain: 'mainnet', period: 'last 7d', - }); + }) - expect(hooks).toHaveLength(2); + expect(hooks).toHaveLength(2) expect(hooks[0]).toEqual({ environment: 'prod', block_time: '2025-01-01 12:00:00.000 UTC', @@ -189,11 +182,9 @@ describe('HooksService', () => { app_id: null, target: '0x1234567890123456789012345678901234567890', gas_limit: 250000, - app_hash: - '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', - tx_hash: - '0x9876543210987654321098765432109876543210987654321098765432109876', - }); + app_hash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + tx_hash: '0x9876543210987654321098765432109876543210987654321098765432109876', + }) expect(hooks[1]).toEqual({ environment: 'prod', block_time: '2025-01-01 12:01:00.000 UTC', @@ -206,24 +197,19 @@ describe('HooksService', () => { app_id: 'bridge-app', target: '0xabcdef1234567890abcdef1234567890abcdef1234', gas_limit: 300000, - app_hash: - '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', - tx_hash: - '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', - }); - }); + app_hash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + tx_hash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + }) + }) it('should pass correct parameters to Dune repository', async () => { - const executeQuerySpy = jest.spyOn(mockRepository, 'executeQuery'); - const waitForExecutionSpy = jest.spyOn( - mockRepository, - 'waitForExecution' - ); + const executeQuerySpy = jest.spyOn(mockRepository, 'executeQuery') + const waitForExecutionSpy = jest.spyOn(mockRepository, 'waitForExecution') await hooksService.getHooks({ blockchain: 'arbitrum', period: 'last 1d', - }); + }) expect(executeQuerySpy).toHaveBeenCalledWith({ queryId: 5302473, @@ -231,21 +217,21 @@ describe('HooksService', () => { blockchain: 'arbitrum', period: 'last 1d', }, - }); + }) expect(waitForExecutionSpy).toHaveBeenCalledWith({ executionId: 'test-execution-123', typeAssertion: expect.any(Function), - }); - }); + }) + }) it('should handle different blockchain and period combinations', async () => { - const executeQuerySpy = jest.spyOn(mockRepository, 'executeQuery'); + const executeQuerySpy = jest.spyOn(mockRepository, 'executeQuery') await hooksService.getHooks({ blockchain: 'polygon', period: 'last 30d', - }); + }) expect(executeQuerySpy).toHaveBeenCalledWith({ queryId: 5302473, @@ -253,22 +239,20 @@ describe('HooksService', () => { blockchain: 'polygon', period: 'last 30d', }, - }); - }); + }) + }) it('should throw error when Dune repository throws', async () => { // Mock repository to throw an error - jest - .spyOn(mockRepository, 'executeQuery') - .mockRejectedValue(new Error('Dune API error')); + jest.spyOn(mockRepository, 'executeQuery').mockRejectedValue(new Error('Dune API error')) await expect( hooksService.getHooks({ blockchain: 'mainnet', period: 'last 7d', }) - ).rejects.toThrow('Dune API error'); - }); + ).rejects.toThrow('Dune API error') + }) it('should throw error when data validation fails', async () => { // Create invalid data that doesn't match HookData interface @@ -294,20 +278,8 @@ describe('HooksService', () => { }, ], metadata: { - column_names: [ - 'environment', - 'block_time', - 'is_bridging', - 'success', - 'app_code', - ], - column_types: [ - 'varchar', - 'timestamp', - 'boolean', - 'boolean', - 'varchar', - ], + column_names: ['environment', 'block_time', 'is_bridging', 'success', 'app_code'], + column_types: ['varchar', 'timestamp', 'boolean', 'boolean', 'varchar'], row_count: 1, result_set_bytes: 100, total_row_count: 1, @@ -317,21 +289,17 @@ describe('HooksService', () => { execution_time_millis: 500, }, }, - }; + } - mockRepository.setMockResult( - invalidResult as DuneResultResponse - ); + mockRepository.setMockResult(invalidResult as DuneResultResponse) await expect( hooksService.getHooks({ blockchain: 'mainnet', period: 'last 7d', }) - ).rejects.toThrow( - 'Data validation failed for execution test-execution-123' - ); - }); + ).rejects.toThrow('Data validation failed for execution test-execution-123') + }) it('should handle empty result set', async () => { const emptyResult: DuneResultResponse = { @@ -385,16 +353,16 @@ describe('HooksService', () => { execution_time_millis: 500, }, }, - }; + } - mockRepository.setMockResult(emptyResult); + mockRepository.setMockResult(emptyResult) const hooks = await hooksService.getHooks({ blockchain: 'mainnet', period: 'last 7d', - }); - expect(hooks).toHaveLength(0); - }); + }) + expect(hooks).toHaveLength(0) + }) it('should work with all supported blockchain and period combinations', async () => { const testCases: Array<{ blockchain: Blockchain; period: Period }> = [ @@ -404,12 +372,12 @@ describe('HooksService', () => { { blockchain: 'base', period: 'last 30d' }, { blockchain: 'gnosis', period: 'last 3m' }, { blockchain: 'polygon', period: 'last 6m' }, - ]; + ] for (const { blockchain, period } of testCases) { - const executeQuerySpy = jest.spyOn(mockRepository, 'executeQuery'); + const executeQuerySpy = jest.spyOn(mockRepository, 'executeQuery') - await hooksService.getHooks({ blockchain, period }); + await hooksService.getHooks({ blockchain, period }) expect(executeQuerySpy).toHaveBeenCalledWith({ queryId: 5302473, @@ -417,10 +385,10 @@ describe('HooksService', () => { blockchain, period, }, - }); + }) - executeQuerySpy.mockClear(); + executeQuerySpy.mockClear() } - }); - }); -}); + }) + }) +}) diff --git a/libs/services/src/HooksService/HooksServiceImpl.ts b/libs/services/src/HooksService/HooksServiceImpl.ts index 659a6e87..fa31f7bd 100644 --- a/libs/services/src/HooksService/HooksServiceImpl.ts +++ b/libs/services/src/HooksService/HooksServiceImpl.ts @@ -1,18 +1,18 @@ -import { DuneRepository } from '@cowprotocol/repositories'; -import { GetHooksParams, HookData, HooksService } from './HooksService'; -import { isHookData } from './utils/isHookData'; +import { DuneRepository } from '@cowprotocol/repositories' +import { GetHooksParams, HookData, HooksService } from './HooksService' +import { isHookData } from './utils/isHookData' -const DEFAULT_QUERY_ID = 5302473; // Example on executing a query +const DEFAULT_QUERY_ID = 5302473 // Example on executing a query export class HooksServiceImpl implements HooksService { - private readonly duneRepository: DuneRepository; + private readonly duneRepository: DuneRepository constructor(duneRepository: DuneRepository) { - this.duneRepository = duneRepository; + this.duneRepository = duneRepository } async getHooks(params: GetHooksParams): Promise { - const { blockchain, period, maxWaitTimeMs, limit, offset } = params; + const { blockchain, period, maxWaitTimeMs, limit, offset } = params // Execute the query with parameters const execution = await this.duneRepository.executeQuery({ @@ -21,22 +21,22 @@ export class HooksServiceImpl implements HooksService { blockchain, period, }, - }); + }) // Wait for execution to complete with type assertion await this.duneRepository.waitForExecution({ executionId: execution.execution_id, typeAssertion: isHookData, maxWaitTimeMs, - }); + }) const result = await this.duneRepository.getQueryResults({ queryId: DEFAULT_QUERY_ID, limit, offset, typeAssertion: isHookData, - }); + }) - return result.result.rows; + return result.result.rows } } diff --git a/libs/services/src/HooksService/utils/isHookData.ts b/libs/services/src/HooksService/utils/isHookData.ts index 61447056..bf3dcec0 100644 --- a/libs/services/src/HooksService/utils/isHookData.ts +++ b/libs/services/src/HooksService/utils/isHookData.ts @@ -1,58 +1,44 @@ -import { HookData } from '../HooksService'; +import { HookData } from '../HooksService' // Check required string fields -const requiredStringFields = [ - 'environment', - 'block_time', - 'app_code', - 'hook_type', - 'target', - 'app_hash', - 'tx_hash', -]; +const requiredStringFields = ['environment', 'block_time', 'app_code', 'hook_type', 'target', 'app_hash', 'tx_hash'] export function isHookData(data: unknown): data is HookData { // Check if data is an object if (typeof data !== 'object' || data === null) { - return false; + return false } for (const field of requiredStringFields) { if (typeof (data as Record)[field] !== 'string') { - return false; + return false } } // Check required boolean fields - const requiredBooleanFields = ['is_bridging', 'success']; + const requiredBooleanFields = ['is_bridging', 'success'] for (const field of requiredBooleanFields) { if (typeof (data as Record)[field] !== 'boolean') { - return false; + return false } } // Check required number field if (typeof (data as Record).gas_limit !== 'number') { - return false; + return false } // Check nullable fields - const dataRecord = data as Record; - if ( - dataRecord.destination_chain_id !== null && - typeof dataRecord.destination_chain_id !== 'number' - ) { - return false; + const dataRecord = data as Record + if (dataRecord.destination_chain_id !== null && typeof dataRecord.destination_chain_id !== 'number') { + return false } - if ( - dataRecord.destination_token_address !== null && - typeof dataRecord.destination_token_address !== 'string' - ) { - return false; + if (dataRecord.destination_token_address !== null && typeof dataRecord.destination_token_address !== 'string') { + return false } if (dataRecord.app_id !== null && typeof dataRecord.app_id !== 'string') { - return false; + return false } - return true; + return true } diff --git a/libs/services/src/SSEService/SSEService.ts b/libs/services/src/SSEService/SSEService.ts index 7c7e4388..1bd53973 100644 --- a/libs/services/src/SSEService/SSEService.ts +++ b/libs/services/src/SSEService/SSEService.ts @@ -1,19 +1,19 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { UserTokenBalanceWithToken } from '../TokenBalancesService/TokenBalancesService'; -import { BalanceAllowanceChangeEvent } from '../BalanceTrackingService/BalanceTrackingService'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { UserTokenBalanceWithToken } from '../TokenBalancesService/TokenBalancesService' +import { BalanceAllowanceChangeEvent } from '../BalanceTrackingService/BalanceTrackingService' -export const sseServiceSymbol = Symbol.for('SSEService'); +export const sseServiceSymbol = Symbol.for('SSEService') /** * Models a client subscription to some events, like balance and allowance changes */ export interface SSEClient { - clientId: string; - chainId: SupportedChainId; - userAddress: string; - tokenAddresses: string[]; // TODO: For now, I will let the client specify the list of addresses to track, but as most users use the default list, we could let them specify the lists and the additional tokens on top - send: (data: string) => void; - close: () => void; + clientId: string + chainId: SupportedChainId + userAddress: string + tokenAddresses: string[] // TODO: For now, I will let the client specify the list of addresses to track, but as most users use the default list, we could let them specify the lists and the additional tokens on top + send: (data: string) => void + close: () => void } /** @@ -22,25 +22,22 @@ export interface SSEClient { * Initially focused on balance and allowance tracking, but could be extended to other events (experimental, hackathon mode!) */ export interface SSEService { - addClient(client: SSEClient): void; - removeClient(clientId: string): void; + addClient(client: SSEClient): void + removeClient(clientId: string): void /** * Send data to a specific client * @param clientId * @param data */ - sendToClient(clientId: string, data: string): boolean; + sendToClient(clientId: string, data: string): boolean /** * Return the SSE client subscriptions for a given user account and chain * @param chainId * @param userAddress */ - getClientsForUser( - chainId: SupportedChainId, - userAddress: string - ): SSEClient[]; + getClientsForUser(chainId: SupportedChainId, userAddress: string): SSEClient[] /** * Broadcast an event to users @@ -52,21 +49,18 @@ export interface SSEService { chainId: SupportedChainId, userAddress: string, data: string // TODO: For now this kept agnostic to what is being sent, so other services can push any data. I also added "broadcastBalanceUpdate" which are specific, we might want to delete either the more generic or the most specific (so its done in another service). For now, just experimenting - ): void; + ): void /** * Broadcast user's initial balances and allowances * @param clientId * @param balances */ - broadcastInitialBalances( - clientId: string, - balances: UserTokenBalanceWithToken[] - ): void; + broadcastInitialBalances(clientId: string, balances: UserTokenBalanceWithToken[]): void /** * Push a balance update event to the clients * @param event */ - broadcastBalanceUpdate(event: BalanceAllowanceChangeEvent): void; + broadcastBalanceUpdate(event: BalanceAllowanceChangeEvent): void } diff --git a/libs/services/src/SSEService/SSEServiceMain.ts b/libs/services/src/SSEService/SSEServiceMain.ts index 8185904a..84c44dd9 100644 --- a/libs/services/src/SSEService/SSEServiceMain.ts +++ b/libs/services/src/SSEService/SSEServiceMain.ts @@ -1,96 +1,79 @@ -import { injectable } from 'inversify'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { logger } from '@cowprotocol/shared'; -import { UserTokenBalanceWithToken } from '../TokenBalancesService/TokenBalancesService'; -import { BalanceAllowanceChangeEvent } from '../BalanceTrackingService/BalanceTrackingService'; -import { SSEService, SSEClient } from './SSEService'; +import { injectable } from 'inversify' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { logger } from '@cowprotocol/shared' +import { UserTokenBalanceWithToken } from '../TokenBalancesService/TokenBalancesService' +import { BalanceAllowanceChangeEvent } from '../BalanceTrackingService/BalanceTrackingService' +import { SSEService, SSEClient } from './SSEService' @injectable() export class SSEServiceMain implements SSEService { - private clients = new Map(); + private clients = new Map() addClient(client: SSEClient): void { - this.clients.set(client.clientId, client); - logger.info( - `SSE client ${client.clientId} connected for user ${client.userAddress} on chain ${client.chainId}` - ); + this.clients.set(client.clientId, client) + logger.info(`SSE client ${client.clientId} connected for user ${client.userAddress} on chain ${client.chainId}`) } removeClient(clientId: string): void { - const client = this.clients.get(clientId); + const client = this.clients.get(clientId) if (client) { - client.close(); - this.clients.delete(clientId); - logger.info(`SSE client ${clientId} disconnected`); + client.close() + this.clients.delete(clientId) + logger.info(`SSE client ${clientId} disconnected`) } } - getClientsForUser( - chainId: SupportedChainId, - userAddress: string - ): SSEClient[] { - const userAddressLowerCase = userAddress.toLowerCase(); + getClientsForUser(chainId: SupportedChainId, userAddress: string): SSEClient[] { + const userAddressLowerCase = userAddress.toLowerCase() return Array.from(this.clients.values()).filter( - (client) => - client.chainId === chainId && - client.userAddress.toLowerCase() === userAddressLowerCase - ); + (client) => client.chainId === chainId && client.userAddress.toLowerCase() === userAddressLowerCase + ) } sendToClient(clientId: string, data: string): boolean { - const client = this.clients.get(clientId); + const client = this.clients.get(clientId) if (client) { try { - client.send(data); - return true; + client.send(data) + return true } catch (error) { - logger.error( - `Error sending data to SSE client ${client.clientId}:`, - error - ); - this.removeClient(client.clientId); - return false; + logger.error(`Error sending data to SSE client ${client.clientId}:`, error) + this.removeClient(client.clientId) + return false } } - return false; + return false } - broadcastToUser( - chainId: SupportedChainId, - userAddress: string, - data: string - ): void { - const clients = this.getClientsForUser(chainId, userAddress); + broadcastToUser(chainId: SupportedChainId, userAddress: string, data: string): void { + const clients = this.getClientsForUser(chainId, userAddress) clients.forEach((client) => { - this.sendToClient(client.clientId, data); - }); + this.sendToClient(client.clientId, data) + }) } broadcastBalanceUpdate(event: BalanceAllowanceChangeEvent): void { - const clients = this.getClientsForUser(event.chainId, event.userAddress); - const tokenAddress = event.tokenAddress.toLowerCase(); + const clients = this.getClientsForUser(event.chainId, event.userAddress) + const tokenAddress = event.tokenAddress.toLowerCase() // Send the data to the clients - const data = this.formatSSEData('balance_update', event); + const data = this.formatSSEData('balance_update', event) clients.forEach((client) => { // Make sure the client is interested in this specific token if (this.clientTracksToken(client, tokenAddress)) { - this.sendToClient(client.clientId, data); + this.sendToClient(client.clientId, data) } - }); + }) } - broadcastInitialBalances( - clientId: string, - balances: UserTokenBalanceWithToken[] - ): void { - const data = this.formatSSEData('initial_balances', { balances }); - this.sendToClient(clientId, data); + broadcastInitialBalances(clientId: string, balances: UserTokenBalanceWithToken[]): void { + const data = this.formatSSEData('initial_balances', { balances }) + this.sendToClient(clientId, data) } private formatSSEData(eventType: string, data: unknown): string { - return `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`; + return `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n` } /** @@ -100,20 +83,15 @@ export class SSEServiceMain implements SSEService { * @param tokenAddressLowerCase expected to be in lowercase * @returns true if the client is interested in the given token, false otherwise */ - private clientTracksToken( - client: SSEClient, - tokenAddressLowerCase: string - ): boolean { - return client.tokenAddresses.some( - (address) => address.toLowerCase() === tokenAddressLowerCase - ); + private clientTracksToken(client: SSEClient, tokenAddressLowerCase: string): boolean { + return client.tokenAddresses.some((address) => address.toLowerCase() === tokenAddressLowerCase) } // Cleanup method to remove all clients (useful for graceful shutdown) cleanup(): void { - const clientIds = Array.from(this.clients.keys()); + const clientIds = Array.from(this.clients.keys()) clientIds.forEach((id) => { - this.removeClient(id); - }); + this.removeClient(id) + }) } } diff --git a/libs/services/src/SimulationService/SimulationService.ts b/libs/services/src/SimulationService/SimulationService.ts index d6668e73..4e46a5c7 100644 --- a/libs/services/src/SimulationService/SimulationService.ts +++ b/libs/services/src/SimulationService/SimulationService.ts @@ -3,11 +3,11 @@ import { SimulationRepository, SimulationInput, SimulationData, -} from '@cowprotocol/repositories'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { injectable, inject } from 'inversify'; +} from '@cowprotocol/repositories' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { injectable, inject } from 'inversify' -export const simulationServiceSymbol = Symbol.for('SimulationServiceSymbol'); +export const simulationServiceSymbol = Symbol.for('SimulationServiceSymbol') @injectable() export class SimulationService { @@ -20,9 +20,6 @@ export class SimulationService { chainId: SupportedChainId, simulationInput: SimulationInput[] ): Promise { - return this.simulationRepository.postBundleSimulation( - chainId, - simulationInput - ); + return this.simulationRepository.postBundleSimulation(chainId, simulationInput) } } diff --git a/libs/services/src/SlippageService/SlippageService.ts b/libs/services/src/SlippageService/SlippageService.ts index e3699261..023a19da 100644 --- a/libs/services/src/SlippageService/SlippageService.ts +++ b/libs/services/src/SlippageService/SlippageService.ts @@ -1,56 +1,49 @@ -import { PricePoint } from '@cowprotocol/repositories'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { PricePoint } from '@cowprotocol/repositories' +import { SupportedChainId } from '@cowprotocol/cow-sdk' /** * BPS (Basis Points) */ -export type Bps = number; +export type Bps = number export interface GetSlippageBpsParams { - chainId: SupportedChainId; - quoteTokenAddress: string; - baseTokenAddress: string; - order?: OrderForSlippageCalculation; + chainId: SupportedChainId + quoteTokenAddress: string + baseTokenAddress: string + order?: OrderForSlippageCalculation } -export type OrderKind = 'buy' | 'sell'; +export type OrderKind = 'buy' | 'sell' export interface OrderForSlippageCalculation { - orderKind: OrderKind; - partiallyFillable: boolean; - sellAmount: string; - buyAmount: string; - expirationTimeInSeconds: number; + orderKind: OrderKind + partiallyFillable: boolean + sellAmount: string + buyAmount: string + expirationTimeInSeconds: number } export interface VolatilityDetails { - tokenAddress: string; - usdPrice: number; - prices: PricePoint[] | null; - volatilityInUsd: number; - volatilityInTokens: number; + tokenAddress: string + usdPrice: number + prices: PricePoint[] | null + volatilityInUsd: number + volatilityInTokens: number } -export interface PairVolatility - extends Omit< - VolatilityDetails, - 'tokenAddress' | 'usdPrice' | 'volatilityInUsd' - > { - baseTokenAddress: string; - quoteTokenAddress: string; +export interface PairVolatility extends Omit { + baseTokenAddress: string + quoteTokenAddress: string } export interface SlippageService { - getSlippageBps(params: GetSlippageBpsParams): Promise; - getVolatilityDetails( - chainId: SupportedChainId, - tokenAddress: string - ): Promise; + getSlippageBps(params: GetSlippageBpsParams): Promise + getVolatilityDetails(chainId: SupportedChainId, tokenAddress: string): Promise getVolatilityForPair( chainId: SupportedChainId, baseTokenAddress: string, quoteTokenAddress: string - ): Promise; + ): Promise } -export const slippageServiceSymbol = Symbol.for('SlippageService'); +export const slippageServiceSymbol = Symbol.for('SlippageService') diff --git a/libs/services/src/SlippageService/SlippageServiceMain.spec.ts b/libs/services/src/SlippageService/SlippageServiceMain.spec.ts index bcba1561..c44ada17 100644 --- a/libs/services/src/SlippageService/SlippageServiceMain.spec.ts +++ b/libs/services/src/SlippageService/SlippageServiceMain.spec.ts @@ -1,151 +1,139 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { PricePoint, UsdRepository } from '@cowprotocol/repositories'; -import ms from 'ms'; -import { SlippageServiceMain } from './SlippageServiceMain'; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { PricePoint, UsdRepository } from '@cowprotocol/repositories' +import ms from 'ms' +import { SlippageServiceMain } from './SlippageServiceMain' -const FIVE_MIN = ms('5min'); -const FOUR_MIN = ms('4min'); -const SIX_MIN = ms('6min'); +const FIVE_MIN = ms('5min') +const FOUR_MIN = ms('4min') +const SIX_MIN = ms('6min') -const getUsdPrice = jest.fn(); -const getUsdPrices = jest.fn(); +const getUsdPrice = jest.fn() +const getUsdPrices = jest.fn() -const POINTS_VOLATILITY_ZERO = getPoints([1, 1, 1, 1]); -const POINTS_WITH_HIGH_VOLATILITY = getPoints([100, 110, 120, 130]); // 10% each 5min -const POINTS_WITH_LOW_VOLATILITY = getPoints([ - 100.0001, 100.0002, 100.0003, 100.0004, -]); // 0.0001% each 5min +const POINTS_VOLATILITY_ZERO = getPoints([1, 1, 1, 1]) +const POINTS_WITH_HIGH_VOLATILITY = getPoints([100, 110, 120, 130]) // 10% each 5min +const POINTS_WITH_LOW_VOLATILITY = getPoints([100.0001, 100.0002, 100.0003, 100.0004]) // 0.0001% each 5min /** * Test specification for the SlippageService main implementation */ describe('SlippageServiceMain Specification', () => { - let slippageService: SlippageServiceMain; - let usdRepositoryMock: UsdRepository; + let slippageService: SlippageServiceMain + let usdRepositoryMock: UsdRepository - const chainId = SupportedChainId.MAINNET; - const baseTokenAddress = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; - const quoteTokenAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; + const chainId = SupportedChainId.MAINNET + const baseTokenAddress = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' + const quoteTokenAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' beforeEach(() => { usdRepositoryMock = { name: 'Mock', getUsdPrice, getUsdPrices, - }; + } - slippageService = new SlippageServiceMain(usdRepositoryMock); + slippageService = new SlippageServiceMain(usdRepositoryMock) - getUsdPrice.mockImplementation(getUsdPriceMockFn(baseTokenAddress, 1, 10)); - }); + getUsdPrice.mockImplementation(getUsdPriceMockFn(baseTokenAddress, 1, 10)) + }) describe('should return the 0 slippage if', () => { it('prices are not available', async () => { // GIVEN: No prices available - getUsdPrices.mockResolvedValue(null); + getUsdPrices.mockResolvedValue(null) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the max slippage - expect(result).toBe(0); - }); + expect(result).toBe(0) + }) it('no price points are available', async () => { // GIVEN: No prices available - getUsdPrices.mockResolvedValue([]); + getUsdPrices.mockResolvedValue([]) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the max slippage - expect(result).toBe(0); - }); + expect(result).toBe(0) + }) it(`one of the tokens is volatile`, async () => { getUsdPrices.mockImplementation( - getUsdPricesMockFn( - baseTokenAddress, - POINTS_VOLATILITY_ZERO, - POINTS_WITH_HIGH_VOLATILITY - ) - ); + getUsdPricesMockFn(baseTokenAddress, POINTS_VOLATILITY_ZERO, POINTS_WITH_HIGH_VOLATILITY) + ) // WHEN: Get slippage let result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the maximum slippage - expect(result).toBe(87); + expect(result).toBe(87) // WHEN: Get slippage (with the tokens inverted) result = await slippageService.getSlippageBps({ chainId, quoteTokenAddress: baseTokenAddress, baseTokenAddress: quoteTokenAddress, - }); + }) // THEN: We get the maximum slippage too - expect(result).toBe(11181); - }); + expect(result).toBe(11181) + }) it(`one of the tokens has no prices available`, async () => { - getUsdPrices.mockImplementation( - getUsdPricesMockFn(baseTokenAddress, POINTS_VOLATILITY_ZERO, null) - ); + getUsdPrices.mockImplementation(getUsdPricesMockFn(baseTokenAddress, POINTS_VOLATILITY_ZERO, null)) // WHEN: Get slippage let result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the maximum slippage - expect(result).toBe(0); + expect(result).toBe(0) // WHEN: Get slippage (with the tokens inverted) result = await slippageService.getSlippageBps({ chainId, quoteTokenAddress: baseTokenAddress, baseTokenAddress: quoteTokenAddress, - }); + }) // THEN: We get the maximum slippage too - expect(result).toBe(0); - }); + expect(result).toBe(0) + }) it(`if the prices change a lot`, async () => { // GIVEN: The prices have high volatility getUsdPrices.mockImplementation( - getUsdPricesMockFn( - baseTokenAddress, - POINTS_WITH_HIGH_VOLATILITY, - POINTS_VOLATILITY_ZERO - ) - ); + getUsdPricesMockFn(baseTokenAddress, POINTS_WITH_HIGH_VOLATILITY, POINTS_VOLATILITY_ZERO) + ) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the maximum slippage - expect(result).toBe(1118034); - }); + expect(result).toBe(1118034) + }) it(`if there are no data points matching the date`, async () => { // GIVEN: No data points matching the date @@ -155,35 +143,31 @@ describe('SlippageServiceMain Specification', () => { POINTS_VOLATILITY_ZERO, getPoints([1, 2, 2, 3], undefined, Date.now() - ms('1h')) ) - ); + ) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the minimum slippage - expect(result).toBe(708); - }); + expect(result).toBe(708) + }) it(`if the asset is volatile`, async () => { // GIVEN: The prices have high volatility getUsdPrices.mockImplementation( - getUsdPricesMockFn( - baseTokenAddress, - getPoints([100, 100.02, 100.04, 100.06]), - POINTS_VOLATILITY_ZERO - ) - ); + getUsdPricesMockFn(baseTokenAddress, getPoints([100, 100.02, 100.04, 100.06]), POINTS_VOLATILITY_ZERO) + ) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the maximum slippage // AVG = (100 + 100.02 + 100.04 + 100.06)/4 = 100.03 @@ -194,69 +178,59 @@ describe('SlippageServiceMain Specification', () => { // Volatility Fair Settlement (Token) = 0.02236067977 / 1 = 0.02236067977 // Slippage BPS = ceil(0.02236067977 * 10000) = 224 // Adjusted Slippage = MAX = 200 - expect(result).toBe(2237); - }); - }); + expect(result).toBe(2237) + }) + }) describe('should return the minimum slippage if', () => { it(`if the prices don't change`, async () => { // GIVEN: The prices don't change at all - getUsdPrices.mockResolvedValue(POINTS_VOLATILITY_ZERO); + getUsdPrices.mockResolvedValue(POINTS_VOLATILITY_ZERO) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the minimum slippage - expect(result).toBe(0); - }); + expect(result).toBe(0) + }) it(`if the prices change very little`, async () => { // GIVEN: The prices don't change much getUsdPrices.mockImplementation( - getUsdPricesMockFn( - baseTokenAddress, - POINTS_WITH_LOW_VOLATILITY, - POINTS_VOLATILITY_ZERO - ) - ); + getUsdPricesMockFn(baseTokenAddress, POINTS_WITH_LOW_VOLATILITY, POINTS_VOLATILITY_ZERO) + ) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the minimum slippage - expect(result).toBe(12); - }); - }); + expect(result).toBe(12) + }) + }) describe('should return the estimated slippage', () => { it(`for normal volatility`, async () => { // GIVEN: The prices have high volatility getUsdPrices.mockImplementation( - getUsdPricesMockFn( - baseTokenAddress, - getPoints([100, 100.01, 100.02, 100.03]), - POINTS_VOLATILITY_ZERO - ) - ); + getUsdPricesMockFn(baseTokenAddress, getPoints([100, 100.01, 100.02, 100.03]), POINTS_VOLATILITY_ZERO) + ) - getUsdPrice.mockImplementation( - getUsdPriceMockFn(baseTokenAddress, 100, 1) - ); + getUsdPrice.mockImplementation(getUsdPriceMockFn(baseTokenAddress, 100, 1)) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the the calculated slippage // Relative prices = [100/1, 100.01/1, 100.02/1, 100.03/1] @@ -269,28 +243,22 @@ describe('SlippageServiceMain Specification', () => { // Volatility Fair Settlement (Token) = 0.01118033989 / 100 = 0.1118033989 // Slippage BPS = ceil(0.1118033989 * 10000) = 112 // Adjusted Slippage = 112 - expect(result).toBe(2); - }); + expect(result).toBe(2) + }) it(`if token is worth more in USD, the slippage is smaller`, async () => { // GIVEN: The prices have high volatility getUsdPrices.mockImplementation( - getUsdPricesMockFn( - baseTokenAddress, - getPoints([100, 100.01, 100.02, 100.03]), - POINTS_VOLATILITY_ZERO - ) - ); - getUsdPrice.mockImplementation( - getUsdPriceMockFn(baseTokenAddress, 1.1, 100) - ); + getUsdPricesMockFn(baseTokenAddress, getPoints([100, 100.01, 100.02, 100.03]), POINTS_VOLATILITY_ZERO) + ) + getUsdPrice.mockImplementation(getUsdPriceMockFn(baseTokenAddress, 1.1, 100)) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the the calculated slippage // AVG = (100 + 100.01 + 100.02 + 100.03)/4 = 100.015 @@ -301,28 +269,22 @@ describe('SlippageServiceMain Specification', () => { // Volatility Fair Settlement (Token) = 0.01118033989 / 1.1 = 0.01016394535 // Slippage BPS = ceil(0.01016394535 * 10000) = 102 // Adjusted Slippage = 102 - expect(result).toBe(10164); - }); + expect(result).toBe(10164) + }) it(`if token is worth less in USD, the slippage is bigger`, async () => { // GIVEN: The prices have high volatility getUsdPrices.mockImplementation( - getUsdPricesMockFn( - baseTokenAddress, - getPoints([100, 100.01, 100.02, 100.03]), - POINTS_VOLATILITY_ZERO - ) - ); - getUsdPrice.mockImplementation( - getUsdPriceMockFn(baseTokenAddress, 0.9, 100) - ); + getUsdPricesMockFn(baseTokenAddress, getPoints([100, 100.01, 100.02, 100.03]), POINTS_VOLATILITY_ZERO) + ) + getUsdPrice.mockImplementation(getUsdPriceMockFn(baseTokenAddress, 0.9, 100)) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the the calculated slippage // AVG = (100 + 100.01 + 100.02 + 100.03)/4 = 100.015 @@ -333,31 +295,29 @@ describe('SlippageServiceMain Specification', () => { // Volatility Fair Settlement (Token) = 0.01118033989 / 0.9 = 0.01242259988 // Slippage BPS = ceil(0.01242259988 * 10000) = 125 // Adjusted Slippage = 125 - expect(result).toBe(12423); - }); + expect(result).toBe(12423) + }) it(`price points further away in time, make the slippage smaller`, async () => { // GIVEN: If the points are 6min apart (instead of 5min) - const fixedStartTime = new Date('2024-01-01T00:00:00Z').getTime(); + const fixedStartTime = new Date('2024-01-01T00:00:00Z').getTime() getUsdPrices.mockImplementation( getUsdPricesMockFn( baseTokenAddress, getPoints([100, 100.01, 100.02, 100.03], SIX_MIN, fixedStartTime), getPoints([10, 10, 10, 10], SIX_MIN, fixedStartTime) ) - ); + ) // GIVEN: Price is 1 USD and 10 USD - getUsdPrice.mockImplementation( - getUsdPriceMockFn(baseTokenAddress, 1, 10) - ); + getUsdPrice.mockImplementation(getUsdPriceMockFn(baseTokenAddress, 1, 10)) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the calculated slippage for 6min intervals // This test uses pair-wise relative volatility calculation: @@ -366,26 +326,26 @@ describe('SlippageServiceMain Specification', () => { // Relative prices: [100/10, 100.01/10, 100.02/10, 100.03/10] = [10, 10.001, 10.002, 10.003] // Time stretch factor: 5min / 6min = 0.833 (less volatility per settlement time) // Expected: Lower volatility than 5min intervals due to time stretching - expect(result).toBe(97); // Lower than baseline due to 6min intervals vs 5min - }); + expect(result).toBe(97) // Lower than baseline due to 6min intervals vs 5min + }) it(`price points closer in time, increase volatility`, async () => { // GIVEN: If the points are 4min apart (instead of 5min) - const fixedStartTime = new Date('2024-01-01T00:00:00Z').getTime(); + const fixedStartTime = new Date('2024-01-01T00:00:00Z').getTime() getUsdPrices.mockImplementation( getUsdPricesMockFn( baseTokenAddress, getPoints([100, 100.01, 100.02, 100.03], FOUR_MIN, fixedStartTime), getPoints([10, 10, 10, 10], FOUR_MIN, fixedStartTime) ) - ); + ) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the calculated slippage for 4min intervals // This test uses pair-wise relative volatility calculation: @@ -394,31 +354,31 @@ describe('SlippageServiceMain Specification', () => { // Relative prices: [100/10, 100.01/10, 100.02/10, 100.03/10] = [10, 10.001, 10.002, 10.003] // Time compression factor: 5min / 4min = 1.25 (more volatility per settlement time) // Expected: Higher volatility than 5min intervals due to time compression - expect(result).toBe(160); // Consistently calculated value for 4min intervals - }); - }); + expect(result).toBe(160) // Consistently calculated value for 4min intervals + }) + }) describe('when tokens have the exact same volatility', () => { it(`when usd prices are different`, async () => { // GIVEN: The prices have high volatility - getUsdPrices.mockResolvedValue(getPoints([100, 100.01, 100.02, 100.03])); + getUsdPrices.mockResolvedValue(getPoints([100, 100.01, 100.02, 100.03])) getUsdPrice.mockImplementation(async (chainId, tokenAddress) => { if (tokenAddress === quoteTokenAddress) { // GIVEN: Base token is 1 USD - return 1; + return 1 } else { // GIVEN: Quote token is 1 USD - return 0.9; + return 0.9 } - }); + }) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get the worst slippage of the two tokens // AVG = (100 + 100.01 + 100.02 + 100.03)/4 = 100.015 @@ -434,66 +394,62 @@ describe('SlippageServiceMain Specification', () => { // Volatility Fair Settlement for quote (Token) = 0.01118033989 / 0.9 = 0.01242259988 // Slippage BPS for quote = ceil(0.01242259988 * 10000) = 125 // Adjusted Slippage for quote = 125 - expect(result).toBe(0); + expect(result).toBe(0) // WHEN: Get slippage (inverting the tokens) const resultTokensInverted = await slippageService.getSlippageBps({ chainId, quoteTokenAddress: baseTokenAddress, baseTokenAddress: quoteTokenAddress, - }); + }) // THEN: The result should be the same (worst of the two) - expect(resultTokensInverted).toBe(0); - }); + expect(resultTokensInverted).toBe(0) + }) it(`should return 0 volatility if we can't estimate the USD price of a token`, async () => { // GIVEN: The prices have high volatility - getUsdPrices.mockResolvedValue(getPoints([100, 100.01, 100.02, 100.03])); + getUsdPrices.mockResolvedValue(getPoints([100, 100.01, 100.02, 100.03])) getUsdPrice.mockImplementation(async (chainId, tokenAddress) => { if (tokenAddress === quoteTokenAddress) { // GIVEN: Base token is 1 USD - return 1; + return 1 } else { // GIVEN: Quote token is not available - return null; + return null } - }); + }) // WHEN: Get slippage const result = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: We get 0 slippage - expect(result).toBe(0); + expect(result).toBe(0) // WHEN: Get slippage (inverting the tokens) const resultInverted = await slippageService.getSlippageBps({ chainId, baseTokenAddress: quoteTokenAddress, quoteTokenAddress: baseTokenAddress, - }); + }) // THEN: The result should be the same (worst of the two) - expect(resultInverted).toBe(0); - }); - }); -}); - -function getPoints( - prices: number[], - timeBetweenPoints = FIVE_MIN, - startDate = Date.now() -): PricePoint[] { + expect(resultInverted).toBe(0) + }) + }) +}) + +function getPoints(prices: number[], timeBetweenPoints = FIVE_MIN, startDate = Date.now()): PricePoint[] { return prices.map((price, i) => ({ date: new Date(startDate + timeBetweenPoints * i), price, volume: 1, - })); + })) } function getUsdPricesMockFn( @@ -501,37 +457,25 @@ function getUsdPricesMockFn( tokenAPoints: PricePoint[] | null, tokenBPoints: PricePoint[] | null ) { - return async ( - _chainId: SupportedChainId, - tokenAddress: string, - _interval?: any - ) => { + return async (_chainId: SupportedChainId, tokenAddress: string, _interval?: any) => { if (tokenAddress === baseTokenAddress) { // GIVEN: One token is volatile - return tokenAPoints; + return tokenAPoints } else { // GIVEN: The other token is not - return tokenBPoints; + return tokenBPoints } - }; + } } -function getUsdPriceMockFn( - baseTokenAddress: string, - tokenAPrice: number | null, - tokenBPrice: number | null -) { - return async ( - _chainId: SupportedChainId, - tokenAddress: string, - _interval?: any - ) => { +function getUsdPriceMockFn(baseTokenAddress: string, tokenAPrice: number | null, tokenBPrice: number | null) { + return async (_chainId: SupportedChainId, tokenAddress: string, _interval?: any) => { if (tokenAddress === baseTokenAddress) { // GIVEN: One token is volatile - return tokenAPrice; + return tokenAPrice } else { // GIVEN: The other token is not - return tokenBPrice; + return tokenBPrice } - }; + } } diff --git a/libs/services/src/SlippageService/SlippageServiceMain.test.ts b/libs/services/src/SlippageService/SlippageServiceMain.test.ts index 01dfeff3..f48889b7 100644 --- a/libs/services/src/SlippageService/SlippageServiceMain.test.ts +++ b/libs/services/src/SlippageService/SlippageServiceMain.test.ts @@ -1,10 +1,10 @@ -import { ChainNames, toSupportedChainId } from '@cowprotocol/shared'; -import fs from 'fs'; -import path from 'path'; -import { SlippageServiceMain } from './SlippageServiceMain'; +import { ChainNames, toSupportedChainId } from '@cowprotocol/shared' +import fs from 'fs' +import path from 'path' +import { SlippageServiceMain } from './SlippageServiceMain' -const getUsdPrice = jest.fn(); -const getUsdPrices = jest.fn(); +const getUsdPrice = jest.fn() +const getUsdPrices = jest.fn() /** * Test the SlippageService main implementation using realistic test data. @@ -14,11 +14,11 @@ const getUsdPrices = jest.fn(); * These will allow to refine the slippage calculation algorithm to make it work well with real data. */ describe('SlippageServiceMain: Real test data', () => { - let slippageService: SlippageServiceMain; + let slippageService: SlippageServiceMain // Read all files in test-data folder - const testDataDir = path.join(__dirname, 'test-data'); - const testDataFiles = fs.readdirSync(testDataDir); + const testDataDir = path.join(__dirname, 'test-data') + const testDataFiles = fs.readdirSync(testDataDir) for (const fileName of testDataFiles) { const { @@ -28,49 +28,46 @@ describe('SlippageServiceMain: Real test data', () => { chainId: chainIdValue, volatilityDetails, slippageBps, - } = readTestFile(path.join(testDataDir, fileName)); + } = readTestFile(path.join(testDataDir, fileName)) - const chainId = toSupportedChainId(chainIdValue); - const chainName = ChainNames[chainId]; + const chainId = toSupportedChainId(chainIdValue) + const chainName = ChainNames[chainId] // Uncomment to tests a single pair // if (pair !== 'WETH-xDAI' || chainId !== SupportedChainId.GNOSIS_CHAIN) // continue; test(`Expect ${chainName} ${pair} slippage to be ${slippageBps} BPS`, async () => { - const { - quoteToken: quoteTokenVolatilityDetails, - baseToken: baseTokenVolatilityDetails, - } = volatilityDetails; + const { quoteToken: quoteTokenVolatilityDetails, baseToken: baseTokenVolatilityDetails } = volatilityDetails // GIVEN: USD price for the base and quote tokens getUsdPrice.mockImplementation(async (_chainId, tokenAddress) => { if (tokenAddress === baseTokenAddress) { - return baseTokenVolatilityDetails.usdPrice; + return baseTokenVolatilityDetails.usdPrice } else { - return quoteTokenVolatilityDetails.usdPrice; + return quoteTokenVolatilityDetails.usdPrice } - }); + }) // GIVEN: USD prices for the base and quote tokens getUsdPrices.mockImplementation(async (_chainId, tokenAddress) => { if (tokenAddress === baseTokenAddress) { - return baseTokenVolatilityDetails.prices; + return baseTokenVolatilityDetails.prices } else { - return quoteTokenVolatilityDetails.prices; + return quoteTokenVolatilityDetails.prices } - }); + }) // WHEN: Get the slippage const slippage = await slippageService.getSlippageBps({ chainId, baseTokenAddress, quoteTokenAddress, - }); + }) // THEN: The slippage should be as expected - expect(slippage).toBe(slippageBps); - }); + expect(slippage).toBe(slippageBps) + }) } beforeEach(() => { @@ -78,25 +75,23 @@ describe('SlippageServiceMain: Real test data', () => { name: 'RealTestDataMock', getUsdPrice, getUsdPrices, - }); - }); -}); + }) + }) +}) function readTestFile(filePath: string) { - const testContent = JSON.parse(fs.readFileSync(filePath, 'utf-8')); - const volatilityDetails = testContent.volatilityDetails; + const testContent = JSON.parse(fs.readFileSync(filePath, 'utf-8')) + const volatilityDetails = testContent.volatilityDetails - volatilityDetails.quoteToken.prices = - volatilityDetails.quoteToken.prices.map(fixDateForPrices); - volatilityDetails.baseToken.prices = - volatilityDetails.baseToken.prices.map(fixDateForPrices); + volatilityDetails.quoteToken.prices = volatilityDetails.quoteToken.prices.map(fixDateForPrices) + volatilityDetails.baseToken.prices = volatilityDetails.baseToken.prices.map(fixDateForPrices) - return testContent; + return testContent } function fixDateForPrices(price: any) { return { ...price, date: new Date(price.date), - }; + } } diff --git a/libs/services/src/SlippageService/SlippageServiceMain.ts b/libs/services/src/SlippageService/SlippageServiceMain.ts index 942adbd8..3c2c07d2 100644 --- a/libs/services/src/SlippageService/SlippageServiceMain.ts +++ b/libs/services/src/SlippageService/SlippageServiceMain.ts @@ -1,5 +1,5 @@ -import { PricePoint, UsdRepository, usdRepositorySymbol } from '@cowprotocol/repositories'; -import { injectable, inject } from 'inversify'; +import { PricePoint, UsdRepository, usdRepositorySymbol } from '@cowprotocol/repositories' +import { injectable, inject } from 'inversify' import { Bps, GetSlippageBpsParams, @@ -7,12 +7,12 @@ import { PairVolatility, SlippageService, VolatilityDetails, -} from './SlippageService'; -import ms from 'ms'; -import { toTokenAddress } from '@cowprotocol/shared'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +} from './SlippageService' +import ms from 'ms' +import { toTokenAddress } from '@cowprotocol/shared' +import { SupportedChainId } from '@cowprotocol/cow-sdk' -const FAIR_TIME_TO_SETTLEMENT = ms('5min'); +const FAIR_TIME_TO_SETTLEMENT = ms('5min') @injectable() export class SlippageServiceMain implements SlippageService { @@ -23,27 +23,27 @@ export class SlippageServiceMain implements SlippageService { async getSlippageBps(params: GetSlippageBpsParams): Promise { // Try relative volatility first - const relativeVolatility = await this.getRelativeVolatilityOnSettlement(params); + const relativeVolatility = await this.getRelativeVolatilityOnSettlement(params) // If relative volatility is available, use it if (relativeVolatility !== null) { - return this.getSlippageBpsFromVolatility(relativeVolatility); + return this.getSlippageBpsFromVolatility(relativeVolatility) } // Fall back to max volatility if relative volatility cannot be calculated - const maxVolatility = await this.getMaxVolatilityOnSettlement(params); + const maxVolatility = await this.getMaxVolatilityOnSettlement(params) // If volatility is unknown, we return 0 if (maxVolatility === null) { - return 0; + return 0 } // Return the slippage based on the volatility - return this.getSlippageBpsFromVolatility(maxVolatility); + return this.getSlippageBpsFromVolatility(maxVolatility) } private getSlippageBpsFromVolatility(volatility: number): Bps { - return Math.ceil(volatility * 10_000); + return Math.ceil(volatility * 10_000) } /** @@ -59,32 +59,25 @@ export class SlippageServiceMain implements SlippageService { tokenAddressString: string, order?: OrderForSlippageCalculation ): Promise { - const tokenAddress = toTokenAddress(tokenAddressString, chainId); - const prices = await this.usdRepository.getUsdPrices( - chainId.toString(), - tokenAddress, - '5m' - ); + const tokenAddress = toTokenAddress(tokenAddressString, chainId) + const prices = await this.usdRepository.getUsdPrices(chainId.toString(), tokenAddress, '5m') if (!prices) { - return null; + return null } // Get price of the token - const usdPrice = await this.usdRepository.getUsdPrice( - chainId.toString(), - tokenAddress - ); + const usdPrice = await this.usdRepository.getUsdPrice(chainId.toString(), tokenAddress) if (!usdPrice) { - return null; + return null } // Predict variance between now and a fair settlement - const volatilityForFairSettlement = this.calculateVolatility(prices); + const volatilityForFairSettlement = this.calculateVolatility(prices) // Return the normalized volatility (denominated in the token, not in USD) - const normalizedVolatility = volatilityForFairSettlement / usdPrice; + const normalizedVolatility = volatilityForFairSettlement / usdPrice return { tokenAddress, @@ -92,7 +85,7 @@ export class SlippageServiceMain implements SlippageService { usdPrice, volatilityInUsd: volatilityForFairSettlement, volatilityInTokens: normalizedVolatility, - }; + } } /** @@ -113,65 +106,63 @@ export class SlippageServiceMain implements SlippageService { const [basePrices, quotePrices] = await Promise.all([ this.usdRepository.getUsdPrices(chainId.toString(), baseTokenAddress, '5m'), this.usdRepository.getUsdPrices(chainId.toString(), quoteTokenAddress, '5m'), - ]); + ]) // Check if either price data is missing if (!basePrices || !quotePrices) { - return null; + return null } // Fetch USD prices for both tokens const [baseUsdPrice, quoteUsdPrice] = await Promise.all([ this.usdRepository.getUsdPrice(chainId.toString(), baseTokenAddress), this.usdRepository.getUsdPrice(chainId.toString(), quoteTokenAddress), - ]); + ]) // Check if either USD price is missing if (baseUsdPrice === null || quoteUsdPrice === null) { - return null; + return null } - const relativePrice = baseUsdPrice / quoteUsdPrice; + const relativePrice = baseUsdPrice / quoteUsdPrice // Prices is an array. Build a map with timestamp as key using `basePrices` date, so we can match with the timestamp on `quotePrices` - const basePricesMap = new Map( - basePrices.map((price) => [roundDate(price.date).getTime(), price]) - ); + const basePricesMap = new Map(basePrices.map((price) => [roundDate(price.date).getTime(), price])) // Calculate price ratios for the token prices const prices = quotePrices.reduce((acc, quotePrice) => { // Get the same timestamp - const roundedDate = roundDate(quotePrice.date); - const basePrice = basePricesMap.get(roundedDate.getTime()); + const roundedDate = roundDate(quotePrice.date) + const basePrice = basePricesMap.get(roundedDate.getTime()) if (quotePrice && basePrice) { - const price = basePrice.price / quotePrice.price; // Calculate the price ratio + const price = basePrice.price / quotePrice.price // Calculate the price ratio acc.push({ ...basePrice, price, date: roundedDate, - } as PricePoint); + } as PricePoint) } - return acc; - }, []); + return acc + }, []) // Not enough data, data point don't align if (prices.length < 2) { - return null; + return null } // Predict variance between now and a fair settlement - const volatilityForFairSettlement = this.calculateVolatility(prices); + const volatilityForFairSettlement = this.calculateVolatility(prices) // Return the normalized volatility (denominated in the token, not in USD) - const normalizedVolatility = volatilityForFairSettlement / relativePrice; + const normalizedVolatility = volatilityForFairSettlement / relativePrice return { baseTokenAddress, quoteTokenAddress, prices, volatilityInTokens: normalizedVolatility, - }; + } } private async getMaxVolatilityOnSettlement({ @@ -184,16 +175,13 @@ export class SlippageServiceMain implements SlippageService { const [volatilityQuote, volatilityBase] = await Promise.all([ this.getVolatilityDetails(chainId, quoteTokenAddress, order), this.getVolatilityDetails(chainId, baseTokenAddress, order), - ]); + ]) if (volatilityQuote === null || volatilityBase === null) { - return null; + return null } - return Math.max( - volatilityQuote.volatilityInTokens, - volatilityBase.volatilityInTokens - ); + return Math.max(volatilityQuote.volatilityInTokens, volatilityBase.volatilityInTokens) } private async getRelativeVolatilityOnSettlement({ @@ -202,68 +190,57 @@ export class SlippageServiceMain implements SlippageService { baseTokenAddress, quoteTokenAddress, }: GetSlippageBpsParams) { - const volatility = await this.getVolatilityForPair( - chainId, - baseTokenAddress, - quoteTokenAddress, - order - ); + const volatility = await this.getVolatilityForPair(chainId, baseTokenAddress, quoteTokenAddress, order) if (!volatility) { - return null; + return null } - return volatility.volatilityInTokens; + return volatility.volatilityInTokens } private calculateVolatility(prices: PricePoint[]): number { // Return 0 for empty arrays or arrays with insufficient data if (prices.length === 0) { - return 0; + return 0 } // Calculate the average of the prices (in USD) - const averagePrice = - prices.reduce((acc, price) => acc + price.price, 0) / prices.length; + const averagePrice = prices.reduce((acc, price) => acc + price.price, 0) / prices.length // Calculate the variance const variance = prices.reduce((acc, price) => { // Calculate price differences between the price point and the average - const difference = price.price - averagePrice; + const difference = price.price - averagePrice // Square the difference - const squaredDifference = difference ** 2; + const squaredDifference = difference ** 2 // Sum squared differences - return acc + squaredDifference; - }, 0) / prices.length; + return acc + squaredDifference + }, 0) / prices.length // Calculate the standard deviation - const standardDeviation = Math.sqrt(variance); + const standardDeviation = Math.sqrt(variance) // For single data point, we can't calculate time difference, return standard deviation if (prices.length === 1) { - return standardDeviation; + return standardDeviation } // Average time between each data point const averageTimeBetweenDataPoints = - (prices[prices.length - 1].date.getTime() - prices[0].date.getTime()) / - (prices.length - 1); + (prices[prices.length - 1].date.getTime() - prices[0].date.getTime()) / (prices.length - 1) // Points in Time for Settlement - const pointsForFairSettlement = - FAIR_TIME_TO_SETTLEMENT / averageTimeBetweenDataPoints; + const pointsForFairSettlement = FAIR_TIME_TO_SETTLEMENT / averageTimeBetweenDataPoints // Predict variance between now and a fair settlement - return standardDeviation * Math.sqrt(pointsForFairSettlement); + return standardDeviation * Math.sqrt(pointsForFairSettlement) } } function roundDate(date: Date): Date { - return new Date( - Math.round(date.getTime() / FAIR_TIME_TO_SETTLEMENT) * - FAIR_TIME_TO_SETTLEMENT - ); + return new Date(Math.round(date.getTime() / FAIR_TIME_TO_SETTLEMENT) * FAIR_TIME_TO_SETTLEMENT) } diff --git a/libs/services/src/TokenBalancesService/TokenBalancesService.ts b/libs/services/src/TokenBalancesService/TokenBalancesService.ts index 68e91df2..1330bd35 100644 --- a/libs/services/src/TokenBalancesService/TokenBalancesService.ts +++ b/libs/services/src/TokenBalancesService/TokenBalancesService.ts @@ -7,34 +7,28 @@ import { TokenBalancesResponse, UserBalanceRepository, userBalanceRepositorySymbol, -} from '@cowprotocol/repositories'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { inject, injectable } from 'inversify'; -import { logger } from '@cowprotocol/shared'; +} from '@cowprotocol/repositories' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { inject, injectable } from 'inversify' +import { logger } from '@cowprotocol/shared' export interface UserTokenBalanceWithToken { - balance: string; - allowance: string; - token: Erc20; + balance: string + allowance: string + token: Erc20 } export interface TokenBalancesService { - getTokenBalances({ - chainId, - address, - }: { - chainId: SupportedChainId; - address: string; - }): Promise; + getTokenBalances({ chainId, address }: { chainId: SupportedChainId; address: string }): Promise getUserTokenBalances(params: { - chainId: SupportedChainId; - userAddress: string; - tokenAddresses: string[]; - }): Promise; + chainId: SupportedChainId + userAddress: string + tokenAddresses: string[] + }): Promise } -export const tokenBalancesServiceSymbol = Symbol.for('TokenBalancesService'); +export const tokenBalancesServiceSymbol = Symbol.for('TokenBalancesService') @injectable() export class TokenBalancesServiceMain implements TokenBalancesService { @@ -51,10 +45,10 @@ export class TokenBalancesServiceMain implements TokenBalancesService { chainId, address, }: { - chainId: SupportedChainId; - address: string; + chainId: SupportedChainId + address: string }): Promise { - return this.tokenBalancesRepository.getTokenBalances({ chainId, address }); + return this.tokenBalancesRepository.getTokenBalances({ chainId, address }) } /** @@ -64,20 +58,15 @@ export class TokenBalancesServiceMain implements TokenBalancesService { * @param tokenAddresses The list of token addresses * @returns The list of token infos */ - private async getTokenInfos( - chainId: SupportedChainId, - tokenAddresses: string[] - ): Promise { - const tokens: Erc20[] = []; + private async getTokenInfos(chainId: SupportedChainId, tokenAddresses: string[]): Promise { + const tokens: Erc20[] = [] for (const tokenAddress of tokenAddresses) { // TODO: Potentially consider adding a getAll method in the repository - const token = await this.erc20Repository.get(chainId, tokenAddress); + const token = await this.erc20Repository.get(chainId, tokenAddress) if (!token) { - logger.warn( - `Token ${tokenAddress} not found for chain ${chainId}. Skipping.` - ); - continue; + logger.warn(`Token ${tokenAddress} not found for chain ${chainId}. Skipping.`) + continue } tokens.push({ @@ -85,10 +74,10 @@ export class TokenBalancesServiceMain implements TokenBalancesService { decimals: token.decimals, symbol: token.symbol, name: token.name, - }); + }) } - return tokens; + return tokens } async getUserTokenBalances({ @@ -96,41 +85,30 @@ export class TokenBalancesServiceMain implements TokenBalancesService { userAddress, tokenAddresses, }: { - chainId: SupportedChainId; - userAddress: string; - tokenAddresses: string[]; + chainId: SupportedChainId + userAddress: string + tokenAddresses: string[] }): Promise { - const balancesPromise = this.userBalanceRepository.getUserTokenBalances( - chainId, - userAddress, - tokenAddresses - ); - const tokensPromise = this.getTokenInfos(chainId, tokenAddresses); + const balancesPromise = this.userBalanceRepository.getUserTokenBalances(chainId, userAddress, tokenAddresses) + const tokensPromise = this.getTokenInfos(chainId, tokenAddresses) - const [balances, tokens] = await Promise.all([ - balancesPromise, - tokensPromise, - ]); + const [balances, tokens] = await Promise.all([balancesPromise, tokensPromise]) - const tokensByAddress = new Map( - tokens.map((token) => [token.address.toLowerCase(), token]) - ); + const tokensByAddress = new Map(tokens.map((token) => [token.address.toLowerCase(), token])) return balances .map((balance) => { - const token = tokensByAddress.get(balance.tokenAddress.toLowerCase()); + const token = tokensByAddress.get(balance.tokenAddress.toLowerCase()) if (!token) { - return null; + return null } return { balance: balance.balance, allowance: balance.allowance, token, - }; + } }) - .filter( - (balance): balance is UserTokenBalanceWithToken => balance !== null - ); + .filter((balance): balance is UserTokenBalanceWithToken => balance !== null) } } diff --git a/libs/services/src/TokenDetailService/TokenDetailService.ts b/libs/services/src/TokenDetailService/TokenDetailService.ts index 7fa5e18b..6c125dfe 100644 --- a/libs/services/src/TokenDetailService/TokenDetailService.ts +++ b/libs/services/src/TokenDetailService/TokenDetailService.ts @@ -1,24 +1,14 @@ -import { - Erc20, - Erc20Repository, - erc20RepositorySymbol, -} from '@cowprotocol/repositories'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { inject, injectable } from 'inversify'; +import { Erc20, Erc20Repository, erc20RepositorySymbol } from '@cowprotocol/repositories' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { inject, injectable } from 'inversify' export interface TokenDetailService { - getTokenDetails( - chainId: SupportedChainId, - tokenAddress: string - ): Promise; + getTokenDetails(chainId: SupportedChainId, tokenAddress: string): Promise - getTokensDetails( - chainId: SupportedChainId, - tokenAddresses: string[] - ): Promise<(Erc20 | null)[]>; + getTokensDetails(chainId: SupportedChainId, tokenAddresses: string[]): Promise<(Erc20 | null)[]> } -export const tokenDetailServiceSymbol = Symbol.for('TokenDetailService'); +export const tokenDetailServiceSymbol = Symbol.for('TokenDetailService') @injectable() export class TokenDetailServiceMain implements TokenDetailService { @@ -27,21 +17,11 @@ export class TokenDetailServiceMain implements TokenDetailService { private erc20Repository: Erc20Repository ) {} - async getTokenDetails( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { - return this.erc20Repository.get(chainId, tokenAddress); + async getTokenDetails(chainId: SupportedChainId, tokenAddress: string): Promise { + return this.erc20Repository.get(chainId, tokenAddress) } - async getTokensDetails( - chainId: SupportedChainId, - tokenAddresses: string[] - ): Promise<(Erc20 | null)[]> { - return Promise.all( - tokenAddresses.map((address) => - this.erc20Repository.get(chainId, address) - ) - ); + async getTokensDetails(chainId: SupportedChainId, tokenAddresses: string[]): Promise<(Erc20 | null)[]> { + return Promise.all(tokenAddresses.map((address) => this.erc20Repository.get(chainId, address))) } } diff --git a/libs/services/src/TokenHolderService/TokenHolderService.ts b/libs/services/src/TokenHolderService/TokenHolderService.ts index 812d002c..5ff3c4d1 100644 --- a/libs/services/src/TokenHolderService/TokenHolderService.ts +++ b/libs/services/src/TokenHolderService/TokenHolderService.ts @@ -1,19 +1,12 @@ -import { - TokenHolderRepository, - tokenHolderRepositorySymbol, - TokenHolderPoint, -} from '@cowprotocol/repositories'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; -import { injectable, inject } from 'inversify'; +import { TokenHolderRepository, tokenHolderRepositorySymbol, TokenHolderPoint } from '@cowprotocol/repositories' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { injectable, inject } from 'inversify' export interface TokenHolderService { - getTopTokenHolders( - chainId: SupportedChainId, - tokenAddress: string - ): Promise; + getTopTokenHolders(chainId: SupportedChainId, tokenAddress: string): Promise } -export const tokenHolderServiceSymbol = Symbol.for('TokenHolderService'); +export const tokenHolderServiceSymbol = Symbol.for('TokenHolderService') @injectable() export class TokenHolderServiceMain implements TokenHolderService { @@ -22,10 +15,7 @@ export class TokenHolderServiceMain implements TokenHolderService { private tokenHolderRepository: TokenHolderRepository ) {} - async getTopTokenHolders( - chainId: SupportedChainId, - tokenAddress: string - ): Promise { - return this.tokenHolderRepository.getTopTokenHolders(chainId, tokenAddress); + async getTopTokenHolders(chainId: SupportedChainId, tokenAddress: string): Promise { + return this.tokenHolderRepository.getTopTokenHolders(chainId, tokenAddress) } } diff --git a/libs/services/src/UsdService/UsdService.ts b/libs/services/src/UsdService/UsdService.ts index 64164ab9..38c7529e 100644 --- a/libs/services/src/UsdService/UsdService.ts +++ b/libs/services/src/UsdService/UsdService.ts @@ -1,14 +1,11 @@ -import { UsdRepository, usdRepositorySymbol } from '@cowprotocol/repositories'; -import { inject, injectable } from 'inversify'; +import { UsdRepository, usdRepositorySymbol } from '@cowprotocol/repositories' +import { inject, injectable } from 'inversify' export interface UsdService { - getUsdPrice( - chainIdOrSlug: string, - tokenAddress?: string | undefined - ): Promise; + getUsdPrice(chainIdOrSlug: string, tokenAddress?: string | undefined): Promise } -export const usdServiceSymbol = Symbol.for('UsdService'); +export const usdServiceSymbol = Symbol.for('UsdService') @injectable() export class UsdServiceMain implements UsdService { @@ -17,10 +14,7 @@ export class UsdServiceMain implements UsdService { private usdRepository: UsdRepository ) {} - async getUsdPrice( - chainIdOrSlug: string, - tokenAddress?: string | undefined - ): Promise { - return this.usdRepository.getUsdPrice(chainIdOrSlug, tokenAddress); + async getUsdPrice(chainIdOrSlug: string, tokenAddress?: string | undefined): Promise { + return this.usdRepository.getUsdPrice(chainIdOrSlug, tokenAddress) } } diff --git a/libs/services/src/factories.ts b/libs/services/src/factories.ts index 8d1a1042..28ec78ee 100644 --- a/libs/services/src/factories.ts +++ b/libs/services/src/factories.ts @@ -1,4 +1,4 @@ -import 'reflect-metadata'; +import 'reflect-metadata' import { CacheRepository, @@ -48,183 +48,160 @@ import { UsdRepositoryCoingecko, UsdRepositoryCow, UsdRepositoryFallback, -} from '@cowprotocol/repositories'; +} from '@cowprotocol/repositories' -import ms from 'ms'; -import { Pool } from 'pg'; +import ms from 'ms' +import { Pool } from 'pg' -const DEFAULT_CACHE_VALUE_SECONDS = ms('2min') / 1000; // 2min cache time by default for values -const DEFAULT_CACHE_NULL_SECONDS = ms('30min') / 1000; // 30min cache time by default for NULL values (when the repository isn't known) +const DEFAULT_CACHE_VALUE_SECONDS = ms('2min') / 1000 // 2min cache time by default for values +const DEFAULT_CACHE_NULL_SECONDS = ms('30min') / 1000 // 30min cache time by default for NULL values (when the repository isn't known) -const CACHE_TOKEN_INFO_SECONDS = ms('24h') / 1000; // 24h +const CACHE_TOKEN_INFO_SECONDS = ms('24h') / 1000 // 24h // Singleton instances -let postgresPool: Pool | undefined = undefined; -let telegramBot: TelegramBot | undefined = undefined; +let postgresPool: Pool | undefined = undefined +let telegramBot: TelegramBot | undefined = undefined -export function getErc20Repository( - cacheRepository: CacheRepository -): Erc20Repository { - const viem = new Erc20RepositoryViem(getViemClients()); - const native = new Erc20RepositoryNative(); +export function getErc20Repository(cacheRepository: CacheRepository): Erc20Repository { + const viem = new Erc20RepositoryViem(getViemClients()) + const native = new Erc20RepositoryNative() - const fallback = new Erc20RepositoryFallback([native, viem]); + const fallback = new Erc20RepositoryFallback([native, viem]) - return new Erc20RepositoryCache( - fallback, - cacheRepository, - 'erc20', - CACHE_TOKEN_INFO_SECONDS - ); + return new Erc20RepositoryCache(fallback, cacheRepository, 'erc20', CACHE_TOKEN_INFO_SECONDS) } export function getCacheRepository(): CacheRepository { if (redisClient) { - return new CacheRepositoryRedis(redisClient); + return new CacheRepositoryRedis(redisClient) } - return new CacheRepositoryMemory(); + return new CacheRepositoryMemory() } -export function getUsdRepositoryCow( - cacheRepository: CacheRepository, - erc20Repository: Erc20Repository -): UsdRepository { +export function getUsdRepositoryCow(cacheRepository: CacheRepository, erc20Repository: Erc20Repository): UsdRepository { return new UsdRepositoryCache( new UsdRepositoryCow(cowApiClients, erc20Repository), cacheRepository, 'usdCow', DEFAULT_CACHE_VALUE_SECONDS, DEFAULT_CACHE_NULL_SECONDS - ); + ) } -export function getUsdRepositoryCoingecko( - cacheRepository: CacheRepository -): UsdRepository { +export function getUsdRepositoryCoingecko(cacheRepository: CacheRepository): UsdRepository { return new UsdRepositoryCache( new UsdRepositoryCoingecko(), cacheRepository, 'usdCoingecko', DEFAULT_CACHE_VALUE_SECONDS, DEFAULT_CACHE_NULL_SECONDS - ); + ) } -export function getUsdRepository( - cacheRepository: CacheRepository, - erc20Repository: Erc20Repository -): UsdRepository { +export function getUsdRepository(cacheRepository: CacheRepository, erc20Repository: Erc20Repository): UsdRepository { return new UsdRepositoryFallback([ getUsdRepositoryCoingecko(cacheRepository), getUsdRepositoryCow(cacheRepository, erc20Repository), - ]); + ]) } -export function getTokenHolderRepositoryEthplorer( - cacheRepository: CacheRepository -): TokenHolderRepository { +export function getTokenHolderRepositoryEthplorer(cacheRepository: CacheRepository): TokenHolderRepository { return new TokenHolderRepositoryCache( new TokenHolderRepositoryEthplorer(), cacheRepository, 'tokenHolderEthplorer', DEFAULT_CACHE_VALUE_SECONDS, DEFAULT_CACHE_NULL_SECONDS - ); + ) } -export function getTokenHolderRepositoryMoralis( - cacheRepository: CacheRepository -): TokenHolderRepository { +export function getTokenHolderRepositoryMoralis(cacheRepository: CacheRepository): TokenHolderRepository { return new TokenHolderRepositoryCache( new TokenHolderRepositoryMoralis(), cacheRepository, 'tokenHolderMoralis', DEFAULT_CACHE_VALUE_SECONDS, DEFAULT_CACHE_NULL_SECONDS - ); + ) } -export function getTokenHolderRepository( - cacheRepository: CacheRepository -): TokenHolderRepository { +export function getTokenHolderRepository(cacheRepository: CacheRepository): TokenHolderRepository { return new TokenHolderRepositoryFallback([ getTokenHolderRepositoryMoralis(cacheRepository), getTokenHolderRepositoryEthplorer(cacheRepository), - ]); + ]) } export function getTokenBalancesRepository(): TokenBalancesRepository { - return new TokenBalancesRepositoryAlchemy(); + return new TokenBalancesRepositoryAlchemy() } -export function getUserBalanceRepository( - cacheRepository: CacheRepository -): UserBalanceRepository { +export function getUserBalanceRepository(cacheRepository: CacheRepository): UserBalanceRepository { return new UserBalanceRepositoryCache( new UserBalanceRepositoryViem(getViemClients()), cacheRepository, 'user_balance', 1 // Cache balances for 1 second - ); + ) } export function getPushNotificationsRepository(): PushNotificationsRepository { - return new PushNotificationsRepositoryRabbit(); + return new PushNotificationsRepositoryRabbit() } export function getPushSubscriptionsRepository(): PushSubscriptionsRepository { - return new PushSubscriptionsRepositoryCms(); + return new PushSubscriptionsRepositoryCms() } export function getAffiliatesRepository(): AffiliatesRepository { - return new AffiliatesRepositoryCms(); + return new AffiliatesRepositoryCms() } function getPostgresPool(): Pool { if (!postgresPool) { - postgresPool = createNewPostgresPool(); + postgresPool = createNewPostgresPool() } - return postgresPool; + return postgresPool } export function getIndexerStateRepository(): IndexerStateRepository { - const pool = getPostgresPool(); + const pool = getPostgresPool() - return new IndexerStateRepositoryPostgres(pool); + return new IndexerStateRepositoryPostgres(pool) } export function getOnChainPlacedOrdersRepository(): OnChainPlacedOrdersRepository { - return new OnChainPlacedOrdersRepositoryPostgres(); + return new OnChainPlacedOrdersRepositoryPostgres() } export function getExpiredOrdersRepository(): ExpiredOrdersRepository { - return new ExpiredOrdersRepositoryPostgres(); + return new ExpiredOrdersRepositoryPostgres() } export function getOrdersAppDataRepository(): OrdersAppDataRepository { - return new OrdersAppDataRepositoryPostgres(); + return new OrdersAppDataRepositoryPostgres() } export function getSimulationRepository(): SimulationRepository { - return new SimulationRepositoryTenderly(); + return new SimulationRepositoryTenderly() } export function getTelegramBot(): TelegramBot { if (!telegramBot) { - telegramBot = createTelegramBot(); + telegramBot = createTelegramBot() } - return telegramBot; + return telegramBot } export function getDuneRepository(): DuneRepository { - const apiKey = process.env.DUNE_API_KEY; + const apiKey = process.env.DUNE_API_KEY if (!apiKey) { - throw new Error('DUNE_API_KEY is not set'); + throw new Error('DUNE_API_KEY is not set') } - return new DuneRepositoryImpl(apiKey); + return new DuneRepositoryImpl(apiKey) } diff --git a/libs/services/src/index.ts b/libs/services/src/index.ts index 96a7b3a9..587211ff 100644 --- a/libs/services/src/index.ts +++ b/libs/services/src/index.ts @@ -1,25 +1,25 @@ -export * from './SlippageService/SlippageService'; -export * from './SlippageService/SlippageServiceMain'; +export * from './SlippageService/SlippageService' +export * from './SlippageService/SlippageServiceMain' -export * from './UsdService/UsdService'; +export * from './UsdService/UsdService' -export * from './TokenHolderService/TokenHolderService'; -export * from './TokenBalancesService/TokenBalancesService'; +export * from './TokenHolderService/TokenHolderService' +export * from './TokenBalancesService/TokenBalancesService' -export * from './SimulationService/SimulationService'; -export * from './HooksService/HooksService'; -export * from './HooksService/HooksServiceImpl'; +export * from './SimulationService/SimulationService' +export * from './HooksService/HooksService' +export * from './HooksService/HooksServiceImpl' -export * from './SSEService/SSEService'; -export * from './SSEService/SSEServiceMain'; +export * from './SSEService/SSEService' +export * from './SSEService/SSEServiceMain' -export * from './BalanceTrackingService/BalanceTrackingService'; -export * from './BalanceTrackingService/BalanceTrackingServiceMain'; +export * from './BalanceTrackingService/BalanceTrackingService' +export * from './BalanceTrackingService/BalanceTrackingServiceMain' -export * from './AffiliateStatsService/AffiliateStatsService'; -export * from './AffiliateStatsService/AffiliateStatsServiceImpl'; -export * from './AffiliateProgramExportService/AffiliateProgramExportService'; -export * from './AffiliateProgramExportService/AffiliateProgramExportServiceImpl'; -export * from './TokenDetailService/TokenDetailService'; +export * from './AffiliateStatsService/AffiliateStatsService' +export * from './AffiliateStatsService/AffiliateStatsServiceImpl' +export * from './AffiliateProgramExportService/AffiliateProgramExportService' +export * from './AffiliateProgramExportService/AffiliateProgramExportServiceImpl' +export * from './TokenDetailService/TokenDetailService' -export * from './factories'; +export * from './factories' diff --git a/libs/services/src/utils/type-checking-utils.ts b/libs/services/src/utils/type-checking-utils.ts index 394f6fab..a789fe1c 100644 --- a/libs/services/src/utils/type-checking-utils.ts +++ b/libs/services/src/utils/type-checking-utils.ts @@ -1,28 +1,28 @@ export function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null; + return typeof value === 'object' && value !== null } export function isString(value: unknown): value is string { - return typeof value === 'string'; + return typeof value === 'string' } export function isNumeric(value: unknown): value is number | string { if (typeof value === 'number') { - return !Number.isNaN(value); + return !Number.isNaN(value) } if (typeof value === 'string') { - return value.trim().length > 0 && !Number.isNaN(Number(value)); + return value.trim().length > 0 && !Number.isNaN(Number(value)) } - return false; + return false } export function toNumber(value: number | string, field: string): number { - const parsed = Number(value); + const parsed = Number(value) if (Number.isNaN(parsed)) { - throw new Error(`Invalid numeric value for ${field}: ${value}`); + throw new Error(`Invalid numeric value for ${field}: ${value}`) } - return parsed; + return parsed } diff --git a/libs/shared/src/const.ts b/libs/shared/src/const.ts index 2cad606d..e24306af 100644 --- a/libs/shared/src/const.ts +++ b/libs/shared/src/const.ts @@ -3,30 +3,28 @@ import { ALL_SUPPORTED_CHAINS, SupportedChainId, WRAPPED_NATIVE_CURRENCIES, -} from '@cowprotocol/cow-sdk'; -import { Address } from 'viem'; +} from '@cowprotocol/cow-sdk' +import { Address } from 'viem' /** * Native currency address. For example, represents Ether in Mainnet and Arbitrum, and xDAI in Gnosis chain. */ -export const NativeCurrencyAddress = - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; +export const NativeCurrencyAddress = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' /** * Wrapped native token address. For example, represents WETH in Mainnet and Arbitrum, and wxDAI in Gnosis chain. */ -export const WrappedNativeTokenAddress: Record = - Object.values(WRAPPED_NATIVE_CURRENCIES).reduce((acc, curr) => { - acc[curr.chainId as SupportedChainId] = curr.address as Address; - return acc; - }, {} as Record); - -export const ChainNames: Record = Object.values( - ALL_SUPPORTED_CHAINS +export const WrappedNativeTokenAddress: Record = Object.values( + WRAPPED_NATIVE_CURRENCIES ).reduce((acc, curr) => { - acc[curr.id as SupportedChainId] = curr.label; - return acc; -}, {} as Record); + acc[curr.chainId as SupportedChainId] = curr.address as Address + return acc +}, {} as Record) + +export const ChainNames: Record = Object.values(ALL_SUPPORTED_CHAINS).reduce((acc, curr) => { + acc[curr.id as SupportedChainId] = curr.label + return acc +}, {} as Record) // TODO: Get from SDK export const EXPLORER_NETWORK_NAMES = { @@ -41,7 +39,7 @@ export const EXPLORER_NETWORK_NAMES = { [SupportedChainId.PLASMA]: 'plasma', [SupportedChainId.INK]: 'ink', [SupportedChainId.SEPOLIA]: 'sepolia', -} as const satisfies Record; +} as const satisfies Record // TODO: Get from SDK export const COW_API_NETWORK_NAMES = { @@ -56,6 +54,6 @@ export const COW_API_NETWORK_NAMES = { [SupportedChainId.PLASMA]: 'plasma', [SupportedChainId.INK]: 'ink', [SupportedChainId.SEPOLIA]: 'sepolia', -} as const satisfies Record; +} as const satisfies Record -export const AllChainIds: SupportedChainId[] = ALL_SUPPORTED_CHAIN_IDS; +export const AllChainIds: SupportedChainId[] = ALL_SUPPORTED_CHAIN_IDS diff --git a/libs/shared/src/index.ts b/libs/shared/src/index.ts index 17f67efc..98b791f2 100644 --- a/libs/shared/src/index.ts +++ b/libs/shared/src/index.ts @@ -1,10 +1,10 @@ -export * from './types'; -export * from './const'; -export * from './transformers'; -export * from './logger'; -export * from './types'; +export * from './types' +export * from './const' +export * from './transformers' +export * from './logger' +export * from './types' -export * from './utils/misc'; -export * from './utils/addresses'; -export * from './utils/format'; -export * from './utils/doForever'; +export * from './utils/misc' +export * from './utils/addresses' +export * from './utils/format' +export * from './utils/doForever' diff --git a/libs/shared/src/logger.ts b/libs/shared/src/logger.ts index 42a0fe3f..d0d798a4 100644 --- a/libs/shared/src/logger.ts +++ b/libs/shared/src/logger.ts @@ -1,3 +1,3 @@ -import { createLogger } from './utils/logger'; +import { createLogger } from './utils/logger' -export const logger = createLogger(); +export const logger = createLogger() diff --git a/libs/shared/src/transformers.ts b/libs/shared/src/transformers.ts index 85c956f3..9fe1d437 100644 --- a/libs/shared/src/transformers.ts +++ b/libs/shared/src/transformers.ts @@ -1,15 +1,15 @@ export function stringToBigInt(value: string): bigint { - return BigInt(value); + return BigInt(value) } export function bigIntToString(value: bigint): string { - return value.toString(); + return value.toString() } export function bufferToString(buffer: Buffer): string { - return `0x${buffer.toString('hex')}`; + return `0x${buffer.toString('hex')}` } export function stringToBuffer(value: string): Buffer { - return Buffer.from(value.slice(2), 'hex'); + return Buffer.from(value.slice(2), 'hex') } diff --git a/libs/shared/src/types.ts b/libs/shared/src/types.ts index fc6723b7..bd1fd991 100644 --- a/libs/shared/src/types.ts +++ b/libs/shared/src/types.ts @@ -1,3 +1,3 @@ -import pino from 'pino'; +import pino from 'pino' -export type Logger = pino.Logger; +export type Logger = pino.Logger diff --git a/libs/shared/src/utils/addresses.spec.ts b/libs/shared/src/utils/addresses.spec.ts index 182b2dbc..cb856790 100644 --- a/libs/shared/src/utils/addresses.spec.ts +++ b/libs/shared/src/utils/addresses.spec.ts @@ -1,4 +1,4 @@ -import { parseEthereumAddress, parseEthereumAddressList } from './addresses'; +import { parseEthereumAddress, parseEthereumAddressList } from './addresses' describe('parseEthereumAddressList', () => { it('returns unique checksummed addresses from a comma list', () => { @@ -6,43 +6,34 @@ describe('parseEthereumAddressList', () => { ' 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0x0000000000000000000000000000000000000000 ', - ]; + ] - const result = parseEthereumAddressList(input); + const result = parseEthereumAddressList(input) - expect(result).toEqual([ - '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', - '0x0000000000000000000000000000000000000000', - ]); - }); + expect(result).toEqual(['0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0x0000000000000000000000000000000000000000']) + }) it('ignores empty items and trims whitespace', () => { - const input = [' ', '', '0x0000000000000000000000000000000000000000']; + const input = [' ', '', '0x0000000000000000000000000000000000000000'] - const result = parseEthereumAddressList(input); + const result = parseEthereumAddressList(input) - expect(result).toEqual(['0x0000000000000000000000000000000000000000']); - }); + expect(result).toEqual(['0x0000000000000000000000000000000000000000']) + }) it('throws on invalid addresses', () => { - expect(() => parseEthereumAddressList(['0x123'])).toThrow( - 'Invalid Ethereum address: 0x123' - ); - }); -}); + expect(() => parseEthereumAddressList(['0x123'])).toThrow('Invalid Ethereum address: 0x123') + }) +}) describe('parseEthereumAddress', () => { it('returns the checksummed address', () => { - const result = parseEthereumAddress( - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' - ); + const result = parseEthereumAddress('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') - expect(result).toBe('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); - }); + expect(result).toBe('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2') + }) it('throws on invalid addresses', () => { - expect(() => parseEthereumAddress('0x123')).toThrow( - 'Invalid Ethereum address: 0x123' - ); - }); -}); + expect(() => parseEthereumAddress('0x123')).toThrow('Invalid Ethereum address: 0x123') + }) +}) diff --git a/libs/shared/src/utils/addresses.ts b/libs/shared/src/utils/addresses.ts index bbef5b3b..2dc2cf73 100644 --- a/libs/shared/src/utils/addresses.ts +++ b/libs/shared/src/utils/addresses.ts @@ -1,24 +1,24 @@ -import { Address, getAddress, isAddress } from 'viem'; +import { Address, getAddress, isAddress } from 'viem' export function parseEthereumAddressList(values: string[]): Address[] { - const unique = new Set

(); + const unique = new Set
() for (const item of values) { if (!item.trim()) { - continue; + continue } - unique.add(parseEthereumAddress(item)); + unique.add(parseEthereumAddress(item)) } - return Array.from(unique); + return Array.from(unique) } export function parseEthereumAddress(value: string): Address { - const trimmed = value.trim(); + const trimmed = value.trim() if (!isAddress(trimmed)) { - throw new Error(`Invalid Ethereum address: ${trimmed}`); + throw new Error(`Invalid Ethereum address: ${trimmed}`) } - return getAddress(trimmed); + return getAddress(trimmed) } diff --git a/libs/shared/src/utils/doForever.ts b/libs/shared/src/utils/doForever.ts index 32826243..4517258f 100644 --- a/libs/shared/src/utils/doForever.ts +++ b/libs/shared/src/utils/doForever.ts @@ -1,62 +1,58 @@ -import { Logger } from '../types'; -import { sleep } from './misc'; +import { Logger } from '../types' +import { sleep } from './misc' export async function doForever(params: { - name: string; - callback: (stop: () => void) => Promise; - waitTimeMilliseconds: number; - logger: Logger; + name: string + callback: (stop: () => void) => Promise + waitTimeMilliseconds: number + logger: Logger }) { - const { name, callback, waitTimeMilliseconds, logger } = params; + const { name, callback, waitTimeMilliseconds, logger } = params - logger.info( - `[${params.name}] Starting. Running logic every ${ - waitTimeMilliseconds / 1000 - }s` - ); + logger.info(`[${params.name}] Starting. Running logic every ${waitTimeMilliseconds / 1000}s`) // eslint-disable-next-line no-constant-condition - let running = true; + let running = true - const { wakeUpPromise, wakeUp } = createWakeUpPromise(); + const { wakeUpPromise, wakeUp } = createWakeUpPromise() while (running) { const stop = () => { - logger.info(`[${name}] Stopping...`); - wakeUp(); // Wake up if its sleeping (so it can end faster) - running = false; - }; + logger.info(`[${name}] Stopping...`) + wakeUp() // Wake up if its sleeping (so it can end faster) + running = false + } try { - await callback(stop); + await callback(stop) } catch (error) { - const errorName = error instanceof Error ? `: ${error.name}` : ''; - logger.error(error, `[${name}] Error${errorName}`); - logger.info(`[${name}] Next-run in ${waitTimeMilliseconds / 1000}s...`); + const errorName = error instanceof Error ? `: ${error.name}` : '' + logger.error(error, `[${name}] Error${errorName}`) + logger.info(`[${name}] Next-run in ${waitTimeMilliseconds / 1000}s...`) } finally { - await Promise.race([sleep(waitTimeMilliseconds), wakeUpPromise]); + await Promise.race([sleep(waitTimeMilliseconds), wakeUpPromise]) } } - logger.info(`[${name}] Stopped`); + logger.info(`[${name}] Stopped`) } function createWakeUpPromise(): { - wakeUpPromise: Promise; - wakeUp: () => void; + wakeUpPromise: Promise + wakeUp: () => void } { - let wakeUpResolve: ((value: unknown) => void) | undefined = undefined; + let wakeUpResolve: ((value: unknown) => void) | undefined = undefined const wakeUpPromise = new Promise((resolve) => { - wakeUpResolve = resolve; - }); + wakeUpResolve = resolve + }) return { wakeUpPromise, wakeUp: () => { if (wakeUpResolve) { - wakeUpResolve(undefined); + wakeUpResolve(undefined) } else { - console.warn('WakeUp promise not initialized. Nothing to wake up.'); + console.warn('WakeUp promise not initialized. Nothing to wake up.') } }, - }; + } } diff --git a/libs/shared/src/utils/format.ts b/libs/shared/src/utils/format.ts index a18eda66..74a6eaf8 100644 --- a/libs/shared/src/utils/format.ts +++ b/libs/shared/src/utils/format.ts @@ -1,27 +1,22 @@ -import { formatUnits } from 'viem'; +import { formatUnits } from 'viem' -import { EXPLORER_NETWORK_NAMES } from '../const'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { EXPLORER_NETWORK_NAMES } from '../const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' export function getExplorerUrl(chainId: SupportedChainId, orderUid: string) { - const baseUrl = getExplorerBaseUrl(chainId); - return `${baseUrl}/orders/${orderUid}`; + const baseUrl = getExplorerBaseUrl(chainId) + return `${baseUrl}/orders/${orderUid}` } export function getExplorerBaseUrl(chainId: SupportedChainId) { - const suffix = - chainId === SupportedChainId.MAINNET - ? '' - : `/${EXPLORER_NETWORK_NAMES[chainId]}`; - return `https://explorer.cow.fi${suffix}`; + const suffix = chainId === SupportedChainId.MAINNET ? '' : `/${EXPLORER_NETWORK_NAMES[chainId]}` + return `https://explorer.cow.fi${suffix}` } export function formatAmount(amount: bigint, decimals: number | undefined) { - return decimals ? formatUnits(amount, decimals) : amount.toString(); + return decimals ? formatUnits(amount, decimals) : amount.toString() } -export function formatTokenName( - token: { symbol?: string; address: string } | null -) { - return token?.symbol ? `${token.symbol}` : token?.address; +export function formatTokenName(token: { symbol?: string; address: string } | null) { + return token?.symbol ? `${token.symbol}` : token?.address } diff --git a/libs/shared/src/utils/logger.ts b/libs/shared/src/utils/logger.ts index 820b9372..029dc1ef 100644 --- a/libs/shared/src/utils/logger.ts +++ b/libs/shared/src/utils/logger.ts @@ -1,11 +1,11 @@ -import pino from 'pino'; +import pino from 'pino' export function createLogger() { // Uses pretty print if env.LOG_FORMAT is set to 'pretty'. By default, it will also use it for non-production environments. // If the env.LOG_FORMAT is not 'pretty', it defaults to a JSON logger. const usePrettyPrint = process.env.LOG_FORMAT ? process.env.LOG_FORMAT === 'pretty' - : process.env.NODE_ENV !== 'production'; + : process.env.NODE_ENV !== 'production' const loggerConfigEnv = usePrettyPrint ? { @@ -13,10 +13,10 @@ export function createLogger() { target: 'pino-pretty', }, } - : {}; + : {} return pino({ ...loggerConfigEnv, level: process.env.LOG_LEVEL ?? 'info', - }); + }) } diff --git a/libs/shared/src/utils/misc.ts b/libs/shared/src/utils/misc.ts index 41b27a22..83779bde 100644 --- a/libs/shared/src/utils/misc.ts +++ b/libs/shared/src/utils/misc.ts @@ -1,71 +1,58 @@ -import { Address, getAddress } from 'viem'; +import { Address, getAddress } from 'viem' -import { - AllChainIds, - NativeCurrencyAddress, - WrappedNativeTokenAddress, -} from '../const'; -import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { AllChainIds, NativeCurrencyAddress, WrappedNativeTokenAddress } from '../const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' /** * Returns the token address. This function will throw if the address passed is not an Ethereum address. * It will also convert the address representing the native currency (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) into * its wrapped version. */ -export function toTokenAddress( - address: string, - chainId: SupportedChainId -): Address { +export function toTokenAddress(address: string, chainId: SupportedChainId): Address { if (address.toLocaleLowerCase() === NativeCurrencyAddress) { - return WrappedNativeTokenAddress[chainId]; + return WrappedNativeTokenAddress[chainId] } - return getAddress(address.toLowerCase()); + return getAddress(address.toLowerCase()) } export function isSupportedChain(chain: number): chain is SupportedChainId { - return AllChainIds.includes(chain as SupportedChainId); + return AllChainIds.includes(chain as SupportedChainId) } export function toSupportedChainId(chain: string | number): SupportedChainId { if (typeof chain === 'string') { - chain = parseInt(chain); + chain = parseInt(chain) } if (!isSupportedChain(chain)) { - throw new Error( - `Unsupported chain ID: ${chain}. Supported chains are: ${AllChainIds.join( - ', ' - )}` - ); + throw new Error(`Unsupported chain ID: ${chain}. Supported chains are: ${AllChainIds.join(', ')}`) } - return chain; + return chain } export function bigIntReplacer(key: string, value: any): any { if (typeof value === 'bigint') { - return value.toString() + 'n'; + return value.toString() + 'n' } - return value; + return value } export function bigIntReviver(key: string, value: any): any { if (typeof value === 'string' && /^\d+n$/.test(value)) { - return BigInt(value.slice(0, -1)); + return BigInt(value.slice(0, -1)) } - return value; + return value } export function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)) } export function ensureEnvs(envs: string[]) { - const missingEnvs = envs.filter((env) => !process.env[env]); + const missingEnvs = envs.filter((env) => !process.env[env]) if (missingEnvs.length > 0) { - throw new Error( - `Missing required environment variables: ${missingEnvs.join(', ')}` - ); + throw new Error(`Missing required environment variables: ${missingEnvs.join(', ')}`) } }