Skip to content

Commit

Permalink
Type IBalancesApi as "raw"
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Nov 8, 2024
1 parent 7e6ef93 commit f14ff3e
Show file tree
Hide file tree
Showing 14 changed files with 140 additions and 93 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
15 changes: 8 additions & 7 deletions src/datasources/balances-api/coingecko-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { NetworkResponseError } from '@/datasources/network/entities/network.err
import { asError } from '@/logging/utils';
import { Chain } from '@/domain/chains/entities/chain.entity';
import { z } from 'zod';
import type { Raw } from '@/validation/entities/raw.entity';
import { rawify, type Raw } from '@/validation/entities/raw.entity';

/**
* TODO: Move all usage of Raw to NetworkService/CacheFirstDataSource after fully migrated
Expand Down Expand Up @@ -120,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 Down Expand Up @@ -177,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 @@ -204,18 +205,18 @@ 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`;
Expand All @@ -234,12 +235,12 @@ export class CoingeckoApi implements IPricesApi {
expireTimeSeconds: this.defaultExpirationTimeInSeconds,
})
.then(z.array(z.string()).parse);
return result.map((item) => item.toUpperCase());
return rawify(result.map((item) => item.toUpperCase()));
} catch (error) {
this.loggingService.error(
`CoinGecko error getting fiat codes: ${asError(error)} `,
);
return [];
return rawify([]);
}
}

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[]>>;
}
87 changes: 53 additions & 34 deletions src/datasources/balances-api/safe-balances-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,24 @@ import { CacheFirstDataSource } from '@/datasources/cache/cache.first.data.sourc
import { CacheRouter } from '@/datasources/cache/cache.router';
import { ICacheService } from '@/datasources/cache/cache.service.interface';
import { HttpErrorFactory } from '@/datasources/errors/http-error-factory';
import { Balance } from '@/domain/balances/entities/balance.entity';
import {
Balance,
BalancesSchema,
} from '@/domain/balances/entities/balance.entity';
import { Collectible } from '@/domain/collectibles/entities/collectible.entity';
import { getNumberString } from '@/domain/common/utils/utils';
import { Page } from '@/domain/entities/page.entity';
import { IBalancesApi } from '@/domain/interfaces/balances-api.interface';
import { IPricesApi } from '@/datasources/balances-api/prices-api.interface';
import { Injectable } from '@nestjs/common';
import { Chain } from '@/domain/chains/entities/chain.entity';
import { rawify, type Raw } from '@/validation/entities/raw.entity';
import { AssetPricesSchema } from '@/datasources/balances-api/entities/asset-price.entity';

/**
* TODO: Move all usage of Raw to NetworkService/CacheFirstDataSource after fully migrated
* to "Raw" type implementation.
*/
@Injectable()
export class SafeBalancesApi implements IBalancesApi {
private readonly defaultExpirationTimeInSeconds: number;
Expand Down Expand Up @@ -54,27 +63,33 @@ export class SafeBalancesApi implements IBalancesApi {
chain: Chain;
trusted?: boolean;
excludeSpam?: boolean;
}): Promise<Balance[]> {
}): Promise<Raw<Balance[]>> {
try {
const cacheDir = CacheRouter.getBalancesCacheDir({
chainId: this.chainId,
...args,
});
const url = `${this.baseUrl}/api/v1/safes/${args.safeAddress}/balances/`;
const data = await this.dataSource.get<Balance[]>({
cacheDir,
url,
notFoundExpireTimeSeconds: this.defaultNotFoundExpirationTimeSeconds,
networkRequest: {
params: {
trusted: args.trusted,
exclude_spam: args.excludeSpam,
const data = await this.dataSource
.get<Raw<Balance[]>>({
cacheDir,
url,
notFoundExpireTimeSeconds: this.defaultNotFoundExpirationTimeSeconds,
networkRequest: {
params: {
trusted: args.trusted,
exclude_spam: args.excludeSpam,
},
},
},
expireTimeSeconds: this.defaultExpirationTimeInSeconds,
});
expireTimeSeconds: this.defaultExpirationTimeInSeconds,
})
.then(BalancesSchema.parse);

return this._mapBalances(data, args.fiatCode, args.chain);
return this._mapBalances({
balances: data,
fiatCode: args.fiatCode,
chain: args.chain,
});
} catch (error) {
throw this.httpErrorFactory.from(error);
}
Expand All @@ -94,14 +109,14 @@ export class SafeBalancesApi implements IBalancesApi {
offset?: number;
trusted?: boolean;
excludeSpam?: boolean;
}): Promise<Page<Collectible>> {
}): Promise<Raw<Page<Collectible>>> {
try {
const cacheDir = CacheRouter.getCollectiblesCacheDir({
chainId: this.chainId,
...args,
});
const url = `${this.baseUrl}/api/v2/safes/${args.safeAddress}/collectibles/`;
return await this.dataSource.get({
return await this.dataSource.get<Raw<Page<Collectible>>>({
cacheDir,
url,
notFoundExpireTimeSeconds: this.defaultNotFoundExpirationTimeSeconds,
Expand All @@ -128,7 +143,7 @@ export class SafeBalancesApi implements IBalancesApi {
await this.cacheService.deleteByKey(key);
}

async getFiatCodes(): Promise<string[]> {
async getFiatCodes(): Promise<Raw<string[]>> {
return this.coingeckoApi.getFiatCodes();
}

Expand All @@ -142,41 +157,43 @@ export class SafeBalancesApi implements IBalancesApi {
});
}

private async _mapBalances(
balances: Balance[],
fiatCode: string,
chain: Chain,
): Promise<Balance[]> {
const tokenAddresses = balances
private async _mapBalances(args: {
balances: Balance[];
fiatCode: string;
chain: Chain;
}): Promise<Raw<Balance[]>> {
const tokenAddresses = args.balances
.map((balance) => balance.tokenAddress)
.filter((address): address is `0x${string}` => address !== null);

const assetPrices = await this.coingeckoApi.getTokenPrices({
chain,
fiatCode,
tokenAddresses,
});
const assetPrices = await this.coingeckoApi
.getTokenPrices({
chain: args.chain,
fiatCode: args.fiatCode,
tokenAddresses,
})
.then(AssetPricesSchema.parse);

const lowerCaseAssetPrices = assetPrices?.map((assetPrice) =>
const lowerCaseAssetPrices = assetPrices.map((assetPrice) =>
Object.fromEntries(
Object.entries(assetPrice).map(([k, v]) => [k.toLowerCase(), v]),
),
);

return await Promise.all(
balances.map(async (balance) => {
const balances = await Promise.all(
args.balances.map(async (balance) => {
const tokenAddress = balance.tokenAddress?.toLowerCase() ?? null;
let price: number | null;
if (tokenAddress === null) {
price = await this.coingeckoApi.getNativeCoinPrice({
chain,
fiatCode,
chain: args.chain,
fiatCode: args.fiatCode,
});
} else {
const found = lowerCaseAssetPrices.find(
(assetPrice) => assetPrice[tokenAddress],
);
price = found?.[tokenAddress]?.[fiatCode.toLowerCase()] ?? null;
price = found?.[tokenAddress]?.[args.fiatCode.toLowerCase()] ?? null;
}
const fiatBalance = this._getFiatBalance(price, balance);
return {
Expand All @@ -186,6 +203,8 @@ export class SafeBalancesApi implements IBalancesApi {
};
}),
);

return rawify(balances);
}

private _getFiatBalance(
Expand Down
Loading

0 comments on commit f14ff3e

Please sign in to comment.