Skip to content

Commit

Permalink
Create cow repository for USD estimation based on native price (+test…
Browse files Browse the repository at this point in the history
…s) (#56)

* Create cow API USD estimation

* Add missing type

* Fix broken tests

* Fix repo implementation

* Fix service

* Implement mocking of native price and basic repository test

* Fix test

* Handle unsupported tokens

* chore: test usd repository for nartive cow price

* Rename test to spec

* Delete comment

* Fix compile issue

* Rename throwIfUnsuccessful
  • Loading branch information
anxolin authored Jul 22, 2024
1 parent 5542d56 commit 0e3969b
Show file tree
Hide file tree
Showing 28 changed files with 500 additions and 34 deletions.
1 change: 1 addition & 0 deletions apps/api-e2e/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/api-e2e',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
1 change: 1 addition & 0 deletions apps/api/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/packages/api',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
3 changes: 2 additions & 1 deletion apps/notification-producer-e2e/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export default {
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../..//coverage/notification-producer-e2e',
coverageDirectory: '../../coverage/notification-producer-e2e',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
1 change: 1 addition & 0 deletions apps/notification-producer/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/notification-producer',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
1 change: 1 addition & 0 deletions apps/telegram-e2e/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../..//coverage/telegram-e2e',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
1 change: 1 addition & 0 deletions apps/telegram/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/telegram',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
1 change: 1 addition & 0 deletions apps/twap-e2e/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/twap-e2e',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
1 change: 1 addition & 0 deletions apps/twap/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/twap',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
};
1 change: 1 addition & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import 'reflect-metadata';
1 change: 1 addition & 0 deletions libs/cms-api/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/libs/cms-api',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
1 change: 1 addition & 0 deletions libs/notifications/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/libs/notifications',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
1 change: 1 addition & 0 deletions libs/repositories/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/libs/repositories',
setupFilesAfterEnv: ['../../jest.setup.ts'],
};
25 changes: 23 additions & 2 deletions libs/repositories/src/UsdRepository/UsdRepository.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SupportedChainId } from '../types';

export const usdRepositorySymbol = Symbol.for('UsdRepository');

export type PriceStrategy = '5m' | 'hourly' | 'daily';
Expand All @@ -20,11 +22,30 @@ export interface PricePoint {
}

export interface UsdRepository {
getUsdPrice(chainId: number, tokenAddress: string): Promise<number | null>;
getUsdPrice(
chainId: SupportedChainId,
tokenAddress: string
): Promise<number | null>;

getUsdPrices(
chainId: number,
chainId: SupportedChainId,
tokenAddress: string,
priceStrategy: PriceStrategy
): Promise<PricePoint[] | null>;
}

export class UsdRepositoryNoop implements UsdRepository {
getUsdPrice(
chainId: SupportedChainId,
tokenAddress: string
): Promise<number> {
return null;
}
getUsdPrices(
chainId: SupportedChainId,
tokenAddress: string,
priceStrategy: PriceStrategy
): Promise<PricePoint[] | null> {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ms from 'ms';
const FIVE_MINUTES = ms('5m');
const ONE_HOUR = ms('1h');
const ONE_DAY = ms('1d');
const BUFFER_ERROR_TOLERANCE = 1.5; // 50% error tolerance

describe('UsdRepositoryCoingecko', () => {
let usdRepositoryCoingecko: UsdRepositoryCoingecko;
Expand Down Expand Up @@ -62,7 +63,7 @@ describe('UsdRepositoryCoingecko', () => {
Math.abs(
price.date.getTime() - previousPrice.date.getTime() - FIVE_MINUTES
)
).toBeLessThanOrEqual(FIVE_MINUTES); // 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)
).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);
}
});
Expand Down Expand Up @@ -98,7 +99,7 @@ describe('UsdRepositoryCoingecko', () => {
Math.abs(
price.date.getTime() - previousPrice.date.getTime() - ONE_HOUR
)
).toBeLessThanOrEqual(ONE_HOUR); // 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)
).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);
}
});
Expand All @@ -123,7 +124,7 @@ describe('UsdRepositoryCoingecko', () => {
Math.abs(
price.date.getTime() - previousPrice.date.getTime() - ONE_DAY
)
).toBeLessThanOrEqual(ONE_DAY); // 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)
).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);
}
});
Expand Down
40 changes: 23 additions & 17 deletions libs/repositories/src/UsdRepository/UsdRepositoryCoingecko.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'reflect-metadata';

