Skip to content

Commit

Permalink
TW-1479: [EVM] Transactions history (#176)
Browse files Browse the repository at this point in the history
* TW-1479: [EVM] Transactions history

* TW-1479: [EVM] Transactions history. ++ Covalent SDK
  • Loading branch information
alex-tsx authored Oct 17, 2024
1 parent fc0d150 commit 3a479f6
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 390 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,5 @@ dist

# IDE
.idea

.DS_Store
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"author": "Inokentii Mazhara <[email protected]>",
"license": "MIT",
"dependencies": {
"@covalenthq/client-sdk": "^1.0.2",
"@covalenthq/client-sdk": "^2",
"@ethersproject/address": "^5.7.0",
"@ethersproject/hash": "^5.7.0",
"@ethersproject/strings": "^5.7.0",
Expand Down
104 changes: 71 additions & 33 deletions src/routers/evm/covalent.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,41 @@
import { ChainID, CovalentClient } from '@covalenthq/client-sdk';
import {
GoldRushClient,
ChainID,
GoldRushResponse,
GetTransactionsForAddressV3QueryParamOpts
} from '@covalenthq/client-sdk';
import retry from 'async-retry';

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

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

const RETRY_OPTIONS = { maxRetryTime: 30_000 };
const RETRY_OPTIONS: retry.Options = { maxRetryTime: 30_000 };

export const getEvmBalances = async (walletAddress: string, chainId: string) =>
await retry(
async () =>
/** For v2 only for now. No support in v3. */
const ACTIVITIES_PER_PAGE = 30;

export const getEvmBalances = (walletAddress: string, chainId: string) =>
retry(
() =>
client.BalanceService.getTokenBalancesForWalletAddress(Number(chainId) as ChainID, walletAddress, {
nft: true,
noNftAssetMetadata: true,
quoteCurrency: 'USD',
noSpam: false
}).then(({ data, error, error_message, error_code }) => {
if (error) {
throw new CodedError(Number(error_code) || 500, error_message);
}

return data;
}),
}).then(processGoldRushResponse),
RETRY_OPTIONS
);

export const getEvmTokensMetadata = async (walletAddress: string, chainId: string) =>
await retry(
async () =>
export const getEvmTokensMetadata = (walletAddress: string, chainId: string) =>
retry(
() =>
client.BalanceService.getTokenBalancesForWalletAddress(Number(chainId) as ChainID, walletAddress, {
nft: false,
quoteCurrency: 'USD',
noSpam: false
}).then(({ data, error, error_message, error_code }) => {
if (error) {
throw new CodedError(Number(error_code) || 500, error_message);
}

return data;
}),
}).then(processGoldRushResponse),
RETRY_OPTIONS
);

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

return await retry(
async () =>
() =>
client.NftService.getNftsForAddress(Number(chainId) as ChainID, walletAddress, {
withUncached,
noSpam: false
}).then(({ data, error, error_message, error_code }) => {
if (error) {
throw new CodedError(Number(error_code) || 500, error_message);
}

return data;
}),
}).then(processGoldRushResponse),
RETRY_OPTIONS
);
};

export const getStringifiedResponse = (response: any) =>
JSON.stringify(response, (_, value) => (typeof value === 'bigint' ? value.toString() : value));
export const getEvmAccountTransactions = (walletAddress: string, chainId: string, page?: number) =>
retry(async () => {
const options: GetTransactionsForAddressV3QueryParamOpts = {
// blockSignedAtAsc: true,
noLogs: false,
quoteCurrency: 'USD',
withSafe: false
};

const res = await (typeof page === 'number'
? client.TransactionService.getTransactionsForAddressV3(Number(chainId) as ChainID, walletAddress, page, options)
: client.TransactionService.getAllTransactionsForAddressByPage(
Number(chainId) as ChainID,
walletAddress,
options
));

return processGoldRushResponse(res);
}, RETRY_OPTIONS);

export const getEvmAccountERC20Transfers = (
walletAddress: string,
chainId: string,
contractAddress: string,
page?: number
) =>
retry(async () => {
const res = await client.BalanceService.getErc20TransfersForWalletAddressByPage(
Number(chainId) as ChainID,
walletAddress,
{
contractAddress,
quoteCurrency: 'USD',
pageNumber: page,
pageSize: ACTIVITIES_PER_PAGE
}
);

return processGoldRushResponse(res);
}, RETRY_OPTIONS);

function processGoldRushResponse<T>({ data, error, error_message, error_code }: GoldRushResponse<T>) {
if (error) {
const code = error_code && Number.isSafeInteger(Number(error_code)) ? Number(error_code) : 500;

throw new CodedError(code, error_message ?? 'Unknown error');
}

return JSON.stringify(data, (_, value) => (typeof value === 'bigint' ? value.toString() : value));
}
71 changes: 48 additions & 23 deletions src/routers/evm/index.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,69 @@
import { Router } from 'express';

