diff --git a/src/common/indexer/elastic/elastic.indexer.service.ts b/src/common/indexer/elastic/elastic.indexer.service.ts index 0bc3b7f72..3bfcb19a3 100644 --- a/src/common/indexer/elastic/elastic.indexer.service.ts +++ b/src/common/indexer/elastic/elastic.indexer.service.ts @@ -49,7 +49,7 @@ export class ElasticIndexerService implements IndexerInterface { return await this.elasticService.getCount('operations', query); } - async getAccountContractsCount(address: string): Promise { + async getAccountDeploysCount(address: string): Promise { const elasticQuery: ElasticQuery = ElasticQuery.create() .withCondition(QueryConditionOptions.must, [QueryType.Match("deployer", address)]); @@ -435,7 +435,7 @@ export class ElasticIndexerService implements IndexerInterface { ); } - async getAccountContracts(pagination: QueryPagination, address: string): Promise { + async getAccountDeploys(pagination: QueryPagination, address: string): Promise { const elasticQuery: ElasticQuery = ElasticQuery.create() .withPagination(pagination) .withCondition(QueryConditionOptions.must, [QueryType.Match("deployer", address)]) @@ -444,6 +444,22 @@ export class ElasticIndexerService implements IndexerInterface { return await this.elasticService.getList('scdeploys', "contract", elasticQuery); } + async getAccountContracts(pagination: QueryPagination, address: string): Promise { + const elasticQuery: ElasticQuery = ElasticQuery.create() + .withPagination(pagination) + .withCondition(QueryConditionOptions.must, [QueryType.Match("currentOwner", address)]) + .withSort([{ name: 'timestamp', order: ElasticSortOrder.descending }]); + + return await this.elasticService.getList('scdeploys', "contract", elasticQuery); + } + + async getAccountContractsCount(address: string): Promise { + const elasticQuery: ElasticQuery = ElasticQuery.create() + .withCondition(QueryConditionOptions.must, [QueryType.Match("currentOwner", address)]); + + return await this.elasticService.getCount('scdeploys', elasticQuery); + } + async getProviderDelegators(address: string, pagination: QueryPagination): Promise { const elasticQuery: ElasticQuery = ElasticQuery.create() .withPagination(pagination) diff --git a/src/common/indexer/indexer.interface.ts b/src/common/indexer/indexer.interface.ts index 9e2a29c4b..a2f4e62b4 100644 --- a/src/common/indexer/indexer.interface.ts +++ b/src/common/indexer/indexer.interface.ts @@ -22,7 +22,7 @@ export interface IndexerInterface { getScResultsCount(filter: SmartContractResultFilter): Promise - getAccountContractsCount(address: string): Promise + getAccountDeploysCount(address: string): Promise getBlocksCount(filter: BlockFilter): Promise @@ -104,7 +104,11 @@ export interface IndexerInterface { getAccounts(queryPagination: QueryPagination, filter: AccountQueryOptions): Promise - getAccountContracts(pagination: QueryPagination, address: string): Promise + getAccountDeploys(pagination: QueryPagination, address: string): Promise + + getAccountContracts(pagination: QueryPagination, address: string): Promise + + getAccountContractsCount( address: string): Promise getAccountHistory(address: string, pagination: QueryPagination, filter: AccountHistoryFilter): Promise diff --git a/src/common/indexer/indexer.service.ts b/src/common/indexer/indexer.service.ts index fca32f4f2..b4fc8c24c 100644 --- a/src/common/indexer/indexer.service.ts +++ b/src/common/indexer/indexer.service.ts @@ -39,8 +39,8 @@ export class IndexerService implements IndexerInterface { } @LogPerformanceAsync(MetricsEvents.SetIndexerDuration) - async getAccountContractsCount(address: string): Promise { - return await this.indexerInterface.getAccountContractsCount(address); + async getAccountDeploysCount(address: string): Promise { + return await this.indexerInterface.getAccountDeploysCount(address); } @LogPerformanceAsync(MetricsEvents.SetIndexerDuration) @@ -230,10 +230,20 @@ export class IndexerService implements IndexerInterface { } @LogPerformanceAsync(MetricsEvents.SetIndexerDuration) - async getAccountContracts(pagination: QueryPagination, address: string): Promise { + async getAccountDeploys(pagination: QueryPagination, address: string): Promise { + return await this.indexerInterface.getAccountDeploys(pagination, address); + } + + @LogPerformanceAsync(MetricsEvents.SetIndexerDuration) + async getAccountContracts(pagination: QueryPagination, address: string): Promise { return await this.indexerInterface.getAccountContracts(pagination, address); } + @LogPerformanceAsync(MetricsEvents.SetIndexerDuration) + async getAccountContractsCount(address: string): Promise { + return await this.indexerInterface.getAccountContractsCount(address); + } + @LogPerformanceAsync(MetricsEvents.SetIndexerDuration) async getAccountHistory(address: string, pagination: QueryPagination, filter: AccountHistoryFilter): Promise { return await this.indexerInterface.getAccountHistory(address, pagination, filter); diff --git a/src/common/indexer/postgres/postgres.indexer.service.ts b/src/common/indexer/postgres/postgres.indexer.service.ts index e9910ec28..fa3cb1bbe 100644 --- a/src/common/indexer/postgres/postgres.indexer.service.ts +++ b/src/common/indexer/postgres/postgres.indexer.service.ts @@ -14,7 +14,7 @@ import { TokenFilter } from "src/endpoints/tokens/entities/token.filter"; import { TokenWithRolesFilter } from "src/endpoints/tokens/entities/token.with.roles.filter"; import { TransactionFilter } from "src/endpoints/transactions/entities/transaction.filter"; import { Repository } from "typeorm"; -import { Collection, ScResult, Account, MiniBlock, Tag, TokenType, Block } from "../entities"; +import { Collection, ScResult, Account, MiniBlock, Tag, TokenType, Block, ScDeploy } from "../entities"; import { IndexerInterface } from "../indexer.interface"; import { AccountDb, AccountsEsdtDb, BlockDb, LogDb, MiniBlockDb, ReceiptDb, RoundInfoDb, ScDeployInfoDb, ScResultDb, TagDb, TokenInfoDb, TransactionDb, ValidatorPublicKeysDb } from "./entities"; import { PostgresIndexerHelper } from "./postgres.indexer.helper"; @@ -53,6 +53,10 @@ export class PostgresIndexerService implements IndexerInterface { private readonly validatorPublicKeysRepository: Repository, private readonly indexerHelper: PostgresIndexerHelper, ) { } + + getAccountDeploys(_pagination: QueryPagination, _address: string): Promise { + throw new Error("Method not implemented."); + } getApplicationCount(): Promise { throw new Error("Method not implemented."); } @@ -118,7 +122,7 @@ export class PostgresIndexerService implements IndexerInterface { return await this.scResultsRepository.count(); } - async getAccountContractsCount(address: string): Promise { + async getAccountDeploysCount(address: string): Promise { const query = this.scDeploysRepository .createQueryBuilder() .where('creator = :address', { address }); @@ -398,14 +402,12 @@ export class PostgresIndexerService implements IndexerInterface { return await query.getMany(); } - async getAccountContracts({ from, size }: QueryPagination, address: string): Promise { - const query = this.scDeploysRepository - .createQueryBuilder() - .skip(from).take(size) - .where('creator = :address', { address }) - .orderBy('timestamp', 'DESC'); + getAccountContracts(): Promise { + throw new Error("Method not implemented."); + } - return await query.getMany(); + getAccountContractsCount(): Promise { + throw new Error("Method not implemented."); } async getAccountHistory(address: string, { from, size }: QueryPagination): Promise { diff --git a/src/endpoints/accounts/account.controller.ts b/src/endpoints/accounts/account.controller.ts index d3d9944c6..a13f5fc04 100644 --- a/src/endpoints/accounts/account.controller.ts +++ b/src/endpoints/accounts/account.controller.ts @@ -56,6 +56,7 @@ import { AccountKeyFilter } from './entities/account.key.filter'; import { ScamType } from 'src/common/entities/scam-type.enum'; import { DeepHistoryInterceptor } from 'src/interceptors/deep-history.interceptor'; import { MexPairType } from '../mex/entities/mex.pair.type'; +import { AccountContract } from './entities/account.contract'; @Controller() @ApiTags('accounts') @@ -1080,8 +1081,34 @@ export class AccountController { })); } + @Get("/accounts/:address/deploys") + @ApiOperation({ summary: 'Account deploys details', description: 'Returns deploys details for a given account' }) + @ApiQuery({ name: 'from', description: 'Number of items to skip for the result set', required: false }) + @ApiQuery({ name: 'size', description: 'Number of items to retrieve', required: false }) + @ApiOkResponse({ type: [DeployedContract] }) + getAccountDeploys( + @Param('address', ParseAddressPipe) address: string, + @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, + @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, + ): Promise { + return this.accountService.getAccountDeploys(new QueryPagination({ from, size }), address); + } + + @Get("/accounts/:address/deploys/count") + @ApiOperation({ summary: 'Account deploys count', description: 'Returns total number of deploys for a given address' }) + @ApiOkResponse({ type: Number }) + getAccountDeploysCount(@Param('address', ParseAddressPipe) address: string): Promise { + return this.accountService.getAccountDeploysCount(address); + } + + @Get("/accounts/:address/deploys/c") + @ApiExcludeEndpoint() + getAccountDeploysCountAlternative(@Param('address', ParseAddressPipe) address: string): Promise { + return this.accountService.getAccountDeploysCount(address); + } + @Get("/accounts/:address/contracts") - @ApiOperation({ summary: 'Account smart contracts details', description: 'Returns smart contracts details for a given account' }) + @ApiOperation({ summary: 'Account contracts details', description: 'Returns contracts details for a given account' }) @ApiQuery({ name: 'from', description: 'Number of items to skip for the result set', required: false }) @ApiQuery({ name: 'size', description: 'Number of items to retrieve', required: false }) @ApiOkResponse({ type: [DeployedContract] }) @@ -1089,12 +1116,12 @@ export class AccountController { @Param('address', ParseAddressPipe) address: string, @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - ): Promise { + ): Promise { return this.accountService.getAccountContracts(new QueryPagination({ from, size }), address); } @Get("/accounts/:address/contracts/count") - @ApiOperation({ summary: 'Account contracts count', description: 'Returns total number of deployed contracts for a given address' }) + @ApiOperation({ summary: 'Account contracts count', description: 'Returns total number of contracts for a given address' }) @ApiOkResponse({ type: Number }) getAccountContractsCount(@Param('address', ParseAddressPipe) address: string): Promise { return this.accountService.getAccountContractsCount(address); diff --git a/src/endpoints/accounts/account.service.ts b/src/endpoints/accounts/account.service.ts index df8183d11..26b3e7bad 100644 --- a/src/endpoints/accounts/account.service.ts +++ b/src/endpoints/accounts/account.service.ts @@ -35,6 +35,7 @@ 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'; @Injectable() export class AccountService { @@ -614,8 +615,8 @@ export class AccountService { } } - async getAccountContracts(pagination: QueryPagination, address: string): Promise { - const accountDeployedContracts = await this.indexerService.getAccountContracts(pagination, address); + async getAccountDeploys(pagination: QueryPagination, address: string): Promise { + const accountDeployedContracts = await this.indexerService.getAccountDeploys(pagination, address); const assets = await this.assetsService.getAllAccountAssets(); const accounts: DeployedContract[] = accountDeployedContracts.map(contract => ({ @@ -628,6 +629,24 @@ export class AccountService { return accounts; } + async getAccountDeploysCount(address: string): Promise { + return await this.indexerService.getAccountDeploysCount(address); + } + + async getAccountContracts(pagination: QueryPagination, address: string): Promise { + const accountContracts = await this.indexerService.getAccountContracts(pagination, address); + const assets = await this.assetsService.getAllAccountAssets(); + + const accounts: DeployedContract[] = accountContracts.map(contract => ({ + address: contract.contract, + deployTxHash: contract.deployTxHash, + timestamp: contract.timestamp, + assets: assets[contract.contract], + })); + + return accounts; + } + async getAccountContractsCount(address: string): Promise { return await this.indexerService.getAccountContractsCount(address); } diff --git a/src/endpoints/accounts/entities/account.contract.ts b/src/endpoints/accounts/entities/account.contract.ts new file mode 100644 index 000000000..7a73f8eda --- /dev/null +++ b/src/endpoints/accounts/entities/account.contract.ts @@ -0,0 +1,26 @@ +import { Field, Float, ObjectType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AccountAssets } from "src/common/assets/entities/account.assets"; + +@ObjectType("AccountContract", { description: "Account contract object type." }) +export class AccountContract { + constructor(init?: Partial) { + Object.assign(this, init); + } + + @Field(() => String, { description: 'Address for the given account.' }) + @ApiProperty({ type: String }) + address: string = ""; + + @Field(() => String, { description: 'DeployTxHash for the given account.' }) + @ApiProperty({ type: String }) + deployTxHash: string = ""; + + @Field(() => Float, { description: 'Timestamp for the given account.' }) + @ApiProperty({ type: Number }) + timestamp: number = 0; + + @Field(() => AccountAssets, { description: 'Assets for the given account.', nullable: true }) + @ApiProperty({ type: AccountAssets, nullable: true, description: 'Contract assets' }) + assets: AccountAssets | undefined = undefined; +} diff --git a/src/graphql/entities/account.detailed/account.detailed.resolver.ts b/src/graphql/entities/account.detailed/account.detailed.resolver.ts index 89544deef..92fa56ed9 100644 --- a/src/graphql/entities/account.detailed/account.detailed.resolver.ts +++ b/src/graphql/entities/account.detailed/account.detailed.resolver.ts @@ -124,9 +124,9 @@ export class AccountDetailedResolver extends AccountDetailedQuery { })); } - @ResolveField("contractAccount", () => [DeployedContract], { name: "contractAccount", description: "Contracts for the given detailed account.", nullable: true }) - public async getAccountContracts(@Args("input", { description: "Input to retrieve the given contracts for." }) input: GetFromAndSizeInput, @Parent() account: AccountDetailed) { - return await this.accountService.getAccountContracts( + @ResolveField("deploysAccount", () => [DeployedContract], { name: "deploysAccount", description: "Deploys for the given detailed account.", nullable: true }) + public async getAccountDeploys(@Args("input", { description: "Input to retrieve the given deploys for." }) input: GetFromAndSizeInput, @Parent() account: AccountDetailed) { + return await this.accountService.getAccountDeploys( new QueryPagination({ from: input.from, size: input.size, @@ -134,9 +134,9 @@ export class AccountDetailedResolver extends AccountDetailedQuery { ); } - @ResolveField("contractAccountCount", () => Float, { name: "contractAccountCount", description: "Contracts count for the given detailed account." }) - public async getAccountContractsCount(@Parent() account: AccountDetailed) { - return await this.accountService.getAccountContractsCount(account.address); + @ResolveField("deployAccountCount", () => Float, { name: "deployAccountCount", description: "Contracts count for the given detailed account." }) + public async getAccountDeploysCount(@Parent() account: AccountDetailed) { + return await this.accountService.getAccountDeploysCount(account.address); } @ResolveField("nftCollections", () => [NftCollectionAccountFlat], { name: "nftCollections", description: "NFT collections for the given detailed account.", nullable: true }) diff --git a/src/test/unit/services/accounts.spec.ts b/src/test/unit/services/accounts.spec.ts index a6d453bf1..31e3026e0 100644 --- a/src/test/unit/services/accounts.spec.ts +++ b/src/test/unit/services/accounts.spec.ts @@ -52,8 +52,8 @@ describe('Account Service', () => { getAccounts: jest.fn(), getAccountsCount: jest.fn(), getAccountsForAddresses: jest.fn(), - getAccountContracts: jest.fn(), - getAccountContractsCount: jest.fn(), + getAccountDeploys: jest.fn(), + getAccountDeploysCount: jest.fn(), getAccountHistory: jest.fn(), getAccountTokenHistory: jest.fn(), getAccountHistoryCount: jest.fn(), @@ -556,11 +556,11 @@ describe('Account Service', () => { const address = 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz'; const contractsCount = 5; - jest.spyOn(indexerService, 'getAccountContractsCount').mockResolvedValue(contractsCount); + jest.spyOn(indexerService, 'getAccountDeploysCount').mockResolvedValue(contractsCount); - const result = await service.getAccountContractsCount(address); + const result = await service.getAccountDeploysCount(address); - expect(indexerService.getAccountContractsCount).toHaveBeenCalledWith(address); + expect(indexerService.getAccountDeploysCount).toHaveBeenCalledWith(address); expect(result).toEqual(contractsCount); }); }); @@ -860,12 +860,12 @@ describe('Account Service', () => { }; it('should return the account contracts', async () => { - jest.spyOn(indexerService, 'getAccountContracts').mockResolvedValue(details); + jest.spyOn(indexerService, 'getAccountDeploys').mockResolvedValue(details); jest.spyOn(assetsService, 'getAllAccountAssets').mockResolvedValue(assets); - const result = await service.getAccountContracts(pagination, address); + const result = await service.getAccountDeploys(pagination, address); - expect(indexerService.getAccountContracts).toHaveBeenCalledWith(pagination, address); + expect(indexerService.getAccountDeploys).toHaveBeenCalledWith(pagination, address); expect(assetsService.getAllAccountAssets).toHaveBeenCalled(); const expectedAccounts = details.map(contract => ({