Skip to content

Commit

Permalink
feat: add getErc20Transfers
Browse files Browse the repository at this point in the history
  • Loading branch information
ErnoW committed Mar 20, 2023
1 parent 24a2f3b commit 3e03168
Show file tree
Hide file tree
Showing 17 changed files with 542 additions and 29 deletions.
9 changes: 9 additions & 0 deletions .changeset/plenty-dodos-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@moralisweb3/common-evm-utils': minor
'@moralisweb3/api-utils': minor
'@moralisweb3/evm-api': minor
'@moralisweb3/react': minor
'@moralisweb3/next': minor
---

Add `getErc20Transfers` endpoint at `Moralis.EvmApi.token.getErc20Transfers()`
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ const outputClassName = outputFilePath.split('/').pop().split('.')[0];
const sourcePackageName = process.argv[3];

/* eslint-disable @typescript-eslint/no-var-requires */
const package = require(sourcePackageName);
const sourcePackage = require(sourcePackageName);
const fs = require('fs');
const { determineOperationType } = require('@moralisweb3/common-core');

const uniqueGroupNames = new Set(package.operations.map((o) => o.groupName));
const uniqueGroupNames = new Set(sourcePackage.operations.map((o) => o.groupName));
const sourcePackageImports = new Set();
const apiUtilsPackageImports = new Set();
const corePackageImports = new Set();
Expand All @@ -28,7 +28,7 @@ for (const groupName of uniqueGroupNames) {
public readonly ${groupName} = {
`;

for (const operation of package.operations.filter((o) => o.groupName === groupName)) {
for (const operation of sourcePackage.operations.filter((o) => o.groupName === groupName)) {
const operationVarName = `${operation.name}Operation`;
const requestClassName = `${capitalizeFirst(operation.name)}Request`;
const responseClassName = `${capitalizeFirst(operation.name)}ResponseAdapter`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export class Erc20Transfer implements MoralisDataObject {
toAddress: EvmAddress.create(data.toAddress),
fromAddress: EvmAddress.create(data.fromAddress),
value: BigNumber.create(data.value),
transactionIndex: Number(data.transactionIndex),
logIndex: Number(data.logIndex),
});

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/common/evmUtils/src/dataTypes/Erc20Transfer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export interface Erc20TransferData {
chain: EvmChain;
transactionHash: string;
address: EvmAddress;
blockTimestamp: DateInput;
blockNumber: BigNumberish;
blockTimestamp: Date;
blockNumber: BigNumber;
blockHash: string;
toAddress: EvmAddress;
fromAddress: EvmAddress;
Expand Down
77 changes: 77 additions & 0 deletions packages/common/evmUtils/src/operations/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ export interface paths {
/** Get ERC20 token transactions from a contract ordered by block number in descending order. */
get: operations["getTokenTransfers"];
};
"/erc20/transfers": {
get: operations["getErc20Transfers"];
};
"/{address}/balance": {
/** Get the native balance for a specific wallet address. */
get: operations["getNativeBalance"];
Expand Down Expand Up @@ -362,6 +365,49 @@ export interface components {
*/
block_hash: string;
};
erc20Transfer: {
/** @example 0x3105d328c66d8d55092358cf595d54608178e9b5 */
contract_address: string;
/**
* @description The hash of the transaction
* @example 0xdd9006489e46670e0e85d1fb88823099e7f596b08aeaac023e9da0851f26fdd5
*/
transaction_hash: string;
/** @example 204 */
transaction_index: number;
/** @example 204 */
log_index: number;
/**
* @description The timestamp of the block
* @example 2021-05-07T11:08:35.000Z
*/
block_timestamp: string;
/**
* @description The block number
* @example 12386788
*/
block_number: number;
/**
* @description The hash of the block
* @example 0x9b559aef7ea858608c2e554246fe4a24287e7aeeb976848df2b9a2531f4b9171
*/
block_hash: string;
/**
* @description The address of the contract
* @example 0x3105d328c66d8d55092358cf595d54608178e9b5
*/
from_wallet: string;
/**
* @description The address of the contract
* @example 0x3105d328c66d8d55092358cf595d54608178e9b5
*/
to_wallet: string;
/**
* @description The address of the contract
* @example 1234
*/
value: string;
};
blockTransaction: {
/**
* @description The hash of the transaction
Expand Down Expand Up @@ -2624,6 +2670,37 @@ export interface operations {
};
};
};
getErc20Transfers: {
parameters: {
query: {
/** The chain to query */
chain?: components["schemas"]["chainList"];
/** The block number from which the transfers will be returned */
from_block?: number;
/** The block number to which the transfers will be returned */
to_block?: number;
/** The desired page size of the result. */
limit?: number;
/** Contract addresses to only include */
contract_addresses?: string[];
/** Contract addresses to ignore */
exclude_contracts?: string[];
/** Wallet addresses to only include */
wallet_addresses?: string[];
/** Wallet addresses to ignore */
exclude_wallets?: string[];
/** The cursor returned in the previous response (used to getting the next page). */
cursor?: string;
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["erc20Transfer"];
};
};
};
};
/** Get the native balance for a specific wallet address. */
getNativeBalance: {
parameters: {
Expand Down
2 changes: 2 additions & 0 deletions packages/common/evmUtils/src/operations/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
getTokenPriceOperation,
getTokenTransfersOperation,
getWalletTokenBalancesOperation,
getErc20TransfersOperation
} from './token';
import { getWalletTokenTransfersOperation } from './token/getWalletTokenTransfersOperation';
import {
Expand All @@ -48,6 +49,7 @@ export const operations = [
getContractLogsOperation,
getContractNFTsOperation,
getDateToBlockOperation,
getErc20TransfersOperation,
getMultipleNFTsOperation,
getNativeBalanceOperation,
getNativeBalancesForAddressesOperation,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import MoralisCore from '@moralisweb3/common-core';
import { EvmAddress, EvmChain } from '../../dataTypes';
import { getErc20TransfersOperation, GetErc20TransfersRequest } from './getErc20TransfersOperation';

describe('getErc20TransfersOperation', () => {
let core: MoralisCore;

beforeAll(() => {
core = MoralisCore.create();
});

it('serializeRequest() serializes correctly and deserializeRequest() deserializes correctly', () => {
const contractAddresses = ['0xA1ec0345033E7817FB532F68ceb83cD43B05A867'];
const excludeContracts = ['0x65d10A783486d778b036E4715ce69e504aC72536'];
const walletAddresses = ['0x80454f1785347e23f8CC232159FF26fB2a4D3F38'];
const excludeWallets = ['0xD667dC4da4469C064c9200C7CdfC3E60f0f22ba2'];
const chain = '0x10';

const request: Required<GetErc20TransfersRequest> = {
chain: EvmChain.create(chain, core),
fromBlock: 10,
toBlock: 20,
cursor: 'CURSOR1',
limit: 333,
contractAddresses: contractAddresses,
excludeContracts: excludeContracts,
walletAddresses: walletAddresses,
excludeWallets: excludeWallets,
};

const serializedRequest = getErc20TransfersOperation.serializeRequest(request, core);

expect(serializedRequest.chain).toBe(chain);
expect(serializedRequest.fromBlock).toBe(request.fromBlock);
expect(serializedRequest.toBlock).toBe(request.toBlock);
expect(serializedRequest.cursor).toBe(request.cursor);
expect(serializedRequest.limit).toBe(request.limit);
expect(serializedRequest.contractAddresses).toStrictEqual(request.contractAddresses);
expect(serializedRequest.excludeContracts).toStrictEqual(request.excludeContracts);
expect(serializedRequest.walletAddresses).toStrictEqual(request.walletAddresses);
expect(serializedRequest.excludeWallets).toStrictEqual(request.excludeWallets);

const deserializedRequest = getErc20TransfersOperation.deserializeRequest(serializedRequest, core);

expect((deserializedRequest.chain as EvmChain).apiHex).toBe(chain);
expect(deserializedRequest.fromBlock).toBe(request.fromBlock);
expect(deserializedRequest.toBlock).toBe(request.toBlock);
expect(deserializedRequest.cursor).toBe(request.cursor);
expect(deserializedRequest.limit).toBe(request.limit);
expect((deserializedRequest.contractAddresses as EvmAddress[]).map((address) => address.checksum)).toStrictEqual(
request.contractAddresses,
);
expect((deserializedRequest.excludeContracts as EvmAddress[]).map((address) => address.checksum)).toStrictEqual(
request.excludeContracts,
);
expect((deserializedRequest.walletAddresses as EvmAddress[]).map((address) => address.checksum)).toStrictEqual(
request.walletAddresses,
);
expect((deserializedRequest.excludeWallets as EvmAddress[]).map((address) => address.checksum)).toStrictEqual(
request.excludeWallets,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {
Core,
Camelize,
PaginatedOperation,
BigNumber,
PaginatedResponseAdapter,
maybe,
toCamelCase,
} from '@moralisweb3/common-core';
import { EvmChain, EvmChainish, EvmAddress, EvmAddressish, Erc20Transfer } from '../../dataTypes';
import { EvmChainResolver } from '../../EvmChainResolver';
import { operations } from '../openapi';

type OperationId = 'getErc20Transfers';

type QueryParams = operations[OperationId]['parameters']['query'];
type RequestParams = QueryParams;

// TODO: remove this overwrite when the swagger is fixxed
// type SuccessResponse = operations[OperationId]['responses']['200']['content']['application/json'];
type SuccessResponse = {
cursor?: string | undefined;
result: operations[OperationId]['responses']['200']['content']['application/json'][];
};

// Exports

export interface GetErc20TransfersRequest
extends Camelize<
Omit<RequestParams, 'chain' | 'contract_addresses' | 'exclude_contracts' | 'wallet_addresses' | 'exclude_wallets'>
> {
chain?: EvmChainish;
contractAddresses?: EvmAddressish[];
excludeContracts?: EvmAddressish[];
walletAddresses?: EvmAddressish[];
excludeWallets?: EvmAddressish[];
}

export type GetErc20TransfersJSONRequest = ReturnType<typeof serializeRequest>;

export type GetErc20TransfersJSONResponse = SuccessResponse;

export type GetErc20TransfersResponse = ReturnType<typeof deserializeResponse>;

export interface GetErc20TransfersResponseAdapter
extends PaginatedResponseAdapter<GetErc20TransfersResponse, GetErc20TransfersJSONResponse["result"]> {}

/** Get the amount which the spender is allowed to withdraw on behalf of the owner. */
export const getErc20TransfersOperation: PaginatedOperation<
GetErc20TransfersRequest,
GetErc20TransfersJSONRequest,
GetErc20TransfersResponse,
GetErc20TransfersJSONResponse['result']
> = {
method: 'GET',
name: 'getErc20Transfers',
id: 'getErc20Transfers',
groupName: 'token',
urlPathPattern: '/erc20/transfers',
urlSearchParamNames: [
'chain',
'fromBlock',
'toBlock',
'limit',
'contractAddresses',
'excludeContracts',
'walletAddresses',
'excludeWallets',
],
firstPageIndex: 0,

getRequestUrlParams,
serializeRequest,
deserializeRequest,
deserializeResponse,
};

// Methods

function getRequestUrlParams(request: GetErc20TransfersRequest, core: Core) {
return {
chain: EvmChainResolver.resolve(request.chain, core).apiHex,
from_block: maybe(request.fromBlock, String),
to_block: maybe(request.toBlock, String),
limit: maybe(request.limit, String),
cursor: request.cursor,

contract_addresses: request.contractAddresses?.map((address) => EvmAddress.create(address, core).lowercase),
exclude_contracts: request.excludeContracts?.map((address) => EvmAddress.create(address, core).lowercase),
wallet_addresses: request.walletAddresses?.map((address) => EvmAddress.create(address, core).lowercase),
exclude_wallets: request.excludeWallets?.map((address) => EvmAddress.create(address, core).lowercase),
};
}

function deserializeResponse(
jsonResponse: GetErc20TransfersJSONResponse,
request: GetErc20TransfersRequest,
core: Core,
) {
return (jsonResponse.result ?? []).map((transfer) =>
Erc20Transfer.create({
...toCamelCase(transfer),
chain: EvmChainResolver.resolve(request.chain, core),
address: EvmAddress.create(transfer.contract_address, core),
toAddress: EvmAddress.create(transfer.to_wallet, core),
fromAddress: EvmAddress.create(transfer.from_wallet, core),
value: BigNumber.create(transfer.value),
blockTimestamp: new Date(transfer.block_timestamp),
}),
);
}

function serializeRequest(request: GetErc20TransfersRequest, core: Core) {
return {
chain: EvmChainResolver.resolve(request.chain, core).apiHex,
limit: request.limit,
cursor: request.cursor,
fromBlock: request.fromBlock,
toBlock: request.toBlock,

contractAddresses: request.contractAddresses?.map((address) => EvmAddress.create(address, core).lowercase),
excludeContracts: request.excludeContracts?.map((address) => EvmAddress.create(address, core).lowercase),
walletAddresses: request.walletAddresses?.map((address) => EvmAddress.create(address, core).lowercase),
excludeWallets: request.excludeWallets?.map((address) => EvmAddress.create(address, core).lowercase),
};
}

function deserializeRequest(jsonRequest: GetErc20TransfersJSONRequest, core: Core): GetErc20TransfersRequest {
return {
chain: EvmChain.create(jsonRequest.chain, core),
limit: jsonRequest.limit,
cursor: jsonRequest.cursor,
fromBlock: jsonRequest.fromBlock,
toBlock: jsonRequest.toBlock,

contractAddresses: jsonRequest.contractAddresses?.map((address) => EvmAddress.create(address, core)),
excludeContracts: jsonRequest.excludeContracts?.map((address) => EvmAddress.create(address, core)),
walletAddresses: jsonRequest.walletAddresses?.map((address) => EvmAddress.create(address, core)),
excludeWallets: jsonRequest.excludeWallets?.map((address) => EvmAddress.create(address, core)),
};
}
3 changes: 2 additions & 1 deletion packages/common/evmUtils/src/operations/token/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './getErc20TransfersOperation'
export * from './getTokenAllowanceOperation';
export * from './getTokenMetadataOperation';
export * from './getTokenMetadataBySymbolOperation';
export * from './getTokenMetadataOperation';
export * from './getTokenPriceOperation';
export * from './getTokenTransfersOperation';
export * from './getWalletTokenBalancesOperation';
Expand Down
Loading

0 comments on commit 3e03168

Please sign in to comment.