Skip to content

Commit

Permalink
Type IPricesApi/IBalancesApi responses as "raw" (#2100)
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook authored Nov 19, 2024
1 parent da1f1ad commit 88970c0
Show file tree
Hide file tree
Showing 13 changed files with 182 additions and 124 deletions.
11 changes: 5 additions & 6 deletions src/datasources/balances-api/balances-api.manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ describe('Balances API Manager Tests', () => {
});
configApiMock.getChain.mockResolvedValue(rawify(chain));
dataSourceMock.get.mockResolvedValue([]);
coingeckoApiMock.getTokenPrices.mockResolvedValue(rawify([]));
const balancesApiManager = new BalancesApiManager(
configurationService,
configApiMock,
Expand Down Expand Up @@ -204,12 +205,10 @@ describe('Balances API Manager Tests', () => {

describe('getFiatCodes checks', () => {
it('should return the intersection of all providers supported currencies', async () => {
zerionBalancesApiMock.getFiatCodes.mockResolvedValue([
'EUR',
'GBP',
'ETH',
]);
coingeckoApiMock.getFiatCodes.mockResolvedValue(['GBP']);
zerionBalancesApiMock.getFiatCodes.mockResolvedValue(
rawify(['EUR', 'GBP', 'ETH']),
);
coingeckoApiMock.getFiatCodes.mockResolvedValue(rawify(['GBP']));
const manager = new BalancesApiManager(
configurationService,
configApiMock,
Expand Down
8 changes: 5 additions & 3 deletions src/datasources/balances-api/balances-api.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { Inject, Injectable } from '@nestjs/common';
import { intersection } from 'lodash';
import { ITransactionApiManager } from '@/domain/interfaces/transaction-api.manager.interface';
import { ChainSchema } from '@/domain/chains/entities/schemas/chain.schema';
import { z } from 'zod';
import { type Raw, rawify } from '@/validation/entities/raw.entity';

@Injectable()
export class BalancesApiManager implements IBalancesApiManager {
Expand Down Expand Up @@ -72,12 +74,12 @@ export class BalancesApiManager implements IBalancesApiManager {
}
}

async getFiatCodes(): Promise<string[]> {
async getFiatCodes(): Promise<Raw<string[]>> {
const [zerionFiatCodes, safeFiatCodes] = await Promise.all([
this.zerionBalancesApi.getFiatCodes(),
this.coingeckoApi.getFiatCodes(),
]);
return intersection(zerionFiatCodes, safeFiatCodes).sort();
]).then(z.array(z.array(z.string())).parse);
return rawify(intersection(zerionFiatCodes, safeFiatCodes).sort());
}

private async _getSafeBalancesApi(chainId: string): Promise<SafeBalancesApi> {
Expand Down
45 changes: 26 additions & 19 deletions src/datasources/balances-api/coingecko-api.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { CacheDir } from '@/datasources/cache/entities/cache-dir.entity';
import { CoingeckoApi } from '@/datasources/balances-api/coingecko-api.service';
import { faker } from '@faker-js/faker';
import type { CacheFirstDataSource } from '../cache/cache.first.data.source';
import type { AssetPrice } from '@/datasources/balances-api/entities/asset-price.entity';
import {
AssetPricesSchema,
type AssetPrice,
} from '@/datasources/balances-api/entities/asset-price.entity';
import type { ICacheService } from '@/datasources/cache/cache.service.interface';
import type { INetworkService } from '@/datasources/network/network.service.interface';
import { sortBy } from 'lodash';
Expand Down Expand Up @@ -504,15 +507,17 @@ describe('CoingeckoAPI', () => {
status: 200,
});

const assetPrices = await service.getTokenPrices({
chain,
tokenAddresses: [
firstTokenAddress,
secondTokenAddress,
thirdTokenAddress,
],
fiatCode,
});
const assetPrices = await service
.getTokenPrices({
chain,
tokenAddresses: [
firstTokenAddress,
secondTokenAddress,
thirdTokenAddress,
],
fiatCode,
})
.then(AssetPricesSchema.parse);

expect(sortBy(assetPrices, (i) => Object.keys(i)[0])).toEqual(
sortBy(
Expand Down Expand Up @@ -606,15 +611,17 @@ describe('CoingeckoAPI', () => {
status: 200,
});

const assetPrices = await service.getTokenPrices({
chain,
tokenAddresses: [
firstTokenAddress,
secondTokenAddress,
thirdTokenAddress,
],
fiatCode,
});
const assetPrices = await service
.getTokenPrices({
chain,
tokenAddresses: [
firstTokenAddress,
secondTokenAddress,
thirdTokenAddress,
],
fiatCode,
})
.then(AssetPricesSchema.parse);

expect(sortBy(assetPrices, (i) => Object.keys(i)[0])).toEqual(
sortBy(
Expand Down
89 changes: 51 additions & 38 deletions src/datasources/balances-api/coingecko-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ import { LoggingService, ILoggingService } from '@/logging/logging.interface';
import { NetworkResponseError } from '@/datasources/network/entities/network.error.entity';
import { asError } from '@/logging/utils';
import { Chain } from '@/domain/chains/entities/chain.entity';
import { z } from 'zod';
import { rawify, type Raw } from '@/validation/entities/raw.entity';

/**
* TODO: Move all usage of Raw to NetworkService/CacheFirstDataSource after fully migrated
* to "Raw" type implementation.
*/
@Injectable()
export class CoingeckoApi implements IPricesApi {
/**
Expand Down Expand Up @@ -114,6 +120,7 @@ export class CoingeckoApi implements IPricesApi {
async getNativeCoinPrice(args: {
chain: Chain;
fiatCode: string;
// TODO: Change to Raw when cache service is migrated
}): Promise<number | null> {
try {
const nativeCoinId = args.chain.pricesProvider.nativeCoin;
Expand All @@ -126,30 +133,34 @@ export class CoingeckoApi implements IPricesApi {
fiatCode: lowerCaseFiatCode,
});
const url = `${this.baseUrl}/simple/price`;
const result = await this.dataSource.get<AssetPrice>({
cacheDir,
url,
networkRequest: {
params: {
vs_currencies: lowerCaseFiatCode,
ids: nativeCoinId,
},
...(this.apiKey && {
headers: {
[CoingeckoApi.COINGECKO_API_HEADER]: this.apiKey,
const result = await this.dataSource
.get<Raw<AssetPrice>>({
cacheDir,
url,
networkRequest: {
params: {
vs_currencies: lowerCaseFiatCode,
ids: nativeCoinId,
},
}),
},
notFoundExpireTimeSeconds: this.defaultNotFoundExpirationTimeSeconds,
expireTimeSeconds: this.nativeCoinPricesTtlSeconds,
});
...(this.apiKey && {
headers: {
[CoingeckoApi.COINGECKO_API_HEADER]: this.apiKey,
},
}),
},
notFoundExpireTimeSeconds: this.defaultNotFoundExpirationTimeSeconds,
expireTimeSeconds: this.nativeCoinPricesTtlSeconds,
})
.then(AssetPriceSchema.parse);
// TODO: Change to Raw when cache service is migrated
return result?.[nativeCoinId]?.[lowerCaseFiatCode];
} catch (error) {
// Error at this level are logged out, but not thrown to the upper layers.
// The service won't throw an error if a single coin price retrieval fails.
this.loggingService.error(
`Error getting native coin price: ${asError(error)} `,
);
// TODO: Change to Raw when cache service is migrated
return null;
}
}
Expand All @@ -167,7 +178,7 @@ export class CoingeckoApi implements IPricesApi {
chain: Chain;
tokenAddresses: string[];
fiatCode: string;
}): Promise<AssetPrice[]> {
}): Promise<Raw<AssetPrice[]>> {
try {
const chainName = args.chain.pricesProvider.chainName;
if (chainName == null) {
Expand All @@ -194,40 +205,42 @@ export class CoingeckoApi implements IPricesApi {
})
: [];

return [pricesFromCache, pricesFromNetwork].flat();
return rawify([pricesFromCache, pricesFromNetwork].flat());
} catch (error) {
// Error at this level are logged out, but not thrown to the upper layers.
// The service won't throw an error if a single token price retrieval fails.
this.loggingService.error(
`Error getting token prices: ${asError(error)} `,
);
return [];
return rawify([]);
}
}

async getFiatCodes(): Promise<string[]> {
async getFiatCodes(): Promise<Raw<string[]>> {
try {
const cacheDir = CacheRouter.getPriceFiatCodesCacheDir();
const url = `${this.baseUrl}/simple/supported_vs_currencies`;
const result = await this.dataSource.get<string[]>({
cacheDir,
url,
networkRequest: {
...(this.apiKey && {
headers: {
[CoingeckoApi.COINGECKO_API_HEADER]: this.apiKey,
},
}),
},
notFoundExpireTimeSeconds: this.defaultNotFoundExpirationTimeSeconds,
expireTimeSeconds: this.defaultExpirationTimeInSeconds,
});
return result.map((item) => item.toUpperCase());
const result = await this.dataSource
.get<Raw<string[]>>({
cacheDir,
url,
networkRequest: {
...(this.apiKey && {
headers: {
[CoingeckoApi.COINGECKO_API_HEADER]: this.apiKey,
},
}),
},
notFoundExpireTimeSeconds: this.defaultNotFoundExpirationTimeSeconds,
expireTimeSeconds: this.defaultExpirationTimeInSeconds,
})
.then(z.array(z.string()).parse);
return rawify(result.map((item) => item.toUpperCase()));
} catch (error) {
this.loggingService.error(
`CoinGecko error getting fiat codes: ${asError(error)} `,
);
return [];
return rawify([]);
}
}

Expand Down Expand Up @@ -276,7 +289,7 @@ export class CoingeckoApi implements IPricesApi {
const prices = await this._requestPricesFromNetwork({
...args,
tokenAddresses: args.tokenAddresses.slice(0, CoingeckoApi.MAX_BATCH_SIZE),
});
}).then(AssetPriceSchema.parse);

return Promise.all(
args.tokenAddresses.map(async (tokenAddress) => {
Expand Down Expand Up @@ -321,10 +334,10 @@ export class CoingeckoApi implements IPricesApi {
chainName: string;
tokenAddresses: string[];
fiatCode: string;
}): Promise<AssetPrice> {
}): Promise<Raw<AssetPrice>> {
try {
const url = `${this.baseUrl}/simple/token_price/${args.chainName}`;
const { data } = await this.networkService.get<AssetPrice>({
const { data } = await this.networkService.get<Raw<AssetPrice>>({
url,
networkRequest: {
params: {
Expand Down
2 changes: 2 additions & 0 deletions src/datasources/balances-api/entities/asset-price.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export type AssetPrice = z.infer<typeof AssetPriceSchema>;

// TODO: Enforce Ethereum address keys (and maybe checksum them)
export const AssetPriceSchema = z.record(z.record(z.number().nullable()));

export const AssetPricesSchema = z.array(AssetPriceSchema);
5 changes: 3 additions & 2 deletions src/datasources/balances-api/prices-api.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AssetPrice } from '@/datasources/balances-api/entities/asset-price.entity';
import type { Chain } from '@/domain/chains/entities/chain.entity';
import type { Raw } from '@/validation/entities/raw.entity';

export const IPricesApi = Symbol('IPricesApi');

Expand All @@ -13,7 +14,7 @@ export interface IPricesApi {
chain: Chain;
tokenAddresses: string[];
fiatCode: string;
}): Promise<AssetPrice[]>;
}): Promise<Raw<AssetPrice[]>>;

getFiatCodes(): Promise<string[]>;
getFiatCodes(): Promise<Raw<string[]>>;
}
Loading

0 comments on commit 88970c0

Please sign in to comment.