Skip to content

Commit 887bd0b

Browse files
authored
update applications endpoint (#1443)
* update applications endpoint * implement cache system * added max size withTxCount request * return single application details * add getApplicationBalance private method to be able to reuse the same functionallity * simplify txCount for single application * Update GitHub Actions workflow to use actions/upload-artifact@v4 * bump version actions/download-artifact@v4 * refactor * update cs tests * fixes after review
1 parent 6e3b838 commit 887bd0b

11 files changed

Lines changed: 261 additions & 10 deletions

File tree

src/common/indexer/elastic/elastic.indexer.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,10 @@ export class ElasticIndexerService implements IndexerInterface {
974974
return await this.elasticService.getList('scdeploys', 'address', elasticQuery);
975975
}
976976

977+
async getApplication(address: string): Promise<any> {
978+
return await this.elasticService.getItem('scdeploys', 'address', address);
979+
}
980+
977981
async getApplicationCount(filter: ApplicationFilter): Promise<number> {
978982
const elasticQuery = this.indexerHelper.buildApplicationFilter(filter);
979983

src/common/indexer/indexer.interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ export interface IndexerInterface {
188188

189189
getApplicationCount(filter: ApplicationFilter): Promise<number>
190190

191+
getApplication(address: string): Promise<any>
192+
191193
getAddressesWithTransfersLast24h(): Promise<string[]>
192194

193195
getEvents(pagination: QueryPagination, filter: EventsFilter): Promise<Events[]>

src/common/indexer/indexer.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,11 @@ export class IndexerService implements IndexerInterface {
446446
return await this.indexerInterface.getApplications(filter, pagination);
447447
}
448448

449+
@LogPerformanceAsync(MetricsEvents.SetIndexerDuration)
450+
async getApplication(address: string): Promise<any> {
451+
return await this.indexerInterface.getApplication(address);
452+
}
453+
449454
@LogPerformanceAsync(MetricsEvents.SetIndexerDuration)
450455
async getApplicationCount(filter: ApplicationFilter): Promise<number> {
451456
return await this.indexerInterface.getApplicationCount(filter);

src/endpoints/applications/application.controller.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Controller, DefaultValuePipe, Get, Query } from "@nestjs/common";
1+
import { Controller, DefaultValuePipe, Get, Param, Query } from "@nestjs/common";
22
import { ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger";
33
import { ApplicationService } from "./application.service";
44
import { QueryPagination } from "src/common/entities/query.pagination";
55
import { ApplicationFilter } from "./entities/application.filter";
6-
import { ParseIntPipe } from "@multiversx/sdk-nestjs-common";
6+
import { ParseIntPipe, ParseBoolPipe, ParseAddressPipe } from "@multiversx/sdk-nestjs-common";
77
import { Application } from "./entities/application";
88

99
@Controller()
@@ -20,15 +20,18 @@ export class ApplicationController {
2020
@ApiQuery({ name: 'size', description: 'Number of items to retrieve', required: false })
2121
@ApiQuery({ name: 'before', description: 'Before timestamp', required: false })
2222
@ApiQuery({ name: 'after', description: 'After timestamp', required: false })
23+
@ApiQuery({ name: 'withTxCount', description: 'Include transaction count', required: false, type: Boolean })
2324
async getApplications(
2425
@Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number,
2526
@Query("size", new DefaultValuePipe(25), ParseIntPipe) size: number,
2627
@Query('before', ParseIntPipe) before?: number,
2728
@Query('after', ParseIntPipe) after?: number,
28-
): Promise<any[]> {
29+
@Query('withTxCount', new ParseBoolPipe()) withTxCount?: boolean,
30+
): Promise<Application[]> {
31+
const applicationFilter = new ApplicationFilter({ before, after, withTxCount });
2932
return await this.applicationService.getApplications(
3033
new QueryPagination({ size, from }),
31-
new ApplicationFilter({ before, after })
34+
applicationFilter
3235
);
3336
}
3437

@@ -45,4 +48,13 @@ export class ApplicationController {
4548

4649
return await this.applicationService.getApplicationsCount(filter);
4750
}
51+
52+
@Get("applications/:address")
53+
@ApiOperation({ summary: 'Application details', description: 'Returns details of a smart contract' })
54+
@ApiOkResponse({ type: Application })
55+
async getApplication(
56+
@Param('address', ParseAddressPipe) address: string,
57+
): Promise<Application> {
58+
return await this.applicationService.getApplication(address);
59+
}
4860
}

src/endpoints/applications/application.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ import { Module } from "@nestjs/common";
22
import { ElasticIndexerModule } from "src/common/indexer/elastic/elastic.indexer.module";
33
import { ApplicationService } from "./application.service";
44
import { AssetsService } from '../../common/assets/assets.service';
5+
import { GatewayService } from "src/common/gateway/gateway.service";
6+
import { TransferModule } from "../transfers/transfer.module";
57

68
@Module({
79
imports: [
810
ElasticIndexerModule,
11+
TransferModule,
912
],
1013
providers: [
1114
ApplicationService,
1215
AssetsService,
16+
GatewayService,
1317
],
1418
exports: [
1519
ApplicationService,

src/endpoints/applications/application.service.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,110 @@ import { Application } from './entities/application';
44
import { QueryPagination } from 'src/common/entities/query.pagination';
55
import { ApplicationFilter } from './entities/application.filter';
66
import { AssetsService } from '../../common/assets/assets.service';
7+
import { GatewayService } from 'src/common/gateway/gateway.service';
8+
import { TransferService } from '../transfers/transfer.service';
9+
import { TransactionFilter } from '../transactions/entities/transaction.filter';
10+
import { TransactionType } from '../transactions/entities/transaction.type';
11+
import { Logger } from '@nestjs/common';
12+
import { CacheService } from '@multiversx/sdk-nestjs-cache';
13+
import { CacheInfo } from 'src/utils/cache.info';
714

815
@Injectable()
916
export class ApplicationService {
17+
private readonly logger = new Logger(ApplicationService.name);
18+
1019
constructor(
1120
private readonly elasticIndexerService: ElasticIndexerService,
1221
private readonly assetsService: AssetsService,
22+
private readonly gatewayService: GatewayService,
23+
private readonly transferService: TransferService,
24+
private readonly cacheService: CacheService,
1325
) { }
1426

1527
async getApplications(pagination: QueryPagination, filter: ApplicationFilter): Promise<Application[]> {
28+
filter.validate(pagination.size);
29+
30+
if (!filter.isSet) {
31+
return await this.cacheService.getOrSet(
32+
CacheInfo.Applications(pagination).key,
33+
async () => await this.getApplicationsRaw(pagination, filter),
34+
CacheInfo.Applications(pagination).ttl
35+
);
36+
}
37+
38+
return await this.getApplicationsRaw(pagination, filter);
39+
}
40+
41+
async getApplicationsRaw(pagination: QueryPagination, filter: ApplicationFilter): Promise<Application[]> {
1642
const elasticResults = await this.elasticIndexerService.getApplications(filter, pagination);
1743
const assets = await this.assetsService.getAllAccountAssets();
1844

1945
if (!elasticResults) {
2046
return [];
2147
}
2248

23-
return elasticResults.map(item => ({
49+
const applications = elasticResults.map(item => new Application({
2450
contract: item.address,
2551
deployer: item.deployer,
2652
owner: item.currentOwner,
2753
codeHash: item.initialCodeHash,
2854
timestamp: item.timestamp,
2955
assets: assets[item.address],
56+
balance: '0',
57+
...(filter.withTxCount && { txCount: 0 }),
3058
}));
59+
60+
const balancePromises = applications.map(application =>
61+
this.getApplicationBalance(application.contract)
62+
.then(balance => { application.balance = balance; })
63+
);
64+
await Promise.all(balancePromises);
65+
66+
if (filter.withTxCount) {
67+
for (const application of applications) {
68+
application.txCount = await this.getApplicationTxCount(application.contract);
69+
}
70+
}
71+
72+
return applications;
3173
}
3274

3375
async getApplicationsCount(filter: ApplicationFilter): Promise<number> {
3476
return await this.elasticIndexerService.getApplicationCount(filter);
3577
}
78+
79+
async getApplication(address: string): Promise<Application> {
80+
const indexResult = await this.elasticIndexerService.getApplication(address);
81+
const assets = await this.assetsService.getAllAccountAssets();
82+
83+
const result = new Application({
84+
contract: indexResult.address,
85+
deployer: indexResult.deployer,
86+
owner: indexResult.currentOwner,
87+
codeHash: indexResult.initialCodeHash,
88+
timestamp: indexResult.timestamp,
89+
assets: assets[address],
90+
balance: '0',
91+
txCount: 0,
92+
});
93+
94+
result.txCount = await this.getApplicationTxCount(result.contract);
95+
result.balance = await this.getApplicationBalance(result.contract);
96+
97+
return result;
98+
}
99+
100+
private async getApplicationTxCount(address: string): Promise<number> {
101+
return await this.transferService.getTransfersCount(new TransactionFilter({ address, type: TransactionType.Transaction }));
102+
}
103+
104+
private async getApplicationBalance(address: string): Promise<string> {
105+
try {
106+
const { account: { balance } } = await this.gatewayService.getAddressDetails(address);
107+
return balance;
108+
} catch (error) {
109+
this.logger.error(`Error when getting balance for contract ${address}`, error);
110+
return '0';
111+
}
112+
}
36113
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1+
import { BadRequestException } from "@nestjs/common";
2+
13
export class ApplicationFilter {
24
constructor(init?: Partial<ApplicationFilter>) {
35
Object.assign(this, init);
46
}
57

68
after?: number;
79
before?: number;
10+
withTxCount?: boolean;
11+
12+
validate(size: number) {
13+
if (this.withTxCount && size > 25) {
14+
throw new BadRequestException('Size must be less than or equal to 25 when withTxCount is set');
15+
}
16+
}
817

918
isSet(): boolean {
1019
return this.after !== undefined ||
11-
this.before !== undefined;
20+
this.before !== undefined ||
21+
this.withTxCount !== undefined;
1222
}
1323
}

src/endpoints/applications/entities/application.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,10 @@ export class Application {
2323

2424
@ApiProperty({ type: AccountAssets, nullable: true, description: 'Contract assets' })
2525
assets: AccountAssets | undefined = undefined;
26+
27+
@ApiProperty({ type: String })
28+
balance: string = '0';
29+
30+
@ApiProperty({ type: Number, required: false })
31+
txCount?: number;
2632
}

src/test/chain-simulator/applications.cs-e2e.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,33 @@ describe('Applications e2e tests with chain simulator', () => {
3333
}
3434
});
3535

36+
it('should return applications with txCount field if withTxCount query param is true', async () => {
37+
const response = await axios.get(`${config.apiServiceUrl}/applications?withTxCount=true`);
38+
expect(response.status).toBe(200);
39+
expect(response.data).toBeInstanceOf(Array);
40+
expect(response.data[0]).toHaveProperty('txCount');
41+
});
42+
});
43+
44+
describe('GET /applications/:address', () => {
45+
it('should return status code 200 and an application', async () => {
46+
const application = await axios.get(`${config.apiServiceUrl}/applications`);
47+
const response = await axios.get(`${config.apiServiceUrl}/applications/${application.data[0].contract}`);
48+
expect(response.status).toBe(200);
49+
expect(response.data).toBeInstanceOf(Object);
50+
});
51+
52+
it('should return application details with txCount and balance fields', async () => {
53+
const application = await axios.get(`${config.apiServiceUrl}/applications`);
54+
const response = await axios.get(`${config.apiServiceUrl}/applications/${application.data[0].contract}`);
55+
expect(response.status).toBe(200);
56+
expect(response.data).toBeInstanceOf(Object);
57+
expect(response.data).toHaveProperty('txCount');
58+
expect(response.data).toHaveProperty('balance');
59+
});
60+
});
61+
62+
describe('GET /applications/count', () => {
3663
it('should return the number of applications', async () => {
3764
const response = await axios.get(`${config.apiServiceUrl}/applications/count`);
3865
expect(response.status).toBe(200);

0 commit comments

Comments
 (0)