Skip to content

Commit 3a479f6

Browse files
authored
TW-1479: [EVM] Transactions history (#176)
* TW-1479: [EVM] Transactions history * TW-1479: [EVM] Transactions history. ++ Covalent SDK
1 parent fc0d150 commit 3a479f6

File tree

7 files changed

+141
-390
lines changed

7 files changed

+141
-390
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,5 @@ dist
108108

109109
# IDE
110110
.idea
111+
112+
.DS_Store

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"author": "Inokentii Mazhara <[email protected]>",
77
"license": "MIT",
88
"dependencies": {
9-
"@covalenthq/client-sdk": "^1.0.2",
9+
"@covalenthq/client-sdk": "^2",
1010
"@ethersproject/address": "^5.7.0",
1111
"@ethersproject/hash": "^5.7.0",
1212
"@ethersproject/strings": "^5.7.0",

src/routers/evm/covalent.ts

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,41 @@
1-
import { ChainID, CovalentClient } from '@covalenthq/client-sdk';
1+
import {
2+
GoldRushClient,
3+
ChainID,
4+
GoldRushResponse,
5+
GetTransactionsForAddressV3QueryParamOpts
6+
} from '@covalenthq/client-sdk';
27
import retry from 'async-retry';
38

49
import { EnvVars } from '../../config';
510
import { CodedError } from '../../utils/errors';
611

7-
const client = new CovalentClient(EnvVars.COVALENT_API_KEY, { enableRetry: false, threadCount: 10 });
12+
const client = new GoldRushClient(EnvVars.COVALENT_API_KEY, { enableRetry: false, threadCount: 10 });
813

9-
const RETRY_OPTIONS = { maxRetryTime: 30_000 };
14+
const RETRY_OPTIONS: retry.Options = { maxRetryTime: 30_000 };
1015

11-
export const getEvmBalances = async (walletAddress: string, chainId: string) =>
12-
await retry(
13-
async () =>
16+
/** For v2 only for now. No support in v3. */
17+
const ACTIVITIES_PER_PAGE = 30;
18+
19+
export const getEvmBalances = (walletAddress: string, chainId: string) =>
20+
retry(
21+
() =>
1422
client.BalanceService.getTokenBalancesForWalletAddress(Number(chainId) as ChainID, walletAddress, {
1523
nft: true,
1624
noNftAssetMetadata: true,
1725
quoteCurrency: 'USD',
1826
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-
}),
27+
}).then(processGoldRushResponse),
2628
RETRY_OPTIONS
2729
);
2830

29-
export const getEvmTokensMetadata = async (walletAddress: string, chainId: string) =>
30-
await retry(
31-
async () =>
31+
export const getEvmTokensMetadata = (walletAddress: string, chainId: string) =>
32+
retry(
33+
() =>
3234
client.BalanceService.getTokenBalancesForWalletAddress(Number(chainId) as ChainID, walletAddress, {
3335
nft: false,
3436
quoteCurrency: 'USD',
3537
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-
}),
38+
}).then(processGoldRushResponse),
4339
RETRY_OPTIONS
4440
);
4541

@@ -49,20 +45,62 @@ export const getEvmCollectiblesMetadata = async (walletAddress: string, chainId:
4945
const withUncached = CHAIN_IDS_WITHOUT_CACHE_SUPPORT.includes(Number(chainId));
5046

5147
return await retry(
52-
async () =>
48+
() =>
5349
client.NftService.getNftsForAddress(Number(chainId) as ChainID, walletAddress, {
5450
withUncached,
5551
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-
}),
52+
}).then(processGoldRushResponse),
6353
RETRY_OPTIONS
6454
);
6555
};
6656

