Skip to content

Commit

Permalink
Implement multi-chain support for cow api (#74)
Browse files Browse the repository at this point in the history
* Implement multi-chain support for cow api

* Improve name of the const

Co-authored-by: Leandro <[email protected]>

---------

Co-authored-by: Leandro <[email protected]>
  • Loading branch information
anxolin and alfetopito authored Jul 30, 2024
1 parent ffab2bb commit ea9d2c8
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 40 deletions.
3 changes: 2 additions & 1 deletion apps/api/src/app/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
UsdRepositoryCow,
UsdRepositoryFallback,
cacheRepositorySymbol,
cowApiClients,
erc20RepositorySymbol,
redisClient,
usdRepositorySymbol,
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 6 additions & 15 deletions libs/repositories/src/Erc20Repository/Erc20RepositoryViem.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,7 +16,6 @@ const mockPublicClient: PublicClient = {
export default mockPublicClient;

describe('Erc20RepositoryViem', () => {
let viemClients: Record<SupportedChainId, PublicClient>;
let erc20RepositoryViem: Erc20RepositoryViem;

const chainId = SupportedChainId.MAINNET;
Expand All @@ -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);
});

Expand Down
43 changes: 26 additions & 17 deletions libs/repositories/src/UsdRepository/UsdRepositoryCow.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SupportedChainId } from '../types';
import { UsdRepositoryCow } from './UsdRepositoryCow';
import { cowApiClientMainnet } from '../datasources/cowApi';

import {
DEFINITELY_NOT_A_TOKEN,
Expand All @@ -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<CowApiClient>;

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
Expand All @@ -32,15 +36,24 @@ const mockErc20Repository = {
},
} as jest.Mocked<Erc20Repository>;

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:
Expand All @@ -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,
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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",
Expand Down
12 changes: 8 additions & 4 deletions libs/repositories/src/UsdRepository/UsdRepositoryCow.ts
Original file line number Diff line number Diff line change
@@ -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<SupportedChainId, CowApiClient>,
private erc20Repository: Erc20Repository
) {
super();
}

Expand Down Expand Up @@ -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,
Expand Down
22 changes: 19 additions & 3 deletions libs/repositories/src/datasources/cowApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,23 @@ 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<paths>({
baseUrl: COW_API_BASE_URL + '/mainnet',
});
export type CowApiClient = ReturnType<typeof createClient<paths>>;

const COW_API_NETWORK_NAMES: Record<SupportedChainId, string> = {
[SupportedChainId.MAINNET]: 'mainnet',
[SupportedChainId.GNOSIS_CHAIN]: 'xdai',
[SupportedChainId.ARBITRUM_ONE]: 'arbitrum_one',
[SupportedChainId.SEPOLIA]: 'sepolia',
};

export const cowApiClients = ALL_CHAIN_IDS.reduce<
Record<SupportedChainId, CowApiClient>
>((acc, chainId) => {
acc[chainId] = createClient<paths>({
baseUrl: COW_API_BASE_URL + '/' + COW_API_NETWORK_NAMES[chainId],
});

return acc;
}, {} as Record<SupportedChainId, CowApiClient>);
1 change: 1 addition & 0 deletions libs/repositories/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down

0 comments on commit ea9d2c8

Please sign in to comment.