Skip to content

Commit 02f98e3

Browse files
authored
TW-1406: [EVM] balances loading (#162)
* added endpoints * rm commented imports * refactoring according to comments * separate router for evm endpoints
1 parent 05e6803 commit 02f98e3

File tree

9 files changed

+496
-4
lines changed

9 files changed

+496
-4
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"author": "Inokentii Mazhara <[email protected]>",
77
"license": "MIT",
88
"dependencies": {
9+
"@covalenthq/client-sdk": "^1.0.2",
910
"@ethersproject/address": "^5.7.0",
1011
"@ethersproject/hash": "^5.7.0",
1112
"@ethersproject/strings": "^5.7.0",
@@ -16,6 +17,7 @@
1617
"@taquito/tzip12": "14.0.0",
1718
"@taquito/tzip16": "14.0.0",
1819
"@taquito/utils": "14.0.0",
20+
"async-retry": "^1.3.3",
1921
"axios": "^0.27.2",
2022
"bignumber.js": "^9.1.0",
2123
"body-parser": "^1.20.2",
@@ -49,6 +51,7 @@
4951
"db-migration": "cd migrations/notifications && npx ts-node index.ts"
5052
},
5153
"devDependencies": {
54+
"@types/async-retry": "^1.4.8",
5255
"@types/body-parser": "^1.19.2",
5356
"@types/express": "^4.17.17",
5457
"@types/express-jwt": "^7.4.2",

src/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export const EnvVars = {
1212
THREE_ROUTE_API_AUTH_TOKEN: getEnv('THREE_ROUTE_API_AUTH_TOKEN'),
1313
REDIS_URL: getEnv('REDIS_URL'),
1414
ADMIN_USERNAME: getEnv('ADMIN_USERNAME'),
15-
ADMIN_PASSWORD: getEnv('ADMIN_PASSWORD')
15+
ADMIN_PASSWORD: getEnv('ADMIN_PASSWORD'),
16+
COVALENT_API_KEY: getEnv('COVALENT_API_KEY')
1617
};
1718

1819
for (const name in EnvVars) {

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { getNotifications } from './notifications/utils/get-notifications.util';
2020
import { getParsedContent } from './notifications/utils/get-parsed-content.util';
2121
import { getPlatforms } from './notifications/utils/get-platforms.util';
2222
import { redisClient } from './redis';
23+
import { evmRouter } from './routers/evm';
2324
import { adRulesRouter } from './routers/slise-ad-rules';
2425
import { getABData } from './utils/ab-test';
2526
import { cancelAliceBobOrder } from './utils/alice-bob/cancel-alice-bob-order';
@@ -334,6 +335,8 @@ app.get('/api/advertising-info', (_req, res) => {
334335

335336
app.use('/api/slise-ad-rules', adRulesRouter);
336337

338+
app.use('/api/evm', evmRouter);
339+
337340
app.post('/api/magic-square-quest/start', async (req, res) => {
338341
try {
339342
await startMagicSquareQuest(req.body);

src/routers/evm/covalent.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { ChainID, CovalentClient } from '@covalenthq/client-sdk';
2+
import retry from 'async-retry';
3+
4+
import { EnvVars } from '../../config';
5+
import { CodedError } from '../../utils/errors';
6+
7+
const client = new CovalentClient(EnvVars.COVALENT_API_KEY, { enableRetry: false, threadCount: 10 });
8+
9+
const RETRY_OPTIONS = { maxRetryTime: 30_000 };
10+
11+
export const getEvmBalances = async (walletAddress: string, chainId: string) =>
12+
await retry(
13+
async () =>
14+
client.BalanceService.getTokenBalancesForWalletAddress(Number(chainId) as ChainID, walletAddress, {
15+
nft: true,
16+
noNftAssetMetadata: true,
17+
quoteCurrency: 'USD',
18+
noSpam: false
19+
}).then(({ data, error, error_message, error_code }) => {
20+
if (error) {
21+
throw new CodedError(Number(error_code) || 500, error_message);
22+
}
23+
24+
return data;
25+
}),
26+
RETRY_OPTIONS
27+
);
28+
29+
export const getEvmTokensMetadata = async (walletAddress: string, chainId: string) =>
30+
await retry(
31+
async () =>
32+
client.BalanceService.getTokenBalancesForWalletAddress(Number(chainId) as ChainID, walletAddress, {
33+
nft: false,
34+
quoteCurrency: 'USD',
35+
noSpam: false
36+
}).then(({ data, error, error_message, error_code }) => {
37+
if (error) {
38+
throw new CodedError(Number(error_code) || 500, error_message);
39+
}
40+
41+
return data;
42+
}),
43+
RETRY_OPTIONS
44+
);
45+
46+
const CHAIN_IDS_WITHOUT_CACHE_SUPPORT = [10, 11155420, 43114, 43113];
47+
48+
export const getEvmCollectiblesMetadata = async (walletAddress: string, chainId: string) => {
49+
const withUncached = CHAIN_IDS_WITHOUT_CACHE_SUPPORT.includes(Number(chainId));
50+
51+
return await retry(
52+
async () =>
53+
client.NftService.getNftsForAddress(Number(chainId) as ChainID, walletAddress, {
54+
withUncached,
55+
noSpam: false
56+
}).then(({ data, error, error_message, error_code }) => {
57+
if (error) {
58+
throw new CodedError(Number(error_code) || 500, error_message);
59+
}
60+
61+
return data;
62+
}),
63+
RETRY_OPTIONS
64+
);
65+
};
66+
67+
export const getStringifiedResponse = (response: any) =>
68+
JSON.stringify(response, (_, value) => (typeof value === 'bigint' ? value.toString() : value));

src/routers/evm/index.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Router } from 'express';
2+
3+
import { withCodedExceptionHandler, withEvmQueryValidation } from '../../utils/express-helpers';
4+
import { getEvmBalances, getEvmCollectiblesMetadata, getEvmTokensMetadata, getStringifiedResponse } from './covalent';
5+
6+
export const evmRouter = Router();
7+
8+
evmRouter
9+
.get(
10+
'/balances',
11+
withCodedExceptionHandler(
12+
withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => {
13+
const { walletAddress, chainId } = evmQueryParams;
14+
15+
const data = await getEvmBalances(walletAddress, chainId);
16+
17+
res.status(200).send(getStringifiedResponse(data));
18+
})
19+
)
20+
)
21+
.get(
22+
'/tokens-metadata',
23+
withCodedExceptionHandler(
24+
withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => {
25+
const { walletAddress, chainId } = evmQueryParams;
26+
27+
const data = await getEvmTokensMetadata(walletAddress, chainId);
28+
29+
res.status(200).send(getStringifiedResponse(data));
30+
})
31+
)
32+
)
33+
.get(
34+
'/collectibles-metadata',
35+
withCodedExceptionHandler(
36+
withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => {
37+
const { walletAddress, chainId } = evmQueryParams;
38+
39+
const data = await getEvmCollectiblesMetadata(walletAddress, chainId);
40+
41+
res.status(200).send(getStringifiedResponse(data));
42+
})
43+
)
44+
);

src/utils/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface CodedErrorForResponse {
88
type StatusCodeNumber = (typeof StatusCodes)[keyof typeof StatusCodes];
99

1010
export class CodedError extends Error {
11-
constructor(public code: StatusCodeNumber, message: string, public errorCode?: string) {
11+
constructor(public code: StatusCodeNumber | number, message: string, public errorCode?: string) {
1212
super(message);
1313
}
1414

src/utils/express-helpers.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { NextFunction, Request, RequestHandler, Response, Router } from 'express
22
import { ArraySchema as IArraySchema, ObjectSchema as IObjectSchema, Schema, ValidationError } from 'yup';
33

44
import { basicAuth } from '../middlewares/basic-auth.middleware';
5+
import { CodedError } from './errors';
56
import logger from './logger';
7+
import { evmQueryParamsSchema } from './schemas';
68

79
interface ObjectStorageMethods<V> {
810
getByKey: (key: string) => Promise<V>;
@@ -33,6 +35,36 @@ export const withBodyValidation =
3335
return handler(req, res, next);
3436
};
3537

38+
interface EvmQueryParams {
39+
walletAddress: string;
40+
chainId: string;
41+
}
42+
43+
type TypedEvmQueryRequestHandler = (
44+
req: Request,
45+
res: Response,
46+
next: NextFunction,
47+
evmQueryParams: EvmQueryParams
48+
) => void;
49+
50+
export const withEvmQueryValidation =
51+
(handler: TypedEvmQueryRequestHandler): RequestHandler =>
52+
async (req, res, next) => {
53+
let evmQueryParams: EvmQueryParams;
54+
55+
try {
56+
evmQueryParams = await evmQueryParamsSchema.validate(req.query);
57+
} catch (error) {
58+
if (error instanceof ValidationError) {
59+
return res.status(400).send({ error: error.message });
60+
}
61+
62+
throw error;
63+
}
64+
65+
return handler(req, res, next, evmQueryParams);
66+
};
67+
3668
export const withExceptionHandler =
3769
(handler: RequestHandler): RequestHandler =>
3870
async (req, res, next) => {
@@ -44,6 +76,22 @@ export const withExceptionHandler =
4476
}
4577
};
4678

79+
export const withCodedExceptionHandler =
80+
(handler: RequestHandler): RequestHandler =>
81+
async (req, res, next) => {
82+
try {
83+
await handler(req, res, next);
84+
} catch (error: any) {
85+
logger.error(error);
86+
87+
if (error instanceof CodedError) {
88+
res.status(error.code).send(error.buildResponse());
89+
} else {
90+
res.status(500).send({ message: error?.message });
91+
}
92+
}
93+
};
94+
4795
interface ObjectStorageMethodsEntrypointsConfig<StoredValue, ObjectResponse, ValueResponse> {
4896
path: string;
4997
methods: ObjectStorageMethods<StoredValue>;

src/utils/schemas.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ const adStylesOverridesSchema = objectSchema().shape({
108108
style: styleSchema.clone().required()
109109
});
110110

111+
export const evmQueryParamsSchema = objectSchema().shape({
112+
walletAddress: nonEmptyStringSchema.clone().required('walletAddress is undefined'),
113+
chainId: nonEmptyStringSchema.clone().required('chainId is undefined')
114+
});
115+
111116
const adPlacesRulesSchema = arraySchema()
112117
.of(
113118
objectSchema()

0 commit comments

Comments
 (0)