diff --git a/.changeset/nasty-wolves-shop.md b/.changeset/nasty-wolves-shop.md new file mode 100644 index 0000000000..31cdb7810d --- /dev/null +++ b/.changeset/nasty-wolves-shop.md @@ -0,0 +1,6 @@ +--- +'@moralisweb3/common-evm-utils': minor +'@moralisweb3/evm-api': minor +--- + +Add `getErc20Approvals` endpoint at `Moralis.EvmApi.token.getErc20Approvals()` diff --git a/packages/common/evmUtils/src/dataTypes/Erc20Approval/Erc20Approval.test.ts b/packages/common/evmUtils/src/dataTypes/Erc20Approval/Erc20Approval.test.ts new file mode 100644 index 0000000000..e60d88e281 --- /dev/null +++ b/packages/common/evmUtils/src/dataTypes/Erc20Approval/Erc20Approval.test.ts @@ -0,0 +1,95 @@ +import { Core } from '@moralisweb3/common-core'; +import { Erc20Approval } from './Erc20Approval'; +import { setupEvmUtils } from '../../test/setup'; +import { Erc20ApprovalInput } from './types'; + +const exampleInput: Erc20ApprovalInput = { + chain: '0x1', + toWallet: '0x09f4fc6081026c85070886599e83f599ecf82405', + fromWallet: '0xaabc2c22426993f3d219d94a5ee6e95d4954f3bf', + contractAddress: '0xa0e8fed3426391fdb446516799c4d6248e2b2860', + blockHash: '0xa5f87d4341642b89e3ccb81449e3083032c36fface2c2042941b8bd9afe83f79', + blockNumber: '16868690', + blockTimestamp: '2023-03-20T11:48:59.000Z', + transactionHash: '0xb7b4d321e2ab26c1cde1a2ef49413e21b65dcc663d6de8f75ddbdd868b98b4bf', + transactionIndex: 4, + logIndex: 25, + value: '100000000000000000000000000000', +}; + +describe('Erc20Approval', () => { + let core: Core; + + beforeAll(() => { + core = setupEvmUtils(); + }); + + beforeEach(() => { + core.config.reset(); + }); + + /** + * Creation + */ + it('should create a new Erc20Approval', () => { + const erc20Approval = Erc20Approval.create(exampleInput); + + expect(erc20Approval.chain.hex).toBe('0x1'); + expect(erc20Approval.toWallet.lowercase).toBe('0x09f4fc6081026c85070886599e83f599ecf82405'); + expect(erc20Approval.fromWallet.lowercase).toBe('0xaabc2c22426993f3d219d94a5ee6e95d4954f3bf'); + expect(erc20Approval.contractAddress.lowercase).toBe('0xa0e8fed3426391fdb446516799c4d6248e2b2860'); + expect(erc20Approval.blockNumber.toString()).toBe('16868690'); + expect(erc20Approval.blockTimestamp.toISOString()).toBe('2023-03-20T11:48:59.000Z'); + expect(erc20Approval.transactionHash).toBe('0xb7b4d321e2ab26c1cde1a2ef49413e21b65dcc663d6de8f75ddbdd868b98b4bf'); + expect(erc20Approval.transactionIndex).toBe(4); + expect(erc20Approval.logIndex).toBe(25); + expect(erc20Approval.value.toString()).toBe('100000000000000000000000000000'); + }); + + /** + * Formatting + */ + it('should return formatting in json', () => { + const erc20Approval = Erc20Approval.create(exampleInput); + + const value = erc20Approval.toJSON(); + + expect(value).toStrictEqual({ + chain: '0x1', + toWallet: '0x09f4fc6081026c85070886599e83f599ecf82405', + fromWallet: '0xaabc2c22426993f3d219d94a5ee6e95d4954f3bf', + contractAddress: '0xa0e8fed3426391fdb446516799c4d6248e2b2860', + blockHash: '0xa5f87d4341642b89e3ccb81449e3083032c36fface2c2042941b8bd9afe83f79', + blockNumber: '16868690', + blockTimestamp: new Date('2023-03-20T11:48:59.000Z'), + transactionHash: '0xb7b4d321e2ab26c1cde1a2ef49413e21b65dcc663d6de8f75ddbdd868b98b4bf', + transactionIndex: 4, + logIndex: 25, + value: '100000000000000000000000000000', + }); + }); + + /** + * Methods + */ + it('should check equality of 2 erc20Approvals of the same value', () => { + const erc20ApprovalA = Erc20Approval.create(exampleInput); + const erc20ApprovalB = Erc20Approval.create(exampleInput); + + expect(erc20ApprovalA.equals(erc20ApprovalB)).toBeTruthy(); + }); + + it('should check equality of 2 erc20Approvals of the same value via a static method', () => { + const erc20ApprovalA = Erc20Approval.create(exampleInput); + const erc20ApprovalB = Erc20Approval.create(exampleInput); + + expect(Erc20Approval.equals(erc20ApprovalA, erc20ApprovalB)).toBeTruthy(); + }); + + it('should check inequality when chain is different', () => { + const erc20ApprovalA = Erc20Approval.create(exampleInput); + const erc20ApprovalB = Erc20Approval.create({ ...exampleInput, chain: '0x2' }); + + expect(erc20ApprovalA.equals(erc20ApprovalB)).toBeFalsy(); + }); +}); diff --git a/packages/common/evmUtils/src/dataTypes/Erc20Approval/Erc20Approval.ts b/packages/common/evmUtils/src/dataTypes/Erc20Approval/Erc20Approval.ts new file mode 100644 index 0000000000..e3459d6ac4 --- /dev/null +++ b/packages/common/evmUtils/src/dataTypes/Erc20Approval/Erc20Approval.ts @@ -0,0 +1,192 @@ +import Core, { MoralisDataObject, BigNumber, dateInputToDate, CoreProvider } from '@moralisweb3/common-core'; +import { EvmAddress } from '../EvmAddress'; +import { EvmChain } from '../EvmChain'; +import { Erc20ApprovalInput, Erc20ApprovalData } from './types'; + +/** + * The Erc20Approval is a representation of an Erc20 token approval. + * + * @category DataType + */ +export class Erc20Approval implements MoralisDataObject { + /** + * Create a new instance of Erc20Approval from any valid input + * @param data - Erc20Approval instance or valid Erc20ApprovalInput + * @example + * ``` + * const approval = Erc20Approval.create(data); + *``` + */ + static create(data: Erc20Approval | Erc20ApprovalInput, core?: Core) { + if (data instanceof Erc20Approval) { + return data; + } + + const finalCore = core ?? CoreProvider.getDefault(); + return new Erc20Approval(data, finalCore); + } + + private _data: Erc20ApprovalData; + + constructor(data: Erc20ApprovalInput, core: Core) { + this._data = Erc20Approval.parse(data, core); + } + + static parse = (data: Erc20ApprovalInput, core: Core): Erc20ApprovalData => ({ + ...data, + chain: EvmChain.create(data.chain, core), + contractAddress: EvmAddress.create(data.contractAddress, core), + fromWallet: EvmAddress.create(data.fromWallet, core), + toWallet: EvmAddress.create(data.toWallet, core), + blockTimestamp: dateInputToDate(data.blockTimestamp), + blockNumber: BigNumber.create(data.blockNumber), + value: BigNumber.create(data.value), + transactionIndex: Number(data.transactionIndex), + logIndex: Number(data.logIndex), + }); + + /** + * Check the equality between two Erc20 approvals + * @param dataA - The first approval to compare + * @param dataB - The second approval to compare + * @example Erc20Approval.equals(dataA, dataB) + * @returns true if the approvals are equal, false otherwise + */ + static equals(dataA: Erc20Approval | Erc20ApprovalInput, dataB: Erc20Approval | Erc20ApprovalInput) { + const approvalA = Erc20Approval.create(dataA); + const approvalB = Erc20Approval.create(dataB); + + return JSON.stringify(approvalA.toJSON()) === JSON.stringify(approvalB.toJSON()); + } + + /** + * Checks the equality of the current approval with another erc20 approval + * @param data - the approval to compare with + * @example approval.equals(data) + * @returns true if the approvals are equal, false otherwise + */ + equals(data: Erc20Approval | Erc20ApprovalInput): boolean { + return Erc20Approval.equals(this, data); + } + + /** + * @returns a JSON representation of the approval. + * @example approval.toJSON() + */ + toJSON() { + const data = this._data; + return { + ...data, + chain: data.chain.format(), + contractAddress: data.contractAddress.format(), + blockNumber: data.blockNumber.toString(), + toWallet: data.toWallet.format(), + fromWallet: data.fromWallet.format(), + value: data.value.toString(), + }; + } + + /** + * @returns a JSON representation of the approval. + * @example approval.format() + */ + format() { + return this.toJSON(); + } + + /** + * @returns all the data without casting it to JSON. + * @example approval.result + */ + get result() { + return this._data; + } + + /** + * @returns the toWallet of the approval + * @example approval.toWallet // EvmAddress + */ + get toWallet() { + return this._data.toWallet; + } + + /** + * @returns the fromWallet of the approval + * @example approval.fromWallet // EvmAddress + */ + get fromWallet() { + return this._data.fromWallet; + } + + /** + * @returns the contractAddress of the approval + * @example approval.contractAddress // EvmAddress + */ + get contractAddress() { + return this._data.contractAddress; + } + + /** + * @returns the block hash of the approval + * @example approval.blockHash // "0x0372c302e3c52e8f2e15d155e2c545e6d802e479236564af052759253b20fd86" + */ + get blockHash() { + return this._data.blockHash; + } + + /** + * @returns the block number of the approval + * @example approval.blockNumber // BigNumber + */ + get blockNumber() { + return this._data.blockNumber; + } + + /** + * @returns the block timestamp of the approval + * @example approval.blockTimestamp // Date + */ + get blockTimestamp() { + return this._data.blockTimestamp; + } + + /** + * @returns the chain of the approval + * @example approval.chain // EvmChain + */ + get chain() { + return this._data.chain; + } + + /** + * @returns the transaction hash of the approval + * @example approval.transactionHash // "0x0372c302e3c52e8f2e15d155e2c545e6d802e479236564af052759253b20fd86" + */ + get transactionHash() { + return this._data.transactionHash; + } + + /** + * @returns the value of the approval + * @example approval.value // BigNumber + */ + get value() { + return this._data.value; + } + + /** + * @returns the transactionIndex of the approval + * @example approval.transactionIndex // 3 + */ + get transactionIndex() { + return this._data.transactionIndex; + } + + /** + * @returns the logIndex of the approval + * @example approval.logIndex // 2 + */ + get logIndex() { + return this._data.logIndex; + } +} diff --git a/packages/common/evmUtils/src/dataTypes/Erc20Approval/index.ts b/packages/common/evmUtils/src/dataTypes/Erc20Approval/index.ts new file mode 100644 index 0000000000..aaa3a61936 --- /dev/null +++ b/packages/common/evmUtils/src/dataTypes/Erc20Approval/index.ts @@ -0,0 +1,2 @@ +export * from './Erc20Approval'; +export * from './types'; diff --git a/packages/common/evmUtils/src/dataTypes/Erc20Approval/types.ts b/packages/common/evmUtils/src/dataTypes/Erc20Approval/types.ts new file mode 100644 index 0000000000..b8dace0844 --- /dev/null +++ b/packages/common/evmUtils/src/dataTypes/Erc20Approval/types.ts @@ -0,0 +1,52 @@ +import { BigNumber, BigNumberish, DateInput } from '@moralisweb3/common-core'; +import { EvmAddressish, EvmAddress } from '../EvmAddress'; +import { EvmChain, EvmChainish } from '../EvmChain'; + +/** + * This can be any object with valid erc20 approval data. + * @example + * ``` + * const input = { + * chain: 1, + * toWallet: "0x09f4fc6081026c85070886599e83f599ecf82405", + * contractAddress: "0xa0e8fed3426391fdb446516799c4d6248e2b2860", + * blockHash: "0xa5f87d4341642b89e3ccb81449e3083032c36fface2c2042941b8bd9afe83f79", + * blockNumber: "16868690", + * blockTimestamp: "2023-03-20T11:48:59.000Z", + * transactionHash: "0xb7b4d321e2ab26c1cde1a2ef49413e21b65dcc663d6de8f75ddbdd868b98b4bf", + * transactionIndex: "4", + * logIndex: "25", + * value: "100000000000000000000000000000" + * } + * ``` + */ +export interface Erc20ApprovalInput { + chain: EvmChainish; + fromWallet: EvmAddressish; + toWallet: EvmAddressish; + contractAddress: EvmAddressish; + blockHash: string; + blockNumber: BigNumberish; + blockTimestamp: DateInput; + transactionHash: string; + transactionIndex: number; + logIndex: number; + value: BigNumberish; +} + +/** + * This is the return type of Erc20Approval + */ +export interface Erc20ApprovalData { + chain: EvmChain; + fromWallet: EvmAddress; + toWallet: EvmAddress; + contractAddress: EvmAddress; + blockHash: string; + blockNumber: BigNumber; + blockTimestamp: Date; + transactionHash: string; + transactionIndex: number; + logIndex: number; + value: BigNumber; +} diff --git a/packages/common/evmUtils/src/dataTypes/index.ts b/packages/common/evmUtils/src/dataTypes/index.ts index 259e33b2f0..0241d41a0d 100644 --- a/packages/common/evmUtils/src/dataTypes/index.ts +++ b/packages/common/evmUtils/src/dataTypes/index.ts @@ -1,4 +1,5 @@ export * from './Erc20'; +export * from './Erc20Approval'; export * from './Erc20Mint'; export * from './Erc20Transfer'; export * from './Erc20Value'; diff --git a/packages/common/evmUtils/src/operations/openapi.ts b/packages/common/evmUtils/src/operations/openapi.ts index 8a3c603d23..67f60666ce 100644 --- a/packages/common/evmUtils/src/operations/openapi.ts +++ b/packages/common/evmUtils/src/operations/openapi.ts @@ -403,11 +403,59 @@ export interface components { */ value: string; }; - erc20MintResponse: { + erc20Approval: { + /** @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; + }; + erc20MintsResponse: { /** @description The cursor to get to the next page */ cursor?: string; result?: components["schemas"]["erc20Mint"][]; }; + erc20ApprovalsResponse: { + /** @description The cursor to get to the next page */ + cursor?: string; + result?: components["schemas"]["erc20Approval"][]; + }; blockTransaction: { /** * @description The hash of the transaction @@ -2696,7 +2744,38 @@ export interface operations { responses: { 200: { content: { - "application/json": components["schemas"]["erc20MintResponse"]; + "application/json": components["schemas"]["erc20MintsResponse"]; + }; + }; + }; + }; + getErc20Approvals: { + parameters: { + query: { + /** The chain to query */ + chain?: components["schemas"]["chainList"]; + /** The block number from which the approvals will be returned */ + from_block?: number; + /** The block number to which the approvals 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"]["erc20ApprovalsResponse"]; }; }; }; diff --git a/packages/common/evmUtils/src/operations/operations.ts b/packages/common/evmUtils/src/operations/operations.ts index 9fa462ce71..00b2792a6f 100644 --- a/packages/common/evmUtils/src/operations/operations.ts +++ b/packages/common/evmUtils/src/operations/operations.ts @@ -32,6 +32,7 @@ import { getTokenTransfersOperation, getWalletTokenBalancesOperation, getErc20MintsOperation, + getErc20ApprovalsOperation, } from './token'; import { getWalletTokenTransfersOperation } from './token/getWalletTokenTransfersOperation'; import { @@ -49,6 +50,7 @@ export const operations = [ getContractLogsOperation, getContractNFTsOperation, getDateToBlockOperation, + getErc20ApprovalsOperation, getErc20MintsOperation, getMultipleNFTsOperation, getNativeBalanceOperation, diff --git a/packages/common/evmUtils/src/operations/token/getErc20ApprovalsOperation.test.ts b/packages/common/evmUtils/src/operations/token/getErc20ApprovalsOperation.test.ts new file mode 100644 index 0000000000..7cd4d4d757 --- /dev/null +++ b/packages/common/evmUtils/src/operations/token/getErc20ApprovalsOperation.test.ts @@ -0,0 +1,63 @@ +import MoralisCore from '@moralisweb3/common-core'; +import { EvmAddress, EvmChain } from '../../dataTypes'; +import { getErc20ApprovalsOperation, GetErc20ApprovalsRequest } from './getErc20ApprovalsOperation'; + +describe('getErc20ApprovalsOperation', () => { + 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 = { + chain: EvmChain.create(chain, core), + fromBlock: 10, + toBlock: 20, + cursor: 'CURSOR1', + limit: 333, + contractAddresses: [contractAddresses], + excludeContracts: [excludeContracts], + walletAddresses: [walletAddresses], + excludeWallets: [excludeWallets], + }; + + const serializedRequest = getErc20ApprovalsOperation.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([contractAddresses.toLowerCase()]); + expect(serializedRequest.excludeContracts).toStrictEqual([excludeContracts.toLowerCase()]); + expect(serializedRequest.walletAddresses).toStrictEqual([walletAddresses.toLowerCase()]); + expect(serializedRequest.excludeWallets).toStrictEqual([excludeWallets.toLowerCase()]); + + const deserializedRequest = getErc20ApprovalsOperation.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, + ); + }); +}); diff --git a/packages/common/evmUtils/src/operations/token/getErc20ApprovalsOperation.ts b/packages/common/evmUtils/src/operations/token/getErc20ApprovalsOperation.ts new file mode 100644 index 0000000000..50675f9340 --- /dev/null +++ b/packages/common/evmUtils/src/operations/token/getErc20ApprovalsOperation.ts @@ -0,0 +1,135 @@ +import { + Core, + Camelize, + PaginatedOperation, + PaginatedResponseAdapter, + maybe, + toCamelCase, +} from '@moralisweb3/common-core'; +import { EvmChain, EvmChainish, EvmAddress, EvmAddressish } from '../../dataTypes'; +import { Erc20Approval } from '../../dataTypes/Erc20Approval'; +import { EvmChainResolver } from '../../EvmChainResolver'; +import { operations } from '../openapi'; + +type OperationId = 'getErc20Approvals'; + +type QueryParams = operations[OperationId]['parameters']['query']; +type RequestParams = QueryParams; + +type SuccessResponse = operations[OperationId]['responses']['200']['content']['application/json']; + +// Exports + +export interface GetErc20ApprovalsRequest + extends Camelize< + Omit + > { + chain?: EvmChainish; + contractAddresses?: EvmAddressish[]; + excludeContracts?: EvmAddressish[]; + walletAddresses?: EvmAddressish[]; + excludeWallets?: EvmAddressish[]; +} + +export type GetErc20ApprovalsJSONRequest = ReturnType; + +export type GetErc20ApprovalsJSONResponse = SuccessResponse; + +export type GetErc20ApprovalsResponse = ReturnType; + +export interface GetErc20ApprovalsResponseAdapter + extends PaginatedResponseAdapter {} + +/** Get the amount which the spender is allowed to withdraw on behalf of the owner. */ +export const getErc20ApprovalsOperation: PaginatedOperation< + GetErc20ApprovalsRequest, + GetErc20ApprovalsJSONRequest, + GetErc20ApprovalsResponse, + GetErc20ApprovalsJSONResponse['result'] +> = { + method: 'GET', + name: 'getErc20Approvals', + id: 'getErc20Approvals', + groupName: 'token', + urlPathPattern: '/erc20/approvals', + urlSearchParamNames: [ + 'chain', + 'fromBlock', + 'toBlock', + 'limit', + 'cursor', + 'contractAddresses', + 'excludeContracts', + 'walletAddresses', + 'excludeWallets', + ], + firstPageIndex: 0, + + getRequestUrlParams, + serializeRequest, + deserializeRequest, + deserializeResponse, +}; + +// Methods + +function getRequestUrlParams(request: GetErc20ApprovalsRequest, 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: GetErc20ApprovalsJSONResponse, + request: GetErc20ApprovalsRequest, + core: Core, +) { + return (jsonResponse.result ?? []).map((approval) => + Erc20Approval.create( + { + ...toCamelCase(approval), + chain: EvmChainResolver.resolve(request.chain, core), + }, + core, + ), + ); +} + +function serializeRequest(request: GetErc20ApprovalsRequest, 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: GetErc20ApprovalsJSONRequest, core: Core): GetErc20ApprovalsRequest { + 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)), + }; +} diff --git a/packages/common/evmUtils/src/operations/token/index.ts b/packages/common/evmUtils/src/operations/token/index.ts index dc4e7a7a13..8c39f1ddfa 100644 --- a/packages/common/evmUtils/src/operations/token/index.ts +++ b/packages/common/evmUtils/src/operations/token/index.ts @@ -1,3 +1,4 @@ +export * from './getErc20ApprovalsOperation'; export * from './getErc20MintsOperation'; export * from './getTokenAllowanceOperation'; export * from './getTokenMetadataBySymbolOperation'; diff --git a/packages/evmApi/integration/mocks/endpoints/getErc20Approvals.ts b/packages/evmApi/integration/mocks/endpoints/getErc20Approvals.ts new file mode 100644 index 0000000000..04ff79c1f9 --- /dev/null +++ b/packages/evmApi/integration/mocks/endpoints/getErc20Approvals.ts @@ -0,0 +1,88 @@ +import { MockScenarios } from '@moralisweb3/test-utils'; +import { createErrorResponse } from '../response/errorResponse'; + +export const mockGetErc20Approvals = MockScenarios.create( + { + method: 'get', + name: 'mockGetErc20Approvals', + url: `/erc20/approvals`, + getParams: ({ req }) => { + return { + limit: req.url.searchParams.get('limit'), + from_block: req.url.searchParams.get('from_block'), + to_block: req.url.searchParams.get('to_block'), + contract_addresses: req.url.searchParams.getAll('contract_addresses[]'), + }; + }, + }, + [ + { + condition: { + limit: '3', + from_block: '16000000', + to_block: '16867742', + contract_addresses: [ + '0x5e8422345238f34275888049021821e8e08caa1f', + '0x6fadf4aea85e1cd1d2b4b57d65954b424ddaa6ae', + ], + }, + response: { + cursor: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YWx1ZSI6IjE2ODY3NzQyIiwib2Zmc2V0IjozLCJpYXQiOjE2NzkzOTQ4Mjl9.CJ5i7l-auXpDp7jAqVpqL5O8MIRaXPiHMHVK5uYdtWo', + result: [ + { + from_wallet: '0xbafa44efe7901e04e39dad13167d089c559c1138', + to_wallet: '0xac3e018457b222d93114458476f3e3416abbe38f', + contract_address: '0x5e8422345238f34275888049021821e8e08caa1f', + block_hash: '0x987cab4a8a33ac6d1112f44f59bdda0410059e7bb033507ed8ebcbade331459c', + block_number: '16867742', + block_timestamp: '2023-03-20T08:37:35.000Z', + transaction_hash: '0x61b87b32196fda49b93998236fa0d51e0a0598ab0a052fa706053391d1950be2', + transaction_index: '148', + log_index: '356', + value: '80000000000000000', + }, + { + from_wallet: '0xbafa44efe7901e04e39dad13167d089c559c1138', + to_wallet: '0xac3e018457b222d93114458476f3e3416abbe38f', + contract_address: '0x5e8422345238f34275888049021821e8e08caa1f', + block_hash: '0x987cab4a8a33ac6d1112f44f59bdda0410059e7bb033507ed8ebcbade331459c', + block_number: '16867742', + block_timestamp: '2023-03-20T08:37:35.000Z', + transaction_hash: '0x61b87b32196fda49b93998236fa0d51e0a0598ab0a052fa706053391d1950be2', + transaction_index: '148', + log_index: '355', + value: '80000000000000000', + }, + { + from_wallet: '0xa7ba1c6d5ffa3ce2542e04bc3e4b4021bcc2e134', + to_wallet: '0x000000000022d473030f116ddee9f6b43ac78ba3', + contract_address: '0x6fadf4aea85e1cd1d2b4b57d65954b424ddaa6ae', + block_hash: '0x987cab4a8a33ac6d1112f44f59bdda0410059e7bb033507ed8ebcbade331459c', + block_number: '16867742', + block_timestamp: '2023-03-20T08:37:35.000Z', + transaction_hash: '0x0ece9d47857cf8575500e97b6332c6aae9a65eba547338dba83e91a1c873b744', + transaction_index: '141', + log_index: '340', + value: '115792089237316195423570985008687907853269984665640563249457584007913129639935', + }, + ], + }, + }, + { + condition: { + contract_addresses: ['0x0000000000000000000000000000000000000000'], + }, + response: { + cursor: null, + result: [], + }, + }, + { + condition: { + contract_addresses: ['oops'], + }, + response: createErrorResponse("contract_addresses with value ''oops'' is not a valid hex address"), + }, + ], +); diff --git a/packages/evmApi/integration/mocks/mockServer.ts b/packages/evmApi/integration/mocks/mockServer.ts index e1e79f17a1..631894005b 100644 --- a/packages/evmApi/integration/mocks/mockServer.ts +++ b/packages/evmApi/integration/mocks/mockServer.ts @@ -43,6 +43,7 @@ import { mockSearchNFTs } from './endpoints/searchNFTs'; import { mockSyncNFTContract } from './endpoints/syncNFTContract'; import { mockGetMultipleNFTs } from './endpoints/getMultipleNFTs'; import { mockGetErc20Mints } from './endpoints/getErc20Mints'; +import { mockGetErc20Approvals } from './endpoints/getErc20Approvals'; const handler = [ mockGetDateToBlock, @@ -88,6 +89,7 @@ const handler = [ mockGetWalletNFTCollections, mockGetMultipleNFTs, mockGetErc20Mints, + mockGetErc20Approvals, ]; export const mockServer = MockServer.create({ apiKey: MOCK_API_KEY, apiRoot: EVM_API_ROOT }, handler).start(); diff --git a/packages/evmApi/integration/test/getErc20Approvals.test.ts b/packages/evmApi/integration/test/getErc20Approvals.test.ts new file mode 100644 index 0000000000..24611980e3 --- /dev/null +++ b/packages/evmApi/integration/test/getErc20Approvals.test.ts @@ -0,0 +1,58 @@ +import { EvmApi } from '../../src/EvmApi'; +import { cleanEvmApi, setupEvmApi } from '../setup'; + +describe('getErc20Approvals', () => { + let EvmApi: EvmApi; + + beforeAll(() => { + EvmApi = setupEvmApi(); + }); + + afterAll(() => { + cleanEvmApi(); + }); + + describe('Get ERC20 Approvals', () => { + it('should get correct ERC20 transfers, based on the provided contract addresses', async () => { + const { result, pagination } = await EvmApi.token.getErc20Approvals({ + limit: 3, + fromBlock: 16000000, + toBlock: 16867742, + contractAddresses: ['0x5e8422345238f34275888049021821e8e08caa1f', '0x6fadf4aea85e1cd1d2b4b57d65954b424ddaa6ae'], + }); + + expect(result.length).toEqual(3); + expect(pagination.cursor).toBeDefined(); + + const transfer = result[0]; + + expect(transfer.fromWallet.lowercase).toBe('0xbafa44efe7901e04e39dad13167d089c559c1138'); + expect(transfer.toWallet.lowercase).toBe('0xac3e018457b222d93114458476f3e3416abbe38f'); + expect(transfer.contractAddress.lowercase).toBe('0x5e8422345238f34275888049021821e8e08caa1f'); + expect(transfer.blockHash).toBe('0x987cab4a8a33ac6d1112f44f59bdda0410059e7bb033507ed8ebcbade331459c'); + expect(transfer.blockNumber.toString()).toBe('16867742'); + expect(transfer.blockTimestamp.toISOString()).toBe('2023-03-20T08:37:35.000Z'); + expect(transfer.transactionHash).toBe('0x61b87b32196fda49b93998236fa0d51e0a0598ab0a052fa706053391d1950be2'); + expect(transfer.transactionIndex).toBe(148); + expect(transfer.logIndex).toBe(356); + expect(transfer.value.toString()).toBe('80000000000000000'); + }); + + it('should get handle no results', async () => { + const { result, pagination } = await EvmApi.token.getErc20Approvals({ + contractAddresses: ['0x0000000000000000000000000000000000000000'], + }); + + expect(result.length).toEqual(0); + expect(pagination.cursor).toBe(null); + }); + + it('should get handle invalid address input errors', async () => { + expect( + EvmApi.token.getErc20Approvals({ + contractAddresses: ['oops'], + }), + ).rejects.toThrowError(`[C0005] Invalid address provided`); + }); + }); +}); diff --git a/packages/evmApi/src/generated/ClientEvmApi.ts b/packages/evmApi/src/generated/ClientEvmApi.ts index d96e93f708..57678648b0 100644 --- a/packages/evmApi/src/generated/ClientEvmApi.ts +++ b/packages/evmApi/src/generated/ClientEvmApi.ts @@ -1,6 +1,6 @@ // CAUTION: This file is automatically generated. Do not edit it manually! -import { endpointWeightsOperation, EndpointWeightsResponseAdapter, runContractFunctionOperation, RunContractFunctionRequest, RunContractFunctionResponseAdapter, web3ApiVersionOperation, Web3ApiVersionResponseAdapter, getBlockOperation, GetBlockRequest, GetBlockResponseAdapter, getDateToBlockOperation, GetDateToBlockRequest, GetDateToBlockResponseAdapter, getContractEventsOperation, GetContractEventsRequest, GetContractEventsResponseAdapter, getContractLogsOperation, GetContractLogsRequest, GetContractLogsResponseAdapter, getContractNFTsOperation, GetContractNFTsRequest, GetContractNFTsResponseAdapter, getMultipleNFTsOperation, GetMultipleNFTsRequest, GetMultipleNFTsResponseAdapter, getNFTContractMetadataOperation, GetNFTContractMetadataRequest, GetNFTContractMetadataResponseAdapter, getNFTContractTransfersOperation, GetNFTContractTransfersRequest, GetNFTContractTransfersResponseAdapter, getNFTLowestPriceOperation, GetNFTLowestPriceRequest, GetNFTLowestPriceResponseAdapter, getNFTMetadataOperation, GetNFTMetadataRequest, GetNFTMetadataResponseAdapter, getNFTOwnersOperation, GetNFTOwnersRequest, GetNFTOwnersResponseAdapter, getNFTTokenIdOwnersOperation, GetNFTTokenIdOwnersRequest, GetNFTTokenIdOwnersResponseAdapter, getNFTTradesOperation, GetNFTTradesRequest, GetNFTTradesResponseAdapter, getNFTTransfersByBlockOperation, GetNFTTransfersByBlockRequest, GetNFTTransfersByBlockResponseAdapter, getNFTTransfersFromToBlockOperation, GetNFTTransfersFromToBlockRequest, GetNFTTransfersFromToBlockResponseAdapter, getNFTTransfersOperation, GetNFTTransfersRequest, GetNFTTransfersResponseAdapter, getWalletNFTCollectionsOperation, GetWalletNFTCollectionsRequest, GetWalletNFTCollectionsResponseAdapter, getWalletNFTsOperation, GetWalletNFTsRequest, GetWalletNFTsResponseAdapter, getWalletNFTTransfersOperation, GetWalletNFTTransfersRequest, GetWalletNFTTransfersResponseAdapter, reSyncMetadataOperation, ReSyncMetadataRequest, ReSyncMetadataResponseAdapter, searchNFTsOperation, SearchNFTsRequest, SearchNFTsResponseAdapter, syncNFTContractOperation, SyncNFTContractRequest, SyncNFTContractResponseAdapter, getErc20MintsOperation, GetErc20MintsRequest, GetErc20MintsResponseAdapter, getTokenAllowanceOperation, GetTokenAllowanceRequest, GetTokenAllowanceResponseAdapter, getTokenMetadataBySymbolOperation, GetTokenMetadataBySymbolRequest, GetTokenMetadataBySymbolResponseAdapter, getTokenMetadataOperation, GetTokenMetadataRequest, GetTokenMetadataResponseAdapter, getTokenPriceOperation, GetTokenPriceRequest, GetTokenPriceResponseAdapter, getTokenTransfersOperation, GetTokenTransfersRequest, GetTokenTransfersResponseAdapter, getWalletTokenBalancesOperation, GetWalletTokenBalancesRequest, GetWalletTokenBalancesResponseAdapter, getWalletTokenTransfersOperation, GetWalletTokenTransfersRequest, GetWalletTokenTransfersResponseAdapter, getNativeBalanceOperation, GetNativeBalanceRequest, GetNativeBalanceResponseAdapter, getNativeBalancesForAddressesOperation, GetNativeBalancesForAddressesRequest, GetNativeBalancesForAddressesResponseAdapter, getPairAddressOperation, GetPairAddressRequest, GetPairAddressResponseAdapter, getPairReservesOperation, GetPairReservesRequest, GetPairReservesResponseAdapter, getTransactionOperation, GetTransactionRequest, GetTransactionResponseAdapter, getWalletTransactionsOperation, GetWalletTransactionsRequest, GetWalletTransactionsResponseAdapter, getWalletTransactionsVerboseOperation, GetWalletTransactionsVerboseRequest, GetWalletTransactionsVerboseResponseAdapter, resolveAddressOperation, ResolveAddressRequest, ResolveAddressResponseAdapter, resolveDomainOperation, ResolveDomainRequest, ResolveDomainResponseAdapter, uploadFolderOperation, UploadFolderRequest, UploadFolderResponseAdapter } from '@moralisweb3/common-evm-utils'; +import { endpointWeightsOperation, EndpointWeightsResponseAdapter, runContractFunctionOperation, RunContractFunctionRequest, RunContractFunctionResponseAdapter, web3ApiVersionOperation, Web3ApiVersionResponseAdapter, getBlockOperation, GetBlockRequest, GetBlockResponseAdapter, getDateToBlockOperation, GetDateToBlockRequest, GetDateToBlockResponseAdapter, getContractEventsOperation, GetContractEventsRequest, GetContractEventsResponseAdapter, getContractLogsOperation, GetContractLogsRequest, GetContractLogsResponseAdapter, getContractNFTsOperation, GetContractNFTsRequest, GetContractNFTsResponseAdapter, getMultipleNFTsOperation, GetMultipleNFTsRequest, GetMultipleNFTsResponseAdapter, getNFTContractMetadataOperation, GetNFTContractMetadataRequest, GetNFTContractMetadataResponseAdapter, getNFTContractTransfersOperation, GetNFTContractTransfersRequest, GetNFTContractTransfersResponseAdapter, getNFTLowestPriceOperation, GetNFTLowestPriceRequest, GetNFTLowestPriceResponseAdapter, getNFTMetadataOperation, GetNFTMetadataRequest, GetNFTMetadataResponseAdapter, getNFTOwnersOperation, GetNFTOwnersRequest, GetNFTOwnersResponseAdapter, getNFTTokenIdOwnersOperation, GetNFTTokenIdOwnersRequest, GetNFTTokenIdOwnersResponseAdapter, getNFTTradesOperation, GetNFTTradesRequest, GetNFTTradesResponseAdapter, getNFTTransfersByBlockOperation, GetNFTTransfersByBlockRequest, GetNFTTransfersByBlockResponseAdapter, getNFTTransfersFromToBlockOperation, GetNFTTransfersFromToBlockRequest, GetNFTTransfersFromToBlockResponseAdapter, getNFTTransfersOperation, GetNFTTransfersRequest, GetNFTTransfersResponseAdapter, getWalletNFTCollectionsOperation, GetWalletNFTCollectionsRequest, GetWalletNFTCollectionsResponseAdapter, getWalletNFTsOperation, GetWalletNFTsRequest, GetWalletNFTsResponseAdapter, getWalletNFTTransfersOperation, GetWalletNFTTransfersRequest, GetWalletNFTTransfersResponseAdapter, reSyncMetadataOperation, ReSyncMetadataRequest, ReSyncMetadataResponseAdapter, searchNFTsOperation, SearchNFTsRequest, SearchNFTsResponseAdapter, syncNFTContractOperation, SyncNFTContractRequest, SyncNFTContractResponseAdapter, getErc20ApprovalsOperation, GetErc20ApprovalsRequest, GetErc20ApprovalsResponseAdapter, getErc20MintsOperation, GetErc20MintsRequest, GetErc20MintsResponseAdapter, getTokenAllowanceOperation, GetTokenAllowanceRequest, GetTokenAllowanceResponseAdapter, getTokenMetadataBySymbolOperation, GetTokenMetadataBySymbolRequest, GetTokenMetadataBySymbolResponseAdapter, getTokenMetadataOperation, GetTokenMetadataRequest, GetTokenMetadataResponseAdapter, getTokenPriceOperation, GetTokenPriceRequest, GetTokenPriceResponseAdapter, getTokenTransfersOperation, GetTokenTransfersRequest, GetTokenTransfersResponseAdapter, getWalletTokenBalancesOperation, GetWalletTokenBalancesRequest, GetWalletTokenBalancesResponseAdapter, getWalletTokenTransfersOperation, GetWalletTokenTransfersRequest, GetWalletTokenTransfersResponseAdapter, getNativeBalanceOperation, GetNativeBalanceRequest, GetNativeBalanceResponseAdapter, getNativeBalancesForAddressesOperation, GetNativeBalancesForAddressesRequest, GetNativeBalancesForAddressesResponseAdapter, getPairAddressOperation, GetPairAddressRequest, GetPairAddressResponseAdapter, getPairReservesOperation, GetPairReservesRequest, GetPairReservesResponseAdapter, getTransactionOperation, GetTransactionRequest, GetTransactionResponseAdapter, getWalletTransactionsOperation, GetWalletTransactionsRequest, GetWalletTransactionsResponseAdapter, getWalletTransactionsVerboseOperation, GetWalletTransactionsVerboseRequest, GetWalletTransactionsVerboseResponseAdapter, resolveAddressOperation, ResolveAddressRequest, ResolveAddressResponseAdapter, resolveDomainOperation, ResolveDomainRequest, ResolveDomainResponseAdapter, uploadFolderOperation, UploadFolderRequest, UploadFolderResponseAdapter } from '@moralisweb3/common-evm-utils'; import { OperationResolver, NullableOperationResolver, PaginatedOperationResolver } from '@moralisweb3/api-utils'; import { ApiModule, } from '@moralisweb3/common-core'; export abstract class ClientEvmApi extends ApiModule { @@ -97,6 +97,9 @@ export abstract class ClientEvmApi extends ApiModule { }; public readonly token = { + getErc20Approvals: (request: GetErc20ApprovalsRequest): Promise => { + return new PaginatedOperationResolver(getErc20ApprovalsOperation, this.baseUrl, this.core).fetch(request); + }, getErc20Mints: (request: GetErc20MintsRequest): Promise => { return new PaginatedOperationResolver(getErc20MintsOperation, this.baseUrl, this.core).fetch(request); }, diff --git a/packages/next/src/hooks/evmApi/generated/index.ts b/packages/next/src/hooks/evmApi/generated/index.ts index 228b2646ab..6915db84c5 100644 --- a/packages/next/src/hooks/evmApi/generated/index.ts +++ b/packages/next/src/hooks/evmApi/generated/index.ts @@ -4,6 +4,7 @@ export * from './events/useEvmContractLogs' export * from './nft/useEvmContractNFTs' export * from './block/useEvmDateToBlock' + export * from './token/useEvmErc20Approvals' export * from './token/useEvmErc20Mints' export * from './balance/useEvmNativeBalance' export * from './nft/useEvmNFTContractMetadata' diff --git a/packages/next/src/hooks/evmApi/generated/token/useEvmErc20Approvals.ts b/packages/next/src/hooks/evmApi/generated/token/useEvmErc20Approvals.ts new file mode 100644 index 0000000000..7f0acfb118 --- /dev/null +++ b/packages/next/src/hooks/evmApi/generated/token/useEvmErc20Approvals.ts @@ -0,0 +1,37 @@ +import { + getErc20ApprovalsOperation as operation, + GetErc20ApprovalsRequest, +} from 'moralis/common-evm-utils'; +import { FetchParams } from '../../../types'; +import { useResolverPaginated } from '../../../resolvers'; + +export const useEvmErc20Approvals = ( + request?: GetErc20ApprovalsRequest, + fetchParams?: FetchParams, +) => { + const { data, error, fetch, isFetching } = useResolverPaginated({ + endpoint: 'evmApi/getErc20Approvals', + operation, + request, + fetchParams, + }); + + return { + data: data?.data, + cursor: data?.cursor, + page: data?.page, + pageSize: data?.pageSize, + total: data?.total, + error, + fetch, + /** + * @deprecated use `fetch()` instead + */ + refetch: () => fetch(), + isFetching, + /** + * @deprecated use `isFetching` instead + */ + isValidating: isFetching, + }; +}; diff --git a/packages/react/src/hooks/evmApi/generated/index.ts b/packages/react/src/hooks/evmApi/generated/index.ts index 17ce1b1667..a91eb7edca 100644 --- a/packages/react/src/hooks/evmApi/generated/index.ts +++ b/packages/react/src/hooks/evmApi/generated/index.ts @@ -4,6 +4,7 @@ export * from './useEvmContractLogs'; export * from './useEvmContractNFTs'; export * from './useEvmDateToBlock'; export * from './useEvmEndpointWeights'; +export * from './useEvmErc20Approvals' export * from './useEvmErc20Mints' export * from './useEvmMultipleNFTs'; export * from './useEvmNativeBalance'; diff --git a/packages/react/src/hooks/evmApi/generated/useEvmErc20Approvals.ts b/packages/react/src/hooks/evmApi/generated/useEvmErc20Approvals.ts new file mode 100644 index 0000000000..2d7030c225 --- /dev/null +++ b/packages/react/src/hooks/evmApi/generated/useEvmErc20Approvals.ts @@ -0,0 +1,33 @@ +import Moralis from 'moralis'; +import { GetErc20ApprovalsRequest, GetErc20ApprovalsResponse, getErc20ApprovalsOperation } from 'moralis/common-evm-utils'; +import { useMemo } from 'react'; +import { QueryOptions } from '../../types'; +import { usePaginatedOperationResolver, useQuery } from '../../utils'; + + +export type UseEvmErc20ApprovalsParams = GetErc20ApprovalsRequest; +export type UseEvmErc20ApprovalsQueryOptions = QueryOptions; + +export function useEvmErc20Approvals({ chain, fromBlock, toBlock, limit, cursor, contractAddresses, excludeContracts, walletAddresses, excludeWallets }: UseEvmErc20ApprovalsParams = {}, queryOptions: UseEvmErc20ApprovalsQueryOptions = {}) { + const resolver = usePaginatedOperationResolver(getErc20ApprovalsOperation, Moralis.EvmApi.baseUrl); + + + const queryKey: [string, GetErc20ApprovalsRequest] = useMemo(() => { + return [ + getErc20ApprovalsOperation.id, + { + chain, fromBlock, toBlock, limit, cursor, contractAddresses, excludeContracts, walletAddresses, excludeWallets + }, + ]; + }, [chain, fromBlock, toBlock, limit, cursor, contractAddresses, excludeContracts, walletAddresses, excludeWallets]); + + return useQuery({ + ...queryOptions, + queryKey, + queryFn: async ({ queryKey: [_id, request] }) => { + const response = await resolver.fetch(request); + return response.result; + }, + enabled: queryOptions.enabled, + }); +} \ No newline at end of file