67-
export const getStringifiedResponse = (response: any) =>
68-
JSON.stringify(response, (_, value) => (typeof value === 'bigint' ? value.toString() : value));
57+
export const getEvmAccountTransactions = (walletAddress: string, chainId: string, page?: number) =>
58+
retry(async () => {
59+
const options: GetTransactionsForAddressV3QueryParamOpts = {
60+
// blockSignedAtAsc: true,
61+
noLogs: false,
62+
quoteCurrency: 'USD',
63+
withSafe: false
64+
};
65+
66+
const res = await (typeof page === 'number'
67+
? client.TransactionService.getTransactionsForAddressV3(Number(chainId) as ChainID, walletAddress, page, options)
68+
: client.TransactionService.getAllTransactionsForAddressByPage(
69+
Number(chainId) as ChainID,
70+
walletAddress,
71+
options
72+
));
73+
74+
return processGoldRushResponse(res);
75+
}, RETRY_OPTIONS);
76+
77+
export const getEvmAccountERC20Transfers = (
78+
walletAddress: string,
79+
chainId: string,
80+
contractAddress: string,
81+
page?: number
82+
) =>
83+
retry(async () => {
84+
const res = await client.BalanceService.getErc20TransfersForWalletAddressByPage(
85+
Number(chainId) as ChainID,
86+
walletAddress,
87+
{
88+
contractAddress,
89+
quoteCurrency: 'USD',
90+
pageNumber: page,
91+
pageSize: ACTIVITIES_PER_PAGE
92+
}
93+
);
94+
95+
return processGoldRushResponse(res);
96+
}, RETRY_OPTIONS);
97+
98+
function processGoldRushResponse<T>({ data, error, error_message, error_code }: GoldRushResponse<T>) {
99+
if (error) {
100+
const code = error_code && Number.isSafeInteger(Number(error_code)) ? Number(error_code) : 500;
101+
102+
throw new CodedError(code, error_message ?? 'Unknown error');
103+
}
104+
105+
return JSON.stringify(data, (_, value) => (typeof value === 'bigint' ? value.toString() : value));
106+
}

src/routers/evm/index.ts

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,69 @@
11
import { Router } from 'express';
22

3-
import { withCodedExceptionHandler, withEvmQueryValidation } from '../../utils/express-helpers';
4-
import { getEvmBalances, getEvmCollectiblesMetadata, getEvmTokensMetadata, getStringifiedResponse } from './covalent';
3+
import { withCodedExceptionHandler } from '../../utils/express-helpers';
4+
import {
5+
evmQueryParamsSchema,
6+
evmQueryParamsPaginatedSchema,
7+
evmQueryParamsTransfersSchema
8+
} from '../../utils/schemas';
9+
import {
10+
getEvmBalances,
11+
getEvmCollectiblesMetadata,
12+
getEvmTokensMetadata,
13+
getEvmAccountTransactions,
14+
getEvmAccountERC20Transfers
15+
} from './covalent';
516

617
export const evmRouter = Router();
718

819
evmRouter
920
.get(
1021
'/balances',
11-
withCodedExceptionHandler(
12-
withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => {
13-
const { walletAddress, chainId } = evmQueryParams;
22+
withCodedExceptionHandler(async (req, res) => {
23+
const { walletAddress, chainId } = await evmQueryParamsSchema.validate(req.query);
1424

15-
const data = await getEvmBalances(walletAddress, chainId);
25+
const data = await getEvmBalances(walletAddress, chainId);
1626

17-
res.status(200).send(getStringifiedResponse(data));
18-
})
19-
)
27+
res.status(200).send(data);
28+
})
2029
)
2130
.get(
2231
'/tokens-metadata',
23-
withCodedExceptionHandler(
24-
withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => {
25-
const { walletAddress, chainId } = evmQueryParams;
32+
withCodedExceptionHandler(async (req, res) => {
33+
const { walletAddress, chainId } = await evmQueryParamsSchema.validate(req.query);
2634

27-
const data = await getEvmTokensMetadata(walletAddress, chainId);
35+
const data = await getEvmTokensMetadata(walletAddress, chainId);
2836

29-
res.status(200).send(getStringifiedResponse(data));
30-
})
31-
)
37+
res.status(200).send(data);
38+
})
3239
)
3340
.get(
3441
'/collectibles-metadata',
35-
withCodedExceptionHandler(
36-
withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => {
37-
const { walletAddress, chainId } = evmQueryParams;
42+
withCodedExceptionHandler(async (req, res) => {
43+
const { walletAddress, chainId } = await evmQueryParamsSchema.validate(req.query);
3844

39-
const data = await getEvmCollectiblesMetadata(walletAddress, chainId);
45+
const data = await getEvmCollectiblesMetadata(walletAddress, chainId);
4046

41-
res.status(200).send(getStringifiedResponse(data));
42-
})
43-
)
47+
res.status(200).send(data);
48+
})
49+
)
50+
.get(
51+
'/transactions',
52+
withCodedExceptionHandler(async (req, res) => {
53+
const { walletAddress, chainId, page } = await evmQueryParamsPaginatedSchema.validate(req.query);
54+
55+
const data = await getEvmAccountTransactions(walletAddress, chainId, page);
56+
57+
res.status(200).send(data);
58+
})
59+
)
60+
.get(
61+
'/erc20-transfers',
62+
withCodedExceptionHandler(async (req, res) => {
63+
const { walletAddress, chainId, contractAddress, page } = await evmQueryParamsTransfersSchema.validate(req.query);
64+
65+
const data = await getEvmAccountERC20Transfers(walletAddress, chainId, contractAddress, page);
66+
67+
res.status(200).send(data);
68+
})
4469
);

