Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/common/gateway/entities/block-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class BlockInfo {
hash: string = '';
nonce: number = 0;
rootHash: string = '';

constructor(init?: Partial<BlockInfo>) {
Object.assign(this, init);
}
}
1 change: 1 addition & 0 deletions src/common/gateway/entities/gateway.component.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum GatewayComponentRequest {
addressEsdtHistorical = 'addressEsdtHistorical',
addressEsdtBalance = 'addressEsdtBalance',
addressNfts = 'addressNfts',
addressIterateKeys = 'addressIterateKeys',
nodeHeartbeat = 'nodeHeartbeat',
getNodeWaitingEpochsLeft = 'getNodeWaitingEpochsLeft',
validatorStatistics = 'validatorStatistics',
Expand Down
9 changes: 9 additions & 0 deletions src/common/gateway/entities/iterate-keys-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class IterateKeysRequest {
constructor(init?: Partial<IterateKeysRequest>) {
Object.assign(this, init);
}

address: string = '';
numKeys: number = 0;
iteratorState: string[] = [];
}
15 changes: 15 additions & 0 deletions src/common/gateway/entities/iterate-keys-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BlockInfo } from './block-info';

export class IterateKeysResponse {
blockInfo?: BlockInfo;
newIteratorState: string[] = [];
pairs: { [key: string]: string } = {};

constructor(init?: Partial<IterateKeysResponse>) {
Object.assign(this, init);

if (init?.blockInfo) {
this.blockInfo = new BlockInfo(init.blockInfo);
}
}
}
17 changes: 17 additions & 0 deletions src/common/gateway/gateway.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Auction } from "./entities/auction";
import { EsdtAddressRoles } from "./entities/esdt.roles";
import { EsdtSupply } from "./entities/esdt.supply";
import { GatewayComponentRequest } from "./entities/gateway.component.request";
import { IterateKeysRequest } from "./entities/iterate-keys-request";
import { IterateKeysResponse } from "./entities/iterate-keys-response";
import { MetricsEvents } from "src/utils/metrics-events.constants";
import { LogPerformanceAsync } from "src/utils/log.performance.decorator";
import { HeartbeatStatus } from "./entities/heartbeat.status";
Expand Down Expand Up @@ -39,6 +41,7 @@ export class GatewayService {
GatewayComponentRequest.addressEsdt,
GatewayComponentRequest.addressEsdtBalance,
GatewayComponentRequest.addressNftByNonce,
GatewayComponentRequest.addressIterateKeys,
GatewayComponentRequest.vmQuery,
]);

Expand Down Expand Up @@ -195,6 +198,20 @@ export class GatewayService {
return result.block;
}

async getAddressIterateKeys(request: IterateKeysRequest): Promise<IterateKeysResponse> {
// eslint-disable-next-line require-await
const result = await this.create('address/iterate-keys', GatewayComponentRequest.addressIterateKeys, request, async (error) => {
const errorMessage = error?.response?.data?.error;
if (errorMessage && errorMessage.includes('account was not found')) {
return true;
}

return false;
});

return new IterateKeysResponse(result);
}

@LogPerformanceAsync(MetricsEvents.SetGatewayDuration, { argIndex: 1 })
async get(url: string, component: GatewayComponentRequest, errorHandler?: (error: any) => Promise<boolean>): Promise<any> {
const result = await this.getRaw(url, component, errorHandler);
Expand Down
36 changes: 35 additions & 1 deletion src/endpoints/accounts/account.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Controller, DefaultValuePipe, Get, HttpException, HttpStatus, NotFoundException, Param, Query, UseInterceptors } from '@nestjs/common';
import { Controller, DefaultValuePipe, Get, HttpException, HttpStatus, NotFoundException, Param, Query, UseInterceptors, Post, Body } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { AccountService } from './account.service';
import { AccountDetailed } from './entities/account.detailed';
import { Account } from './entities/account';
import { AccountDeferred } from './entities/account.deferred';
import { IterateKeysRequestDto } from './entities/iterate-keys-request.dto';
import { IterateKeysResponseDto } from './entities/iterate-keys-response.dto';
import { TokenService } from '../tokens/token.service';
import { TokenWithBalance } from '../tokens/entities/token.with.balance';
import { DelegationLegacyService } from '../delegation.legacy/delegation.legacy.service';
Expand Down Expand Up @@ -1428,4 +1430,36 @@ export class AccountController {
new QueryPagination({ from, size }),
new AccountHistoryFilter({ before, after }));
}

