diff --git a/.github/workflows/load-tests.yml b/.github/workflows/load-tests.yml index 4bda65fd4..5f14d7355 100644 --- a/.github/workflows/load-tests.yml +++ b/.github/workflows/load-tests.yml @@ -1,8 +1,6 @@ name: Load Tests on: - push: - branches: [main, development] pull_request: branches: [main, development] diff --git a/config/config.placeholder.yaml b/config/config.placeholder.yaml new file mode 100644 index 000000000..e764432d9 --- /dev/null +++ b/config/config.placeholder.yaml @@ -0,0 +1,171 @@ +network: 'DAPP_CONFIG' +metaChainShardId: 4294967295 +api: + public: true + publicPort: 3001 + private: true + privatePort: 4001 + websocket: true +cron: + cacheWarmer: true + fastWarm: true + queueWorker: true + elasticUpdater: false +flags: + useRequestCaching: true + useKeepAliveAgent: true + useTracing: false + useRequestLogging: false + useVmQueryTracing: false + processNfts: true + collectionPropertiesFromGateway: false +features: + eventsNotifier: + enabled: false + port: 5674 + url: 'amqp://guest:guest@127.0.0.1:5673' + exchange: 'all_events' + queue: 'api-process-logs-and-events' + guestCaching: + enabled: false + hitsThreshold: 100 + ttl: 12 + transactionPool: + enabled: false + transactionPoolWarmer: + enabled: false + cronExpression: '*/5 * * * * *' + ttlInSeconds: 60 + updateCollectionExtraDetails: + enabled: false + updateAccountExtraDetails: + enabled: false + marketplace: + enabled: false + serviceUrl: 'MARKETPLACE_URL' + exchange: + enabled: false + serviceUrl: 'EXCHANGE_URL' + dataApi: + enabled: false + serviceUrl: 'DATAAPI_URL' + assetsFetch: + enabled: true + assetesUrl: 'ASSETSFETCH_URL' + auth: + enabled: false + maxExpirySeconds: 86400 + acceptedOrigins: + - '' + admins: + - '' + jwtSecret: '' + stakingV4: + enabled: false + cronExpression: '*/5 * * * * *' + activationEpoch: 1043 + nodeEpochsLeft: + enabled: false + transactionProcessor: + enabled: false + maxLookBehind: 100 + transactionCompleted: + enabled: false + maxLookBehind: 100 + logLevel: 'Error' + transactionBatch: + enabled: true + maxLookBehind: 100 + statusChecker: + enabled: false + thresholds: + tokens: 500 + nodes: 3000 + providers: 10 + tokenSupplyCount: 20 + tokenAssets: 20 + tokenAccounts: 500 + tokenTransactions: 500 + nodeValidators: 300 + nftScamInfo: + enabled: false + processNfts: + enabled: false + nftQueueName: 'api-process-nfts' + deadLetterQueueName: 'api-process-nfts-dlq' + tps: + enabled: false + maxLookBehindNonces: 100 + nodesFetch: + enabled: true + serviceUrl: 'NODESFETCH_URL' + tokensFetch: + enabled: true + serviceUrl: 'TOKENSFETCH_URL' + providersFetch: + enabled: true + serviceUrl: 'PROVIDERSFETCH_URL' +image: + width: 600 + height: 600 + type: 'png' +aws: + s3KeyId: '' + s3Secret: '' + s3Bucket: 'devnet-media.elrond.com' + s3Region: '' +urls: + self: 'https://devnet-api.multiversx.com' + elastic: + - 'ELASTICSEARCH_URL' + gateway: + - 'GATEWAY_URL' + verifier: 'https://play-api.multiversx.com' + redis: 'REDIS_IP' + rabbitmq: 'RABBITMQ_URL' + providers: 'PROVIDERS_URL' + delegation: 'DELEGATION_URL' + media: 'https://devnet-media.elrond.com' + nftThumbnails: 'https://devnet-media.elrond.com/nfts/thumbnail' + tmp: '/tmp' + ipfs: 'https://ipfs.io/ipfs' + socket: 'SOCKET_URL' + maiarId: 'https://devnet-id-api.multiversx.com' +indexer: + type: 'elastic' + maxPagination: 10000 +database: + enabled: false + url: 'mongodb://127.0.0.1:27017/api?authSource=admin' + type: 'mysql' + host: 'localhost' + port: 3306 + username: 'root' + password: 'root' + database: 'api' +caching: + cacheTtl: 6 + processTtl: 600 + poolLimit: 50 + cacheDuration: 3 +keepAliveTimeout: + downstream: 61000 + upstream: 60000 +contracts: + esdt: 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u' + auction: 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l' + staking: 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqllls0lczs7' + delegationManager: 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6' + delegation: 'erd1qqqqqqqqqqqqqpgq97wezxw6l7lgg7k9rxvycrz66vn92ksh2tssxwf7ep' + metabonding: 'erd1qqqqqqqqqqqqqpgqkg7we73j769ew5we4yyx7uyvnn0nefqgd8ssm6vjc2' +inflation: + - 1952123 + - 1746637 + - 1541150 + - 1335663 + - 1130177 + - 924690 + - 719203 +nftProcess: + parallelism: 1 + maxRetries: 3 diff --git a/config/dapp.config.placeholder.json b/config/dapp.config.placeholder.json new file mode 100644 index 000000000..b042458a2 --- /dev/null +++ b/config/dapp.config.placeholder.json @@ -0,0 +1,17 @@ +{ + "id": "PLACEHOLDER_DAPP_id", + "name": "PLACEHOLDER_DAPP_name", + "egldLabel": "PLACEHOLDER_DAPP_egldLabel", + "decimals": "4", + "egldDenomination": "18", + "gasPerDataByte": "1500", + "apiTimeout": "4000", + "walletConnectDeepLink": "https://maiar.page.link/?apn=com.multiversx.maiar.wallet&isi=1519405832&ibi=com.multiversx.maiar.wallet&link=https://maiar.com/", + "walletConnectBridgeAddresses": [ + "https://bridge.walletconnect.org" + ], + "walletAddress": "PLACEHOLDER_DAPP_walletAddress", + "apiAddress": "PLACEHOLDER_DAPP_apiAddress", + "explorerAddress": "PLACEHOLDER_DAPP_explorerAddress", + "chainId": "PLACEHOLDER_DAPP_chainId" +} \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh old mode 100644 new mode 100755 index 4d199798f..6f380f09a --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,46 +1,65 @@ #!/bin/sh # ENV VARIABLES - # MVX_ENV - defines what config to copy (default devnet) - # ELASTICSEARCH_URL - defines custom elasticsearch url - eg https://devnet-index.multiversx.com - # GATEWAY_URL - defines custom gateway url - eg https://devnet-gateway.multiversx.com - # REDIS_IP - defines redis ip - default 127.0.0.1 - -# CHECK IF ENV IS DEFINED -if [ -n "$MVX_ENV" ] && [ "$MVX_ENV" = "devnet" ]; then - # Copy config file - cp ./config/config.${MVX_ENV}.yaml /app/dist/config/config.yaml - - if [ $? -eq 0 ]; then - echo "Config file copied successfully from config/config.${MVX_ENV}.yaml /app/dist/config/config.yaml" - else - echo "Failed to copy the file." - fi - -else - cp ./config/config.devnet.yaml /app/dist/config/config.yaml - - if [ $? -eq 0 ]; then - echo "Default config file copied successfully from config/config.devnet.yaml /app/dist/config/config.yaml" - else - echo "Failed to copy the file." - fi -fi - -# Replaces urls if defined -if [ -n "$REDIS_IP" ]; then - echo "Redis IP defined: ${REDIS_IP}, replacing in config" - sed -i "s|redis: '127.0.0.1'|redis: '${REDIS_IP}'|g" /app/dist/config/config.yaml -fi - -if [ -n "$ELASTICSEARCH_URL" ]; then - echo "Elasticsearch url defined: ${ELASTICSEARCH_URL}, replacing in config" - sed -i "/^ elastic:/!b; n; s|.*| - '${ELASTICSEARCH_URL}'|" /app/dist/config/config.yaml -fi - -if [ -n "$GATEWAY_URL" ]; then - echo "Gateway url defined: ${GATEWAY_URL}, replacing in config" - sed -i "/^ gateway:/!b; n; s|.*| - '${GATEWAY_URL}'|" /app/dist/config/config.yaml -fi + # MVX_ENV=devnet + # DAPP_CONFIG=devnet + # REDIS_IP=127.0.0.1 + # ELASTICSEARCH_URL=https://devnet-index.multiversx.com + # GATEWAY_URL=https://devnet-gateway.multiversx.com + # RABBITMQ_URL=amqp://127.0.0.1:5672 + # PROVIDERS_URL=https://devnet-delegation-api.multiversx.com/providers + # DELEGATION_URL=https://devnet-delegation-api.multiversx.com + # SOCKET_URL=devnet-socket-api.multiversx.com + # NODESFETCH_URL= https://devnet-api.multiversx.com + # TOKENSFETCH_URL= https://devnet-api.multiversx.com + # PROVIDERSFETCH_URL= https://devnet-api.multiversx.com + # DATAAPI_URL=https://devnet-data-api.multiversx.com + # EXCHANGE_URL=https://devnet-graph.xexchange.com/graphql + # MARKETPLACE_URL=https://devnet-nfts-graph.multiversx.com/graphql + # ASSETSFETCH_URL=https://tools.multiversx.com/assets-cdn + # PLACEHOLDER_DAPP_id=devnet + # PLACEHOLDER_DAPP_name=Devnet + # PLACEHOLDER_DAPP_egldLabel=xEGLD + # PLACEHOLDER_DAPP_walletAddress=https://devnet-wallet.multiversx.com + # PLACEHOLDER_DAPP_apiAddress=https://devnet-api.multiversx.com + # PLACEHOLDER_DAPP_explorerAddress=http://devnet-explorer.multiversx.com + # PLACEHOLDER_DAPP_chainId=D +env_vars_with_defaults="MVX_ENV=devnet DAPP_CONFIG=devnet REDIS_IP=127.0.0.1 ELASTICSEARCH_URL=https://devnet-index.multiversx.com GATEWAY_URL=https://devnet-gateway.multiversx.com RABBITMQ_URL=amqp://127.0.0.1:5672 PROVIDERS_URL=https://devnet-delegation-api.multiversx.com/providers DATAAPI_URL=https://devnet-data-api.multiversx.com EXCHANGE_URL=https://devnet-graph.xexchange.com/graphql MARKETPLACE_URL=https://devnet-nfts-graph.multiversx.com/graphql ASSETSFETCH_URL=https://tools.multiversx.com/assets-cdn DELEGATION_URL=https://devnet-delegation-api.multiversx.com SOCKET_URL=devnet-socket-api.multiversx.com NODESFETCH_URL=https://devnet-api.multiversx.com TOKENSFETCH_URL=https://devnet-api.multiversx.com PROVIDERSFETCH_URL=https://devnet-api.multiversx.com PLACEHOLDER_DAPP_id=devnet PLACEHOLDER_DAPP_name=Devnet PLACEHOLDER_DAPP_egldLabel=xEGLD PLACEHOLDER_DAPP_walletAddress=https://devnet-wallet.multiversx.com PLACEHOLDER_DAPP_apiAddress=https://devnet-api.multiversx.com PLACEHOLDER_DAPP_explorerAddress=http://devnet-explorer.multiversx.com PLACEHOLDER_DAPP_chainId=D" + +replace_placeholder() { + local var_name=$1 + local var_value=$2 + + case $var_name in + PLACEHOLDER_DAPP*) + echo "Var ${var_name} defined, replacing ${var_value} in /app/config/dapp.config.placeholder.json" + sed -i "s|${var_name}|${var_value}|g" /app/config/dapp.config.placeholder.json + ;; + *) + echo "Var ${var_name} defined, replacing ${var_value} in /app/dist/config/config.yaml" + sed -i "s|${var_name}|${var_value}|g" /app/dist/config/config.yaml + ;; + esac + +} + +# Loop through each environment variable +for entry in $env_vars_with_defaults; do + # Split the entry into name and value + var_name=$(echo $entry | cut -d= -f1) + default_value=$(echo $entry | cut -d= -f2) + + # Use the environment variable value if defined; otherwise, use the default + eval "value=\${$var_name:-$default_value}" + + cp ./config/config.placeholder.yaml /app/dist/config/config.yaml + if [ $? -eq 0 ]; then + echo "Config file copied successfully from config/config.placeholder.yaml /app/dist/config/config.yaml" + fi + + # Execute the function with the variable name and value + replace_placeholder "$var_name" "$value" + +done exec /usr/local/bin/node dist/src/main.js diff --git a/src/common/data-api/data-api.service.ts b/src/common/data-api/data-api.service.ts index bf0d50180..2b43b1304 100644 --- a/src/common/data-api/data-api.service.ts +++ b/src/common/data-api/data-api.service.ts @@ -72,17 +72,18 @@ export class DataApiService { } try { - const [cexTokensRaw, xExchangeTokensRaw, hatomTokensRaw] = await Promise.all([ + const [cexTokensRaw, xExchangeTokensRaw, hatomTokensRaw, xoxnoTokensRaw] = await Promise.all([ this.apiService.get(`${this.apiConfigService.getDataApiServiceUrl()}/v1/tokens/cex?fields=identifier`), this.apiService.get(`${this.apiConfigService.getDataApiServiceUrl()}/v1/tokens/xexchange?fields=identifier`), this.apiService.get(`${this.apiConfigService.getDataApiServiceUrl()}/v1/tokens/hatom?fields=identifier`), + this.apiService.get(`${this.apiConfigService.getDataApiServiceUrl()}/v1/tokens/xoxno?fields=identifier`), ]); const cexTokens: DataApiToken[] = cexTokensRaw.data.map((token: any) => new DataApiToken({ identifier: token.identifier, market: 'cex' })); const xExchangeTokens: DataApiToken[] = xExchangeTokensRaw.data.map((token: any) => new DataApiToken({ identifier: token.identifier, market: 'xexchange' })); const hatomTokens: DataApiToken[] = hatomTokensRaw.data.map((token: any) => new DataApiToken({ identifier: token.identifier, market: 'hatom' })); - - const tokens = [...cexTokens, ...xExchangeTokens, ...hatomTokens].toRecord(x => x.identifier); + const xoxnoTokens: DataApiToken[] = xoxnoTokensRaw.data.map((token: any) => new DataApiToken({ identifier: token.identifier, market: 'xoxno' })); + const tokens = [...cexTokens, ...xExchangeTokens, ...hatomTokens, ...xoxnoTokens].toRecord(x => x.identifier); return tokens; } catch (error) { this.logger.error(`An unexpected error occurred while fetching tokens from Data API.`); diff --git a/src/common/data-api/entities/data-api.token.ts b/src/common/data-api/entities/data-api.token.ts index 8695f31fb..bc2b273c3 100644 --- a/src/common/data-api/entities/data-api.token.ts +++ b/src/common/data-api/entities/data-api.token.ts @@ -4,5 +4,5 @@ export class DataApiToken { } identifier: string = ''; - market: 'cex' | 'xexchange' | 'hatom' = 'cex'; + market: 'cex' | 'xexchange' | 'hatom' | 'xoxno' = 'cex'; } diff --git a/src/common/gateway/entities/gateway.component.request.ts b/src/common/gateway/entities/gateway.component.request.ts index a3821082c..066b06904 100644 --- a/src/common/gateway/entities/gateway.component.request.ts +++ b/src/common/gateway/entities/gateway.component.request.ts @@ -5,6 +5,7 @@ export enum GatewayComponentRequest { networkEconomics = 'networkEconomics', networkTotalStaked = 'networkTotalStaked', addressDetails = 'addressDetails', + addressesBulk = 'addressesBulk', addressEsdt = 'addressEsdt', addressEsdtHistorical = 'addressEsdtHistorical', addressEsdtBalance = 'addressEsdtBalance', diff --git a/src/common/gateway/gateway.service.ts b/src/common/gateway/gateway.service.ts index 6888fbfb3..597ced989 100644 --- a/src/common/gateway/gateway.service.ts +++ b/src/common/gateway/gateway.service.ts @@ -95,6 +95,11 @@ export class GatewayService { return result; } + async getAccountsBulk(addresses: string[]): Promise { + const result = await this.create('address/bulk', GatewayComponentRequest.addressesBulk, addresses); + return result.accounts; + } + async getEsdtSupply(identifier: string): Promise { const result = await this.get(`network/esdt/supply/${identifier}`, GatewayComponentRequest.esdtSupply); return result; diff --git a/src/common/indexer/elastic/elastic.indexer.helper.ts b/src/common/indexer/elastic/elastic.indexer.helper.ts index be10ff0a9..adb19699c 100644 --- a/src/common/indexer/elastic/elastic.indexer.helper.ts +++ b/src/common/indexer/elastic/elastic.indexer.helper.ts @@ -18,6 +18,7 @@ import { SmartContractResultFilter } from "src/endpoints/sc-results/entities/sma import { ApplicationFilter } from "src/endpoints/applications/entities/application.filter"; import { NftType } from "../entities/nft.type"; import { EventsFilter } from "src/endpoints/events/entities/events.filter"; +import { ScriptQuery } from "./script.query"; @Injectable() export class ElasticIndexerHelper { @@ -93,7 +94,7 @@ export class ElasticIndexerHelper { QueryType.Nested('roles', [new MatchQuery('roles.ESDTRoleNFTUpdateAttributes', address)]), QueryType.Nested('roles', [new MatchQuery('roles.ESDTRoleNFTAddURI', address)]), QueryType.Nested('roles', [new MatchQuery('roles.ESDTTransferRole', address)]), - ] + ], )); } @@ -253,7 +254,7 @@ export class ElasticIndexerHelper { QueryType.Should([ QueryType.Match('nft_scamInfoType', 'scam'), QueryType.Match('nft_scamInfoType', 'potentialScam'), - ]) + ]), ); } @@ -309,12 +310,16 @@ export class ElasticIndexerHelper { smartContractResultConditions.push(QueryType.Match('sender', filter.address)); } + let mustNotQueries: AbstractQuery[] = [ + QueryType.Exists('canBeIgnored'), + ]; + if (filter.withRefunds) { + mustNotQueries = []; + } elasticQuery = elasticQuery.withCondition(QueryConditionOptions.should, QueryType.Must([ QueryType.Match('type', 'unsigned'), QueryType.Should(smartContractResultConditions), - ], [ - QueryType.Exists('canBeIgnored'), - ])) + ], mustNotQueries)) .withCondition(QueryConditionOptions.should, QueryType.Must([ QueryType.Should([QueryType.Match('type', 'normal')]), QueryType.Should([ @@ -429,7 +434,7 @@ export class ElasticIndexerHelper { [ QueryType.Match('currentOwner', address), ...rolesConditions, - ] + ], )) .withMustMatchCondition('token', filter.identifier) .withMustMatchCondition('currentOwner', filter.owner); @@ -507,9 +512,22 @@ export class ElasticIndexerHelper { } public buildTransactionFilterQuery(filter: TransactionFilter, address?: string): ElasticQuery { - let elasticQuery = ElasticQuery.create() - .withMustMatchCondition('type', 'normal') - .withMustMatchCondition('senderShard', filter.senderShard) + let elasticQuery = ElasticQuery.create(); + + if (!filter.withRelayedScresults) { + elasticQuery = elasticQuery.withMustMatchCondition('type', 'normal'); + } else { + elasticQuery = elasticQuery.withShouldCondition([ + QueryType.Match('type', 'normal'), + QueryType.Must([ + QueryType.Exists('relayerAddr'), + QueryType.Match('type', 'unsigned'), + new ScriptQuery(`doc['originalTxHash'].size() > 0 && doc['prevTxHash'].size() > 0 && doc['originalTxHash'].value == doc['prevTxHash'].value`), + ]), + ]); + } + + elasticQuery = elasticQuery.withMustMatchCondition('senderShard', filter.senderShard) .withMustMatchCondition('receiverShard', filter.receiverShard) .withMustMatchCondition('miniBlockHash', filter.miniBlockHash) .withMustMultiShouldCondition(filter.hashes, hash => QueryType.Match('_id', hash)) diff --git a/src/common/indexer/elastic/script.query.ts b/src/common/indexer/elastic/script.query.ts new file mode 100644 index 000000000..fa0b75ac3 --- /dev/null +++ b/src/common/indexer/elastic/script.query.ts @@ -0,0 +1,14 @@ +import { AbstractQuery } from "@multiversx/sdk-nestjs-elastic"; + +// TODO: remove this and use ScriptQuery from sdk-nestjs when PR #247 is merged +export class ScriptQuery extends AbstractQuery { + constructor( + private readonly source: string | undefined, + ) { + super(); + } + + getQuery(): any { + return { script: { script: { source: this.source, lang: 'painless' } } }; + } +} diff --git a/src/common/metrics/api.metrics.service.ts b/src/common/metrics/api.metrics.service.ts index 5cd064536..eea34676a 100644 --- a/src/common/metrics/api.metrics.service.ts +++ b/src/common/metrics/api.metrics.service.ts @@ -1,7 +1,7 @@ import { MetricsService } from '@multiversx/sdk-nestjs-monitoring'; import { forwardRef, Inject, Injectable } from "@nestjs/common"; import { OnEvent } from '@nestjs/event-emitter'; -import { register, Histogram, Gauge } from 'prom-client'; +import { register, Histogram, Gauge, Counter } from 'prom-client'; import { ApiConfigService } from "src/common/api-config/api.config.service"; import { GatewayService } from "../gateway/gateway.service"; import { ProtocolService } from "../protocol/protocol.service"; @@ -19,6 +19,9 @@ export class ApiMetricsService { private static lastProcessedNonceGauge: Gauge; private static lastProcessedBatchProcessorNonce: Gauge; private static lastProcessedTransactionCompletedProcessorNonce: Gauge; + private static transactionsCompletedCounter: Counter; + private static transactionsPendingResultsCounter: Counter; + private static batchUpdatesCounter: Counter; constructor( private readonly apiConfigService: ApiConfigService, @@ -105,6 +108,27 @@ export class ApiMetricsService { labelNames: ['shardId'], }); } + + if (!ApiMetricsService.transactionsCompletedCounter) { + ApiMetricsService.transactionsCompletedCounter = new Counter({ + name: 'websocket_transactions_completed_total', + help: 'Total number of completed transactions processed via websocket', + }); + } + + if (!ApiMetricsService.transactionsPendingResultsCounter) { + ApiMetricsService.transactionsPendingResultsCounter = new Counter({ + name: 'websocket_transactions_pending_results_total', + help: 'Total number of transactions with pending results processed via websocket', + }); + } + + if (!ApiMetricsService.batchUpdatesCounter) { + ApiMetricsService.batchUpdatesCounter = new Counter({ + name: 'websocket_batch_updates_total', + help: 'Total number of batch updates processed via websocket', + }); + } } @OnEvent(MetricsEvents.SetVmQuery) @@ -158,6 +182,21 @@ export class ApiMetricsService { ApiMetricsService.lastProcessedTransactionCompletedProcessorNonce.set({ shardId }, nonce); } + @OnEvent(MetricsEvents.SetTransactionsCompleted) + recordTransactionsCompleted(payload: { transactions: any[] }) { + ApiMetricsService.transactionsCompletedCounter.inc(payload.transactions.length); + } + + @OnEvent(MetricsEvents.SetTransactionsPendingResults) + recordTransactionsPendingResults(payload: { transactions: any[] }) { + ApiMetricsService.transactionsPendingResultsCounter.inc(payload.transactions.length); + } + + @OnEvent(MetricsEvents.SetBatchUpdated) + recordBatchUpdated() { + ApiMetricsService.batchUpdatesCounter.inc(); + } + async getMetrics(): Promise { const shardIds = await this.protocolService.getShardIds(); if (this.apiConfigService.getIsTransactionProcessorCronActive()) { diff --git a/src/common/websockets/web-socket-publisher-controller.ts b/src/common/websockets/web-socket-publisher-controller.ts index 7fe669a4d..17a9addc0 100644 --- a/src/common/websockets/web-socket-publisher-controller.ts +++ b/src/common/websockets/web-socket-publisher-controller.ts @@ -3,6 +3,8 @@ import { ShardTransaction } from "@elrondnetwork/transaction-processor"; import { Controller } from "@nestjs/common"; import { EventPattern } from "@nestjs/microservices"; import { WebSocketPublisherService } from "src/common/websockets/web-socket-publisher-service"; +import { EventEmitter2 } from "@nestjs/event-emitter"; +import { MetricsEvents } from "src/utils/metrics-events.constants"; @Controller() export class WebSocketPublisherController { @@ -10,6 +12,7 @@ export class WebSocketPublisherController { constructor( private readonly webSocketPublisherService: WebSocketPublisherService, + private readonly eventEmitter: EventEmitter2, ) { } @EventPattern('transactionsCompleted') @@ -17,6 +20,10 @@ export class WebSocketPublisherController { for (const transaction of transactions) { await this.webSocketPublisherService.onTransactionCompleted(transaction); } + + this.eventEmitter.emit(MetricsEvents.SetTransactionsCompleted, { + transactions, + }); } @EventPattern('transactionsPendingResults') @@ -24,11 +31,17 @@ export class WebSocketPublisherController { for (const transaction of transactions) { await this.webSocketPublisherService.onTransactionPendingResults(transaction); } + + this.eventEmitter.emit(MetricsEvents.SetTransactionsPendingResults, { + transactions, + }); } @EventPattern('onBatchUpdated') onBatchUpdated(payload: { address: string, batchId: string, txHashes: string[] }) { this.logger.log(`Notifying batch updated for address ${payload.address}, batch id '${payload.batchId}', hashes ${payload.txHashes} `); this.webSocketPublisherService.onBatchUpdated(payload.address, payload.batchId, payload.txHashes); + + this.eventEmitter.emit(MetricsEvents.SetBatchUpdated); } } diff --git a/src/endpoints/accounts/account.controller.ts b/src/endpoints/accounts/account.controller.ts index 9af332110..38fd1b17d 100644 --- a/src/endpoints/accounts/account.controller.ts +++ b/src/endpoints/accounts/account.controller.ts @@ -58,6 +58,7 @@ import { DeepHistoryInterceptor } from 'src/interceptors/deep-history.intercepto import { MexPairType } from '../mex/entities/mex.pair.type'; import { NftSubType } from '../nfts/entities/nft.sub.type'; import { AccountContract } from './entities/account.contract'; +import { AccountFetchOptions } from './entities/account.fetch.options'; @Controller() @ApiTags('accounts') @@ -96,26 +97,29 @@ export class AccountController { @ApiQuery({ name: 'excludeTags', description: 'Exclude specific tags from result', required: false }) @ApiQuery({ name: 'hasAssets', description: 'Returns a list of accounts that have assets', required: false }) @ApiQuery({ name: 'search', description: 'Search by account address', required: false }) + @ApiQuery({ name: 'addresses', description: 'A comma-separated list of addresses to filter by', required: false, type: String }) getAccounts( @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @Query("size", new DefaultValuePipe(25), ParseIntPipe) size: number, @Query("ownerAddress", ParseAddressPipe) ownerAddress?: string, @Query("name") name?: string, - @Query("tags", new ParseArrayPipe()) tags?: string[], + @Query("tags", ParseArrayPipe) tags?: string[], @Query('sort', new ParseEnumPipe(AccountSort)) sort?: AccountSort, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @Query("isSmartContract", new ParseBoolPipe) isSmartContract?: boolean, - @Query("withOwnerAssets", new ParseBoolPipe) withOwnerAssets?: boolean, - @Query("withDeployInfo", new ParseBoolPipe) withDeployInfo?: boolean, - @Query("withTxCount", new ParseBoolPipe) withTxCount?: boolean, - @Query("withScrCount", new ParseBoolPipe) withScrCount?: boolean, - @Query("excludeTags", new ParseArrayPipe) excludeTags?: string[], - @Query("hasAssets", new ParseBoolPipe) hasAssets?: boolean, + @Query("isSmartContract", ParseBoolPipe) isSmartContract?: boolean, + @Query("withOwnerAssets", ParseBoolPipe) withOwnerAssets?: boolean, + @Query("withDeployInfo", ParseBoolPipe) withDeployInfo?: boolean, + @Query("withTxCount", ParseBoolPipe) withTxCount?: boolean, + @Query("withScrCount", ParseBoolPipe) withScrCount?: boolean, + @Query("excludeTags", ParseArrayPipe) excludeTags?: string[], + @Query("hasAssets", ParseBoolPipe) hasAssets?: boolean, @Query("search") search?: string, + @Query("addresses", ParseAddressArrayPipe) addresses?: string[], ): Promise { const queryOptions = new AccountQueryOptions( { ownerAddress, + addresses, sort, order, isSmartContract, @@ -147,11 +151,11 @@ export class AccountController { @ApiQuery({ name: 'hasAssets', description: 'Returns a list of accounts that have assets', required: false }) async getAccountsCount( @Query("ownerAddress", ParseAddressPipe) ownerAddress?: string, - @Query("isSmartContract", new ParseBoolPipe) isSmartContract?: boolean, + @Query("isSmartContract", ParseBoolPipe) isSmartContract?: boolean, @Query("name") name?: string, - @Query("tags", new ParseArrayPipe()) tags?: string[], - @Query("excludeTags", new ParseArrayPipe) excludeTags?: string[], - @Query("hasAssets", new ParseBoolPipe) hasAssets?: boolean, + @Query("tags", ParseArrayPipe) tags?: string[], + @Query("excludeTags", ParseArrayPipe) excludeTags?: string[], + @Query("hasAssets", ParseBoolPipe) hasAssets?: boolean, ): Promise { return await this.accountService.getAccountsCount( new AccountQueryOptions( @@ -169,11 +173,11 @@ export class AccountController { @ApiExcludeEndpoint() async getAccountsCountAlternative( @Query("ownerAddress", ParseAddressPipe) ownerAddress?: string, - @Query("isSmartContract", new ParseBoolPipe) isSmartContract?: boolean, + @Query("isSmartContract", ParseBoolPipe) isSmartContract?: boolean, @Query("name") name?: string, - @Query("tags", new ParseArrayPipe()) tags?: string[], - @Query("excludeTags", new ParseArrayPipe) excludeTags?: string[], - @Query("hasAssets", new ParseBoolPipe) hasAssets?: boolean, + @Query("tags", ParseArrayPipe) tags?: string[], + @Query("excludeTags", ParseArrayPipe) excludeTags?: string[], + @Query("hasAssets", ParseBoolPipe) hasAssets?: boolean, ): Promise { return await this.accountService.getAccountsCount( new AccountQueryOptions( @@ -191,16 +195,25 @@ export class AccountController { @UseInterceptors(DeepHistoryInterceptor) @ApiOperation({ summary: 'Account details', description: 'Returns account details for a given address' }) @ApiQuery({ name: 'withGuardianInfo', description: 'Returns guardian data for a given address', required: false }) - @ApiQuery({ name: 'fields', description: 'List of fields to filter by', required: false, isArray: true, style: 'form', explode: false }) + @ApiQuery({ name: 'withTxCount', description: 'Returns the count of the transactions for a given address', required: false }) + @ApiQuery({ name: 'withScrCount', description: 'Returns the sc results count for a given address', required: false }) + @ApiQuery({ name: 'withTimestamp', description: 'Returns the timestamp of the last activity for a given address', required: false }) + @ApiQuery({ name: 'withAssets', description: 'Returns the assets for a given address', required: false }) @ApiQuery({ name: 'timestamp', description: 'Retrieve entry from timestamp', required: false, type: Number }) @ApiOkResponse({ type: AccountDetailed }) async getAccountDetails( @Param('address', ParseAddressPipe) address: string, - @Query('withGuardianInfo', new ParseBoolPipe) withGuardianInfo?: boolean, - @Query('fields', ParseArrayPipe) fields?: string[], + @Query('withGuardianInfo', ParseBoolPipe) withGuardianInfo?: boolean, + @Query('withTxCount', ParseBoolPipe) withTxCount?: boolean, + @Query('withScrCount', ParseBoolPipe) withScrCount?: boolean, + @Query('withTimestamp', ParseBoolPipe) withTimestamp?: boolean, + @Query('withAssets', ParseBoolPipe) withAssets?: boolean, @Query('timestamp', ParseIntPipe) _timestamp?: number, ): Promise { - const account = await this.accountService.getAccount(address, fields, withGuardianInfo); + const account = await this.accountService.getAccount( + address, + new AccountFetchOptions({ withGuardianInfo, withTxCount, withScrCount, withTimestamp, withAssets }), + ); if (!account) { throw new NotFoundException('Account not found'); } @@ -259,7 +272,7 @@ export class AccountController { @Query('name') name?: string, @Query('identifier') identifier?: string, @Query('identifiers', ParseArrayPipe) identifiers?: string[], - @Query('includeMetaESDT', new ParseBoolPipe) includeMetaESDT?: boolean, + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, @Query('timestamp', ParseIntPipe) _timestamp?: number, @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], ): Promise { @@ -292,7 +305,7 @@ export class AccountController { @Query('name') name?: string, @Query('identifier') identifier?: string, @Query('identifiers', ParseArrayPipe) identifiers?: string[], - @Query('includeMetaESDT', new ParseBoolPipe) includeMetaESDT?: boolean, + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, @Query('timestamp', ParseIntPipe) _timestamp?: number, @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], ): Promise { @@ -316,7 +329,7 @@ export class AccountController { @Query('name') name?: string, @Query('identifier') identifier?: string, @Query('identifiers', ParseArrayPipe) identifiers?: string[], - @Query('includeMetaESDT', new ParseBoolPipe) includeMetaESDT?: boolean, + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, @Query('timestamp', ParseIntPipe) _timestamp?: number, @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], ): Promise { @@ -372,13 +385,13 @@ export class AccountController { @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], @Query('owner', ParseAddressPipe) owner?: string, - @Query('canCreate', new ParseBoolPipe) canCreate?: boolean, - @Query('canBurn', new ParseBoolPipe) canBurn?: boolean, - @Query('canAddQuantity', new ParseBoolPipe) canAddQuantity?: boolean, - @Query('canUpdateAttributes', new ParseBoolPipe) canUpdateAttributes?: boolean, - @Query('canAddUri', new ParseBoolPipe) canAddUri?: boolean, - @Query('canTransferRole', new ParseBoolPipe) canTransferRole?: boolean, - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('canCreate', ParseBoolPipe) canCreate?: boolean, + @Query('canBurn', ParseBoolPipe) canBurn?: boolean, + @Query('canAddQuantity', ParseBoolPipe) canAddQuantity?: boolean, + @Query('canUpdateAttributes', ParseBoolPipe) canUpdateAttributes?: boolean, + @Query('canAddUri', ParseBoolPipe) canAddUri?: boolean, + @Query('canTransferRole', ParseBoolPipe) canTransferRole?: boolean, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, ): Promise { return await this.collectionService.getCollectionsWithRolesForAddress( address, @@ -414,10 +427,10 @@ export class AccountController { @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], @Query('owner', ParseAddressPipe) owner?: string, - @Query('canCreate', new ParseBoolPipe) canCreate?: boolean, - @Query('canBurn', new ParseBoolPipe) canBurn?: boolean, - @Query('canAddQuantity', new ParseBoolPipe) canAddQuantity?: boolean, - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('canCreate', ParseBoolPipe) canCreate?: boolean, + @Query('canBurn', ParseBoolPipe) canBurn?: boolean, + @Query('canAddQuantity', ParseBoolPipe) canAddQuantity?: boolean, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, ): Promise { return await this.collectionService.getCollectionCountForAddressWithRoles(address, new CollectionFilter({ search, type, subType, owner, canCreate, canBurn, canAddQuantity, excludeMetaESDT })); } @@ -430,10 +443,10 @@ export class AccountController { @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], @Query('owner', ParseAddressPipe) owner?: string, - @Query('canCreate', new ParseBoolPipe) canCreate?: boolean, - @Query('canBurn', new ParseBoolPipe) canBurn?: boolean, - @Query('canAddQuantity', new ParseBoolPipe) canAddQuantity?: boolean, - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('canCreate', ParseBoolPipe) canCreate?: boolean, + @Query('canBurn', ParseBoolPipe) canBurn?: boolean, + @Query('canAddQuantity', ParseBoolPipe) canAddQuantity?: boolean, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, ): Promise { return await this.collectionService.getCollectionCountForAddressWithRoles(address, new CollectionFilter({ search, type, subType, owner, canCreate, canBurn, canAddQuantity, excludeMetaESDT, @@ -471,9 +484,9 @@ export class AccountController { @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, @Query('search') search?: string, @Query('owner', ParseAddressPipe) owner?: string, - @Query('canMint', new ParseBoolPipe) canMint?: boolean, - @Query('canBurn', new ParseBoolPipe) canBurn?: boolean, - @Query('includeMetaESDT', new ParseBoolPipe) includeMetaESDT?: boolean, + @Query('canMint', ParseBoolPipe) canMint?: boolean, + @Query('canBurn', ParseBoolPipe) canBurn?: boolean, + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, ): Promise { return await this.tokenService.getTokensWithRolesForAddress(address, new TokenWithRolesFilter({ search, owner, canMint, canBurn, includeMetaESDT }), new QueryPagination({ from, size })); } @@ -490,9 +503,9 @@ export class AccountController { @Param('address', ParseAddressPipe) address: string, @Query('search') search?: string, @Query('owner', ParseAddressPipe) owner?: string, - @Query('canMint', new ParseBoolPipe) canMint?: boolean, - @Query('canBurn', new ParseBoolPipe) canBurn?: boolean, - @Query('includeMetaESDT', new ParseBoolPipe) includeMetaESDT?: boolean, + @Query('canMint', ParseBoolPipe) canMint?: boolean, + @Query('canBurn', ParseBoolPipe) canBurn?: boolean, + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, ): Promise { return await this.tokenService.getTokensWithRolesForAddressCount(address, new TokenWithRolesFilter({ search, owner, canMint, canBurn, includeMetaESDT })); } @@ -503,9 +516,9 @@ export class AccountController { @Param('address', ParseAddressPipe) address: string, @Query('search') search?: string, @Query('owner', ParseAddressPipe) owner?: string, - @Query('canMint', new ParseBoolPipe) canMint?: boolean, - @Query('canBurn', new ParseBoolPipe) canBurn?: boolean, - @Query('includeMetaESDT', new ParseBoolPipe) includeMetaESDT?: boolean, + @Query('canMint', ParseBoolPipe) canMint?: boolean, + @Query('canBurn', ParseBoolPipe) canBurn?: boolean, + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, ): Promise { return await this.tokenService.getTokensWithRolesForAddressCount(address, new TokenWithRolesFilter({ search, owner, canMint, canBurn, includeMetaESDT })); } @@ -541,7 +554,7 @@ export class AccountController { @Query('search') search?: string, @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, ): Promise { return await this.collectionService.getCollectionsForAddress( address, @@ -561,7 +574,7 @@ export class AccountController { @Query('search') search?: string, @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, ): Promise { return await this.collectionService.getCollectionCountForAddress(address, new CollectionFilter({ search, type, subType, excludeMetaESDT })); } @@ -573,7 +586,7 @@ export class AccountController { @Query('search') search?: string, @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, ): Promise { return await this.collectionService.getCollectionCountForAddress(address, new CollectionFilter({ search, type, subType, excludeMetaESDT })); } @@ -632,13 +645,13 @@ export class AccountController { @Query('name') name?: string, @Query('tags', ParseArrayPipe) tags?: string[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('hasUris', new ParseBoolPipe) hasUris?: boolean, - @Query('includeFlagged', new ParseBoolPipe) includeFlagged?: boolean, - @Query('withSupply', new ParseBoolPipe) withSupply?: boolean, + @Query('hasUris', ParseBoolPipe) hasUris?: boolean, + @Query('includeFlagged', ParseBoolPipe) includeFlagged?: boolean, + @Query('withSupply', ParseBoolPipe) withSupply?: boolean, @Query('source', new ParseEnumPipe(EsdtDataSource)) source?: EsdtDataSource, - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, @Query('fields', ParseArrayPipe) fields?: string[], - @Query('isScam', new ParseBoolPipe) isScam?: boolean, + @Query('isScam', ParseBoolPipe) isScam?: boolean, @Query('scamType', new ParseEnumPipe(ScamType)) scamType?: ScamType, @Query('timestamp', ParseIntPipe) _timestamp?: number, ): Promise { @@ -697,10 +710,10 @@ export class AccountController { @Query('name') name?: string, @Query('tags', ParseArrayPipe) tags?: string[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('hasUris', new ParseBoolPipe) hasUris?: boolean, - @Query('includeFlagged', new ParseBoolPipe) includeFlagged?: boolean, - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, - @Query('isScam', new ParseBoolPipe) isScam?: boolean, + @Query('hasUris', ParseBoolPipe) hasUris?: boolean, + @Query('includeFlagged', ParseBoolPipe) includeFlagged?: boolean, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('isScam', ParseBoolPipe) isScam?: boolean, @Query('scamType', new ParseEnumPipe(ScamType)) scamType?: ScamType, @Query('timestamp', ParseIntPipe) _timestamp?: number, ): Promise { @@ -736,10 +749,10 @@ export class AccountController { @Query('name') name?: string, @Query('tags', ParseArrayPipe) tags?: string[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('hasUris', new ParseBoolPipe) hasUris?: boolean, - @Query('includeFlagged', new ParseBoolPipe) includeFlagged?: boolean, - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, - @Query('isScam', new ParseBoolPipe) isScam?: boolean, + @Query('hasUris', ParseBoolPipe) hasUris?: boolean, + @Query('includeFlagged', ParseBoolPipe) includeFlagged?: boolean, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('isScam', ParseBoolPipe) isScam?: boolean, @Query('scamType', new ParseEnumPipe(ScamType)) scamType?: ScamType, @Query('timestamp', ParseIntPipe) _timestamp?: number, ): Promise { @@ -865,6 +878,7 @@ export class AccountController { @ApiQuery({ name: 'senderOrReceiver', description: 'One address that current address interacted with', required: false }) @ApiQuery({ name: 'isRelayed', description: 'Returns isRelayed transactions details', required: false, type: Boolean }) @ApiQuery({ name: 'withActionTransferValue', description: 'Returns value in USD and EGLD for transferred tokens within the action attribute', required: false }) + @ApiQuery({ name: 'withRelayedScresults', description: 'If set to true, will include smart contract results that resemble relayed transactions', required: false, type: Boolean }) async getAccountTransactions( @Param('address', ParseAddressPipe) address: string, @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @@ -883,19 +897,20 @@ export class AccountController { @Query('round', ParseIntPipe) round?: number, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, @Query('fields', ParseArrayPipe) fields?: string[], - @Query('withScResults', new ParseBoolPipe) withScResults?: boolean, - @Query('withOperations', new ParseBoolPipe) withOperations?: boolean, - @Query('withLogs', new ParseBoolPipe) withLogs?: boolean, - @Query('withScamInfo', new ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', new ParseBoolPipe) withUsername?: boolean, - @Query('withBlockInfo', new ParseBoolPipe) withBlockInfo?: boolean, + @Query('withScResults', ParseBoolPipe) withScResults?: boolean, + @Query('withOperations', ParseBoolPipe) withOperations?: boolean, + @Query('withLogs', ParseBoolPipe) withLogs?: boolean, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, + @Query('withBlockInfo', ParseBoolPipe) withBlockInfo?: boolean, @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, - @Query('isRelayed', new ParseBoolPipe) isRelayed?: boolean, + @Query('isRelayed', ParseBoolPipe) isRelayed?: boolean, @Query('withActionTransferValue', ParseBoolPipe) withActionTransferValue?: boolean, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, ) { const options = TransactionQueryOptions.applyDefaultOptions(size, { withScResults, withOperations, withLogs, withScamInfo, withUsername, withBlockInfo, withActionTransferValue }); - return await this.transactionService.getTransactions(new TransactionFilter({ + const transactionFilter = new TransactionFilter({ sender, receivers: receiver, token, @@ -911,7 +926,10 @@ export class AccountController { senderOrReceiver, isRelayed, round, - }), new QueryPagination({ from, size }), options, address, fields); + withRelayedScresults, + }); + TransactionFilter.validate(transactionFilter, size); + return await this.transactionService.getTransactions(transactionFilter, new QueryPagination({ from, size }), options, address, fields); } @Get("/accounts/:address/transactions/count") @@ -931,6 +949,7 @@ export class AccountController { @ApiQuery({ name: 'round', description: 'Round number', required: false }) @ApiQuery({ name: 'senderOrReceiver', description: 'One address that current address interacted with', required: false }) @ApiQuery({ name: 'isRelayed', description: 'Returns isRelayed transactions details', required: false, type: Boolean }) + @ApiQuery({ name: 'withRelayedScresults', description: 'If set to true, will include smart contract results that resemble relayed transactions', required: false, type: Boolean }) async getAccountTransactionsCount( @Param('address', ParseAddressPipe) address: string, @Query('sender', ParseAddressPipe) sender?: string, @@ -946,7 +965,9 @@ export class AccountController { @Query('after', ParseIntPipe) after?: number, @Query('round', ParseIntPipe) round?: number, @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, - @Query('isRelayed', new ParseBoolPipe) isRelayed?: boolean, + @Query('isRelayed', ParseBoolPipe) isRelayed?: boolean, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, + ): Promise { return await this.transactionService.getTransactionCount(new TransactionFilter({ @@ -964,6 +985,7 @@ export class AccountController { senderOrReceiver, isRelayed, round, + withRelayedScresults, }), address); } @@ -995,6 +1017,7 @@ export class AccountController { @ApiQuery({ name: 'withLogs', description: 'Return logs for transfers. When "withLogs" parameter is applied, complexity estimation is 200', required: false }) @ApiQuery({ name: 'withOperations', description: 'Return operations for transfers. When "withOperations" parameter is applied, complexity estimation is 200', required: false }) @ApiQuery({ name: 'withActionTransferValue', description: 'Returns value in USD and EGLD for transferred tokens within the action attribute', required: false }) + @ApiQuery({ name: 'withRefunds', description: 'Include refund transactions', required: false }) async getAccountTransfers( @Param('address', ParseAddressPipe) address: string, @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @@ -1014,13 +1037,14 @@ export class AccountController { @Query('fields', ParseArrayPipe) fields?: string[], @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, @Query('relayer', ParseAddressPipe) relayer?: string, - @Query('withScamInfo', new ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', new ParseBoolPipe) withUsername?: boolean, - @Query('withBlockInfo', new ParseBoolPipe) withBlockInfo?: boolean, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, + @Query('withBlockInfo', ParseBoolPipe) withBlockInfo?: boolean, @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, - @Query('withLogs', new ParseBoolPipe) withLogs?: boolean, - @Query('withOperations', new ParseBoolPipe) withOperations?: boolean, + @Query('withLogs', ParseBoolPipe) withLogs?: boolean, + @Query('withOperations', ParseBoolPipe) withOperations?: boolean, @Query('withActionTransferValue', ParseBoolPipe) withActionTransferValue?: boolean, + @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, ): Promise { const options = TransactionQueryOptions.applyDefaultOptions( size, { withScamInfo, withUsername, withBlockInfo, withOperations, withLogs, withActionTransferValue }); @@ -1042,6 +1066,7 @@ export class AccountController { senderOrReceiver, relayer, round, + withRefunds, }), new QueryPagination({ from, size }), options, @@ -1065,6 +1090,7 @@ export class AccountController { @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) @ApiQuery({ name: 'round', description: 'Round number', required: false }) @ApiQuery({ name: 'senderOrReceiver', description: 'One address that current address interacted with', required: false }) + @ApiQuery({ name: 'withRefunds', description: 'Include refund transactions', required: false }) async getAccountTransfersCount( @Param('address', ParseAddressPipe) address: string, @Query('sender', ParseAddressArrayPipe) sender?: string[], @@ -1080,6 +1106,7 @@ export class AccountController { @Query('after', ParseIntPipe) after?: number, @Query('round', ParseIntPipe) round?: number, @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, + @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, ): Promise { return await this.transferService.getTransfersCount(new TransactionFilter({ address, @@ -1096,6 +1123,7 @@ export class AccountController { after, senderOrReceiver, round, + withRefunds, })); } @@ -1116,6 +1144,7 @@ export class AccountController { @Query('after', ParseIntPipe) after?: number, @Query('round', ParseIntPipe) round?: number, @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, + @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, ): Promise { return await this.transferService.getTransfersCount(new TransactionFilter({ address, @@ -1132,6 +1161,7 @@ export class AccountController { after, senderOrReceiver, round, + withRefunds, })); } diff --git a/src/endpoints/accounts/account.service.ts b/src/endpoints/accounts/account.service.ts index 26b3e7bad..96f647f8f 100644 --- a/src/endpoints/accounts/account.service.ts +++ b/src/endpoints/accounts/account.service.ts @@ -20,7 +20,6 @@ import { AddressUtils, BinaryUtils, OriginLogger } from '@multiversx/sdk-nestjs- import { ApiService, ApiUtils } from "@multiversx/sdk-nestjs-http"; import { GatewayService } from 'src/common/gateway/gateway.service'; import { IndexerService } from "src/common/indexer/indexer.service"; -import { AccountOptionalFieldOption } from './entities/account.optional.field.options'; import { AccountAssets } from 'src/common/assets/entities/account.assets'; import { CacheInfo } from 'src/utils/cache.info'; import { UsernameService } from '../usernames/username.service'; @@ -33,9 +32,10 @@ import { ProviderService } from '../providers/provider.service'; import { KeysService } from '../keys/keys.service'; import { NodeStatusRaw } from '../nodes/entities/node.status'; import { AccountKeyFilter } from './entities/account.key.filter'; -import { Provider } from '../providers/entities/provider'; import { ApplicationMostUsed } from './entities/application.most.used'; import { AccountContract } from './entities/account.contract'; +import { AccountFetchOptions } from './entities/account.fetch.options'; +import { Provider } from '../providers/entities/provider'; @Injectable() export class AccountService { @@ -74,39 +74,38 @@ export class AccountService { return await this.indexerService.getAccountsCount(filter); } - async getAccount(address: string, fields?: string[], withGuardianInfo?: boolean): Promise { + async getAccount(address: string, options?: AccountFetchOptions): Promise { if (!AddressUtils.isAddressValid(address)) { return null; } - const provider: Provider | undefined = await this.providerService.getProvider(address); - - let txCount: number = 0; - let scrCount: number = 0; - - if (!fields || fields.length === 0 || fields.includes(AccountOptionalFieldOption.txCount)) { - txCount = await this.getAccountTxCount(address); + const account = await this.getAccountRaw(address, options?.withAssets); + if (!account) { + return null; } - if (!fields || fields.length === 0 || fields.includes(AccountOptionalFieldOption.scrCount)) { - scrCount = await this.getAccountScResults(address); + if (options?.withTxCount === true) { + account.txCount = await this.getAccountTxCount(address); } - const [account, elasticSearchAccount] = await Promise.all([ - this.getAccountRaw(address, txCount, scrCount), - this.indexerService.getAccount(address), - ]); + if (options?.withScrCount === true) { + account.scrCount = await this.getAccountScResults(address); + } - if (account && withGuardianInfo === true) { + if (options?.withGuardianInfo === true) { await this.applyGuardianInfo(account); } - if (account && elasticSearchAccount) { + if (options?.withTimestamp) { + const elasticSearchAccount = await this.indexerService.getAccount(address); account.timestamp = elasticSearchAccount.timestamp; } - if (account && provider && provider.owner) { - account.ownerAddress = provider.owner; + if (AddressUtils.isSmartContractAddress(address)) { + const provider: Provider | undefined = await this.providerService.getProvider(address); + if (provider && provider.owner) { + account.ownerAddress = provider.owner; + } } return account; @@ -161,8 +160,7 @@ export class AccountService { return await this.getAccountRaw(address); } - async getAccountRaw(address: string, txCount: number = 0, scrCount: number = 0): Promise { - const assets = await this.assetsService.getAllAccountAssets(); + async getAccountRaw(address: string, withAssets?: boolean): Promise { try { const { account: { nonce, balance, code, codeHash, rootHash, developerReward, ownerAddress, codeMetadata }, @@ -170,7 +168,13 @@ export class AccountService { const shardCount = await this.protocolService.getShardCount(); const shard = AddressUtils.computeShard(AddressUtils.bech32Decode(address), shardCount); - let account = new AccountDetailed({ address, nonce, balance, code, codeHash, rootHash, txCount, scrCount, shard, developerReward, ownerAddress, scamInfo: undefined, assets: assets[address], ownerAssets: assets[ownerAddress], nftCollections: undefined, nfts: undefined }); + let account = new AccountDetailed({ address, nonce, balance, code, codeHash, rootHash, shard, developerReward, ownerAddress, scamInfo: undefined, nftCollections: undefined, nfts: undefined }); + + if (withAssets === true) { + const assets = await this.assetsService.getAllAccountAssets(); + account.assets = assets[address]; + account.ownerAssets = assets[ownerAddress]; + } const codeAttributes = AddressUtils.decodeCodeMetadata(codeMetadata); if (codeAttributes) { @@ -328,6 +332,24 @@ export class AccountService { const verifiedAccounts = await this.cachingService.get(CacheInfo.VerifiedAccounts.key); + if (options.addresses && options.addresses.length > 0) { + const gatewayResponse: any = await this.gatewayService.getAccountsBulk(options.addresses); + const finalAccounts: Record = {}; + + for (const address in gatewayResponse) { + if (gatewayResponse.hasOwnProperty(address)) { + finalAccounts[address] = gatewayResponse[address] as AccountDetailed; + } + } + + for (const account of accounts) { + const gatewayAccount = finalAccounts[account.address]; + if (gatewayAccount) { + account.balance = gatewayAccount.balance; + } + } + } + for (const account of accounts) { account.shard = AddressUtils.computeShard(AddressUtils.bech32Decode(account.address), shardCount); account.assets = assets[account.address]; @@ -357,8 +379,6 @@ export class AccountService { if (verifiedAccounts && verifiedAccounts.includes(account.address)) { account.isVerified = true; } - - } return accounts; diff --git a/src/endpoints/accounts/entities/account.fetch.options.ts b/src/endpoints/accounts/entities/account.fetch.options.ts new file mode 100644 index 000000000..367686761 --- /dev/null +++ b/src/endpoints/accounts/entities/account.fetch.options.ts @@ -0,0 +1,11 @@ +export class AccountFetchOptions { + constructor(init?: Partial) { + Object.assign(this, init); + } + + withGuardianInfo?: boolean; + withTxCount?: boolean; + withScrCount?: boolean; + withTimestamp?: boolean; + withAssets?: boolean; +} diff --git a/src/endpoints/accounts/entities/account.optional.field.options.ts b/src/endpoints/accounts/entities/account.optional.field.options.ts deleted file mode 100644 index fb11f731e..000000000 --- a/src/endpoints/accounts/entities/account.optional.field.options.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum AccountOptionalFieldOption { - scrCount = "scrCount", - txCount = "txCount", -} diff --git a/src/endpoints/accounts/entities/account.query.options.ts b/src/endpoints/accounts/entities/account.query.options.ts index 35b32bca7..91649d3c1 100644 --- a/src/endpoints/accounts/entities/account.query.options.ts +++ b/src/endpoints/accounts/entities/account.query.options.ts @@ -35,6 +35,10 @@ export class AccountQueryOptions { if (this.withScrCount && size > 25) { throw new BadRequestException('Size must be less than or equal to 25 when withScrCount is set'); } + + if (this.addresses && this.addresses.length > 25) { + throw new BadRequestException('Addresses array must contain 25 or fewer elements'); + } } isSet(): boolean { @@ -48,6 +52,7 @@ export class AccountQueryOptions { this.tags !== undefined || this.excludeTags !== undefined || this.hasAssets !== undefined || - this.search !== undefined; + this.search !== undefined || + this.addresses !== undefined; } } diff --git a/src/endpoints/collections/collection.controller.ts b/src/endpoints/collections/collection.controller.ts index 039ccd434..df5f2fa24 100644 --- a/src/endpoints/collections/collection.controller.ts +++ b/src/endpoints/collections/collection.controller.ts @@ -66,15 +66,15 @@ export class CollectionController { @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('before', new ParseIntPipe) before?: number, - @Query('after', new ParseIntPipe) after?: number, - @Query('canCreate', new ParseAddressPipe) canCreate?: string, - @Query('canBurn', new ParseAddressPipe) canBurn?: string, - @Query('canAddQuantity', new ParseAddressPipe) canAddQuantity?: string, - @Query('canUpdateAttributes', new ParseAddressPipe) canUpdateAttributes?: string, - @Query('canAddUri', new ParseAddressPipe) canAddUri?: string, - @Query('canTransferRole', new ParseAddressPipe) canTransferRole?: string, - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('canCreate', ParseAddressPipe) canCreate?: string, + @Query('canBurn', ParseAddressPipe) canBurn?: string, + @Query('canAddQuantity', ParseAddressPipe) canAddQuantity?: string, + @Query('canUpdateAttributes', ParseAddressPipe) canUpdateAttributes?: string, + @Query('canAddUri', ParseAddressPipe) canAddUri?: string, + @Query('canTransferRole', ParseAddressPipe) canTransferRole?: string, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, @Query('sort', new ParseEnumPipe(SortCollections)) sort?: SortCollections, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, ): Promise { @@ -117,15 +117,15 @@ export class CollectionController { @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('before', new ParseIntPipe) before?: number, - @Query('after', new ParseIntPipe) after?: number, - @Query('canCreate', new ParseAddressPipe) canCreate?: string, - @Query('canBurn', new ParseAddressPipe) canBurn?: string, - @Query('canAddQuantity', new ParseAddressPipe) canAddQuantity?: string, - @Query('canUpdateAttributes', new ParseAddressPipe) canUpdateAttributes?: string, - @Query('canAddUri', new ParseAddressPipe) canAddUri?: string, - @Query('canTransferRole', new ParseAddressPipe) canTransferRole?: string, - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('canCreate', ParseAddressPipe) canCreate?: string, + @Query('canBurn', ParseAddressPipe) canBurn?: string, + @Query('canAddQuantity', ParseAddressPipe) canAddQuantity?: string, + @Query('canUpdateAttributes', ParseAddressPipe) canUpdateAttributes?: string, + @Query('canAddUri', ParseAddressPipe) canAddUri?: string, + @Query('canTransferRole', ParseAddressPipe) canTransferRole?: string, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, ): Promise { return await this.collectionService.getNftCollectionCount(new CollectionFilter({ search, @@ -149,15 +149,15 @@ export class CollectionController { @Query('search') search?: string, @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('before', new ParseIntPipe) before?: number, - @Query('after', new ParseIntPipe) after?: number, - @Query('canCreate', new ParseAddressPipe) canCreate?: string, - @Query('canBurn', new ParseAddressPipe) canBurn?: string, - @Query('canAddQuantity', new ParseAddressPipe) canAddQuantity?: string, - @Query('canUpdateAttributes', new ParseAddressPipe) canUpdateAttributes?: string, - @Query('canAddUri', new ParseAddressPipe) canAddUri?: string, - @Query('canTransferRole', new ParseAddressPipe) canTransferRole?: string, - @Query('excludeMetaESDT', new ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('canCreate', ParseAddressPipe) canCreate?: string, + @Query('canBurn', ParseAddressPipe) canBurn?: string, + @Query('canAddQuantity', ParseAddressPipe) canAddQuantity?: string, + @Query('canUpdateAttributes', ParseAddressPipe) canUpdateAttributes?: string, + @Query('canAddUri', ParseAddressPipe) canAddUri?: string, + @Query('canTransferRole', ParseAddressPipe) canTransferRole?: string, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, ): Promise { return await this.collectionService.getNftCollectionCount(new CollectionFilter({ search, @@ -235,14 +235,14 @@ export class CollectionController { @Query('name') name?: string, @Query('tags', ParseArrayPipe) tags?: string[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('isWhitelistedStorage', new ParseBoolPipe) isWhitelistedStorage?: boolean, - @Query('hasUris', new ParseBoolPipe) hasUris?: boolean, - @Query('isNsfw', new ParseBoolPipe) isNsfw?: boolean, - @Query('traits', new ParseRecordPipe) traits?: Record, - @Query('nonceBefore', new ParseIntPipe) nonceBefore?: number, - @Query('nonceAfter', new ParseIntPipe) nonceAfter?: number, - @Query('withOwner', new ParseBoolPipe) withOwner?: boolean, - @Query('withSupply', new ParseBoolPipe) withSupply?: boolean, + @Query('isWhitelistedStorage', ParseBoolPipe) isWhitelistedStorage?: boolean, + @Query('hasUris', ParseBoolPipe) hasUris?: boolean, + @Query('isNsfw', ParseBoolPipe) isNsfw?: boolean, + @Query('traits', ParseRecordPipe) traits?: Record, + @Query('nonceBefore', ParseIntPipe) nonceBefore?: number, + @Query('nonceAfter', ParseIntPipe) nonceAfter?: number, + @Query('withOwner', ParseBoolPipe) withOwner?: boolean, + @Query('withSupply', ParseBoolPipe) withSupply?: boolean, @Query('sort', new ParseEnumPipe(SortCollectionNfts)) sort?: SortCollectionNfts, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, ): Promise { @@ -279,11 +279,11 @@ export class CollectionController { @Query('name') name?: string, @Query('tags', ParseArrayPipe) tags?: string[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('isWhitelistedStorage', new ParseBoolPipe) isWhitelistedStorage?: boolean, - @Query('hasUris', new ParseBoolPipe) hasUris?: boolean, - @Query('traits', new ParseRecordPipe) traits?: Record, - @Query('nonceBefore', new ParseIntPipe) nonceBefore?: number, - @Query('nonceAfter', new ParseIntPipe) nonceAfter?: number, + @Query('isWhitelistedStorage', ParseBoolPipe) isWhitelistedStorage?: boolean, + @Query('hasUris', ParseBoolPipe) hasUris?: boolean, + @Query('traits', ParseRecordPipe) traits?: Record, + @Query('nonceBefore', ParseIntPipe) nonceBefore?: number, + @Query('nonceAfter', ParseIntPipe) nonceAfter?: number, ): Promise { const isCollection = await this.collectionService.isCollection(collection); if (!isCollection) { @@ -336,6 +336,7 @@ export class CollectionController { @ApiQuery({ name: 'withLogs', description: 'Return logs for transactions', required: false, type: Boolean }) @ApiQuery({ name: 'withScamInfo', description: 'Returns scam information', required: false, type: Boolean }) @ApiQuery({ name: 'withUsername', description: 'Integrates username in assets for all addresses present in the transactions', required: false, type: Boolean }) + @ApiQuery({ name: 'withRelayedScresults', description: 'If set to true, will include smart contract results that resemble relayed transactions', required: false, type: Boolean }) async getCollectionTransactions( @Param('collection', ParseCollectionPipe) identifier: string, @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @@ -352,11 +353,12 @@ export class CollectionController { @Query('after', ParseIntPipe) after?: number, @Query('round', ParseIntPipe) round?: number, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @Query('withScResults', new ParseBoolPipe) withScResults?: boolean, - @Query('withOperations', new ParseBoolPipe) withOperations?: boolean, - @Query('withLogs', new ParseBoolPipe) withLogs?: boolean, - @Query('withScamInfo', new ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', new ParseBoolPipe) withUsername?: boolean, + @Query('withScResults', ParseBoolPipe) withScResults?: boolean, + @Query('withOperations', ParseBoolPipe) withOperations?: boolean, + @Query('withLogs', ParseBoolPipe) withLogs?: boolean, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, ) { const options = TransactionQueryOptions.applyDefaultOptions(size, { withScResults, withOperations, withLogs, withScamInfo, withUsername }); @@ -365,7 +367,7 @@ export class CollectionController { throw new HttpException('Collection not found', HttpStatus.NOT_FOUND); } - return await this.transactionService.getTransactions(new TransactionFilter({ + const transactionFilter = new TransactionFilter({ sender, receivers: receiver, token: identifier, @@ -379,7 +381,11 @@ export class CollectionController { after, order, round, - }), new QueryPagination({ from, size }), options); + withRelayedScresults, + }); + TransactionFilter.validate(transactionFilter, size); + + return await this.transactionService.getTransactions(transactionFilter, new QueryPagination({ from, size }), options); } @Get("/collections/:collection/transactions/count") @@ -396,6 +402,7 @@ export class CollectionController { @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) @ApiQuery({ name: 'round', description: 'Filter by round number', required: false }) + @ApiQuery({ name: 'withRelayedScresults', description: 'If set to true, will include smart contract results that resemble relayed transactions', required: false, type: Boolean }) async getCollectionTransactionsCount( @Param('collection', ParseCollectionPipe) identifier: string, @Query('sender', ParseAddressPipe) sender?: string, @@ -408,6 +415,7 @@ export class CollectionController { @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, @Query('round', ParseIntPipe) round?: number, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, ) { const isCollection = await this.collectionService.isCollection(identifier); if (!isCollection) { @@ -426,6 +434,7 @@ export class CollectionController { before, after, round, + withRelayedScresults, })); } @@ -469,11 +478,11 @@ export class CollectionController { @Query('after', ParseIntPipe) after?: number, @Query('round', ParseIntPipe) round?: number, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @Query('withScResults', new ParseBoolPipe) withScResults?: boolean, - @Query('withOperations', new ParseBoolPipe) withOperations?: boolean, - @Query('withLogs', new ParseBoolPipe) withLogs?: boolean, - @Query('withScamInfo', new ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', new ParseBoolPipe) withUsername?: boolean, + @Query('withScResults', ParseBoolPipe) withScResults?: boolean, + @Query('withOperations', ParseBoolPipe) withOperations?: boolean, + @Query('withLogs', ParseBoolPipe) withLogs?: boolean, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, ) { const options = TransactionQueryOptions.applyDefaultOptions(size, { withScResults, withOperations, withLogs, withScamInfo, withUsername }); diff --git a/src/endpoints/keys/keys.service.ts b/src/endpoints/keys/keys.service.ts index 149196bf3..b4887e654 100644 --- a/src/endpoints/keys/keys.service.ts +++ b/src/endpoints/keys/keys.service.ts @@ -27,6 +27,10 @@ export class KeysService { [key] ); + if (!encoded || !encoded[0]) { + return { remainingUnBondPeriod: 0 }; + } + let remainingUnBondPeriod = parseInt(Buffer.from(encoded[0], 'base64').toString('ascii')); if (isNaN(remainingUnBondPeriod)) { diff --git a/src/endpoints/nfts/nft.controller.ts b/src/endpoints/nfts/nft.controller.ts index 539d1797e..50a495d24 100644 --- a/src/endpoints/nfts/nft.controller.ts +++ b/src/endpoints/nfts/nft.controller.ts @@ -68,16 +68,16 @@ export class NftController { @Query('name') name?: string, @Query('tags', ParseArrayPipe) tags?: string[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('isWhitelistedStorage', new ParseBoolPipe) isWhitelistedStorage?: boolean, - @Query('hasUris', new ParseBoolPipe) hasUris?: boolean, - @Query('isNsfw', new ParseBoolPipe) isNsfw?: boolean, - @Query('isScam', new ParseBoolPipe) isScam?: boolean, + @Query('isWhitelistedStorage', ParseBoolPipe) isWhitelistedStorage?: boolean, + @Query('hasUris', ParseBoolPipe) hasUris?: boolean, + @Query('isNsfw', ParseBoolPipe) isNsfw?: boolean, + @Query('isScam', ParseBoolPipe) isScam?: boolean, @Query('scamType', new ParseEnumPipe(ScamType)) scamType?: ScamType, - @Query('traits', new ParseRecordPipe) traits?: Record, - @Query('before', new ParseIntPipe) before?: number, - @Query('after', new ParseIntPipe) after?: number, - @Query('withOwner', new ParseBoolPipe) withOwner?: boolean, - @Query('withSupply', new ParseBoolPipe) withSupply?: boolean, + @Query('traits', ParseRecordPipe) traits?: Record, + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('withOwner', ParseBoolPipe) withOwner?: boolean, + @Query('withSupply', ParseBoolPipe) withSupply?: boolean, ): Promise { return await this.nftService.getNfts( new QueryPagination({ from, size }), @@ -134,13 +134,13 @@ export class NftController { @Query('name') name?: string, @Query('tags', ParseArrayPipe) tags?: string[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('isWhitelistedStorage', new ParseBoolPipe) isWhitelistedStorage?: boolean, - @Query('hasUris', new ParseBoolPipe) hasUris?: boolean, - @Query('isNsfw', new ParseBoolPipe) isNsfw?: boolean, - @Query('traits', new ParseRecordPipe) traits?: Record, - @Query('before', new ParseIntPipe) before?: number, - @Query('after', new ParseIntPipe) after?: number, - @Query('isScam', new ParseBoolPipe) isScam?: boolean, + @Query('isWhitelistedStorage', ParseBoolPipe) isWhitelistedStorage?: boolean, + @Query('hasUris', ParseBoolPipe) hasUris?: boolean, + @Query('isNsfw', ParseBoolPipe) isNsfw?: boolean, + @Query('traits', ParseRecordPipe) traits?: Record, + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('isScam', ParseBoolPipe) isScam?: boolean, @Query('scamType', new ParseEnumPipe(ScamType)) scamType?: ScamType, ): Promise { return await this.nftService.getNftCount( @@ -177,13 +177,13 @@ export class NftController { @Query('name') name?: string, @Query('tags', ParseArrayPipe) tags?: string[], @Query('creator', ParseAddressPipe) creator?: string, - @Query('isWhitelistedStorage', new ParseBoolPipe) isWhitelistedStorage?: boolean, - @Query('hasUris', new ParseBoolPipe) hasUris?: boolean, - @Query('isNsfw', new ParseBoolPipe) isNsfw?: boolean, - @Query('traits', new ParseRecordPipe) traits?: Record, - @Query('before', new ParseIntPipe) before?: number, - @Query('after', new ParseIntPipe) after?: number, - @Query('isScam', new ParseBoolPipe) isScam?: boolean, + @Query('isWhitelistedStorage', ParseBoolPipe) isWhitelistedStorage?: boolean, + @Query('hasUris', ParseBoolPipe) hasUris?: boolean, + @Query('isNsfw', ParseBoolPipe) isNsfw?: boolean, + @Query('traits', ParseRecordPipe) traits?: Record, + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('isScam', ParseBoolPipe) isScam?: boolean, @Query('scamType', new ParseEnumPipe(ScamType)) scamType?: ScamType, ): Promise { return await this.nftService.getNftCount(new NftFilter({ search, identifiers, type, subType, collection, collections, name, tags, creator, isWhitelistedStorage, hasUris, isNsfw, traits, before, after, isScam, scamType })); @@ -297,6 +297,7 @@ export class NftController { @ApiQuery({ name: 'withLogs', description: 'Return logs for transactions', required: false, type: Boolean }) @ApiQuery({ name: 'withScamInfo', description: 'Returns scam information', required: false, type: Boolean }) @ApiQuery({ name: 'withUsername', description: 'Integrates username in assets for all addresses present in the transactions', required: false, type: Boolean }) + @ApiQuery({ name: 'withRelayedScresults', description: 'If set to true, will include smart contract results that resemble relayed transactions', required: false, type: Boolean }) async getNftTransactions( @Param('identifier', ParseNftPipe) identifier: string, @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @@ -312,15 +313,17 @@ export class NftController { @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @Query('withScResults', new ParseBoolPipe) withScResults?: boolean, - @Query('withOperations', new ParseBoolPipe) withOperations?: boolean, - @Query('withLogs', new ParseBoolPipe) withLogs?: boolean, - @Query('withScamInfo', new ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', new ParseBoolPipe) withUsername?: boolean, + + @Query('withScResults', ParseBoolPipe) withScResults?: boolean, + @Query('withOperations', ParseBoolPipe) withOperations?: boolean, + @Query('withLogs', ParseBoolPipe) withLogs?: boolean, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, ) { const options = TransactionQueryOptions.applyDefaultOptions(size, { withScResults, withOperations, withLogs, withScamInfo, withUsername }); - return await this.transactionService.getTransactions(new TransactionFilter({ + const transactionFilter = new TransactionFilter({ sender, receivers: receiver, token: identifier, @@ -333,7 +336,11 @@ export class NftController { before, after, order, - }), new QueryPagination({ from, size }), options); + withRelayedScresults, + }); + TransactionFilter.validate(transactionFilter, size); + + return await this.transactionService.getTransactions(transactionFilter, new QueryPagination({ from, size }), options); } @Get("/nfts/:identifier/transactions/count") @@ -349,6 +356,7 @@ export class NftController { @ApiQuery({ name: 'status', description: 'Status of the transaction (success / pending / invalid / fail)', required: false, enum: TransactionStatus }) @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiQuery({ name: 'withRelayedScresults', description: 'If set to true, will include smart contract results that resemble relayed transactions', required: false, type: Boolean }) async getNftTransactionsCount( @Param('identifier', ParseNftPipe) identifier: string, @Query('sender', ParseAddressPipe) sender?: string, @@ -360,6 +368,7 @@ export class NftController { @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, ) { return await this.transactionService.getTransactionCount(new TransactionFilter({ @@ -373,6 +382,7 @@ export class NftController { status, before, after, + withRelayedScresults, })); } @@ -414,11 +424,11 @@ export class NftController { @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @Query('withScResults', new ParseBoolPipe) withScResults?: boolean, - @Query('withOperations', new ParseBoolPipe) withOperations?: boolean, - @Query('withLogs', new ParseBoolPipe) withLogs?: boolean, - @Query('withScamInfo', new ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', new ParseBoolPipe) withUsername?: boolean, + @Query('withScResults', ParseBoolPipe) withScResults?: boolean, + @Query('withOperations', ParseBoolPipe) withOperations?: boolean, + @Query('withLogs', ParseBoolPipe) withLogs?: boolean, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, ) { const options = TransactionQueryOptions.applyDefaultOptions(size, { withScResults, withOperations, withLogs, withScamInfo, withUsername }); diff --git a/src/endpoints/nfts/nft.service.ts b/src/endpoints/nfts/nft.service.ts index 1c357d8ef..e0af78f6b 100644 --- a/src/endpoints/nfts/nft.service.ts +++ b/src/endpoints/nfts/nft.service.ts @@ -31,6 +31,7 @@ import { NftRarities } from "./entities/nft.rarities"; import { SortCollectionNfts } from "../collections/entities/sort.collection.nfts"; import { TokenAssets } from "src/common/assets/entities/token.assets"; import { ScamInfo } from "src/common/entities/scam-info.dto"; +import { NftSubType } from "./entities/nft.sub.type"; @Injectable() export class NftService { @@ -217,7 +218,10 @@ export class NftService { return undefined; } - if (nft.type.in(NftType.SemiFungibleESDT, NftType.MetaESDT)) { + if (nft.type && nft.type.in( + NftType.SemiFungibleESDT, NftType.MetaESDT, + NftSubType.DynamicSemiFungibleESDT, NftSubType.DynamicMetaESDT + )) { await this.applySupply(nft); } diff --git a/src/endpoints/nodes/node.controller.ts b/src/endpoints/nodes/node.controller.ts index f00bea6bb..eb478981d 100644 --- a/src/endpoints/nodes/node.controller.ts +++ b/src/endpoints/nodes/node.controller.ts @@ -59,7 +59,7 @@ export class NodeController { @Query('fullHistory', ParseBoolPipe) fullHistory?: boolean, @Query('sort', new ParseEnumPipe(NodeSort)) sort?: NodeSort, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @Query('withIdentityInfo', new ParseBoolPipe) withIdentityInfo?: boolean, + @Query('withIdentityInfo', ParseBoolPipe) withIdentityInfo?: boolean, @Query('isQualified', ParseBoolPipe) isQualified?: boolean, @Query('isAuctioned', ParseBoolPipe) isAuctioned?: boolean, @Query('isAuctionDangerZone', ParseBoolPipe) isAuctionDangerZone?: boolean, diff --git a/src/endpoints/pool/entities/pool.filter.ts b/src/endpoints/pool/entities/pool.filter.ts index d1bd4c8e8..539c422a4 100644 --- a/src/endpoints/pool/entities/pool.filter.ts +++ b/src/endpoints/pool/entities/pool.filter.ts @@ -10,4 +10,5 @@ export class PoolFilter { senderShard?: number; receiverShard?: number; type?: TransactionType; + functions?: string[]; } diff --git a/src/endpoints/pool/entities/transaction.in.pool.dto.ts b/src/endpoints/pool/entities/transaction.in.pool.dto.ts index a18830d54..d6aa725a9 100644 --- a/src/endpoints/pool/entities/transaction.in.pool.dto.ts +++ b/src/endpoints/pool/entities/transaction.in.pool.dto.ts @@ -48,6 +48,9 @@ export class TransactionInPool { @ApiProperty({ type: String, example: "0228618b6339c5eaf71ed1a8cd71df010ccd0369a29d957c37d53b0409408161726dd97e10ac7836996f666ffd636a797b9b9abecbd276971376fb3479b48203" }) signature: string = ''; + @ApiProperty({ type: String, nullable: true, example: 'composeTasks', required: false }) + function: string = ''; + @ApiProperty({ type: String, example: "SmartContractResult" }) type: TransactionType = TransactionType.Transaction; } diff --git a/src/endpoints/pool/pool.controller.ts b/src/endpoints/pool/pool.controller.ts index 18aa91a32..f6948e4f9 100644 --- a/src/endpoints/pool/pool.controller.ts +++ b/src/endpoints/pool/pool.controller.ts @@ -1,4 +1,4 @@ -import { ParseAddressAndMetachainPipe, ParseAddressPipe, ParseEnumPipe, ParseIntPipe, ParseTransactionHashPipe } from "@multiversx/sdk-nestjs-common"; +import { ParseAddressAndMetachainPipe, ParseAddressPipe, ParseEnumPipe, ParseIntPipe, ParseTransactionHashPipe, ParseArrayPipe } from "@multiversx/sdk-nestjs-common"; import { Controller, DefaultValuePipe, Get, NotFoundException, Param, Query } from "@nestjs/common"; import { ApiExcludeEndpoint, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger"; import { PoolService } from "./pool.service"; @@ -6,6 +6,7 @@ import { QueryPagination } from "src/common/entities/query.pagination"; import { TransactionInPool } from "./entities/transaction.in.pool.dto"; import { TransactionType } from "../transactions/entities/transaction.type"; import { PoolFilter } from "./entities/pool.filter"; +import { ParseArrayPipeOptions } from "@multiversx/sdk-nestjs-common/lib/pipes/entities/parse.array.options"; @Controller() @ApiTags('pool') @@ -24,6 +25,8 @@ export class PoolController { @ApiQuery({ name: 'senderShard', description: 'The shard of the sender', required: false }) @ApiQuery({ name: 'receiverShard', description: 'The shard of the receiver', required: false }) @ApiQuery({ name: 'type', description: 'Search in transaction pool by type', required: false }) + @ApiQuery({ name: 'function', description: 'Filter transactions by function name', required: false }) + async getTransactionPool( @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, @@ -32,6 +35,7 @@ export class PoolController { @Query('senderShard', ParseIntPipe) senderShard?: number, @Query('receiverShard', ParseIntPipe) receiverShard?: number, @Query('type', new ParseEnumPipe(TransactionType)) type?: TransactionType, + @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], ): Promise { return await this.poolService.getPool(new QueryPagination({ from, size }), new PoolFilter({ sender: sender, @@ -39,6 +43,7 @@ export class PoolController { senderShard: senderShard, receiverShard: receiverShard, type: type, + functions: functions, })); } diff --git a/src/endpoints/pool/pool.module.ts b/src/endpoints/pool/pool.module.ts index 11ff8ec83..88c7a780e 100644 --- a/src/endpoints/pool/pool.module.ts +++ b/src/endpoints/pool/pool.module.ts @@ -1,7 +1,11 @@ import { Module } from "@nestjs/common"; import { PoolService } from "./pool.service"; +import { TransactionActionModule } from "../transactions/transaction-action/transaction.action.module"; @Module({ + imports: [ + TransactionActionModule, + ], providers: [ PoolService, ], diff --git a/src/endpoints/pool/pool.service.ts b/src/endpoints/pool/pool.service.ts index b45e9efc3..4bfd15eb1 100644 --- a/src/endpoints/pool/pool.service.ts +++ b/src/endpoints/pool/pool.service.ts @@ -11,6 +11,9 @@ import { PoolFilter } from "./entities/pool.filter"; import { TxInPoolFields } from "src/common/gateway/entities/tx.in.pool.fields"; import { AddressUtils } from "@multiversx/sdk-nestjs-common"; import { ProtocolService } from "../../common/protocol/protocol.service"; +import { TransactionActionService } from "../transactions/transaction-action/transaction.action.service"; +import { Transaction } from "../transactions/entities/transaction"; +import { ApiUtils } from "@multiversx/sdk-nestjs-http"; @Injectable() export class PoolService { @@ -19,6 +22,7 @@ export class PoolService { private readonly apiConfigService: ApiConfigService, private readonly cacheService: CacheService, private readonly protocolService: ProtocolService, + private readonly transactionActionService: TransactionActionService, ) { } async getTransactionFromPool(txHash: string): Promise { @@ -105,6 +109,11 @@ export class PoolService { transaction.receiverShard = AddressUtils.computeShard(AddressUtils.bech32Decode(transaction.receiver), shardCount); } + const metadata = await this.transactionActionService.getTransactionMetadata(this.poolTransactionToTransaction(transaction), false); + if (metadata && metadata.functionName) { + transaction.function = metadata.functionName; + } + return transaction; } @@ -115,8 +124,13 @@ export class PoolService { (!filters.receiver || transaction.receiver === filters.receiver) && (!filters.type || transaction.type === filters.type) && (filters.senderShard === undefined || transaction.senderShard === filters.senderShard) && - (filters.receiverShard === undefined || transaction.receiverShard === filters.receiverShard) + (filters.receiverShard === undefined || transaction.receiverShard === filters.receiverShard) && + (filters.functions === undefined || transaction.function === undefined || filters.functions.indexOf(transaction.function) > -1) ); }); } + + private poolTransactionToTransaction(transaction: TransactionInPool): Transaction { + return ApiUtils.mergeObjects(new Transaction(), transaction); + } } diff --git a/src/endpoints/providers/provider.controller.ts b/src/endpoints/providers/provider.controller.ts index 03bc999ef..eb444c9ee 100644 --- a/src/endpoints/providers/provider.controller.ts +++ b/src/endpoints/providers/provider.controller.ts @@ -26,8 +26,8 @@ export class ProviderController { @Query('identity') identity?: string, @Query('owner', ParseAddressPipe) owner?: string, @Query('providers', ParseAddressArrayPipe) providers?: string[], - @Query('withIdentityInfo', new ParseBoolPipe) withIdentityInfo?: boolean, - @Query('withLatestInfo', new ParseBoolPipe) withLatestInfo?: boolean, + @Query('withIdentityInfo', ParseBoolPipe) withIdentityInfo?: boolean, + @Query('withLatestInfo', ParseBoolPipe) withLatestInfo?: boolean, ): Promise { const options = ProviderQueryOptions.applyDefaultOptions(owner, { withIdentityInfo, withLatestInfo }); diff --git a/src/endpoints/providers/provider.service.ts b/src/endpoints/providers/provider.service.ts index 7c76bcd6c..0cd9d3396 100644 --- a/src/endpoints/providers/provider.service.ts +++ b/src/endpoints/providers/provider.service.ts @@ -106,13 +106,19 @@ export class ProviderService { const nodesGroupedByProvider: { [key: string]: any[] } = nodes.groupBy(x => x.provider); - const providersDelegationData: DelegationData[] = await this.getDelegationProviders(); + const providersDelegationData = await this.getDelegationProviders(); + if (!Array.isArray(providersDelegationData)) { + return providers; + } providers.forEach((element) => { const providerAddress = element.provider; // Delegation details for provider - const delegationData: DelegationData | undefined = providersDelegationData.find((providerDelegationInfo: any) => providerDelegationInfo !== null && providerAddress === providerDelegationInfo.contract); + const delegationData = providersDelegationData.find((providerDelegationInfo: any) => + providerDelegationInfo !== null && providerAddress === providerDelegationInfo.contract + ); + if (delegationData) { if (delegationData.aprValue) { element.apr = parseFloat(delegationData.aprValue.toFixed(2)); diff --git a/src/endpoints/proxy/gateway.proxy.controller.ts b/src/endpoints/proxy/gateway.proxy.controller.ts index 48f9fb434..3b7e12c37 100644 --- a/src/endpoints/proxy/gateway.proxy.controller.ts +++ b/src/endpoints/proxy/gateway.proxy.controller.ts @@ -6,7 +6,7 @@ import { GatewayService } from "src/common/gateway/gateway.service"; import { Response, Request } from "express"; import { GatewayComponentRequest } from "src/common/gateway/entities/gateway.component.request"; import { PluginService } from "src/common/plugins/plugin.service"; -import { Constants, ParseAddressPipe, ParseBlockHashPipe, ParseBlsHashPipe, ParseIntPipe, ParseTransactionHashPipe } from "@multiversx/sdk-nestjs-common"; +import { Constants, ParseAddressPipe, ParseBlockHashPipe, ParseBlsHashPipe, ParseIntPipe, ParseTransactionHashPipe, ParseBoolPipe } from "@multiversx/sdk-nestjs-common"; import { CacheService, NoCache } from "@multiversx/sdk-nestjs-cache"; import { OriginLogger } from "@multiversx/sdk-nestjs-common"; import { DeepHistoryInterceptor } from "src/interceptors/deep-history.interceptor"; @@ -118,8 +118,12 @@ export class GatewayProxyController { } @Post('/transaction/simulate') - async transactionSimulate(@Body() body: any) { - return await this.gatewayPost('transaction/simulate', GatewayComponentRequest.simulateTransaction, body); + async transactionSimulate(@Query('checkSignature', ParseBoolPipe) checkSignature: boolean, @Body() body: any) { + let url = 'transaction/simulate'; + if (checkSignature !== undefined) { + url += `?checkSignature=${checkSignature}`; + } + return await this.gatewayPost(url, GatewayComponentRequest.simulateTransaction, body); } @Post('/transaction/send-multiple') diff --git a/src/endpoints/rounds/round.controller.ts b/src/endpoints/rounds/round.controller.ts index 783e0eaf3..9fefac936 100644 --- a/src/endpoints/rounds/round.controller.ts +++ b/src/endpoints/rounds/round.controller.ts @@ -26,8 +26,8 @@ export class RoundController { @Query("size", new DefaultValuePipe(25), ParseIntPipe) size: number, @Query("validator", ParseBlsHashPipe) validator?: string, @Query('condition', new ParseEnumPipe(QueryConditionOptions)) condition?: QueryConditionOptions, - @Query("shard", new ParseIntPipe) shard?: number, - @Query("epoch", new ParseIntPipe) epoch?: number, + @Query("shard", ParseIntPipe) shard?: number, + @Query("epoch", ParseIntPipe) epoch?: number, ): Promise { return this.roundService.getRounds(new RoundFilter({ from, size, condition, validator, shard, epoch })); } @@ -42,8 +42,8 @@ export class RoundController { getRoundCount( @Query("validator", ParseBlsHashPipe) validator?: string, @Query('condition', new ParseEnumPipe(QueryConditionOptions)) condition?: QueryConditionOptions, - @Query("shard", new ParseIntPipe) shard?: number, - @Query("epoch", new ParseIntPipe) epoch?: number, + @Query("shard", ParseIntPipe) shard?: number, + @Query("epoch", ParseIntPipe) epoch?: number, ): Promise { return this.roundService.getRoundCount(new RoundFilter({ condition, validator, shard, epoch })); } @@ -53,8 +53,8 @@ export class RoundController { getRoundCountAlternative( @Query("validator", ParseBlsHashPipe) validator?: string, @Query('condition', new ParseEnumPipe(QueryConditionOptions)) condition?: QueryConditionOptions, - @Query("shard", new ParseIntPipe) shard?: number, - @Query("epoch", new ParseIntPipe) epoch?: number, + @Query("shard", ParseIntPipe) shard?: number, + @Query("epoch", ParseIntPipe) epoch?: number, ): Promise { return this.roundService.getRoundCount(new RoundFilter({ condition, validator, shard, epoch })); } diff --git a/src/endpoints/tokens/token.controller.ts b/src/endpoints/tokens/token.controller.ts index e3d1e1854..757a29db6 100644 --- a/src/endpoints/tokens/token.controller.ts +++ b/src/endpoints/tokens/token.controller.ts @@ -59,7 +59,7 @@ export class TokenController { @Query('identifiers', ParseArrayPipe) identifiers?: string[], @Query('sort', new ParseEnumPipe(TokenSort)) sort?: TokenSort, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @Query('includeMetaESDT', new ParseBoolPipe) includeMetaESDT?: boolean, + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], @Query('priceSource', new ParseEnumPipe(TokenAssetsPriceSourceType)) priceSource?: TokenAssetsPriceSourceType, ): Promise { @@ -87,7 +87,7 @@ export class TokenController { @Query('type', new ParseEnumPipe(TokenType)) type?: TokenType, @Query('identifier', ParseTokenPipe) identifier?: string, @Query('identifiers', ParseArrayPipe) identifiers?: string[], - @Query('includeMetaESDT', new ParseBoolPipe) includeMetaESDT?: boolean, + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], @Query('priceSource', new ParseEnumPipe(TokenAssetsPriceSourceType)) priceSource?: TokenAssetsPriceSourceType, ): Promise { @@ -102,7 +102,7 @@ export class TokenController { @Query('type', new ParseEnumPipe(TokenType)) type?: TokenType, @Query('identifier', ParseTokenPipe) identifier?: string, @Query('identifiers', ParseArrayPipe) identifiers?: string[], - @Query('includeMetaESDT', new ParseBoolPipe) includeMetaESDT?: boolean, + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], @Query('priceSource', new ParseEnumPipe(TokenAssetsPriceSourceType)) priceSource?: TokenAssetsPriceSourceType, ): Promise { @@ -116,7 +116,7 @@ export class TokenController { @ApiNotFoundResponse({ description: 'Token not found' }) async getToken( @Param('identifier', ParseTokenPipe) identifier: string, - @Query('denominated', new ParseBoolPipe) denominated?: boolean, + @Query('denominated', ParseBoolPipe) denominated?: boolean, ): Promise { const supplyOptions = { denominated }; const token = await this.tokenService.getToken(identifier, supplyOptions); @@ -134,7 +134,7 @@ export class TokenController { @ApiNotFoundResponse({ description: 'Token not found' }) async getTokenSupply( @Param('identifier', ParseTokenPipe) identifier: string, - @Query('denominated', new ParseBoolPipe) denominated?: boolean, + @Query('denominated', ParseBoolPipe) denominated?: boolean, ): Promise { const isToken = await this.tokenService.isToken(identifier); if (!isToken) { @@ -219,6 +219,7 @@ export class TokenController { @ApiQuery({ name: 'withUsername', description: 'Integrates username in assets for all addresses present in the transactions', required: false, type: Boolean }) @ApiQuery({ name: 'withBlockInfo', description: 'Returns sender / receiver block details', required: false, type: Boolean }) @ApiQuery({ name: 'withActionTransferValue', description: 'Returns value in USD and EGLD for transferred tokens within the action attribute', required: false }) + @ApiQuery({ name: 'withRelayedScresults', description: 'If set to true, will include smart contract results that resemble relayed transactions', required: false, type: Boolean }) async getTokenTransactions( @Param('identifier', ParseTokenPipe) identifier: string, @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @@ -236,13 +237,14 @@ export class TokenController { @Query('round', ParseIntPipe) round?: number, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, @Query('fields', ParseArrayPipe) fields?: string[], - @Query('withScResults', new ParseBoolPipe) withScResults?: boolean, - @Query('withOperations', new ParseBoolPipe) withOperations?: boolean, - @Query('withLogs', new ParseBoolPipe) withLogs?: boolean, - @Query('withScamInfo', new ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', new ParseBoolPipe) withUsername?: boolean, - @Query('withBlockInfo', new ParseBoolPipe) withBlockInfo?: boolean, + @Query('withScResults', ParseBoolPipe) withScResults?: boolean, + @Query('withOperations', ParseBoolPipe) withOperations?: boolean, + @Query('withLogs', ParseBoolPipe) withLogs?: boolean, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, + @Query('withBlockInfo', ParseBoolPipe) withBlockInfo?: boolean, @Query('withActionTransferValue', ParseBoolPipe) withActionTransferValue?: boolean, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, ) { const options = TransactionQueryOptions.applyDefaultOptions(size, { withScResults, withOperations, withLogs, withScamInfo, withUsername, withBlockInfo, withActionTransferValue }); @@ -251,7 +253,7 @@ export class TokenController { throw new NotFoundException('Token not found'); } - return await this.transactionService.getTransactions(new TransactionFilter({ + const transactionFilter = new TransactionFilter({ sender, receivers: receiver, token: identifier, @@ -265,7 +267,12 @@ export class TokenController { after, order, round, - }), + withRelayedScresults, + }); + TransactionFilter.validate(transactionFilter, size); + + return await this.transactionService.getTransactions( + transactionFilter, new QueryPagination({ from, size }), options, undefined, @@ -287,6 +294,7 @@ export class TokenController { @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) @ApiQuery({ name: 'round', description: 'Filter by round number', required: false }) + @ApiQuery({ name: 'withRelayedScresults', description: 'If set to true, will include smart contract results that resemble relayed transactions', required: false, type: Boolean }) async getTokenTransactionsCount( @Param('identifier', ParseTokenPipe) identifier: string, @Query('sender', ParseAddressPipe) sender?: string, @@ -299,6 +307,7 @@ export class TokenController { @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, @Query('round', ParseIntPipe) round?: number, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, ) { const isToken = await this.tokenService.isToken(identifier); if (!isToken) { @@ -317,6 +326,7 @@ export class TokenController { before, after, round, + withRelayedScresults, })); } @@ -341,7 +351,7 @@ export class TokenController { } @Get("/tokens/:identifier/roles/:address") - @ApiOperation({ summary: 'Token address roles', description: 'Returns roles detalils for a specific address of a given token', deprecated: true }) + @ApiOperation({ summary: 'Token address roles', description: 'Returns roles details for a specific address of a given token', deprecated: true }) @ApiOkResponse({ type: TokenRoles }) @ApiNotFoundResponse({ description: 'Token not found' }) async getTokenRolesForAddress( @@ -401,9 +411,9 @@ export class TokenController { @Query('round', ParseIntPipe) round?: number, @Query('fields', ParseArrayPipe) fields?: string[], @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @Query('withScamInfo', new ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', new ParseBoolPipe) withUsername?: boolean, - @Query('withBlockInfo', new ParseBoolPipe) withBlockInfo?: boolean, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, + @Query('withBlockInfo', ParseBoolPipe) withBlockInfo?: boolean, @Query('withActionTransferValue', ParseBoolPipe) withActionTransferValue?: boolean, ): Promise { const isToken = await this.tokenService.isToken(identifier); @@ -435,7 +445,7 @@ export class TokenController { } @Get("/tokens/:identifier/transfers/count") - @ApiOperation({ summary: 'Account transfer count', description: 'Return total count of tranfers triggerred by a user account (type = Transaction), as well as transfers triggerred by smart contracts (type = SmartContractResult)' }) + @ApiOperation({ summary: 'Account transfer count', description: 'Return total count of transfers triggerred by a user account (type = Transaction), as well as transfers triggerred by smart contracts (type = SmartContractResult)' }) @ApiOkResponse({ type: Number }) @ApiQuery({ name: 'sender', description: 'Address of the transfer sender', required: false }) @ApiQuery({ name: 'receiver', description: 'Search by multiple receiver addresses, comma-separated', required: false }) diff --git a/src/endpoints/tokens/token.service.ts b/src/endpoints/tokens/token.service.ts index 0f7782fc8..3ceb5183a 100644 --- a/src/endpoints/tokens/token.service.ts +++ b/src/endpoints/tokens/token.service.ts @@ -48,6 +48,7 @@ import { NftSubType } from "../nfts/entities/nft.sub.type"; export class TokenService { private readonly logger = new OriginLogger(TokenService.name); private readonly nftSubTypes = [NftSubType.DynamicNonFungibleESDT, NftSubType.DynamicMetaESDT, NftSubType.NonFungibleESDTv2, NftSubType.DynamicSemiFungibleESDT]; + private readonly egldIdentifierInMultiTransfer = 'EGLD-000000'; constructor( private readonly esdtService: EsdtService, @@ -126,7 +127,9 @@ export class TokenService { this.applyTickerFromAssets(token); } - return tokens.map(item => ApiUtils.mergeObjects(new TokenDetailed(), item)); + return tokens + .map(item => ApiUtils.mergeObjects(new TokenDetailed(), item)) + .filter(t => t.identifier !== this.egldIdentifierInMultiTransfer); } applyTickerFromAssets(token: Token) { @@ -838,6 +841,20 @@ export class TokenService { token => token.transactions ?? 0, ); + const egldToken = new TokenDetailed({ + identifier: this.egldIdentifierInMultiTransfer, + name: 'EGLD', + type: TokenType.FungibleESDT, + assets: await this.assetsService.getTokenAssets(this.egldIdentifierInMultiTransfer), + decimals: 18, + isLowLiquidity: false, + price: await this.dataApiService.getEgldPrice(), + supply: '0', + circulatingSupply: '0', + marketCap: 0, + }); + tokens = [...tokens, egldToken]; + return tokens; } diff --git a/src/endpoints/transactions.batch/transactions.batch.service.ts b/src/endpoints/transactions.batch/transactions.batch.service.ts index 2771e72e0..78bea9c89 100644 --- a/src/endpoints/transactions.batch/transactions.batch.service.ts +++ b/src/endpoints/transactions.batch/transactions.batch.service.ts @@ -146,7 +146,11 @@ export class TransactionsBatchService { return transactionBatchItem; } - transaction.hash = result.txHash; + if (result.txHash) { + transaction.hash = result.txHash; + } else { + transactionBatchItem.status = BatchTransactionStatus.invalid; + } return transactionBatchItem; } diff --git a/src/endpoints/transactions/entities/transaction.filter.ts b/src/endpoints/transactions/entities/transaction.filter.ts index b39779eb0..5462a0822 100644 --- a/src/endpoints/transactions/entities/transaction.filter.ts +++ b/src/endpoints/transactions/entities/transaction.filter.ts @@ -2,12 +2,9 @@ import { QueryConditionOptions } from "@multiversx/sdk-nestjs-elastic"; import { SortOrder } from "src/common/entities/sort.order"; import { TransactionStatus } from "./transaction.status"; import { TransactionType } from "./transaction.type"; +import { BadRequestException } from "@nestjs/common"; export class TransactionFilter { - constructor(init?: Partial) { - Object.assign(this, init); - } - address?: string; sender?: string; senders?: string[] = []; @@ -29,4 +26,16 @@ export class TransactionFilter { isRelayed?: boolean; relayer?: string; round?: number; + withRefunds?: boolean; + withRelayedScresults?: boolean; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + static validate(filter: TransactionFilter, size: number) { + if (filter.withRelayedScresults && size > 50) { + throw new BadRequestException('Size must be less than or equal to 50 when withRelayedScresults is set'); + } + } } diff --git a/src/endpoints/transactions/transaction.controller.ts b/src/endpoints/transactions/transaction.controller.ts index 4e2c415cf..90858a346 100644 --- a/src/endpoints/transactions/transaction.controller.ts +++ b/src/endpoints/transactions/transaction.controller.ts @@ -49,6 +49,7 @@ export class TransactionController { @ApiQuery({ name: 'withBlockInfo', description: 'Returns sender / receiver block details', required: false, type: Boolean }) @ApiQuery({ name: 'isRelayed', description: 'Returns relayed transactions details', required: false, type: Boolean }) @ApiQuery({ name: 'withActionTransferValue', description: 'Returns value in USD and EGLD for transferred tokens within the action attribute', required: false }) + @ApiQuery({ name: 'withRelayedScresults', description: 'If set to true, will include smart contract results that resemble relayed transactions', required: false, type: Boolean }) getTransactions( @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, @@ -67,18 +68,19 @@ export class TransactionController { @Query('round', ParseIntPipe) round?: number, @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, @Query('fields', ParseArrayPipe) fields?: string[], - @Query('withScResults', new ParseBoolPipe) withScResults?: boolean, - @Query('withOperations', new ParseBoolPipe) withOperations?: boolean, - @Query('withLogs', new ParseBoolPipe) withLogs?: boolean, - @Query('withScamInfo', new ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', new ParseBoolPipe) withUsername?: boolean, - @Query('withBlockInfo', new ParseBoolPipe) withBlockInfo?: boolean, - @Query('isRelayed', new ParseBoolPipe) isRelayed?: boolean, + @Query('withScResults', ParseBoolPipe) withScResults?: boolean, + @Query('withOperations', ParseBoolPipe) withOperations?: boolean, + @Query('withLogs', ParseBoolPipe) withLogs?: boolean, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, + @Query('withBlockInfo', ParseBoolPipe) withBlockInfo?: boolean, + @Query('isRelayed', ParseBoolPipe) isRelayed?: boolean, @Query('withActionTransferValue', ParseBoolPipe) withActionTransferValue?: boolean, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, ) { const options = TransactionQueryOptions.applyDefaultOptions(size, { withScResults, withOperations, withLogs, withScamInfo, withUsername, withBlockInfo, withActionTransferValue }); - return this.transactionService.getTransactions(new TransactionFilter({ + const transactionFilter = new TransactionFilter({ sender, receivers: receiver, token, @@ -94,7 +96,10 @@ export class TransactionController { order, isRelayed, round, - }), + withRelayedScresults: withRelayedScresults, + }); + TransactionFilter.validate(transactionFilter, size); + return this.transactionService.getTransactions(transactionFilter, new QueryPagination({ from, size }), options, undefined, @@ -119,6 +124,7 @@ export class TransactionController { @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) @ApiQuery({ name: 'round', description: 'Round number', required: false }) @ApiQuery({ name: 'isRelayed', description: 'Returns relayed transactions details', required: false, type: Boolean }) + @ApiQuery({ name: 'withRelayedScresults', description: 'If set to true, will include smart contract results that resemble relayed transactions', required: false, type: Boolean }) getTransactionCount( @Query('sender', ParseAddressAndMetachainPipe) sender?: string, @Query('receiver', ParseAddressArrayPipe) receiver?: string[], @@ -133,7 +139,8 @@ export class TransactionController { @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, @Query('round', ParseIntPipe) round?: number, - @Query('isRelayed', new ParseBoolPipe) isRelayed?: boolean, + @Query('isRelayed', ParseBoolPipe) isRelayed?: boolean, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, ): Promise { return this.transactionService.getTransactionCount(new TransactionFilter({ sender, @@ -150,6 +157,7 @@ export class TransactionController { condition, isRelayed, round, + withRelayedScresults: withRelayedScresults, })); } @@ -168,8 +176,9 @@ export class TransactionController { @Query('condition') condition?: QueryConditionOptions, @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, - @Query('round', new ParseIntPipe) round?: number, - @Query('isRelayed', new ParseBoolPipe) isRelayed?: boolean, + @Query('round', ParseIntPipe) round?: number, + @Query('isRelayed', ParseBoolPipe) isRelayed?: boolean, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, ): Promise { return this.transactionService.getTransactionCount(new TransactionFilter({ sender, @@ -186,6 +195,7 @@ export class TransactionController { condition, isRelayed, round, + withRelayedScresults: withRelayedScresults, })); } @@ -249,4 +259,3 @@ export class TransactionController { return await this.transactionService.decodeTransaction(transaction); } } - diff --git a/src/endpoints/transfers/transfer.controller.ts b/src/endpoints/transfers/transfer.controller.ts index 117be703e..1e723c463 100644 --- a/src/endpoints/transfers/transfer.controller.ts +++ b/src/endpoints/transfers/transfer.controller.ts @@ -46,6 +46,7 @@ export class TransferController { @ApiQuery({ name: 'withLogs', description: 'Return logs for transfers. When "withLogs" parameter is applied, complexity estimation is 200', required: false }) @ApiQuery({ name: 'withOperations', description: 'Return operations for transfers. When "withOperations" parameter is applied, complexity estimation is 200', required: false }) @ApiQuery({ name: 'withActionTransferValue', description: 'Returns value in USD and EGLD for transferred tokens within the action attribute', required: false }) + @ApiQuery({ name: 'withRefunds', description: 'Include refund transactions', required: false }) async getAccountTransfers( @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, @@ -64,13 +65,14 @@ export class TransferController { @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, @Query('fields', ParseArrayPipe) fields?: string[], @Query('relayer', ParseAddressPipe) relayer?: string, - @Query('isRelayed', new ParseBoolPipe) isRelayed?: boolean, - @Query('withScamInfo', new ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', new ParseBoolPipe) withUsername?: boolean, - @Query('withBlockInfo', new ParseBoolPipe) withBlockInfo?: boolean, - @Query('withLogs', new ParseBoolPipe) withLogs?: boolean, - @Query('withOperations', new ParseBoolPipe) withOperations?: boolean, + @Query('isRelayed', ParseBoolPipe) isRelayed?: boolean, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, + @Query('withBlockInfo', ParseBoolPipe) withBlockInfo?: boolean, + @Query('withLogs', ParseBoolPipe) withLogs?: boolean, + @Query('withOperations', ParseBoolPipe) withOperations?: boolean, @Query('withActionTransferValue', ParseBoolPipe) withActionTransferValue?: boolean, + @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, ): Promise { const options = TransactionQueryOptions.applyDefaultOptions( size, new TransactionQueryOptions({ withScamInfo, withUsername, withBlockInfo, withLogs, withOperations, withActionTransferValue }), @@ -92,6 +94,7 @@ export class TransferController { relayer, isRelayed, round, + withRefunds, }), new QueryPagination({ from, size }), options, @@ -115,6 +118,7 @@ export class TransferController { @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) @ApiQuery({ name: 'round', description: 'Round number', required: false }) @ApiQuery({ name: 'isRelayed', description: 'Returns relayed transactions details', required: false, type: Boolean }) + @ApiQuery({ name: 'withRefunds', description: 'Include refund transactions', required: false }) async getAccountTransfersCount( @Query('sender', ParseAddressArrayPipe) sender?: string[], @Query('receiver', ParseAddressArrayPipe) receiver?: string[], @@ -128,7 +132,8 @@ export class TransferController { @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, @Query('round', ParseIntPipe) round?: number, - @Query('isRelayed', new ParseBoolPipe) isRelayed?: boolean, + @Query('isRelayed', ParseBoolPipe) isRelayed?: boolean, + @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, ): Promise { return await this.transferService.getTransfersCount(new TransactionFilter({ senders: sender, @@ -144,6 +149,7 @@ export class TransferController { after, isRelayed, round, + withRefunds, })); } @@ -162,6 +168,7 @@ export class TransferController { @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, @Query('round', ParseIntPipe) round?: number, + @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, ): Promise { return await this.transferService.getTransfersCount(new TransactionFilter({ senders: sender, @@ -176,6 +183,7 @@ export class TransferController { before, after, round, + withRefunds, })); } } diff --git a/src/endpoints/usernames/username.controller.ts b/src/endpoints/usernames/username.controller.ts index b0aeadb05..3a1f242e4 100644 --- a/src/endpoints/usernames/username.controller.ts +++ b/src/endpoints/usernames/username.controller.ts @@ -22,7 +22,7 @@ export class UsernameController { async getUsernameDetails( @Res() res: any, @Param('username') username: string, - @Query('withGuardianInfo', new ParseBoolPipe) withGuardianInfo: boolean + @Query('withGuardianInfo', ParseBoolPipe) withGuardianInfo: boolean ): Promise { const address = await this.usernameService.getAddressForUsername(username); if (!address) { diff --git a/src/test/unit/controllers/accounts.controller.spec.ts b/src/test/unit/controllers/accounts.controller.spec.ts index 0ba47fec3..ab99de595 100644 --- a/src/test/unit/controllers/accounts.controller.spec.ts +++ b/src/test/unit/controllers/accounts.controller.spec.ts @@ -30,6 +30,7 @@ import { ConfigModule } from "@nestjs/config"; import { AccountDeferred } from "src/endpoints/accounts/entities/account.deferred"; import request = require('supertest'); import { mockAccountService, mockTokenService, mockNftService, mockDelegationLegacyService, mockWaitingListService, mockStakeService, mockTransactionService, mockSmartContractResultService, mockCollectionService, mockTransferService, mockApiConfigService, mockDelegationService } from "./services.mock/account.services.mock"; +import { AccountFetchOptions } from "src/endpoints/accounts/entities/account.fetch.options"; describe('AccountController', () => { let app: INestApplication; @@ -228,6 +229,16 @@ describe('AccountController', () => { .expect(200) .expect(response => { expect(response.body).toEqual(mockAccount); + expect(accountServiceMocks.getAccount).toHaveBeenCalledWith( + mockAccount.address, + new AccountFetchOptions({ + withGuardianInfo: undefined, + withTxCount: undefined, + withScrCount: undefined, + withTimestamp: undefined, + withAssets: undefined, + }) + ); }); }); @@ -240,28 +251,153 @@ describe('AccountController', () => { }); }); - it('should return account details including guardian info when withGuardianInfo is true', async () => { + it('should return account details with withTxCount parameter', async () => { + const address = "erd1rf4hv70arudgzus0ymnnsnc4pml0jkywg2xjvzslg0mz4nn2tg7q7k0t6p"; + const mockAccountWithTxCount = { + ...mockAccount, + txCount: 100, + }; + + accountServiceMocks.getAccount.mockResolvedValue(mockAccountWithTxCount); + + await request(app.getHttpServer()) + .get(`/accounts/${address}?withTxCount=true`) + .expect(200) + .expect(response => { + expect(response.body).toEqual(mockAccountWithTxCount); + expect(accountServiceMocks.getAccount).toHaveBeenCalledWith( + address, + new AccountFetchOptions({ + withGuardianInfo: undefined, + withTxCount: true, + withScrCount: undefined, + withTimestamp: undefined, + withAssets: undefined, + }) + ); + }); + }); + + it('should return account details with withScrCount parameter', async () => { + const address = "erd1rf4hv70arudgzus0ymnnsnc4pml0jkywg2xjvzslg0mz4nn2tg7q7k0t6p"; + const mockAccountWithScrCount = { + ...mockAccount, + scrCount: 50, + }; + + accountServiceMocks.getAccount.mockResolvedValue(mockAccountWithScrCount); + + await request(app.getHttpServer()) + .get(`/accounts/${address}?withScrCount=true`) + .expect(200) + .expect(response => { + expect(response.body).toEqual(mockAccountWithScrCount); + expect(accountServiceMocks.getAccount).toHaveBeenCalledWith( + address, + new AccountFetchOptions({ + withGuardianInfo: undefined, + withTxCount: undefined, + withScrCount: true, + withTimestamp: undefined, + withAssets: undefined, + }) + ); + }); + }); + + it('should return account details with withTimestamp parameter', async () => { + const address = "erd1rf4hv70arudgzus0ymnnsnc4pml0jkywg2xjvzslg0mz4nn2tg7q7k0t6p"; + const mockAccountWithTimestamp = { + ...mockAccount, + timestamp: 1708946805, + }; + + accountServiceMocks.getAccount.mockResolvedValue(mockAccountWithTimestamp); + + await request(app.getHttpServer()) + .get(`/accounts/${address}?withTimestamp=true`) + .expect(200) + .expect(response => { + expect(response.body).toEqual(mockAccountWithTimestamp); + expect(accountServiceMocks.getAccount).toHaveBeenCalledWith( + address, + new AccountFetchOptions({ + withGuardianInfo: undefined, + withTxCount: undefined, + withScrCount: undefined, + withTimestamp: true, + withAssets: undefined, + }) + ); + }); + }); + + it('should return account details with withAssets parameter', async () => { + const address = "erd1rf4hv70arudgzus0ymnnsnc4pml0jkywg2xjvzslg0mz4nn2tg7q7k0t6p"; + const mockAccountWithAssets = { + ...mockAccount, + assets: { + name: "Test Asset", + description: "Test Description", + }, + }; + + accountServiceMocks.getAccount.mockResolvedValue(mockAccountWithAssets); + + await request(app.getHttpServer()) + .get(`/accounts/${address}?withAssets=true`) + .expect(200) + .expect(response => { + expect(response.body).toEqual(mockAccountWithAssets); + expect(accountServiceMocks.getAccount).toHaveBeenCalledWith( + address, + new AccountFetchOptions({ + withGuardianInfo: undefined, + withTxCount: undefined, + withScrCount: undefined, + withTimestamp: undefined, + withAssets: true, + }) + ); + }); + }); + + it('should return account details with all optional parameters set to true', async () => { const mockAddressList = createMockAccountsList(1); const accountDetails = mockAddressList[0]; const address = "erd1rf4hv70arudgzus0ymnnsnc4pml0jkywg2xjvzslg0mz4nn2tg7q7k0t6p"; - const mockAccountWithGuardianInfo = { + const mockAccountWithAllParams = { ...accountDetails, isGuarded: true, activeGuardianActivationEpoch: 496, activeGuardianAddress: "erd1x5d4p63uwcns8cvyrl4g3qgvwwa2nkt5jdp0vwetc7csqzpjzz0qec58k0", activeGuardianServiceUid: "MultiversXTCSService", + txCount: 100, + scrCount: 50, + timestamp: 1708946805, + assets: { + name: "Test Asset", + description: "Test Description", + }, }; - accountServiceMocks.getAccount.mockResolvedValue(mockAccountWithGuardianInfo); + accountServiceMocks.getAccount.mockResolvedValue(mockAccountWithAllParams); await request(app.getHttpServer()) - .get(`/accounts/${address}?withGuardianInfo=true`) + .get(`/accounts/${address}?withGuardianInfo=true&withTxCount=true&withScrCount=true&withTimestamp=true&withAssets=true`) .expect(200) .expect(response => { - expect(response.body).toEqual(mockAccountWithGuardianInfo); - expect(response.body.isGuarded).toStrictEqual(true); + expect(response.body).toEqual(mockAccountWithAllParams); expect(accountServiceMocks.getAccount).toHaveBeenCalledWith( - expect.any(String), undefined, true); + address, + new AccountFetchOptions({ + withGuardianInfo: true, + withTxCount: true, + withScrCount: true, + withTimestamp: true, + withAssets: true, + }) + ); }); }); @@ -281,6 +417,91 @@ describe('AccountController', () => { .expect(response => { expect(response.body).toEqual(mockAccountFilteredFields); expect(Object.keys(response.body).sort()).toEqual(fields.sort()); + expect(accountServiceMocks.getAccount).toHaveBeenCalledWith( + address, + new AccountFetchOptions({ + withGuardianInfo: undefined, + withTxCount: undefined, + withScrCount: undefined, + withTimestamp: undefined, + withAssets: undefined, + }) + ); + }); + }); + + it('should return account details with all filters set to false', async () => { + const address = "erd1rf4hv70arudgzus0ymnnsnc4pml0jkywg2xjvzslg0mz4nn2tg7q7k0t6p"; + const mockAccountWithAllParamsFalse = { + ...mockAccount, + }; + + accountServiceMocks.getAccount.mockResolvedValue(mockAccountWithAllParamsFalse); + + await request(app.getHttpServer()) + .get(`/accounts/${address}?withGuardianInfo=false&withTxCount=false&withScrCount=false&withTimestamp=false&withAssets=false`) + .expect(200) + .expect(response => { + expect(response.body).toEqual(mockAccountWithAllParamsFalse); + expect(accountServiceMocks.getAccount).toHaveBeenCalledWith( + address, + new AccountFetchOptions({ + withGuardianInfo: false, + withTxCount: false, + withScrCount: false, + withTimestamp: false, + withAssets: false, + }) + ); + }); + }); + + it('should return account details with all filters set to true', async () => { + const address = "erd1rf4hv70arudgzus0ymnnsnc4pml0jkywg2xjvzslg0mz4nn2tg7q7k0t6p"; + const mockAccountWithAllParamsTrue = { + ...mockAccount, + guardianInfo: { + guarded: true, + activeGuardian: "erd1guardianaddress", + }, + txCount: 100, + scrCount: 50, + timestamp: 1708946805, + assets: { + name: "Test Asset", + description: "Test Description", + }, + }; + + accountServiceMocks.getAccount.mockResolvedValue(mockAccountWithAllParamsTrue); + + await request(app.getHttpServer()) + .get(`/accounts/${address}?withGuardianInfo=true&withTxCount=true&withScrCount=true&withTimestamp=true&withAssets=true`) + .expect(200) + .expect(response => { + expect(response.body).toEqual(mockAccountWithAllParamsTrue); + expect(accountServiceMocks.getAccount).toHaveBeenCalledWith( + address, + new AccountFetchOptions({ + withGuardianInfo: true, + withTxCount: true, + withScrCount: true, + withTimestamp: true, + withAssets: true, + }) + ); + }); + }); + + it('should throw 404 Not Found when account does not exist', async () => { + const address = "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu"; + accountServiceMocks.getAccount.mockResolvedValue(null); + + await request(app.getHttpServer()) + .get(`${path}/${address}`) + .expect(404) + .expect(response => { + expect(response.body.message).toEqual('Account not found'); }); }); }); diff --git a/src/test/unit/controllers/web.socket.publiser.controller.spec.ts b/src/test/unit/controllers/web.socket.publiser.controller.spec.ts index bec012c47..91fc3bb9c 100644 --- a/src/test/unit/controllers/web.socket.publiser.controller.spec.ts +++ b/src/test/unit/controllers/web.socket.publiser.controller.spec.ts @@ -1,11 +1,14 @@ import { ShardTransaction } from "@elrondnetwork/transaction-processor"; import { TestingModule, Test } from "@nestjs/testing"; +import { EventEmitter2 } from "@nestjs/event-emitter"; import { WebSocketPublisherController } from "src/common/websockets/web-socket-publisher-controller"; import { WebSocketPublisherService } from "src/common/websockets/web-socket-publisher-service"; +import { MetricsEvents } from "src/utils/metrics-events.constants"; describe('WebSocketPublisherController', () => { let controller: WebSocketPublisherController; let webSocketPublisherService: WebSocketPublisherService; + let eventEmitter: EventEmitter2; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -19,23 +22,32 @@ describe('WebSocketPublisherController', () => { onBatchUpdated: jest.fn(), }, }, + { + provide: EventEmitter2, + useValue: { + emit: jest.fn(), + }, + }, ], }).compile(); controller = module.get(WebSocketPublisherController); webSocketPublisherService = module.get(WebSocketPublisherService); + eventEmitter = module.get(EventEmitter2); }); it('should handle transactionsCompleted event', async () => { const mockTransactions = [{}, {}] as ShardTransaction[]; await controller.transactionsCompleted(mockTransactions); expect(webSocketPublisherService.onTransactionCompleted).toHaveBeenCalledTimes(mockTransactions.length); + expect(eventEmitter.emit).toHaveBeenCalledWith(MetricsEvents.SetTransactionsCompleted, { transactions: mockTransactions }); }); it('should handle transactionsPendingResults event', async () => { const mockTransactions = [{}, {}] as ShardTransaction[]; await controller.transactionsPendingResults(mockTransactions); expect(webSocketPublisherService.onTransactionPendingResults).toHaveBeenCalledTimes(mockTransactions.length); + expect(eventEmitter.emit).toHaveBeenCalledWith(MetricsEvents.SetTransactionsPendingResults, { transactions: mockTransactions }); }); it('should handle onBatchUpdated event', () => { @@ -43,5 +55,6 @@ describe('WebSocketPublisherController', () => { controller.onBatchUpdated(mockPayload); expect(webSocketPublisherService.onBatchUpdated).toHaveBeenCalledWith(mockPayload.address, mockPayload.batchId, mockPayload.txHashes); expect(webSocketPublisherService.onBatchUpdated).toHaveBeenCalledTimes(1); + expect(eventEmitter.emit).toHaveBeenCalledWith(MetricsEvents.SetBatchUpdated); }); }); diff --git a/src/test/unit/services/pool.spec.ts b/src/test/unit/services/pool.spec.ts index a8505f257..a276ca2e3 100644 --- a/src/test/unit/services/pool.spec.ts +++ b/src/test/unit/services/pool.spec.ts @@ -7,6 +7,7 @@ import { PoolFilter } from "src/endpoints/pool/entities/pool.filter"; import { PoolService } from "src/endpoints/pool/pool.service"; import { TransactionType } from "src/endpoints/transactions/entities/transaction.type"; import { ProtocolService } from "../../../common/protocol/protocol.service"; +import { TransactionActionService } from "../../../endpoints/transactions/transaction-action/transaction.action.service"; describe('PoolService', () => { let service: PoolService; @@ -41,6 +42,12 @@ describe('PoolService', () => { isTransactionPoolEnabled: jest.fn().mockResolvedValue(true), }, }, + { + provide: TransactionActionService, + useValue: { + getTransactionMetadata: jest.fn(), + }, + }, ], }).compile(); diff --git a/src/test/unit/services/tokens.spec.ts b/src/test/unit/services/tokens.spec.ts index 222e5ae15..f025eaddb 100644 --- a/src/test/unit/services/tokens.spec.ts +++ b/src/test/unit/services/tokens.spec.ts @@ -134,6 +134,7 @@ describe('Token Service', () => { provide: DataApiService, useValue: { getEsdtTokenPrice: jest.fn(), + getEgldPrice: jest.fn(), }, }, { @@ -690,6 +691,7 @@ describe('Token Service', () => { jest.spyOn(tokenService as any, 'applyMexPairTradesCount').mockImplementation(() => Promise.resolve()); jest.spyOn(cacheService as any, 'batchApplyAll').mockImplementation(() => Promise.resolve()); jest.spyOn(dataApiService, 'getEsdtTokenPrice').mockResolvedValue(100); + jest.spyOn(dataApiService, 'getEgldPrice').mockResolvedValue(100); jest.spyOn(tokenService as any, 'fetchTokenDataFromUrl').mockResolvedValue(100); jest.spyOn(esdtService, 'getTokenSupply').mockResolvedValue(mockTokenSupply as EsdtSupply); @@ -710,7 +712,7 @@ describe('Token Service', () => { expect(assetsService.getTokenAssets).toHaveBeenCalledWith(mockToken.identifier); mockToken.name = mockTokenAssets.name; }); - expect(assetsService.getTokenAssets).toHaveBeenCalledTimes(mockTokens.length); + expect(assetsService.getTokenAssets).toHaveBeenCalledTimes(mockTokens.length + 1); // add 1 for EGLD-000000 expect((collectionService as any).getNftCollections).toHaveBeenCalledWith(expect.anything(), { type: [TokenType.MetaESDT] }); @@ -763,6 +765,24 @@ describe('Token Service', () => { token => token.isLowLiquidity ? 0 : (token.marketCap ?? 0), token => token.transactions ?? 0, ); + + mockTokens.push(new TokenDetailed({ + identifier: 'EGLD-000000', + name: 'EGLD', + canPause: false, + canUpgrade: false, + canWipe: false, + price: 100, + decimals: 18, + isLowLiquidity: false, + marketCap: 0, + circulatingSupply: '0', + supply: '0', + assets: { + name: 'mockName', + } as TokenAssets, + })); + expect(result).toEqual(mockTokens); }); }); diff --git a/src/utils/metrics-events.constants.ts b/src/utils/metrics-events.constants.ts index 41337fd23..a2e480638 100644 --- a/src/utils/metrics-events.constants.ts +++ b/src/utils/metrics-events.constants.ts @@ -7,4 +7,7 @@ export enum MetricsEvents { SetLastProcessedNonce = "setLastProcessedNonce", SetLastProcessedBatchProcessorNonce = "setLastProcessedBatchProcessorNonce", SetLastProcessedTransactionCompletedProcessorNonce = "setLastProcessedTransactionCompletedProcessorNonce", + SetTransactionsCompleted = "setTransactionsCompleted", + SetTransactionsPendingResults = "setTransactionsPendingResults", + SetBatchUpdated = "setBatchUpdated", }