From 89aca9170efd2b693975b6bf88a1dbb530138ae6 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 30 Jul 2024 14:07:50 +0100 Subject: [PATCH] Implement multi-chain support for cow api --- apps/api/src/app/inversify.config.ts | 3 +- .../Erc20RepositoryViem.spec.ts | 21 +++------ .../UsdRepository/UsdRepositoryCow.spec.ts | 43 +++++++++++-------- .../src/UsdRepository/UsdRepositoryCow.ts | 12 ++++-- libs/repositories/src/datasources/cowApi.ts | 25 +++++++++-- libs/repositories/src/index.ts | 1 + 6 files changed, 65 insertions(+), 40 deletions(-) diff --git a/apps/api/src/app/inversify.config.ts b/apps/api/src/app/inversify.config.ts index 5e2ed81..7ee33f3 100644 --- a/apps/api/src/app/inversify.config.ts +++ b/apps/api/src/app/inversify.config.ts @@ -11,6 +11,7 @@ import { UsdRepositoryCow, UsdRepositoryFallback, cacheRepositorySymbol, + cowApiClients, erc20RepositorySymbol, redisClient, usdRepositorySymbol, @@ -55,7 +56,7 @@ function getUsdRepositoryCow( erc20Repository: Erc20Repository ): UsdRepository { return new UsdRepositoryCache( - new UsdRepositoryCow(erc20Repository), + new UsdRepositoryCow(cowApiClients, erc20Repository), cacheRepository, 'usdCow', DEFAULT_CACHE_VALUE_SECONDS, diff --git a/libs/repositories/src/Erc20Repository/Erc20RepositoryViem.spec.ts b/libs/repositories/src/Erc20Repository/Erc20RepositoryViem.spec.ts index 91b35ff..f996a5a 100644 --- a/libs/repositories/src/Erc20Repository/Erc20RepositoryViem.spec.ts +++ b/libs/repositories/src/Erc20Repository/Erc20RepositoryViem.spec.ts @@ -4,14 +4,6 @@ import { PublicClient } from 'viem'; import { erc20Abi } from 'viem'; import { Erc20 } from './Erc20Repository'; -jest.mock('viem', () => ({ - ...jest.requireActual('viem'), - PublicClient: jest.fn().mockImplementation(() => ({ - getCode: jest.fn(), - multicall: jest.fn(), - })), -})); - const multicallMock = jest.fn(); // Mock implementation for PublicClient @@ -24,7 +16,6 @@ const mockPublicClient: PublicClient = { export default mockPublicClient; describe('Erc20RepositoryViem', () => { - let viemClients: Record; let erc20RepositoryViem: Erc20RepositoryViem; const chainId = SupportedChainId.MAINNET; @@ -44,14 +35,14 @@ describe('Erc20RepositoryViem', () => { { address: tokenAddress, abi: erc20Abi, functionName: 'decimals' }, ], }; + const viemClients = { + [SupportedChainId.MAINNET]: mockPublicClient, + [SupportedChainId.GNOSIS_CHAIN]: mockPublicClient, + [SupportedChainId.ARBITRUM_ONE]: mockPublicClient, + [SupportedChainId.SEPOLIA]: mockPublicClient, + }; beforeEach(() => { - viemClients = { - [SupportedChainId.MAINNET]: mockPublicClient, - [SupportedChainId.GNOSIS_CHAIN]: mockPublicClient, - [SupportedChainId.ARBITRUM_ONE]: mockPublicClient, - [SupportedChainId.SEPOLIA]: mockPublicClient, - }; erc20RepositoryViem = new Erc20RepositoryViem(viemClients); }); diff --git a/libs/repositories/src/UsdRepository/UsdRepositoryCow.spec.ts b/libs/repositories/src/UsdRepository/UsdRepositoryCow.spec.ts index 4ac403d..8ba6c61 100644 --- a/libs/repositories/src/UsdRepository/UsdRepositoryCow.spec.ts +++ b/libs/repositories/src/UsdRepository/UsdRepositoryCow.spec.ts @@ -1,6 +1,5 @@ import { SupportedChainId } from '../types'; import { UsdRepositoryCow } from './UsdRepositoryCow'; -import { cowApiClientMainnet } from '../datasources/cowApi'; import { DEFINITELY_NOT_A_TOKEN, @@ -10,10 +9,15 @@ import { } from '../../test/mock'; import { USDC } from '../const'; import { Erc20Repository, Erc20 } from '../Erc20Repository/Erc20Repository'; +import { CowApiClient } from '../datasources/cowApi'; -function getTokenDecimalsMock(tokenAddress: string) { - return tokenAddress === WETH ? 18 : 6; -} +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; 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 @@ -32,15 +36,24 @@ const mockErc20Repository = { }, } as jest.Mocked; -const usdRepositoryCow = new UsdRepositoryCow(mockErc20Repository); +const cowApiClients = { + [SupportedChainId.MAINNET]: mockApi, + [SupportedChainId.GNOSIS_CHAIN]: mockApi, + [SupportedChainId.ARBITRUM_ONE]: mockApi, + [SupportedChainId.SEPOLIA]: mockApi, +}; +const usdRepositoryCow = new UsdRepositoryCow( + cowApiClients, + mockErc20Repository +); -const cowApiMock = jest.spyOn(cowApiClientMainnet, 'GET'); +// const cowApiMock = jest.spyOn(cowApiClientMainnet, 'GET'); describe('UsdRepositoryCow', () => { describe('getUsdPrice', () => { it('USD price calculation is correct', async () => { // Mock native price - cowApiMock.mockImplementation(async (url, params) => { + mockApiGet.mockImplementation(async (url, params) => { const token = (params as any).params.path.token || undefined; switch (token) { case WETH: @@ -66,8 +79,8 @@ describe('UsdRepositoryCow', () => { ); // Assert that the implementation did the right calls to the API - expect(cowApiMock).toHaveBeenCalledTimes(2); - expect(cowApiMock.mock.calls).toEqual([ + expect(mockApiGet).toHaveBeenCalledTimes(2); + expect(mockApiGet.mock.calls).toEqual([ [NATIVE_PRICE_ENDPOINT, { params: { path: { token: WETH } } }], [ NATIVE_PRICE_ENDPOINT, @@ -82,8 +95,7 @@ describe('UsdRepositoryCow', () => { }); it('Handles UnsupportedToken(400) errors', async () => { // Mock native price - const cowApiGet = jest.spyOn(cowApiClientMainnet, 'GET'); - cowApiGet.mockReturnValue( + mockApiGet.mockReturnValue( errorResponse({ status: 400, statusText: 'Bad Request', @@ -106,8 +118,7 @@ describe('UsdRepositoryCow', () => { it('Handles NewErrorTypeWeDontHandleYet(400) errors', async () => { // Mock native price - const cowApiGet = jest.spyOn(cowApiClientMainnet, 'GET'); - cowApiGet.mockReturnValue( + mockApiGet.mockReturnValue( errorResponse({ status: 400, statusText: 'Bad Request', @@ -133,8 +144,7 @@ describe('UsdRepositoryCow', () => { it('Handles NotFound(404) errors', async () => { // Mock native price - const cowApiGet = jest.spyOn(cowApiClientMainnet, 'GET'); - cowApiGet.mockReturnValue( + mockApiGet.mockReturnValue( errorResponse({ status: 404, statusText: 'Not Found', @@ -154,8 +164,7 @@ describe('UsdRepositoryCow', () => { it('Handles un-expected errors (I_AM_A_TEA_POT)', async () => { // Mock native price - const cowApiGet = jest.spyOn(cowApiClientMainnet, 'GET'); - cowApiGet.mockReturnValue( + mockApiGet.mockReturnValue( errorResponse({ status: 418, statusText: "I'm a teapot", diff --git a/libs/repositories/src/UsdRepository/UsdRepositoryCow.ts b/libs/repositories/src/UsdRepository/UsdRepositoryCow.ts index 35bb0fa..482aa0b 100644 --- a/libs/repositories/src/UsdRepository/UsdRepositoryCow.ts +++ b/libs/repositories/src/UsdRepository/UsdRepositoryCow.ts @@ -1,15 +1,18 @@ import { injectable } from 'inversify'; import { UsdRepositoryNoop } from './UsdRepository'; -import { cowApiClientMainnet } from '../datasources/cowApi'; import { OneBigNumber, TenBigNumber, USDC, ZeroBigNumber } from '../const'; import { SupportedChainId } from '../types'; import { BigNumber } from 'bignumber.js'; import { throwIfUnsuccessful } from '../utils/throwIfUnsuccessful'; import { Erc20Repository } from '../Erc20Repository/Erc20Repository'; +import { CowApiClient } from '../datasources/cowApi'; @injectable() export class UsdRepositoryCow extends UsdRepositoryNoop { - constructor(private erc20Repository: Erc20Repository) { + constructor( + private cowApiClients: Record, + private erc20Repository: Erc20Repository + ) { super(); } @@ -52,14 +55,15 @@ export class UsdRepositoryCow extends UsdRepositoryNoop { } private async getNativePrice( - _chainId: SupportedChainId, + chainId: SupportedChainId, tokenAddress: string ) { + const cowApiClient = this.cowApiClients[chainId]; const { data: priceResult = {}, response, error, - } = await cowApiClientMainnet.GET('/api/v1/token/{token}/native_price', { + } = await cowApiClient.GET('/api/v1/token/{token}/native_price', { params: { path: { token: tokenAddress, diff --git a/libs/repositories/src/datasources/cowApi.ts b/libs/repositories/src/datasources/cowApi.ts index ced0770..dac3fdc 100644 --- a/libs/repositories/src/datasources/cowApi.ts +++ b/libs/repositories/src/datasources/cowApi.ts @@ -3,7 +3,26 @@ import createClient from 'openapi-fetch'; const COW_API_BASE_URL = process.env.COW_API_BASE_URL || 'https://api.cow.fi'; import type { paths } from '../gen/cow/cow-api-types'; +import { ALL_CHAIN_IDS, SupportedChainId } from '../types'; -export const cowApiClientMainnet = createClient({ - baseUrl: COW_API_BASE_URL + '/mainnet', -}); +export type CowApiClient = ReturnType>; + +/** + * Name of the networks as they are in the API + */ +const NETWORK_NAMES: Record = { + [SupportedChainId.MAINNET]: 'mainnet', + [SupportedChainId.GNOSIS_CHAIN]: 'xdai', + [SupportedChainId.ARBITRUM_ONE]: 'arbitrum_one', + [SupportedChainId.SEPOLIA]: 'sepolia', +}; + +export const cowApiClients = ALL_CHAIN_IDS.reduce< + Record +>((acc, chainId) => { + acc[chainId] = createClient({ + baseUrl: COW_API_BASE_URL + '/' + NETWORK_NAMES[chainId], + }); + + return acc; +}, {} as Record); diff --git a/libs/repositories/src/index.ts b/libs/repositories/src/index.ts index 6a334a8..ceb9f59 100644 --- a/libs/repositories/src/index.ts +++ b/libs/repositories/src/index.ts @@ -1,6 +1,7 @@ export * from './types'; export * from './datasources/redis'; export * from './datasources/viem'; +export * from './datasources/cowApi'; // Data sources export { COINGECKO_PRO_BASE_URL } from './datasources/coingecko';