import { injectable } from 'inversify';
import { PricePoint, PriceStrategy, UsdRepository } from './UsdRepository';
import { COINGECKO_PLATFORMS, coingeckoProClient } from '../coingecko';
import { SupportedChainId } from '../types';
import { throwIfUnsuccessful } from '../utils/throwIfUnsuccessful';

/**
* Number of days of data to fetch for each price strategy
Expand All @@ -22,7 +22,7 @@ const DAYS_PER_PRICE_STRATEGY: Record<PriceStrategy, number> = {
@injectable()
export class UsdRepositoryCoingecko implements UsdRepository {
async getUsdPrice(
chainId: number,
chainId: SupportedChainId,
tokenAddress: string
): Promise<number | null> {
const platform = COINGECKO_PLATFORMS[chainId];
Expand All @@ -32,9 +32,8 @@ export class UsdRepositoryCoingecko implements UsdRepository {

const tokenAddressLower = tokenAddress.toLowerCase();

// Get prices: See https://docs.coingecko.com/reference/contract-address-market-chart
// Get prices. See https://docs.coingecko.com/reference/coins-id-market-chart
const fetchResponse = await coingeckoProClient.GET(
// Get USD price: https://docs.coingecko.com/reference/simple-token-price
const { data: priceData, response } = await coingeckoProClient.GET(
`/simple/token_price/{id}`,
{
params: {
Expand All @@ -49,20 +48,23 @@ export class UsdRepositoryCoingecko implements UsdRepository {
}
);

if (fetchResponse.error) {
throw fetchResponse.error;
}
const priceData = fetchResponse.data;

if (!priceData[tokenAddressLower] || !priceData[tokenAddressLower].usd) {
if (
response.status === 404 ||
!priceData[tokenAddressLower] ||
!priceData[tokenAddressLower].usd
) {
return null;
}
await throwIfUnsuccessful(
'Error getting USD price from Coingecko',
response
);

return priceData[tokenAddressLower].usd;
}

async getUsdPrices(
chainId: number,
chainId: SupportedChainId,
tokenAddress: string,
priceStrategy: PriceStrategy
): Promise<PricePoint[] | null> {
Expand All @@ -75,7 +77,7 @@ export class UsdRepositoryCoingecko implements UsdRepository {
const days = DAYS_PER_PRICE_STRATEGY[priceStrategy].toString();

// Get prices: See https://docs.coingecko.com/reference/contract-address-market-chart
const fetchResponses = await coingeckoProClient.GET(
const { data: priceData, response } = await coingeckoProClient.GET(
`/coins/{id}/contract/{contract_address}/market_chart`,
{
params: {
Expand All @@ -92,17 +94,21 @@ export class UsdRepositoryCoingecko implements UsdRepository {
}
);

if (!fetchResponses.data) {
if (response.status === 404 || !priceData) {
return null;
}
await throwIfUnsuccessful(
'Error getting USD prices from Coingecko',
response
);

const volumesMap =
fetchResponses.data.total_volumes?.reduce((acc, [timestamp, volume]) => {
priceData.total_volumes?.reduce((acc, [timestamp, volume]) => {
acc.set(timestamp, volume);
return acc;
}, new Map<number, number>()) || undefined;

const prices = fetchResponses.data.prices;
const prices = priceData.prices;
const pricePoints = prices.map(([timestamp, price]) => ({
date: new Date(timestamp),
price,
Expand Down
Loading

0 comments on commit 0e3969b

Please sign in to comment.