import { withCodedExceptionHandler, withEvmQueryValidation } from '../../utils/express-helpers';
import { getEvmBalances, getEvmCollectiblesMetadata, getEvmTokensMetadata, getStringifiedResponse } from './covalent';
import { withCodedExceptionHandler } from '../../utils/express-helpers';
import {
evmQueryParamsSchema,
evmQueryParamsPaginatedSchema,
evmQueryParamsTransfersSchema
} from '../../utils/schemas';
import {
getEvmBalances,
getEvmCollectiblesMetadata,
getEvmTokensMetadata,
getEvmAccountTransactions,
getEvmAccountERC20Transfers
} from './covalent';

export const evmRouter = Router();

evmRouter
.get(
'/balances',
withCodedExceptionHandler(
withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => {
const { walletAddress, chainId } = evmQueryParams;
withCodedExceptionHandler(async (req, res) => {
const { walletAddress, chainId } = await evmQueryParamsSchema.validate(req.query);

const data = await getEvmBalances(walletAddress, chainId);
const data = await getEvmBalances(walletAddress, chainId);

res.status(200).send(getStringifiedResponse(data));
})
)
res.status(200).send(data);
})
)
.get(
'/tokens-metadata',
withCodedExceptionHandler(
withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => {
const { walletAddress, chainId } = evmQueryParams;
withCodedExceptionHandler(async (req, res) => {
const { walletAddress, chainId } = await evmQueryParamsSchema.validate(req.query);

const data = await getEvmTokensMetadata(walletAddress, chainId);
const data = await getEvmTokensMetadata(walletAddress, chainId);

res.status(200).send(getStringifiedResponse(data));
})
)
res.status(200).send(data);
})
)
.get(
'/collectibles-metadata',
withCodedExceptionHandler(
withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => {
const { walletAddress, chainId } = evmQueryParams;
withCodedExceptionHandler(async (req, res) => {
const { walletAddress, chainId } = await evmQueryParamsSchema.validate(req.query);

const data = await getEvmCollectiblesMetadata(walletAddress, chainId);
const data = await getEvmCollectiblesMetadata(walletAddress, chainId);

res.status(200).send(getStringifiedResponse(data));
})
)
res.status(200).send(data);
})
)
.get(
'/transactions',
withCodedExceptionHandler(async (req, res) => {
const { walletAddress, chainId, page } = await evmQueryParamsPaginatedSchema.validate(req.query);

const data = await getEvmAccountTransactions(walletAddress, chainId, page);

res.status(200).send(data);
})
)
.get(
'/erc20-transfers',
withCodedExceptionHandler(async (req, res) => {
const { walletAddress, chainId, contractAddress, page } = await evmQueryParamsTransfersSchema.validate(req.query);

const data = await getEvmAccountERC20Transfers(walletAddress, chainId, contractAddress, page);

res.status(200).send(data);
})
);
33 changes: 2 additions & 31 deletions src/utils/express-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ArraySchema as IArraySchema, ObjectSchema as IObjectSchema, Schema, Val
import { basicAuth } from '../middlewares/basic-auth.middleware';
import { CodedError } from './errors';
import logger from './logger';
import { evmQueryParamsSchema } from './schemas';

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

interface EvmQueryParams {
walletAddress: string;
chainId: string;
}

type TypedEvmQueryRequestHandler = (
req: Request,
res: Response,
next: NextFunction,
evmQueryParams: EvmQueryParams
) => void;

export const withEvmQueryValidation =
(handler: TypedEvmQueryRequestHandler): RequestHandler =>
async (req, res, next) => {
let evmQueryParams: EvmQueryParams;

try {
evmQueryParams = await evmQueryParamsSchema.validate(req.query);
} catch (error) {
if (error instanceof ValidationError) {
return res.status(400).send({ error: error.message });
}

throw error;
}

return handler(req, res, next, evmQueryParams);
};

export const withExceptionHandler =
(handler: RequestHandler): RequestHandler =>
async (req, res, next) => {
Expand All @@ -92,6 +61,8 @@ export const withCodedExceptionHandler =

if (error instanceof CodedError) {
res.status(error.code).send(error.buildResponse());
} else if (error instanceof ValidationError) {
res.status(400).send({ error: error.message });
} else {
res.status(500).send({ message: error?.message });
}
Expand Down
11 changes: 11 additions & 0 deletions src/utils/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getAddress } from '@ethersproject/address';
import { validRange as getValidatedRange } from 'semver';
import {
array as arraySchema,
Expand Down Expand Up @@ -114,6 +115,16 @@ export const evmQueryParamsSchema = objectSchema().shape({
chainId: nonEmptyStringSchema.clone().required('chainId is undefined')
});

export const evmQueryParamsPaginatedSchema = evmQueryParamsSchema.clone().shape({
page: numberSchema().integer().min(1)
});

export const evmQueryParamsTransfersSchema = evmQueryParamsPaginatedSchema.clone().shape({
contractAddress: stringSchema()
.required()
.test(val => getAddress(val) === val)
});

const adPlacesRulesSchema = arraySchema()
.of(
objectSchema()
Expand Down
Loading

0 comments on commit 3a479f6

Please sign in to comment.