From b7c4c0b03300ca2fae17e09b350bb8652ce62f07 Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Wed, 29 Jan 2025 12:14:42 +0200 Subject: [PATCH 01/11] update applications endpoint --- .../applications/application.controller.ts | 8 +- .../applications/application.module.ts | 4 + .../applications/application.service.ts | 41 ++++++++- .../entities/application.filter.ts | 4 +- .../applications/entities/application.ts | 6 ++ src/test/unit/services/applications.spec.ts | 83 ++++++++++++++++++- 6 files changed, 140 insertions(+), 6 deletions(-) diff --git a/src/endpoints/applications/application.controller.ts b/src/endpoints/applications/application.controller.ts index c76a0df14..8f93f549a 100644 --- a/src/endpoints/applications/application.controller.ts +++ b/src/endpoints/applications/application.controller.ts @@ -3,7 +3,7 @@ import { ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger" import { ApplicationService } from "./application.service"; import { QueryPagination } from "src/common/entities/query.pagination"; import { ApplicationFilter } from "./entities/application.filter"; -import { ParseIntPipe } from "@multiversx/sdk-nestjs-common"; +import { ParseIntPipe, ParseBoolPipe } from "@multiversx/sdk-nestjs-common"; import { Application } from "./entities/application"; @Controller() @@ -20,15 +20,17 @@ export class ApplicationController { @ApiQuery({ name: 'size', description: 'Number of items to retrieve', required: false }) @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiQuery({ name: 'withTxCount', description: 'Include transaction count', required: false, type: Boolean }) async getApplications( @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @Query("size", new DefaultValuePipe(25), ParseIntPipe) size: number, @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, - ): Promise { + @Query('withTxCount', new ParseBoolPipe()) withTxCount?: boolean, + ): Promise { return await this.applicationService.getApplications( new QueryPagination({ size, from }), - new ApplicationFilter({ before, after }) + new ApplicationFilter({ before, after, withTxCount }) ); } diff --git a/src/endpoints/applications/application.module.ts b/src/endpoints/applications/application.module.ts index f9e6dccd2..e8e912c30 100644 --- a/src/endpoints/applications/application.module.ts +++ b/src/endpoints/applications/application.module.ts @@ -2,14 +2,18 @@ import { Module } from "@nestjs/common"; import { ElasticIndexerModule } from "src/common/indexer/elastic/elastic.indexer.module"; import { ApplicationService } from "./application.service"; import { AssetsService } from '../../common/assets/assets.service'; +import { GatewayService } from "src/common/gateway/gateway.service"; +import { TransferModule } from "../transfers/transfer.module"; @Module({ imports: [ ElasticIndexerModule, + TransferModule, ], providers: [ ApplicationService, AssetsService, + GatewayService, ], exports: [ ApplicationService, diff --git a/src/endpoints/applications/application.service.ts b/src/endpoints/applications/application.service.ts index c1d163eeb..d0b063de4 100644 --- a/src/endpoints/applications/application.service.ts +++ b/src/endpoints/applications/application.service.ts @@ -4,12 +4,21 @@ import { Application } from './entities/application'; import { QueryPagination } from 'src/common/entities/query.pagination'; import { ApplicationFilter } from './entities/application.filter'; import { AssetsService } from '../../common/assets/assets.service'; +import { GatewayService } from 'src/common/gateway/gateway.service'; +import { TransferService } from '../transfers/transfer.service'; +import { TransactionFilter } from '../transactions/entities/transaction.filter'; +import { TransactionType } from '../transactions/entities/transaction.type'; +import { Logger } from '@nestjs/common'; @Injectable() export class ApplicationService { + private readonly logger = new Logger(ApplicationService.name); + constructor( private readonly elasticIndexerService: ElasticIndexerService, private readonly assetsService: AssetsService, + private readonly gatewayService: GatewayService, + private readonly transferService: TransferService, ) { } async getApplications(pagination: QueryPagination, filter: ApplicationFilter): Promise { @@ -20,17 +29,47 @@ export class ApplicationService { return []; } - return elasticResults.map(item => ({ + const applications = elasticResults.map(item => new Application({ contract: item.address, deployer: item.deployer, owner: item.currentOwner, codeHash: item.initialCodeHash, timestamp: item.timestamp, assets: assets[item.address], + balance: '0', + ...(filter.withTxCount && { txCount: 0 }), })); + + await Promise.all(applications.map(async (application) => { + const promises: Promise[] = []; + + promises.push((async () => { + try { + const { account: { balance } } = await this.gatewayService.getAddressDetails(application.contract); + application.balance = balance; + } catch (error) { + this.logger.error(`Error when getting balance for contract ${application.contract}`, error); + application.balance = '0'; + } + })()); + + if (filter.withTxCount) { + promises.push((async () => { + application.txCount = await this.getApplicationTxCount(application.contract); + })()); + } + + await Promise.all(promises); + })); + + return applications; } async getApplicationsCount(filter: ApplicationFilter): Promise { return await this.elasticIndexerService.getApplicationCount(filter); } + + async getApplicationTxCount(address: string): Promise { + return await this.transferService.getTransfersCount(new TransactionFilter({ address, type: TransactionType.Transaction })); + } } diff --git a/src/endpoints/applications/entities/application.filter.ts b/src/endpoints/applications/entities/application.filter.ts index 69c16c124..a29e21c55 100644 --- a/src/endpoints/applications/entities/application.filter.ts +++ b/src/endpoints/applications/entities/application.filter.ts @@ -5,9 +5,11 @@ export class ApplicationFilter { after?: number; before?: number; + withTxCount?: boolean; isSet(): boolean { return this.after !== undefined || - this.before !== undefined; + this.before !== undefined || + this.withTxCount !== undefined; } } diff --git a/src/endpoints/applications/entities/application.ts b/src/endpoints/applications/entities/application.ts index ea5dc2da5..88ef36da9 100644 --- a/src/endpoints/applications/entities/application.ts +++ b/src/endpoints/applications/entities/application.ts @@ -23,4 +23,10 @@ export class Application { @ApiProperty({ type: AccountAssets, nullable: true, description: 'Contract assets' }) assets: AccountAssets | undefined = undefined; + + @ApiProperty({ type: String }) + balance: string = '0'; + + @ApiProperty({ type: Number, required: false }) + txCount?: number; } diff --git a/src/test/unit/services/applications.spec.ts b/src/test/unit/services/applications.spec.ts index 65de79ac4..d0f5751ff 100644 --- a/src/test/unit/services/applications.spec.ts +++ b/src/test/unit/services/applications.spec.ts @@ -6,11 +6,16 @@ import { ApplicationFilter } from 'src/endpoints/applications/entities/applicati import { AssetsService } from '../../../common/assets/assets.service'; import { AccountAssetsSocial } from '../../../common/assets/entities/account.assets.social'; import { AccountAssets } from '../../../common/assets/entities/account.assets'; +import { Application } from 'src/endpoints/applications/entities/application'; +import { GatewayService } from 'src/common/gateway/gateway.service'; +import { TransferService } from 'src/endpoints/transfers/transfer.service'; describe('ApplicationService', () => { let service: ApplicationService; let indexerService: ElasticIndexerService; let assetsService: AssetsService; + let gatewayService: GatewayService; + let transferService: TransferService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -29,12 +34,26 @@ describe('ApplicationService', () => { getAllAccountAssets: jest.fn(), }, }, + { + provide: GatewayService, + useValue: { + getAddressDetails: jest.fn(), + }, + }, + { + provide: TransferService, + useValue: { + getTransfersCount: jest.fn(), + }, + }, ], }).compile(); service = module.get(ApplicationService); indexerService = module.get(ElasticIndexerService); assetsService = module.get(AssetsService); + gatewayService = module.get(GatewayService); + transferService = module.get(TransferService); }); it('should be defined', () => { @@ -80,6 +99,20 @@ describe('ApplicationService', () => { jest.spyOn(indexerService, 'getApplications').mockResolvedValue(indexResult); jest.spyOn(assetsService, 'getAllAccountAssets').mockResolvedValue(assets); + jest.spyOn(gatewayService, 'getAddressDetails').mockResolvedValue({ + account: { + address: '', + nonce: 0, + balance: '0', + username: '', + code: '', + codeHash: '', + rootHash: '', + codeMetadata: '', + developerReward: '', + ownerAddress: '', + }, + }); const queryPagination = new QueryPagination(); const filter = new ApplicationFilter(); @@ -89,13 +122,14 @@ describe('ApplicationService', () => { expect(indexerService.getApplications).toHaveBeenCalledTimes(1); expect(assetsService.getAllAccountAssets).toHaveBeenCalled(); - const expectedApplications = indexResult.map(item => ({ + const expectedApplications = indexResult.map(item => new Application({ contract: item.address, deployer: item.deployer, owner: item.currentOwner, codeHash: item.initialCodeHash, timestamp: item.timestamp, assets: assets[item.address], + balance: '0', })); expect(result).toEqual(expectedApplications); @@ -115,6 +149,53 @@ describe('ApplicationService', () => { expect(result).toEqual([]); }); + + it('should return an array of applications with tx count', async () => { + const indexResult = [ + { + address: 'erd1qqqqqqqqqqqqqpgq8372f63glekg7zl22tmx7wzp4drql25r6avs70dmp0', + deployer: 'erd1j770k2n46wzfn5g63gjthhqemu9r23n9tp7seu95vpz5gk5s6avsk5aams', + currentOwner: 'erd1j770k2n46wzfn5g63gjthhqemu9r23n9tp7seu95vpz5gk5s6avsk5aams', + initialCodeHash: 'kDh8hR9vyceELMUuy6JdAg0X90+ZaLeyVQS6tPbY82s=', + timestamp: 1724955216, + }, + ]; + + jest.spyOn(indexerService, 'getApplications').mockResolvedValue(indexResult); + jest.spyOn(assetsService, 'getAllAccountAssets').mockResolvedValue({}); + jest.spyOn(gatewayService, 'getAddressDetails').mockResolvedValue({ + account: { + address: '', + nonce: 0, + balance: '0', + username: '', + code: '', + codeHash: '', + rootHash: '', + codeMetadata: '', + developerReward: '', + ownerAddress: '', + }, + }); + jest.spyOn(transferService, 'getTransfersCount').mockResolvedValue(42); + + const queryPagination = new QueryPagination(); + const filter = new ApplicationFilter({ withTxCount: true }); + const result = await service.getApplications(queryPagination, filter); + + const expectedApplications = indexResult.map(item => new Application({ + contract: item.address, + deployer: item.deployer, + owner: item.currentOwner, + codeHash: item.initialCodeHash, + timestamp: item.timestamp, + balance: '0', + txCount: 42, + })); + + expect(result).toEqual(expectedApplications); + expect(transferService.getTransfersCount).toHaveBeenCalled(); + }); }); describe('getApplicationsCount', () => { From 3b61ea3ab84de0757026046017278e82db13a123 Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Wed, 29 Jan 2025 13:11:43 +0200 Subject: [PATCH 02/11] implement cache system --- .../applications/application.service.ts | 15 +++++++++++++ src/test/unit/services/applications.spec.ts | 22 ++++++++++++++++--- src/utils/cache.info.ts | 2 +- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/endpoints/applications/application.service.ts b/src/endpoints/applications/application.service.ts index d0b063de4..6985125b6 100644 --- a/src/endpoints/applications/application.service.ts +++ b/src/endpoints/applications/application.service.ts @@ -9,6 +9,8 @@ import { TransferService } from '../transfers/transfer.service'; import { TransactionFilter } from '../transactions/entities/transaction.filter'; import { TransactionType } from '../transactions/entities/transaction.type'; import { Logger } from '@nestjs/common'; +import { CacheService } from '@multiversx/sdk-nestjs-cache'; +import { CacheInfo } from 'src/utils/cache.info'; @Injectable() export class ApplicationService { @@ -19,9 +21,22 @@ export class ApplicationService { private readonly assetsService: AssetsService, private readonly gatewayService: GatewayService, private readonly transferService: TransferService, + private readonly cacheService: CacheService, ) { } async getApplications(pagination: QueryPagination, filter: ApplicationFilter): Promise { + if (!filter.isSet) { + return await this.cacheService.getOrSet( + CacheInfo.Applications(pagination).key, + async () => await this.getApplicationsRaw(pagination, filter), + CacheInfo.Applications(pagination).ttl + ); + } + + return await this.getApplicationsRaw(pagination, filter); + } + + async getApplicationsRaw(pagination: QueryPagination, filter: ApplicationFilter): Promise { const elasticResults = await this.elasticIndexerService.getApplications(filter, pagination); const assets = await this.assetsService.getAllAccountAssets(); diff --git a/src/test/unit/services/applications.spec.ts b/src/test/unit/services/applications.spec.ts index d0f5751ff..1718a3cc4 100644 --- a/src/test/unit/services/applications.spec.ts +++ b/src/test/unit/services/applications.spec.ts @@ -9,6 +9,7 @@ import { AccountAssets } from '../../../common/assets/entities/account.assets'; import { Application } from 'src/endpoints/applications/entities/application'; import { GatewayService } from 'src/common/gateway/gateway.service'; import { TransferService } from 'src/endpoints/transfers/transfer.service'; +import { CacheService } from '@multiversx/sdk-nestjs-cache'; describe('ApplicationService', () => { let service: ApplicationService; @@ -16,7 +17,7 @@ describe('ApplicationService', () => { let assetsService: AssetsService; let gatewayService: GatewayService; let transferService: TransferService; - + let cacheService: CacheService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -46,6 +47,12 @@ describe('ApplicationService', () => { getTransfersCount: jest.fn(), }, }, + { + provide: CacheService, + useValue: { + getOrSet: jest.fn(), + }, + }, ], }).compile(); @@ -54,6 +61,7 @@ describe('ApplicationService', () => { assetsService = module.get(AssetsService); gatewayService = module.get(GatewayService); transferService = module.get(TransferService); + cacheService = module.get(CacheService); }); it('should be defined', () => { @@ -116,7 +124,7 @@ describe('ApplicationService', () => { const queryPagination = new QueryPagination(); const filter = new ApplicationFilter(); - const result = await service.getApplications(queryPagination, filter); + const result = await service.getApplicationsRaw(queryPagination, filter); expect(indexerService.getApplications).toHaveBeenCalledWith(filter, queryPagination); expect(indexerService.getApplications).toHaveBeenCalledTimes(1); @@ -181,7 +189,7 @@ describe('ApplicationService', () => { const queryPagination = new QueryPagination(); const filter = new ApplicationFilter({ withTxCount: true }); - const result = await service.getApplications(queryPagination, filter); + const result = await service.getApplicationsRaw(queryPagination, filter); const expectedApplications = indexResult.map(item => new Application({ contract: item.address, @@ -196,6 +204,14 @@ describe('ApplicationService', () => { expect(result).toEqual(expectedApplications); expect(transferService.getTransfersCount).toHaveBeenCalled(); }); + + it('should return an empty array of applications from cache', async () => { + const queryPagination = new QueryPagination(); + const filter = new ApplicationFilter(); + jest.spyOn(cacheService, 'getOrSet').mockResolvedValue([]); + const result = await service.getApplications(queryPagination, filter); + expect(result).toEqual([]); + }); }); describe('getApplicationsCount', () => { diff --git a/src/utils/cache.info.ts b/src/utils/cache.info.ts index d95dcec43..fadff9559 100644 --- a/src/utils/cache.info.ts +++ b/src/utils/cache.info.ts @@ -692,7 +692,7 @@ export class CacheInfo { static Applications(queryPagination: QueryPagination): CacheInfo { return { key: `applications:${queryPagination.from}:${queryPagination.size}`, - ttl: Constants.oneHour(), + ttl: Constants.oneMinute(), }; } From 2e9e02f4d2af8f77d9fb61d9ad42b62c2dffe70f Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Wed, 29 Jan 2025 13:28:07 +0200 Subject: [PATCH 03/11] added max size withTxCount request --- src/endpoints/applications/application.controller.ts | 3 ++- src/endpoints/applications/application.service.ts | 2 ++ src/endpoints/applications/entities/application.filter.ts | 8 ++++++++ src/test/unit/services/applications.spec.ts | 7 +++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/endpoints/applications/application.controller.ts b/src/endpoints/applications/application.controller.ts index 8f93f549a..250c62d9a 100644 --- a/src/endpoints/applications/application.controller.ts +++ b/src/endpoints/applications/application.controller.ts @@ -28,9 +28,10 @@ export class ApplicationController { @Query('after', ParseIntPipe) after?: number, @Query('withTxCount', new ParseBoolPipe()) withTxCount?: boolean, ): Promise { + const applicationFilter = new ApplicationFilter({ before, after, withTxCount }); return await this.applicationService.getApplications( new QueryPagination({ size, from }), - new ApplicationFilter({ before, after, withTxCount }) + applicationFilter ); } diff --git a/src/endpoints/applications/application.service.ts b/src/endpoints/applications/application.service.ts index 6985125b6..6d7a0a5f2 100644 --- a/src/endpoints/applications/application.service.ts +++ b/src/endpoints/applications/application.service.ts @@ -25,6 +25,8 @@ export class ApplicationService { ) { } async getApplications(pagination: QueryPagination, filter: ApplicationFilter): Promise { + filter.validate(pagination.size); + if (!filter.isSet) { return await this.cacheService.getOrSet( CacheInfo.Applications(pagination).key, diff --git a/src/endpoints/applications/entities/application.filter.ts b/src/endpoints/applications/entities/application.filter.ts index a29e21c55..ce7d27475 100644 --- a/src/endpoints/applications/entities/application.filter.ts +++ b/src/endpoints/applications/entities/application.filter.ts @@ -1,3 +1,5 @@ +import { BadRequestException } from "@nestjs/common"; + export class ApplicationFilter { constructor(init?: Partial) { Object.assign(this, init); @@ -7,6 +9,12 @@ export class ApplicationFilter { before?: number; withTxCount?: boolean; + validate(size: number) { + if (this.withTxCount && size > 25) { + throw new BadRequestException('Size must be less than or equal to 25 when withTxCount is set'); + } + } + isSet(): boolean { return this.after !== undefined || this.before !== undefined || diff --git a/src/test/unit/services/applications.spec.ts b/src/test/unit/services/applications.spec.ts index 1718a3cc4..6cc7bb6af 100644 --- a/src/test/unit/services/applications.spec.ts +++ b/src/test/unit/services/applications.spec.ts @@ -10,6 +10,7 @@ import { Application } from 'src/endpoints/applications/entities/application'; import { GatewayService } from 'src/common/gateway/gateway.service'; import { TransferService } from 'src/endpoints/transfers/transfer.service'; import { CacheService } from '@multiversx/sdk-nestjs-cache'; +import { BadRequestException } from '@nestjs/common'; describe('ApplicationService', () => { let service: ApplicationService; @@ -212,6 +213,12 @@ describe('ApplicationService', () => { const result = await service.getApplications(queryPagination, filter); expect(result).toEqual([]); }); + + it('should throw an error when size is greater than 25 and withTxCount is set', async () => { + const queryPagination = new QueryPagination({ size: 50 }); + const filter = new ApplicationFilter({ withTxCount: true }); + await expect(service.getApplications(queryPagination, filter)).rejects.toThrow(BadRequestException); + }); }); describe('getApplicationsCount', () => { From e122819d96b8acfcc818cfcdeb052ae9aa5e7538 Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Wed, 29 Jan 2025 13:40:39 +0200 Subject: [PATCH 04/11] return single application details --- .../elastic/elastic.indexer.service.ts | 4 +++ src/common/indexer/indexer.interface.ts | 2 ++ src/common/indexer/indexer.service.ts | 5 ++++ .../applications/application.controller.ts | 14 +++++++-- .../applications/application.service.ts | 30 +++++++++++++++++++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/common/indexer/elastic/elastic.indexer.service.ts b/src/common/indexer/elastic/elastic.indexer.service.ts index e596d89d4..661012cb1 100644 --- a/src/common/indexer/elastic/elastic.indexer.service.ts +++ b/src/common/indexer/elastic/elastic.indexer.service.ts @@ -974,6 +974,10 @@ export class ElasticIndexerService implements IndexerInterface { return await this.elasticService.getList('scdeploys', 'address', elasticQuery); } + async getApplication(address: string): Promise { + return await this.elasticService.getItem('scdeploys', 'address', address); + } + async getApplicationCount(filter: ApplicationFilter): Promise { const elasticQuery = this.indexerHelper.buildApplicationFilter(filter); diff --git a/src/common/indexer/indexer.interface.ts b/src/common/indexer/indexer.interface.ts index e6b3fe961..d22317bff 100644 --- a/src/common/indexer/indexer.interface.ts +++ b/src/common/indexer/indexer.interface.ts @@ -188,6 +188,8 @@ export interface IndexerInterface { getApplicationCount(filter: ApplicationFilter): Promise + getApplication(address: string): Promise + getAddressesWithTransfersLast24h(): Promise getEvents(pagination: QueryPagination, filter: EventsFilter): Promise diff --git a/src/common/indexer/indexer.service.ts b/src/common/indexer/indexer.service.ts index f70ca70e1..715670321 100644 --- a/src/common/indexer/indexer.service.ts +++ b/src/common/indexer/indexer.service.ts @@ -446,6 +446,11 @@ export class IndexerService implements IndexerInterface { return await this.indexerInterface.getApplications(filter, pagination); } + @LogPerformanceAsync(MetricsEvents.SetIndexerDuration) + async getApplication(address: string): Promise { + return await this.indexerInterface.getApplication(address); + } + @LogPerformanceAsync(MetricsEvents.SetIndexerDuration) async getApplicationCount(filter: ApplicationFilter): Promise { return await this.indexerInterface.getApplicationCount(filter); diff --git a/src/endpoints/applications/application.controller.ts b/src/endpoints/applications/application.controller.ts index 250c62d9a..3d389daaf 100644 --- a/src/endpoints/applications/application.controller.ts +++ b/src/endpoints/applications/application.controller.ts @@ -1,9 +1,9 @@ -import { Controller, DefaultValuePipe, Get, Query } from "@nestjs/common"; +import { Controller, DefaultValuePipe, Get, Param, Query } from "@nestjs/common"; import { ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger"; import { ApplicationService } from "./application.service"; import { QueryPagination } from "src/common/entities/query.pagination"; import { ApplicationFilter } from "./entities/application.filter"; -import { ParseIntPipe, ParseBoolPipe } from "@multiversx/sdk-nestjs-common"; +import { ParseIntPipe, ParseBoolPipe, ParseAddressPipe } from "@multiversx/sdk-nestjs-common"; import { Application } from "./entities/application"; @Controller() @@ -35,6 +35,16 @@ export class ApplicationController { ); } + @Get("applications/:address") + @ApiOperation({ summary: 'Application details', description: 'Returns details of a smart contract' }) + @ApiOkResponse({ type: Application }) + async getApplication( + @Param('address', ParseAddressPipe) address: string, + @Query('withTxCount', new ParseBoolPipe()) withTxCount?: boolean, + ): Promise { + return await this.applicationService.getApplication(address, withTxCount ?? false); + } + @Get("applications/count") @ApiOperation({ summary: 'Applications count', description: 'Returns total number of smart contracts' }) @ApiOkResponse({ type: Number }) diff --git a/src/endpoints/applications/application.service.ts b/src/endpoints/applications/application.service.ts index 6d7a0a5f2..2187b946a 100644 --- a/src/endpoints/applications/application.service.ts +++ b/src/endpoints/applications/application.service.ts @@ -86,6 +86,36 @@ export class ApplicationService { return await this.elasticIndexerService.getApplicationCount(filter); } + async getApplication(address: string, withTxCount: boolean): Promise { + const indexResult = await this.elasticIndexerService.getApplication(address); + const assets = await this.assetsService.getAllAccountAssets(); + + const result = new Application({ + contract: indexResult.address, + deployer: indexResult.deployer, + owner: indexResult.currentOwner, + codeHash: indexResult.initialCodeHash, + timestamp: indexResult.timestamp, + assets: assets[address], + balance: '0', + ...(withTxCount && { txCount: 0 }), + }); + + try { + const { account: { balance } } = await this.gatewayService.getAddressDetails(result.contract); + result.balance = balance; + } catch (error) { + this.logger.error(`Error when getting balance for contract ${result.contract}`, error); + result.balance = '0'; + } + + if (withTxCount) { + result.txCount = await this.getApplicationTxCount(result.contract); + } + + return result; + } + async getApplicationTxCount(address: string): Promise { return await this.transferService.getTransfersCount(new TransactionFilter({ address, type: TransactionType.Transaction })); } From 945c58810e2a0e4792a7faaea58235e5200a4a60 Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Wed, 29 Jan 2025 13:53:50 +0200 Subject: [PATCH 05/11] add getApplicationBalance private method to be able to reuse the same functionallity --- .../applications/application.service.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/endpoints/applications/application.service.ts b/src/endpoints/applications/application.service.ts index 2187b946a..716475ea0 100644 --- a/src/endpoints/applications/application.service.ts +++ b/src/endpoints/applications/application.service.ts @@ -61,13 +61,7 @@ export class ApplicationService { const promises: Promise[] = []; promises.push((async () => { - try { - const { account: { balance } } = await this.gatewayService.getAddressDetails(application.contract); - application.balance = balance; - } catch (error) { - this.logger.error(`Error when getting balance for contract ${application.contract}`, error); - application.balance = '0'; - } + application.balance = await this.getApplicationBalance(application.contract); })()); if (filter.withTxCount) { @@ -101,22 +95,26 @@ export class ApplicationService { ...(withTxCount && { txCount: 0 }), }); - try { - const { account: { balance } } = await this.gatewayService.getAddressDetails(result.contract); - result.balance = balance; - } catch (error) { - this.logger.error(`Error when getting balance for contract ${result.contract}`, error); - result.balance = '0'; - } - if (withTxCount) { result.txCount = await this.getApplicationTxCount(result.contract); } + result.balance = await this.getApplicationBalance(result.contract); + return result; } - async getApplicationTxCount(address: string): Promise { + private async getApplicationTxCount(address: string): Promise { return await this.transferService.getTransfersCount(new TransactionFilter({ address, type: TransactionType.Transaction })); } + + private async getApplicationBalance(address: string): Promise { + try { + const { account: { balance } } = await this.gatewayService.getAddressDetails(address); + return balance; + } catch (error) { + this.logger.error(`Error when getting balance for contract ${address}`, error); + return '0'; + } + } } From b76b49003e8127f75c79bb80d92546b3b717f369 Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Wed, 29 Jan 2025 18:36:39 +0200 Subject: [PATCH 06/11] simplify txCount for single application --- src/endpoints/applications/application.controller.ts | 3 +-- src/endpoints/applications/application.service.ts | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/endpoints/applications/application.controller.ts b/src/endpoints/applications/application.controller.ts index 3d389daaf..5dc635dcb 100644 --- a/src/endpoints/applications/application.controller.ts +++ b/src/endpoints/applications/application.controller.ts @@ -40,9 +40,8 @@ export class ApplicationController { @ApiOkResponse({ type: Application }) async getApplication( @Param('address', ParseAddressPipe) address: string, - @Query('withTxCount', new ParseBoolPipe()) withTxCount?: boolean, ): Promise { - return await this.applicationService.getApplication(address, withTxCount ?? false); + return await this.applicationService.getApplication(address); } @Get("applications/count") diff --git a/src/endpoints/applications/application.service.ts b/src/endpoints/applications/application.service.ts index 716475ea0..831687af3 100644 --- a/src/endpoints/applications/application.service.ts +++ b/src/endpoints/applications/application.service.ts @@ -80,7 +80,7 @@ export class ApplicationService { return await this.elasticIndexerService.getApplicationCount(filter); } - async getApplication(address: string, withTxCount: boolean): Promise { + async getApplication(address: string): Promise { const indexResult = await this.elasticIndexerService.getApplication(address); const assets = await this.assetsService.getAllAccountAssets(); @@ -92,13 +92,10 @@ export class ApplicationService { timestamp: indexResult.timestamp, assets: assets[address], balance: '0', - ...(withTxCount && { txCount: 0 }), + txCount: 0, }); - if (withTxCount) { - result.txCount = await this.getApplicationTxCount(result.contract); - } - + result.txCount = await this.getApplicationTxCount(result.contract); result.balance = await this.getApplicationBalance(result.contract); return result; From babd9f3f9df5b52b900cde3fa6a0fad70996fce5 Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Mon, 3 Feb 2025 13:34:10 +0200 Subject: [PATCH 07/11] Update GitHub Actions workflow to use actions/upload-artifact@v4 --- .github/workflows/load-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/load-tests.yml b/.github/workflows/load-tests.yml index 5f14d7355..775a1cdee 100644 --- a/.github/workflows/load-tests.yml +++ b/.github/workflows/load-tests.yml @@ -58,7 +58,7 @@ jobs: run: k6 run ./k6/script.js - name: Upload result file for base branch - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: base-results path: k6/output/summary.json @@ -119,7 +119,7 @@ jobs: run: k6 run ./k6/script.js - name: Upload result file for head branch - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: head-results path: k6/output/summary.json @@ -159,7 +159,7 @@ jobs: head: ${{ github.event.pull_request.head.sha }} - name: Upload the report markdown - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: github.event_name == 'pull_request' with: name: report-markdown From 2d130e0b0cdcf00b492bc3ee5e08330271511cba Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Mon, 3 Feb 2025 13:40:09 +0200 Subject: [PATCH 08/11] bump version actions/download-artifact@v4 --- .github/workflows/load-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/load-tests.yml b/.github/workflows/load-tests.yml index 775a1cdee..50568016a 100644 --- a/.github/workflows/load-tests.yml +++ b/.github/workflows/load-tests.yml @@ -135,7 +135,7 @@ jobs: - uses: actions/checkout@v2 - name: Download all artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts From 4ba83307b8c0041bd28df335f98b909f74907f75 Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Mon, 3 Feb 2025 14:09:08 +0200 Subject: [PATCH 09/11] refactor --- .../applications/application.controller.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/endpoints/applications/application.controller.ts b/src/endpoints/applications/application.controller.ts index 5dc635dcb..dd444f1cd 100644 --- a/src/endpoints/applications/application.controller.ts +++ b/src/endpoints/applications/application.controller.ts @@ -35,15 +35,6 @@ export class ApplicationController { ); } - @Get("applications/:address") - @ApiOperation({ summary: 'Application details', description: 'Returns details of a smart contract' }) - @ApiOkResponse({ type: Application }) - async getApplication( - @Param('address', ParseAddressPipe) address: string, - ): Promise { - return await this.applicationService.getApplication(address); - } - @Get("applications/count") @ApiOperation({ summary: 'Applications count', description: 'Returns total number of smart contracts' }) @ApiOkResponse({ type: Number }) @@ -57,4 +48,13 @@ export class ApplicationController { return await this.applicationService.getApplicationsCount(filter); } + + @Get("applications/:address") + @ApiOperation({ summary: 'Application details', description: 'Returns details of a smart contract' }) + @ApiOkResponse({ type: Application }) + async getApplication( + @Param('address', ParseAddressPipe) address: string, + ): Promise { + return await this.applicationService.getApplication(address); + } } From e0981d5d1a38fa7bdb78c824c11f38a75b31f065 Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Mon, 3 Feb 2025 14:17:42 +0200 Subject: [PATCH 10/11] update cs tests --- .../chain-simulator/applications.cs-e2e.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/test/chain-simulator/applications.cs-e2e.ts b/src/test/chain-simulator/applications.cs-e2e.ts index 42eaf858f..e496ecdc6 100644 --- a/src/test/chain-simulator/applications.cs-e2e.ts +++ b/src/test/chain-simulator/applications.cs-e2e.ts @@ -33,6 +33,33 @@ describe('Applications e2e tests with chain simulator', () => { } }); + it('should return applications with txCount field if withTxCount query param is true', async () => { + const response = await axios.get(`${config.apiServiceUrl}/applications?withTxCount=true`); + expect(response.status).toBe(200); + expect(response.data).toBeInstanceOf(Array); + expect(response.data[0]).toHaveProperty('txCount'); + }); + }); + + describe('GET /applications/:address', () => { + it('should return status code 200 and an application', async () => { + const application = await axios.get(`${config.apiServiceUrl}/applications`); + const response = await axios.get(`${config.apiServiceUrl}/applications/${application.data[0].contract}`); + expect(response.status).toBe(200); + expect(response.data).toBeInstanceOf(Object); + }); + + it('should return application details with txCount and balance fields', async () => { + const application = await axios.get(`${config.apiServiceUrl}/applications`); + const response = await axios.get(`${config.apiServiceUrl}/applications/${application.data[0].contract}`); + expect(response.status).toBe(200); + expect(response.data).toBeInstanceOf(Object); + expect(response.data).toHaveProperty('txCount'); + expect(response.data).toHaveProperty('balance'); + }); + }); + + describe('GET /applications/count', () => { it('should return the number of applications', async () => { const response = await axios.get(`${config.apiServiceUrl}/applications/count`); expect(response.status).toBe(200); From 0c370cf7cdebe2626831ac5da292dfa462319086 Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Mon, 3 Feb 2025 14:44:00 +0200 Subject: [PATCH 11/11] fixes after review --- .../applications/application.service.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/endpoints/applications/application.service.ts b/src/endpoints/applications/application.service.ts index 831687af3..49a6ff96a 100644 --- a/src/endpoints/applications/application.service.ts +++ b/src/endpoints/applications/application.service.ts @@ -57,21 +57,17 @@ export class ApplicationService { ...(filter.withTxCount && { txCount: 0 }), })); - await Promise.all(applications.map(async (application) => { - const promises: Promise[] = []; - - promises.push((async () => { - application.balance = await this.getApplicationBalance(application.contract); - })()); - - if (filter.withTxCount) { - promises.push((async () => { - application.txCount = await this.getApplicationTxCount(application.contract); - })()); + const balancePromises = applications.map(application => + this.getApplicationBalance(application.contract) + .then(balance => { application.balance = balance; }) + ); + await Promise.all(balancePromises); + + if (filter.withTxCount) { + for (const application of applications) { + application.txCount = await this.getApplicationTxCount(application.contract); } - - await Promise.all(promises); - })); + } return applications; }