@Post("/accounts/:address/iterate-keys")
@UseInterceptors(DeepHistoryInterceptor)
@ApiOperation({
summary: 'Iterate account storage keys',
description: 'Returns paginated account storage keys with state-based iteration. Supports deep history via timestamp query parameter.',
})
@ApiQuery({
name: 'timestamp',
description: 'Retrieve keys from specific timestamp (requires deep history)',
required: false,
type: Number,
})
@ApiOkResponse({ type: IterateKeysResponseDto })
async getAccountIterateKeys(
@Param('address', ParseAddressPipe) address: string,
@Body() request: IterateKeysRequestDto,
@Query('timestamp', ParseIntPipe) _timestamp?: number,
): Promise<IterateKeysResponseDto> {
try {
const result = await this.accountService.getIterateKeys(address, request);
return {
blockInfo: result.blockInfo,
newIteratorState: result.newIteratorState,
pairs: result.pairs,
};
} catch (error) {
this.logger.error(`Error in getAccountIterateKeys for address ${address}`);
this.logger.error(error);
throw new HttpException('Failed to iterate keys', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
13 changes: 13 additions & 0 deletions src/endpoints/accounts/account.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { forwardRef, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { AccountDetailed } from './entities/account.detailed';
import { Account } from './entities/account';
import { IterateKeysRequestDto } from './entities/iterate-keys-request.dto';
import { IterateKeysResponse } from 'src/common/gateway/entities/iterate-keys-response';
import { IterateKeysRequest } from 'src/common/gateway/entities/iterate-keys-request';
import { VmQueryService } from 'src/endpoints/vm.query/vm.query.service';
import { ApiConfigService } from 'src/common/api-config/api.config.service';
import { AccountDeferred } from './entities/account.deferred';
Expand Down Expand Up @@ -745,4 +748,14 @@ export class AccountService {
transfers24H: item.value,
}));
}

async getIterateKeys(address: string, request: IterateKeysRequestDto): Promise<IterateKeysResponse> {
const gatewayRequest = new IterateKeysRequest({
address,
numKeys: request.numKeys,
iteratorState: request.iteratorState,
});

return await this.gatewayService.getAddressIterateKeys(gatewayRequest);
}
}
17 changes: 17 additions & 0 deletions src/endpoints/accounts/entities/iterate-keys-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';

export class IterateKeysRequestDto {
@ApiProperty({
description: 'Number of keys to retrieve. Set to 0 to retrieve keys until timeout is reached.',
example: 10,
minimum: 0,
})
numKeys: number = 0;

@ApiProperty({
description: 'Iterator state for pagination. Empty array for the first request, use returned newIteratorState for subsequent requests.',
example: [],
type: [String],
})
iteratorState: string[] = [];
}
34 changes: 34 additions & 0 deletions src/endpoints/accounts/entities/iterate-keys-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';

export class BlockInfoDto {
@ApiProperty({ description: 'Block hash' })
hash: string = '';

@ApiProperty({ description: 'Block nonce' })
nonce: number = 0;

@ApiProperty({ description: 'Block root hash' })
rootHash: string = '';
}

export class IterateKeysResponseDto {
@ApiProperty({
description: 'Block information for consistency guarantees',
type: BlockInfoDto,
required: false,
})
blockInfo?: BlockInfoDto;

@ApiProperty({
description: 'Iterator state for the next request. Empty array indicates no more keys available.',
type: [String],
})
newIteratorState: string[] = [];

@ApiProperty({
description: 'Key-value pairs from the account storage. Keys and values are hex-encoded.',
type: 'object',
additionalProperties: { type: 'string' },
})
pairs: { [key: string]: string } = {};
}
14 changes: 14 additions & 0 deletions src/endpoints/proxy/gateway.proxy.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CacheService, NoCache } from "@multiversx/sdk-nestjs-cache";
import { OriginLogger } from "@multiversx/sdk-nestjs-common";
import { DeepHistoryInterceptor } from "src/interceptors/deep-history.interceptor";
import { DisableFieldsInterceptorOnController } from "@multiversx/sdk-nestjs-http";
import { IterateKeysRequest } from "src/common/gateway/entities/iterate-keys-request";

@Controller()
@ApiTags('proxy')
Expand Down Expand Up @@ -91,6 +92,19 @@ export class GatewayProxyController {
});
}

@Post('/address/iterate-keys')
async iterateKeys(@Body() request: IterateKeysRequest) {
// eslint-disable-next-line require-await
return await this.gatewayPost('address/iterate-keys', GatewayComponentRequest.addressIterateKeys, request, async (error) => {
const errorMessage = error?.response?.data?.error;
if (errorMessage && errorMessage.includes('account was not found')) {
throw error;
}

return false;
});
}

@Post('/transaction/send')
async transactionSend(@Body() body: any) {
if (!body.sender) {
Expand Down