From 969f7d6b89caee93565ef2f2b618e11c577cef2c Mon Sep 17 00:00:00 2001 From: Sleyter Sandoval Date: Thu, 2 May 2024 22:12:43 -0500 Subject: [PATCH] feat: add date filters to transactions endpoint --- .env | 2 +- src/api/openapi.js | 36 ++++++++++++ src/blockscoutApi/index.ts | 70 +++++++++++++++++------ src/blockscoutApi/types.ts | 80 +++++++++++---------------- src/blockscoutApi/utils.ts | 62 +++++++++++---------- src/controller/httpsAPI.ts | 20 +++++-- src/index.ts | 6 +- src/repository/DataSource.ts | 10 +++- src/rskExplorerApi/index.ts | 4 ++ src/service/address/AddressService.ts | 21 ++++--- test/MockProvider.ts | 4 ++ test/address.test.ts | 2 +- 12 files changed, 202 insertions(+), 115 deletions(-) diff --git a/.env b/.env index a0f06a6..bc72f19 100644 --- a/.env +++ b/.env @@ -6,4 +6,4 @@ NODE_URL=https://public-node.testnet.rsk.co NODE_MAINNET_URL=https://public-node.rsk.co CYPHER_ESTIMATE_FEE_URL=https://api.blockcypher.com/v1/btc/test3 CYPHER_ESTIMATE_FEE_MAINNET_URL=https://api.blockcypher.com/v1/btc/main -# API_URL=https://rootstock-testnet.blockscout.com/api \ No newline at end of file +API_URL=https://rootstock-testnet.blockscout.com/api \ No newline at end of file diff --git a/src/api/openapi.js b/src/api/openapi.js index 085ee2e..7ff9b8b 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -85,6 +85,24 @@ module.exports = { required: false, schema: { type: 'string' } }, + { + name: 'startTimestamp', + in: 'query', + description: 'starting block unix timestamp.', + required: false, + schema: { + type: 'string' + } + }, + { + name: 'endTimestamp', + in: 'query', + description: 'ending block unix timestamp.', + required: false, + schema: { + type: 'string' + } + }, { name: 'limit', in: 'query', @@ -295,6 +313,24 @@ module.exports = { default: '0' } }, + { + name: 'startTimestamp', + in: 'query', + description: 'starting block unix timestamp.', + required: false, + schema: { + type: 'string' + } + }, + { + name: 'endTimestamp', + in: 'query', + description: 'ending block unix timestamp.', + required: false, + schema: { + type: 'string' + } + }, { name: 'prev', in: 'query', diff --git a/src/blockscoutApi/index.ts b/src/blockscoutApi/index.ts index 8e1d727..ae2c0b8 100644 --- a/src/blockscoutApi/index.ts +++ b/src/blockscoutApi/index.ts @@ -1,9 +1,8 @@ import _axios from 'axios' import { DataSource } from '../repository/DataSource' import { - BalanceServerResponse, InternalTransactionResponse, ServerResponse, TokenBalanceServerResponse, - TokenServerResponse, TokenTransferApi, TransactionServerResponse, - TransactionsServerResponse + BalanceServerResponse, BlockResponse, InternalTransaction, ServerResponse, TokenBalanceServerResponse, + TokenServerResponse, TokenTransferApi, TransactionResponse } from './types' import { fromApiToInternalTransaction, fromApiToRtbcBalance, fromApiToTEvents, @@ -22,6 +21,18 @@ export class BlockscoutAPI extends DataSource { this.chainId = chainId } + getBlockNumberByTimestamp (timestamp: number, timeDirection: string) { + const params = { + module: 'block', + action: 'getblocknobytime', + timestamp, + closest: timeDirection + } + return this.axios?.get>(this.url, { params }) + .then(response => response.data.result.blockNumber) + .catch(() => '0') + } + getTokens () { return this.axios?.get(`${this.url}/v2/tokens`) .then(response => response.data.items @@ -47,13 +58,15 @@ export class BlockscoutAPI extends DataSource { .catch(this.errorHandling) } - async getEventsByAddress (address: string) { + async getEventsByAddress (address: string, limit?: string, startBlock?: number, endBlock?: number) { const params = { module: 'account', action: 'tokentx', - address: address.toLowerCase() + address: address.toLowerCase(), + ...(startBlock && { startblock: startBlock }), + ...(endBlock && { endblock: endBlock }) } - return this.axios?.get>(`${this.url}`, { params }) + return this.axios?.get>(`${this.url}`, { params }) .then(response => response.data.result .map(tokenTranfer => { @@ -63,25 +76,48 @@ export class BlockscoutAPI extends DataSource { } getTransaction (hash: string) { - return this.axios?.get(`${this.url}/v2/transactions/${hash}`) + const params = { + module: 'transaction', + action: 'gettxinfo', + txhash: hash + } + return this.axios?.get>(`${this.url}`, { params }) .then(response => - fromApiToTransaction(response.data)) + fromApiToTransaction(response.data.result)) .catch(this.errorHandling) } - getInternalTransactionByAddress (address: string) { - return this.axios?.get( - `${this.url}/v2/addresses/${address.toLowerCase()}/internal-transactions` - ) - .then(response => response.data.items.map(fromApiToInternalTransaction)) + getInternalTransactionByAddress (address: string, limit?: string, startBlock?: number, endBlock?: number) { + const params = { + module: 'account', + action: 'txlistinternal', + address, + ...(startBlock && { startblock: startBlock }), + ...(endBlock && { endblock: endBlock }) + } + return this.axios?.get>(this.url, { params }) + .then(response => response.data.result.map(fromApiToInternalTransaction)) .catch(this.errorHandling) } - getTransactionsByAddress (address: string) { - return this.axios?.get( - `${this.url}/v2/addresses/${address.toLowerCase()}/transactions` + getTransactionsByAddress (address: string, limit?: string, + prev?: string, + next?: string, + blockNumber?: string, + startTimestamp?: number, + endTimestamp?: number) { + const params = { + module: 'account', + action: 'txlist', + startblock: blockNumber, + address: address.toLowerCase(), + ...(startTimestamp && { start_timestamp: startTimestamp }), + ...(endTimestamp && { end_timestamp: endTimestamp }) + } + return this.axios?.get>( + `${this.url}`, { params } ) - .then(response => ({ data: response.data.items.map(fromApiToTransaction) })) + .then(response => ({ data: response.data.result.map(fromApiToTransaction) })) .catch(this.errorHandling) } } diff --git a/src/blockscoutApi/types.ts b/src/blockscoutApi/types.ts index b141862..9f7b1e5 100644 --- a/src/blockscoutApi/types.ts +++ b/src/blockscoutApi/types.ts @@ -110,60 +110,46 @@ export interface DecodedInput { } export interface InternalTransaction { - block: number - created_contract: any - error: any - from: Account - gas_limit: string - index: number - success: boolean - timestamp: string - to: Account - transaction_hash: string + blockNumber: string + callType: string + contractAddress: string + errCode: string + from: string + gas: string + gasUsed: string + index: string + input: string + isError: string + timeStamp: string + to: string + transactionHash: string type: string value: string } -export interface TransactionServerResponse { - timestamp: string - fee: Fee - gas_limit: string - block: number - status: string - method: string - confirmations: number - type: number - exchange_rate: any - to: Account - tx_burnt_fee: any - max_fee_per_gas: any - result: string +export interface TransactionResponse { + blockHash: string + blockNumber: string + confirmations: string + contractAddress: string + cumulativeGasUsed: string + from: string + gas: string + gasPrice: string + gasUsed: string hash: string - gas_price: string - priority_fee: any - base_fee_per_gas: any - from: Account - token_transfers: TokenTransfer[] - tx_types: string[] - gas_used: string - created_contract: any - position: number - nonce: number - has_error_in_internal_txs: boolean - actions: any[] - decoded_input: DecodedInput - token_transfers_overflow: boolean - raw_input: string + input: string + isError: string + nonce: string + timeStamp: string + to: string + transactionIndex: string + txreceipt_status: string value: string - max_priority_fee_per_gas: any - revert_reason: any - confirmation_duration: number[] - tx_tag: any } -export interface TransactionsServerResponse { - items: TransactionServerResponse[] - next_page_params: NextPageParams +export interface BlockResponse { + blockNumber: string } export interface BalanceServerResponse { @@ -229,5 +215,5 @@ export interface TokenTransferApi { export interface ServerResponse { message: string status: string - result: T[] + result: T } diff --git a/src/blockscoutApi/utils.ts b/src/blockscoutApi/utils.ts index 9ad868b..7881703 100644 --- a/src/blockscoutApi/utils.ts +++ b/src/blockscoutApi/utils.ts @@ -1,6 +1,6 @@ import { IApiToken, ITokenWithBalance, InternalTransaction, - Token, TokenTransferApi, TransactionServerResponse + Token, TokenTransferApi, TransactionResponse } from './types' import tokens from '@rsksmart/rsk-contract-metadata' import { toChecksumAddress } from '@rsksmart/rsk-utils' @@ -126,68 +126,70 @@ export const fromApiToTEvents = (tokenTransfer:TokenTransferApi): IEvent => txStatus: '0x1' }) -export const fromApiToTransaction = (transaction: TransactionServerResponse): ITransaction => - ({ +export const fromApiToTransaction = (transaction: TransactionResponse): ITransaction => { + const txType = transaction.input === '0x' ? 'normal' : 'contract call' + return ({ _id: '', hash: transaction.hash, - nonce: transaction.nonce, - blockHash: '', - blockNumber: transaction.block, + nonce: Number(transaction.nonce), + blockHash: transaction.blockHash, + blockNumber: Number(transaction.blockNumber), transactionIndex: 0, - from: transaction.from.hash, - to: transaction.to.hash, - gas: Number(transaction.gas_used), - gasPrice: transaction.gas_price, + from: transaction.from, + to: transaction.to, + gas: Number(transaction.gas), + gasPrice: transaction.gasPrice, value: transaction.value, - input: transaction.raw_input, + input: transaction.input, v: '', r: '', s: '', - type: String(transaction.type), - timestamp: Date.parse(transaction.timestamp) / 1000, + type: String(), + timestamp: Number(transaction.timeStamp), receipt: { transactionHash: transaction.hash, transactionIndex: 0, blockHash: '', - blockNumber: transaction.block, - cumulativeGasUsed: Number(transaction.gas_limit), - gasUsed: Number(transaction.gas_used), + blockNumber: Number(transaction.blockNumber), + cumulativeGasUsed: Number(transaction.gasUsed), + gasUsed: Number(transaction.gasUsed), contractAddress: null, logs: [], - from: transaction.from.hash, - to: transaction.to.hash, - status: transaction.status === 'ok' ? '0x1' : '0x0', + from: transaction.from, + to: transaction.to, + status: transaction.txreceipt_status === '1' ? '0x1' : '0x0', logsBloom: '', - type: String(transaction.type) + type: txType }, - txType: transaction.tx_types[0], + txType, txId: '' }) +} export const fromApiToInternalTransaction = (internalTransaction: InternalTransaction): IInternalTransaction => ({ _id: '', action: { callType: internalTransaction.type, - from: internalTransaction.from.hash, - to: internalTransaction.to.hash, + from: internalTransaction.from, + to: internalTransaction.to, value: internalTransaction.value, - gas: internalTransaction.gas_limit, + gas: internalTransaction.gas, input: '0x' }, blockHash: '', - blockNumber: internalTransaction.block, - transactionHash: internalTransaction.transaction_hash, - transactionPosition: internalTransaction.index, + blockNumber: Number(internalTransaction.blockNumber), + transactionHash: internalTransaction.transactionHash, + transactionPosition: Number(internalTransaction.index), type: internalTransaction.type, subtraces: 0, traceAddress: [], result: { - gasUsed: internalTransaction.gas_limit, + gasUsed: internalTransaction.gasUsed, output: '0x' }, - _index: internalTransaction.index, - timestamp: Date.parse(internalTransaction.timestamp) / 1000, + _index: Number(internalTransaction.index), + timestamp: Date.parse(internalTransaction.timeStamp) / 1000, internalTxId: '' }) diff --git a/src/controller/httpsAPI.ts b/src/controller/httpsAPI.ts index 3bd733d..f9b5a2d 100644 --- a/src/controller/httpsAPI.ts +++ b/src/controller/httpsAPI.ts @@ -112,19 +112,25 @@ export class HttpsAPI { this.app.get( '/address/:address/transactions', - async ({ params: { address }, query: { limit, prev, next, chainId = '31', blockNumber = '0' } }: Request, - res: Response, nextFunction: NextFunction) => { + async ({ + params: { address }, query: { + limit, prev, next, chainId = '31', + blockNumber = '0', startTimestamp = '0', endTimestamp + } + }: Request, + res: Response, nextFunction: NextFunction) => { try { chainIdSchema.validateSync({ chainId }) addressSchema.validateSync({ address }) - const transactions = await this.addressService.getTransactionsByAddress({ address: address as string, chainId: chainId as string, limit: limit as string, prev: prev as string, next: next as string, - blockNumber: blockNumber as string + blockNumber: blockNumber as string, + startTimestamp: Number(startTimestamp), + endTimestamp: Number(endTimestamp) }).catch(nextFunction) return this.responseJsonOk(res)(transactions) } catch (e) { @@ -164,7 +170,7 @@ export class HttpsAPI { '/address/:address', async (req, res, next: NextFunction) => { try { - const { limit, prev, next, chainId = '31', blockNumber = '0' } = req.query + const { limit, prev, next, chainId = '31', blockNumber = '0', startTimestamp = '0', endTimestamp } = req.query const { address } = req.params const data = await this.addressService.getAddressDetails({ chainId: chainId as string, @@ -172,7 +178,9 @@ export class HttpsAPI { blockNumber: blockNumber as string, limit: limit as string, prev: prev as string, - next: next as string + next: next as string, + startTimestamp: Number(startTimestamp), + endTimestamp: Number(endTimestamp) }) return this.responseJsonOk(res)(data) } catch (error) { diff --git a/src/index.ts b/src/index.ts index 364eb71..579a0f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ import { BitcoinDatasource, RSKDatasource, RSKNodeProvider } from './repository/ import BitcoinCore from './service/bitcoin/BitcoinCore' import { ethers } from 'ethers' import { AddressService } from './service/address/AddressService' -import { RSKExplorerAPI } from './rskExplorerApi' +import { BlockscoutAPI } from './blockscoutApi' async function main () { const environment = { @@ -51,8 +51,8 @@ async function main () { const bitcoinMapping: BitcoinDatasource = {} const nodeProvider: RSKNodeProvider = {} environment.NETWORKS.forEach(network => { - dataSourceMapping[network.ID] = new RSKExplorerAPI(network.API_URL, network.CHAIN_ID, axios, network.ID) - // dataSourceMapping[network.ID] = new BlockscoutAPI(network.API_URL, network.CHAIN_ID, axios, network.ID) + // dataSourceMapping[network.ID] = new RSKExplorerAPI(network.API_URL, network.CHAIN_ID, axios, network.ID) + dataSourceMapping[network.ID] = new BlockscoutAPI(network.API_URL, network.CHAIN_ID, axios, network.ID) bitcoinMapping[network.ID] = new BitcoinCore({ BLOCKBOOK_URL: network.BLOCKBOOK_URL, CYPHER_ESTIMATE_FEE_URL: network.CYPHER_ESTIMATE_FEE_URL diff --git a/src/repository/DataSource.ts b/src/repository/DataSource.ts index 402a3b8..c3e060f 100644 --- a/src/repository/DataSource.ts +++ b/src/repository/DataSource.ts @@ -14,16 +14,20 @@ export abstract class DataSource { } abstract getTokens(); + abstract getBlockNumberByTimestamp(timestamp: number, timeDirection: string); abstract getTokensByAddress(address: string); abstract getRbtcBalanceByAddress(address: string); - abstract getEventsByAddress(address: string, limit?: string); + abstract getEventsByAddress(address: string, limit?: string, startBlock?: number, endBlock?: number); abstract getTransaction(hash: string); - abstract getInternalTransactionByAddress(address: string, limit?: string); + abstract getInternalTransactionByAddress(address: string, limit?: string, startBlock?: number, endBlock?: number); abstract getTransactionsByAddress(address:string, limit?: string, prev?: string, next?: string, - blockNumber?: string); + blockNumber?: string, + startTimestamp?: number, + endTimestamp?: number, + ); } export type RSKDatasource = { diff --git a/src/rskExplorerApi/index.ts b/src/rskExplorerApi/index.ts index 89376be..2298228 100644 --- a/src/rskExplorerApi/index.ts +++ b/src/rskExplorerApi/index.ts @@ -22,6 +22,10 @@ export class RSKExplorerAPI extends DataSource { this.chainId = chainId } + getBlockNumberByTimestamp () { + return '0' + } + async getEventsByAddress (address:string, limit?: string) { const params = { module: 'events', diff --git a/src/service/address/AddressService.ts b/src/service/address/AddressService.ts index 560e8b9..4de439b 100644 --- a/src/service/address/AddressService.ts +++ b/src/service/address/AddressService.ts @@ -17,6 +17,8 @@ interface GetTransactionsByAddressFunction { prev?: string next?: string blockNumber: string + startTimestamp?: number, + endTimestamp?: number } interface GetPricesFunction { @@ -48,7 +50,7 @@ export class AddressService { } async getTransactionsByAddress ( - { chainId, address, limit, next, prev, blockNumber }: GetTransactionsByAddressFunction + { chainId, address, limit, next, prev, blockNumber, startTimestamp, endTimestamp }: GetTransactionsByAddressFunction ) { const dataSource = this.dataSourceMapping[chainId] /* A transaction has the following structure { to: string, from: string } @@ -56,8 +58,11 @@ export class AddressService { * (such as RBTC). */ const transactions: {data: IApiTransactions[], prev: string, next: string} = - await dataSource.getTransactionsByAddress(address, limit, prev, next, blockNumber) - + await dataSource.getTransactionsByAddress(address, limit, prev, next, blockNumber, startTimestamp, endTimestamp) + let startBlock = 0 + let endBlock = await this.providerMapping[chainId].getBlockNumber() + if (startTimestamp) { startBlock = await dataSource.getBlockNumberByTimestamp(startTimestamp, 'after') } + if (endTimestamp) { endBlock = await dataSource.getBlockNumberByTimestamp(endTimestamp, 'before') } /* We query events to find transactions when we send or receive a token(ERC20) * such as RIF,RDOC * Additionally, we query internal transactions because we could send or receive a cryptocurrency @@ -65,8 +70,8 @@ export class AddressService { * Finally, we filter by blocknumber and duplicates */ const hashes: string[] = await Promise.all([ - dataSource.getEventsByAddress(address, limit as string), - dataSource.getInternalTransactionByAddress(address, limit as string) + dataSource.getEventsByAddress(address, limit as string, startBlock, endBlock), + dataSource.getInternalTransactionByAddress(address, limit as string, startBlock, endBlock) ]) .then((promises) => { return promises.flat() @@ -112,12 +117,14 @@ export class AddressService { blockNumber, limit, prev, - next + next, + startTimestamp, + endTimestamp }: GetBalancesTransactionsPricesByAddress) { const [prices, tokens, transactions] = await Promise.all([ this.getLatestPrices(), this.getTokensByAddress({ chainId, address }), - this.getTransactionsByAddress({ chainId, address, blockNumber, limit, prev, next }) + this.getTransactionsByAddress({ chainId, address, blockNumber, limit, prev, next, startTimestamp, endTimestamp }) ]) return { prices, diff --git a/test/MockProvider.ts b/test/MockProvider.ts index f2c70b0..018defc 100644 --- a/test/MockProvider.ts +++ b/test/MockProvider.ts @@ -5,4 +5,8 @@ export class MockProvider extends ethers.providers.BaseProvider { Promise { return Promise.resolve(ethers.BigNumber.from('0x56900d33ca7fc0000')) } + + getBlockNumber (): Promise { + return Promise.resolve(100) + } } diff --git a/test/address.test.ts b/test/address.test.ts index 8554a2f..6d58e43 100644 --- a/test/address.test.ts +++ b/test/address.test.ts @@ -55,7 +55,7 @@ describe('transactions', () => { .expect('Content-Type', /json/) .expect(200) expect(JSON.parse(text)).toEqual(transactionWithEventResponse) - expect(getTransactionsByAddressMock).toHaveBeenCalledWith(mockAddress, '50', undefined, undefined, '0') + expect(getTransactionsByAddressMock).toHaveBeenCalledWith(mockAddress, '50', undefined, undefined, '0', 0, NaN) }) })