src/utils/express-helpers.ts

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { ArraySchema as IArraySchema, ObjectSchema as IObjectSchema, Schema, Val
44
import { basicAuth } from '../middlewares/basic-auth.middleware';
55
import { CodedError } from './errors';
66
import logger from './logger';
7-
import { evmQueryParamsSchema } from './schemas';
87

98
interface ObjectStorageMethods<V> {
109
getByKey: (key: string) => Promise<V>;
@@ -41,36 +40,6 @@ export const withBodyValidation =
4140
return handler(req, res, next);
4241
};
4342

44-
interface EvmQueryParams {
45-
walletAddress: string;
46-
chainId: string;
47-
}
48-
49-
type TypedEvmQueryRequestHandler = (
50-
req: Request,
51-
res: Response,
52-
next: NextFunction,
53-
evmQueryParams: EvmQueryParams
54-
) => void;
55-
56-
export const withEvmQueryValidation =
57-
(handler: TypedEvmQueryRequestHandler): RequestHandler =>
58-
async (req, res, next) => {
59-
let evmQueryParams: EvmQueryParams;
60-
61-
try {
62-
evmQueryParams = await evmQueryParamsSchema.validate(req.query);
63-
} catch (error) {
64-
if (error instanceof ValidationError) {
65-
return res.status(400).send({ error: error.message });
66-
}
67-
68-
throw error;
69-
}
70-
71-
return handler(req, res, next, evmQueryParams);
72-
};
73-
7443
export const withExceptionHandler =
7544
(handler: RequestHandler): RequestHandler =>
7645
async (req, res, next) => {
@@ -92,6 +61,8 @@ export const withCodedExceptionHandler =
9261

9362
if (error instanceof CodedError) {
9463
res.status(error.code).send(error.buildResponse());
64+
} else if (error instanceof ValidationError) {
65+
res.status(400).send({ error: error.message });
9566
} else {
9667
res.status(500).send({ message: error?.message });
9768
}

src/utils/schemas.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getAddress } from '@ethersproject/address';
12
import { validRange as getValidatedRange } from 'semver';
23
import {
34
array as arraySchema,
@@ -114,6 +115,16 @@ export const evmQueryParamsSchema = objectSchema().shape({
114115
chainId: nonEmptyStringSchema.clone().required('chainId is undefined')
115116
});
116117

118+
export const evmQueryParamsPaginatedSchema = evmQueryParamsSchema.clone().shape({
119+
page: numberSchema().integer().min(1)
120+
});
121+
122+
export const evmQueryParamsTransfersSchema = evmQueryParamsPaginatedSchema.clone().shape({
123+
contractAddress: stringSchema()
124+
.required()
125+
.test(val => getAddress(val) === val)
126+
});
127+
117128
const adPlacesRulesSchema = arraySchema()
118129
.of(
119130
objectSchema()

0 commit comments

Comments
 (0)