From 5239843c0b6b89b8c1176501acf637ef467b01e6 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 20 Aug 2025 16:47:55 +0300 Subject: [PATCH 01/90] add websockets for blocks and txs --- config/config.devnet.yaml | 2 +- package-lock.json | 93 ++++++++++++++++++ package.json | 2 + src/crons/websocket/websocket.cron.service.ts | 24 +++++ src/crons/websocket/websocket.crons.module.ts | 17 ++++ src/endpoints/blocks/block.module.ts | 5 +- src/endpoints/blocks/blocks.gateway.ts | 72 ++++++++++++++ .../transactions/transaction.gateway.ts | 98 +++++++++++++++++++ .../transactions/transaction.module.ts | 5 +- src/main.ts | 2 + src/public.app.module.ts | 2 + 11 files changed, 317 insertions(+), 5 deletions(-) create mode 100644 src/crons/websocket/websocket.cron.service.ts create mode 100644 src/crons/websocket/websocket.crons.module.ts create mode 100644 src/endpoints/blocks/blocks.gateway.ts create mode 100644 src/endpoints/transactions/transaction.gateway.ts diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 94a330119..7d42d5d61 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -5,7 +5,7 @@ api: publicPort: 3001 private: true privatePort: 4001 - websocket: true + websocket: false cron: cacheWarmer: true fastWarm: true diff --git a/package-lock.json b/package-lock.json index d8ed257b7..6be5f9318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,8 @@ "rxjs": "^7.1.0", "sharp": "^0.34.2", "simple-git": "^3.16.0", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", "swagger-ui-express": "^4.3.0", "tiny-async-pool": "^1.2.0", "typeorm": "^0.3.25", @@ -9287,6 +9289,57 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -14946,6 +14999,38 @@ } } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -16755,6 +16840,14 @@ } } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xss": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", diff --git a/package.json b/package.json index d9bc0daf4..15542773f 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,8 @@ "rxjs": "^7.1.0", "sharp": "^0.34.2", "simple-git": "^3.16.0", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", "swagger-ui-express": "^4.3.0", "tiny-async-pool": "^1.2.0", "typeorm": "^0.3.25", diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts new file mode 100644 index 000000000..fa5401a91 --- /dev/null +++ b/src/crons/websocket/websocket.cron.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { TransactionsGateway } from '../../endpoints/transactions/transaction.gateway'; +import { BlocksGateway } from 'src/endpoints/blocks/blocks.gateway'; + +@Injectable() +export class WebsocketCronService { + constructor( + private readonly transactionsGateway: TransactionsGateway, + private readonly blocksGateway: BlocksGateway, + ) { } + + @Cron('*/6 * * * * *') + async handleTransactionsUpdate() { + console.log('executer websocket push transactions') + await this.transactionsGateway.pushTransactions(); + } + + @Cron('*/6 * * * * *') + async handleBlocksUpdate() { + console.log('executed websocket push blocks') + await this.blocksGateway.pushBlocks(); + } +} diff --git a/src/crons/websocket/websocket.crons.module.ts b/src/crons/websocket/websocket.crons.module.ts new file mode 100644 index 000000000..1e2b25946 --- /dev/null +++ b/src/crons/websocket/websocket.crons.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { TransactionModule } from 'src/endpoints/transactions/transaction.module'; +import { WebsocketCronService } from './websocket.cron.service'; +import { BlockModule } from 'src/endpoints/blocks/block.module'; + +@Module({ + imports: [ + ScheduleModule.forRoot(), + TransactionModule, + BlockModule, + ], + providers: [ + WebsocketCronService, + ], +}) +export class WebSocketCronModule { } diff --git a/src/endpoints/blocks/block.module.ts b/src/endpoints/blocks/block.module.ts index 24d9a8191..df5a5ca87 100644 --- a/src/endpoints/blocks/block.module.ts +++ b/src/endpoints/blocks/block.module.ts @@ -3,6 +3,7 @@ import { BlsModule } from "../bls/bls.module"; import { IdentitiesModule } from "../identities/identities.module"; import { NodeModule } from "../nodes/node.module"; import { BlockService } from "./block.service"; +import { BlocksGateway } from "./blocks.gateway"; @Module({ imports: [ @@ -11,10 +12,10 @@ import { BlockService } from "./block.service"; forwardRef(() => IdentitiesModule), ], providers: [ - BlockService, + BlockService, BlocksGateway, ], exports: [ - BlockService, + BlockService, BlocksGateway, ], }) export class BlockModule { } diff --git a/src/endpoints/blocks/blocks.gateway.ts b/src/endpoints/blocks/blocks.gateway.ts new file mode 100644 index 000000000..dbdf3815a --- /dev/null +++ b/src/endpoints/blocks/blocks.gateway.ts @@ -0,0 +1,72 @@ +import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect } from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; +import { BlockService } from './block.service'; +import { BlockFilter } from './entities/block.filter'; +import { QueryPagination } from 'src/common/entities/query.pagination'; + +@WebSocketGateway({ cors: { origin: '*' } }) +export class BlocksGateway implements OnGatewayDisconnect { + @WebSocketServer() + server!: Server; + + // Map: filterHash -> set of clientIds + private filterClients = new Map>(); + // Map: clientId -> filterHash + private clientFilterHash = new Map(); + + constructor(private readonly blockService: BlockService) { } + + @SubscribeMessage('subscribeBlocks') + async handleSubscription(client: Socket, payload: any) { + const filterHash = JSON.stringify(payload); + + if (!this.filterClients.has(filterHash)) { + this.filterClients.set(filterHash, new Set()); + } + this.filterClients.get(filterHash)!.add(client.id); + this.clientFilterHash.set(client.id, filterHash); + } + + async pushBlocks() { + for (const [filterHash, clientIds] of this.filterClients.entries()) { + const filter = JSON.parse(filterHash); + + const blockFilter = new BlockFilter({ + shard: filter.shard, + proposer: filter.proposer, + validator: filter.validator, + epoch: filter.epoch, + nonce: filter.nonce, + hashes: filter.hashes, + order: filter.order, + }); + + const txs = await this.blockService.getBlocks( + blockFilter, + new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), + filter.withProposerIdentity, + ); + + for (const clientId of clientIds) { + const client = this.server.sockets.sockets.get(clientId); + if (client) { + client.emit('blocksUpdate', txs); + } + } + } + } + + handleDisconnect(client: Socket) { + const filterHash = this.clientFilterHash.get(client.id); + if (filterHash) { + const set = this.filterClients.get(filterHash); + if (set) { + set.delete(client.id); + if (set.size === 0) { + this.filterClients.delete(filterHash); + } + } + this.clientFilterHash.delete(client.id); + } + } +} diff --git a/src/endpoints/transactions/transaction.gateway.ts b/src/endpoints/transactions/transaction.gateway.ts new file mode 100644 index 000000000..d2c2cfe9f --- /dev/null +++ b/src/endpoints/transactions/transaction.gateway.ts @@ -0,0 +1,98 @@ +import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect } from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; +import { TransactionService } from './transaction.service'; +import { TransactionFilter } from './entities/transaction.filter'; +import { QueryPagination } from 'src/common/entities/query.pagination'; +import { TransactionQueryOptions } from './entities/transactions.query.options'; + +@WebSocketGateway({ cors: { origin: '*' } }) +export class TransactionsGateway implements OnGatewayDisconnect { + @WebSocketServer() + server!: Server; + + // Map: filterHash -> set of clientIds + private filterClients = new Map>(); + // Map: clientId -> filterHash + private clientFilterHash = new Map(); + + constructor(private readonly transactionService: TransactionService) { } + + @SubscribeMessage('subscribeTransactions') + async handleSubscription(client: Socket, payload: any) { + const filterHash = JSON.stringify(payload); + + if (!this.filterClients.has(filterHash)) { + this.filterClients.set(filterHash, new Set()); + } + this.filterClients.get(filterHash)!.add(client.id); + this.clientFilterHash.set(client.id, filterHash); + } + + async pushTransactions() { + for (const [filterHash, clientIds] of this.filterClients.entries()) { + const filter = JSON.parse(filterHash); + + const options = TransactionQueryOptions.applyDefaultOptions(filter.size || 25, { + withScResults: filter.withScResults, + withOperations: filter.withOperations, + withLogs: filter.withLogs, + withScamInfo: filter.withScamInfo, + withUsername: filter.withUsername, + withBlockInfo: filter.withBlockInfo, + withActionTransferValue: filter.withActionTransferValue, + }); + + const transactionFilter = new TransactionFilter({ + sender: filter.sender, + receivers: filter.receiver, + token: filter.token, + functions: filter.functions, + senderShard: filter.senderShard, + receiverShard: filter.receiverShard, + miniBlockHash: filter.miniBlockHash, + hashes: filter.hashes, + status: filter.status, + before: filter.before, + after: filter.after, + condition: filter.condition, + order: filter.order, + relayer: filter.relayer, + isRelayed: filter.isRelayed, + isScCall: filter.isScCall, + round: filter.round, + withRelayedScresults: filter.withRelayedScresults, + }); + + TransactionFilter.validate(transactionFilter, filter.size || 25); + + const txs = await this.transactionService.getTransactions( + transactionFilter, + new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), + options, + undefined, + filter.fields || [], + ); + + for (const clientId of clientIds) { + const client = this.server.sockets.sockets.get(clientId); + if (client) { + client.emit('transactionUpdate', txs); + } + } + } + } + + handleDisconnect(client: Socket) { + const filterHash = this.clientFilterHash.get(client.id); + if (filterHash) { + const set = this.filterClients.get(filterHash); + if (set) { + set.delete(client.id); + if (set.size === 0) { + this.filterClients.delete(filterHash); + } + } + this.clientFilterHash.delete(client.id); + } + } +} diff --git a/src/endpoints/transactions/transaction.module.ts b/src/endpoints/transactions/transaction.module.ts index f5fbf6cfd..09f7dce9f 100644 --- a/src/endpoints/transactions/transaction.module.ts +++ b/src/endpoints/transactions/transaction.module.ts @@ -11,6 +11,7 @@ import { TransactionActionModule } from "./transaction-action/transaction.action import { TransactionGetService } from "./transaction.get.service"; import { TransactionPriceService } from "./transaction.price.service"; import { TransactionService } from "./transaction.service"; +import { TransactionsGateway } from "./transaction.gateway"; @Module({ imports: [ @@ -25,10 +26,10 @@ import { TransactionService } from "./transaction.service"; DataApiModule, ], providers: [ - TransactionGetService, TransactionPriceService, TransactionService, + TransactionGetService, TransactionPriceService, TransactionService, TransactionsGateway ], exports: [ - TransactionGetService, TransactionPriceService, TransactionService, + TransactionGetService, TransactionPriceService, TransactionService, TransactionsGateway ], }) export class TransactionModule { } diff --git a/src/main.ts b/src/main.ts index 3ff1d98de..b31cb34a6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -36,6 +36,7 @@ import { NotWritableError } from './common/indexer/entities/not.writable.error'; import * as bodyParser from 'body-parser'; import * as requestIp from 'request-ip'; import compression from 'compression'; +import { IoAdapter } from '@nestjs/platform-socket.io'; async function bootstrap() { const logger = new Logger('Bootstrapper'); @@ -52,6 +53,7 @@ async function bootstrap() { if (apiConfigService.getIsPublicApiActive()) { const publicApp = await NestFactory.create(PublicAppModule); + publicApp.useWebSocketAdapter(new IoAdapter(publicApp)); await configurePublicApp(publicApp, apiConfigService); diff --git a/src/public.app.module.ts b/src/public.app.module.ts index 426717e6d..b28d53b7c 100644 --- a/src/public.app.module.ts +++ b/src/public.app.module.ts @@ -9,6 +9,7 @@ import { GuestCacheService } from '@multiversx/sdk-nestjs-cache'; import { LoggingModule } from '@multiversx/sdk-nestjs-common'; import { DynamicModuleUtils } from './utils/dynamic.module.utils'; import { LocalCacheController } from './endpoints/caching/local.cache.controller'; +import { WebSocketCronModule } from './crons/websocket/websocket.crons.module'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { LocalCacheController } from './endpoints/caching/local.cache.controller EndpointsServicesModule, EndpointsControllersModule.forRoot(), DynamicModuleUtils.getRedisCacheModule(), + WebSocketCronModule, ], controllers: [ LocalCacheController, From 6be90b6acdae43353547b016e08afbbf1d1c571b Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 21 Aug 2025 17:46:56 +0300 Subject: [PATCH 02/90] add support for subscribe to stats --- src/crons/websocket/websocket.cron.service.ts | 8 +++++ src/crons/websocket/websocket.crons.module.ts | 2 ++ src/endpoints/network/network.gateway.ts | 33 +++++++++++++++++++ src/endpoints/network/network.module.ts | 5 +-- 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/endpoints/network/network.gateway.ts diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index fa5401a91..20f29070e 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -2,12 +2,14 @@ import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { TransactionsGateway } from '../../endpoints/transactions/transaction.gateway'; import { BlocksGateway } from 'src/endpoints/blocks/blocks.gateway'; +import { NetworkGateway } from 'src/endpoints/network/network.gateway'; @Injectable() export class WebsocketCronService { constructor( private readonly transactionsGateway: TransactionsGateway, private readonly blocksGateway: BlocksGateway, + private readonly networkGateway: NetworkGateway, ) { } @Cron('*/6 * * * * *') @@ -21,4 +23,10 @@ export class WebsocketCronService { console.log('executed websocket push blocks') await this.blocksGateway.pushBlocks(); } + + @Cron('*/6 * * * * *') + async handleStatsUpdate() { + console.log('executed websocket push stats') + await this.networkGateway.pushStats(); + } } diff --git a/src/crons/websocket/websocket.crons.module.ts b/src/crons/websocket/websocket.crons.module.ts index 1e2b25946..f3c9ddef3 100644 --- a/src/crons/websocket/websocket.crons.module.ts +++ b/src/crons/websocket/websocket.crons.module.ts @@ -3,12 +3,14 @@ import { ScheduleModule } from '@nestjs/schedule'; import { TransactionModule } from 'src/endpoints/transactions/transaction.module'; import { WebsocketCronService } from './websocket.cron.service'; import { BlockModule } from 'src/endpoints/blocks/block.module'; +import { NetworkModule } from 'src/endpoints/network/network.module'; @Module({ imports: [ ScheduleModule.forRoot(), TransactionModule, BlockModule, + NetworkModule, ], providers: [ WebsocketCronService, diff --git a/src/endpoints/network/network.gateway.ts b/src/endpoints/network/network.gateway.ts new file mode 100644 index 000000000..a9ea49ff1 --- /dev/null +++ b/src/endpoints/network/network.gateway.ts @@ -0,0 +1,33 @@ +import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect } from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; +import { NetworkService } from './network.service'; + +@WebSocketGateway({ cors: { origin: '*' } }) +export class NetworkGateway implements OnGatewayDisconnect { + @WebSocketServer() + server!: Server; + + private clients = new Set(); + + constructor(private readonly networkService: NetworkService) { } + + @SubscribeMessage('subscribeStats') + async handleSubscription(client: Socket) { + this.clients.add(client.id); + } + + async pushStats() { + const stats = await this.networkService.getStats(); + + for (const clientId of this.clients) { + const client = this.server.sockets.sockets.get(clientId); + if (client) { + client.emit('statsUpdate', stats); + } + } + } + + handleDisconnect(client: Socket) { + this.clients.delete(client.id); + } +} diff --git a/src/endpoints/network/network.module.ts b/src/endpoints/network/network.module.ts index 42671fdd0..246488f47 100644 --- a/src/endpoints/network/network.module.ts +++ b/src/endpoints/network/network.module.ts @@ -8,6 +8,7 @@ import { TokenModule } from "../tokens/token.module"; import { TransactionModule } from "../transactions/transaction.module"; import { VmQueryModule } from "../vm.query/vm.query.module"; import { NetworkService } from "./network.service"; +import { NetworkGateway } from "./network.gateway"; @Module({ imports: [ @@ -21,10 +22,10 @@ import { NetworkService } from "./network.service"; forwardRef(() => SmartContractResultModule), ], providers: [ - NetworkService, + NetworkService, NetworkGateway, ], exports: [ - NetworkService, + NetworkService, NetworkGateway, ], }) export class NetworkModule { } From 144250930ccbe0fc7c1d6a6c91482b595dab5e83 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 22 Aug 2025 10:54:10 +0300 Subject: [PATCH 03/90] use websockets rooms --- src/endpoints/blocks/blocks.gateway.ts | 38 ++++--------------- src/endpoints/network/network.gateway.ts | 14 ++----- .../transactions/transaction.gateway.ts | 36 ++++-------------- 3 files changed, 18 insertions(+), 70 deletions(-) diff --git a/src/endpoints/blocks/blocks.gateway.ts b/src/endpoints/blocks/blocks.gateway.ts index dbdf3815a..ff46ee255 100644 --- a/src/endpoints/blocks/blocks.gateway.ts +++ b/src/endpoints/blocks/blocks.gateway.ts @@ -9,26 +9,19 @@ export class BlocksGateway implements OnGatewayDisconnect { @WebSocketServer() server!: Server; - // Map: filterHash -> set of clientIds - private filterClients = new Map>(); - // Map: clientId -> filterHash - private clientFilterHash = new Map(); - constructor(private readonly blockService: BlockService) { } @SubscribeMessage('subscribeBlocks') async handleSubscription(client: Socket, payload: any) { const filterHash = JSON.stringify(payload); - - if (!this.filterClients.has(filterHash)) { - this.filterClients.set(filterHash, new Set()); - } - this.filterClients.get(filterHash)!.add(client.id); - this.clientFilterHash.set(client.id, filterHash); + await client.join(`block-${filterHash}`); } async pushBlocks() { - for (const [filterHash, clientIds] of this.filterClients.entries()) { + for (const [roomName] of this.server.sockets.adapter.rooms) { + if (!roomName.startsWith("block-")) continue; + + const filterHash = roomName.replace("block-", ""); const filter = JSON.parse(filterHash); const blockFilter = new BlockFilter({ @@ -41,32 +34,17 @@ export class BlocksGateway implements OnGatewayDisconnect { order: filter.order, }); - const txs = await this.blockService.getBlocks( + const blocks = await this.blockService.getBlocks( blockFilter, new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), filter.withProposerIdentity, ); - for (const clientId of clientIds) { - const client = this.server.sockets.sockets.get(clientId); - if (client) { - client.emit('blocksUpdate', txs); - } - } + this.server.to(roomName).emit('blocksUpdate', blocks); } } handleDisconnect(client: Socket) { - const filterHash = this.clientFilterHash.get(client.id); - if (filterHash) { - const set = this.filterClients.get(filterHash); - if (set) { - set.delete(client.id); - if (set.size === 0) { - this.filterClients.delete(filterHash); - } - } - this.clientFilterHash.delete(client.id); - } + console.log(`client ${client.id} disconnected`); } } diff --git a/src/endpoints/network/network.gateway.ts b/src/endpoints/network/network.gateway.ts index a9ea49ff1..ff775bee9 100644 --- a/src/endpoints/network/network.gateway.ts +++ b/src/endpoints/network/network.gateway.ts @@ -7,27 +7,19 @@ export class NetworkGateway implements OnGatewayDisconnect { @WebSocketServer() server!: Server; - private clients = new Set(); - constructor(private readonly networkService: NetworkService) { } @SubscribeMessage('subscribeStats') async handleSubscription(client: Socket) { - this.clients.add(client.id); + await client.join('statsRoom'); } async pushStats() { const stats = await this.networkService.getStats(); - - for (const clientId of this.clients) { - const client = this.server.sockets.sockets.get(clientId); - if (client) { - client.emit('statsUpdate', stats); - } - } + this.server.to('statsRoom').emit('statsUpdate', stats); } handleDisconnect(client: Socket) { - this.clients.delete(client.id); + console.log(`client ${client.id} disconnected`); } } diff --git a/src/endpoints/transactions/transaction.gateway.ts b/src/endpoints/transactions/transaction.gateway.ts index d2c2cfe9f..0fecedc85 100644 --- a/src/endpoints/transactions/transaction.gateway.ts +++ b/src/endpoints/transactions/transaction.gateway.ts @@ -10,26 +10,19 @@ export class TransactionsGateway implements OnGatewayDisconnect { @WebSocketServer() server!: Server; - // Map: filterHash -> set of clientIds - private filterClients = new Map>(); - // Map: clientId -> filterHash - private clientFilterHash = new Map(); - constructor(private readonly transactionService: TransactionService) { } @SubscribeMessage('subscribeTransactions') async handleSubscription(client: Socket, payload: any) { const filterHash = JSON.stringify(payload); - - if (!this.filterClients.has(filterHash)) { - this.filterClients.set(filterHash, new Set()); - } - this.filterClients.get(filterHash)!.add(client.id); - this.clientFilterHash.set(client.id, filterHash); + await client.join(`tx-${filterHash}`); } async pushTransactions() { - for (const [filterHash, clientIds] of this.filterClients.entries()) { + for (const [roomName] of this.server.sockets.adapter.rooms) { + if (!roomName.startsWith("tx-")) continue; + + const filterHash = roomName.replace("tx-", ""); const filter = JSON.parse(filterHash); const options = TransactionQueryOptions.applyDefaultOptions(filter.size || 25, { @@ -73,26 +66,11 @@ export class TransactionsGateway implements OnGatewayDisconnect { filter.fields || [], ); - for (const clientId of clientIds) { - const client = this.server.sockets.sockets.get(clientId); - if (client) { - client.emit('transactionUpdate', txs); - } - } + this.server.to(roomName).emit('transactionUpdate', txs); } } handleDisconnect(client: Socket) { - const filterHash = this.clientFilterHash.get(client.id); - if (filterHash) { - const set = this.filterClients.get(filterHash); - if (set) { - set.delete(client.id); - if (set.size === 0) { - this.filterClients.delete(filterHash); - } - } - this.clientFilterHash.delete(client.id); - } + console.log(`client ${client.id} disconnected`); } } From 7d322eeac1af6b029d6ebd3f5ce1e7470d5ac04f Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 22 Aug 2025 10:57:53 +0300 Subject: [PATCH 04/90] remove logs --- src/crons/websocket/websocket.cron.service.ts | 3 --- src/endpoints/blocks/blocks.gateway.ts | 4 +--- src/endpoints/network/network.gateway.ts | 4 +--- src/endpoints/transactions/transaction.gateway.ts | 4 +--- src/endpoints/transactions/transaction.module.ts | 4 ++-- 5 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index 20f29070e..9b21fdc1c 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -14,19 +14,16 @@ export class WebsocketCronService { @Cron('*/6 * * * * *') async handleTransactionsUpdate() { - console.log('executer websocket push transactions') await this.transactionsGateway.pushTransactions(); } @Cron('*/6 * * * * *') async handleBlocksUpdate() { - console.log('executed websocket push blocks') await this.blocksGateway.pushBlocks(); } @Cron('*/6 * * * * *') async handleStatsUpdate() { - console.log('executed websocket push stats') await this.networkGateway.pushStats(); } } diff --git a/src/endpoints/blocks/blocks.gateway.ts b/src/endpoints/blocks/blocks.gateway.ts index ff46ee255..531f1cb24 100644 --- a/src/endpoints/blocks/blocks.gateway.ts +++ b/src/endpoints/blocks/blocks.gateway.ts @@ -44,7 +44,5 @@ export class BlocksGateway implements OnGatewayDisconnect { } } - handleDisconnect(client: Socket) { - console.log(`client ${client.id} disconnected`); - } + handleDisconnect(_client: Socket) { } } diff --git a/src/endpoints/network/network.gateway.ts b/src/endpoints/network/network.gateway.ts index ff775bee9..9cf24cfd1 100644 --- a/src/endpoints/network/network.gateway.ts +++ b/src/endpoints/network/network.gateway.ts @@ -19,7 +19,5 @@ export class NetworkGateway implements OnGatewayDisconnect { this.server.to('statsRoom').emit('statsUpdate', stats); } - handleDisconnect(client: Socket) { - console.log(`client ${client.id} disconnected`); - } + handleDisconnect(_client: Socket) { } } diff --git a/src/endpoints/transactions/transaction.gateway.ts b/src/endpoints/transactions/transaction.gateway.ts index 0fecedc85..a725ccd51 100644 --- a/src/endpoints/transactions/transaction.gateway.ts +++ b/src/endpoints/transactions/transaction.gateway.ts @@ -70,7 +70,5 @@ export class TransactionsGateway implements OnGatewayDisconnect { } } - handleDisconnect(client: Socket) { - console.log(`client ${client.id} disconnected`); - } + handleDisconnect(_client: Socket) { } } diff --git a/src/endpoints/transactions/transaction.module.ts b/src/endpoints/transactions/transaction.module.ts index 09f7dce9f..b2eaa7f44 100644 --- a/src/endpoints/transactions/transaction.module.ts +++ b/src/endpoints/transactions/transaction.module.ts @@ -26,10 +26,10 @@ import { TransactionsGateway } from "./transaction.gateway"; DataApiModule, ], providers: [ - TransactionGetService, TransactionPriceService, TransactionService, TransactionsGateway + TransactionGetService, TransactionPriceService, TransactionService, TransactionsGateway, ], exports: [ - TransactionGetService, TransactionPriceService, TransactionService, TransactionsGateway + TransactionGetService, TransactionPriceService, TransactionService, TransactionsGateway, ], }) export class TransactionModule { } From 62974c754b96fe8ff53e09a49df1b8f1589accad Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 22 Aug 2025 11:21:33 +0300 Subject: [PATCH 05/90] add lock on crons --- src/crons/websocket/websocket.cron.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index 9b21fdc1c..b0562f6ad 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -3,7 +3,7 @@ import { Cron } from '@nestjs/schedule'; import { TransactionsGateway } from '../../endpoints/transactions/transaction.gateway'; import { BlocksGateway } from 'src/endpoints/blocks/blocks.gateway'; import { NetworkGateway } from 'src/endpoints/network/network.gateway'; - +import { Lock } from "@multiversx/sdk-nestjs-common"; @Injectable() export class WebsocketCronService { constructor( @@ -13,16 +13,19 @@ export class WebsocketCronService { ) { } @Cron('*/6 * * * * *') + @Lock({ name: 'Push transactions to subscribers', verbose: true }) async handleTransactionsUpdate() { await this.transactionsGateway.pushTransactions(); } @Cron('*/6 * * * * *') + @Lock({ name: 'Push blocks to subscribers', verbose: true }) async handleBlocksUpdate() { await this.blocksGateway.pushBlocks(); } @Cron('*/6 * * * * *') + @Lock({ name: 'Push stats to subscribers', verbose: true }) async handleStatsUpdate() { await this.networkGateway.pushStats(); } From 8006d5edfc1435c145a84f45f72820a344f7f265 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 22 Aug 2025 11:24:52 +0300 Subject: [PATCH 06/90] check stats room exists Signed-off-by: GuticaStefan --- src/endpoints/network/network.gateway.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/endpoints/network/network.gateway.ts b/src/endpoints/network/network.gateway.ts index 9cf24cfd1..a4d523f57 100644 --- a/src/endpoints/network/network.gateway.ts +++ b/src/endpoints/network/network.gateway.ts @@ -15,8 +15,10 @@ export class NetworkGateway implements OnGatewayDisconnect { } async pushStats() { - const stats = await this.networkService.getStats(); - this.server.to('statsRoom').emit('statsUpdate', stats); + if (this.server.sockets.adapter.rooms.has('statsRoom')) { + const stats = await this.networkService.getStats(); + this.server.to('statsRoom').emit('statsUpdate', stats); + } } handleDisconnect(_client: Socket) { } From d09f688fcf3dfadb6906824d4d82fcb74defb524 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 22 Aug 2025 11:45:54 +0300 Subject: [PATCH 07/90] fix indent spaces --- src/crons/websocket/websocket.cron.service.ts | 40 +++---- src/endpoints/blocks/blocks.gateway.ts | 74 ++++++------ src/endpoints/network/network.gateway.ts | 26 ++--- .../transactions/transaction.gateway.ts | 106 +++++++++--------- 4 files changed, 123 insertions(+), 123 deletions(-) diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index b0562f6ad..5ebcc5e7c 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -6,27 +6,27 @@ import { NetworkGateway } from 'src/endpoints/network/network.gateway'; import { Lock } from "@multiversx/sdk-nestjs-common"; @Injectable() export class WebsocketCronService { - constructor( - private readonly transactionsGateway: TransactionsGateway, - private readonly blocksGateway: BlocksGateway, - private readonly networkGateway: NetworkGateway, - ) { } + constructor( + private readonly transactionsGateway: TransactionsGateway, + private readonly blocksGateway: BlocksGateway, + private readonly networkGateway: NetworkGateway, + ) { } - @Cron('*/6 * * * * *') - @Lock({ name: 'Push transactions to subscribers', verbose: true }) - async handleTransactionsUpdate() { - await this.transactionsGateway.pushTransactions(); - } + @Cron('*/6 * * * * *') + @Lock({ name: 'Push transactions to subscribers', verbose: true }) + async handleTransactionsUpdate() { + await this.transactionsGateway.pushTransactions(); + } - @Cron('*/6 * * * * *') - @Lock({ name: 'Push blocks to subscribers', verbose: true }) - async handleBlocksUpdate() { - await this.blocksGateway.pushBlocks(); - } + @Cron('*/6 * * * * *') + @Lock({ name: 'Push blocks to subscribers', verbose: true }) + async handleBlocksUpdate() { + await this.blocksGateway.pushBlocks(); + } - @Cron('*/6 * * * * *') - @Lock({ name: 'Push stats to subscribers', verbose: true }) - async handleStatsUpdate() { - await this.networkGateway.pushStats(); - } + @Cron('*/6 * * * * *') + @Lock({ name: 'Push stats to subscribers', verbose: true }) + async handleStatsUpdate() { + await this.networkGateway.pushStats(); + } } diff --git a/src/endpoints/blocks/blocks.gateway.ts b/src/endpoints/blocks/blocks.gateway.ts index 531f1cb24..5c55d1d1d 100644 --- a/src/endpoints/blocks/blocks.gateway.ts +++ b/src/endpoints/blocks/blocks.gateway.ts @@ -6,43 +6,43 @@ import { QueryPagination } from 'src/common/entities/query.pagination'; @WebSocketGateway({ cors: { origin: '*' } }) export class BlocksGateway implements OnGatewayDisconnect { - @WebSocketServer() - server!: Server; - - constructor(private readonly blockService: BlockService) { } - - @SubscribeMessage('subscribeBlocks') - async handleSubscription(client: Socket, payload: any) { - const filterHash = JSON.stringify(payload); - await client.join(`block-${filterHash}`); - } - - async pushBlocks() { - for (const [roomName] of this.server.sockets.adapter.rooms) { - if (!roomName.startsWith("block-")) continue; - - const filterHash = roomName.replace("block-", ""); - const filter = JSON.parse(filterHash); - - const blockFilter = new BlockFilter({ - shard: filter.shard, - proposer: filter.proposer, - validator: filter.validator, - epoch: filter.epoch, - nonce: filter.nonce, - hashes: filter.hashes, - order: filter.order, - }); - - const blocks = await this.blockService.getBlocks( - blockFilter, - new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), - filter.withProposerIdentity, - ); - - this.server.to(roomName).emit('blocksUpdate', blocks); - } + @WebSocketServer() + server!: Server; + + constructor(private readonly blockService: BlockService) { } + + @SubscribeMessage('subscribeBlocks') + async handleSubscription(client: Socket, payload: any) { + const filterHash = JSON.stringify(payload); + await client.join(`block-${filterHash}`); + } + + async pushBlocks() { + for (const [roomName] of this.server.sockets.adapter.rooms) { + if (!roomName.startsWith("block-")) continue; + + const filterHash = roomName.replace("block-", ""); + const filter = JSON.parse(filterHash); + + const blockFilter = new BlockFilter({ + shard: filter.shard, + proposer: filter.proposer, + validator: filter.validator, + epoch: filter.epoch, + nonce: filter.nonce, + hashes: filter.hashes, + order: filter.order, + }); + + const blocks = await this.blockService.getBlocks( + blockFilter, + new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), + filter.withProposerIdentity, + ); + + this.server.to(roomName).emit('blocksUpdate', blocks); } + } - handleDisconnect(_client: Socket) { } + handleDisconnect(_client: Socket) { } } diff --git a/src/endpoints/network/network.gateway.ts b/src/endpoints/network/network.gateway.ts index a4d523f57..66d7b99c2 100644 --- a/src/endpoints/network/network.gateway.ts +++ b/src/endpoints/network/network.gateway.ts @@ -4,22 +4,22 @@ import { NetworkService } from './network.service'; @WebSocketGateway({ cors: { origin: '*' } }) export class NetworkGateway implements OnGatewayDisconnect { - @WebSocketServer() - server!: Server; + @WebSocketServer() + server!: Server; - constructor(private readonly networkService: NetworkService) { } + constructor(private readonly networkService: NetworkService) { } - @SubscribeMessage('subscribeStats') - async handleSubscription(client: Socket) { - await client.join('statsRoom'); - } + @SubscribeMessage('subscribeStats') + async handleSubscription(client: Socket) { + await client.join('statsRoom'); + } - async pushStats() { - if (this.server.sockets.adapter.rooms.has('statsRoom')) { - const stats = await this.networkService.getStats(); - this.server.to('statsRoom').emit('statsUpdate', stats); - } + async pushStats() { + if (this.server.sockets.adapter.rooms.has('statsRoom')) { + const stats = await this.networkService.getStats(); + this.server.to('statsRoom').emit('statsUpdate', stats); } + } - handleDisconnect(_client: Socket) { } + handleDisconnect(_client: Socket) { } } diff --git a/src/endpoints/transactions/transaction.gateway.ts b/src/endpoints/transactions/transaction.gateway.ts index a725ccd51..204f6f4af 100644 --- a/src/endpoints/transactions/transaction.gateway.ts +++ b/src/endpoints/transactions/transaction.gateway.ts @@ -7,68 +7,68 @@ import { TransactionQueryOptions } from './entities/transactions.query.options'; @WebSocketGateway({ cors: { origin: '*' } }) export class TransactionsGateway implements OnGatewayDisconnect { - @WebSocketServer() - server!: Server; + @WebSocketServer() + server!: Server; - constructor(private readonly transactionService: TransactionService) { } + constructor(private readonly transactionService: TransactionService) { } - @SubscribeMessage('subscribeTransactions') - async handleSubscription(client: Socket, payload: any) { - const filterHash = JSON.stringify(payload); - await client.join(`tx-${filterHash}`); - } + @SubscribeMessage('subscribeTransactions') + async handleSubscription(client: Socket, payload: any) { + const filterHash = JSON.stringify(payload); + await client.join(`tx-${filterHash}`); + } - async pushTransactions() { - for (const [roomName] of this.server.sockets.adapter.rooms) { - if (!roomName.startsWith("tx-")) continue; + async pushTransactions() { + for (const [roomName] of this.server.sockets.adapter.rooms) { + if (!roomName.startsWith("tx-")) continue; - const filterHash = roomName.replace("tx-", ""); - const filter = JSON.parse(filterHash); + const filterHash = roomName.replace("tx-", ""); + const filter = JSON.parse(filterHash); - const options = TransactionQueryOptions.applyDefaultOptions(filter.size || 25, { - withScResults: filter.withScResults, - withOperations: filter.withOperations, - withLogs: filter.withLogs, - withScamInfo: filter.withScamInfo, - withUsername: filter.withUsername, - withBlockInfo: filter.withBlockInfo, - withActionTransferValue: filter.withActionTransferValue, - }); + const options = TransactionQueryOptions.applyDefaultOptions(filter.size || 25, { + withScResults: filter.withScResults, + withOperations: filter.withOperations, + withLogs: filter.withLogs, + withScamInfo: filter.withScamInfo, + withUsername: filter.withUsername, + withBlockInfo: filter.withBlockInfo, + withActionTransferValue: filter.withActionTransferValue, + }); - const transactionFilter = new TransactionFilter({ - sender: filter.sender, - receivers: filter.receiver, - token: filter.token, - functions: filter.functions, - senderShard: filter.senderShard, - receiverShard: filter.receiverShard, - miniBlockHash: filter.miniBlockHash, - hashes: filter.hashes, - status: filter.status, - before: filter.before, - after: filter.after, - condition: filter.condition, - order: filter.order, - relayer: filter.relayer, - isRelayed: filter.isRelayed, - isScCall: filter.isScCall, - round: filter.round, - withRelayedScresults: filter.withRelayedScresults, - }); + const transactionFilter = new TransactionFilter({ + sender: filter.sender, + receivers: filter.receiver, + token: filter.token, + functions: filter.functions, + senderShard: filter.senderShard, + receiverShard: filter.receiverShard, + miniBlockHash: filter.miniBlockHash, + hashes: filter.hashes, + status: filter.status, + before: filter.before, + after: filter.after, + condition: filter.condition, + order: filter.order, + relayer: filter.relayer, + isRelayed: filter.isRelayed, + isScCall: filter.isScCall, + round: filter.round, + withRelayedScresults: filter.withRelayedScresults, + }); - TransactionFilter.validate(transactionFilter, filter.size || 25); + TransactionFilter.validate(transactionFilter, filter.size || 25); - const txs = await this.transactionService.getTransactions( - transactionFilter, - new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), - options, - undefined, - filter.fields || [], - ); + const txs = await this.transactionService.getTransactions( + transactionFilter, + new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), + options, + undefined, + filter.fields || [], + ); - this.server.to(roomName).emit('transactionUpdate', txs); - } + this.server.to(roomName).emit('transactionUpdate', txs); } + } - handleDisconnect(_client: Socket) { } + handleDisconnect(_client: Socket) { } } From 62fa1090f664cfed3ab8b918948a884c47ab1937 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 22 Aug 2025 14:40:34 +0300 Subject: [PATCH 08/90] add validation pipes + filters --- package-lock.json | 40 ++++++ package.json | 6 +- src/endpoints/blocks/blocks.gateway.ts | 16 ++- .../blocks/entities/block.subscribe.ts | 49 +++++++ src/endpoints/network/network.gateway.ts | 3 + .../entities/dtos/transaction.subscribe.ts | 127 ++++++++++++++++++ .../transactions/transaction.gateway.ts | 13 +- src/utils/ws-exceptions.filter.ts | 20 +++ src/utils/ws-validation.pipe.ts | 16 +++ 9 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 src/endpoints/blocks/entities/block.subscribe.ts create mode 100644 src/endpoints/transactions/entities/dtos/transaction.subscribe.ts create mode 100644 src/utils/ws-exceptions.filter.ts create mode 100644 src/utils/ws-validation.pipe.ts diff --git a/package-lock.json b/package-lock.json index 6be5f9318..f964e0d74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,8 @@ "apollo-server-core": "^3.13.0", "apollo-server-express": "3.13.0", "bignumber.js": "^9.0.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", "compression": "^1.8.0", "crypto-js": "^4.1.1", "dataloader": "^2.2.2", @@ -6312,6 +6314,12 @@ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "license": "MIT" }, + "node_modules/@types/validator": { + "version": "13.15.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", + "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -8239,6 +8247,23 @@ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, "node_modules/cli-color": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", @@ -12420,6 +12445,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.13", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.13.tgz", + "integrity": "sha512-QZXnR/OGiDcBjF4hGk0wwVrPcZvbSSyzlvkjXv5LFfktj7O2VZDrt4Xs8SgR/vOFco+qk1i8J43ikMXZoTrtPw==", + "license": "MIT" + }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -16457,6 +16488,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-or-promise": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", diff --git a/package.json b/package.json index 15542773f..dbbad71f9 100644 --- a/package.json +++ b/package.json @@ -113,11 +113,13 @@ "@sendgrid/mail": "^8.1.5", "agentkeepalive": "^4.2.1", "amqp-connection-manager": "^4.1.3", - "anchorme": "^3.0.8", "amqplib": "^0.10.0", + "anchorme": "^3.0.8", "apollo-server-core": "^3.13.0", "apollo-server-express": "3.13.0", "bignumber.js": "^9.0.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", "compression": "^1.8.0", "crypto-js": "^4.1.1", "dataloader": "^2.2.2", @@ -218,4 +220,4 @@ "node_modules" ] } -} \ No newline at end of file +} diff --git a/src/endpoints/blocks/blocks.gateway.ts b/src/endpoints/blocks/blocks.gateway.ts index 5c55d1d1d..346ed92ad 100644 --- a/src/endpoints/blocks/blocks.gateway.ts +++ b/src/endpoints/blocks/blocks.gateway.ts @@ -1,9 +1,14 @@ -import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect } from '@nestjs/websockets'; +import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect, MessageBody, ConnectedSocket } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { BlockService } from './block.service'; import { BlockFilter } from './entities/block.filter'; import { QueryPagination } from 'src/common/entities/query.pagination'; +import { BlockSubscribePayload } from './entities/block.subscribe'; +import { UseFilters } from '@nestjs/common'; +import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; +import { WsValidationPipe } from 'src/utils/ws-validation.pipe'; +@UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' } }) export class BlocksGateway implements OnGatewayDisconnect { @WebSocketServer() @@ -11,10 +16,16 @@ export class BlocksGateway implements OnGatewayDisconnect { constructor(private readonly blockService: BlockService) { } + @SubscribeMessage('subscribeBlocks') - async handleSubscription(client: Socket, payload: any) { + async handleSubscription( + @ConnectedSocket() client: Socket, + @MessageBody(new WsValidationPipe()) payload: BlockSubscribePayload + ) { const filterHash = JSON.stringify(payload); await client.join(`block-${filterHash}`); + + return { status: 'success' } } async pushBlocks() { @@ -46,3 +57,4 @@ export class BlocksGateway implements OnGatewayDisconnect { handleDisconnect(_client: Socket) { } } + diff --git a/src/endpoints/blocks/entities/block.subscribe.ts b/src/endpoints/blocks/entities/block.subscribe.ts new file mode 100644 index 000000000..c801e2287 --- /dev/null +++ b/src/endpoints/blocks/entities/block.subscribe.ts @@ -0,0 +1,49 @@ +// block-subscribe.dto.ts +import { IsOptional, IsString, IsNumber, IsArray, IsBoolean, Min, Max, IsIn } from 'class-validator'; + +export class BlockSubscribePayload { + @IsOptional() + @IsNumber() + @Min(0) + shard?: number; + + @IsOptional() + @IsString() + proposer?: string; + + @IsOptional() + @IsString() + validator?: string; + + @IsOptional() + @IsNumber() + epoch?: number; + + @IsOptional() + @IsNumber() + nonce?: number; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + hashes?: string[]; + + @IsOptional() + @IsIn(['asc', 'desc']) + order?: 'asc' | 'desc'; + + @IsOptional() + @IsNumber() + @Min(0) + from?: number; + + @IsOptional() + @IsNumber() + @Min(1) + @Max(100) + size?: number; + + @IsOptional() + @IsBoolean() + withProposerIdentity?: boolean; +} diff --git a/src/endpoints/network/network.gateway.ts b/src/endpoints/network/network.gateway.ts index 66d7b99c2..a6214b04f 100644 --- a/src/endpoints/network/network.gateway.ts +++ b/src/endpoints/network/network.gateway.ts @@ -1,7 +1,10 @@ import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { NetworkService } from './network.service'; +import { UseFilters } from '@nestjs/common'; +import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; +@UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' } }) export class NetworkGateway implements OnGatewayDisconnect { @WebSocketServer() diff --git a/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts b/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts new file mode 100644 index 000000000..b25b48839 --- /dev/null +++ b/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts @@ -0,0 +1,127 @@ +import { IsOptional, IsString, IsArray, IsBoolean, IsNumber, IsIn } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class TransactionSubscribePayload { + @IsOptional() + @IsString() + sender?: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + receiver?: string[]; + + @IsOptional() + @IsString() + token?: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + functions?: string[]; + + @IsOptional() + @IsNumber() + @Type(() => Number) + senderShard?: number; + + @IsOptional() + @IsNumber() + @Type(() => Number) + receiverShard?: number; + + @IsOptional() + @IsString() + miniBlockHash?: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + hashes?: string[]; + + @IsOptional() + @IsIn(['success', 'failed', 'pending']) + status?: string; + + @IsOptional() + @IsNumber() + @Type(() => Number) + before?: number; + + @IsOptional() + @IsNumber() + @Type(() => Number) + after?: number; + + @IsOptional() + @IsString() + condition?: string; + + @IsOptional() + @IsString() + order?: string; + + @IsOptional() + @IsString() + relayer?: string; + + @IsOptional() + @IsBoolean() + isRelayed?: boolean; + + @IsOptional() + @IsBoolean() + isScCall?: boolean; + + @IsOptional() + @IsNumber() + @Type(() => Number) + round?: number; + + @IsOptional() + @IsBoolean() + withScResults?: boolean; + + @IsOptional() + @IsBoolean() + withRelayedScresults?: boolean; + + @IsOptional() + @IsBoolean() + withOperations?: boolean; + + @IsOptional() + @IsBoolean() + withLogs?: boolean; + + @IsOptional() + @IsBoolean() + withScamInfo?: boolean; + + @IsOptional() + @IsBoolean() + withUsername?: boolean; + + @IsOptional() + @IsBoolean() + withBlockInfo?: boolean; + + @IsOptional() + @IsBoolean() + withActionTransferValue?: boolean; + + @IsOptional() + @IsNumber() + @Type(() => Number) + from?: number; + + @IsOptional() + @IsNumber() + @Type(() => Number) + size?: number; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + fields?: string[]; +} diff --git a/src/endpoints/transactions/transaction.gateway.ts b/src/endpoints/transactions/transaction.gateway.ts index 204f6f4af..987c3f6f2 100644 --- a/src/endpoints/transactions/transaction.gateway.ts +++ b/src/endpoints/transactions/transaction.gateway.ts @@ -1,10 +1,15 @@ -import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect } from '@nestjs/websockets'; +import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect, ConnectedSocket, MessageBody } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { TransactionService } from './transaction.service'; import { TransactionFilter } from './entities/transaction.filter'; import { QueryPagination } from 'src/common/entities/query.pagination'; import { TransactionQueryOptions } from './entities/transactions.query.options'; +import { WsValidationPipe } from 'src/utils/ws-validation.pipe'; +import { TransactionSubscribePayload } from './entities/dtos/transaction.subscribe'; +import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; +import { UseFilters } from '@nestjs/common'; +@UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' } }) export class TransactionsGateway implements OnGatewayDisconnect { @WebSocketServer() @@ -13,9 +18,13 @@ export class TransactionsGateway implements OnGatewayDisconnect { constructor(private readonly transactionService: TransactionService) { } @SubscribeMessage('subscribeTransactions') - async handleSubscription(client: Socket, payload: any) { + async handleSubscription( + @ConnectedSocket() client: Socket, + @MessageBody(new WsValidationPipe()) payload: TransactionSubscribePayload) { const filterHash = JSON.stringify(payload); await client.join(`tx-${filterHash}`); + + return { status: 'success' }; } async pushTransactions() { diff --git a/src/utils/ws-exceptions.filter.ts b/src/utils/ws-exceptions.filter.ts new file mode 100644 index 000000000..cc04628c0 --- /dev/null +++ b/src/utils/ws-exceptions.filter.ts @@ -0,0 +1,20 @@ +import { ArgumentsHost, Catch, } from "@nestjs/common"; +import { BaseWsExceptionFilter, WsException } from "@nestjs/websockets"; +import { Socket } from "socket.io"; + +@Catch(WsException) +export class WebsocketExceptionsFilter extends BaseWsExceptionFilter { + catch(exception: WsException, host: ArgumentsHost) { + const client = host.switchToWs().getClient() as Socket; + + const pattern = host.switchToWs().getPattern(); + const data = host.switchToWs().getData(); + const error = exception.getError(); + + client.emit('error', { + pattern, + data, + error, + }) + } +} \ No newline at end of file diff --git a/src/utils/ws-validation.pipe.ts b/src/utils/ws-validation.pipe.ts new file mode 100644 index 000000000..8a5a58010 --- /dev/null +++ b/src/utils/ws-validation.pipe.ts @@ -0,0 +1,16 @@ +// ws-validation.pipe.ts +import { Injectable, ValidationPipe, ValidationPipeOptions } from '@nestjs/common'; +import { WsException } from '@nestjs/websockets'; + +@Injectable() +export class WsValidationPipe extends ValidationPipe { + constructor(options?: ValidationPipeOptions) { + super({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + exceptionFactory: (errors) => new WsException(errors), + ...options, + }); + } +} From a7e71ea6a33f21fa6324d26c249e35411e556c15 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 22 Aug 2025 15:02:10 +0300 Subject: [PATCH 09/90] add try catch + class validator fixes --- src/endpoints/blocks/blocks.gateway.ts | 51 ++++--- .../blocks/entities/block.subscribe.ts | 7 +- src/endpoints/network/network.gateway.ts | 11 +- .../entities/dtos/transaction.subscribe.ts | 17 ++- .../transactions/transaction.gateway.ts | 135 ++++++++++++------ 5 files changed, 140 insertions(+), 81 deletions(-) diff --git a/src/endpoints/blocks/blocks.gateway.ts b/src/endpoints/blocks/blocks.gateway.ts index 346ed92ad..c48848927 100644 --- a/src/endpoints/blocks/blocks.gateway.ts +++ b/src/endpoints/blocks/blocks.gateway.ts @@ -7,10 +7,13 @@ import { BlockSubscribePayload } from './entities/block.subscribe'; import { UseFilters } from '@nestjs/common'; import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; import { WsValidationPipe } from 'src/utils/ws-validation.pipe'; +import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' } }) export class BlocksGateway implements OnGatewayDisconnect { + private readonly logger = new OriginLogger(BlocksGateway.name); + @WebSocketServer() server!: Server; @@ -30,28 +33,32 @@ export class BlocksGateway implements OnGatewayDisconnect { async pushBlocks() { for (const [roomName] of this.server.sockets.adapter.rooms) { - if (!roomName.startsWith("block-")) continue; - - const filterHash = roomName.replace("block-", ""); - const filter = JSON.parse(filterHash); - - const blockFilter = new BlockFilter({ - shard: filter.shard, - proposer: filter.proposer, - validator: filter.validator, - epoch: filter.epoch, - nonce: filter.nonce, - hashes: filter.hashes, - order: filter.order, - }); - - const blocks = await this.blockService.getBlocks( - blockFilter, - new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), - filter.withProposerIdentity, - ); - - this.server.to(roomName).emit('blocksUpdate', blocks); + try { + if (!roomName.startsWith("block-")) continue; + + const filterHash = roomName.replace("block-", ""); + const filter = JSON.parse(filterHash); + + const blockFilter = new BlockFilter({ + shard: filter.shard, + proposer: filter.proposer, + validator: filter.validator, + epoch: filter.epoch, + nonce: filter.nonce, + hashes: filter.hashes, + order: filter.order, + }); + + const blocks = await this.blockService.getBlocks( + blockFilter, + new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), + filter.withProposerIdentity, + ); + + this.server.to(roomName).emit('blocksUpdate', blocks); + } catch (error) { + this.logger.error(error); + } } } diff --git a/src/endpoints/blocks/entities/block.subscribe.ts b/src/endpoints/blocks/entities/block.subscribe.ts index c801e2287..dec878829 100644 --- a/src/endpoints/blocks/entities/block.subscribe.ts +++ b/src/endpoints/blocks/entities/block.subscribe.ts @@ -1,5 +1,6 @@ // block-subscribe.dto.ts -import { IsOptional, IsString, IsNumber, IsArray, IsBoolean, Min, Max, IsIn } from 'class-validator'; +import { IsOptional, IsString, IsNumber, IsArray, IsBoolean, Min, Max, IsEnum } from 'class-validator'; +import { SortOrder } from 'src/common/entities/sort.order'; export class BlockSubscribePayload { @IsOptional() @@ -29,8 +30,8 @@ export class BlockSubscribePayload { hashes?: string[]; @IsOptional() - @IsIn(['asc', 'desc']) - order?: 'asc' | 'desc'; + @IsEnum(SortOrder) + order?: SortOrder; @IsOptional() @IsNumber() diff --git a/src/endpoints/network/network.gateway.ts b/src/endpoints/network/network.gateway.ts index a6214b04f..9db26d666 100644 --- a/src/endpoints/network/network.gateway.ts +++ b/src/endpoints/network/network.gateway.ts @@ -3,10 +3,13 @@ import { Server, Socket } from 'socket.io'; import { NetworkService } from './network.service'; import { UseFilters } from '@nestjs/common'; import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; +import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' } }) export class NetworkGateway implements OnGatewayDisconnect { + private readonly logger = new OriginLogger(NetworkGateway.name); + @WebSocketServer() server!: Server; @@ -19,8 +22,12 @@ export class NetworkGateway implements OnGatewayDisconnect { async pushStats() { if (this.server.sockets.adapter.rooms.has('statsRoom')) { - const stats = await this.networkService.getStats(); - this.server.to('statsRoom').emit('statsUpdate', stats); + try { + const stats = await this.networkService.getStats(); + this.server.to('statsRoom').emit('statsUpdate', stats); + } catch (error) { + this.logger.error(error); + } } } diff --git a/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts b/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts index b25b48839..c229e1e70 100644 --- a/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts +++ b/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts @@ -1,5 +1,8 @@ -import { IsOptional, IsString, IsArray, IsBoolean, IsNumber, IsIn } from 'class-validator'; +import { IsOptional, IsString, IsArray, IsBoolean, IsNumber, IsEnum } from 'class-validator'; import { Type } from 'class-transformer'; +import { TransactionStatus } from '../transaction.status'; +import { QueryConditionOptions } from '@multiversx/sdk-nestjs-elastic'; +import { SortOrder } from 'src/common/entities/sort.order'; export class TransactionSubscribePayload { @IsOptional() @@ -40,8 +43,8 @@ export class TransactionSubscribePayload { hashes?: string[]; @IsOptional() - @IsIn(['success', 'failed', 'pending']) - status?: string; + @IsEnum(TransactionStatus) + status?: TransactionStatus;; @IsOptional() @IsNumber() @@ -54,12 +57,12 @@ export class TransactionSubscribePayload { after?: number; @IsOptional() - @IsString() - condition?: string; + @IsEnum(QueryConditionOptions) + condition?: QueryConditionOptions; @IsOptional() - @IsString() - order?: string; + @IsEnum(SortOrder) + order?: SortOrder; @IsOptional() @IsString() diff --git a/src/endpoints/transactions/transaction.gateway.ts b/src/endpoints/transactions/transaction.gateway.ts index 987c3f6f2..280e35d90 100644 --- a/src/endpoints/transactions/transaction.gateway.ts +++ b/src/endpoints/transactions/transaction.gateway.ts @@ -8,10 +8,13 @@ import { WsValidationPipe } from 'src/utils/ws-validation.pipe'; import { TransactionSubscribePayload } from './entities/dtos/transaction.subscribe'; import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; import { UseFilters } from '@nestjs/common'; +import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' } }) export class TransactionsGateway implements OnGatewayDisconnect { + private readonly logger = new OriginLogger(TransactionsGateway.name); + @WebSocketServer() server!: Server; @@ -21,6 +24,40 @@ export class TransactionsGateway implements OnGatewayDisconnect { async handleSubscription( @ConnectedSocket() client: Socket, @MessageBody(new WsValidationPipe()) payload: TransactionSubscribePayload) { + // If one of these methods throw an exception then the subscription will not be successful + TransactionQueryOptions.applyDefaultOptions(payload.size || 25, { + withScResults: payload.withScResults, + withOperations: payload.withOperations, + withLogs: payload.withLogs, + withScamInfo: payload.withScamInfo, + withUsername: payload.withUsername, + withBlockInfo: payload.withBlockInfo, + withActionTransferValue: payload.withActionTransferValue, + }); + + const transactionFilter = new TransactionFilter({ + sender: payload.sender, + receivers: payload.receiver, + token: payload.token, + functions: payload.functions, + senderShard: payload.senderShard, + receiverShard: payload.receiverShard, + miniBlockHash: payload.miniBlockHash, + hashes: payload.hashes, + status: payload.status, + before: payload.before, + after: payload.after, + condition: payload.condition, + order: payload.order, + relayer: payload.relayer, + isRelayed: payload.isRelayed, + isScCall: payload.isScCall, + round: payload.round, + withRelayedScresults: payload.withRelayedScresults, + }); + + TransactionFilter.validate(transactionFilter, payload.size || 25); + const filterHash = JSON.stringify(payload); await client.join(`tx-${filterHash}`); @@ -29,53 +66,57 @@ export class TransactionsGateway implements OnGatewayDisconnect { async pushTransactions() { for (const [roomName] of this.server.sockets.adapter.rooms) { - if (!roomName.startsWith("tx-")) continue; - - const filterHash = roomName.replace("tx-", ""); - const filter = JSON.parse(filterHash); - - const options = TransactionQueryOptions.applyDefaultOptions(filter.size || 25, { - withScResults: filter.withScResults, - withOperations: filter.withOperations, - withLogs: filter.withLogs, - withScamInfo: filter.withScamInfo, - withUsername: filter.withUsername, - withBlockInfo: filter.withBlockInfo, - withActionTransferValue: filter.withActionTransferValue, - }); - - const transactionFilter = new TransactionFilter({ - sender: filter.sender, - receivers: filter.receiver, - token: filter.token, - functions: filter.functions, - senderShard: filter.senderShard, - receiverShard: filter.receiverShard, - miniBlockHash: filter.miniBlockHash, - hashes: filter.hashes, - status: filter.status, - before: filter.before, - after: filter.after, - condition: filter.condition, - order: filter.order, - relayer: filter.relayer, - isRelayed: filter.isRelayed, - isScCall: filter.isScCall, - round: filter.round, - withRelayedScresults: filter.withRelayedScresults, - }); - - TransactionFilter.validate(transactionFilter, filter.size || 25); - - const txs = await this.transactionService.getTransactions( - transactionFilter, - new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), - options, - undefined, - filter.fields || [], - ); - - this.server.to(roomName).emit('transactionUpdate', txs); + try { + if (!roomName.startsWith("tx-")) continue; + + const filterHash = roomName.replace("tx-", ""); + const filter = JSON.parse(filterHash); + + const options = TransactionQueryOptions.applyDefaultOptions(filter.size || 25, { + withScResults: filter.withScResults, + withOperations: filter.withOperations, + withLogs: filter.withLogs, + withScamInfo: filter.withScamInfo, + withUsername: filter.withUsername, + withBlockInfo: filter.withBlockInfo, + withActionTransferValue: filter.withActionTransferValue, + }); + + const transactionFilter = new TransactionFilter({ + sender: filter.sender, + receivers: filter.receiver, + token: filter.token, + functions: filter.functions, + senderShard: filter.senderShard, + receiverShard: filter.receiverShard, + miniBlockHash: filter.miniBlockHash, + hashes: filter.hashes, + status: filter.status, + before: filter.before, + after: filter.after, + condition: filter.condition, + order: filter.order, + relayer: filter.relayer, + isRelayed: filter.isRelayed, + isScCall: filter.isScCall, + round: filter.round, + withRelayedScresults: filter.withRelayedScresults, + }); + + TransactionFilter.validate(transactionFilter, filter.size || 25); + + const txs = await this.transactionService.getTransactions( + transactionFilter, + new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), + options, + undefined, + filter.fields || [], + ); + + this.server.to(roomName).emit('transactionUpdate', txs); + } catch (error) { + this.logger.error(error); + } } } From 58be392875a848b461fecc19dbab355339901b36 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 22 Aug 2025 15:15:53 +0300 Subject: [PATCH 10/90] fix linter --- src/endpoints/blocks/blocks.gateway.ts | 2 +- .../transactions/entities/dtos/transaction.subscribe.ts | 2 +- src/utils/ws-exceptions.filter.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/endpoints/blocks/blocks.gateway.ts b/src/endpoints/blocks/blocks.gateway.ts index c48848927..518d730a4 100644 --- a/src/endpoints/blocks/blocks.gateway.ts +++ b/src/endpoints/blocks/blocks.gateway.ts @@ -28,7 +28,7 @@ export class BlocksGateway implements OnGatewayDisconnect { const filterHash = JSON.stringify(payload); await client.join(`block-${filterHash}`); - return { status: 'success' } + return { status: 'success' }; } async pushBlocks() { diff --git a/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts b/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts index c229e1e70..2c1e0b45c 100644 --- a/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts +++ b/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts @@ -44,7 +44,7 @@ export class TransactionSubscribePayload { @IsOptional() @IsEnum(TransactionStatus) - status?: TransactionStatus;; + status?: TransactionStatus; @IsOptional() @IsNumber() diff --git a/src/utils/ws-exceptions.filter.ts b/src/utils/ws-exceptions.filter.ts index cc04628c0..bb864231b 100644 --- a/src/utils/ws-exceptions.filter.ts +++ b/src/utils/ws-exceptions.filter.ts @@ -1,4 +1,4 @@ -import { ArgumentsHost, Catch, } from "@nestjs/common"; +import { ArgumentsHost, Catch } from "@nestjs/common"; import { BaseWsExceptionFilter, WsException } from "@nestjs/websockets"; import { Socket } from "socket.io"; @@ -15,6 +15,6 @@ export class WebsocketExceptionsFilter extends BaseWsExceptionFilter { pattern, data, error, - }) + }); } -} \ No newline at end of file +} From 6c82ee3b053a95f3d4366451309c3a09ec65a349 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 27 Aug 2025 12:08:16 +0300 Subject: [PATCH 11/90] receive state changes events from queue --- src/common/api-config/api.config.service.ts | 45 +++++++++++++++++++ src/main.ts | 6 +++ src/state-changes/entities/index.ts | 1 + .../entities/state.changes.entity.ts | 29 ++++++++++++ .../state.changes.consumer.service.ts | 20 +++++++++ src/state-changes/state.changes.module.ts | 37 +++++++++++++++ 6 files changed, 138 insertions(+) create mode 100644 src/state-changes/entities/index.ts create mode 100644 src/state-changes/entities/state.changes.entity.ts create mode 100644 src/state-changes/state.changes.consumer.service.ts create mode 100644 src/state-changes/state.changes.module.ts diff --git a/src/common/api-config/api.config.service.ts b/src/common/api-config/api.config.service.ts index 77c400332..53a5d7e87 100644 --- a/src/common/api-config/api.config.service.ts +++ b/src/common/api-config/api.config.service.ts @@ -954,4 +954,49 @@ export class ApiConfigService { getCompressionChunkSize(): number { return this.configService.get('compression.chunkSize') ?? 16384; } + + isStateChangesFeatureActive(): boolean { + const isStateChangesActive = this.configService.get('features.stateChanges.enabled'); + if (isStateChangesActive === undefined) { + return false; + } + + return isStateChangesActive; + } + + getStateChangesFeaturePort(): number { + const stateChangesPort = this.configService.get('features.stateChanges.port'); + if (stateChangesPort === undefined) { + throw new Error('No state changes port present'); + } + + return stateChangesPort; + } + + getStateChangesUrl(): string { + const url = this.configService.get('features.stateChanges.url'); + if (!url) { + throw new Error('No state changes url present'); + } + + return url; + } + + getStateChangesExchange(): string { + const exchange = this.configService.get('features.stateChanges.exchange'); + if (!exchange) { + throw new Error('No state changes exchange present'); + } + + return exchange; + } + + getStateChangesQueue(): string { + const queue = this.configService.get('features.stateChanges.queue'); + if (!queue) { + throw new Error('No state changes queue present'); + } + + return queue; + } } diff --git a/src/main.ts b/src/main.ts index b31cb34a6..9c6b692a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -37,6 +37,7 @@ import * as bodyParser from 'body-parser'; import * as requestIp from 'request-ip'; import compression from 'compression'; import { IoAdapter } from '@nestjs/platform-socket.io'; +import { StateChangesModule } from './state-changes/state.changes.module'; async function bootstrap() { const logger = new Logger('Bootstrapper'); @@ -140,6 +141,11 @@ async function bootstrap() { await eventsNotifierApp.listen(apiConfigService.getEventsNotifierFeaturePort()); } + if (apiConfigService.isStateChangesFeatureActive()) { + const stateChangesApp = await NestFactory.create(StateChangesModule.register()); + await stateChangesApp.listen(apiConfigService.getStateChangesFeaturePort()); + } + const pubSubApp = await NestFactory.createMicroservice( PubSubListenerModule, { diff --git a/src/state-changes/entities/index.ts b/src/state-changes/entities/index.ts new file mode 100644 index 000000000..e266c949a --- /dev/null +++ b/src/state-changes/entities/index.ts @@ -0,0 +1 @@ +export * from './state.changes.entity'; \ No newline at end of file diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts new file mode 100644 index 000000000..8b9bc4206 --- /dev/null +++ b/src/state-changes/entities/state.changes.entity.ts @@ -0,0 +1,29 @@ +export class AccountChanges { + nonceChanged!: boolean; + balanceChanged!: boolean; + codeHashChanged!: boolean; + rootHashChanged!: boolean; + developerRewardChanged!: boolean; + ownerAddressChanged!: boolean; + userNameChanged!: boolean; + codeMetadataChanged!: boolean; +} + +export class StateAccessPerAccount { + type!: number; + index!: number; + txHash!: string; + mainTrieKey!: string; + mainTrieVal!: string; + operation!: number; + accountChanges?: AccountChanges; +} + +export class StateChanges { + hash!: string; + stateAccessesPerAccounts?: Map; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts new file mode 100644 index 000000000..f8270b610 --- /dev/null +++ b/src/state-changes/state.changes.consumer.service.ts @@ -0,0 +1,20 @@ +import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers"; +import { StateChanges } from "./entities"; +// import { ApiConfigService } from "src/common/api-config/api.config.service"; + +export class StateChangesConsumerService { + constructor( + // private readonly apiConfigService: ApiConfigService, + ) { + console.log('StateChangesConsumerService initialized'); + } + @CompetingRabbitConsumer({ + exchange: 'state_accesses', + queueName: 'state-changes-test', + deadLetterExchange: 'state-changes-test_dlx', + }) + async consumeEvents(stateChanges: StateChanges) { + + console.log(stateChanges); + } +} \ No newline at end of file diff --git a/src/state-changes/state.changes.module.ts b/src/state-changes/state.changes.module.ts new file mode 100644 index 000000000..6fdc4968e --- /dev/null +++ b/src/state-changes/state.changes.module.ts @@ -0,0 +1,37 @@ +import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; +import { DynamicModule, Module } from '@nestjs/common'; +import { DynamicModuleUtils } from 'src/utils/dynamic.module.utils'; +import { ApiConfigModule } from 'src/common/api-config/api.config.module'; +import { ApiConfigService } from 'src/common/api-config/api.config.service'; +import { StateChangesConsumerService } from './state.changes.consumer.service'; + +@Module({ + imports: [ + ApiConfigModule, + ], + providers: [ + StateChangesConsumerService, + DynamicModuleUtils.getPubSubService(), + ], +}) +export class StateChangesModule { + static register(): DynamicModule { + return { + module: StateChangesModule, + imports: [ + RabbitMQModule.forRootAsync(RabbitMQModule, { + imports: [ApiConfigModule], + inject: [ApiConfigService], + useFactory: (apiConfigService: ApiConfigService) => { + return { + name: apiConfigService.getStateChangesExchange(), + type: 'fanout', + options: {}, + uri: apiConfigService.getStateChangesUrl(), + }; + }, + }), + ], + }; + } +} From d67b48f3ee2fc23f6e55925582aa3b90dbe5da75 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 27 Aug 2025 12:09:24 +0300 Subject: [PATCH 12/90] add devnet configs --- config/config.devnet.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 7d42d5d61..7c4f0e9d2 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -20,6 +20,12 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: + stateChanges: + enabled: true + port: 5675 + url: 'amqp://guest:guest@127.0.0.1:5672' + exchange: 'state_accesses' + queue: 'state-changes' eventsNotifier: enabled: false port: 5674 From 69581a7db1f2d41de40576b58fa1df639a84f2ee Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 29 Aug 2025 16:41:31 +0300 Subject: [PATCH 13/90] decode state changes --- .../state.changes.consumer.service.ts | 18 +- src/state-changes/utils/esdt.d.ts | 213 +++++++ src/state-changes/utils/esdt.js | 519 ++++++++++++++++++ src/state-changes/utils/esdt.proto | 18 + .../utils/state-changes.utils.ts | 293 ++++++++++ src/state-changes/utils/token.parser.ts | 53 ++ src/state-changes/utils/trie_leaf_data.d.ts | 110 ++++ src/state-changes/utils/trie_leaf_data.js | 291 ++++++++++ src/state-changes/utils/trie_leaf_data.proto | 7 + src/state-changes/utils/user_account.pb.d.ts | 146 +++++ src/state-changes/utils/user_account.pb.js | 488 ++++++++++++++++ src/state-changes/utils/user_account.proto | 13 + 12 files changed, 2164 insertions(+), 5 deletions(-) create mode 100644 src/state-changes/utils/esdt.d.ts create mode 100644 src/state-changes/utils/esdt.js create mode 100644 src/state-changes/utils/esdt.proto create mode 100644 src/state-changes/utils/state-changes.utils.ts create mode 100644 src/state-changes/utils/token.parser.ts create mode 100644 src/state-changes/utils/trie_leaf_data.d.ts create mode 100644 src/state-changes/utils/trie_leaf_data.js create mode 100644 src/state-changes/utils/trie_leaf_data.proto create mode 100644 src/state-changes/utils/user_account.pb.d.ts create mode 100644 src/state-changes/utils/user_account.pb.js create mode 100644 src/state-changes/utils/user_account.proto diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index f8270b610..787b91246 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -1,20 +1,28 @@ import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers"; import { StateChanges } from "./entities"; +import { decodeStateChangesRaw, getFinalStates } from "./utils/state-changes.utils"; // import { ApiConfigService } from "src/common/api-config/api.config.service"; export class StateChangesConsumerService { constructor( // private readonly apiConfigService: ApiConfigService, - ) { - console.log('StateChangesConsumerService initialized'); - } + ) { } @CompetingRabbitConsumer({ exchange: 'state_accesses', queueName: 'state-changes-test', deadLetterExchange: 'state-changes-test_dlx', }) - async consumeEvents(stateChanges: StateChanges) { + async consumeEvents(stateChanges: any) { + // console.dir(stateChanges, { depth: null }) + const decodedStateChanges = this.decodeStateChanges(stateChanges) + if (Object.keys(decodedStateChanges).length === 0) { + return; + } + const finalStates = getFinalStates(decodedStateChanges); + console.dir(finalStates, { depth: null }) + } - console.log(stateChanges); + decodeStateChanges(stateChanges: StateChanges) { + return decodeStateChangesRaw(stateChanges); } } \ No newline at end of file diff --git a/src/state-changes/utils/esdt.d.ts b/src/state-changes/utils/esdt.d.ts new file mode 100644 index 000000000..b6b878b27 --- /dev/null +++ b/src/state-changes/utils/esdt.d.ts @@ -0,0 +1,213 @@ +import * as $protobuf from "protobufjs"; +import Long = require("long"); +/** Properties of a MetaData. */ +export interface IMetaData { +} + +/** Represents a MetaData. */ +export class MetaData implements IMetaData { + + /** + * Constructs a new MetaData. + * @param [properties] Properties to set + */ + constructor(properties?: IMetaData); + + /** + * Creates a new MetaData instance using the specified properties. + * @param [properties] Properties to set + * @returns MetaData instance + */ + public static create(properties?: IMetaData): MetaData; + + /** + * Encodes the specified MetaData message. Does not implicitly {@link MetaData.verify|verify} messages. + * @param message MetaData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: IMetaData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified MetaData message, length delimited. Does not implicitly {@link MetaData.verify|verify} messages. + * @param message MetaData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: IMetaData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a MetaData message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns MetaData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): MetaData; + + /** + * Decodes a MetaData message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns MetaData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): MetaData; + + /** + * Verifies a MetaData message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a MetaData message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns MetaData + */ + public static fromObject(object: { [k: string]: any }): MetaData; + + /** + * Creates a plain object from a MetaData message. Also converts values to other types if specified. + * @param message MetaData + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: MetaData, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this MetaData to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for MetaData + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; +} + +/** Properties of a ESDigitalToken. */ +export interface IESDigitalToken { + + /** ESDigitalToken Type */ + Type?: (number|null); + + /** ESDigitalToken Value */ + Value?: (Uint8Array|null); + + /** ESDigitalToken Properties */ + Properties?: (Uint8Array|null); + + /** ESDigitalToken TokenMetaData */ + TokenMetaData?: (IMetaData|null); + + /** ESDigitalToken Reserved */ + Reserved?: (Uint8Array|null); +} + +/** Represents a ESDigitalToken. */ +export class ESDigitalToken implements IESDigitalToken { + + /** + * Constructs a new ESDigitalToken. + * @param [properties] Properties to set + */ + constructor(properties?: IESDigitalToken); + + /** ESDigitalToken Type. */ + public Type: number; + + /** ESDigitalToken Value. */ + public Value: Uint8Array; + + /** ESDigitalToken Properties. */ + public Properties: Uint8Array; + + /** ESDigitalToken TokenMetaData. */ + public TokenMetaData?: (IMetaData|null); + + /** ESDigitalToken Reserved. */ + public Reserved: Uint8Array; + + /** + * Creates a new ESDigitalToken instance using the specified properties. + * @param [properties] Properties to set + * @returns ESDigitalToken instance + */ + public static create(properties?: IESDigitalToken): ESDigitalToken; + + /** + * Encodes the specified ESDigitalToken message. Does not implicitly {@link ESDigitalToken.verify|verify} messages. + * @param message ESDigitalToken message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: IESDigitalToken, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified ESDigitalToken message, length delimited. Does not implicitly {@link ESDigitalToken.verify|verify} messages. + * @param message ESDigitalToken message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: IESDigitalToken, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a ESDigitalToken message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns ESDigitalToken + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): ESDigitalToken; + + /** + * Decodes a ESDigitalToken message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns ESDigitalToken + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): ESDigitalToken; + + /** + * Verifies a ESDigitalToken message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a ESDigitalToken message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns ESDigitalToken + */ + public static fromObject(object: { [k: string]: any }): ESDigitalToken; + + /** + * Creates a plain object from a ESDigitalToken message. Also converts values to other types if specified. + * @param message ESDigitalToken + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: ESDigitalToken, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this ESDigitalToken to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for ESDigitalToken + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; +} diff --git a/src/state-changes/utils/esdt.js b/src/state-changes/utils/esdt.js new file mode 100644 index 000000000..ecac3c922 --- /dev/null +++ b/src/state-changes/utils/esdt.js @@ -0,0 +1,519 @@ +/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/ +"use strict"; + +var $protobuf = require("protobufjs/minimal"); + +// Common aliases +var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; + +// Exported root namespace +var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); + +$root.MetaData = (function () { + + /** + * Properties of a MetaData. + * @exports IMetaData + * @interface IMetaData + */ + + /** + * Constructs a new MetaData. + * @exports MetaData + * @classdesc Represents a MetaData. + * @implements IMetaData + * @constructor + * @param {IMetaData=} [properties] Properties to set + */ + function MetaData(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * Creates a new MetaData instance using the specified properties. + * @function create + * @memberof MetaData + * @static + * @param {IMetaData=} [properties] Properties to set + * @returns {MetaData} MetaData instance + */ + MetaData.create = function create(properties) { + return new MetaData(properties); + }; + + /** + * Encodes the specified MetaData message. Does not implicitly {@link MetaData.verify|verify} messages. + * @function encode + * @memberof MetaData + * @static + * @param {IMetaData} message MetaData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + MetaData.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + return writer; + }; + + /** + * Encodes the specified MetaData message, length delimited. Does not implicitly {@link MetaData.verify|verify} messages. + * @function encodeDelimited + * @memberof MetaData + * @static + * @param {IMetaData} message MetaData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + MetaData.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a MetaData message from the specified reader or buffer. + * @function decode + * @memberof MetaData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {MetaData} MetaData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + MetaData.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.MetaData(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a MetaData message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof MetaData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {MetaData} MetaData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + MetaData.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a MetaData message. + * @function verify + * @memberof MetaData + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + MetaData.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + return null; + }; + + /** + * Creates a MetaData message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof MetaData + * @static + * @param {Object.} object Plain object + * @returns {MetaData} MetaData + */ + MetaData.fromObject = function fromObject(object) { + if (object instanceof $root.MetaData) + return object; + return new $root.MetaData(); + }; + + /** + * Creates a plain object from a MetaData message. Also converts values to other types if specified. + * @function toObject + * @memberof MetaData + * @static + * @param {MetaData} message MetaData + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + MetaData.toObject = function toObject() { + return {}; + }; + + /** + * Converts this MetaData to JSON. + * @function toJSON + * @memberof MetaData + * @instance + * @returns {Object.} JSON object + */ + MetaData.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for MetaData + * @function getTypeUrl + * @memberof MetaData + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + MetaData.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/MetaData"; + }; + + return MetaData; +})(); + +$root.ESDigitalToken = (function () { + + /** + * Properties of a ESDigitalToken. + * @exports IESDigitalToken + * @interface IESDigitalToken + * @property {number|null} [Type] ESDigitalToken Type + * @property {Uint8Array|null} [Value] ESDigitalToken Value + * @property {Uint8Array|null} [Properties] ESDigitalToken Properties + * @property {IMetaData|null} [TokenMetaData] ESDigitalToken TokenMetaData + * @property {Uint8Array|null} [Reserved] ESDigitalToken Reserved + */ + + /** + * Constructs a new ESDigitalToken. + * @exports ESDigitalToken + * @classdesc Represents a ESDigitalToken. + * @implements IESDigitalToken + * @constructor + * @param {IESDigitalToken=} [properties] Properties to set + */ + function ESDigitalToken(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * ESDigitalToken Type. + * @member {number} Type + * @memberof ESDigitalToken + * @instance + */ + ESDigitalToken.prototype.Type = 0; + + /** + * ESDigitalToken Value. + * @member {Uint8Array} Value + * @memberof ESDigitalToken + * @instance + */ + ESDigitalToken.prototype.Value = $util.newBuffer([]); + + /** + * ESDigitalToken Properties. + * @member {Uint8Array} Properties + * @memberof ESDigitalToken + * @instance + */ + ESDigitalToken.prototype.Properties = $util.newBuffer([]); + + /** + * ESDigitalToken TokenMetaData. + * @member {IMetaData|null|undefined} TokenMetaData + * @memberof ESDigitalToken + * @instance + */ + ESDigitalToken.prototype.TokenMetaData = null; + + /** + * ESDigitalToken Reserved. + * @member {Uint8Array} Reserved + * @memberof ESDigitalToken + * @instance + */ + ESDigitalToken.prototype.Reserved = $util.newBuffer([]); + + /** + * Creates a new ESDigitalToken instance using the specified properties. + * @function create + * @memberof ESDigitalToken + * @static + * @param {IESDigitalToken=} [properties] Properties to set + * @returns {ESDigitalToken} ESDigitalToken instance + */ + ESDigitalToken.create = function create(properties) { + return new ESDigitalToken(properties); + }; + + /** + * Encodes the specified ESDigitalToken message. Does not implicitly {@link ESDigitalToken.verify|verify} messages. + * @function encode + * @memberof ESDigitalToken + * @static + * @param {IESDigitalToken} message ESDigitalToken message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + ESDigitalToken.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.Type != null && Object.hasOwnProperty.call(message, "Type")) + writer.uint32(/* id 1, wireType 0 =*/8).uint32(message.Type); + if (message.Value != null && Object.hasOwnProperty.call(message, "Value")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.Value); + if (message.Properties != null && Object.hasOwnProperty.call(message, "Properties")) + writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.Properties); + if (message.TokenMetaData != null && Object.hasOwnProperty.call(message, "TokenMetaData")) + $root.MetaData.encode(message.TokenMetaData, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); + if (message.Reserved != null && Object.hasOwnProperty.call(message, "Reserved")) + writer.uint32(/* id 5, wireType 2 =*/42).bytes(message.Reserved); + return writer; + }; + + /** + * Encodes the specified ESDigitalToken message, length delimited. Does not implicitly {@link ESDigitalToken.verify|verify} messages. + * @function encodeDelimited + * @memberof ESDigitalToken + * @static + * @param {IESDigitalToken} message ESDigitalToken message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + ESDigitalToken.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a ESDigitalToken message from the specified reader or buffer. + * @function decode + * @memberof ESDigitalToken + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {ESDigitalToken} ESDigitalToken + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + ESDigitalToken.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.ESDigitalToken(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.Type = reader.uint32(); + break; + } + case 2: { + message.Value = reader.bytes(); + break; + } + case 3: { + message.Properties = reader.bytes(); + break; + } + case 4: { + message.TokenMetaData = $root.MetaData.decode(reader, reader.uint32()); + break; + } + case 5: { + message.Reserved = reader.bytes(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a ESDigitalToken message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof ESDigitalToken + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {ESDigitalToken} ESDigitalToken + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + ESDigitalToken.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a ESDigitalToken message. + * @function verify + * @memberof ESDigitalToken + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + ESDigitalToken.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.Type != null && message.hasOwnProperty("Type")) + if (!$util.isInteger(message.Type)) + return "Type: integer expected"; + if (message.Value != null && message.hasOwnProperty("Value")) + if (!(message.Value && typeof message.Value.length === "number" || $util.isString(message.Value))) + return "Value: buffer expected"; + if (message.Properties != null && message.hasOwnProperty("Properties")) + if (!(message.Properties && typeof message.Properties.length === "number" || $util.isString(message.Properties))) + return "Properties: buffer expected"; + if (message.TokenMetaData != null && message.hasOwnProperty("TokenMetaData")) { + var error = $root.MetaData.verify(message.TokenMetaData); + if (error) + return "TokenMetaData." + error; + } + if (message.Reserved != null && message.hasOwnProperty("Reserved")) + if (!(message.Reserved && typeof message.Reserved.length === "number" || $util.isString(message.Reserved))) + return "Reserved: buffer expected"; + return null; + }; + + /** + * Creates a ESDigitalToken message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof ESDigitalToken + * @static + * @param {Object.} object Plain object + * @returns {ESDigitalToken} ESDigitalToken + */ + ESDigitalToken.fromObject = function fromObject(object) { + if (object instanceof $root.ESDigitalToken) + return object; + var message = new $root.ESDigitalToken(); + if (object.Type != null) + message.Type = object.Type >>> 0; + if (object.Value != null) + if (typeof object.Value === "string") + $util.base64.decode(object.Value, message.Value = $util.newBuffer($util.base64.length(object.Value)), 0); + else if (object.Value.length >= 0) + message.Value = object.Value; + if (object.Properties != null) + if (typeof object.Properties === "string") + $util.base64.decode(object.Properties, message.Properties = $util.newBuffer($util.base64.length(object.Properties)), 0); + else if (object.Properties.length >= 0) + message.Properties = object.Properties; + if (object.TokenMetaData != null) { + if (typeof object.TokenMetaData !== "object") + throw TypeError(".ESDigitalToken.TokenMetaData: object expected"); + message.TokenMetaData = $root.MetaData.fromObject(object.TokenMetaData); + } + if (object.Reserved != null) + if (typeof object.Reserved === "string") + $util.base64.decode(object.Reserved, message.Reserved = $util.newBuffer($util.base64.length(object.Reserved)), 0); + else if (object.Reserved.length >= 0) + message.Reserved = object.Reserved; + return message; + }; + + /** + * Creates a plain object from a ESDigitalToken message. Also converts values to other types if specified. + * @function toObject + * @memberof ESDigitalToken + * @static + * @param {ESDigitalToken} message ESDigitalToken + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + ESDigitalToken.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + object.Type = 0; + if (options.bytes === String) + object.Value = ""; + else { + object.Value = []; + if (options.bytes !== Array) + object.Value = $util.newBuffer(object.Value); + } + if (options.bytes === String) + object.Properties = ""; + else { + object.Properties = []; + if (options.bytes !== Array) + object.Properties = $util.newBuffer(object.Properties); + } + object.TokenMetaData = null; + if (options.bytes === String) + object.Reserved = ""; + else { + object.Reserved = []; + if (options.bytes !== Array) + object.Reserved = $util.newBuffer(object.Reserved); + } + } + if (message.Type != null && message.hasOwnProperty("Type")) + object.Type = message.Type; + if (message.Value != null && message.hasOwnProperty("Value")) + object.Value = options.bytes === String ? $util.base64.encode(message.Value, 0, message.Value.length) : options.bytes === Array ? Array.prototype.slice.call(message.Value) : message.Value; + if (message.Properties != null && message.hasOwnProperty("Properties")) + object.Properties = options.bytes === String ? $util.base64.encode(message.Properties, 0, message.Properties.length) : options.bytes === Array ? Array.prototype.slice.call(message.Properties) : message.Properties; + if (message.TokenMetaData != null && message.hasOwnProperty("TokenMetaData")) + object.TokenMetaData = $root.MetaData.toObject(message.TokenMetaData, options); + if (message.Reserved != null && message.hasOwnProperty("Reserved")) + object.Reserved = options.bytes === String ? $util.base64.encode(message.Reserved, 0, message.Reserved.length) : options.bytes === Array ? Array.prototype.slice.call(message.Reserved) : message.Reserved; + return object; + }; + + /** + * Converts this ESDigitalToken to JSON. + * @function toJSON + * @memberof ESDigitalToken + * @instance + * @returns {Object.} JSON object + */ + ESDigitalToken.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for ESDigitalToken + * @function getTypeUrl + * @memberof ESDigitalToken + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + ESDigitalToken.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/ESDigitalToken"; + }; + + return ESDigitalToken; +})(); + +module.exports = $root; diff --git a/src/state-changes/utils/esdt.proto b/src/state-changes/utils/esdt.proto new file mode 100644 index 000000000..0fccddc7f --- /dev/null +++ b/src/state-changes/utils/esdt.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +message MetaData { + // ex: string name = 1; + // optional uint64 id = 2; +} + +message ESDigitalToken { + uint32 Type = 1; + + bytes Value = 2; + + bytes Properties = 3; + + MetaData TokenMetaData = 4; + + bytes Reserved = 5; +} diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts new file mode 100644 index 000000000..f8a1ef907 --- /dev/null +++ b/src/state-changes/utils/state-changes.utils.ts @@ -0,0 +1,293 @@ +import { Address } from "@multiversx/sdk-core/out"; +import { UserAccountData } from "./user_account.pb"; +import { ESDigitalToken } from "./esdt"; +import { TrieLeafData } from "./trie_leaf_data"; +import { TokenParser } from "./token.parser"; + + +export enum StateAccessOperation { + NotSet = 0, + GetCode = 1, + SaveAccount = 2, + GetAccount = 4, + WriteCode = 8, + RemoveDataTrie = 16, + GetDataTrieValue = 32, +} + +export enum ESDTType { + // 0 + Fungible, + // 1 + NonFungible, + // 2 + NonFungibleV2, + // 3 + SemiFungible, + // 4 + MetaFungible, + // 5 + DynamicNFT, + // 6 + DynamicSFT, + // 7 + DynamicMeta, +} + + + +const bech32FromHex = (hex: any) => { + const clean = hex.startsWith("0x") ? hex.slice(2) : hex; + return Address.newFromHex(clean).toBech32(); +}; +const bech32FromBytes = (u8: any) => (u8 && u8.length ? Address.newFromHex(bytesToHex(u8)).toBech32() : ""); + +const bytesToHex = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("hex") : ""); +// const bytesToString = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("utf8") : ""); +const longToString = (v: any) => + v == null ? "" : (typeof v === "object" && typeof v.toString === "function" ? v.toString() : String(v)); +function bigEndianBytesToBigInt(u8: any) { + let v = BigInt(0); + for (const b of u8) { + v = (v << BigInt(8)) + BigInt(b); + } + return v; +} + +/** + * MultiversX custom sign & magnitude BigInt for proto: + * - zero => [0x00, 0x00] + * - positive non-zero => 0x00 || magnitude (big-endian) + * - (if present) negative => 0x01 || magnitude + * Fallback: if first byte is not a sign marker, treat whole buffer as positive magnitude. + */ +function decodeMxSignMagBigInt(u8: any) { + if (!u8 || u8.length === 0) return BigInt(0); + + // canonical zero used by the serializer + if (u8.length === 2 && u8[0] === 0x00 && u8[1] === 0x00) return BigInt(0); + + const sign = u8[0]; + if (sign === 0x00 || sign === 0x01) { + const mag = u8.slice(1); + const m = bigEndianBytesToBigInt(mag); + return sign === 0x01 ? -m : m; + } + + // fallback for legacy "magnitude only" encodings + return bigEndianBytesToBigInt(u8); +} + +function getDecodedUserAccountData(buf: any) { + try { + const msg = UserAccountData.decode(buf); + + const balance = decodeMxSignMagBigInt(msg.Balance); + const devReward = decodeMxSignMagBigInt(msg.DeveloperReward); + const address = bech32FromBytes(msg.Address); + const ownerAddress = bech32FromBytes(msg.OwnerAddress); + + return { + nonce: longToString(msg.Nonce), + balance: balance, + developerReward: devReward, + address: address, + ownerAddress: ownerAddress, + codeHash: bytesToHex(msg.CodeHash), + rootHash: bytesToHex(msg.RootHash), + userName: bytesToHex(msg.UserName), + codeMetadata: bytesToHex(msg.CodeMetadata), + }; + } catch (e: any) { + console.warn(`Could not decode as UserAccountData: ${e.message}`); + return null; + } +} + + +function getDecodedEsdtData(buf: any) { + try { + const msgTrieLeafData: TrieLeafData = TrieLeafData.decode(buf); + // console.log(msgTrieLeafData) + const bufEsdtData = msgTrieLeafData.value; + const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufEsdtData); + + const valueBigInt: bigint = decodeMxSignMagBigInt(msgEsdtData.Value); + const key = Buffer.from(bytesToHex(msgTrieLeafData.key), "hex").toString().slice('ELRONDesdt'.length); + const [identifier, nonce] = TokenParser.extractTokenIDAndNonceFromTokenStorageKey(key); + + return { + address: bech32FromHex(bytesToHex(msgTrieLeafData.address)), + identifier, + nonce, + type: msgEsdtData.Type, + value: valueBigInt, + propertiesHex: bytesToHex(msgEsdtData.Properties), + reservedHex: bytesToHex(msgEsdtData.Reserved), + tokenMetaData: msgEsdtData.TokenMetaData ?? null, + }; + } catch (e: any) { + console.warn(`Could not decode as EsdtData: ${e.message}`); + return null; + } +} + +export function decodeStateChangesRaw(stateChanges: any) { + const allAccounts: Record = {}; + const accounts = stateChanges.stateAccessesPerAccounts || {}; + + for (const accountHex of Object.keys(accounts)) { + const address = bech32FromHex(accountHex); + + const { stateAccess = [] } = accounts[accountHex] || {}; + const allDecoded: Record = {}; + stateAccess.forEach((sa: any, i: any) => { + + const base64AccountData = sa.mainTrieVal; + let decodedAccountData: any = null + if (base64AccountData) { + const bufAccountData = Buffer.from(base64AccountData, "base64"); + decodedAccountData = getDecodedUserAccountData(bufAccountData); + } + const dataTrieChanges = sa.dataTrieChanges; + + let allDecodedEsdtData: any[] = []; + if (!dataTrieChanges) { + console.log(` Entry #${i}: empty dataTrieChanges`); + } else { + for (const dataTrieChange of dataTrieChanges) { + if (dataTrieChange.version === 0) { + console.warn(` Entry #${i}: unsupported dataTrieChanges version 0`); + } else { + const bufEsdtData = Buffer.from(dataTrieChange.val, "base64"); + + const decodedEsdtData = getDecodedEsdtData(bufEsdtData); + if (decodedEsdtData) { + allDecodedEsdtData.push(decodedEsdtData); + } + } + } + + } + if (decodedAccountData || allDecodedEsdtData.length > 0) { + const groupedEsdtStates = allDecodedEsdtData.reduce>( + (acc, state) => { + const typeName = ESDTType[state.type]; // numeric -> string + if (typeName) { + acc[typeName].push(state); + } + + return acc; + }, + { + Fungible: [], + NonFungible: [], + NonFungibleV2: [], + SemiFungible: [], + MetaFungible: [], + DynamicNFT: [], + DynamicSFT: [], + DynamicMeta: [], + } + ); + if (allDecoded[address] === undefined) allDecoded[address] = []; + const newAccount = sa.accountChanges && sa.operation === StateAccessOperation.SaveAccount ? false : true; + + const accountChanges = sa.accountChanges + || { + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false + }; + + allDecoded[address].push({ + entry: `Entry #${i}`, + accountState: decodedAccountData, + esdtStates: groupedEsdtStates, + accountChanges, + newAccount, + }); + } + }); + if (Object.keys(allDecoded).length === 0) continue; + + allAccounts[address] = [...(allAccounts[address] || []), ...Object.values(allDecoded).flat()]; + } + + return allAccounts; +} + +export function getFinalStates(stateChanges: Record) { + const finalStates: Record = {}; + + + for (const [address, entries] of Object.entries(stateChanges)) { + let finalAccountState = {}; + const finalEsdtStates = { + Fungible: [] as any[], + NonFungible: [] as any[], + NonFungibleV2: [] as any[], + SemiFungible: [] as any[], + MetaFungible: [] as any[], + DynamicNFT: [] as any[], + DynamicSFT: [] as any[], + DynamicMeta: [] as any[], + }; + const finalAccountChanges = { + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false + }; + let finalNewAccount = false; + for (const entry of entries) { + const currentAccountState = entry.accountState; + const currentEsdtStates = entry.esdtStates; + const currentAccountChanges = entry.accountChanges; + const currentNewAccount = entry.newAccount as boolean; + + + finalNewAccount = finalNewAccount ? finalNewAccount : currentNewAccount; + + finalAccountState = currentAccountState; + // console.log(entry); + + (Object.entries(finalAccountChanges) as [keyof typeof finalAccountChanges, boolean][]).forEach( + ([key, value]) => { + finalAccountChanges[key] = value || currentAccountChanges[key]; + } + ); + + + (Object.entries(currentEsdtStates) as [keyof typeof finalEsdtStates, any[]][]).forEach( + ([tokenType, tokenChanges]) => { + for (const tokenChange of tokenChanges) { + let index = finalEsdtStates[tokenType].findIndex((item: any) => item.key === tokenChange.key); + index = index !== -1 ? index : finalEsdtStates[tokenType].length; + finalEsdtStates[tokenType][index] = tokenChange; + } + + } + ); + + } + + finalStates[address] = { + finalAccountState, + finalEsdtStates, + finalAccountChanges, + finalNewAccount, + }; + } + + return finalStates; +} \ No newline at end of file diff --git a/src/state-changes/utils/token.parser.ts b/src/state-changes/utils/token.parser.ts new file mode 100644 index 000000000..d49b78bb6 --- /dev/null +++ b/src/state-changes/utils/token.parser.ts @@ -0,0 +1,53 @@ +export class TokenParser { + // Constants from Go + private static readonly esdtTickerNumRandChars: number = 6; + private static readonly separatorChar: string = "-"; + private static readonly minLengthForTickerName: number = 3; + private static readonly maxLengthForTickerName: number = 10; + + /** + * ExtractTokenIDAndNonceFromTokenStorageKey + * Parses the token's storage key and extracts the identifier and the nonce. + * + * Examples: + * "ALC-1q2w3e" -> ["ALC-1q2w3e", "0"] (fungible, no nonce) + * "ALC-2w3e4rX" -> ["ALC-2w3e4r", "X"] (non-fungible, nonce = "X") + */ + public static extractTokenIDAndNonceFromTokenStorageKey( + tokenKey: string + ): [string, string] { + const token = tokenKey; + + const indexOfFirstHyphen = token.indexOf(this.separatorChar); + if (indexOfFirstHyphen < 0) { + return [tokenKey, "0"]; + } + + const tokenTicker = token.slice(0, indexOfFirstHyphen); + const randomSequencePlusNonce = token.slice(indexOfFirstHyphen + 1); + + const tokenTickerLen = tokenTicker.length; + + const areTickerAndRandomSequenceInvalid = + tokenTickerLen === 0 || + tokenTickerLen < this.minLengthForTickerName || + tokenTickerLen > this.maxLengthForTickerName || + randomSequencePlusNonce.length === 0; + + if (areTickerAndRandomSequenceInvalid) { + return [tokenKey, "0"]; + } + + if (randomSequencePlusNonce.length < this.esdtTickerNumRandChars + 1) { + return [tokenKey, "0"]; + } + + // ALC-1q2w3eX -> X is the nonce + const nonceStr = randomSequencePlusNonce.slice(this.esdtTickerNumRandChars); + + const numCharsSinceNonce = token.length - nonceStr.length; + const tokenID = token.slice(0, numCharsSinceNonce); + + return [tokenID, nonceStr || "0"]; + } +} diff --git a/src/state-changes/utils/trie_leaf_data.d.ts b/src/state-changes/utils/trie_leaf_data.d.ts new file mode 100644 index 000000000..560a4180b --- /dev/null +++ b/src/state-changes/utils/trie_leaf_data.d.ts @@ -0,0 +1,110 @@ +import * as $protobuf from "protobufjs"; +import Long = require("long"); +/** Properties of a TrieLeafData. */ +export interface ITrieLeafData { + + /** TrieLeafData value */ + value?: (Uint8Array|null); + + /** TrieLeafData key */ + key?: (Uint8Array|null); + + /** TrieLeafData address */ + address?: (Uint8Array|null); +} + +/** Represents a TrieLeafData. */ +export class TrieLeafData implements ITrieLeafData { + + /** + * Constructs a new TrieLeafData. + * @param [properties] Properties to set + */ + constructor(properties?: ITrieLeafData); + + /** TrieLeafData value. */ + public value: Uint8Array; + + /** TrieLeafData key. */ + public key: Uint8Array; + + /** TrieLeafData address. */ + public address: Uint8Array; + + /** + * Creates a new TrieLeafData instance using the specified properties. + * @param [properties] Properties to set + * @returns TrieLeafData instance + */ + public static create(properties?: ITrieLeafData): TrieLeafData; + + /** + * Encodes the specified TrieLeafData message. Does not implicitly {@link TrieLeafData.verify|verify} messages. + * @param message TrieLeafData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: ITrieLeafData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified TrieLeafData message, length delimited. Does not implicitly {@link TrieLeafData.verify|verify} messages. + * @param message TrieLeafData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: ITrieLeafData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a TrieLeafData message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns TrieLeafData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): TrieLeafData; + + /** + * Decodes a TrieLeafData message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns TrieLeafData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): TrieLeafData; + + /** + * Verifies a TrieLeafData message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a TrieLeafData message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns TrieLeafData + */ + public static fromObject(object: { [k: string]: any }): TrieLeafData; + + /** + * Creates a plain object from a TrieLeafData message. Also converts values to other types if specified. + * @param message TrieLeafData + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: TrieLeafData, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this TrieLeafData to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for TrieLeafData + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; +} diff --git a/src/state-changes/utils/trie_leaf_data.js b/src/state-changes/utils/trie_leaf_data.js new file mode 100644 index 000000000..55a82bf56 --- /dev/null +++ b/src/state-changes/utils/trie_leaf_data.js @@ -0,0 +1,291 @@ +/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/ +"use strict"; + +var $protobuf = require("protobufjs/minimal"); + +// Common aliases +var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; + +// Exported root namespace +var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); + +$root.TrieLeafData = (function() { + + /** + * Properties of a TrieLeafData. + * @exports ITrieLeafData + * @interface ITrieLeafData + * @property {Uint8Array|null} [value] TrieLeafData value + * @property {Uint8Array|null} [key] TrieLeafData key + * @property {Uint8Array|null} [address] TrieLeafData address + */ + + /** + * Constructs a new TrieLeafData. + * @exports TrieLeafData + * @classdesc Represents a TrieLeafData. + * @implements ITrieLeafData + * @constructor + * @param {ITrieLeafData=} [properties] Properties to set + */ + function TrieLeafData(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * TrieLeafData value. + * @member {Uint8Array} value + * @memberof TrieLeafData + * @instance + */ + TrieLeafData.prototype.value = $util.newBuffer([]); + + /** + * TrieLeafData key. + * @member {Uint8Array} key + * @memberof TrieLeafData + * @instance + */ + TrieLeafData.prototype.key = $util.newBuffer([]); + + /** + * TrieLeafData address. + * @member {Uint8Array} address + * @memberof TrieLeafData + * @instance + */ + TrieLeafData.prototype.address = $util.newBuffer([]); + + /** + * Creates a new TrieLeafData instance using the specified properties. + * @function create + * @memberof TrieLeafData + * @static + * @param {ITrieLeafData=} [properties] Properties to set + * @returns {TrieLeafData} TrieLeafData instance + */ + TrieLeafData.create = function create(properties) { + return new TrieLeafData(properties); + }; + + /** + * Encodes the specified TrieLeafData message. Does not implicitly {@link TrieLeafData.verify|verify} messages. + * @function encode + * @memberof TrieLeafData + * @static + * @param {ITrieLeafData} message TrieLeafData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + TrieLeafData.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.value != null && Object.hasOwnProperty.call(message, "value")) + writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.value); + if (message.key != null && Object.hasOwnProperty.call(message, "key")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.key); + if (message.address != null && Object.hasOwnProperty.call(message, "address")) + writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.address); + return writer; + }; + + /** + * Encodes the specified TrieLeafData message, length delimited. Does not implicitly {@link TrieLeafData.verify|verify} messages. + * @function encodeDelimited + * @memberof TrieLeafData + * @static + * @param {ITrieLeafData} message TrieLeafData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + TrieLeafData.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a TrieLeafData message from the specified reader or buffer. + * @function decode + * @memberof TrieLeafData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {TrieLeafData} TrieLeafData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + TrieLeafData.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.TrieLeafData(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.value = reader.bytes(); + break; + } + case 2: { + message.key = reader.bytes(); + break; + } + case 3: { + message.address = reader.bytes(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a TrieLeafData message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof TrieLeafData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {TrieLeafData} TrieLeafData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + TrieLeafData.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a TrieLeafData message. + * @function verify + * @memberof TrieLeafData + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + TrieLeafData.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.value != null && message.hasOwnProperty("value")) + if (!(message.value && typeof message.value.length === "number" || $util.isString(message.value))) + return "value: buffer expected"; + if (message.key != null && message.hasOwnProperty("key")) + if (!(message.key && typeof message.key.length === "number" || $util.isString(message.key))) + return "key: buffer expected"; + if (message.address != null && message.hasOwnProperty("address")) + if (!(message.address && typeof message.address.length === "number" || $util.isString(message.address))) + return "address: buffer expected"; + return null; + }; + + /** + * Creates a TrieLeafData message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof TrieLeafData + * @static + * @param {Object.} object Plain object + * @returns {TrieLeafData} TrieLeafData + */ + TrieLeafData.fromObject = function fromObject(object) { + if (object instanceof $root.TrieLeafData) + return object; + var message = new $root.TrieLeafData(); + if (object.value != null) + if (typeof object.value === "string") + $util.base64.decode(object.value, message.value = $util.newBuffer($util.base64.length(object.value)), 0); + else if (object.value.length >= 0) + message.value = object.value; + if (object.key != null) + if (typeof object.key === "string") + $util.base64.decode(object.key, message.key = $util.newBuffer($util.base64.length(object.key)), 0); + else if (object.key.length >= 0) + message.key = object.key; + if (object.address != null) + if (typeof object.address === "string") + $util.base64.decode(object.address, message.address = $util.newBuffer($util.base64.length(object.address)), 0); + else if (object.address.length >= 0) + message.address = object.address; + return message; + }; + + /** + * Creates a plain object from a TrieLeafData message. Also converts values to other types if specified. + * @function toObject + * @memberof TrieLeafData + * @static + * @param {TrieLeafData} message TrieLeafData + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + TrieLeafData.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + if (options.bytes === String) + object.value = ""; + else { + object.value = []; + if (options.bytes !== Array) + object.value = $util.newBuffer(object.value); + } + if (options.bytes === String) + object.key = ""; + else { + object.key = []; + if (options.bytes !== Array) + object.key = $util.newBuffer(object.key); + } + if (options.bytes === String) + object.address = ""; + else { + object.address = []; + if (options.bytes !== Array) + object.address = $util.newBuffer(object.address); + } + } + if (message.value != null && message.hasOwnProperty("value")) + object.value = options.bytes === String ? $util.base64.encode(message.value, 0, message.value.length) : options.bytes === Array ? Array.prototype.slice.call(message.value) : message.value; + if (message.key != null && message.hasOwnProperty("key")) + object.key = options.bytes === String ? $util.base64.encode(message.key, 0, message.key.length) : options.bytes === Array ? Array.prototype.slice.call(message.key) : message.key; + if (message.address != null && message.hasOwnProperty("address")) + object.address = options.bytes === String ? $util.base64.encode(message.address, 0, message.address.length) : options.bytes === Array ? Array.prototype.slice.call(message.address) : message.address; + return object; + }; + + /** + * Converts this TrieLeafData to JSON. + * @function toJSON + * @memberof TrieLeafData + * @instance + * @returns {Object.} JSON object + */ + TrieLeafData.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for TrieLeafData + * @function getTypeUrl + * @memberof TrieLeafData + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + TrieLeafData.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/TrieLeafData"; + }; + + return TrieLeafData; +})(); + +module.exports = $root; diff --git a/src/state-changes/utils/trie_leaf_data.proto b/src/state-changes/utils/trie_leaf_data.proto new file mode 100644 index 000000000..8a17f74cb --- /dev/null +++ b/src/state-changes/utils/trie_leaf_data.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +message TrieLeafData { + bytes value = 1; + bytes key = 2; + bytes address = 3; +} diff --git a/src/state-changes/utils/user_account.pb.d.ts b/src/state-changes/utils/user_account.pb.d.ts new file mode 100644 index 000000000..508e70625 --- /dev/null +++ b/src/state-changes/utils/user_account.pb.d.ts @@ -0,0 +1,146 @@ +import * as $protobuf from "protobufjs"; +import Long = require("long"); +/** Properties of a UserAccountData. */ +export interface IUserAccountData { + + /** UserAccountData Nonce */ + Nonce?: (number|Long|null); + + /** UserAccountData Balance */ + Balance?: (Uint8Array|null); + + /** UserAccountData CodeHash */ + CodeHash?: (Uint8Array|null); + + /** UserAccountData RootHash */ + RootHash?: (Uint8Array|null); + + /** UserAccountData Address */ + Address?: (Uint8Array|null); + + /** UserAccountData DeveloperReward */ + DeveloperReward?: (Uint8Array|null); + + /** UserAccountData OwnerAddress */ + OwnerAddress?: (Uint8Array|null); + + /** UserAccountData UserName */ + UserName?: (Uint8Array|null); + + /** UserAccountData CodeMetadata */ + CodeMetadata?: (Uint8Array|null); +} + +/** Represents a UserAccountData. */ +export class UserAccountData implements IUserAccountData { + + /** + * Constructs a new UserAccountData. + * @param [properties] Properties to set + */ + constructor(properties?: IUserAccountData); + + /** UserAccountData Nonce. */ + public Nonce: (number|Long); + + /** UserAccountData Balance. */ + public Balance: Uint8Array; + + /** UserAccountData CodeHash. */ + public CodeHash: Uint8Array; + + /** UserAccountData RootHash. */ + public RootHash: Uint8Array; + + /** UserAccountData Address. */ + public Address: Uint8Array; + + /** UserAccountData DeveloperReward. */ + public DeveloperReward: Uint8Array; + + /** UserAccountData OwnerAddress. */ + public OwnerAddress: Uint8Array; + + /** UserAccountData UserName. */ + public UserName: Uint8Array; + + /** UserAccountData CodeMetadata. */ + public CodeMetadata: Uint8Array; + + /** + * Creates a new UserAccountData instance using the specified properties. + * @param [properties] Properties to set + * @returns UserAccountData instance + */ + public static create(properties?: IUserAccountData): UserAccountData; + + /** + * Encodes the specified UserAccountData message. Does not implicitly {@link UserAccountData.verify|verify} messages. + * @param message UserAccountData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: IUserAccountData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified UserAccountData message, length delimited. Does not implicitly {@link UserAccountData.verify|verify} messages. + * @param message UserAccountData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: IUserAccountData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a UserAccountData message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns UserAccountData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): UserAccountData; + + /** + * Decodes a UserAccountData message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns UserAccountData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): UserAccountData; + + /** + * Verifies a UserAccountData message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a UserAccountData message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns UserAccountData + */ + public static fromObject(object: { [k: string]: any }): UserAccountData; + + /** + * Creates a plain object from a UserAccountData message. Also converts values to other types if specified. + * @param message UserAccountData + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: UserAccountData, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this UserAccountData to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for UserAccountData + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; +} diff --git a/src/state-changes/utils/user_account.pb.js b/src/state-changes/utils/user_account.pb.js new file mode 100644 index 000000000..9dd4952fd --- /dev/null +++ b/src/state-changes/utils/user_account.pb.js @@ -0,0 +1,488 @@ +/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/ +"use strict"; + +var $protobuf = require("protobufjs/minimal"); + +// Common aliases +var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; + +// Exported root namespace +var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); + +$root.UserAccountData = (function() { + + /** + * Properties of a UserAccountData. + * @exports IUserAccountData + * @interface IUserAccountData + * @property {number|Long|null} [Nonce] UserAccountData Nonce + * @property {Uint8Array|null} [Balance] UserAccountData Balance + * @property {Uint8Array|null} [CodeHash] UserAccountData CodeHash + * @property {Uint8Array|null} [RootHash] UserAccountData RootHash + * @property {Uint8Array|null} [Address] UserAccountData Address + * @property {Uint8Array|null} [DeveloperReward] UserAccountData DeveloperReward + * @property {Uint8Array|null} [OwnerAddress] UserAccountData OwnerAddress + * @property {Uint8Array|null} [UserName] UserAccountData UserName + * @property {Uint8Array|null} [CodeMetadata] UserAccountData CodeMetadata + */ + + /** + * Constructs a new UserAccountData. + * @exports UserAccountData + * @classdesc Represents a UserAccountData. + * @implements IUserAccountData + * @constructor + * @param {IUserAccountData=} [properties] Properties to set + */ + function UserAccountData(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * UserAccountData Nonce. + * @member {number|Long} Nonce + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.Nonce = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + + /** + * UserAccountData Balance. + * @member {Uint8Array} Balance + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.Balance = $util.newBuffer([]); + + /** + * UserAccountData CodeHash. + * @member {Uint8Array} CodeHash + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.CodeHash = $util.newBuffer([]); + + /** + * UserAccountData RootHash. + * @member {Uint8Array} RootHash + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.RootHash = $util.newBuffer([]); + + /** + * UserAccountData Address. + * @member {Uint8Array} Address + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.Address = $util.newBuffer([]); + + /** + * UserAccountData DeveloperReward. + * @member {Uint8Array} DeveloperReward + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.DeveloperReward = $util.newBuffer([]); + + /** + * UserAccountData OwnerAddress. + * @member {Uint8Array} OwnerAddress + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.OwnerAddress = $util.newBuffer([]); + + /** + * UserAccountData UserName. + * @member {Uint8Array} UserName + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.UserName = $util.newBuffer([]); + + /** + * UserAccountData CodeMetadata. + * @member {Uint8Array} CodeMetadata + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.CodeMetadata = $util.newBuffer([]); + + /** + * Creates a new UserAccountData instance using the specified properties. + * @function create + * @memberof UserAccountData + * @static + * @param {IUserAccountData=} [properties] Properties to set + * @returns {UserAccountData} UserAccountData instance + */ + UserAccountData.create = function create(properties) { + return new UserAccountData(properties); + }; + + /** + * Encodes the specified UserAccountData message. Does not implicitly {@link UserAccountData.verify|verify} messages. + * @function encode + * @memberof UserAccountData + * @static + * @param {IUserAccountData} message UserAccountData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + UserAccountData.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.Nonce != null && Object.hasOwnProperty.call(message, "Nonce")) + writer.uint32(/* id 1, wireType 0 =*/8).uint64(message.Nonce); + if (message.Balance != null && Object.hasOwnProperty.call(message, "Balance")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.Balance); + if (message.CodeHash != null && Object.hasOwnProperty.call(message, "CodeHash")) + writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.CodeHash); + if (message.RootHash != null && Object.hasOwnProperty.call(message, "RootHash")) + writer.uint32(/* id 4, wireType 2 =*/34).bytes(message.RootHash); + if (message.Address != null && Object.hasOwnProperty.call(message, "Address")) + writer.uint32(/* id 5, wireType 2 =*/42).bytes(message.Address); + if (message.DeveloperReward != null && Object.hasOwnProperty.call(message, "DeveloperReward")) + writer.uint32(/* id 6, wireType 2 =*/50).bytes(message.DeveloperReward); + if (message.OwnerAddress != null && Object.hasOwnProperty.call(message, "OwnerAddress")) + writer.uint32(/* id 7, wireType 2 =*/58).bytes(message.OwnerAddress); + if (message.UserName != null && Object.hasOwnProperty.call(message, "UserName")) + writer.uint32(/* id 8, wireType 2 =*/66).bytes(message.UserName); + if (message.CodeMetadata != null && Object.hasOwnProperty.call(message, "CodeMetadata")) + writer.uint32(/* id 9, wireType 2 =*/74).bytes(message.CodeMetadata); + return writer; + }; + + /** + * Encodes the specified UserAccountData message, length delimited. Does not implicitly {@link UserAccountData.verify|verify} messages. + * @function encodeDelimited + * @memberof UserAccountData + * @static + * @param {IUserAccountData} message UserAccountData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + UserAccountData.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a UserAccountData message from the specified reader or buffer. + * @function decode + * @memberof UserAccountData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {UserAccountData} UserAccountData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + UserAccountData.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.UserAccountData(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.Nonce = reader.uint64(); + break; + } + case 2: { + message.Balance = reader.bytes(); + break; + } + case 3: { + message.CodeHash = reader.bytes(); + break; + } + case 4: { + message.RootHash = reader.bytes(); + break; + } + case 5: { + message.Address = reader.bytes(); + break; + } + case 6: { + message.DeveloperReward = reader.bytes(); + break; + } + case 7: { + message.OwnerAddress = reader.bytes(); + break; + } + case 8: { + message.UserName = reader.bytes(); + break; + } + case 9: { + message.CodeMetadata = reader.bytes(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a UserAccountData message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof UserAccountData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {UserAccountData} UserAccountData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + UserAccountData.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a UserAccountData message. + * @function verify + * @memberof UserAccountData + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + UserAccountData.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.Nonce != null && message.hasOwnProperty("Nonce")) + if (!$util.isInteger(message.Nonce) && !(message.Nonce && $util.isInteger(message.Nonce.low) && $util.isInteger(message.Nonce.high))) + return "Nonce: integer|Long expected"; + if (message.Balance != null && message.hasOwnProperty("Balance")) + if (!(message.Balance && typeof message.Balance.length === "number" || $util.isString(message.Balance))) + return "Balance: buffer expected"; + if (message.CodeHash != null && message.hasOwnProperty("CodeHash")) + if (!(message.CodeHash && typeof message.CodeHash.length === "number" || $util.isString(message.CodeHash))) + return "CodeHash: buffer expected"; + if (message.RootHash != null && message.hasOwnProperty("RootHash")) + if (!(message.RootHash && typeof message.RootHash.length === "number" || $util.isString(message.RootHash))) + return "RootHash: buffer expected"; + if (message.Address != null && message.hasOwnProperty("Address")) + if (!(message.Address && typeof message.Address.length === "number" || $util.isString(message.Address))) + return "Address: buffer expected"; + if (message.DeveloperReward != null && message.hasOwnProperty("DeveloperReward")) + if (!(message.DeveloperReward && typeof message.DeveloperReward.length === "number" || $util.isString(message.DeveloperReward))) + return "DeveloperReward: buffer expected"; + if (message.OwnerAddress != null && message.hasOwnProperty("OwnerAddress")) + if (!(message.OwnerAddress && typeof message.OwnerAddress.length === "number" || $util.isString(message.OwnerAddress))) + return "OwnerAddress: buffer expected"; + if (message.UserName != null && message.hasOwnProperty("UserName")) + if (!(message.UserName && typeof message.UserName.length === "number" || $util.isString(message.UserName))) + return "UserName: buffer expected"; + if (message.CodeMetadata != null && message.hasOwnProperty("CodeMetadata")) + if (!(message.CodeMetadata && typeof message.CodeMetadata.length === "number" || $util.isString(message.CodeMetadata))) + return "CodeMetadata: buffer expected"; + return null; + }; + + /** + * Creates a UserAccountData message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof UserAccountData + * @static + * @param {Object.} object Plain object + * @returns {UserAccountData} UserAccountData + */ + UserAccountData.fromObject = function fromObject(object) { + if (object instanceof $root.UserAccountData) + return object; + var message = new $root.UserAccountData(); + if (object.Nonce != null) + if ($util.Long) + (message.Nonce = $util.Long.fromValue(object.Nonce)).unsigned = true; + else if (typeof object.Nonce === "string") + message.Nonce = parseInt(object.Nonce, 10); + else if (typeof object.Nonce === "number") + message.Nonce = object.Nonce; + else if (typeof object.Nonce === "object") + message.Nonce = new $util.LongBits(object.Nonce.low >>> 0, object.Nonce.high >>> 0).toNumber(true); + if (object.Balance != null) + if (typeof object.Balance === "string") + $util.base64.decode(object.Balance, message.Balance = $util.newBuffer($util.base64.length(object.Balance)), 0); + else if (object.Balance.length >= 0) + message.Balance = object.Balance; + if (object.CodeHash != null) + if (typeof object.CodeHash === "string") + $util.base64.decode(object.CodeHash, message.CodeHash = $util.newBuffer($util.base64.length(object.CodeHash)), 0); + else if (object.CodeHash.length >= 0) + message.CodeHash = object.CodeHash; + if (object.RootHash != null) + if (typeof object.RootHash === "string") + $util.base64.decode(object.RootHash, message.RootHash = $util.newBuffer($util.base64.length(object.RootHash)), 0); + else if (object.RootHash.length >= 0) + message.RootHash = object.RootHash; + if (object.Address != null) + if (typeof object.Address === "string") + $util.base64.decode(object.Address, message.Address = $util.newBuffer($util.base64.length(object.Address)), 0); + else if (object.Address.length >= 0) + message.Address = object.Address; + if (object.DeveloperReward != null) + if (typeof object.DeveloperReward === "string") + $util.base64.decode(object.DeveloperReward, message.DeveloperReward = $util.newBuffer($util.base64.length(object.DeveloperReward)), 0); + else if (object.DeveloperReward.length >= 0) + message.DeveloperReward = object.DeveloperReward; + if (object.OwnerAddress != null) + if (typeof object.OwnerAddress === "string") + $util.base64.decode(object.OwnerAddress, message.OwnerAddress = $util.newBuffer($util.base64.length(object.OwnerAddress)), 0); + else if (object.OwnerAddress.length >= 0) + message.OwnerAddress = object.OwnerAddress; + if (object.UserName != null) + if (typeof object.UserName === "string") + $util.base64.decode(object.UserName, message.UserName = $util.newBuffer($util.base64.length(object.UserName)), 0); + else if (object.UserName.length >= 0) + message.UserName = object.UserName; + if (object.CodeMetadata != null) + if (typeof object.CodeMetadata === "string") + $util.base64.decode(object.CodeMetadata, message.CodeMetadata = $util.newBuffer($util.base64.length(object.CodeMetadata)), 0); + else if (object.CodeMetadata.length >= 0) + message.CodeMetadata = object.CodeMetadata; + return message; + }; + + /** + * Creates a plain object from a UserAccountData message. Also converts values to other types if specified. + * @function toObject + * @memberof UserAccountData + * @static + * @param {UserAccountData} message UserAccountData + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + UserAccountData.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + if ($util.Long) { + var long = new $util.Long(0, 0, true); + object.Nonce = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; + } else + object.Nonce = options.longs === String ? "0" : 0; + if (options.bytes === String) + object.Balance = ""; + else { + object.Balance = []; + if (options.bytes !== Array) + object.Balance = $util.newBuffer(object.Balance); + } + if (options.bytes === String) + object.CodeHash = ""; + else { + object.CodeHash = []; + if (options.bytes !== Array) + object.CodeHash = $util.newBuffer(object.CodeHash); + } + if (options.bytes === String) + object.RootHash = ""; + else { + object.RootHash = []; + if (options.bytes !== Array) + object.RootHash = $util.newBuffer(object.RootHash); + } + if (options.bytes === String) + object.Address = ""; + else { + object.Address = []; + if (options.bytes !== Array) + object.Address = $util.newBuffer(object.Address); + } + if (options.bytes === String) + object.DeveloperReward = ""; + else { + object.DeveloperReward = []; + if (options.bytes !== Array) + object.DeveloperReward = $util.newBuffer(object.DeveloperReward); + } + if (options.bytes === String) + object.OwnerAddress = ""; + else { + object.OwnerAddress = []; + if (options.bytes !== Array) + object.OwnerAddress = $util.newBuffer(object.OwnerAddress); + } + if (options.bytes === String) + object.UserName = ""; + else { + object.UserName = []; + if (options.bytes !== Array) + object.UserName = $util.newBuffer(object.UserName); + } + if (options.bytes === String) + object.CodeMetadata = ""; + else { + object.CodeMetadata = []; + if (options.bytes !== Array) + object.CodeMetadata = $util.newBuffer(object.CodeMetadata); + } + } + if (message.Nonce != null && message.hasOwnProperty("Nonce")) + if (typeof message.Nonce === "number") + object.Nonce = options.longs === String ? String(message.Nonce) : message.Nonce; + else + object.Nonce = options.longs === String ? $util.Long.prototype.toString.call(message.Nonce) : options.longs === Number ? new $util.LongBits(message.Nonce.low >>> 0, message.Nonce.high >>> 0).toNumber(true) : message.Nonce; + if (message.Balance != null && message.hasOwnProperty("Balance")) + object.Balance = options.bytes === String ? $util.base64.encode(message.Balance, 0, message.Balance.length) : options.bytes === Array ? Array.prototype.slice.call(message.Balance) : message.Balance; + if (message.CodeHash != null && message.hasOwnProperty("CodeHash")) + object.CodeHash = options.bytes === String ? $util.base64.encode(message.CodeHash, 0, message.CodeHash.length) : options.bytes === Array ? Array.prototype.slice.call(message.CodeHash) : message.CodeHash; + if (message.RootHash != null && message.hasOwnProperty("RootHash")) + object.RootHash = options.bytes === String ? $util.base64.encode(message.RootHash, 0, message.RootHash.length) : options.bytes === Array ? Array.prototype.slice.call(message.RootHash) : message.RootHash; + if (message.Address != null && message.hasOwnProperty("Address")) + object.Address = options.bytes === String ? $util.base64.encode(message.Address, 0, message.Address.length) : options.bytes === Array ? Array.prototype.slice.call(message.Address) : message.Address; + if (message.DeveloperReward != null && message.hasOwnProperty("DeveloperReward")) + object.DeveloperReward = options.bytes === String ? $util.base64.encode(message.DeveloperReward, 0, message.DeveloperReward.length) : options.bytes === Array ? Array.prototype.slice.call(message.DeveloperReward) : message.DeveloperReward; + if (message.OwnerAddress != null && message.hasOwnProperty("OwnerAddress")) + object.OwnerAddress = options.bytes === String ? $util.base64.encode(message.OwnerAddress, 0, message.OwnerAddress.length) : options.bytes === Array ? Array.prototype.slice.call(message.OwnerAddress) : message.OwnerAddress; + if (message.UserName != null && message.hasOwnProperty("UserName")) + object.UserName = options.bytes === String ? $util.base64.encode(message.UserName, 0, message.UserName.length) : options.bytes === Array ? Array.prototype.slice.call(message.UserName) : message.UserName; + if (message.CodeMetadata != null && message.hasOwnProperty("CodeMetadata")) + object.CodeMetadata = options.bytes === String ? $util.base64.encode(message.CodeMetadata, 0, message.CodeMetadata.length) : options.bytes === Array ? Array.prototype.slice.call(message.CodeMetadata) : message.CodeMetadata; + return object; + }; + + /** + * Converts this UserAccountData to JSON. + * @function toJSON + * @memberof UserAccountData + * @instance + * @returns {Object.} JSON object + */ + UserAccountData.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for UserAccountData + * @function getTypeUrl + * @memberof UserAccountData + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + UserAccountData.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/UserAccountData"; + }; + + return UserAccountData; +})(); + +module.exports = $root; diff --git a/src/state-changes/utils/user_account.proto b/src/state-changes/utils/user_account.proto new file mode 100644 index 000000000..d0780866e --- /dev/null +++ b/src/state-changes/utils/user_account.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +message UserAccountData { + uint64 Nonce = 1; + bytes Balance = 2; // big.Int in Go -> bytes on the wire + bytes CodeHash = 3; + bytes RootHash = 4; + bytes Address = 5; + bytes DeveloperReward = 6; // big.Int in Go -> bytes on the wire + bytes OwnerAddress = 7; + bytes UserName = 8; + bytes CodeMetadata = 9; +} From f8de446e444cc155c8a2aa800f20083bdc64c611 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Mon, 1 Sep 2025 18:42:01 +0300 Subject: [PATCH 14/90] update states in db --- src/common/indexer/db/index.ts | 5 + src/common/indexer/db/mongodb.module.ts | 34 ++ .../indexer/db/mongodb.repositories.module.ts | 21 + .../account.details.repository.ts | 362 ++++++++++++++++++ src/common/indexer/db/repositories/index.ts | 1 + .../db/schemas/account.details.schema.ts | 125 ++++++ src/common/indexer/db/schemas/index.ts | 1 + src/common/indexer/db/utils/errors.ts | 3 + src/common/indexer/db/utils/index.ts | 1 + .../entities/state.changes.entity.ts | 84 +++- .../state.changes.consumer.service.ts | 50 ++- src/state-changes/state.changes.module.ts | 3 + .../utils/state-changes.utils.ts | 60 +-- 13 files changed, 701 insertions(+), 49 deletions(-) create mode 100644 src/common/indexer/db/index.ts create mode 100644 src/common/indexer/db/mongodb.module.ts create mode 100644 src/common/indexer/db/mongodb.repositories.module.ts create mode 100644 src/common/indexer/db/repositories/account.details.repository.ts create mode 100644 src/common/indexer/db/repositories/index.ts create mode 100644 src/common/indexer/db/schemas/account.details.schema.ts create mode 100644 src/common/indexer/db/schemas/index.ts create mode 100644 src/common/indexer/db/utils/errors.ts create mode 100644 src/common/indexer/db/utils/index.ts diff --git a/src/common/indexer/db/index.ts b/src/common/indexer/db/index.ts new file mode 100644 index 000000000..edbda0b95 --- /dev/null +++ b/src/common/indexer/db/index.ts @@ -0,0 +1,5 @@ +export * from './mongodb.module'; +export * from './mongodb.repositories.module'; +export * from './repositories'; +export * from './schemas'; +export * from './utils'; diff --git a/src/common/indexer/db/mongodb.module.ts b/src/common/indexer/db/mongodb.module.ts new file mode 100644 index 000000000..d954041f5 --- /dev/null +++ b/src/common/indexer/db/mongodb.module.ts @@ -0,0 +1,34 @@ +import { Module } from "@nestjs/common"; +import { MongooseModule } from "@nestjs/mongoose"; +import { ApiConfigModule } from "src/common/api-config/api.config.module"; +import { ApiConfigService } from "src/common/api-config/api.config.service"; +import { AccountDetails, AccountDetailsSchema } from "./schemas"; +import { AccountDetailsRepository } from "./repositories"; +import { EventEmitterModule } from "@nestjs/event-emitter"; + + +@Module({ + imports: [ + EventEmitterModule.forRoot({ maxListeners: 1 }), + MongooseModule.forRootAsync({ + imports: [ApiConfigModule], + inject: [ApiConfigService], + useFactory: (apiConfigService: ApiConfigService) => ({ + uri: apiConfigService.getDatabaseUrl().replace(":27017", ''), // TODO: remove this hack + tls: false, + tlsAllowInvalidCertificates: true, + }), + }), + MongooseModule.forFeature([ + { name: AccountDetails.name, schema: AccountDetailsSchema }, + + ]), + ], + providers: [ + AccountDetailsRepository, + ], + exports: [ + AccountDetailsRepository, + ], +}) +export class MongoDbModule { } diff --git a/src/common/indexer/db/mongodb.repositories.module.ts b/src/common/indexer/db/mongodb.repositories.module.ts new file mode 100644 index 000000000..ed2e75951 --- /dev/null +++ b/src/common/indexer/db/mongodb.repositories.module.ts @@ -0,0 +1,21 @@ +import { Global, Module } from "@nestjs/common"; +import { MongooseModule } from "@nestjs/mongoose"; +import { AccountDetails, AccountDetailsSchema } from "./schemas"; +import { AccountDetailsRepository } from "./repositories"; + + +@Global() +@Module({ + imports: [ + MongooseModule.forFeature([ + { name: AccountDetails.name, schema: AccountDetailsSchema }, + ]), + ], + providers: [ + AccountDetailsRepository, + ], + exports: [ + AccountDetailsRepository, + ], +}) +export class MongoDbRepositoriesModule { } diff --git a/src/common/indexer/db/repositories/account.details.repository.ts b/src/common/indexer/db/repositories/account.details.repository.ts new file mode 100644 index 000000000..9eae5d781 --- /dev/null +++ b/src/common/indexer/db/repositories/account.details.repository.ts @@ -0,0 +1,362 @@ +import { Model } from 'mongoose'; +import { LogPerformanceAsync } from 'src/utils/log.performance.decorator'; +import { MetricsEvents } from 'src/utils/metrics-events.constants'; +import { AccountDetails } from '../schemas'; +import { InjectModel } from '@nestjs/mongoose'; +import { QueryPagination } from 'src/common/entities/query.pagination'; +import { TokenWithBalance } from 'src/endpoints/tokens/entities/token.with.balance'; +import { NftAccount } from 'src/endpoints/nfts/entities/nft.account'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AccountDetailsRepository { + static readonly exclusionFields = { + _id: 0, + __v: 0, + updatedAt: 0, + createdAt: 0, + address: 0, + balance: 0, + nonce: 0, + timestamp: 0, + shard: 0, + ownerAddress: 0, + assets: 0, + deployedAt: 0, + deployTxHash: 0, + ownerAssets: 0, + isVerified: 0, + txCount: 0, + scrCount: 0, + transfersLast24h: 0, + code: 0, + codeHash: 0, + rootHash: 0, + username: 0, + developerReward: 0, + isUpgradeable: 0, + isReadable: 0, isPayable: 0, + isPayableBySmartContract: 0, + scamInfo: 0, + nftCollections: 0, + activeGuardianActivationEpoch: 0, + activeGuardianAddress: 0, + activeGuardianServiceUid: 0, + pendingGuardianActivationEpoch: 0, + pendingGuardianAddress: 0, + pendingGuardianServiceUid: 0, + isGuarded: 0, + } + constructor( + @InjectModel(AccountDetails.name) + private readonly accountDetailsModel: Model + ) { } + + @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-tokens') + async getTokensForAddress(address: string, queryPagination: QueryPagination): Promise { + try { + // TODO: add more fields in project on demand + const result = await this.accountDetailsModel.aggregate([ + { $match: { address } }, + { + $project: { + _id: 0, + tokens: { + $slice: ["$tokens", queryPagination.from, queryPagination.size] + }, + } + }, + { + $project: { + "tokens.type": 1, + "tokens.subType": 1, + "tokens.identifier": 1, + "tokens.collection": 1, + "tokens.name": 1, + "tokens.nonce": 1, + "tokens.decimals": 1, + "tokens.balance": 1, + } + } + ]).exec(); + // const result = await this.accountDetailsModel.findOne( + // { address }, + // { + // tokens: { + // $slice: [queryPagination.from, queryPagination.size], + // }, + // "tokens.balance": 0, // Exclude direct balance + // ...AccountDetailsRepository.exclusionFields, + // } + // ).lean(); + //@ts-ignore + // console.log('result', result); + // console.log('result', result); + return result[0]?.tokens ?? []; + } catch (error) { + console.error(`Error fetching tokens for address: ${address}:`, error); + return []; + } + } + + @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-tokens') + async getTokenForAddress(address: string, identifier: string): Promise { + try { + // TODO: add more fields in project on demand + // TODO: search for token efficiently: return first occurence and use index on identifier + const result = await this.accountDetailsModel.aggregate([ + { $match: { address } }, + { + $project: { + _id: 0, + tokens: { + $filter: { + input: "$tokens", + as: "token", + cond: { $eq: ["$$token.identifier", identifier] } + } + } + } + }, + { + $project: { + "tokens.type": 1, + "tokens.subType": 1, + "tokens.identifier": 1, + "tokens.collection": 1, + "tokens.name": 1, + "tokens.nonce": 1, + "tokens.decimals": 1, + "tokens.balance": 1, + } + } + ]).exec(); + // console.log('result', result); + // const result = await this.accountDetailsModel.findOne( + // { address }, + // { + // tokens: { + // $slice: [queryPagination.from, queryPagination.size], + // }, + // "tokens.balance": 0, // Exclude direct balance + // ...AccountDetailsRepository.exclusionFields, + // } + // ).lean(); + //@ts-ignore + // console.log('result', result); + // console.log('result', result); + // console.log(result[0].tokens) + return result[0]?.tokens[0] ?? undefined; + } catch (error) { + console.error(`Error fetching token with identifier ${identifier} for address: ${address}:`, error); + return undefined; + } + } + + @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-nfts') + async getNftForAddress(address: string, identifier: string): Promise { + try { + // TODO: add more fields in project on demand + // TODO: search for nft efficiently: return first occurence and use index on identifier + const result = await this.accountDetailsModel.aggregate([ + { $match: { address } }, + { + $project: { + _id: 0, + nfts: { + $filter: { + input: "$nfts", + as: "nft", + cond: { $eq: ["$$nft.identifier", identifier] } + } + } + } + }, + { + $project: { + "nfts.identifier": 1, + "nfts.collection": 1, + "nfts.nonce": 1, + "nfts.type": 1, + "nfts.subType": 1, + "nfts.name": 1, + } + } + ]).exec(); + // const result = await this.accountDetailsModel.findOne( + // { address }, + // { + // nfts: { + // $slice: [queryPagination.from, queryPagination.size] + // }, + // tokens: 0, + // ...AccountDetailsRepository.exclusionFields, + // } + // ).lean(); + return result[0]?.nfts[0] ?? undefined; + } catch (error) { + console.error(`Error fetching nft with identifier ${identifier} for address: ${address}:`, error); + return undefined; + } + } + + @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-nfts') + async getNftsForAddress(address: string, queryPagination: QueryPagination): Promise { + try { + // TODO: add more fields in project on demand + const result = await this.accountDetailsModel.aggregate([ + { $match: { address } }, + { + $project: { + _id: 0, + nfts: { $slice: ["$nfts", queryPagination.from, queryPagination.size] } + } + }, + { + $project: { + "nfts.identifier": 1, + "nfts.collection": 1, + "nfts.nonce": 1, + "nfts.type": 1, + "nfts.subType": 1, + "nfts.name": 1, + } + } + ]).exec(); + // const result = await this.accountDetailsModel.findOne( + // { address }, + // { + // nfts: { + // $slice: [queryPagination.from, queryPagination.size] + // }, + // tokens: 0, + // ...AccountDetailsRepository.exclusionFields, + // } + // ).lean(); + + return result[0]?.nfts || []; + } catch (error) { + console.error(`Error fetching nfts for address: ${address}:`, error); + return []; + } + } + + @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-details') + async getAccount(address: string): Promise { + try { + const accountDb = await this.accountDetailsModel.findOne( + { address }, + { _id: 0, __v: 0, tokens: 0, nfts: 0, updatedAt: 0, createdAt: 0, code: 0 } + ).lean(); + if (!accountDb) { + return null; + } + return accountDb; + } catch (error) { + console.error('Error fetching account:', error); + return null; + } + } + + @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-details') + async updateAccount(accountDetailed: AccountDetails): Promise { + try { + // Create update object with all fields from accountDetailed + const updateFields: Partial = {}; + + // Helper function to check if a value is valid for update + const isValidValue = (value: any): boolean => { + return value !== undefined && value !== null; + }; + + // Build update object with all valid fields from accountDetailed + Object.entries(accountDetailed).forEach(([key, value]) => { + if (isValidValue(value)) { + updateFields[key as keyof AccountDetails] = value; + } + }); + // Use findOneAndUpdate with upsert option + const updatedAccount = await this.accountDetailsModel.findOneAndUpdate( + { address: accountDetailed.address }, + { $set: updateFields }, + { + new: true, // Return the updated document + upsert: true, // Create if doesn't exist + lean: true, // Return plain JavaScript object + projection: { __v: 0, __id: 0, updatedAt: 0 } // Exclude __v field + } + ); + // console.log('updatedAccount', updatedAccount); + return updatedAccount; + } catch (error: any) { + // Handle potential duplicate key errors + if (error.code !== 11000) { + throw error; + } + return null; + } + } + + @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-details') + async updateAccounts(accounts: AccountDetails[]): Promise { + try { + if (!accounts.length) return []; + + const operations = accounts.map((accountDetailed) => { + const updateFields: any = {}; + + const isValidValue = (value: any): boolean => + value !== undefined && value !== null; + + Object.entries(accountDetailed).forEach(([key, value]) => { + if (isValidValue(value) && key !== "tokens" && key !== "nfts") { + updateFields[key as keyof AccountDetails] = value; + } + }); + + const updateOps: any = {}; + if (Object.keys(updateFields).length > 0) { + updateOps.$set = updateFields; + } + + if (accountDetailed.tokens?.length) { + updateOps.$push = updateOps.$push || {}; + updateOps.$push.tokens = updateOps.$push.tokens || { $each: [] }; + + for (const token of accountDetailed.tokens) { + updateOps.$push.tokens.$each.push(token); + } + } + + if (accountDetailed.nfts?.length) { + updateOps.$push = updateOps.$push || {}; + updateOps.$push.nfts = updateOps.$push.nfts || { $each: [] }; + + for (const nft of accountDetailed.nfts) { + updateOps.$push.nfts.$each.push(nft); + } + } + + return { + updateOne: { + filter: { address: accountDetailed.address }, + update: { + ...updateOps, + + }, + upsert: true, + }, + }; + }); + + const result = await this.accountDetailsModel.bulkWrite(operations, { + ordered: true, + }); + + return result; + } catch (error: any) { + throw error; + } + } + +} \ No newline at end of file diff --git a/src/common/indexer/db/repositories/index.ts b/src/common/indexer/db/repositories/index.ts new file mode 100644 index 000000000..dc9908dae --- /dev/null +++ b/src/common/indexer/db/repositories/index.ts @@ -0,0 +1 @@ +export * from './account.details.repository'; \ No newline at end of file diff --git a/src/common/indexer/db/schemas/account.details.schema.ts b/src/common/indexer/db/schemas/account.details.schema.ts new file mode 100644 index 000000000..659fb8dd2 --- /dev/null +++ b/src/common/indexer/db/schemas/account.details.schema.ts @@ -0,0 +1,125 @@ +import { Prop, SchemaFactory, Schema } from '@nestjs/mongoose'; +import mongoose, { HydratedDocument } from 'mongoose'; +import { AccountAssets } from 'src/common/assets/entities/account.assets'; +import { ScamInfo } from 'src/common/entities/scam-info.dto'; +import { NftCollectionAccount } from 'src/endpoints/collections/entities/nft.collection.account'; +import { NftAccount } from 'src/endpoints/nfts/entities/nft.account'; +import { TokenWithBalance } from 'src/endpoints/tokens/entities/token.with.balance'; + +export type AccountDetailsDocument = HydratedDocument; + +@Schema({ collection: 'account-details', timestamps: true }) +export class AccountDetails { + @Prop({ type: mongoose.Schema.Types.ObjectId, auto: true }) + _id!: string; + + @Prop({ required: true, type: String }) + address: string = ''; + + @Prop({ required: true, type: String }) + balance: string = ''; + + @Prop({ required: true, type: String }) + nonce: string = '0'; + + @Prop({ required: true, type: Number }) + timestamp: number = 0; + + @Prop({ required: true, type: Number }) + shard: number = 0; + + @Prop({ required: true, type: String }) + ownerAddress: string = ''; + + @Prop({ type: Object, required: false }) + assets?: AccountAssets; + + @Prop({ required: false, type: Number }) + deployedAt?: number | null; + + @Prop({ required: false, type: String }) + deployTxHash?: string | null; + + @Prop({ type: Object, required: false }) + ownerAssets?: AccountAssets; + + @Prop({ required: false, type: Boolean }) + isVerified?: boolean; + + @Prop({ required: false, type: Number }) + txCount?: number; + + @Prop({ required: false, type: Number }) + scrCount?: number; + + @Prop({ required: false, type: Number }) + transfersLast24h?: number; + + @Prop({ required: true, type: String }) + code: string = ''; + + @Prop({ required: true, type: String }) + codeHash: string = ''; + + @Prop({ required: true, type: String }) + rootHash: string = ''; + + @Prop({ required: false, type: String }) + username?: string; + + @Prop({ required: true, type: String }) + developerReward: string = ''; + + @Prop({ required: false, type: Boolean }) + isUpgradeable?: boolean; + + @Prop({ required: false, type: Boolean }) + isReadable?: boolean; + + @Prop({ required: false, type: Boolean }) + isPayable?: boolean; + + @Prop({ required: false, type: Boolean }) + isPayableBySmartContract?: boolean; + + @Prop({ type: Object, required: false }) + scamInfo?: ScamInfo; + + @Prop({ type: Array, required: false }) + nftCollections?: NftCollectionAccount[]; + + @Prop({ type: Array, required: false }) + nfts?: NftAccount[]; + + @Prop({ type: Array, required: false }) + tokens?: TokenWithBalance[]; + + @Prop({ required: false, type: Number }) + activeGuardianActivationEpoch?: number; + + @Prop({ required: false, type: String }) + activeGuardianAddress?: string; + + @Prop({ required: false, type: String }) + activeGuardianServiceUid?: string; + + @Prop({ required: false, type: Number }) + pendingGuardianActivationEpoch?: number; + + @Prop({ required: false, type: String }) + pendingGuardianAddress?: string; + + @Prop({ required: false, type: String }) + pendingGuardianServiceUid?: string; + + @Prop({ required: false, type: Boolean }) + isGuarded?: boolean; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export const AccountDetailsSchema = SchemaFactory.createForClass(AccountDetails); + +AccountDetailsSchema.index({ address: 1 }, { unique: true }); \ No newline at end of file diff --git a/src/common/indexer/db/schemas/index.ts b/src/common/indexer/db/schemas/index.ts new file mode 100644 index 000000000..72b1aaabe --- /dev/null +++ b/src/common/indexer/db/schemas/index.ts @@ -0,0 +1 @@ +export * from './account.details.schema'; \ No newline at end of file diff --git a/src/common/indexer/db/utils/errors.ts b/src/common/indexer/db/utils/errors.ts new file mode 100644 index 000000000..942e05c1d --- /dev/null +++ b/src/common/indexer/db/utils/errors.ts @@ -0,0 +1,3 @@ +export enum MongoDbErrorCode { + DUPLICATE_KEY_ERROR = 11000, +} diff --git a/src/common/indexer/db/utils/index.ts b/src/common/indexer/db/utils/index.ts new file mode 100644 index 000000000..f72bc43e2 --- /dev/null +++ b/src/common/indexer/db/utils/index.ts @@ -0,0 +1 @@ +export * from './errors'; diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts index 8b9bc4206..0b95cc9ee 100644 --- a/src/state-changes/entities/state.changes.entity.ts +++ b/src/state-changes/entities/state.changes.entity.ts @@ -7,9 +7,13 @@ export class AccountChanges { ownerAddressChanged!: boolean; userNameChanged!: boolean; codeMetadataChanged!: boolean; + + constructor(init?: Partial) { + Object.assign(this, init); + } } -export class StateAccessPerAccount { +export class StateAccessPerAccountRaw { type!: number; index!: number; txHash!: string; @@ -17,11 +21,85 @@ export class StateAccessPerAccount { mainTrieVal!: string; operation!: number; accountChanges?: AccountChanges; + + constructor(init?: Partial) { + Object.assign(this, init); + } } -export class StateChanges { +export class StateChangesRaw { hash!: string; - stateAccessesPerAccounts?: Map; + stateAccessesPerAccounts?: Map; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export class AccountState { + nonce!: string; + balance!: string; + developerReward!: string; + address!: string; + codeHash!: string; + rootHash!: string; + ownerAddress!: string; + userName!: string; + codeMetadata!: string; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export class EsdtState { + address!: string; + identifier!: string; + nonce!: string; + type!: ESDTType; + value!: string; + propertiesHex!: string; + reservedHex!: string; + tokenMetaData!: any; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export enum ESDTType { + // 0 + Fungible, + // 1 + NonFungible, + // 2 + NonFungibleV2, + // 3 + SemiFungible, + // 4 + MetaFungible, + // 5 + DynamicNFT, + // 6 + DynamicSFT, + // 7 + DynamicMeta, +} + +export class StateChanges { + accountState!: AccountState | undefined; + esdtState!: { + 'Fungible': EsdtState[], + 'NonFungible': EsdtState[], + 'NonFungibleV2': EsdtState[], + 'SemiFungible': EsdtState[], + 'MetaFungible': EsdtState[], + 'DynamicNFT': EsdtState[], + 'DynamicSFT': EsdtState[], + 'DynamicMeta': EsdtState[], + }; + accountChanges!: AccountChanges; + isNewAccount!: boolean; constructor(init?: Partial) { Object.assign(this, init); diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 787b91246..c6b37cee1 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -1,28 +1,64 @@ import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers"; import { StateChanges } from "./entities"; import { decodeStateChangesRaw, getFinalStates } from "./utils/state-changes.utils"; +import { AccountDetails, AccountDetailsRepository } from "src/common/indexer/db"; +import { Injectable } from "@nestjs/common"; +import { TokenWithBalance } from "src/endpoints/tokens/entities/token.with.balance"; // import { ApiConfigService } from "src/common/api-config/api.config.service"; +@Injectable() export class StateChangesConsumerService { constructor( // private readonly apiConfigService: ApiConfigService, + private readonly accountDetailsRepository: AccountDetailsRepository, ) { } + @CompetingRabbitConsumer({ exchange: 'state_accesses', queueName: 'state-changes-test', deadLetterExchange: 'state-changes-test_dlx', }) async consumeEvents(stateChanges: any) { - // console.dir(stateChanges, { depth: null }) - const decodedStateChanges = this.decodeStateChanges(stateChanges) - if (Object.keys(decodedStateChanges).length === 0) { - return; + try { + // console.dir(stateChanges, { depth: null }) + const decodedStateChanges = this.decodeStateChanges(stateChanges) + if (Object.keys(decodedStateChanges).length === 0) { + return; + } + + const finalStates = getFinalStates(decodedStateChanges); + const transformedStates = this.transformFinalStatesToDbFormat(finalStates); + await this.accountDetailsRepository.updateAccounts(transformedStates); + + } catch (error) { + console.error(`Error consuming state changes:`, error); + throw error; } - const finalStates = getFinalStates(decodedStateChanges); - console.dir(finalStates, { depth: null }) } - decodeStateChanges(stateChanges: StateChanges) { + private decodeStateChanges(stateChanges: StateChanges) { return decodeStateChangesRaw(stateChanges); } + + private transformFinalStatesToDbFormat(finalStates: Record) { + const transformed: AccountDetails[] = []; + for (const [_key, value] of Object.entries(finalStates)) { + const newAccountState = value.accountState; + const tokens = [...value.esdtState.Fungible]; // TODO: add other token types + + if (newAccountState) { + transformed.push(new AccountDetails({ + ...newAccountState, + tokens: tokens.map(t => new TokenWithBalance({ + identifier: t.identifier, + nonce: parseInt(t.nonce), + balance: t.value, + })), + })); + } + + } + // console.dir(transformed, { depth: null }) + return transformed; + } } \ No newline at end of file diff --git a/src/state-changes/state.changes.module.ts b/src/state-changes/state.changes.module.ts index 6fdc4968e..04e8ab915 100644 --- a/src/state-changes/state.changes.module.ts +++ b/src/state-changes/state.changes.module.ts @@ -4,10 +4,13 @@ import { DynamicModuleUtils } from 'src/utils/dynamic.module.utils'; import { ApiConfigModule } from 'src/common/api-config/api.config.module'; import { ApiConfigService } from 'src/common/api-config/api.config.service'; import { StateChangesConsumerService } from './state.changes.consumer.service'; +import { MongoDbModule } from 'src/common/indexer/db'; @Module({ imports: [ ApiConfigModule, + MongoDbModule, + // MongoDbRepositoriesModule, ], providers: [ StateChangesConsumerService, diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index f8a1ef907..1fd8c8962 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -3,6 +3,7 @@ import { UserAccountData } from "./user_account.pb"; import { ESDigitalToken } from "./esdt"; import { TrieLeafData } from "./trie_leaf_data"; import { TokenParser } from "./token.parser"; +import { AccountChanges, AccountState, EsdtState, ESDTType, StateChanges } from "../entities"; export enum StateAccessOperation { @@ -15,27 +16,6 @@ export enum StateAccessOperation { GetDataTrieValue = 32, } -export enum ESDTType { - // 0 - Fungible, - // 1 - NonFungible, - // 2 - NonFungibleV2, - // 3 - SemiFungible, - // 4 - MetaFungible, - // 5 - DynamicNFT, - // 6 - DynamicSFT, - // 7 - DynamicMeta, -} - - - const bech32FromHex = (hex: any) => { const clean = hex.startsWith("0x") ? hex.slice(2) : hex; return Address.newFromHex(clean).toBech32(); @@ -153,7 +133,7 @@ export function decodeStateChangesRaw(stateChanges: any) { let allDecodedEsdtData: any[] = []; if (!dataTrieChanges) { - console.log(` Entry #${i}: empty dataTrieChanges`); + // console.log(` Entry #${i}: empty dataTrieChanges`); } else { for (const dataTrieChange of dataTrieChanges) { if (dataTrieChange.version === 0) { @@ -223,22 +203,22 @@ export function decodeStateChangesRaw(stateChanges: any) { } export function getFinalStates(stateChanges: Record) { - const finalStates: Record = {}; + const finalStates: Record = {}; for (const [address, entries] of Object.entries(stateChanges)) { - let finalAccountState = {}; + let finalAccountState: AccountState | undefined = undefined; const finalEsdtStates = { - Fungible: [] as any[], - NonFungible: [] as any[], - NonFungibleV2: [] as any[], - SemiFungible: [] as any[], - MetaFungible: [] as any[], - DynamicNFT: [] as any[], - DynamicSFT: [] as any[], - DynamicMeta: [] as any[], + Fungible: [] as EsdtState[], + NonFungible: [] as EsdtState[], + NonFungibleV2: [] as EsdtState[], + SemiFungible: [] as EsdtState[], + MetaFungible: [] as EsdtState[], + DynamicNFT: [] as EsdtState[], + DynamicSFT: [] as EsdtState[], + DynamicMeta: [] as EsdtState[], }; - const finalAccountChanges = { + const finalAccountChanges: AccountChanges = new AccountChanges({ nonceChanged: false, balanceChanged: false, codeHashChanged: false, @@ -247,10 +227,12 @@ export function getFinalStates(stateChanges: Record) { ownerAddressChanged: false, userNameChanged: false, codeMetadataChanged: false - }; + }); + let finalNewAccount = false; + for (const entry of entries) { - const currentAccountState = entry.accountState; + const currentAccountState: AccountState = entry.accountState; const currentEsdtStates = entry.esdtStates; const currentAccountChanges = entry.accountChanges; const currentNewAccount = entry.newAccount as boolean; @@ -282,10 +264,10 @@ export function getFinalStates(stateChanges: Record) { } finalStates[address] = { - finalAccountState, - finalEsdtStates, - finalAccountChanges, - finalNewAccount, + accountState: finalAccountState, + esdtState: finalEsdtStates, + accountChanges: finalAccountChanges, + isNewAccount: finalNewAccount, }; } From aaaf343e330aa25a56e2581561a66c26eafb85c8 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 3 Sep 2025 15:24:59 +0300 Subject: [PATCH 15/90] store last processed block timestamp --- .../entities/state.changes.entity.ts | 3 +++ .../state.changes.consumer.service.ts | 27 ++++++++++++------- src/state-changes/state.changes.module.ts | 2 +- src/utils/cache.info.ts | 7 +++++ 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts index 0b95cc9ee..1bf4aba70 100644 --- a/src/state-changes/entities/state.changes.entity.ts +++ b/src/state-changes/entities/state.changes.entity.ts @@ -29,6 +29,9 @@ export class StateAccessPerAccountRaw { export class StateChangesRaw { hash!: string; + shardID!: number; + nonce!: number; + timestampMs!: number; stateAccessesPerAccounts?: Map; constructor(init?: Partial) { diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index c6b37cee1..f56f7157e 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -1,15 +1,18 @@ import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers"; -import { StateChanges } from "./entities"; +import { StateChanges, StateChangesRaw } from "./entities"; import { decodeStateChangesRaw, getFinalStates } from "./utils/state-changes.utils"; import { AccountDetails, AccountDetailsRepository } from "src/common/indexer/db"; import { Injectable } from "@nestjs/common"; import { TokenWithBalance } from "src/endpoints/tokens/entities/token.with.balance"; +import { CacheService } from "@multiversx/sdk-nestjs-cache"; +import { CacheInfo } from "src/utils/cache.info"; // import { ApiConfigService } from "src/common/api-config/api.config.service"; @Injectable() export class StateChangesConsumerService { constructor( // private readonly apiConfigService: ApiConfigService, + private readonly cacheService: CacheService, private readonly accountDetailsRepository: AccountDetailsRepository, ) { } @@ -18,25 +21,29 @@ export class StateChangesConsumerService { queueName: 'state-changes-test', deadLetterExchange: 'state-changes-test_dlx', }) - async consumeEvents(stateChanges: any) { + async consumeEvents(stateChanges: StateChangesRaw) { + console.log(stateChanges) try { - // console.dir(stateChanges, { depth: null }) + const decodedStateChanges = this.decodeStateChanges(stateChanges) - if (Object.keys(decodedStateChanges).length === 0) { - return; + if (Object.keys(decodedStateChanges).length !== 0) { + const finalStates = getFinalStates(decodedStateChanges); + const transformedStates = this.transformFinalStatesToDbFormat(finalStates); + await this.accountDetailsRepository.updateAccounts(transformedStates); } - const finalStates = getFinalStates(decodedStateChanges); - const transformedStates = this.transformFinalStatesToDbFormat(finalStates); - await this.accountDetailsRepository.updateAccounts(transformedStates); - + this.cacheService.setRemote( + CacheInfo.LatestProcessedBlockTimestamp(stateChanges.shardID).key, + stateChanges.timestampMs, + CacheInfo.LatestProcessedBlockTimestamp(stateChanges.shardID).ttl, + ); } catch (error) { console.error(`Error consuming state changes:`, error); throw error; } } - private decodeStateChanges(stateChanges: StateChanges) { + private decodeStateChanges(stateChanges: StateChangesRaw) { return decodeStateChangesRaw(stateChanges); } diff --git a/src/state-changes/state.changes.module.ts b/src/state-changes/state.changes.module.ts index 04e8ab915..3bf8ae7f0 100644 --- a/src/state-changes/state.changes.module.ts +++ b/src/state-changes/state.changes.module.ts @@ -10,7 +10,7 @@ import { MongoDbModule } from 'src/common/indexer/db'; imports: [ ApiConfigModule, MongoDbModule, - // MongoDbRepositoriesModule, + DynamicModuleUtils.getCacheModule(), ], providers: [ StateChangesConsumerService, diff --git a/src/utils/cache.info.ts b/src/utils/cache.info.ts index 6ca7e69ac..e1166c2d8 100644 --- a/src/utils/cache.info.ts +++ b/src/utils/cache.info.ts @@ -710,4 +710,11 @@ export class CacheInfo { ttl: Constants.oneSecond() * 30, }; } + + static LatestProcessedBlockTimestamp(shardId: number): CacheInfo { + return { + key: `latestProcessedBlock:${shardId}`, + ttl: Constants.oneMinute(), + }; + } } From c00356bc11e75b6e6790d08b10048ee4463d7ca5 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 4 Sep 2025 11:47:23 +0300 Subject: [PATCH 16/90] npm packages --- package-lock.json | 2685 +++++++++++++++++++++++++++------------------ package.json | 6 +- 2 files changed, 1644 insertions(+), 1047 deletions(-) diff --git a/package-lock.json b/package-lock.json index f964e0d74..7d036da6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@nestjs/event-emitter": "^2.0.3", "@nestjs/graphql": "^12.0.11", "@nestjs/microservices": "10.2.4", + "@nestjs/mongoose": "^11.0.3", "@nestjs/platform-express": "10.4.19", "@nestjs/platform-socket.io": "^10.2.4", "@nestjs/schedule": "3.0.3", @@ -38,7 +39,7 @@ "@sendgrid/mail": "^8.1.5", "agentkeepalive": "^4.2.1", "amqp-connection-manager": "^4.1.3", - "amqplib": "^0.10.0", + "amqplib": "^0.10.9", "anchorme": "^3.0.8", "apollo-server-core": "^3.13.0", "apollo-server-express": "3.13.0", @@ -64,6 +65,7 @@ "node-object-hash": "^2.3.10", "pg": "^8.7.3", "prom-client": "^14.0.1", + "protobufjs": "^7.5.4", "redis": "^3.1.2", "reflect-metadata": "^0.1.13", "request-ip": "^3.3.0", @@ -88,6 +90,7 @@ "@nestjs/schematics": "10.0.2", "@nestjs/testing": "10.2.4", "@testing-library/jest-dom": "6.1.4", + "@types/amqplib": "^0.10.7", "@types/compression": "^1.8.1", "@types/cron": "^1.7.3", "@types/crypto-js": "^4.0.2", @@ -110,6 +113,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "29.5.0", "prettier": "^2.5.1", + "protobufjs-cli": "^1.1.3", "run-script-os": "^1.1.6", "supertest": "^6.2.2", "ts-jest": "^29.0.5", @@ -121,9 +125,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", - "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true, "license": "MIT" }, @@ -222,9 +226,9 @@ } }, "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", "dev": true, "license": "MIT", "engines": { @@ -340,6 +344,7 @@ "version": "4.12.2", "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.12.2.tgz", "integrity": "sha512-jKRlf+sBMMdKYrjMoiWKne42Eb6paBfDOr08KJnUaeaiyWFj+/040FjVPQI7YGLfdwnYIsl1NUUqS2UdgezJDg==", + "deprecated": "Apollo Server v4 is deprecated and will transition to end-of-life on January 26, 2026. As long as you are already using a non-EOL version of Node.js, upgrading to v5 should take only a few minutes. See https://www.apollographql.com/docs/apollo-server/previous-versions for details.", "license": "MIT", "peer": true, "dependencies": { @@ -379,6 +384,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@apollo/server-gateway-interface/-/server-gateway-interface-1.1.1.tgz", "integrity": "sha512-pGwCl/po6+rxRmDMFgozKQo2pbsSwE91TpsDBAOgf74CRDPXHHtM88wbwjab0wMMZh95QfR45GGyDIdhY24bkQ==", + "deprecated": "@apollo/server-gateway-interface v1 is part of Apollo Server v4, which is deprecated and will transition to end-of-life on January 26, 2026. As long as you are already using a non-EOL version of Node.js, upgrading to v2 should take only a few minutes. See https://www.apollographql.com/docs/apollo-server/previous-versions for details.", "license": "MIT", "peer": true, "dependencies": { @@ -866,66 +872,66 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.840.0.tgz", - "integrity": "sha512-dRuo03EqGBbl9+PTogpwY9bYmGWIjn8nB82HN5Qj20otgjUvhLOdEkkip9mroYsrvqNoKbMedWdCudIcB/YY1w==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.879.0.tgz", + "integrity": "sha512-1bD2Do/OdCIzl72ncHKYamDhPijUErLYpuLvciyYD4Ywt4cVLHjWtVIqb22XOOHYYHE3NqHMd4uRhvXMlsBRoQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.840.0", - "@aws-sdk/credential-provider-node": "3.840.0", - "@aws-sdk/middleware-bucket-endpoint": "3.840.0", - "@aws-sdk/middleware-expect-continue": "3.840.0", - "@aws-sdk/middleware-flexible-checksums": "3.840.0", - "@aws-sdk/middleware-host-header": "3.840.0", - "@aws-sdk/middleware-location-constraint": "3.840.0", - "@aws-sdk/middleware-logger": "3.840.0", - "@aws-sdk/middleware-recursion-detection": "3.840.0", - "@aws-sdk/middleware-sdk-s3": "3.840.0", - "@aws-sdk/middleware-ssec": "3.840.0", - "@aws-sdk/middleware-user-agent": "3.840.0", - "@aws-sdk/region-config-resolver": "3.840.0", - "@aws-sdk/signature-v4-multi-region": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@aws-sdk/util-endpoints": "3.840.0", - "@aws-sdk/util-user-agent-browser": "3.840.0", - "@aws-sdk/util-user-agent-node": "3.840.0", - "@aws-sdk/xml-builder": "3.821.0", - "@smithy/config-resolver": "^4.1.4", - "@smithy/core": "^3.6.0", - "@smithy/eventstream-serde-browser": "^4.0.4", - "@smithy/eventstream-serde-config-resolver": "^4.1.2", - "@smithy/eventstream-serde-node": "^4.0.4", - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/hash-blob-browser": "^4.0.4", - "@smithy/hash-node": "^4.0.4", - "@smithy/hash-stream-node": "^4.0.4", - "@smithy/invalid-dependency": "^4.0.4", - "@smithy/md5-js": "^4.0.4", - "@smithy/middleware-content-length": "^4.0.4", - "@smithy/middleware-endpoint": "^4.1.13", - "@smithy/middleware-retry": "^4.1.14", - "@smithy/middleware-serde": "^4.0.8", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/protocol-http": "^5.1.2", - "@smithy/smithy-client": "^4.4.5", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/credential-provider-node": "3.879.0", + "@aws-sdk/middleware-bucket-endpoint": "3.873.0", + "@aws-sdk/middleware-expect-continue": "3.873.0", + "@aws-sdk/middleware-flexible-checksums": "3.879.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-location-constraint": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-sdk-s3": "3.879.0", + "@aws-sdk/middleware-ssec": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.879.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/signature-v4-multi-region": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.879.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.879.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.9.0", + "@smithy/eventstream-serde-browser": "^4.0.5", + "@smithy/eventstream-serde-config-resolver": "^4.1.3", + "@smithy/eventstream-serde-node": "^4.0.5", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-blob-browser": "^4.0.5", + "@smithy/hash-node": "^4.0.5", + "@smithy/hash-stream-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/md5-js": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.19", + "@smithy/middleware-retry": "^4.1.20", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.21", - "@smithy/util-defaults-mode-node": "^4.0.21", - "@smithy/util-endpoints": "^3.0.6", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.6", - "@smithy/util-stream": "^4.2.2", + "@smithy/util-defaults-mode-browser": "^4.0.27", + "@smithy/util-defaults-mode-node": "^4.0.27", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-stream": "^4.2.4", "@smithy/util-utf8": "^4.0.0", - "@smithy/util-waiter": "^4.0.6", + "@smithy/util-waiter": "^4.0.7", "@types/uuid": "^9.0.1", "tslib": "^2.6.2", "uuid": "^9.0.1" @@ -935,47 +941,47 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.840.0.tgz", - "integrity": "sha512-3Zp+FWN2hhmKdpS0Ragi5V2ZPsZNScE3jlbgoJjzjI/roHZqO+e3/+XFN4TlM0DsPKYJNp+1TAjmhxN6rOnfYA==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.879.0.tgz", + "integrity": "sha512-+Pc3OYFpRYpKLKRreovPM63FPPud1/SF9vemwIJfz6KwsBCJdvg7vYD1xLSIp5DVZLeetgf4reCyAA5ImBfZuw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.840.0", - "@aws-sdk/middleware-host-header": "3.840.0", - "@aws-sdk/middleware-logger": "3.840.0", - "@aws-sdk/middleware-recursion-detection": "3.840.0", - "@aws-sdk/middleware-user-agent": "3.840.0", - "@aws-sdk/region-config-resolver": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@aws-sdk/util-endpoints": "3.840.0", - "@aws-sdk/util-user-agent-browser": "3.840.0", - "@aws-sdk/util-user-agent-node": "3.840.0", - "@smithy/config-resolver": "^4.1.4", - "@smithy/core": "^3.6.0", - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/hash-node": "^4.0.4", - "@smithy/invalid-dependency": "^4.0.4", - "@smithy/middleware-content-length": "^4.0.4", - "@smithy/middleware-endpoint": "^4.1.13", - "@smithy/middleware-retry": "^4.1.14", - "@smithy/middleware-serde": "^4.0.8", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/protocol-http": "^5.1.2", - "@smithy/smithy-client": "^4.4.5", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.879.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.879.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.879.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.9.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.19", + "@smithy/middleware-retry": "^4.1.20", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.21", - "@smithy/util-defaults-mode-node": "^4.0.21", - "@smithy/util-endpoints": "^3.0.6", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.6", + "@smithy/util-defaults-mode-browser": "^4.0.27", + "@smithy/util-defaults-mode-node": "^4.0.27", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -984,25 +990,25 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.840.0.tgz", - "integrity": "sha512-x3Zgb39tF1h2XpU+yA4OAAQlW6LVEfXNlSedSYJ7HGKXqA/E9h3rWQVpYfhXXVVsLdYXdNw5KBUkoAoruoZSZA==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.879.0.tgz", + "integrity": "sha512-AhNmLCrx980LsK+SfPXGh7YqTyZxsK0Qmy18mWmkfY0TSq7WLaSDB5zdQbgbnQCACCHy8DUYXbi4KsjlIhv3PA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@aws-sdk/xml-builder": "3.821.0", - "@smithy/core": "^3.6.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/property-provider": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/signature-v4": "^5.1.2", - "@smithy/smithy-client": "^4.4.5", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/core": "^3.9.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", + "@smithy/util-middleware": "^4.0.5", "@smithy/util-utf8": "^4.0.0", - "fast-xml-parser": "4.4.1", + "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, "engines": { @@ -1010,15 +1016,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.840.0.tgz", - "integrity": "sha512-EzF6VcJK7XvQ/G15AVEfJzN2mNXU8fcVpXo4bRyr1S6t2q5zx6UPH/XjDbn18xyUmOq01t+r8gG+TmHEVo18fA==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.879.0.tgz", + "integrity": "sha512-JgG7A8SSbr5IiCYL8kk39Y9chdSB5GPwBorDW8V8mr19G9L+qd6ohED4fAocoNFaDnYJ5wGAHhCfSJjzcsPBVQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1026,20 +1032,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.840.0.tgz", - "integrity": "sha512-wbnUiPGLVea6mXbUh04fu+VJmGkQvmToPeTYdHE8eRZq3NRDi3t3WltT+jArLBKD/4NppRpMjf2ju4coMCz91g==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.879.0.tgz", + "integrity": "sha512-2hM5ByLpyK+qORUexjtYyDZsgxVCCUiJQZRMGkNXFEGz6zTpbjfTIWoh3zRgWHEBiqyPIyfEy50eIF69WshcuA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/property-provider": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/smithy-client": "^4.4.5", - "@smithy/types": "^4.3.1", - "@smithy/util-stream": "^4.2.2", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", "tslib": "^2.6.2" }, "engines": { @@ -1047,23 +1053,23 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.840.0.tgz", - "integrity": "sha512-7F290BsWydShHb+7InXd+IjJc3mlEIm9I0R57F/Pjl1xZB69MdkhVGCnuETWoBt4g53ktJd6NEjzm/iAhFXFmw==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.879.0.tgz", + "integrity": "sha512-07M8zfb73KmMBqVO5/V3Ea9kqDspMX0fO0kaI1bsjWI6ngnMye8jCE0/sIhmkVAI0aU709VA0g+Bzlopnw9EoQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.840.0", - "@aws-sdk/credential-provider-env": "3.840.0", - "@aws-sdk/credential-provider-http": "3.840.0", - "@aws-sdk/credential-provider-process": "3.840.0", - "@aws-sdk/credential-provider-sso": "3.840.0", - "@aws-sdk/credential-provider-web-identity": "3.840.0", - "@aws-sdk/nested-clients": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@smithy/credential-provider-imds": "^4.0.6", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/credential-provider-env": "3.879.0", + "@aws-sdk/credential-provider-http": "3.879.0", + "@aws-sdk/credential-provider-process": "3.879.0", + "@aws-sdk/credential-provider-sso": "3.879.0", + "@aws-sdk/credential-provider-web-identity": "3.879.0", + "@aws-sdk/nested-clients": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1071,22 +1077,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.840.0.tgz", - "integrity": "sha512-KufP8JnxA31wxklLm63evUPSFApGcH8X86z3mv9SRbpCm5ycgWIGVCTXpTOdgq6rPZrwT9pftzv2/b4mV/9clg==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.879.0.tgz", + "integrity": "sha512-FYaAqJbnSTrVL2iZkNDj2hj5087yMv2RN2GA8DJhe7iOJjzhzRojrtlfpWeJg6IhK0sBKDH+YXbdeexCzUJvtA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.840.0", - "@aws-sdk/credential-provider-http": "3.840.0", - "@aws-sdk/credential-provider-ini": "3.840.0", - "@aws-sdk/credential-provider-process": "3.840.0", - "@aws-sdk/credential-provider-sso": "3.840.0", - "@aws-sdk/credential-provider-web-identity": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@smithy/credential-provider-imds": "^4.0.6", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/credential-provider-env": "3.879.0", + "@aws-sdk/credential-provider-http": "3.879.0", + "@aws-sdk/credential-provider-ini": "3.879.0", + "@aws-sdk/credential-provider-process": "3.879.0", + "@aws-sdk/credential-provider-sso": "3.879.0", + "@aws-sdk/credential-provider-web-identity": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1094,16 +1100,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.840.0.tgz", - "integrity": "sha512-HkDQWHy8tCI4A0Ps2NVtuVYMv9cB4y/IuD/TdOsqeRIAT12h8jDb98BwQPNLAImAOwOWzZJ8Cu0xtSpX7CQhMw==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.879.0.tgz", + "integrity": "sha512-7r360x1VyEt35Sm1JFOzww2WpnfJNBbvvnzoyLt7WRfK0S/AfsuWhu5ltJ80QvJ0R3AiSNbG+q/btG2IHhDYPQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1111,18 +1117,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.840.0.tgz", - "integrity": "sha512-2qgdtdd6R0Z1y0KL8gzzwFUGmhBHSUx4zy85L2XV1CXhpRNwV71SVWJqLDVV5RVWVf9mg50Pm3AWrUC0xb0pcA==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.879.0.tgz", + "integrity": "sha512-gd27B0NsgtKlaPNARj4IX7F7US5NuU691rGm0EUSkDsM7TctvJULighKoHzPxDQlrDbVI11PW4WtKS/Zg5zPlQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.840.0", - "@aws-sdk/core": "3.840.0", - "@aws-sdk/token-providers": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/client-sso": "3.879.0", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/token-providers": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1130,16 +1136,16 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.840.0.tgz", - "integrity": "sha512-dpEeVXG8uNZSmVXReE4WP0lwoioX2gstk4RnUgrdUE3YaPq8A+hJiVAyc3h+cjDeIqfbsQbZm9qFetKC2LF9dQ==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.879.0.tgz", + "integrity": "sha512-Jy4uPFfGzHk1Mxy+/Wr43vuw9yXsE2yiF4e4598vc3aJfO0YtA2nSfbKD3PNKRORwXbeKqWPfph9SCKQpWoxEg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.840.0", - "@aws-sdk/nested-clients": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/nested-clients": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1147,16 +1153,16 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.840.0.tgz", - "integrity": "sha512-+gkQNtPwcSMmlwBHFd4saVVS11In6ID1HczNzpM3MXKXRBfSlbZJbCt6wN//AZ8HMklZEik4tcEOG0qa9UY8SQ==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.873.0.tgz", + "integrity": "sha512-b4bvr0QdADeTUs+lPc9Z48kXzbKHXQKgTvxx/jXDgSW9tv4KmYPO1gIj6Z9dcrBkRWQuUtSW3Tu2S5n6pe+zeg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@aws-sdk/util-arn-parser": "3.804.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.873.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", "@smithy/util-config-provider": "^4.0.0", "tslib": "^2.6.2" }, @@ -1165,14 +1171,14 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.840.0.tgz", - "integrity": "sha512-iJg2r6FKsKKvdiU4oCOuCf7Ro/YE0Q2BT/QyEZN3/Rt8Nr4SAZiQOlcBXOCpGvuIKOEAhvDOUnW3aDHL01PdVw==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.873.0.tgz", + "integrity": "sha512-GIqoc8WgRcf/opBOZXFLmplJQKwOMjiOMmDz9gQkaJ8FiVJoAp8EGVmK2TOWZMQUYsavvHYsHaor5R2xwPoGVg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1180,22 +1186,22 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.840.0.tgz", - "integrity": "sha512-Kg/o2G6o72sdoRH0J+avdcf668gM1bp6O4VeEXpXwUj/urQnV5qiB2q1EYT110INHUKWOLXPND3sQAqh6sTqHw==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.879.0.tgz", + "integrity": "sha512-U1rcWToy2rlQPQLsx5h73uTC1XYo/JpnlJGCc3Iw7b1qrK8Mke4+rgMPKCfnXELD5TTazGrbT03frxH4Y1Ycvw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.840.0", - "@aws-sdk/types": "3.840.0", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/types": "3.862.0", "@smithy/is-array-buffer": "^4.0.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.2", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -1204,14 +1210,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.840.0.tgz", - "integrity": "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.873.0.tgz", + "integrity": "sha512-KZ/W1uruWtMOs7D5j3KquOxzCnV79KQW9MjJFZM/M0l6KI8J6V3718MXxFHsTjUE4fpdV6SeCNLV1lwGygsjJA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1219,13 +1225,13 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.840.0.tgz", - "integrity": "sha512-KVLD0u0YMF3aQkVF8bdyHAGWSUY6N1Du89htTLgqCcIhSxxAJ9qifrosVZ9jkAzqRW99hcufyt2LylcVU2yoKQ==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.873.0.tgz", + "integrity": "sha512-r+hIaORsW/8rq6wieDordXnA/eAu7xAPLue2InhoEX6ML7irP52BgiibHLpt9R0psiCzIHhju8qqKa4pJOrmiw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1233,13 +1239,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.840.0.tgz", - "integrity": "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==", + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.876.0.tgz", + "integrity": "sha512-cpWJhOuMSyz9oV25Z/CMHCBTgafDCbv7fHR80nlRrPdPZ8ETNsahwRgltXP1QJJ8r3X/c1kwpOR7tc+RabVzNA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1247,14 +1253,14 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.840.0.tgz", - "integrity": "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.873.0.tgz", + "integrity": "sha512-OtgY8EXOzRdEWR//WfPkA/fXl0+WwE8hq0y9iw2caNyKPtca85dzrrZWnPqyBK/cpImosrpR1iKMYr41XshsCg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1262,23 +1268,23 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.840.0.tgz", - "integrity": "sha512-rOUji7CayWN3O09zvvgLzDVQe0HiJdZkxoTS6vzOS3WbbdT7joGdVtAJHtn+x776QT3hHzbKU5gnfhel0o6gQA==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.879.0.tgz", + "integrity": "sha512-ZTpLr2AbZcCsEzu18YCtB8Tp8tjAWHT0ccfwy3HiL6g9ncuSMW+7BVi1hDYmBidFwpPbnnIMtM0db3pDMR6/WA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@aws-sdk/util-arn-parser": "3.804.0", - "@smithy/core": "^3.6.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/signature-v4": "^5.1.2", - "@smithy/smithy-client": "^4.4.5", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.873.0", + "@smithy/core": "^3.9.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -1287,13 +1293,13 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.840.0.tgz", - "integrity": "sha512-CBZP9t1QbjDFGOrtnUEHL1oAvmnCUUm7p0aPNbIdSzNtH42TNKjPRN3TuEIJDGjkrqpL3MXyDSmNayDcw/XW7Q==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.873.0.tgz", + "integrity": "sha512-AF55J94BoiuzN7g3hahy0dXTVZahVi8XxRBLgzNp6yQf0KTng+hb/V9UQZVYY1GZaDczvvvnqC54RGe9OZZ9zQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1301,17 +1307,17 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.840.0.tgz", - "integrity": "sha512-hiiMf7BP5ZkAFAvWRcK67Mw/g55ar7OCrvrynC92hunx/xhMkrgSLM0EXIZ1oTn3uql9kH/qqGF0nqsK6K555A==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.879.0.tgz", + "integrity": "sha512-DDSV8228lQxeMAFKnigkd0fHzzn5aauZMYC3CSj6e5/qE7+9OwpkUcjHfb7HZ9KWG6L2/70aKZXHqiJ4xKhOZw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@aws-sdk/util-endpoints": "3.840.0", - "@smithy/core": "^3.6.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.879.0", + "@smithy/core": "^3.9.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1319,47 +1325,47 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.840.0.tgz", - "integrity": "sha512-LXYYo9+n4hRqnRSIMXLBb+BLz+cEmjMtTudwK1BF6Bn2RfdDv29KuyeDRrPCS3TwKl7ZKmXUmE9n5UuHAPfBpA==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.879.0.tgz", + "integrity": "sha512-7+n9NpIz9QtKYnxmw1fHi9C8o0GrX8LbBR4D50c7bH6Iq5+XdSuL5AFOWWQ5cMD0JhqYYJhK/fJsVau3nUtC4g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.840.0", - "@aws-sdk/middleware-host-header": "3.840.0", - "@aws-sdk/middleware-logger": "3.840.0", - "@aws-sdk/middleware-recursion-detection": "3.840.0", - "@aws-sdk/middleware-user-agent": "3.840.0", - "@aws-sdk/region-config-resolver": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@aws-sdk/util-endpoints": "3.840.0", - "@aws-sdk/util-user-agent-browser": "3.840.0", - "@aws-sdk/util-user-agent-node": "3.840.0", - "@smithy/config-resolver": "^4.1.4", - "@smithy/core": "^3.6.0", - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/hash-node": "^4.0.4", - "@smithy/invalid-dependency": "^4.0.4", - "@smithy/middleware-content-length": "^4.0.4", - "@smithy/middleware-endpoint": "^4.1.13", - "@smithy/middleware-retry": "^4.1.14", - "@smithy/middleware-serde": "^4.0.8", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/protocol-http": "^5.1.2", - "@smithy/smithy-client": "^4.4.5", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.879.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.879.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.879.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.9.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.19", + "@smithy/middleware-retry": "^4.1.20", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.21", - "@smithy/util-defaults-mode-node": "^4.0.21", - "@smithy/util-endpoints": "^3.0.6", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.6", + "@smithy/util-defaults-mode-browser": "^4.0.27", + "@smithy/util-defaults-mode-node": "^4.0.27", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -1368,16 +1374,16 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.840.0.tgz", - "integrity": "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.873.0.tgz", + "integrity": "sha512-q9sPoef+BBG6PJnc4x60vK/bfVwvRWsPgcoQyIra057S/QGjq5VkjvNk6H8xedf6vnKlXNBwq9BaANBXnldUJg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", + "@smithy/util-middleware": "^4.0.5", "tslib": "^2.6.2" }, "engines": { @@ -1385,16 +1391,16 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.840.0.tgz", - "integrity": "sha512-8AoVgHrkSfhvGPtwx23hIUO4MmMnux2pjnso1lrLZGqxfElM6jm2w4jTNLlNXk8uKHGyX89HaAIuT0lL6dJj9g==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.879.0.tgz", + "integrity": "sha512-MDsw0EWOHyKac75X3gD8tLWtmPuRliS/s4IhWRhsdDCU13wewHIs5IlA5B65kT6ISf49yEIalEH3FHUSVqdmIQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/signature-v4": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/middleware-sdk-s3": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1402,17 +1408,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.840.0.tgz", - "integrity": "sha512-6BuTOLTXvmgwjK7ve7aTg9JaWFdM5UoMolLVPMyh3wTv9Ufalh8oklxYHUBIxsKkBGO2WiHXytveuxH6tAgTYg==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.879.0.tgz", + "integrity": "sha512-47J7sCwXdnw9plRZNAGVkNEOlSiLb/kR2slnDIHRK9NB/ECKsoqgz5OZQJ9E2f0yqOs8zSNJjn3T01KxpgW8Qw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.840.0", - "@aws-sdk/nested-clients": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "3.879.0", + "@aws-sdk/nested-clients": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1420,12 +1426,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", - "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", + "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1433,9 +1439,9 @@ } }, "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", - "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.873.0.tgz", + "integrity": "sha512-qag+VTqnJWDn8zTAXX4wiVioa0hZDQMtbZcGRERVnLar4/3/VIKBhxX2XibNQXFu1ufgcRn4YntT/XEPecFWcg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1445,14 +1451,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.840.0.tgz", - "integrity": "sha512-eqE9ROdg/Kk0rj3poutyRCFauPDXIf/WSvCqFiRDDVi6QOnCv/M0g2XW8/jSvkJlOyaXkNCptapIp6BeeFFGYw==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.879.0.tgz", + "integrity": "sha512-aVAJwGecYoEmbEFju3127TyJDF9qJsKDUUTRMDuS8tGn+QiWQFnfInmbt+el9GU1gEJupNTXV+E3e74y51fb7A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/types": "^4.3.1", - "@smithy/util-endpoints": "^3.0.6", + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", "tslib": "^2.6.2" }, "engines": { @@ -1460,9 +1467,9 @@ } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", - "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", + "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1472,27 +1479,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.840.0.tgz", - "integrity": "sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.873.0.tgz", + "integrity": "sha512-AcRdbK6o19yehEcywI43blIBhOCSo6UgyWcuOJX5CFF8k39xm1ILCjQlRRjchLAxWrm0lU0Q7XV90RiMMFMZtA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.840.0.tgz", - "integrity": "sha512-Fy5JUEDQU1tPm2Yw/YqRYYc27W5+QD/J4mYvQvdWjUGZLB5q3eLFMGD35Uc28ZFoGMufPr4OCxK/bRfWROBRHQ==", + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.879.0.tgz", + "integrity": "sha512-A5KGc1S+CJRzYnuxJQQmH1BtGsz46AgyHkqReKfGiNQA8ET/9y9LQ5t2ABqnSBHHIh3+MiCcQSkUZ0S3rTodrQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", + "@aws-sdk/middleware-user-agent": "3.879.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1508,12 +1515,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", - "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.873.0.tgz", + "integrity": "sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1536,9 +1543,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.7.tgz", - "integrity": "sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "license": "MIT", "engines": { @@ -1546,22 +1553,22 @@ } }, "node_modules/@babel/core": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", - "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.5", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.27.7", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.7", - "@babel/types": "^7.27.7", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -1587,16 +1594,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -1640,6 +1647,16 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -1655,15 +1672,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -1713,27 +1730,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", - "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.7" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -1982,9 +1999,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2006,38 +2023,28 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", - "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.5", - "@babel/parser": "^7.27.7", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.7", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.2", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", - "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2055,6 +2062,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2193,9 +2210,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "license": "MIT", "optional": true, "dependencies": { @@ -2203,9 +2220,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", + "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2380,6 +2397,12 @@ "node": ">=10" } }, + "node_modules/@golevelup/nestjs-rabbitmq/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, "node_modules/@golevelup/nestjs-rabbitmq/node_modules/promise-breaker": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", @@ -2508,9 +2531,9 @@ "license": "BSD-3-Clause" }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", - "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", "cpu": [ "arm64" ], @@ -2526,13 +2549,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.1.0" + "@img/sharp-libvips-darwin-arm64": "1.2.0" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz", - "integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", "cpu": [ "x64" ], @@ -2548,13 +2571,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.1.0" + "@img/sharp-libvips-darwin-x64": "1.2.0" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", - "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", "cpu": [ "arm64" ], @@ -2568,9 +2591,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", - "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", "cpu": [ "x64" ], @@ -2584,9 +2607,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", - "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", "cpu": [ "arm" ], @@ -2600,9 +2623,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", - "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", "cpu": [ "arm64" ], @@ -2616,9 +2639,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", - "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", "cpu": [ "ppc64" ], @@ -2632,9 +2655,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", - "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", "cpu": [ "s390x" ], @@ -2648,9 +2671,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", - "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", "cpu": [ "x64" ], @@ -2664,9 +2687,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", - "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", "cpu": [ "arm64" ], @@ -2680,9 +2703,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", - "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", "cpu": [ "x64" ], @@ -2696,9 +2719,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz", - "integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", "cpu": [ "arm" ], @@ -2714,13 +2737,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.1.0" + "@img/sharp-libvips-linux-arm": "1.2.0" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz", - "integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", "cpu": [ "arm64" ], @@ -2736,13 +2759,35 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.1.0" + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", - "integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", "cpu": [ "s390x" ], @@ -2758,13 +2803,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.1.0" + "@img/sharp-libvips-linux-s390x": "1.2.0" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz", - "integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", "cpu": [ "x64" ], @@ -2780,13 +2825,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.1.0" + "@img/sharp-libvips-linux-x64": "1.2.0" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", - "integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", "cpu": [ "arm64" ], @@ -2802,13 +2847,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz", - "integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", "cpu": [ "x64" ], @@ -2824,20 +2869,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz", - "integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.3" + "@emnapi/runtime": "^1.4.4" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -2847,9 +2892,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz", - "integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", "cpu": [ "arm64" ], @@ -2866,9 +2911,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz", - "integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", "cpu": [ "ia32" ], @@ -2885,9 +2930,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz", - "integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", "cpu": [ "x64" ], @@ -2904,9 +2949,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.1.tgz", + "integrity": "sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -2927,9 +2972,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", "license": "MIT", "engines": { "node": ">=12" @@ -3452,9 +3497,9 @@ "license": "ISC" }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.11.tgz", - "integrity": "sha512-C512c1ytBTio4MrpWKlJpyFHT6+qfFL8SZ58zBzJ1OOzUEjHeF1BtjY2fH7n4x/g2OV/KiiMLAivOp1DXmiMMw==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -3473,9 +3518,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.9.tgz", - "integrity": "sha512-amBU75CKOOkcQLfyM6J+DnWwz41yTsWI7o8MQ003LwUIWb4NYX/evAblTx1oBBYJySqL/zHPxHXDw5ewpQaUFw==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", "dependencies": { @@ -3484,16 +3529,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.3.tgz", - "integrity": "sha512-AiR5uKpFxP3PjO4R19kQGIMwxyRyPuXmKEEy301V1C0+1rVjS94EZQXf1QKZYN8Q0YM+estSPhmx5JwNftv6nw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.28", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.28.tgz", - "integrity": "sha512-KNNHHwW3EIp4EDYOvYFGyIFfx36R2dNJYH4knnZlF8T5jdbD5Wx8xmSaQ2gP9URkJ04LGEtlcCtwArKcmFcwKw==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3501,6 +3546,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdoc/salty": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", + "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -3603,9 +3661,9 @@ } }, "node_modules/@multiversx/sdk-exchange": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-exchange/-/sdk-exchange-0.2.22.tgz", - "integrity": "sha512-mRo1U9Fj9S+O4BNGR4Gs9g9+T7hUw/cGoGe2rzqUsGLimfudaGWtHLlkV20Wm3GITVMe2zcFbDFewxkprnSjhg==", + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-exchange/-/sdk-exchange-0.2.23.tgz", + "integrity": "sha512-eGOA9CeRfsf3aKf4/NXbk8VeHJsWSm9wuaMfrF4cUPhWoqpA93QGlW/iTxpM4u4WkLeaNITkGI3NgRL9MNrDDA==", "license": "MIT", "dependencies": { "@multiversx/sdk-core": "^13.6.3", @@ -3648,9 +3706,9 @@ } }, "node_modules/@multiversx/sdk-nestjs-auth/node_modules/@multiversx/sdk-core": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-14.2.3.tgz", - "integrity": "sha512-h52YIJ3udMyQSNX1QaZKbm7YpwIFvRI78QEJTes0UoboAUt5Vx5bTc8kZt8Lrfks7FKHEPaUeMdLochUcQbQ4Q==", + "version": "14.2.9", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-14.2.9.tgz", + "integrity": "sha512-tssRDroQpYTLX7ZNQyVzb6Bai4N1Nz3mUuJCW7k0i179bGUKR/VFVa0RviAXkv+d3pUkP/NepxdUaVTyaj5jmg==", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", @@ -3669,7 +3727,7 @@ }, "optionalDependencies": { "@multiversx/sdk-bls-wasm": "0.3.5", - "axios": "^1.7.4", + "axios": "^1.10.0", "bip39": "3.1.0" }, "peerDependencies": { @@ -3678,16 +3736,16 @@ } }, "node_modules/@multiversx/sdk-nestjs-auth/node_modules/@multiversx/sdk-native-auth-server": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-native-auth-server/-/sdk-native-auth-server-2.0.0.tgz", - "integrity": "sha512-DdjWyt60Chk6unMVprBV/5zA8rEXjufYdE67UG6XP8RRZiGU1+k6UU14E0TU/iii8K+/5Tj/zfrIrPfbDiASoA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-native-auth-server/-/sdk-native-auth-server-2.1.0.tgz", + "integrity": "sha512-fbDHeYF2cJpRaGpz3Cqv3jfYhm/nf2jlxZzCmBEeetsuRai0eZvDImm4O58GJu3JBkR0iokWrS/zctSgX37sjg==", "license": "GPL-3.0-or-later", "dependencies": { "axios": "^1.7.4", "bech32": "^2.0.0" }, "peerDependencies": { - "@multiversx/sdk-core": "^14.x" + "@multiversx/sdk-core": "^15.x" } }, "node_modules/@multiversx/sdk-nestjs-auth/node_modules/@multiversx/sdk-native-auth-server/node_modules/bech32": { @@ -3763,9 +3821,9 @@ } }, "node_modules/@multiversx/sdk-nestjs-common/node_modules/@multiversx/sdk-core": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-14.2.3.tgz", - "integrity": "sha512-h52YIJ3udMyQSNX1QaZKbm7YpwIFvRI78QEJTes0UoboAUt5Vx5bTc8kZt8Lrfks7FKHEPaUeMdLochUcQbQ4Q==", + "version": "14.2.9", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-14.2.9.tgz", + "integrity": "sha512-tssRDroQpYTLX7ZNQyVzb6Bai4N1Nz3mUuJCW7k0i179bGUKR/VFVa0RviAXkv+d3pUkP/NepxdUaVTyaj5jmg==", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", @@ -3784,7 +3842,7 @@ }, "optionalDependencies": { "@multiversx/sdk-bls-wasm": "0.3.5", - "axios": "^1.7.4", + "axios": "^1.10.0", "bip39": "3.1.0" }, "peerDependencies": { @@ -3830,9 +3888,9 @@ } }, "node_modules/@multiversx/sdk-nestjs-http/node_modules/@multiversx/sdk-core": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-14.2.3.tgz", - "integrity": "sha512-h52YIJ3udMyQSNX1QaZKbm7YpwIFvRI78QEJTes0UoboAUt5Vx5bTc8kZt8Lrfks7FKHEPaUeMdLochUcQbQ4Q==", + "version": "14.2.9", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-14.2.9.tgz", + "integrity": "sha512-tssRDroQpYTLX7ZNQyVzb6Bai4N1Nz3mUuJCW7k0i179bGUKR/VFVa0RviAXkv+d3pUkP/NepxdUaVTyaj5jmg==", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", @@ -3851,7 +3909,7 @@ }, "optionalDependencies": { "@multiversx/sdk-bls-wasm": "0.3.5", - "axios": "^1.7.4", + "axios": "^1.10.0", "bip39": "3.1.0" }, "peerDependencies": { @@ -3949,6 +4007,12 @@ "node": ">=10" } }, + "node_modules/@multiversx/sdk-nestjs-rabbitmq/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, "node_modules/@multiversx/sdk-nestjs-rabbitmq/node_modules/promise-breaker": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", @@ -4165,9 +4229,9 @@ } }, "node_modules/@nestjs/common": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.19.tgz", - "integrity": "sha512-0TZJ8H+7qtaqZt6YfZJkDRp0e+v6jjo5/pevPAjUy0WYxaTy16bNNQxFPRKLMe/v1hUr2oGV9imvL2477zNt5g==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.20.tgz", + "integrity": "sha512-hxJxZF7jcKGuUzM9EYbuES80Z/36piJbiqmPy86mk8qOn5gglFebBTvcx7PWVbRNSb4gngASYnefBj/Y2HAzpQ==", "license": "MIT", "dependencies": { "file-type": "20.4.1", @@ -4499,6 +4563,18 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "license": "0BSD" }, + "node_modules/@nestjs/mongoose": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-11.0.3.tgz", + "integrity": "sha512-tg7bbKD4MnNMPaiDLXK/JUyTNQxIn3rNnI+oYU1HorLpNiR2E8vPraWVvfptpIj+zferpT6LkrHMvtqvuIKNPw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "mongoose": "^7.0.0 || ^8.0.0", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.19.tgz", @@ -4521,9 +4597,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.19.tgz", - "integrity": "sha512-Pfz9dBcXaoUFdVpA/I96NoOTiTQ7eYfDNhQ2sZdQzne3oISEvts5G2SWyQNouoyjqkUPt6X5r/CQ++M4AzSlEQ==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.20.tgz", + "integrity": "sha512-8wqJ7kJnvRC6T1o1U3NNnuzjaMJU43R4hvzKKba7GSdMN6j2Jfzz/vq5gHDx9xbXOAmfsc9bvaIiZegXxvHoJA==", "license": "MIT", "dependencies": { "socket.io": "4.8.1", @@ -4770,9 +4846,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.19.tgz", - "integrity": "sha512-3HhNZU40/ozB64ZZJ1W1bOOYSbxGuJwmM5Ui8y1uPwbzpL1Uov3wOG3eRp1IflSBK4Ia0wb8Iv3ChpFSTzrNiA==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.20.tgz", + "integrity": "sha512-tafsPPvQfAXc+cfxvuRDzS5V+Ixg8uVJq8xSocU24yVl/Xp6ajmhqiGiaVjYOX8mXY0NV836QwEZxHF7WvKHSw==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -5052,12 +5128,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", - "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", + "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5090,15 +5166,15 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", - "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", + "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", + "@smithy/util-middleware": "^4.0.5", "tslib": "^2.6.2" }, "engines": { @@ -5106,35 +5182,37 @@ } }, "node_modules/@smithy/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.6.0.tgz", - "integrity": "sha512-Pgvfb+TQ4wUNLyHzvgCP4aYZMh16y7GcfF59oirRHcgGgkH1e/s9C0nv/v3WP+Quymyr5je71HeFQCwh+44XLg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.9.2.tgz", + "integrity": "sha512-H7H+dnfyHa/XXmZB3+IcqB1snIvbXaeGbV7//PMY69YKMOfGtuHPg6aukxsD0TyqmIU+bcX5nitR+nf/19nTlQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.0.8", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", - "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", + "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/property-provider": "^4.0.4", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", "tslib": "^2.6.2" }, "engines": { @@ -5142,13 +5220,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.4.tgz", - "integrity": "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.5.tgz", + "integrity": "sha512-miEUN+nz2UTNoRYRhRqVTJCx7jMeILdAurStT2XoS+mhokkmz1xAPp95DFW9Gxt4iF2VBqpeF9HbTQ3kY1viOA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "@smithy/util-hex-encoding": "^4.0.0", "tslib": "^2.6.2" }, @@ -5157,13 +5235,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.4.tgz", - "integrity": "sha512-3fb/9SYaYqbpy/z/H3yIi0bYKyAa89y6xPmIqwr2vQiUT2St+avRt8UKwsWt9fEdEasc5d/V+QjrviRaX1JRFA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.5.tgz", + "integrity": "sha512-LCUQUVTbM6HFKzImYlSB9w4xafZmpdmZsOh9rIl7riPC3osCgGFVP+wwvYVw6pXda9PPT9TcEZxaq3XE81EdJQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5171,12 +5249,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.2.tgz", - "integrity": "sha512-JGtambizrWP50xHgbzZI04IWU7LdI0nh/wGbqH3sJesYToMi2j/DcoElqyOcqEIG/D4tNyxgRuaqBXWE3zOFhQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.3.tgz", + "integrity": "sha512-yTTzw2jZjn/MbHu1pURbHdpjGbCuMHWncNBpJnQAPxOVnFUAbSIUSwafiphVDjNV93TdBJWmeVAds7yl5QCkcA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5184,13 +5262,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.4.tgz", - "integrity": "sha512-RD6UwNZ5zISpOWPuhVgRz60GkSIp0dy1fuZmj4RYmqLVRtejFqQ16WmfYDdoSoAjlp1LX+FnZo+/hkdmyyGZ1w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.5.tgz", + "integrity": "sha512-lGS10urI4CNzz6YlTe5EYG0YOpsSp3ra8MXyco4aqSkQDuyZPIw2hcaxDU82OUVtK7UY9hrSvgWtpsW5D4rb4g==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5198,13 +5276,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.4.tgz", - "integrity": "sha512-UeJpOmLGhq1SLox79QWw/0n2PFX+oPRE1ZyRMxPIaFEfCqWaqpB7BU9C8kpPOGEhLF7AwEqfFbtwNxGy4ReENA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.5.tgz", + "integrity": "sha512-JFnmu4SU36YYw3DIBVao3FsJh4Uw65vVDIqlWT4LzR6gXA0F3KP0IXFKKJrhaVzCBhAuMsrUUaT5I+/4ZhF7aw==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/eventstream-codec": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5212,14 +5290,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", - "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", + "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.2", - "@smithy/querystring-builder": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" }, @@ -5228,14 +5306,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.4.tgz", - "integrity": "sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.5.tgz", + "integrity": "sha512-F7MmCd3FH/Q2edhcKd+qulWkwfChHbc9nhguBlVjSUE6hVHhec3q6uPQ+0u69S6ppvLtR3eStfCuEKMXBXhvvA==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^5.0.0", "@smithy/chunked-blob-reader-native": "^4.0.0", - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5243,12 +5321,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", - "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", + "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" @@ -5258,12 +5336,12 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.4.tgz", - "integrity": "sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.5.tgz", + "integrity": "sha512-IJuDS3+VfWB67UC0GU0uYBG/TA30w+PlOaSo0GPm9UHS88A6rCP6uZxNjNYiyRtOcjv7TXn/60cW8ox1yuZsLg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -5272,12 +5350,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", - "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", + "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5297,12 +5375,12 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.4.tgz", - "integrity": "sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.5.tgz", + "integrity": "sha512-8n2XCwdUbGr8W/XhMTaxILkVlw2QebkVTn5tm3HOcbPbOpWg89zr6dPXsH8xbeTsbTXlJvlJNTQsKAIoqQGbdA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -5311,13 +5389,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", - "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", + "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5325,18 +5403,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.13.tgz", - "integrity": "sha512-xg3EHV/Q5ZdAO5b0UiIMj3RIOCobuS40pBBODguUDVdko6YK6QIzCVRrHTogVuEKglBWqWenRnZ71iZnLL3ZAQ==", + "version": "4.1.21", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.21.tgz", + "integrity": "sha512-VCFE6LGSbnXs6uxLTdtar6dbkOHa9mrj692pZJx1mQVEzk0gvckAX9WB9BzlONUpv92QBWGezROz/+yEitQjAQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.6.0", - "@smithy/middleware-serde": "^4.0.8", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", - "@smithy/util-middleware": "^4.0.4", + "@smithy/core": "^3.9.2", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-middleware": "^4.0.5", "tslib": "^2.6.2" }, "engines": { @@ -5344,18 +5422,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.14.tgz", - "integrity": "sha512-eoXaLlDGpKvdmvt+YBfRXE7HmIEtFF+DJCbTPwuLunP0YUnrydl+C4tS+vEM0+nyxXrX3PSUFqC+lP1+EHB1Tw==", + "version": "4.1.22", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.22.tgz", + "integrity": "sha512-mb6/wn4ixnSJCkKVLs51AKAyknbSTvwrHCM7cqgwGfYQ7/J6Qvv+49cBHe6Rl8Q0m3fROVYcSvM6bBiQtuhYWg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/service-error-classification": "^4.0.6", - "@smithy/smithy-client": "^4.4.5", - "@smithy/types": "^4.3.1", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.6", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/service-error-classification": "^4.0.7", + "@smithy/smithy-client": "^4.5.2", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@types/uuid": "^9.0.1", "tslib": "^2.6.2", "uuid": "^9.0.1" }, @@ -5364,13 +5443,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", - "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", + "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5378,12 +5457,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", - "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", + "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5391,14 +5470,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", - "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", + "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5406,15 +5485,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", - "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", + "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/querystring-builder": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/abort-controller": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5422,12 +5501,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", - "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", + "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5435,12 +5514,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", - "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", + "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5448,12 +5527,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", - "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", + "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" }, @@ -5462,12 +5541,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", - "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", + "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5475,24 +5554,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.6.tgz", - "integrity": "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1" + "@smithy/types": "^4.3.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", - "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", + "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5500,16 +5579,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", - "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", + "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", + "@smithy/util-middleware": "^4.0.5", "@smithy/util-uri-escape": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" @@ -5519,17 +5598,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.5.tgz", - "integrity": "sha512-+lynZjGuUFJaMdDYSTMnP/uPBBXXukVfrJlP+1U/Dp5SFTEI++w6NMga8DjOENxecOF71V9Z2DllaVDYRnGlkg==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.5.2.tgz", + "integrity": "sha512-WRdTJ7aNSJY0WuGpxrvVgRaFKGiuvtXX1Txhnu2BdynraSlH2bcP75riQ4SiQfawU1HNEKaPI5gf/ePm+Ro/Cw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.6.0", - "@smithy/middleware-endpoint": "^4.1.13", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "@smithy/util-stream": "^4.2.2", + "@smithy/core": "^3.9.2", + "@smithy/middleware-endpoint": "^4.1.21", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", "tslib": "^2.6.2" }, "engines": { @@ -5537,9 +5616,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", - "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", + "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -5549,13 +5628,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", - "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", + "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/querystring-parser": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5626,14 +5705,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.21", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.21.tgz", - "integrity": "sha512-wM0jhTytgXu3wzJoIqpbBAG5U6BwiubZ6QKzSbP7/VbmF1v96xlAbX2Am/mz0Zep0NLvLh84JT0tuZnk3wmYQA==", + "version": "4.0.29", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.29.tgz", + "integrity": "sha512-awrIb21sWml3OMRhqf8e5GPLuZAcH3PRAHXVOPof/rBOKLxc6N01ZRs25154Ww6Ygm9oNP6G0tVvhcy8ktYXtw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.0.4", - "@smithy/smithy-client": "^4.4.5", - "@smithy/types": "^4.3.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.5.2", + "@smithy/types": "^4.3.2", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -5642,17 +5721,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.21", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.21.tgz", - "integrity": "sha512-/F34zkoU0GzpUgLJydHY8Rxu9lBn8xQC/s/0M0U9lLBkYbA1htaAFjWYJzpzsbXPuri5D1H8gjp2jBum05qBrA==", + "version": "4.0.29", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.29.tgz", + "integrity": "sha512-DxBWCC059GwOQXc5nxVudhdGQLZHTDhU4rkK4rvaBQn8IWBw8G+3H2hWk897LaNv6zwwhh7kpfqF0rJ77DvlSg==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.1.4", - "@smithy/credential-provider-imds": "^4.0.6", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/property-provider": "^4.0.4", - "@smithy/smithy-client": "^4.4.5", - "@smithy/types": "^4.3.1", + "@smithy/config-resolver": "^4.1.5", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.5.2", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5660,13 +5739,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", - "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", + "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5686,12 +5765,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", - "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", + "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5699,13 +5778,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.6.tgz", - "integrity": "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", + "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.0.6", - "@smithy/types": "^4.3.1", + "@smithy/service-error-classification": "^4.0.7", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5713,14 +5792,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", - "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", + "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/types": "^4.3.1", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/types": "^4.3.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", @@ -5757,13 +5836,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.6.tgz", - "integrity": "sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.7.tgz", + "integrity": "sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/abort-controller": "^4.0.5", + "@smithy/types": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5899,6 +5978,16 @@ "@types/node": "*" } }, + "node_modules/@types/amqplib": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.7.tgz", + "integrity": "sha512-IVj3avf9AQd2nXCx0PGk/OYq7VmHiyNxWFSb5HhU9ATh+i+gHWvVcljFTcTWQ/dyHJCTrzCixde+r/asL2ErDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5935,13 +6024,13 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/body-parser": { @@ -6153,10 +6242,17 @@ "@types/node": "*" } }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-NYqRyg/hIQrYPT9lbOeYc3kIRabJDn/k4qQHIXUpx88CBDww2fD15Sg5kbXlW86zm2XEW4g0QxkTI3/Kfkc7xQ==", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", "license": "MIT" }, "node_modules/@types/long": { @@ -6165,6 +6261,24 @@ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "license": "MIT" }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -6185,14 +6299,14 @@ "license": "MIT" }, "node_modules/@types/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", "license": "MIT", "peer": true, "dependencies": { "@types/node": "*", - "form-data": "^4.0.0" + "form-data": "^4.0.4" } }, "node_modules/@types/qs": { @@ -6228,9 +6342,9 @@ } }, "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "dev": true, "license": "MIT" }, @@ -6315,9 +6429,9 @@ "license": "MIT" }, "node_modules/@types/validator": { - "version": "13.15.2", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", - "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", "license": "MIT" }, "node_modules/@types/webidl-conversions": { @@ -6773,6 +6887,19 @@ "acorn": "^8" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -6873,9 +7000,9 @@ } }, "node_modules/amqplib": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.8.tgz", - "integrity": "sha512-Tfn1O9sFgAP8DqeMEpt2IacsVTENBpblB3SqLdn0jK2AeX8iyCvbptBc8lyATT9bQ31MsjVwUSQ1g8f4jHOUfw==", + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", + "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", "license": "MIT", "dependencies": { "buffer-more-ints": "~1.0.0", @@ -7543,13 +7670,13 @@ } }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -7636,9 +7763,9 @@ } }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, "license": "MIT", "dependencies": { @@ -7659,7 +7786,7 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { @@ -7727,9 +7854,9 @@ "license": "MIT" }, "node_modules/bignumber.js": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", - "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", "engines": { "node": "*" @@ -7901,9 +8028,9 @@ "license": "MIT" }, "node_modules/bowser": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -7930,9 +8057,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "dev": true, "funding": [ { @@ -7950,8 +8077,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -8124,9 +8251,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001726", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", - "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", "dev": true, "funding": [ { @@ -8144,6 +8271,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8538,16 +8678,16 @@ } }, "node_modules/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -8839,9 +8979,9 @@ "license": "MIT" }, "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", "license": "MIT" }, "node_modules/dc-polyfill": { @@ -9237,26 +9377,10 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/electron-to-chromium": { - "version": "1.5.178", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.178.tgz", - "integrity": "sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==", + "version": "1.5.213", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz", + "integrity": "sha512-xr9eRzSLNa4neDO0xVFrkXu3vyIzG4Ay08dApecw42Z1NbmCt+keEpXdvlYGVe0wtvY5dhW0Ay0lY0IOfsCg0Q==", "dev": true, "license": "ISC" }, @@ -9422,9 +9546,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -9435,6 +9559,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -9577,52 +9714,140 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "esprima": "^4.0.1", + "estraverse": "^4.2.0", "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" @@ -9635,9 +9860,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", "dev": true, "license": "MIT", "bin": { @@ -9648,9 +9873,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.5.tgz", + "integrity": "sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg==", "dev": true, "license": "MIT", "dependencies": { @@ -10130,22 +10355,18 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -10248,39 +10469,6 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -10437,9 +10625,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -10517,9 +10705,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -10582,9 +10770,9 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", - "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", "dev": true, "license": "Unlicense" }, @@ -10863,14 +11051,15 @@ } }, "node_modules/graphql-request/node_modules/form-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.3.tgz", - "integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.35" }, "engines": { @@ -10916,6 +11105,38 @@ "graphql": ">=0.11 <=16" } }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -11204,12 +11425,12 @@ } }, "node_modules/ioredis": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", - "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", + "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", "license": "MIT", "dependencies": { - "@ioredis/commands": "^1.1.1", + "@ioredis/commands": "^1.3.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -11406,9 +11627,9 @@ } }, "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, "node_modules/isexe": { @@ -11484,9 +11705,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11527,32 +11748,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/jest": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", @@ -12211,6 +12406,69 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz", + "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -12299,9 +12557,9 @@ "license": "MIT" }, "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { @@ -12354,6 +12612,16 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/keccak": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", @@ -12396,6 +12664,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -12446,9 +12724,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.13", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.13.tgz", - "integrity": "sha512-QZXnR/OGiDcBjF4hGk0wwVrPcZvbSSyzlvkjXv5LFfktj7O2VZDrt4Xs8SgR/vOFco+qk1i8J43ikMXZoTrtPw==", + "version": "1.12.15", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.15.tgz", + "integrity": "sha512-TMDCtIhWUDHh91wRC+wFuGlIzKdPzaTUHHVrIZ3vPUEoNaXFLrsIQ1ZpAeZeXApIF6rvDksMTvjrIQlLKaYxqQ==", "license": "MIT" }, "node_modules/limiter": { @@ -12463,6 +12741,16 @@ "dev": true, "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -12686,9 +12974,9 @@ } }, "node_modules/luxon": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", - "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", "license": "MIT", "engines": { "node": ">=12" @@ -12740,6 +13028,48 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -12760,6 +13090,13 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -12987,9 +13324,9 @@ } }, "node_modules/mongodb": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz", - "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.19.0.tgz", + "integrity": "sha512-H3GtYujOJdeKIMLKBT9PwlDhGrQfplABNF1G904w6r5ZXKWyv77aB0X9B+rhmaAwjtllHzaEkvi9mkGVZxs2Bw==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", @@ -13005,7 +13342,7 @@ "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", + "snappy": "^7.3.2", "socks": "^2.7.1" }, "peerDependenciesMeta": { @@ -13042,6 +13379,99 @@ "whatwg-url": "^14.1.0 || ^13.0.0" } }, + "node_modules/mongoose": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.0.tgz", + "integrity": "sha512-3TixPihQKBdyaYDeJqRjzgb86KbilEH07JmzV8SoSjgoskNTpa6oTBmDxeoF9p8YnWQoz7shnCyPkSV/48y3yw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.18.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", + "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -13083,15 +13513,15 @@ } }, "node_modules/mysql2": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz", - "integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==", + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.4.tgz", + "integrity": "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", @@ -13103,15 +13533,19 @@ } }, "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mysql2/node_modules/long": { @@ -13356,9 +13790,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -13917,9 +14351,9 @@ } }, "node_modules/pprof-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pprof-format/-/pprof-format-2.1.0.tgz", - "integrity": "sha512-0+G5bHH0RNr8E5hoZo/zJYsL92MhkZjwrHp3O2IxmY8RJL9ooKeuZ8Tm0ZNBw5sGZ9TiM71sthTjWoR2Vf5/xw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/pprof-format/-/pprof-format-2.2.1.tgz", + "integrity": "sha512-p4tVN7iK19ccDqQv8heyobzUmbHyds4N2FI6aBMcXz6y99MglTWDxIyhFkNaLeEXs6IFUEzT0zya0icbSLLY0g==", "license": "MIT" }, "node_modules/prelude-ls": { @@ -14028,9 +14462,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", - "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -14051,6 +14485,99 @@ "node": ">=12.0.0" } }, + "node_modules/protobufjs-cli": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz", + "integrity": "sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/protobufjs-cli/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/protobufjs-cli/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/protobufjs-cli/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/protobufjs-cli/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs-cli/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/protobufjs/node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -14085,6 +14612,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -14345,6 +14882,16 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -14747,22 +15294,29 @@ "license": "ISC" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/sharp": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz", - "integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -14777,27 +15331,28 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.2", - "@img/sharp-darwin-x64": "0.34.2", - "@img/sharp-libvips-darwin-arm64": "1.1.0", - "@img/sharp-libvips-darwin-x64": "1.1.0", - "@img/sharp-libvips-linux-arm": "1.1.0", - "@img/sharp-libvips-linux-arm64": "1.1.0", - "@img/sharp-libvips-linux-ppc64": "1.1.0", - "@img/sharp-libvips-linux-s390x": "1.1.0", - "@img/sharp-libvips-linux-x64": "1.1.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", - "@img/sharp-libvips-linuxmusl-x64": "1.1.0", - "@img/sharp-linux-arm": "0.34.2", - "@img/sharp-linux-arm64": "0.34.2", - "@img/sharp-linux-s390x": "0.34.2", - "@img/sharp-linux-x64": "0.34.2", - "@img/sharp-linuxmusl-arm64": "0.34.2", - "@img/sharp-linuxmusl-x64": "0.34.2", - "@img/sharp-wasm32": "0.34.2", - "@img/sharp-win32-arm64": "0.34.2", - "@img/sharp-win32-ia32": "0.34.2", - "@img/sharp-win32-x64": "0.34.2" + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" } }, "node_modules/shebang-command": { @@ -14905,6 +15460,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT", + "peer": true + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -15368,9 +15930,9 @@ } }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", "funding": [ { "type": "github", @@ -15380,9 +15942,9 @@ "license": "MIT" }, "node_modules/strtok3": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.1.tgz", - "integrity": "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==", + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0" @@ -15446,7 +16008,7 @@ "version": "8.1.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", - "deprecated": "Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", "dev": true, "license": "MIT", "dependencies": { @@ -15482,6 +16044,7 @@ "version": "6.3.4", "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", "dev": true, "license": "MIT", "dependencies": { @@ -15549,13 +16112,17 @@ } }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tdigest": { @@ -15568,14 +16135,14 @@ } }, "node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -15828,12 +16395,6 @@ "node": ">= 0.4" } }, - "node_modules/to-buffer/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -15856,11 +16417,12 @@ } }, "node_modules/token-types": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.3.tgz", - "integrity": "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", "license": "MIT", "dependencies": { + "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" }, @@ -15910,15 +16472,15 @@ } }, "node_modules/ts-jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", - "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", - "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", @@ -16200,9 +16762,9 @@ "license": "MIT" }, "node_modules/typeorm": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.25.tgz", - "integrity": "sha512-fTKDFzWXKwAaBdEMU4k661seZewbNYET4r1J/z3Jwf+eAvlzMVpTLKAVcAzg75WwQk7GDmtsmkZ5MfkmXCiFWg==", + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz", + "integrity": "sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==", "license": "MIT", "dependencies": { "@sqltools/formatter": "^1.2.5", @@ -16233,9 +16795,8 @@ }, "peerDependencies": { "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", - "@sap/hana-client": "^2.12.25", - "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", - "hdb-pool": "^0.1.6", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", "ioredis": "^5.0.4", "mongodb": "^5.8.0 || ^6.0.0", "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", @@ -16244,7 +16805,7 @@ "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", - "redis": "^3.1.1 || ^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", "reflect-metadata": "^0.1.14 || ^0.2.0", "sql.js": "^1.4.0", "sqlite3": "^5.0.3", @@ -16261,9 +16822,6 @@ "better-sqlite3": { "optional": true }, - "hdb-pool": { - "optional": true - }, "ioredis": { "optional": true }, @@ -16344,6 +16902,26 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", @@ -16357,9 +16935,9 @@ } }, "node_modules/uint8array-extras": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", - "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", "license": "MIT", "engines": { "node": ">=18" @@ -16368,6 +16946,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -16559,22 +17144,23 @@ } }, "node_modules/webpack": { - "version": "5.99.9", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", - "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", + "version": "5.101.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", + "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", + "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", + "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -16588,7 +17174,7 @@ "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -16880,6 +17466,13 @@ } } }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/xmlhttprequest-ssl": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", diff --git a/package.json b/package.json index dbbad71f9..ee94b1a34 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "@nestjs/event-emitter": "^2.0.3", "@nestjs/graphql": "^12.0.11", "@nestjs/microservices": "10.2.4", + "@nestjs/mongoose": "^11.0.3", "@nestjs/platform-express": "10.4.19", "@nestjs/platform-socket.io": "^10.2.4", "@nestjs/schedule": "3.0.3", @@ -113,7 +114,7 @@ "@sendgrid/mail": "^8.1.5", "agentkeepalive": "^4.2.1", "amqp-connection-manager": "^4.1.3", - "amqplib": "^0.10.0", + "amqplib": "^0.10.9", "anchorme": "^3.0.8", "apollo-server-core": "^3.13.0", "apollo-server-express": "3.13.0", @@ -139,6 +140,7 @@ "node-object-hash": "^2.3.10", "pg": "^8.7.3", "prom-client": "^14.0.1", + "protobufjs": "^7.5.4", "redis": "^3.1.2", "reflect-metadata": "^0.1.13", "request-ip": "^3.3.0", @@ -163,6 +165,7 @@ "@nestjs/schematics": "10.0.2", "@nestjs/testing": "10.2.4", "@testing-library/jest-dom": "6.1.4", + "@types/amqplib": "^0.10.7", "@types/compression": "^1.8.1", "@types/cron": "^1.7.3", "@types/crypto-js": "^4.0.2", @@ -185,6 +188,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "29.5.0", "prettier": "^2.5.1", + "protobufjs-cli": "^1.1.3", "run-script-os": "^1.1.6", "supertest": "^6.2.2", "ts-jest": "^29.0.5", From 626d1be25d84c3b649734e3ce4ba37781acd617e Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 4 Sep 2025 13:12:34 +0300 Subject: [PATCH 17/90] add accounts v2 module --- .../account.details.repository.ts | 5 +- src/common/rabbitmq/rabbitmq.module.ts | 1 + .../accounts-v2/account.controller.v2.ts | 1420 +++++++++++++++++ .../accounts-v2/account.module.v2.ts | 46 + .../accounts-v2/account.service.v2.ts | 773 +++++++++ .../accounts-v2/entities/account.contract.ts | 20 + .../accounts-v2/entities/account.deferred.ts | 14 + .../accounts-v2/entities/account.detailed.ts | 74 + .../entities/account.esdt.history.ts | 15 + .../entities/account.fetch.options.ts | 11 + .../entities/account.history.filter.ts | 10 + .../accounts-v2/entities/account.history.ts | 20 + .../entities/account.key.filter.ts | 10 + .../accounts-v2/entities/account.key.ts | 33 + .../entities/account.query.options.ts | 58 + .../accounts-v2/entities/account.sort.ts | 23 + src/endpoints/accounts-v2/entities/account.ts | 51 + .../entities/account.verification.source.ts | 13 + .../entities/account.verification.status.ts | 4 + .../entities/account.verification.ts | 21 + .../entities/application.most.used.ts | 8 + .../accounts-v2/entities/contract.upgrades.ts | 17 + .../accounts-v2/entities/deployed.contract.ts | 20 + src/endpoints/endpoints.controllers.module.ts | 3 +- src/endpoints/endpoints.services.module.ts | 4 +- src/endpoints/nfts/nft.module.ts | 2 + src/endpoints/nfts/nft.service.ts | 25 + src/endpoints/tokens/token.module.ts | 2 + src/endpoints/tokens/token.service.ts | 27 + .../utils/state-changes.utils.ts | 33 +- 30 files changed, 2758 insertions(+), 5 deletions(-) create mode 100644 src/endpoints/accounts-v2/account.controller.v2.ts create mode 100644 src/endpoints/accounts-v2/account.module.v2.ts create mode 100644 src/endpoints/accounts-v2/account.service.v2.ts create mode 100644 src/endpoints/accounts-v2/entities/account.contract.ts create mode 100644 src/endpoints/accounts-v2/entities/account.deferred.ts create mode 100644 src/endpoints/accounts-v2/entities/account.detailed.ts create mode 100644 src/endpoints/accounts-v2/entities/account.esdt.history.ts create mode 100644 src/endpoints/accounts-v2/entities/account.fetch.options.ts create mode 100644 src/endpoints/accounts-v2/entities/account.history.filter.ts create mode 100644 src/endpoints/accounts-v2/entities/account.history.ts create mode 100644 src/endpoints/accounts-v2/entities/account.key.filter.ts create mode 100644 src/endpoints/accounts-v2/entities/account.key.ts create mode 100644 src/endpoints/accounts-v2/entities/account.query.options.ts create mode 100644 src/endpoints/accounts-v2/entities/account.sort.ts create mode 100644 src/endpoints/accounts-v2/entities/account.ts create mode 100644 src/endpoints/accounts-v2/entities/account.verification.source.ts create mode 100644 src/endpoints/accounts-v2/entities/account.verification.status.ts create mode 100644 src/endpoints/accounts-v2/entities/account.verification.ts create mode 100644 src/endpoints/accounts-v2/entities/application.most.used.ts create mode 100644 src/endpoints/accounts-v2/entities/contract.upgrades.ts create mode 100644 src/endpoints/accounts-v2/entities/deployed.contract.ts diff --git a/src/common/indexer/db/repositories/account.details.repository.ts b/src/common/indexer/db/repositories/account.details.repository.ts index 9eae5d781..3a4abffb5 100644 --- a/src/common/indexer/db/repositories/account.details.repository.ts +++ b/src/common/indexer/db/repositories/account.details.repository.ts @@ -7,6 +7,7 @@ import { QueryPagination } from 'src/common/entities/query.pagination'; import { TokenWithBalance } from 'src/endpoints/tokens/entities/token.with.balance'; import { NftAccount } from 'src/endpoints/nfts/entities/nft.account'; import { Injectable } from '@nestjs/common'; +import { AccountDetailed } from 'src/endpoints/accounts/entities/account.detailed'; @Injectable() export class AccountDetailsRepository { @@ -242,7 +243,7 @@ export class AccountDetailsRepository { } @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-details') - async getAccount(address: string): Promise { + async getAccount(address: string): Promise { try { const accountDb = await this.accountDetailsModel.findOne( { address }, @@ -251,7 +252,7 @@ export class AccountDetailsRepository { if (!accountDb) { return null; } - return accountDb; + return new AccountDetailed({ ...accountDb, nonce: parseInt(accountDb.nonce) }); } catch (error) { console.error('Error fetching account:', error); return null; diff --git a/src/common/rabbitmq/rabbitmq.module.ts b/src/common/rabbitmq/rabbitmq.module.ts index e85b77145..ac5457a63 100644 --- a/src/common/rabbitmq/rabbitmq.module.ts +++ b/src/common/rabbitmq/rabbitmq.module.ts @@ -38,6 +38,7 @@ export class RabbitMqModule { type: 'fanout', options: {}, uri: apiConfigService.getEventsNotifierUrl(), + prefetchCount: 1, }; }, }), diff --git a/src/endpoints/accounts-v2/account.controller.v2.ts b/src/endpoints/accounts-v2/account.controller.v2.ts new file mode 100644 index 000000000..d3593bf73 --- /dev/null +++ b/src/endpoints/accounts-v2/account.controller.v2.ts @@ -0,0 +1,1420 @@ +import { Controller, DefaultValuePipe, Get, HttpException, HttpStatus, NotFoundException, Param, Query, UseInterceptors } from '@nestjs/common'; +import { ApiExcludeEndpoint, ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { AccountServiceV2 } from './account.service.v2'; +import { AccountDetailed } from './entities/account.detailed'; +import { Account } from './entities/account'; +import { AccountDeferred } from './entities/account.deferred'; +import { TokenService } from '../tokens/token.service'; +import { TokenWithBalance } from '../tokens/entities/token.with.balance'; +import { DelegationLegacyService } from '../delegation.legacy/delegation.legacy.service'; +import { AccountDelegationLegacy } from '../delegation.legacy/entities/account.delegation.legacy'; +import { AccountKey } from './entities/account.key'; +import { NftAccount } from '../nfts/entities/nft.account'; +import { NftType } from '../nfts/entities/nft.type'; +import { WaitingList } from '../waiting-list/entities/waiting.list'; +import { WaitingListService } from '../waiting-list/waiting.list.service'; +import { StakeService } from '../stake/stake.service'; +import { NftService } from '../nfts/nft.service'; +import { TransactionStatus } from '../transactions/entities/transaction.status'; +import { TransactionService } from '../transactions/transaction.service'; +import { DeployedContract } from './entities/deployed.contract'; +import { SmartContractResult } from '../sc-results/entities/smart.contract.result'; +import { SmartContractResultService } from '../sc-results/scresult.service'; +import { CollectionService } from '../collections/collection.service'; +import { NftCollectionWithRoles } from '../collections/entities/nft.collection.with.roles'; +import { SortOrder } from 'src/common/entities/sort.order'; +import { AccountHistory } from "./entities/account.history"; +import { AccountEsdtHistory } from "./entities/account.esdt.history"; +import { EsdtDataSource } from '../esdt/entities/esdt.data.source'; +import { TransferService } from '../transfers/transfer.service'; +import { Transaction } from '../transactions/entities/transaction'; +import { ProviderStake } from '../stake/entities/provider.stake'; +import { TokenDetailedWithBalance } from '../tokens/entities/token.detailed.with.balance'; +import { NftCollectionAccount } from '../collections/entities/nft.collection.account'; +import { TokenWithRoles } from '../tokens/entities/token.with.roles'; +import { ParseAddressPipe, ParseTokenPipe, OriginLogger, ParseArrayPipe, ParseBlockHashPipe, ParseCollectionPipe, ParseNftPipe, ParseBoolPipe, ParseEnumArrayPipe, ParseEnumPipe, ParseIntPipe, ParseTokenOrNftPipe, ParseTransactionHashPipe, ParseAddressArrayPipe, ApplyComplexity, ParseNftArrayPipe } from '@multiversx/sdk-nestjs-common'; +import { QueryPagination } from 'src/common/entities/query.pagination'; +import { TransactionQueryOptions } from '../transactions/entities/transactions.query.options'; +import { TokenWithRolesFilter } from '../tokens/entities/token.with.roles.filter'; +import { CollectionFilter } from '../collections/entities/collection.filter'; +import { TokenFilter } from '../tokens/entities/token.filter'; +import { NftFilter } from '../nfts/entities/nft.filter'; +import { NftQueryOptions } from '../nfts/entities/nft.query.options'; +import { TransactionFilter } from '../transactions/entities/transaction.filter'; +import { TransactionDetailed } from '../transactions/entities/transaction.detailed'; +import { AccountDelegation } from '../stake/entities/account.delegation'; +import { DelegationService } from '../delegation/delegation.service'; +import { TokenType } from '../tokens/entities/token.type'; +import { ContractUpgrades } from './entities/contract.upgrades'; +import { AccountVerification } from './entities/account.verification'; +import { AccountQueryOptions } from './entities/account.query.options'; +import { AccountSort } from './entities/account.sort'; +import { AccountHistoryFilter } from './entities/account.history.filter'; +import { ParseArrayPipeOptions } from '@multiversx/sdk-nestjs-common/lib/pipes/entities/parse.array.options'; +import { NodeStatusRaw } from '../nodes/entities/node.status'; +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 { NftSubType } from '../nfts/entities/nft.sub.type'; +import { AccountContract } from './entities/account.contract'; +import { AccountFetchOptions } from './entities/account.fetch.options'; +@Controller('/v2') +@ApiTags('accounts') +export class AccountControllerV2 { + private readonly logger = new OriginLogger(AccountControllerV2.name); + + constructor( + private readonly accountService: AccountServiceV2, + private readonly tokenService: TokenService, + private readonly nftService: NftService, + private readonly delegationLegacyService: DelegationLegacyService, + private readonly waitingListService: WaitingListService, + private readonly stakeService: StakeService, + private readonly transactionService: TransactionService, + private readonly scResultService: SmartContractResultService, + private readonly collectionService: CollectionService, + private readonly transferService: TransferService, + private readonly delegationService: DelegationService, + ) { } + + @Get("/accounts") + @ApiOperation({ summary: 'Accounts details', description: 'Returns all accounts available on blockchain. By default it returns 25 accounts' }) + @ApiOkResponse({ type: [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 }) + @ApiQuery({ name: 'ownerAddress', description: 'Search by owner address', required: false }) + @ApiQuery({ name: 'sort', description: 'Sort criteria (balance / timestamp)', required: false, enum: AccountSort }) + @ApiQuery({ name: 'order', description: 'Sort order (asc/desc)', required: false, enum: SortOrder }) + @ApiQuery({ name: 'isSmartContract', description: 'Filter accounts by whether they are smart contract or not', required: false }) + @ApiQuery({ name: 'withOwnerAssets', description: 'Return a list accounts with owner assets', required: false }) + @ApiQuery({ name: 'withDeployInfo', description: 'Include deployedAt and deployTxHash fields in the result', required: false }) + @ApiQuery({ name: 'withTxCount', description: 'Include txCount field in the result', required: false }) + @ApiQuery({ name: 'withScrCount', description: 'Include scrCount field in the result', required: false }) + @ApiQuery({ name: 'name', description: 'Filter accounts by assets name', required: false }) + @ApiQuery({ name: 'tags', description: 'Filter accounts by assets tags', required: false }) + @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", ParseArrayPipe) tags?: string[], + @Query('sort', new ParseEnumPipe(AccountSort)) sort?: AccountSort, + @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, + @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, + withOwnerAssets, + withDeployInfo, + withTxCount, + withScrCount, + name, + tags, + excludeTags, + hasAssets, + search, + }); + queryOptions.validate(size); + return this.accountService.getAccounts( + new QueryPagination({ from, size }), + queryOptions, + ); + } + + @Get("/accounts/count") + @ApiOperation({ summary: 'Total number of accounts', description: 'Returns total number of accounts available on blockchain' }) + @ApiOkResponse({ type: Number }) + @ApiQuery({ name: 'ownerAddress', description: 'Search by owner address', required: false }) + @ApiQuery({ name: 'isSmartContract', description: 'Return total smart contracts count', required: false }) + @ApiQuery({ name: 'name', description: 'Filter accounts by assets name', required: false }) + @ApiQuery({ name: 'tags', description: 'Filter accounts by assets tags', required: false }) + @ApiQuery({ name: 'search', description: 'Search by account address, assets name', required: false }) + @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 }) + async getAccountsCount( + @Query("ownerAddress", ParseAddressPipe) ownerAddress?: string, + @Query("isSmartContract", ParseBoolPipe) isSmartContract?: boolean, + @Query("name") name?: string, + @Query("tags", ParseArrayPipe) tags?: string[], + @Query("excludeTags", ParseArrayPipe) excludeTags?: string[], + @Query("hasAssets", ParseBoolPipe) hasAssets?: boolean, + @Query("search") search?: string, + ): Promise { + return await this.accountService.getAccountsCount( + new AccountQueryOptions( + { + ownerAddress, + isSmartContract, + name, + tags, + excludeTags, + hasAssets, + search, + })); + } + + @Get("/accounts/c") + @ApiExcludeEndpoint() + async getAccountsCountAlternative( + @Query("ownerAddress", ParseAddressPipe) ownerAddress?: string, + @Query("isSmartContract", ParseBoolPipe) isSmartContract?: boolean, + @Query("name") name?: string, + @Query("tags", ParseArrayPipe) tags?: string[], + @Query("excludeTags", ParseArrayPipe) excludeTags?: string[], + @Query("hasAssets", ParseBoolPipe) hasAssets?: boolean, + @Query("search") search?: string, + ): Promise { + return await this.accountService.getAccountsCount( + new AccountQueryOptions( + { + ownerAddress, + isSmartContract, + name, + tags, + excludeTags, + hasAssets, + search, + })); + } + + @Get("/accounts/:address") + @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: '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 }) + // @NoCache() + async getAccountDetails( + @Param('address', ParseAddressPipe) address: 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.getAccountFromDb( + address, + new AccountFetchOptions({ withGuardianInfo, withTxCount, withScrCount, withTimestamp, withAssets }), + ); + if (!account) { + throw new NotFoundException('Account not found'); + } + + return account; + } + + @Get("/accounts/:address/deferred") + @ApiOperation({ summary: 'Account deferred payment details', description: 'Returns deferred payments from legacy staking' }) + @ApiOkResponse({ type: [AccountDeferred] }) + async getAccountDeferred(@Param('address', ParseAddressPipe) address: string): Promise { + try { + return await this.accountService.getDeferredAccount(address); + } catch (error) { + this.logger.error(`Error in getAccountDeferred for address ${address}`); + this.logger.error(error); + throw new HttpException('Account not found', HttpStatus.NOT_FOUND); + } + } + + @Get("/accounts/:address/verification") + @ApiOperation({ summary: 'Account verification details', description: 'Returns contract verification details' }) + @ApiOkResponse({ type: AccountVerification }) + async getAccountVerification(@Param('address', ParseAddressPipe) address: string): Promise { + try { + return await this.accountService.getAccountVerification(address); + } catch (error) { + this.logger.error(`Error in getAccountVerification for address ${address}`); + this.logger.error(error); + throw new HttpException('Account verification not found', HttpStatus.NOT_FOUND); + } + } + + @Get("/accounts/:address/tokens") + @UseInterceptors(DeepHistoryInterceptor) + @ApiOperation({ summary: 'Account tokens', description: 'Returns a list of all available fungible tokens for a given address, together with their balance' }) + @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 }) + @ApiQuery({ name: 'type', description: 'Token type', required: false, enum: TokenType }) + @ApiQuery({ name: 'subType', description: 'Token sub type', required: false, enum: NftSubType }) + @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) + @ApiQuery({ name: 'name', description: 'Search by token name', required: false }) + @ApiQuery({ name: 'identifier', description: 'Search by token identifier', required: false }) + @ApiQuery({ name: 'identifiers', description: 'A comma-separated list of identifiers to filter by', required: false, type: String }) + @ApiQuery({ name: 'includeMetaESDT', description: 'Include MetaESDTs in response', required: false, type: Boolean }) + @ApiQuery({ name: 'timestamp', description: 'Retrieve entries from timestamp', required: false, type: Number }) + @ApiQuery({ name: 'mexPairType', description: 'Token Mex Pair', required: false, enum: MexPairType }) + @ApiOkResponse({ type: [TokenWithBalance] }) + // @NoCache() + async getAccountTokens( + @Param('address', ParseAddressPipe) address: string, + @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, + @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, + @Query('type', new ParseEnumPipe(TokenType)) type?: TokenType, + @Query('subType', new ParseEnumPipe(NftSubType)) subType?: NftSubType, + @Query('search') search?: string, + @Query('name') name?: string, + @Query('identifier') identifier?: string, + @Query('identifiers', ParseArrayPipe) identifiers?: string[], + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, + @Query('timestamp', ParseIntPipe) _timestamp?: number, + @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], + ): Promise { + try { + return await this.tokenService.getTokensForAddressFromDb(address, new QueryPagination({ from, size }), new TokenFilter({ type, subType, search, name, identifier, identifiers, includeMetaESDT, mexPairType })); + } catch (error) { + this.logger.error(`Error in getAccountTokens for address ${address}`); + this.logger.error(error); + // throw new HttpException('Account not found', HttpStatus.NOT_FOUND); + return []; + } + } + + @Get("/accounts/:address/tokens/count") + @UseInterceptors(DeepHistoryInterceptor) + @ApiOperation({ summary: 'Account token count', description: 'Returns the total number of tokens for a given address' }) + @ApiQuery({ name: 'type', description: 'Token type', required: false, enum: TokenType }) + @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) + @ApiQuery({ name: 'name', description: 'Search by token name', required: false }) + @ApiQuery({ name: 'identifier', description: 'Search by token identifier', required: false }) + @ApiQuery({ name: 'identifiers', description: 'A comma-separated list of identifiers to filter by', required: false, type: String }) + @ApiQuery({ name: 'includeMetaESDT', description: 'Include MetaESDTs in response', required: false, type: Boolean }) + @ApiQuery({ name: 'timestamp', description: 'Retrieve entries from timestamp', required: false, type: Number }) + @ApiQuery({ name: 'mexPairType', description: 'Token Mex Pair', required: false, enum: MexPairType }) + @ApiOkResponse({ type: Number }) + async getTokenCount( + @Param('address', ParseAddressPipe) address: string, + @Query('type', new ParseEnumPipe(TokenType)) type?: TokenType, + @Query('search') search?: string, + @Query('name') name?: string, + @Query('identifier') identifier?: string, + @Query('identifiers', ParseArrayPipe) identifiers?: string[], + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, + @Query('timestamp', ParseIntPipe) _timestamp?: number, + @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], + ): Promise { + try { + return await this.tokenService.getTokenCountForAddress(address, new TokenFilter({ type, search, name, identifier, identifiers, includeMetaESDT, mexPairType })); + } catch (error) { + this.logger.error(`Error in getTokenCount for address ${address}`); + this.logger.error(error); + // throw new HttpException('Account not found', HttpStatus.NOT_FOUND); + return 0; + } + } + + @Get("/accounts/:address/tokens/c") + @UseInterceptors(DeepHistoryInterceptor) + @ApiExcludeEndpoint() + async getTokenCountAlternative( + @Param('address', ParseAddressPipe) address: string, + @Query('type', new ParseEnumPipe(TokenType)) type?: TokenType, + @Query('search') search?: string, + @Query('name') name?: string, + @Query('identifier') identifier?: string, + @Query('identifiers', ParseArrayPipe) identifiers?: string[], + @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, + @Query('timestamp', ParseIntPipe) _timestamp?: number, + @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], + ): Promise { + try { + return await this.tokenService.getTokenCountForAddress(address, new TokenFilter({ type, search, name, identifier, identifiers, includeMetaESDT, mexPairType })); + } catch (error) { + this.logger.error(`Error in getTokenCount for address ${address}`); + this.logger.error(error); + // throw new HttpException('Account not found', HttpStatus.NOT_FOUND); + return 0; + } + } + + @Get("/accounts/:address/tokens/:token") + @UseInterceptors(DeepHistoryInterceptor) + @ApiOkResponse({ type: TokenWithBalance }) + @ApiOperation({ summary: 'Account token details', description: 'Returns details about a specific fungible token from a given address' }) + @ApiQuery({ name: 'timestamp', description: 'Retrieve entries from timestamp', required: false, type: Number }) + // @NoCache() + async getAccountToken( + @Param('address', ParseAddressPipe) address: string, + @Param('token', ParseTokenOrNftPipe) token: string, + @Query('timestamp', ParseIntPipe) _timestamp?: number, + ): Promise { + const result = await this.tokenService.getTokenForAddressFromDb(address, token); + if (!result) { + throw new HttpException('Token for given account not found', HttpStatus.NOT_FOUND); + } + + return result; + } + + @Get("/accounts/:address/roles/collections") + @ApiOperation({ summary: 'Account collections', description: 'Returns NFT/SFT/MetaESDT collections where the account is owner or has some special roles assigned to it' }) + @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 }) + @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) + @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) + @ApiQuery({ name: 'subType', description: 'Filter by type (NonFungibleESDTv2/DynamicNonFungibleESDT/DynamicSemiFungibleESDT)', required: false }) + @ApiQuery({ name: 'owner', description: 'Filter by collection owner', required: false }) + @ApiQuery({ name: 'canCreate', description: 'Filter by property canCreate (boolean)', required: false }) + @ApiQuery({ name: 'canBurn', description: 'Filter by property canBurn (boolean)', required: false }) + @ApiQuery({ name: 'canAddQuantity', description: 'Filter by property canAddQuantity (boolean)', required: false }) + @ApiQuery({ name: 'canUpdateAttributes', description: 'Filter by property canUpdateAttributes (boolean)', required: false }) + @ApiQuery({ name: 'canAddUri', description: 'Filter by property canAddUri (boolean)', required: false }) + @ApiQuery({ name: 'canTransferRole', description: 'Filter by property canTransferRole (boolean)', required: false }) + @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude collections of type "MetaESDT" in the response', required: false, type: Boolean }) + @ApiOkResponse({ type: [NftCollectionWithRoles] }) + async getAccountCollectionsWithRoles( + @Param('address', ParseAddressPipe) address: string, + @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, + @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, + @Query('search') search?: string, + @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], + @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], + @Query('owner', ParseAddressPipe) owner?: string, + @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, + new CollectionFilter({ + search, + type, + subType, + owner, + canCreate, + canBurn, + canAddQuantity, + canUpdateAttributes, + canAddUri, + canTransferRole, + excludeMetaESDT, + }), new QueryPagination({ from, size })); + } + + @Get("/accounts/:address/roles/collections/count") + @ApiOperation({ summary: 'Account collection count', description: 'Returns the total number of NFT/SFT/MetaESDT collections where the account is owner or has some special roles assigned to it' }) + @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) + @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) + @ApiQuery({ name: 'subType', description: 'Filter by type (NonFungibleESDTv2/DynamicNonFungibleESDT/DynamicSemiFungibleESDT)', required: false }) + @ApiQuery({ name: 'owner', description: 'Filter by collection owner', required: false }) + @ApiQuery({ name: 'canCreate', description: 'Filter by property canCreate (boolean)', required: false }) + @ApiQuery({ name: 'canBurn', description: 'Filter by property canCreate (boolean)', required: false }) + @ApiQuery({ name: 'canAddQuantity', description: 'Filter by property canAddQuantity (boolean)', required: false }) + @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude collections of type "MetaESDT" in the response', required: false, type: Boolean }) + @ApiOkResponse({ type: Number }) + async getCollectionWithRolesCount( + @Param('address', ParseAddressPipe) address: string, + @Query('search') search?: string, + @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], + @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], + @Query('owner', ParseAddressPipe) owner?: string, + @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 })); + } + + @Get("/accounts/:address/roles/collections/c") + @ApiExcludeEndpoint() + async getCollectionCountAlternative( + @Param('address', ParseAddressPipe) address: string, + @Query('search') search?: string, + @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], + @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], + @Query('owner', ParseAddressPipe) owner?: string, + @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, + })); + } + + @Get("/accounts/:address/roles/collections/:collection") + @ApiOperation({ summary: 'Account collection details', description: 'Returns details about a specific NFT/SFT/MetaESDT collection from a given address' }) + @ApiOkResponse({ type: NftCollectionWithRoles }) + async getAccountCollection( + @Param('address', ParseAddressPipe) address: string, + @Param('collection', ParseCollectionPipe) collection: string, + ): Promise { + const result = await this.collectionService.getCollectionForAddressWithRole(address, collection); + if (!result) { + throw new NotFoundException('Collection for given account not found'); + } + + return result; + } + + @Get("/accounts/:address/roles/tokens") + @ApiOperation({ summary: 'Account token roles', description: 'Returns fungible token roles where the account is owner or has some special roles assigned to it' }) + @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 }) + @ApiQuery({ name: 'search', description: 'Search by token identifier or name', required: false }) + @ApiQuery({ name: 'owner', description: 'Filter by token owner', required: false }) + @ApiQuery({ name: 'canMint', description: 'Filter by property canMint (boolean)', required: false }) + @ApiQuery({ name: 'canBurn', description: 'Filter by property canBurn (boolean)', required: false }) + @ApiQuery({ name: 'includeMetaESDT', description: 'Include MetaESDTs in response', required: false, type: Boolean }) + @ApiOkResponse({ type: [TokenWithRoles] }) + async getAccountTokensWithRoles( + @Param('address', ParseAddressPipe) address: string, + @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, + @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, + @Query('search') search?: string, + @Query('owner', ParseAddressPipe) owner?: string, + @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 })); + } + + @Get("/accounts/:address/roles/tokens/count") + @ApiOperation({ summary: 'Account token roles count', description: 'Returns the total number of fungible token roles where the account is owner or has some special roles assigned to it' }) + @ApiQuery({ name: 'search', description: 'Search by token identifier or name', required: false }) + @ApiQuery({ name: 'owner', description: 'Filter by token owner', required: false }) + @ApiQuery({ name: 'canMint', description: 'Filter by property canMint (boolean)', required: false }) + @ApiQuery({ name: 'canBurn', description: 'Filter by property canCreate (boolean)', required: false }) + @ApiQuery({ name: 'includeMetaESDT', description: 'Include MetaESDTs in response', required: false, type: Boolean }) + @ApiOkResponse({ type: Number }) + async getTokensWithRolesCount( + @Param('address', ParseAddressPipe) address: string, + @Query('search') search?: string, + @Query('owner', ParseAddressPipe) owner?: string, + @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 })); + } + + @Get("/accounts/:address/roles/tokens/c") + @ApiExcludeEndpoint() + async getTokensWithRolesCountAlternative( + @Param('address', ParseAddressPipe) address: string, + @Query('search') search?: string, + @Query('owner', ParseAddressPipe) owner?: string, + @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 })); + } + + @Get("/accounts/:address/roles/tokens/:identifier") + @ApiOperation({ summary: 'Account token roles details', description: 'Returns details about fungible token roles where the account is owner or has some special roles assigned to it' }) + @ApiOkResponse({ type: TokenWithRoles }) + async getTokenWithRoles( + @Param('address', ParseAddressPipe) address: string, + @Param('identifier', ParseTokenPipe) identifier: string, + ): Promise { + const result = await this.tokenService.getTokenWithRolesForAddress(address, identifier); + if (!result) { + throw new NotFoundException('Token with roles for given account not found'); + } + + return result; + } + + @Get("/accounts/:address/collections") + @ApiOperation({ summary: 'Account collections', description: 'Returns NFT/SFT/MetaESDT collections where the account owns one or more NFTs' }) + @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 }) + @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) + @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) + @ApiQuery({ name: 'subType', description: 'Filter by type (NonFungibleESDTv2/DynamicNonFungibleESDT/DynamicSemiFungibleESDT)', required: false }) + @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude collections of type "MetaESDT" in the response', required: false, type: Boolean }) + @ApiOkResponse({ type: [NftCollectionAccount] }) + async getAccountNftCollections( + @Param('address', ParseAddressPipe) address: string, + @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, + @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, + @Query('search') search?: string, + @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], + @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, + ): Promise { + return await this.collectionService.getCollectionsForAddress( + address, + new CollectionFilter({ search, type, subType, excludeMetaESDT }), + new QueryPagination({ from, size })); + } + + @Get("/accounts/:address/collections/count") + @ApiOperation({ summary: 'Account collection count', description: 'Returns the total number of NFT/SFT/MetaESDT collections where the account is owner or has some special roles assigned to it' }) + @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) + @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) + @ApiQuery({ name: 'subType', description: 'Filter by type (NonFungibleESDTv2/DynamicNonFungibleESDT/DynamicSemiFungibleESDT)', required: false }) + @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude collections of type "MetaESDT" in the response', required: false, type: Boolean }) + @ApiOkResponse({ type: Number }) + async getNftCollectionCount( + @Param('address', ParseAddressPipe) address: string, + @Query('search') search?: string, + @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], + @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, + ): Promise { + return await this.collectionService.getCollectionCountForAddress(address, new CollectionFilter({ search, type, subType, excludeMetaESDT })); + } + + @Get("/accounts/:address/collections/c") + @ApiExcludeEndpoint() + async getNftCollectionCountAlternative( + @Param('address', ParseAddressPipe) address: string, + @Query('search') search?: string, + @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], + @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, + ): Promise { + return await this.collectionService.getCollectionCountForAddress(address, new CollectionFilter({ search, type, subType, excludeMetaESDT })); + } + + @Get("/accounts/:address/collections/:collection") + @ApiOperation({ summary: 'Account collection details', description: 'Returns details about a specific NFT/SFT/MetaESDT collection from a given address' }) + @ApiOkResponse({ type: NftCollectionAccount }) + async getAccountNftCollection( + @Param('address', ParseAddressPipe) address: string, + @Param('collection', ParseCollectionPipe) collection: string, + ): Promise { + const result = await this.collectionService.getCollectionForAddress(address, collection); + if (!result) { + throw new NotFoundException('Collection for given account not found'); + } + + return result; + } + + @Get("/accounts/:address/nfts") + @UseInterceptors(DeepHistoryInterceptor) + @ApiOkResponse({ type: [NftAccount] }) + @ApiOperation({ summary: 'Account NFTs', description: 'Returns a list of all available NFTs/SFTs/MetaESDTs owned by the provided address' }) + @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 }) + @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) + @ApiQuery({ name: 'identifiers', description: 'Filter by identifiers, comma-separated', required: false }) + @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) + @ApiQuery({ name: 'subType', description: 'Filter by type (NonFungibleESDTv2/DynamicNonFungibleESDT/DynamicSemiFungibleESDT)', required: false }) + @ApiQuery({ name: 'collection', description: 'Get all tokens by token collection. Deprecated, replaced by collections parameter', required: false, deprecated: true }) + @ApiQuery({ name: 'collections', description: 'Get all tokens by token collections, comma-separated', required: false }) + @ApiQuery({ name: 'name', description: 'Get all nfts by name', required: false }) + @ApiQuery({ name: 'tags', description: 'Filter by one or more comma-separated tags', required: false }) + @ApiQuery({ name: 'creator', description: 'Return all NFTs associated with a given creator', required: false }) + @ApiQuery({ name: 'hasUris', description: 'Return all NFTs that have one or more uris', required: false }) + @ApiQuery({ name: 'includeFlagged', description: 'Include NFTs that are flagged or not', required: false }) + @ApiQuery({ name: 'withSupply', description: 'Return supply where type = SemiFungibleESDT', required: false }) + @ApiQuery({ name: 'source', description: 'Data source of request', required: false }) + @ApiQuery({ name: 'withScamInfo', description: 'Include scam info in the response', required: false, type: Boolean }) + @ApiQuery({ name: 'computeScamInfo', description: 'Compute scam info in the response', required: false, type: Boolean }) + @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude NFTs of type "MetaESDT" in the response', required: false, type: Boolean }) + @ApiQuery({ name: 'fields', description: 'List of fields to filter by', required: false, isArray: true, style: 'form', explode: false }) + @ApiQuery({ name: 'isScam', description: 'Filter by scam status', required: false, type: Boolean }) + @ApiQuery({ name: 'scamType', description: 'Filter by type (scam/potentialScam)', required: false }) + @ApiQuery({ name: 'timestamp', description: 'Retrieve entry from timestamp', required: false, type: Number }) + // @NoCache() + async getAccountNfts( + @Param('address', ParseAddressPipe) address: string, + @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, + @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, + @Query('search') search?: string, + @Query('identifiers', ParseNftArrayPipe) identifiers?: string[], + @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], + @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], + @Query('collection') collection?: string, + @Query('collections', ParseArrayPipe) collections?: string[], + @Query('name') name?: string, + @Query('tags', ParseArrayPipe) tags?: string[], + @Query('creator', ParseAddressPipe) creator?: string, + @Query('hasUris', ParseBoolPipe) hasUris?: boolean, + @Query('includeFlagged', ParseBoolPipe) includeFlagged?: boolean, + @Query('withSupply', ParseBoolPipe) withSupply?: boolean, + @Query('source', new ParseEnumPipe(EsdtDataSource)) source?: EsdtDataSource, + @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, + @Query('fields', ParseArrayPipe) fields?: string[], + @Query('isScam', ParseBoolPipe) isScam?: boolean, + @Query('scamType', new ParseEnumPipe(ScamType)) scamType?: ScamType, + @Query('timestamp', ParseIntPipe) _timestamp?: number, + ): Promise { + return await this.nftService.getNftsForAddressFromDb( + address, + new QueryPagination({ from, size }), + new NftFilter({ + search, + identifiers, + type, + subType, + collection, + name, + collections, + tags, + creator, + hasUris, + includeFlagged, + excludeMetaESDT, + isScam, + scamType, + }), + fields, + new NftQueryOptions({ withSupply }), + source + ); + } + + @Get("/accounts/:address/nfts/count") + @UseInterceptors(DeepHistoryInterceptor) + @ApiOperation({ summary: 'Account NFT/SFT tokens count', description: 'Returns the total number of NFT/SFT tokens from a given address, as well as the total number of a certain type of ESDT ' }) + @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) + @ApiQuery({ name: 'identifiers', description: 'Filter by identifiers, comma-separated', required: false }) + @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) + @ApiQuery({ name: 'subType', description: 'Filter by subType', required: false }) + @ApiQuery({ name: 'collection', description: 'Get all tokens by token collection', required: false }) + @ApiQuery({ name: 'collections', description: 'Get all tokens by token collections, comma-separated', required: false }) + @ApiQuery({ name: 'name', description: 'Get all nfts by name', required: false }) + @ApiQuery({ name: 'tags', description: 'Filter by one or more comma-separated tags', required: false }) + @ApiQuery({ name: 'creator', description: 'Return all NFTs associated with a given creator', required: false }) + @ApiQuery({ name: 'hasUris', description: 'Return all NFTs that have one or more uris', required: false }) + @ApiQuery({ name: 'includeFlagged', description: 'Include NFTs that are flagged or not', required: false }) + @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude NFTs of type "MetaESDT" in the response', required: false, type: Boolean }) + @ApiQuery({ name: 'isScam', description: 'Filter by scam status', required: false, type: Boolean }) + @ApiQuery({ name: 'scamType', description: 'Filter by type (scam/potentialScam)', required: false }) + @ApiQuery({ name: 'timestamp', description: 'Retrieve entry from timestamp', required: false, type: Number }) + @ApiOkResponse({ type: Number }) + // @NoCache() + async getNftCount( + @Param('address', ParseAddressPipe) address: string, + @Query('identifiers', ParseNftArrayPipe) identifiers?: string[], + @Query('search') search?: string, + @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], + @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], + @Query('collection') collection?: string, + @Query('collections', ParseArrayPipe) collections?: string[], + @Query('name') name?: string, + @Query('tags', ParseArrayPipe) tags?: string[], + @Query('creator', ParseAddressPipe) creator?: string, + @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 { + return await this.nftService.getNftCountForAddress(address, new NftFilter({ + search, + identifiers, + type, + subType, + collection, + collections, + name, + tags, + creator, + hasUris, + includeFlagged, + excludeMetaESDT, + isScam, + scamType, + })); + } + + @Get("/accounts/:address/nfts/c") + @UseInterceptors(DeepHistoryInterceptor) + @ApiExcludeEndpoint() + async getNftCountAlternative( + @Param('address', ParseAddressPipe) address: string, + @Query('search') search?: string, + @Query('identifiers', ParseNftArrayPipe) identifiers?: string[], + @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], + @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], + @Query('collection') collection?: string, + @Query('collections', ParseArrayPipe) collections?: string[], + @Query('name') name?: string, + @Query('tags', ParseArrayPipe) tags?: string[], + @Query('creator', ParseAddressPipe) creator?: string, + @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 { + return await this.nftService.getNftCountForAddress(address, new NftFilter({ search, identifiers, type, subType, collection, collections, name, tags, creator, hasUris, includeFlagged, excludeMetaESDT, isScam, scamType })); + } + + @Get("/accounts/:address/nfts/:nft") + @UseInterceptors(DeepHistoryInterceptor) + @ApiOperation({ summary: 'Account NFT/SFT token details', description: 'Returns details about a specific fungible token for a given address' }) + @ApiQuery({ name: 'fields', description: 'List of fields to filter by', required: false, isArray: true, style: 'form', explode: false }) + @ApiQuery({ name: 'extract', description: 'Extract a specific field', required: false }) + @ApiQuery({ name: 'timestamp', description: 'Retrieve entry from timestamp', required: false, type: Number }) + @ApiOkResponse({ type: NftAccount }) + // @NoCache() + async getAccountNft( + @Param('address', ParseAddressPipe) address: string, + @Param('nft', ParseNftPipe) nft: string, + @Query('fields', ParseArrayPipe) fields?: string[], + @Query('extract') extract?: string, + @Query('timestamp', ParseIntPipe) _timestamp?: number, + ): Promise { + const actualFields = extract ? [extract] : fields; + + const result = await this.nftService.getNftForAddressFromDb(address, nft, actualFields); + if (!result) { + throw new HttpException('Token for given account not found', HttpStatus.NOT_FOUND); + } + + return result; + } + + @Get('/accounts/:address/stake') + @UseInterceptors(DeepHistoryInterceptor) + @ApiOperation({ + summary: 'Account stake details', + description: + 'Summarizes total staked amount for the given provider, as well as when and how much unbond will be performed', + }) + @ApiQuery({ + name: 'timestamp', + description: 'Retrieve entry from timestamp', + required: false, + type: Number, + }) + @ApiOkResponse({ type: ProviderStake }) + async getAccountStake( + @Param('address', ParseAddressPipe) address: string, + @Query('timestamp', ParseIntPipe) _timestamp?: number, + ): Promise { + return await this.stakeService.getStakeForAddress(address); + } + + @Get("/accounts/:address/delegation") + @ApiOperation({ summary: 'Account delegations with staking providers', description: 'Summarizes all delegation positions with staking providers, together with unDelegation positions' }) + @ApiOkResponse({ type: AccountDelegation, isArray: true }) + async getDelegationForAddress(@Param('address', ParseAddressPipe) address: string): Promise { + return await this.delegationService.getDelegationForAddress(address); + } + + @Get("/accounts/:address/delegation-legacy") + @UseInterceptors(DeepHistoryInterceptor) + @ApiOperation({ summary: 'Account legacy delegation details', description: 'Returns staking information related to the legacy delegation pool' }) + @ApiOkResponse({ type: AccountDelegationLegacy }) + @ApiQuery({ name: 'timestamp', description: 'Retrieve entry from timestamp', required: false, type: Number }) + async getAccountDelegationLegacy( + @Param('address', ParseAddressPipe) address: string, + @Query('timestamp', ParseIntPipe) _timestamp?: number, + ): Promise { + return await this.delegationLegacyService.getDelegationForAddress(address); + } + + @Get("/accounts/:address/keys") + @ApiOperation({ summary: 'Account nodes', description: 'Returns all active / queued nodes where the account is owner' }) + @ApiOkResponse({ type: [AccountKey] }) + @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 }) + @ApiQuery({ name: 'status', description: 'Key status', required: false, enum: NodeStatusRaw }) + async getAccountKeys( + @Param('address', ParseAddressPipe) address: string, + @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, + @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, + @Query('status', new ParseEnumArrayPipe(NodeStatusRaw)) status?: NodeStatusRaw[], + ): Promise { + return await this.accountService.getKeys( + address, + new AccountKeyFilter({ status }), + new QueryPagination({ from, size })); + } + + @Get("/accounts/:address/waiting-list") + @ApiOperation({ summary: 'Account queued nodes', description: 'Returns all nodes in the node queue where the account is owner' }) + @ApiOkResponse({ type: [WaitingList] }) + async getAccountWaitingList(@Param('address', ParseAddressPipe) address: string): Promise { + return await this.waitingListService.getWaitingListForAddress(address); + } + + @Get("/accounts/:address/transactions") + @ApiOperation({ summary: 'Account transaction list', description: 'Returns details of all transactions where the account is sender or receiver' }) + @ApplyComplexity({ target: TransactionDetailed }) + @ApiOkResponse({ type: [Transaction] }) + @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 }) + @ApiQuery({ name: 'sender', description: 'Address of the transaction sender', required: false }) + @ApiQuery({ name: 'receiver', description: 'Search by multiple receiver addresses, comma-separated', required: false }) + @ApiQuery({ name: 'token', description: 'Identifier of the token', required: false }) + @ApiQuery({ name: 'senderShard', description: 'Id of the shard the sender address belongs to', required: false }) + @ApiQuery({ name: 'receiverShard', description: 'Id of the shard the receiver address belongs to', required: false }) + @ApiQuery({ name: 'miniBlockHash', description: 'Filter by miniblock hash', required: false }) + @ApiQuery({ name: 'hashes', description: 'Filter by a comma-separated list of transaction hashes', required: false }) + @ApiQuery({ name: 'status', description: 'Status of the transaction (success / pending / invalid / fail)', required: false, enum: TransactionStatus }) + @ApiQuery({ name: 'function', description: 'Filter transactions by function name', required: false }) + @ApiQuery({ name: 'order', description: 'Sort order (asc/desc)', required: false, enum: SortOrder }) + @ApiQuery({ name: 'fields', description: 'List of fields to filter by', required: false, isArray: true, style: 'form', explode: false }) + @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) + @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiQuery({ name: 'round', description: 'Round number', required: false }) + @ApiQuery({ name: 'withScResults', description: 'Return scResults for transactions. When "withScresults" parameter is applied, complexity estimation is 200', required: false }) + @ApiQuery({ name: 'withOperations', description: 'Return operations for transactions. When "withOperations" parameter is applied, complexity estimation is 200', required: false }) + @ApiQuery({ name: 'withLogs', description: 'Return logs for transactions. When "withLogs" parameter is applied, complexity estimation is 200', required: false }) + @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: 'withBlockInfo', description: 'Returns sender / receiver block details', required: false, type: Boolean }) + @ApiQuery({ name: 'computeScamInfo', required: false, type: Boolean }) + @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: 'isScCall', description: 'Returns sc call 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, + @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, + @Query('sender', ParseAddressPipe) sender?: string, + @Query('receiver', ParseAddressArrayPipe) receiver?: string[], + @Query('token') token?: string, + @Query('senderShard', ParseIntPipe) senderShard?: number, + @Query('receiverShard', ParseIntPipe) receiverShard?: number, + @Query('miniBlockHash', ParseBlockHashPipe) miniBlockHash?: string, + @Query('hashes', ParseArrayPipe) hashes?: string[], + @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, + @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('round', ParseIntPipe) round?: number, + @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, + @Query('fields', ParseArrayPipe) fields?: string[], + @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', ParseBoolPipe) isRelayed?: boolean, + @Query('isScCall', ParseBoolPipe) isScCall?: boolean, + @Query('withActionTransferValue', ParseBoolPipe) withActionTransferValue?: boolean, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, + ) { + const options = TransactionQueryOptions.applyDefaultOptions(size, { withScResults, withOperations, withLogs, withScamInfo, withUsername, withBlockInfo, withActionTransferValue }); + + const transactionFilter = new TransactionFilter({ + sender, + receivers: receiver, + token, + functions, + senderShard, + receiverShard, + miniBlockHash, + hashes, + status, + before, + after, + order, + senderOrReceiver, + isRelayed, + isScCall, + round, + withRelayedScresults, + }); + TransactionFilter.validate(transactionFilter, size); + return await this.transactionService.getTransactions(transactionFilter, new QueryPagination({ from, size }), options, address, fields); + } + + @Get("/accounts/:address/transactions/count") + @ApiOperation({ summary: 'Account transactions count', description: 'Returns total number of transactions for a given address where the account is sender or receiver, as well as total transactions count that have a certain status' }) + @ApiOkResponse({ type: Number }) + @ApiQuery({ name: 'sender', description: 'Address of the transaction sender', required: false }) + @ApiQuery({ name: 'receiver', description: 'Search by multiple receiver addresses, comma-separated', required: false }) + @ApiQuery({ name: 'token', description: 'Identifier of the token', required: false }) + @ApiQuery({ name: 'senderShard', description: 'Id of the shard the sender address belongs to', required: false }) + @ApiQuery({ name: 'receiverShard', description: 'Id of the shard the receiver address belongs to', required: false }) + @ApiQuery({ name: 'miniBlockHash', description: 'Filter by miniblock hash', required: false }) + @ApiQuery({ name: 'hashes', description: 'Filter by a comma-separated list of transaction hashes', required: false }) + @ApiQuery({ name: 'status', description: 'Status of the transaction (success / pending / invalid / fail)', required: false, enum: TransactionStatus }) + @ApiQuery({ name: 'function', description: 'Filter transactions by function name', required: false }) + @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) + @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: 'isRelayed', description: 'Returns isRelayed transactions details', required: false, type: Boolean }) + @ApiQuery({ name: 'isScCall', description: 'Returns sc call 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, + @Query('receiver', ParseAddressArrayPipe) receiver?: string[], + @Query('token') token?: string, + @Query('senderShard', ParseIntPipe) senderShard?: number, + @Query('receiverShard', ParseIntPipe) receiverShard?: number, + @Query('miniBlockHash', ParseBlockHashPipe) miniBlockHash?: string, + @Query('hashes', ParseArrayPipe) hashes?: string[], + @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, + @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('round', ParseIntPipe) round?: number, + @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, + @Query('isRelayed', ParseBoolPipe) isRelayed?: boolean, + @Query('isScCall', ParseBoolPipe) isScCall?: boolean, + @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, + + ): Promise { + + return await this.transactionService.getTransactionCount(new TransactionFilter({ + sender, + receivers: receiver, + token, + functions, + senderShard, + receiverShard, + miniBlockHash, + hashes, + status, + before, + after, + senderOrReceiver, + isRelayed, + isScCall, + round, + withRelayedScresults, + }), address); + } + + @Get("/accounts/:address/transfers") + @ApiOperation({ summary: 'Account value transfers', description: 'Returns both transfers triggerred by a user account (type = Transaction), as well as transfers triggerred by smart contracts (type = SmartContractResult), thus providing a full picture of all in/out value transfers for a given account' }) + @ApiOkResponse({ type: [Transaction] }) + @ApplyComplexity({ target: TransactionDetailed }) + @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 }) + @ApiQuery({ name: 'sender', description: 'Address of the transfer sender', required: false }) + @ApiQuery({ name: 'receiver', description: 'Search by multiple receiver addresses, comma-separated', required: false }) + @ApiQuery({ name: 'token', description: 'Identifier of the token', required: false }) + @ApiQuery({ name: 'senderShard', description: 'Id of the shard the sender address belongs to', required: false }) + @ApiQuery({ name: 'receiverShard', description: 'Id of the shard the receiver address belongs to', required: false }) + @ApiQuery({ name: 'miniBlockHash', description: 'Filter by miniblock hash', required: false }) + @ApiQuery({ name: 'hashes', description: 'Filter by a comma-separated list of transfer hashes', required: false }) + @ApiQuery({ name: 'status', description: 'Status of the transaction (success / pending / invalid / fail)', required: false, enum: TransactionStatus }) + @ApiQuery({ name: 'function', description: 'Filter transactions by function name', required: false }) + @ApiQuery({ name: 'order', description: 'Sort order (asc/desc)', required: false, enum: SortOrder }) + @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) + @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiQuery({ name: 'round', description: 'Round number', required: false }) + @ApiQuery({ name: 'fields', description: 'List of fields to filter by', required: false, isArray: true, style: 'form', explode: false }) + @ApiQuery({ name: 'relayer', description: 'Address of the relayer', required: false }) + @ApiQuery({ name: 'isScCall', description: 'Returns sc call transactions details', 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: 'withBlockInfo', description: 'Returns sender / receiver block details', required: false, type: Boolean }) + @ApiQuery({ name: 'senderOrReceiver', description: 'One address that current address interacted with', required: false }) + @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 }) + @ApiQuery({ name: 'withTxsRelayedByAddress', description: 'Include transactions that were relayed by the address', required: false }) + async getAccountTransfers( + @Param('address', ParseAddressPipe) address: string, + @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, + @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, + @Query('sender', ParseAddressArrayPipe) sender?: string[], + @Query('receiver', ParseAddressArrayPipe) receiver?: string[], + @Query('token') token?: string, + @Query('senderShard', ParseIntPipe) senderShard?: number, + @Query('receiverShard', ParseIntPipe) receiverShard?: number, + @Query('miniBlockHash', ParseBlockHashPipe) miniBlockHash?: string, + @Query('hashes', ParseArrayPipe) hashes?: string[], + @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, + @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('round', ParseIntPipe) round?: number, + @Query('fields', ParseArrayPipe) fields?: string[], + @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, + @Query('relayer', ParseAddressPipe) relayer?: string, + @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, + @Query('withUsername', ParseBoolPipe) withUsername?: boolean, + @Query('withBlockInfo', ParseBoolPipe) withBlockInfo?: boolean, + @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, + @Query('isScCall', ParseBoolPipe) isScCall?: boolean, + @Query('withLogs', ParseBoolPipe) withLogs?: boolean, + @Query('withOperations', ParseBoolPipe) withOperations?: boolean, + @Query('withActionTransferValue', ParseBoolPipe) withActionTransferValue?: boolean, + @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, + @Query('withTxsRelayedByAddress', ParseBoolPipe) withTxsRelayedByAddress?: boolean, + ): Promise { + const options = TransactionQueryOptions.applyDefaultOptions( + size, { withScamInfo, withUsername, withBlockInfo, withOperations, withLogs, withActionTransferValue }); + + return await this.transferService.getTransfers(new TransactionFilter({ + address, + senders: sender, + receivers: receiver, + token, + functions, + senderShard, + receiverShard, + miniBlockHash, + hashes, + status, + before, + after, + order, + senderOrReceiver, + relayer, + round, + withRefunds, + withTxsRelayedByAddress, + isScCall, + }), + new QueryPagination({ from, size }), + options, + fields + ); + } + + @Get("/accounts/:address/transfers/count") + @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 }) + @ApiQuery({ name: 'token', description: 'Identifier of the token', required: false }) + @ApiQuery({ name: 'senderShard', description: 'Id of the shard the sender address belongs to', required: false }) + @ApiQuery({ name: 'receiverShard', description: 'Id of the shard the receiver address belongs to', required: false }) + @ApiQuery({ name: 'miniBlockHash', description: 'Filter by miniblock hash', required: false }) + @ApiQuery({ name: 'hashes', description: 'Filter by a comma-separated list of transfer hashes', required: false }) + @ApiQuery({ name: 'status', description: 'Status of the transaction (success / pending / invalid / fail)', required: false, enum: TransactionStatus }) + @ApiQuery({ name: 'function', description: 'Filter transfers by function name', required: false }) + @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) + @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiQuery({ name: 'round', description: 'Round number', required: false }) + @ApiQuery({ name: 'isScCall', description: 'Returns sc call transactions details', required: false, type: Boolean }) + @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[], + @Query('receiver', ParseAddressArrayPipe) receiver?: string[], + @Query('token') token?: string, + @Query('senderShard', ParseIntPipe) senderShard?: number, + @Query('receiverShard', ParseIntPipe) receiverShard?: number, + @Query('miniBlockHash', ParseBlockHashPipe) miniBlockHash?: string, + @Query('hashes', ParseArrayPipe) hashes?: string[], + @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, + @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('round', ParseIntPipe) round?: number, + @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, + @Query('isScCall', ParseBoolPipe) isScCall?: boolean, + @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, + ): Promise { + return await this.transferService.getTransfersCount(new TransactionFilter({ + address, + senders: sender, + receivers: receiver, + token, + functions, + senderShard, + receiverShard, + miniBlockHash, + hashes, + status, + before, + after, + senderOrReceiver, + round, + isScCall, + withRefunds, + })); + } + + @Get("/accounts/:address/transfers/c") + @ApiExcludeEndpoint() + async getAccountTransfersCountAlternative( + @Param('address', ParseAddressPipe) address: string, + @Query('sender', ParseAddressArrayPipe) sender?: string[], + @Query('receiver', ParseAddressArrayPipe) receiver?: string[], + @Query('token') token?: string, + @Query('senderShard', ParseIntPipe) senderShard?: number, + @Query('receiverShard', ParseIntPipe) receiverShard?: number, + @Query('miniBlockHash', ParseBlockHashPipe) miniBlockHash?: string, + @Query('hashes', ParseArrayPipe) hashes?: string[], + @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, + @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('round', ParseIntPipe) round?: number, + @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, + @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, + @Query('isScCall', ParseBoolPipe) isScCall?: boolean, + ): Promise { + return await this.transferService.getTransfersCount(new TransactionFilter({ + address, + senders: sender, + receivers: receiver, + token, + functions, + senderShard, + receiverShard, + miniBlockHash, + hashes, + status, + before, + after, + senderOrReceiver, + round, + withRefunds, + isScCall, + })); + } + + @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 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] }) + getAccountContracts( + @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.getAccountContracts(new QueryPagination({ from, size }), address); + } + + @Get("/accounts/:address/contracts/count") + @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); + } + + @Get("/accounts/:address/contracts/c") + @ApiExcludeEndpoint() + getAccountContractsCountAlternative(@Param('address', ParseAddressPipe) address: string): Promise { + return this.accountService.getAccountContractsCount(address); + } + + @Get("/accounts/:address/upgrades") + @ApiOperation({ summary: 'Account upgrades details', description: 'Returns all upgrades details for a specific contract address' }) + @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: ContractUpgrades }) + getContractUpgrades( + @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.getContractUpgrades(new QueryPagination({ from, size }), address); + } + + @Get("/accounts/:address/results") + @ApiOperation({ summary: 'Account smart contract results', description: 'Returns smart contract results where the account is sender or receiver' }) + @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: [SmartContractResult] }) + getAccountScResults( + @Param('address', ParseAddressPipe) address: string, + @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, + @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, + ): Promise { + return this.scResultService.getAccountScResults(address, new QueryPagination({ from, size })); + } + + @Get("/accounts/:address/results/count") + @ApiOperation({ summary: 'Account smart contracts results count', description: 'Returns number of smart contract results where the account is sender or receiver' }) + @ApiOkResponse({ type: Number }) + getAccountScResultsCount( + @Param('address', ParseAddressPipe) address: string, + ): Promise { + return this.scResultService.getAccountScResultsCount(address); + } + + @Get("/accounts/:address/results/:scHash") + @ApiOperation({ summary: 'Account smart contract result', description: 'Returns details of a smart contract result where the account is sender or receiver' }) + @ApiOkResponse({ type: SmartContractResult }) + async getAccountScResult( + @Param('address', ParseAddressPipe) address: string, + @Param('scHash', ParseTransactionHashPipe) scHash: string, + ): Promise { + const scResult = await this.scResultService.getScResult(scHash); + if (!scResult || (scResult.sender !== address && scResult.receiver !== address)) { + throw new NotFoundException('Smart contract result not found'); + } + + return scResult; + } + + @Get("/accounts/:address/history") + @ApiOperation({ summary: 'Account history', description: 'Return account EGLD balance history' }) + @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 }) + @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) + @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiOkResponse({ type: [AccountHistory] }) + getAccountHistory( + @Param('address', ParseAddressPipe) address: string, + @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 { + return this.accountService.getAccountHistory( + address, + new QueryPagination({ from, size }), + new AccountHistoryFilter({ before, after })); + } + + @Get("/accounts/:address/history/count") + @ApiOperation({ summary: 'Account history count', description: 'Return account EGLD balance history count' }) + @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) + @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiOkResponse({ type: Number }) + getAccountHistoryCount( + @Param('address', ParseAddressPipe) address: string, + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + ): Promise { + return this.accountService.getAccountHistoryCount( + address, + new AccountHistoryFilter({ before, after })); + } + + @Get("/accounts/:address/history/:tokenIdentifier/count") + @ApiOperation({ summary: 'Account token history count', description: 'Return account token balance history count' }) + @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) + @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiOkResponse({ type: Number }) + async getAccountTokenHistoryCount( + @Param('address', ParseAddressPipe) address: string, + @Param('tokenIdentifier', ParseTokenOrNftPipe) tokenIdentifier: string, + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + ): Promise { + const isToken = await this.tokenService.isToken(tokenIdentifier) || await this.collectionService.isCollection(tokenIdentifier) || await this.nftService.isNft(tokenIdentifier); + if (!isToken) { + throw new NotFoundException(`Token '${tokenIdentifier}' not found`); + } + return this.accountService.getAccountTokenHistoryCount( + address, + tokenIdentifier, + new AccountHistoryFilter({ before, after })); + } + + @Get("/accounts/:address/esdthistory") + @ApiOperation({ summary: 'Account esdts history', description: 'Returns account esdts balance history' }) + @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 }) + @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) + @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiQuery({ name: 'identifier', description: 'Filter by multiple esdt identifiers, comma-separated', required: false }) + @ApiQuery({ name: 'token', description: 'Token identifier', required: false }) + @ApiOkResponse({ type: [AccountEsdtHistory] }) + async getAccountEsdtHistory( + @Param('address', ParseAddressPipe) address: string, + @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, + @Query('identifier', ParseArrayPipe) identifier?: string[], + @Query('token', ParseTokenPipe) token?: string, + ): Promise { + return await this.accountService.getAccountEsdtHistory( + address, + new QueryPagination({ from, size }), + new AccountHistoryFilter({ before, after, identifiers: identifier, token })); + } + + @Get("/accounts/:address/esdthistory/count") + @ApiOperation({ summary: 'Account esdts history count', description: 'Returns account esdts balance history count' }) + @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) + @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiQuery({ name: 'identifier', description: 'Filter by multiple esdt identifiers, comma-separated', required: false }) + @ApiQuery({ name: 'token', description: 'Token identifier', required: false }) + async getAccountEsdtHistoryCount( + @Param('address', ParseAddressPipe) address: string, + @Query('before', ParseIntPipe) before?: number, + @Query('after', ParseIntPipe) after?: number, + @Query('identifier', ParseArrayPipe) identifier?: string[], + @Query('token', ParseTokenPipe) token?: string, + ): Promise { + return await this.accountService.getAccountEsdtHistoryCount( + address, + new AccountHistoryFilter({ before, after, identifiers: identifier, token })); + } + + @Get("/accounts/:address/history/:tokenIdentifier") + @ApiOperation({ summary: 'Account token history', description: 'Returns account token balance history' }) + @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 }) + @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) + @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) + @ApiOkResponse({ type: [AccountEsdtHistory] }) + async getAccountTokenHistory( + @Param('address', ParseAddressPipe) address: string, + @Param('tokenIdentifier', ParseTokenOrNftPipe) tokenIdentifier: string, + @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 { + const isToken = await this.tokenService.isToken(tokenIdentifier) || await this.collectionService.isCollection(tokenIdentifier) || await this.nftService.isNft(tokenIdentifier); + if (!isToken) { + throw new NotFoundException(`Token '${tokenIdentifier}' not found`); + } + + return await this.accountService.getAccountTokenHistory( + address, tokenIdentifier, + new QueryPagination({ from, size }), + new AccountHistoryFilter({ before, after })); + } +} diff --git a/src/endpoints/accounts-v2/account.module.v2.ts b/src/endpoints/accounts-v2/account.module.v2.ts new file mode 100644 index 000000000..4fd5545fe --- /dev/null +++ b/src/endpoints/accounts-v2/account.module.v2.ts @@ -0,0 +1,46 @@ +import { forwardRef, Module } from "@nestjs/common"; +import { AssetsModule } from "src/common/assets/assets.module"; +import { PluginModule } from "src/plugins/plugin.module"; +import { CollectionModule } from "../collections/collection.module"; +import { DelegationLegacyModule } from "../delegation.legacy/delegation.legacy.module"; +import { NftModule } from "../nfts/nft.module"; +import { SmartContractResultModule } from "../sc-results/scresult.module"; +import { StakeModule } from "../stake/stake.module"; +import { TokenModule } from "../tokens/token.module"; +import { TransactionModule } from "../transactions/transaction.module"; +import { TransferModule } from "../transfers/transfer.module"; +import { UsernameModule } from "../usernames/username.module"; +import { VmQueryModule } from "../vm.query/vm.query.module"; +import { WaitingListModule } from "../waiting-list/waiting.list.module"; +import { AccountServiceV2 } from "./account.service.v2"; +import { ProviderModule } from "../providers/provider.module"; +import { KeysModule } from "../keys/keys.module"; +import { MongoDbModule } from "src/common/indexer/db"; + +@Module({ + imports: [ + VmQueryModule, + forwardRef(() => NftModule), + DelegationLegacyModule, + WaitingListModule, + forwardRef(() => StakeModule), + forwardRef(() => TransactionModule), + forwardRef(() => SmartContractResultModule), + forwardRef(() => CollectionModule), + forwardRef(() => PluginModule), + forwardRef(() => TransferModule), + forwardRef(() => TokenModule), + forwardRef(() => AssetsModule), + forwardRef(() => ProviderModule), + UsernameModule, + forwardRef(() => KeysModule), + MongoDbModule, + ], + providers: [ + AccountServiceV2, + ], + exports: [ + AccountServiceV2, + ], +}) +export class AccountModuleV2 { } diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts new file mode 100644 index 000000000..a319cdb68 --- /dev/null +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -0,0 +1,773 @@ +import { forwardRef, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { AccountDetailed } from './entities/account.detailed'; +import { Account } from './entities/account'; +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'; +import { QueryPagination } from 'src/common/entities/query.pagination'; +import { AccountKey } from './entities/account.key'; +import { DeployedContract } from './entities/deployed.contract'; +import { PluginService } from 'src/common/plugins/plugin.service'; +import { AccountEsdtHistory } from "./entities/account.esdt.history"; +import { AccountHistory } from "./entities/account.history"; +import { StakeService } from '../stake/stake.service'; +import { TransferService } from '../transfers/transfer.service'; +import { TransactionType } from '../transactions/entities/transaction.type'; +import { AssetsService } from 'src/common/assets/assets.service'; +import { TransactionFilter } from '../transactions/entities/transaction.filter'; +import { CacheService } from "@multiversx/sdk-nestjs-cache"; +import { AddressUtils, BinaryUtils, OriginLogger } from '@multiversx/sdk-nestjs-common'; +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 { AccountAssets } from 'src/common/assets/entities/account.assets'; +import { CacheInfo } from 'src/utils/cache.info'; +import { UsernameService } from '../usernames/username.service'; +import { ContractUpgrades } from './entities/contract.upgrades'; +import { AccountVerification } from './entities/account.verification'; +import { AccountQueryOptions } from './entities/account.query.options'; +import { AccountHistoryFilter } from './entities/account.history.filter'; +import { ProtocolService } from 'src/common/protocol/protocol.service'; +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 { 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'; +import { AccountDetailsRepository } from 'src/common/indexer/db'; +import { isDbValid } from 'src/state-changes/utils/state-changes.utils'; + +@Injectable() +export class AccountServiceV2 { + private readonly logger = new OriginLogger(AccountServiceV2.name); + + constructor( + private readonly indexerService: IndexerService, + private readonly gatewayService: GatewayService, + private readonly cachingService: CacheService, + private readonly vmQueryService: VmQueryService, + private readonly apiConfigService: ApiConfigService, + @Inject(forwardRef(() => PluginService)) + private readonly pluginService: PluginService, + @Inject(forwardRef(() => StakeService)) + private readonly stakeService: StakeService, + @Inject(forwardRef(() => TransferService)) + private readonly transferService: TransferService, + private readonly assetsService: AssetsService, + private readonly usernameService: UsernameService, + private readonly apiService: ApiService, + private readonly protocolService: ProtocolService, + @Inject(forwardRef(() => ProviderService)) + private readonly providerService: ProviderService, + private readonly keysService: KeysService, + private readonly accountDetailsDepository: AccountDetailsRepository, + ) { } + + async getAccountsCount(filter: AccountQueryOptions): Promise { + if (!filter.isSet()) { + return await this.cachingService.getOrSet( + CacheInfo.AccountsCount.key, + async () => await this.indexerService.getAccountsCount(filter), + CacheInfo.AccountsCount.ttl + ); + } + + return await this.indexerService.getAccountsCount(filter); + } + + async getAccount(address: string, options?: AccountFetchOptions): Promise { + if (!AddressUtils.isAddressValid(address)) { + return null; + } + + const account = await this.getAccountRaw(address, options?.withAssets); + if (!account) { + return null; + } + + if (options?.withTxCount === true) { + account.txCount = await this.getAccountTxCount(address); + } + + if (options?.withScrCount === true) { + account.scrCount = await this.getAccountScResults(address); + } + + if (options?.withGuardianInfo === true) { + await this.applyGuardianInfo(account); + } + + if (options?.withTimestamp) { + const elasticSearchAccount = await this.indexerService.getAccount(address); + account.timestamp = elasticSearchAccount.timestamp; + } + + if (AddressUtils.isSmartContractAddress(address)) { + const provider: Provider | undefined = await this.providerService.getProvider(address); + if (provider && provider.owner) { + account.ownerAddress = provider.owner; + } + } + + return account; + } + + async getAccountFromDb(address: string, options?: AccountFetchOptions): Promise { + if (!AddressUtils.isAddressValid(address)) { + return null; + } + + try { + const isDbUpToDate: boolean = await isDbValid(this.cachingService); + if (isDbUpToDate === true) { + // First try to get account from MongoDB + const accountFromDb = await this.accountDetailsDepository.getAccount(address); + + if (accountFromDb) { + // console.log('Account found in DB:', accountFromDb); + return accountFromDb; + } + } + // If not found in DB, call getAccount with the same parameters + return await this.getAccount(address, options); + } catch (error) { + this.logger.error(`Error when getting account from DB for address '${address}'`); + this.logger.error(error); + return null; + } + } + + async applyGuardianInfo(account: AccountDetailed): Promise { + try { + const guardianResult = await this.gatewayService.getGuardianData(account.address); + const guardianData = guardianResult?.guardianData; + if (guardianData) { + const activeGuardian = guardianData.activeGuardian; + if (activeGuardian) { + account.activeGuardianActivationEpoch = activeGuardian.activationEpoch; + account.activeGuardianAddress = activeGuardian.address; + account.activeGuardianServiceUid = activeGuardian.serviceUID; + } + + const pendingGuardian = guardianData.pendingGuardian; + if (pendingGuardian) { + account.pendingGuardianActivationEpoch = pendingGuardian.activationEpoch; + account.pendingGuardianAddress = pendingGuardian.address; + account.pendingGuardianServiceUid = pendingGuardian.serviceUID; + } + + account.isGuarded = guardianData.guarded; + } + } catch (error) { + this.logger.error(`Error when getting guardian data for address '${account.address}'`); + this.logger.error(error); + } + } + + async getAccountVerification(address: string): Promise { + if (!AddressUtils.isAddressValid(address)) { + return null; + } + + const verificationResponse = await this.apiService.get(`${this.apiConfigService.getVerifierUrl()}/verifier/${address}`); + return verificationResponse.data; + } + + async getVerifiedAccounts(): Promise { + const verificationResponse = await this.apiService.get(`${this.apiConfigService.getVerifierUrl()}/verifier`); + return verificationResponse.data; + } + + async getAccountSimple(address: string): Promise { + if (!AddressUtils.isAddressValid(address)) { + return null; + } + + return await this.getAccountRaw(address); + } + + async getAccountRaw(address: string, withAssets?: boolean): Promise { + try { + const { + account: { nonce, balance, code, codeHash, rootHash, developerReward, ownerAddress, codeMetadata }, + } = await this.gatewayService.getAddressDetails(address); + + const shardCount = await this.protocolService.getShardCount(); + const shard = AddressUtils.computeShard(AddressUtils.bech32Decode(address), shardCount); + 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) { + account = { ...account, ...codeAttributes }; + } + + if (AddressUtils.isSmartContractAddress(address) && account.code) { + const deployTxHash = await this.getAccountDeployedTxHash(address); + if (deployTxHash) { + account.deployTxHash = deployTxHash; + } + + const deployedAt = await this.getAccountDeployedAt(address); + if (deployedAt) { + account.deployedAt = deployedAt; + } + + const isVerified = await this.getAccountIsVerified(address, account.codeHash); + if (isVerified) { + account.isVerified = isVerified; + } + } + + if (!AddressUtils.isSmartContractAddress(address)) { + account.username = await this.usernameService.getUsernameForAddress(address) ?? undefined; + account.isPayableBySmartContract = undefined; + account.isUpgradeable = undefined; + account.isReadable = undefined; + account.isPayable = undefined; + } + + await this.pluginService.processAccount(account); + return account; + } catch (error) { + this.logger.error(error); + this.logger.error(`Error when getting account details for address '${address}'`); + return null; + } + } + + async getAccountTxCount(address: string): Promise { + return await this.transferService.getTransfersCount(new TransactionFilter({ address, type: TransactionType.Transaction })); + } + + async getAccountScResults(address: string): Promise { + return await this.transferService.getTransfersCount(new TransactionFilter({ address, type: TransactionType.SmartContractResult })); + } + + async getAccountDeployedAt(address: string): Promise { + return await this.cachingService.getOrSet( + CacheInfo.AccountDeployedAt(address).key, + async () => await this.getAccountDeployedAtRaw(address), + CacheInfo.AccountDeployedAt(address).ttl + ); + } + + async getAccountDeployedAtRaw(address: string): Promise { + const scDeploy = await this.indexerService.getScDeploy(address); + if (!scDeploy) { + return null; + } + + const txHash = scDeploy.deployTxHash; + if (!txHash) { + return null; + } + + const transaction = await this.indexerService.getTransaction(txHash); + if (!transaction) { + return null; + } + + return transaction.timestamp; + } + + async getAccountDeployedTxHash(address: string): Promise { + return await this.cachingService.getOrSet( + CacheInfo.AccountDeployTxHash(address).key, + async () => await this.getAccountDeployedTxHashRaw(address), + CacheInfo.AccountDeployTxHash(address).ttl, + ); + } + + async getAccountDeployedTxHashRaw(address: string): Promise { + const scDeploy = await this.indexerService.getScDeploy(address); + if (!scDeploy) { + return null; + } + + return scDeploy.deployTxHash; + } + + async getAccountIsVerified(address: string, codeHash: string): Promise { + return await this.cachingService.getOrSet( + CacheInfo.AccountIsVerified(address).key, + async () => await this.getAccountIsVerifiedRaw(address, codeHash), + CacheInfo.AccountIsVerified(address).ttl + ); + } + + async getAccountIsVerifiedRaw(address: string, codeHash: string): Promise { + try { + // eslint-disable-next-line require-await + const { data } = await this.apiService.get(`${this.apiConfigService.getVerifierUrl()}/verifier/${address}/codehash`, undefined, async (error) => error.response?.status === HttpStatus.NOT_FOUND); + + if (data.codeHash === Buffer.from(codeHash, 'base64').toString('hex')) { + return true; + } + } catch { + // ignore + } + + return null; + } + + async getAccounts(queryPagination: QueryPagination, filter: AccountQueryOptions): Promise { + if (!filter.isSet()) { + return await this.cachingService.getOrSet( + CacheInfo.Accounts(queryPagination).key, + async () => await this.getAccountsRaw(queryPagination, filter), + CacheInfo.Accounts(queryPagination).ttl + ); + } + + return await this.getAccountsRaw(queryPagination, filter); + } + + public async getAccountsForAddresses(addresses: Array): Promise> { + const assets: { [key: string]: AccountAssets; } = await this.assetsService.getAllAccountAssets(); + + const accountsRaw = await this.indexerService.getAccountsForAddresses(addresses); + const accounts: Array = accountsRaw.map(account => ApiUtils.mergeObjects(new Account(), account)); + const shardCount = await this.protocolService.getShardCount(); + + for (const account of accounts) { + account.shard = AddressUtils.computeShard(AddressUtils.bech32Decode(account.address), shardCount); + account.assets = assets[account.address]; + } + + return accounts; + } + + async getAccountsRaw(queryPagination: QueryPagination, options: AccountQueryOptions): Promise { + const result = await this.indexerService.getAccounts(queryPagination, options); + const assets = await this.assetsService.getAllAccountAssets(); + const accounts: Account[] = result.map(item => { + const account = ApiUtils.mergeObjects(new Account(), item); + account.ownerAddress = item.currentOwner; + account.transfersLast24h = item.api_transfersLast24h; + + return account; + }); + + const shardCount = await this.protocolService.getShardCount(); + + 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]; + + if (options.withDeployInfo && AddressUtils.isSmartContractAddress(account.address)) { + const [deployedAt, deployTxHash] = await Promise.all([ + this.getAccountDeployedAt(account.address), + this.getAccountDeployedTxHash(account.address), + ]); + + account.deployedAt = deployedAt; + account.deployTxHash = deployTxHash; + } + + if (options.withTxCount) { + account.txCount = await this.getAccountTxCount(account.address); + } + + if (options.withScrCount) { + account.scrCount = await this.getAccountScResults(account.address); + } + + if (options.withOwnerAssets && account.ownerAddress) { + account.ownerAssets = assets[account.ownerAddress]; + } + + if (verifiedAccounts && verifiedAccounts.includes(account.address)) { + account.isVerified = true; + } + } + + return accounts; + } + + async getDeferredAccount(address: string): Promise { + const publicKey = AddressUtils.bech32Decode(address); + const delegationContractAddress = this.apiConfigService.getDelegationContractAddress(); + if (!delegationContractAddress) { + return []; + } + + const delegationContractShardId = AddressUtils.computeShard(AddressUtils.bech32Decode(delegationContractAddress), await this.protocolService.getShardCount()); + + const [ + encodedUserDeferredPaymentList, + [encodedNumBlocksBeforeUnBond], + { + erd_nonce, + }, + ] = await Promise.all([ + this.vmQueryService.vmQuery( + delegationContractAddress, + 'getUserDeferredPaymentList', + undefined, + [publicKey] + ), + this.vmQueryService.vmQuery( + delegationContractAddress, + 'getNumBlocksBeforeUnBond', + ), + this.gatewayService.getNetworkStatus(`${delegationContractShardId}`), + ]); + + const numBlocksBeforeUnBond = parseInt(BinaryUtils.base64ToBigInt(encodedNumBlocksBeforeUnBond).toString()); + const erdNonce = erd_nonce; + + const data: AccountDeferred[] = encodedUserDeferredPaymentList.reduce((result: AccountDeferred[], _, index, array) => { + if (index % 2 === 0) { + const [encodedDeferredPayment, encodedUnstakedNonce] = array.slice(index, index + 2); + + const deferredPayment = BinaryUtils.base64ToBigInt(encodedDeferredPayment).toString(); + const unstakedNonce = parseInt(BinaryUtils.base64ToBigInt(encodedUnstakedNonce).toString()); + const blocksLeft = Math.max(0, unstakedNonce + numBlocksBeforeUnBond - erdNonce); + const secondsLeft = blocksLeft * 6; // 6 seconds per block + + result.push({ deferredPayment, secondsLeft }); + } + + return result; + }, []); + + return data; + } + + private async getBlsKeysStatusForPublicKey(publicKey: string) { + const auctionContractAddress = this.apiConfigService.getAuctionContractAddress(); + if (!auctionContractAddress) { + return undefined; + } + + return await this.vmQueryService.vmQuery( + auctionContractAddress, + 'getBlsKeysStatus', + auctionContractAddress, + [publicKey], + ); + } + + private async getRewardAddressForNode(blsKey: string): Promise { + const stakingContractAddress = this.apiConfigService.getStakingContractAddress(); + if (!stakingContractAddress) { + return ''; + } + + const [encodedRewardsPublicKey] = await this.vmQueryService.vmQuery( + stakingContractAddress, + 'getRewardAddress', + undefined, + [blsKey], + ); + + const rewardsPublicKey = Buffer.from(encodedRewardsPublicKey, 'base64').toString(); + return AddressUtils.bech32Encode(rewardsPublicKey); + } + + private async getAllNodeStates(address: string) { + return await this.vmQueryService.vmQuery( + address, + 'getAllNodeStates' + ); + } + + async getKeys(address: string, filter: AccountKeyFilter, pagination: QueryPagination): Promise { + const { from, size } = pagination; + const publicKey = AddressUtils.bech32Decode(address); + const isStakingProvider = await this.providerService.isProvider(address); + + let notStakedNodes: AccountKey[] = []; + + if (isStakingProvider) { + const allNodeStates = await this.getAllNodeStates(address); + const inactiveNodesBuffers = this.getInactiveNodesBuffers(allNodeStates); + notStakedNodes = this.createNotStakedNodes(inactiveNodesBuffers); + } + + const blsKeysStatus = await this.getBlsKeysStatusForPublicKey(publicKey); + let nodes: AccountKey[] = []; + + if (blsKeysStatus) { + nodes = this.createAccountKeys(blsKeysStatus); + await this.applyRewardAddressAndTopUpToNodes(nodes, address); + await this.applyNodeUnbondingPeriods(nodes); + await this.updateQueuedNodes(nodes); + } + + let filteredNodes = [...notStakedNodes, ...nodes]; + + if (filter && filter.status && filter.status.length > 0) { + filteredNodes = filteredNodes.filter(node => filter.status.includes(node.status as NodeStatusRaw)); + filteredNodes = this.sortNodesByStatus(filteredNodes, filter.status); + } + + return filteredNodes.slice(from, from + size); + } + + getInactiveNodesBuffers(allNodeStates: string[]): string[] { + if (!allNodeStates) { + return []; + } + + const checkIfCurrentItemIsStatus = (currentNodeData: string) => + Object.values(NodeStatusRaw).includes( + currentNodeData as NodeStatusRaw + ); + + return allNodeStates.reduce( + (totalNodes: string[], currentNodeState, nodeIndex, allNodesDataArray) => { + const decodedData = Buffer.from(currentNodeState, 'base64').toString(); + const isNotStakedStatus = + decodedData === NodeStatusRaw.notStaked; + + const isCurrentItemTheStatus = checkIfCurrentItemIsStatus(decodedData); + + const nextStatusItemIndex = allNodesDataArray.findIndex( + (nodeData, nodeDataIndex) => + nodeIndex < nodeDataIndex + ? checkIfCurrentItemIsStatus(Buffer.from(nodeData, 'base64').toString()) + : false + ); + + if (isCurrentItemTheStatus && nextStatusItemIndex < 0 && isNotStakedStatus) { + return [...totalNodes, ...allNodesDataArray.slice(nodeIndex + 1)]; + } + + if (isCurrentItemTheStatus && isNotStakedStatus) { + return [...totalNodes, ...allNodesDataArray.slice(nodeIndex + 1, nextStatusItemIndex)]; + } + + return totalNodes; + }, + [] + ); + } + + createNotStakedNodes(inactiveNodesBuffers: string[]): AccountKey[] { + return inactiveNodesBuffers.map((inactiveNodeBuffer) => { + const accountKey: AccountKey = new AccountKey(); + accountKey.blsKey = BinaryUtils.padHex(Buffer.from(inactiveNodeBuffer, 'base64').toString('hex')); + accountKey.status = NodeStatusRaw.notStaked; + accountKey.stake = '2500000000000000000000'; + + return accountKey; + }); + } + + createAccountKeys(blsKeysStatus: string[]): AccountKey[] { + const nodes: AccountKey[] = []; + for (let index = 0; index < blsKeysStatus.length; index += 2) { + const [encodedBlsKey, encodedStatus] = blsKeysStatus.slice(index, index + 2); + + const accountKey: AccountKey = new AccountKey(); + accountKey.blsKey = BinaryUtils.padHex(Buffer.from(encodedBlsKey, 'base64').toString('hex')); + accountKey.status = Buffer.from(encodedStatus, 'base64').toString(); + accountKey.stake = '2500000000000000000000'; + + nodes.push(accountKey); + } + return nodes; + } + + private sortNodesByStatus(nodes: AccountKey[], status: NodeStatusRaw[]): AccountKey[] { + return nodes.sorted(node => { + const statusIndex = status.indexOf(node.status as NodeStatusRaw); + return statusIndex === -1 ? status.length : statusIndex; + }); + } + + async applyRewardAddressAndTopUpToNodes(nodes: AccountKey[], address: string) { + if (nodes.length) { + const rewardAddress = await this.getRewardAddressForNode(nodes[0].blsKey); + const { topUp } = await this.stakeService.getAllStakesForNode(address); + + for (const node of nodes) { + node.rewardAddress = rewardAddress; + node.topUp = topUp; + node.remainingUnBondPeriod = undefined; + } + } + } + + async updateQueuedNodes(nodes: AccountKey[]) { + const stakingContractAddress = this.apiConfigService.getStakingContractAddress(); + if (!stakingContractAddress) { + return; + } + + const queuedNodes: string[] = nodes + .filter((node: AccountKey) => node.status === 'queued') + .map(({ blsKey }) => blsKey); + + if (queuedNodes.length) { + const [queueSizeEncoded] = await this.vmQueryService.vmQuery( + stakingContractAddress, + 'getQueueSize', + ); + + if (queueSizeEncoded) { + const queueSize = Buffer.from(queueSizeEncoded, 'base64').toString(); + + const queueIndexes = await Promise.all( + queuedNodes.map((blsKey: string) => + this.vmQueryService.vmQuery( + stakingContractAddress, + 'getQueueIndex', + this.apiConfigService.getAuctionContractAddress(), + [blsKey], + ) + ), + ); + + let index = 0; + for (const queueIndexEncoded of queueIndexes) { + if (queueIndexEncoded) { + const [found] = nodes.filter((x: AccountKey) => x.blsKey === queuedNodes[index]); + + found.queueIndex = Buffer.from(queueIndexEncoded[0], 'base64').toString(); + found.queueSize = queueSize; + + index++; + } + } + } + } + } + + 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 => ({ + address: contract.contract, + deployTxHash: contract.deployTxHash, + timestamp: contract.timestamp, + assets: assets[contract.contract], + })); + + 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); + } + + async getContractUpgrades(queryPagination: QueryPagination, address: string): Promise { + const details = await this.indexerService.getScDeploy(address); + if (!details) { + return []; + } + + const upgrades = details.upgrades.map(item => ApiUtils.mergeObjects(new ContractUpgrades(), { + address: item.upgrader, + txHash: item.upgradeTxHash, + codeHash: item.codeHash, + timestamp: item.timestamp, + })).sortedDescending(item => item.timestamp); + + return upgrades.slice(queryPagination.from, queryPagination.from + queryPagination.size); + } + + async getAccountHistory(address: string, pagination: QueryPagination, filter: AccountHistoryFilter): Promise { + const elasticResult = await this.indexerService.getAccountHistory(address, pagination, filter); + return elasticResult.map(item => ApiUtils.mergeObjects(new AccountHistory(), item)); + } + + async getAccountHistoryCount(address: string, filter: AccountHistoryFilter): Promise { + return await this.indexerService.getAccountHistoryCount(address, filter); + } + + async getAccountTokenHistoryCount(address: string, tokenIdentifier: string, filter: AccountHistoryFilter): Promise { + return await this.indexerService.getAccountTokenHistoryCount(address, tokenIdentifier, filter); + } + + async getAccountTokenHistory(address: string, tokenIdentifier: string, pagination: QueryPagination, filter: AccountHistoryFilter): Promise { + const elasticResult = await this.indexerService.getAccountTokenHistory(address, tokenIdentifier, pagination, filter); + return elasticResult.map(item => ApiUtils.mergeObjects(new AccountEsdtHistory(), item)); + } + + async getAccountEsdtHistory(address: string, pagination: QueryPagination, filter: AccountHistoryFilter): Promise { + const elasticResult = await this.indexerService.getAccountEsdtHistory(address, pagination, filter); + return elasticResult.map(item => ApiUtils.mergeObjects(new AccountEsdtHistory(), item)); + } + + async getAccountEsdtHistoryCount(address: string, filter: AccountHistoryFilter): Promise { + return await this.indexerService.getAccountEsdtHistoryCount(address, filter); + } + + private async applyNodeUnbondingPeriods(nodes: AccountKey[]): Promise { + const leavingNodes = nodes.filter(node => node.status === 'unStaked'); + await Promise.all(leavingNodes.map(async node => { + const keyUnbondPeriod = await this.keysService.getKeyUnbondPeriod(node.blsKey); + node.remainingUnBondPeriod = keyUnbondPeriod?.remainingUnBondPeriod; + })); + } + + async getApplicationMostUsed(): Promise { + return await this.cachingService.getOrSet( + CacheInfo.ApplicationMostUsed.key, + async () => await this.getApplicationMostUsedRaw(), + CacheInfo.ApplicationMostUsed.ttl + ); + } + + async getApplicationMostUsedRaw(): Promise { + const transfersLast24hUrl = this.apiConfigService.getAccountExtraDetailsTransfersLast24hUrl(); + if (!transfersLast24hUrl) { + throw new Error('Transfers last 24h URL is not set'); + } + + const { data: mostUsedApplications } = await this.apiService.get(transfersLast24hUrl); + return mostUsedApplications.map((item: any) => new ApplicationMostUsed({ + address: item.key, + transfers24H: item.value, + })); + } +} diff --git a/src/endpoints/accounts-v2/entities/account.contract.ts b/src/endpoints/accounts-v2/entities/account.contract.ts new file mode 100644 index 000000000..e75cf782a --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.contract.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { AccountAssets } from "src/common/assets/entities/account.assets"; + +export class AccountContract { + constructor(init?: Partial) { + Object.assign(this, init); + } + + @ApiProperty({ type: String }) + address: string = ""; + + @ApiProperty({ type: String }) + deployTxHash: string = ""; + + @ApiProperty({ type: Number }) + timestamp: number = 0; + + @ApiProperty({ type: AccountAssets, nullable: true, description: 'Contract assets' }) + assets: AccountAssets | undefined = undefined; +} diff --git a/src/endpoints/accounts-v2/entities/account.deferred.ts b/src/endpoints/accounts-v2/entities/account.deferred.ts new file mode 100644 index 000000000..ce99c31e1 --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.deferred.ts @@ -0,0 +1,14 @@ +import { SwaggerUtils } from '@multiversx/sdk-nestjs-common'; +import { ApiProperty } from '@nestjs/swagger'; + +export class AccountDeferred { + constructor(init?: Partial) { + Object.assign(this, init); + } + + @ApiProperty(SwaggerUtils.amountPropertyOptions({ description: 'Deferred payment amount' })) + deferredPayment: string = ''; + + @ApiProperty({ description: 'Seconds left until unbonding time' }) + secondsLeft: number = 0; +} diff --git a/src/endpoints/accounts-v2/entities/account.detailed.ts b/src/endpoints/accounts-v2/entities/account.detailed.ts new file mode 100644 index 000000000..2c9f131ec --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.detailed.ts @@ -0,0 +1,74 @@ +import { ComplexityEstimation } from "@multiversx/sdk-nestjs-common"; +import { ApiProperty } from "@nestjs/swagger"; +import { ScamInfo } from "src/common/entities/scam-info.dto"; +import { NftCollectionAccount } from "src/endpoints/collections/entities/nft.collection.account"; +import { NftAccount } from "src/endpoints/nfts/entities/nft.account"; +import { Account } from "./account"; + +export class AccountDetailed extends Account { + constructor(init?: Partial) { + super(); + Object.assign(this, init); + } + + @ApiProperty({ description: 'The source code in hex format', required: false }) + code: string = ''; + + @ApiProperty({ description: 'The hash of the source code', required: false }) + codeHash: string = ''; + + @ApiProperty({ description: 'The hash of the root node' }) + rootHash: string = ''; + + @ApiProperty({ description: 'The username specific for this account', nullable: true, required: false }) + username: string | undefined = undefined; + + @ApiProperty({ description: 'The developer reward' }) + developerReward: string = ''; + + @ApiProperty({ description: 'The address in bech 32 format of owner account', required: false }) + ownerAddress: string = ''; + + @ApiProperty({ description: 'Specific property flag for smart contract', type: Boolean, required: false }) + isUpgradeable?: boolean; + + @ApiProperty({ description: 'Specific property flag for smart contract', type: Boolean, required: false }) + isReadable?: boolean; + + @ApiProperty({ description: 'Specific property flag for smart contract', type: Boolean, required: false }) + isPayable?: boolean; + + @ApiProperty({ description: 'Specific property flag for smart contract', type: Boolean, nullable: true, required: false }) + isPayableBySmartContract?: boolean | undefined = undefined; + + @ApiProperty({ type: ScamInfo, nullable: true, required: false }) + scamInfo: ScamInfo | undefined = undefined; + + @ApiProperty({ description: 'Account nft collections', type: Boolean, nullable: true, required: false }) + nftCollections: NftCollectionAccount[] | undefined = undefined; + + @ApiProperty({ description: 'Account nfts', type: Boolean, nullable: true, required: false }) + @ComplexityEstimation({ group: 'nfts', value: 1000 }) + nfts: NftAccount[] | undefined = undefined; + + @ApiProperty({ type: Number, nullable: true, required: false }) + activeGuardianActivationEpoch?: number; + + @ApiProperty({ type: String, nullable: true, required: false }) + activeGuardianAddress?: string; + + @ApiProperty({ type: String, nullable: true, required: false }) + activeGuardianServiceUid?: string; + + @ApiProperty({ type: Number, nullable: true, required: false }) + pendingGuardianActivationEpoch?: number; + + @ApiProperty({ type: String, nullable: true, required: false }) + pendingGuardianAddress?: string; + + @ApiProperty({ type: String, nullable: true, required: false }) + pendingGuardianServiceUid?: string; + + @ApiProperty({ type: Boolean, nullable: true, required: false }) + isGuarded?: boolean; +} diff --git a/src/endpoints/accounts-v2/entities/account.esdt.history.ts b/src/endpoints/accounts-v2/entities/account.esdt.history.ts new file mode 100644 index 000000000..ce48af609 --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.esdt.history.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { AccountHistory } from "./account.history"; + +export class AccountEsdtHistory extends AccountHistory { + constructor(init?: Partial) { + super(); + Object.assign(this, init); + } + + @ApiProperty({ type: String, example: 'WEGLD-bd4d79' }) + token: string = ''; + + @ApiProperty({ type: String, example: 'XPACHIEVE-5a0519-01' }) + identifier: string | undefined = undefined; +} diff --git a/src/endpoints/accounts-v2/entities/account.fetch.options.ts b/src/endpoints/accounts-v2/entities/account.fetch.options.ts new file mode 100644 index 000000000..367686761 --- /dev/null +++ b/src/endpoints/accounts-v2/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-v2/entities/account.history.filter.ts b/src/endpoints/accounts-v2/entities/account.history.filter.ts new file mode 100644 index 000000000..bed36d66a --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.history.filter.ts @@ -0,0 +1,10 @@ + +export class AccountHistoryFilter { + constructor(init?: Partial) { + Object.assign(this, init); + } + before?: number; + after?: number; + identifiers?: string[]; + token?: string; +} diff --git a/src/endpoints/accounts-v2/entities/account.history.ts b/src/endpoints/accounts-v2/entities/account.history.ts new file mode 100644 index 000000000..4fe64333b --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.history.ts @@ -0,0 +1,20 @@ +import { SwaggerUtils } from "@multiversx/sdk-nestjs-common"; +import { ApiProperty } from "@nestjs/swagger"; + +export class AccountHistory { + constructor(init?: Partial) { + Object.assign(this, init); + } + + @ApiProperty({ type: String, example: 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz' }) + address: string = ''; + + @ApiProperty(SwaggerUtils.amountPropertyOptions()) + balance: string = ''; + + @ApiProperty({ type: Number, example: 10000 }) + timestamp: number = 0; + + @ApiProperty({ type: Boolean, nullable: true, example: true, required: false }) + isSender?: boolean | undefined = undefined; +} diff --git a/src/endpoints/accounts-v2/entities/account.key.filter.ts b/src/endpoints/accounts-v2/entities/account.key.filter.ts new file mode 100644 index 000000000..67f73fade --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.key.filter.ts @@ -0,0 +1,10 @@ + +import { NodeStatusRaw } from "src/endpoints/nodes/entities/node.status"; + +export class AccountKeyFilter { + constructor(init?: Partial) { + Object.assign(this, init); + } + + status: NodeStatusRaw[] = []; +} diff --git a/src/endpoints/accounts-v2/entities/account.key.ts b/src/endpoints/accounts-v2/entities/account.key.ts new file mode 100644 index 000000000..51a59f4fc --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.key.ts @@ -0,0 +1,33 @@ + +import { SwaggerUtils } from "@multiversx/sdk-nestjs-common"; +import { ApiProperty } from "@nestjs/swagger"; + +export class AccountKey { + constructor(init?: Partial) { + Object.assign(this, init); + } + + @ApiProperty({ type: String, example: '2ef384d4d38bf3aad5cef34ce6eab047fba6d52b9735dbfdf7591289ed9c26ac7e816c9bb56ebf4f09129f045860f401275a91009befb4dc8ddc24ea4bc597290bd916b9f984c2a415ec9b2cfbc4a09de42c032314e6a21e69daf76302fcaa99' }) + blsKey: string = ''; + + @ApiProperty(SwaggerUtils.amountPropertyOptions()) + stake: string = ''; + + @ApiProperty(SwaggerUtils.amountPropertyOptions()) + topUp: string = ''; + + @ApiProperty({ type: String, example: 'online' }) + status: string = ''; + + @ApiProperty({ type: String, example: 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz' }) + rewardAddress: string = ''; + + @ApiProperty({ type: String, nullable: true, example: '2' }) + queueIndex: string | undefined = undefined; + + @ApiProperty({ type: String, nullable: true, example: '100' }) + queueSize: string | undefined = undefined; + + @ApiProperty({ type: Number, example: 10 }) + remainingUnBondPeriod: number | undefined = 0; +} diff --git a/src/endpoints/accounts-v2/entities/account.query.options.ts b/src/endpoints/accounts-v2/entities/account.query.options.ts new file mode 100644 index 000000000..91649d3c1 --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.query.options.ts @@ -0,0 +1,58 @@ +import { SortOrder } from "src/common/entities/sort.order"; +import { AccountSort } from "./account.sort"; +import { BadRequestException } from "@nestjs/common"; + +export class AccountQueryOptions { + constructor(init?: Partial) { + Object.assign(this, init); + } + + addresses?: string[]; + ownerAddress?: string; + + sort?: AccountSort; + order?: SortOrder; + isSmartContract?: boolean; + withOwnerAssets?: boolean; + withDeployInfo?: boolean; + withTxCount?: boolean; + withScrCount?: boolean; + name?: string; + tags?: string[]; + excludeTags?: string[]; + hasAssets?: boolean; + search?: string; + + validate(size: number) { + if (this.withDeployInfo && size > 25) { + throw new BadRequestException('Size must be less than or equal to 25 when withDeployInfo is set'); + } + + if (this.withTxCount && size > 25) { + throw new BadRequestException('Size must be less than or equal to 25 when withTxCount is set'); + } + + 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 { + return this.ownerAddress !== undefined || + this.sort !== undefined || + this.order !== undefined || + this.isSmartContract !== undefined || + this.withOwnerAssets !== undefined || + this.withDeployInfo !== undefined || + this.name !== undefined || + this.tags !== undefined || + this.excludeTags !== undefined || + this.hasAssets !== undefined || + this.search !== undefined || + this.addresses !== undefined; + } +} diff --git a/src/endpoints/accounts-v2/entities/account.sort.ts b/src/endpoints/accounts-v2/entities/account.sort.ts new file mode 100644 index 000000000..fbbdfddc4 --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.sort.ts @@ -0,0 +1,23 @@ +import { registerEnumType } from "@nestjs/graphql"; + +export enum AccountSort { + balance = 'balance', + timestamp = 'timestamp', + transfersLast24h = 'transfersLast24h', +} + +registerEnumType(AccountSort, { + name: 'AccountSort', + description: 'Account Sort object.', + valuesMap: { + balance: { + description: 'Sort by balance.', + }, + timestamp: { + description: 'Sort by timestamp.', + }, + transfersLast24h: { + description: 'Sort by transfersLast24h.', + }, + }, +}); diff --git a/src/endpoints/accounts-v2/entities/account.ts b/src/endpoints/accounts-v2/entities/account.ts new file mode 100644 index 000000000..6403d3cce --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.ts @@ -0,0 +1,51 @@ +import { SwaggerUtils } from "@multiversx/sdk-nestjs-common"; +import { ApiProperty } from "@nestjs/swagger"; +import { AccountAssets } from "src/common/assets/entities/account.assets"; + +export class Account { + constructor(init?: Partial) { + Object.assign(this, init); + } + + @ApiProperty({ type: String, description: 'Account bech32 address', example: 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz' }) + address: string = ''; + + @ApiProperty(SwaggerUtils.amountPropertyOptions({ description: 'Account current balance' })) + balance: string = ''; + + @ApiProperty({ type: Number, description: 'Account current nonce', example: 42 }) + nonce: number = 0; + + @ApiProperty({ type: Number, description: 'Timestamp of the block where the account was first indexed', example: 1676979360 }) + timestamp: number = 0; + + @ApiProperty({ type: Number, description: 'The shard ID allocated to the account', example: 0 }) + shard: number = 0; + + @ApiProperty({ type: String, description: 'Current owner address', required: false }) + ownerAddress: string | undefined = undefined; + + @ApiProperty({ type: AccountAssets, nullable: true, description: 'Account assets', required: false }) + assets: AccountAssets | undefined = undefined; + + @ApiProperty({ description: 'Specific property flag for smart contract', type: Number, required: false }) + deployedAt?: number | null; + + @ApiProperty({ description: 'The contract deploy transaction hash', required: false }) + deployTxHash?: string | null; + + @ApiProperty({ type: AccountAssets, nullable: true, description: 'Account assets', required: false }) + ownerAssets: AccountAssets | undefined = undefined; + + @ApiProperty({ description: 'Specific property flag for smart contract', type: Boolean, required: false }) + isVerified?: boolean; + + @ApiProperty({ description: 'The number of transactions performed on this account' }) + txCount?: number; + + @ApiProperty({ description: 'The number of smart contract results of this account' }) + scrCount?: number; + + @ApiProperty({ type: Number, description: 'Transfers in the last 24 hours', required: false }) + transfersLast24h: number | undefined = undefined; +} diff --git a/src/endpoints/accounts-v2/entities/account.verification.source.ts b/src/endpoints/accounts-v2/entities/account.verification.source.ts new file mode 100644 index 000000000..040cfc251 --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.verification.source.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AccountVerificationSource { + constructor(init?: Partial) { + Object.assign(this, init); + } + + @ApiProperty({ description: 'Abi file source' }) + abi: any = ''; + + @ApiProperty({ description: 'Contract source code' }) + contract: any = ''; +} diff --git a/src/endpoints/accounts-v2/entities/account.verification.status.ts b/src/endpoints/accounts-v2/entities/account.verification.status.ts new file mode 100644 index 000000000..996c4447d --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.verification.status.ts @@ -0,0 +1,4 @@ +export enum AccountVerificationStatus { + success = 'success', + byteCodeChangedSinceLastVerification = 'byteCodeChangedSinceLastVerification' +} diff --git a/src/endpoints/accounts-v2/entities/account.verification.ts b/src/endpoints/accounts-v2/entities/account.verification.ts new file mode 100644 index 000000000..29e72590d --- /dev/null +++ b/src/endpoints/accounts-v2/entities/account.verification.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { AccountVerificationSource } from './account.verification.source'; +import { AccountVerificationStatus } from './account.verification.status'; + +export class AccountVerification { + constructor(init?: Partial) { + Object.assign(this, init); + } + + @ApiProperty({ description: 'Source code hash' }) + codeHash?: string = ''; + + @ApiProperty({ description: 'Source code of contract', type: AccountVerificationSource, required: false }) + source?: any; + + @ApiProperty({ description: 'Verifier process status', enum: AccountVerificationStatus }) + status!: AccountVerificationStatus; + + @ApiProperty({ description: 'File hash for IPFS', required: false }) + ipfsFileHash?: string; +} diff --git a/src/endpoints/accounts-v2/entities/application.most.used.ts b/src/endpoints/accounts-v2/entities/application.most.used.ts new file mode 100644 index 000000000..cf507a4b4 --- /dev/null +++ b/src/endpoints/accounts-v2/entities/application.most.used.ts @@ -0,0 +1,8 @@ +export class ApplicationMostUsed { + constructor(init?: Partial) { + Object.assign(this, init); + } + + address: string = ''; + transfers24H: number = 0; +} diff --git a/src/endpoints/accounts-v2/entities/contract.upgrades.ts b/src/endpoints/accounts-v2/entities/contract.upgrades.ts new file mode 100644 index 000000000..2c7c7c601 --- /dev/null +++ b/src/endpoints/accounts-v2/entities/contract.upgrades.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from "@nestjs/swagger"; +export class ContractUpgrades { + constructor(init?: Partial) { + Object.assign(this, init); + } + @ApiProperty({ type: String, nullable: true, example: 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz' }) + address: string = ''; + + @ApiProperty({ type: String, nullable: true, example: '1c8c6b2148f25621fa2c798a2c9a184df61fdd1991aa0af7ea01eb7b89025d2a' }) + txHash: string = ''; + + @ApiProperty({ type: String, nullable: true, example: '1c8c6b2148f25621fa2c798a2c9a184df61fdd1991aa0af7ea01eb7b89025d2a' }) + codeHash: string = ''; + + @ApiProperty({ type: Number, nullable: true, example: '1638577452' }) + timestamp: number = 0; +} diff --git a/src/endpoints/accounts-v2/entities/deployed.contract.ts b/src/endpoints/accounts-v2/entities/deployed.contract.ts new file mode 100644 index 000000000..b2dce1663 --- /dev/null +++ b/src/endpoints/accounts-v2/entities/deployed.contract.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { AccountAssets } from "src/common/assets/entities/account.assets"; + +export class DeployedContract { + constructor(init?: Partial) { + Object.assign(this, init); + } + + @ApiProperty({ type: String }) + address: string = ""; + + @ApiProperty({ type: String }) + deployTxHash: string = ""; + + @ApiProperty({ type: Number }) + timestamp: number = 0; + + @ApiProperty({ type: AccountAssets, nullable: true, required: false, description: 'Contract assets' }) + assets: AccountAssets | undefined = undefined; +} diff --git a/src/endpoints/endpoints.controllers.module.ts b/src/endpoints/endpoints.controllers.module.ts index 318e527f0..787599feb 100644 --- a/src/endpoints/endpoints.controllers.module.ts +++ b/src/endpoints/endpoints.controllers.module.ts @@ -39,12 +39,13 @@ import { PoolController } from "./pool/pool.controller"; import { TpsController } from "./tps/tps.controller"; import { ApplicationController } from "./applications/application.controller"; import { EventsController } from "./events/events.controller"; +import { AccountControllerV2 } from "./accounts-v2/account.controller.v2"; @Module({}) export class EndpointsControllersModule { static forRoot(): DynamicModule { const controllers: Type[] = [ - AccountController, BlockController, CollectionController, DelegationController, DelegationLegacyController, IdentitiesController, + AccountController, AccountControllerV2, BlockController, CollectionController, DelegationController, DelegationLegacyController, IdentitiesController, KeysController, MiniBlockController, NetworkController, NftController, TagController, NodeController, ProviderController, GatewayProxyController, RoundController, SmartContractResultController, ShardController, StakeController, StakeController, TokenController, TransactionController, UsernameController, VmQueryController, WaitingListController, diff --git a/src/endpoints/endpoints.services.module.ts b/src/endpoints/endpoints.services.module.ts index fc8531828..5b1d095cd 100644 --- a/src/endpoints/endpoints.services.module.ts +++ b/src/endpoints/endpoints.services.module.ts @@ -36,10 +36,12 @@ import { PoolModule } from "./pool/pool.module"; import { TpsModule } from "./tps/tps.module"; import { ApplicationModule } from "./applications/application.module"; import { EventsModule } from "./events/events.module"; +import { AccountModuleV2 } from "./accounts-v2/account.module.v2"; @Module({ imports: [ AccountModule, + AccountModuleV2, BlockModule, CollectionModule, DelegationModule, @@ -79,7 +81,7 @@ import { EventsModule } from "./events/events.module"; EventsModule, ], exports: [ - AccountModule, CollectionModule, BlockModule, DelegationModule, DelegationLegacyModule, IdentitiesModule, KeysModule, + AccountModule, AccountModuleV2, CollectionModule, BlockModule, DelegationModule, DelegationLegacyModule, IdentitiesModule, KeysModule, MiniBlockModule, NetworkModule, NftModule, NftMediaModule, TagModule, NodeModule, ProviderModule, RoundModule, SmartContractResultModule, ShardModule, StakeModule, TokenModule, RoundModule, TransactionModule, UsernameModule, VmQueryModule, WaitingListModule, EsdtModule, BlsModule, DappConfigModule, TransferModule, PoolModule, TransactionActionModule, WebsocketModule, MexModule, diff --git a/src/endpoints/nfts/nft.module.ts b/src/endpoints/nfts/nft.module.ts index 6242b358d..1e4733716 100644 --- a/src/endpoints/nfts/nft.module.ts +++ b/src/endpoints/nfts/nft.module.ts @@ -10,6 +10,7 @@ import { TokenModule } from "../tokens/token.module"; import { NftExtendedAttributesService } from "./nft.extendedattributes.service"; import { NftService } from "./nft.service"; import { LockedAssetModule } from "../../common/locked-asset/locked-asset.module"; +import { MongoDbModule } from "src/common/indexer/db"; @Module({ imports: [ @@ -22,6 +23,7 @@ import { LockedAssetModule } from "../../common/locked-asset/locked-asset.module forwardRef(() => AssetsModule), forwardRef(() => LockedAssetModule), NftMediaModule, + MongoDbModule, ], providers: [ NftService, NftExtendedAttributesService, diff --git a/src/endpoints/nfts/nft.service.ts b/src/endpoints/nfts/nft.service.ts index f3703125d..464b14685 100644 --- a/src/endpoints/nfts/nft.service.ts +++ b/src/endpoints/nfts/nft.service.ts @@ -31,6 +31,8 @@ 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"; +import { AccountDetailsRepository } from "src/common/indexer/db"; +import { isDbValid } from "src/state-changes/utils/state-changes.utils"; @Injectable() export class NftService { @@ -51,6 +53,7 @@ export class NftService { private readonly esdtAddressService: EsdtAddressService, private readonly mexTokenService: MexTokenService, private readonly lockedAssetService: LockedAssetService, + private readonly accountDetailsRepository: AccountDetailsRepository, ) { this.NFT_THUMBNAIL_PREFIX = this.apiConfigService.getExternalMediaUrl() + '/nfts/asset'; this.DEFAULT_MEDIA = [ @@ -504,6 +507,17 @@ export class NftService { return await this.indexerService.getNftCount(filter); } + async getNftsForAddressFromDb(address: string, queryPagination: QueryPagination, filter: NftFilter, fields?: string[], queryOptions?: NftQueryOptions, source?: EsdtDataSource): Promise { + const isDbUpToDate: boolean = await isDbValid(this.cachingService); + if (isDbUpToDate === true) { + const nfts = await this.accountDetailsRepository.getNftsForAddress(address, queryPagination) as NftAccount[]; + if (nfts && nfts.length > 0) { + return nfts; + } + } + return await this.getNftsForAddress(address, queryPagination, filter, fields, queryOptions, source); + } + async getNftsForAddress(address: string, queryPagination: QueryPagination, filter: NftFilter, fields?: string[], queryOptions?: NftQueryOptions, source?: EsdtDataSource): Promise { let nfts = await this.esdtAddressService.getNftsForAddress(address, filter, queryPagination, source, queryOptions); for (const nft of nfts) { @@ -603,6 +617,17 @@ export class NftService { return await this.esdtAddressService.getNftCountForAddressFromElastic(address, filter); } + async getNftForAddressFromDb(address: string, identifier: string, fields?: string[]): Promise { + const isDbUpToDate: boolean = await isDbValid(this.cachingService); + if (isDbUpToDate === true) { + const nft = await this.accountDetailsRepository.getNftForAddress(address, identifier) as NftAccount; + if (nft) { + return nft; + } + } + return await this.getNftForAddress(address, identifier, fields); + } + async getNftForAddress(address: string, identifier: string, fields?: string[]): Promise { const filter = new NftFilter(); filter.identifiers = [identifier]; diff --git a/src/endpoints/tokens/token.module.ts b/src/endpoints/tokens/token.module.ts index 14669ed7b..811a90d33 100644 --- a/src/endpoints/tokens/token.module.ts +++ b/src/endpoints/tokens/token.module.ts @@ -9,6 +9,7 @@ import { MexModule } from "../mex/mex.module"; import { CollectionModule } from "../collections/collection.module"; import { PluginModule } from "src/plugins/plugin.module"; import { TransferModule } from "../transfers/transfer.module"; +import { MongoDbModule } from "src/common/indexer/db"; @Module({ imports: [ @@ -20,6 +21,7 @@ import { TransferModule } from "../transfers/transfer.module"; forwardRef(() => MexModule.forRoot()), forwardRef(() => CollectionModule), forwardRef(() => PluginModule), + MongoDbModule, ], providers: [ TokenService, TokenTransferService, diff --git a/src/endpoints/tokens/token.service.ts b/src/endpoints/tokens/token.service.ts index 52d1f77da..c8b2d5fd5 100644 --- a/src/endpoints/tokens/token.service.ts +++ b/src/endpoints/tokens/token.service.ts @@ -43,6 +43,8 @@ import { MexPairService } from "../mex/mex.pair.service"; import { MexPairState } from "../mex/entities/mex.pair.state"; import { MexTokenType } from "../mex/entities/mex.token.type"; import { NftSubType } from "../nfts/entities/nft.sub.type"; +import { AccountDetailsRepository } from "src/common/indexer/db"; +import { isDbValid } from "src/state-changes/utils/state-changes.utils"; @Injectable() export class TokenService { @@ -68,6 +70,7 @@ export class TokenService { private readonly dataApiService: DataApiService, private readonly mexPairService: MexPairService, private readonly apiService: ApiService, + private readonly accountDetailsRepository: AccountDetailsRepository, ) { } async isToken(identifier: string): Promise { @@ -250,6 +253,18 @@ export class TokenService { return tokens.length; } + async getTokensForAddressFromDb(address: string, queryPagination: QueryPagination, filter: TokenFilter): Promise { + const isDbUpToDate: boolean = await isDbValid(this.cachingService); + if (isDbUpToDate === true) { + const tokens = await this.accountDetailsRepository.getTokensForAddress(address, queryPagination) as TokenWithBalance[]; + if (tokens && tokens.length > 0) { + return tokens; + } + } + + return await this.getTokensForAddress(address, queryPagination, filter); + } + async getTokensForAddress(address: string, queryPagination: QueryPagination, filter: TokenFilter): Promise { let tokens: TokenWithBalance[]; if (AddressUtils.isSmartContractAddress(address)) { @@ -339,6 +354,18 @@ export class TokenService { return tokens; } + async getTokenForAddressFromDb(address: string, identifier: string): Promise { + const isDbUpToDate: boolean = await isDbValid(this.cachingService); + if (isDbUpToDate === true) { + const token = await this.accountDetailsRepository.getTokenForAddress(address, identifier) as TokenDetailedWithBalance; + if (token) { + return token; + } + } + return await this.getTokenForAddress(address, identifier); + } + + async getTokenForAddress(address: string, identifier: string): Promise { const esdtIdentifier = identifier.split('-').slice(0, 2).join('-'); diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 1fd8c8962..00fa8a3ab 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -4,6 +4,8 @@ import { ESDigitalToken } from "./esdt"; import { TrieLeafData } from "./trie_leaf_data"; import { TokenParser } from "./token.parser"; import { AccountChanges, AccountState, EsdtState, ESDTType, StateChanges } from "../entities"; +import { CacheInfo } from "src/utils/cache.info"; +import { CacheService } from "@multiversx/sdk-nestjs-cache"; export enum StateAccessOperation { @@ -272,4 +274,33 @@ export function getFinalStates(stateChanges: Record) { } return finalStates; -} \ No newline at end of file +} + +export async function isDbValid(cacheService: CacheService): Promise { + // TODO: do not hardcode shard IDs + const timestampsMs: (string | undefined)[] = await Promise.all([ + cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(1).key), + cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(2).key), + // cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(3).key), + cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(4294967295).key), + ]) as (string | undefined)[];; + + const numericValues = timestampsMs + .map((timestampMsRaw: string | null | undefined) => + timestampMsRaw !== null && timestampMsRaw !== undefined ? Number(timestampMsRaw) : undefined + ) + .filter((timestampMs: number | undefined): timestampMs is number => timestampMs !== undefined && !isNaN(timestampMs)); + + const minTimestamp = numericValues.length > 0 ? Math.min(...numericValues) : null; + + if (minTimestamp === null) { + return false; + } + + const diff = Date.now() - minTimestamp; + console.log('Min timestamp from cache:', minTimestamp); + console.log('Current timestamp:', Date.now()); + console.group('diff', diff); + const blockTime = 6000; + return diff < blockTime; +} From 270638f3bd63baac405cb89fb122788de5c53369 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Mon, 8 Sep 2025 12:43:26 +0300 Subject: [PATCH 18/90] fix rabbit queue ordering --- src/state-changes/state.changes.module.ts | 1 + src/state-changes/utils/state-changes.utils.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/state-changes/state.changes.module.ts b/src/state-changes/state.changes.module.ts index 3bf8ae7f0..9149e3365 100644 --- a/src/state-changes/state.changes.module.ts +++ b/src/state-changes/state.changes.module.ts @@ -31,6 +31,7 @@ export class StateChangesModule { type: 'fanout', options: {}, uri: apiConfigService.getStateChangesUrl(), + prefetchCount: 1, }; }, }), diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 00fa8a3ab..2fec43c26 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -279,6 +279,7 @@ export function getFinalStates(stateChanges: Record) { export async function isDbValid(cacheService: CacheService): Promise { // TODO: do not hardcode shard IDs const timestampsMs: (string | undefined)[] = await Promise.all([ + cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(0).key), cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(1).key), cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(2).key), // cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(3).key), From 0ae19729371c297967f4917d468ec61ea6f996af Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 9 Sep 2025 15:04:46 +0300 Subject: [PATCH 19/90] fix tokens update + add index on tokens identifiers --- .../account.details.repository.ts | 146 +++++++++++++++--- .../db/schemas/account.details.schema.ts | 12 +- .../state.changes.consumer.service.ts | 14 +- .../utils/state-changes.utils.ts | 4 +- 4 files changed, 143 insertions(+), 33 deletions(-) diff --git a/src/common/indexer/db/repositories/account.details.repository.ts b/src/common/indexer/db/repositories/account.details.repository.ts index 3a4abffb5..404034611 100644 --- a/src/common/indexer/db/repositories/account.details.repository.ts +++ b/src/common/indexer/db/repositories/account.details.repository.ts @@ -302,10 +302,9 @@ export class AccountDetailsRepository { async updateAccounts(accounts: AccountDetails[]): Promise { try { if (!accounts.length) return []; - + let totalOperations = 0; const operations = accounts.map((accountDetailed) => { const updateFields: any = {}; - const isValidValue = (value: any): boolean => value !== undefined && value !== null; @@ -315,47 +314,150 @@ export class AccountDetailsRepository { } }); - const updateOps: any = {}; + const updateOps: any[] = []; + if (Object.keys(updateFields).length > 0) { - updateOps.$set = updateFields; + updateOps.push({ $set: updateFields }); } + // --- tokens --- if (accountDetailed.tokens?.length) { - updateOps.$push = updateOps.$push || {}; - updateOps.$push.tokens = updateOps.$push.tokens || { $each: [] }; - - for (const token of accountDetailed.tokens) { - updateOps.$push.tokens.$each.push(token); - } + updateOps.push({ + $set: { + tokens: { + $let: { + vars: { newTokens: accountDetailed.tokens }, + in: { + $concatArrays: [ + { + $map: { + input: { $ifNull: ["$tokens", []] }, // asigurăm array gol dacă tokens nu există + as: "t", + in: { + $let: { + vars: { + updated: { + $filter: { + input: "$$newTokens", + cond: { $eq: ["$$this.identifier", "$$t.identifier"] }, + }, + }, + }, + in: { + $cond: [ + { $gt: [{ $size: "$$updated" }, 0] }, + { $arrayElemAt: ["$$updated", 0] }, + "$$t", + ], + }, + }, + }, + }, + }, + { + $filter: { + input: "$$newTokens", + cond: { + $not: { + $in: [ + "$$this.identifier", + { + $map: { + input: { $ifNull: ["$tokens", []] }, + as: "t", + in: "$$t.identifier", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + }); } + // --- nfts (analog tokens) --- if (accountDetailed.nfts?.length) { - updateOps.$push = updateOps.$push || {}; - updateOps.$push.nfts = updateOps.$push.nfts || { $each: [] }; - - for (const nft of accountDetailed.nfts) { - updateOps.$push.nfts.$each.push(nft); - } + updateOps.push({ + $set: { + nfts: { + $let: { + vars: { newNfts: accountDetailed.nfts }, + in: { + $concatArrays: [ + { + $map: { + input: { $ifNull: ["$nfts", []] }, // asigurăm array gol dacă nfts nu există + as: "n", + in: { + $let: { + vars: { + updated: { + $filter: { + input: "$$newNfts", + cond: { $eq: ["$$this.identifier", "$$n.identifier"] }, + }, + }, + }, + in: { + $cond: [ + { $gt: [{ $size: "$$updated" }, 0] }, + { $arrayElemAt: ["$$updated", 0] }, + "$$n", + ], + }, + }, + }, + }, + }, + { + $filter: { + input: "$$newNfts", + cond: { + $not: { + $in: [ + "$$this.identifier", + { + $map: { + input: { $ifNull: ["$nfts", []] }, + as: "n", + in: "$$n.identifier", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + }); } - + totalOperations += updateOps.length; return { updateOne: { filter: { address: accountDetailed.address }, - update: { - ...updateOps, - - }, + update: updateOps, upsert: true, }, }; }); - + console.log('number of write operations:', totalOperations); const result = await this.accountDetailsModel.bulkWrite(operations, { ordered: true, }); return result; } catch (error: any) { + console.error('Error updating accounts:', error); throw error; } } diff --git a/src/common/indexer/db/schemas/account.details.schema.ts b/src/common/indexer/db/schemas/account.details.schema.ts index 659fb8dd2..7430d7c7c 100644 --- a/src/common/indexer/db/schemas/account.details.schema.ts +++ b/src/common/indexer/db/schemas/account.details.schema.ts @@ -88,11 +88,11 @@ export class AccountDetails { @Prop({ type: Array, required: false }) nftCollections?: NftCollectionAccount[]; - @Prop({ type: Array, required: false }) - nfts?: NftAccount[]; + @Prop({ type: Array, default: [] }) + nfts?: NftAccount[] = []; - @Prop({ type: Array, required: false }) - tokens?: TokenWithBalance[]; + @Prop({ type: Array, default: [] }) + tokens?: TokenWithBalance[] = []; @Prop({ required: false, type: Number }) activeGuardianActivationEpoch?: number; @@ -122,4 +122,6 @@ export class AccountDetails { export const AccountDetailsSchema = SchemaFactory.createForClass(AccountDetails); -AccountDetailsSchema.index({ address: 1 }, { unique: true }); \ No newline at end of file +AccountDetailsSchema.index({ address: 1 }, { unique: true }); +AccountDetailsSchema.index({ "tokens.identifier": 1 }); +AccountDetailsSchema.index({ "nfts.identifier": 1 }); \ No newline at end of file diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index f56f7157e..f9bc9f6e4 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -22,21 +22,27 @@ export class StateChangesConsumerService { deadLetterExchange: 'state-changes-test_dlx', }) async consumeEvents(stateChanges: StateChangesRaw) { - console.log(stateChanges) + // console.log(stateChanges) try { - + console.time(`processing time shard ${stateChanges.shardID}`) const decodedStateChanges = this.decodeStateChanges(stateChanges) if (Object.keys(decodedStateChanges).length !== 0) { const finalStates = getFinalStates(decodedStateChanges); const transformedStates = this.transformFinalStatesToDbFormat(finalStates); + // transformedStates.forEach(s => { + // if (s.tokens && s.tokens.length > 0) console.log(s.address, s.tokens); + // }); + + // console.log(transformedStates) await this.accountDetailsRepository.updateAccounts(transformedStates); } - this.cacheService.setRemote( + await this.cacheService.setRemote( CacheInfo.LatestProcessedBlockTimestamp(stateChanges.shardID).key, stateChanges.timestampMs, CacheInfo.LatestProcessedBlockTimestamp(stateChanges.shardID).ttl, ); + console.timeEnd(`processing time shard ${stateChanges.shardID}`) } catch (error) { console.error(`Error consuming state changes:`, error); throw error; @@ -65,7 +71,7 @@ export class StateChangesConsumerService { } } - // console.dir(transformed, { depth: null }) + return transformed; } } \ No newline at end of file diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 2fec43c26..25c98d58f 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -71,7 +71,7 @@ function getDecodedUserAccountData(buf: any) { return { nonce: longToString(msg.Nonce), - balance: balance, + balance: balance.toString(), developerReward: devReward, address: address, ownerAddress: ownerAddress, @@ -103,7 +103,7 @@ function getDecodedEsdtData(buf: any) { identifier, nonce, type: msgEsdtData.Type, - value: valueBigInt, + value: valueBigInt.toString(), propertiesHex: bytesToHex(msgEsdtData.Properties), reservedHex: bytesToHex(msgEsdtData.Reserved), tokenMetaData: msgEsdtData.TokenMetaData ?? null, From 434c9604bb36378fb60e5a956d6e391fe26b63d9 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 10 Sep 2025 14:16:31 +0300 Subject: [PATCH 20/90] decoding improvements --- .../state.changes.consumer.service.ts | 28 ++--- .../utils/state-changes.utils.ts | 117 ++++++++++++++++-- 2 files changed, 123 insertions(+), 22 deletions(-) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index f9bc9f6e4..590c72366 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -1,12 +1,11 @@ import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers"; import { StateChanges, StateChangesRaw } from "./entities"; -import { decodeStateChangesRaw, getFinalStates } from "./utils/state-changes.utils"; +import { decodeStateChangesFinal } from "./utils/state-changes.utils"; import { AccountDetails, AccountDetailsRepository } from "src/common/indexer/db"; import { Injectable } from "@nestjs/common"; import { TokenWithBalance } from "src/endpoints/tokens/entities/token.with.balance"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; import { CacheInfo } from "src/utils/cache.info"; -// import { ApiConfigService } from "src/common/api-config/api.config.service"; @Injectable() export class StateChangesConsumerService { @@ -22,20 +21,15 @@ export class StateChangesConsumerService { deadLetterExchange: 'state-changes-test_dlx', }) async consumeEvents(stateChanges: StateChangesRaw) { - // console.log(stateChanges) try { console.time(`processing time shard ${stateChanges.shardID}`) - const decodedStateChanges = this.decodeStateChanges(stateChanges) - if (Object.keys(decodedStateChanges).length !== 0) { - const finalStates = getFinalStates(decodedStateChanges); - const transformedStates = this.transformFinalStatesToDbFormat(finalStates); - // transformedStates.forEach(s => { - // if (s.tokens && s.tokens.length > 0) console.log(s.address, s.tokens); - // }); - // console.log(transformedStates) - await this.accountDetailsRepository.updateAccounts(transformedStates); - } + console.time('decode time') + const finalStates = this.decodeStateChangesFinal(stateChanges); + const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates); + console.timeEnd('decode time') + + await this.accountDetailsRepository.updateAccounts(transformedFinalStates); await this.cacheService.setRemote( CacheInfo.LatestProcessedBlockTimestamp(stateChanges.shardID).key, @@ -49,8 +43,12 @@ export class StateChangesConsumerService { } } - private decodeStateChanges(stateChanges: StateChangesRaw) { - return decodeStateChangesRaw(stateChanges); + // private decodeStateChanges(stateChanges: StateChangesRaw) { + // return decodeStateChangesRaw(stateChanges); + // } + + private decodeStateChangesFinal(stateChanges: StateChangesRaw) { + return decodeStateChangesFinal(stateChanges); } private transformFinalStatesToDbFormat(finalStates: Record) { diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 25c98d58f..5e7f4fe8d 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -72,7 +72,7 @@ function getDecodedUserAccountData(buf: any) { return { nonce: longToString(msg.Nonce), balance: balance.toString(), - developerReward: devReward, + developerReward: devReward.toString(), address: address, ownerAddress: ownerAddress, codeHash: bytesToHex(msg.CodeHash), @@ -134,12 +134,10 @@ export function decodeStateChangesRaw(stateChanges: any) { const dataTrieChanges = sa.dataTrieChanges; let allDecodedEsdtData: any[] = []; - if (!dataTrieChanges) { - // console.log(` Entry #${i}: empty dataTrieChanges`); - } else { + if (dataTrieChanges) { for (const dataTrieChange of dataTrieChanges) { if (dataTrieChange.version === 0) { - console.warn(` Entry #${i}: unsupported dataTrieChanges version 0`); + console.warn(`Entry #${i}: unsupported dataTrieChanges version 0`); } else { const bufEsdtData = Buffer.from(dataTrieChange.val, "base64"); @@ -149,8 +147,8 @@ export function decodeStateChangesRaw(stateChanges: any) { } } } - } + if (decodedAccountData || allDecodedEsdtData.length > 0) { const groupedEsdtStates = allDecodedEsdtData.reduce>( (acc, state) => { @@ -204,6 +202,112 @@ export function decodeStateChangesRaw(stateChanges: any) { return allAccounts; } +export function decodeStateChangesFinal(stateChanges: any) { + const accounts = stateChanges.stateAccessesPerAccounts || {}; + const finalStates: Record = {}; + + for (const accountHex of Object.keys(accounts)) { + const address = bech32FromHex(accountHex); + const esdtOccured: Record = {}; + + const { stateAccess = [] } = accounts[accountHex] || {}; + const finalAccountChanges: AccountChanges = new AccountChanges({ + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false + }); + + let finalNewAccount = false; + + let finalAccountState: AccountState | undefined = undefined; + const finalEsdtStates = { + Fungible: [] as EsdtState[], + NonFungible: [] as EsdtState[], + NonFungibleV2: [] as EsdtState[], + SemiFungible: [] as EsdtState[], + MetaFungible: [] as EsdtState[], + DynamicNFT: [] as EsdtState[], + DynamicSFT: [] as EsdtState[], + DynamicMeta: [] as EsdtState[], + }; + + for (let i = stateAccess.length - 1; i >= 0; i--) { + const sa = stateAccess[i]; + + const currentAccountChanges = sa.accountChanges + || { + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false + }; + + (Object.entries(finalAccountChanges) as [keyof typeof finalAccountChanges, boolean][]).forEach( + ([key, value]) => { + finalAccountChanges[key] = value || currentAccountChanges[key]; + } + ); + + if (!finalNewAccount) { + const currentNewAccount = sa.accountChanges && sa.operation === StateAccessOperation.SaveAccount ? false : true; + finalNewAccount = currentNewAccount || finalNewAccount; + } + + const base64AccountData = sa.mainTrieVal; + if (base64AccountData && !finalAccountState) { + const bufAccountData = Buffer.from(base64AccountData, "base64"); + const decodedAccountData = getDecodedUserAccountData(bufAccountData); + if (decodedAccountData) { + finalAccountState = decodedAccountData; + } + } + + const dataTrieChanges = sa.dataTrieChanges; + if (dataTrieChanges) { + for (let i = dataTrieChanges.length - 1; i >= 0; i--) { + const dataTrieChange = dataTrieChanges[i]; + if (dataTrieChange.version === 0) { + console.warn(` Entry #${i}: unsupported dataTrieChanges version 0`); + } else { + const bufEsdtData = Buffer.from(dataTrieChange.val, "base64"); + + const decodedEsdtData = getDecodedEsdtData(bufEsdtData); + if (decodedEsdtData) { + const esdtId = decodedEsdtData.identifier; + if (!esdtOccured[esdtId]) { + const typeName = ESDTType[decodedEsdtData.type] as keyof typeof finalEsdtStates; // numeric -> string + if (typeName) { + finalEsdtStates[typeName].push(decodedEsdtData); + esdtOccured[esdtId] = true; + } + } + + } + } + } + + } + } + finalStates[address] = { + accountState: finalAccountState, + esdtState: finalEsdtStates, + accountChanges: finalAccountChanges, + isNewAccount: finalNewAccount, + }; + } + return finalStates; +} + + export function getFinalStates(stateChanges: Record) { const finalStates: Record = {}; @@ -282,7 +386,6 @@ export async function isDbValid(cacheService: CacheService): Promise { cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(0).key), cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(1).key), cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(2).key), - // cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(3).key), cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(4294967295).key), ]) as (string | undefined)[];; From ccba9350fef647150b905ec4c25bcef5f2f7b055 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 16 Sep 2025 16:07:19 +0300 Subject: [PATCH 21/90] improvements --- .../entities/state.changes.entity.ts | 38 +++++- .../state.changes.consumer.service.ts | 60 ++++++---- .../utils/state-changes.utils.ts | 109 +++++++----------- 3 files changed, 116 insertions(+), 91 deletions(-) diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts index 1bf4aba70..307eb4500 100644 --- a/src/state-changes/entities/state.changes.entity.ts +++ b/src/state-changes/entities/state.changes.entity.ts @@ -20,21 +20,29 @@ export class StateAccessPerAccountRaw { mainTrieKey!: string; mainTrieVal!: string; operation!: number; - accountChanges?: AccountChanges; + dataTrieChanges?: DataTrieChange[]; + accountChanges?: number; constructor(init?: Partial) { Object.assign(this, init); } } -export class StateChangesRaw { +export class DataTrieChange { + type!: number; + key!: string; + val!: string; + version!: number; +} + +export class BlockWithStateChangesRaw { hash!: string; shardID!: number; nonce!: number; timestampMs!: number; - stateAccessesPerAccounts?: Map; + stateAccessesPerAccounts!: Record; - constructor(init?: Partial) { + constructor(init?: Partial) { Object.assign(this, init); } } @@ -108,3 +116,25 @@ export class StateChanges { Object.assign(this, init); } } + +export enum AccountChangesRaw { + NoChange = 0, + NonceChanged = 1 << 0, // 1 + BalanceChanged = 1 << 1, // 2 + CodeHashChanged = 1 << 2, // 4 + RootHashChanged = 1 << 3, // 8 + DeveloperRewardChanged = 1 << 4, // 16 + OwnerAddressChanged = 1 << 5, // 32 + UserNameChanged = 1 << 6, // 64 + CodeMetadataChanged = 1 << 7 // 128 +} + +export enum StateAccessOperation { + NotSet = 0, + GetCode = 1 << 0, + SaveAccount = 1 << 1, + GetAccount = 1 << 2, + WriteCode = 1 << 3, + RemoveDataTrie = 1 << 4, + GetDataTrieValue = 1 << 5, +} \ No newline at end of file diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 590c72366..e7b54a50e 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -1,12 +1,12 @@ import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers"; -import { StateChanges, StateChangesRaw } from "./entities"; +import { BlockWithStateChangesRaw, StateChanges } from "./entities"; import { decodeStateChangesFinal } from "./utils/state-changes.utils"; import { AccountDetails, AccountDetailsRepository } from "src/common/indexer/db"; import { Injectable } from "@nestjs/common"; import { TokenWithBalance } from "src/endpoints/tokens/entities/token.with.balance"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; import { CacheInfo } from "src/utils/cache.info"; - +import { NftAccount } from "src/endpoints/nfts/entities/nft.account"; @Injectable() export class StateChangesConsumerService { constructor( @@ -17,26 +17,25 @@ export class StateChangesConsumerService { @CompetingRabbitConsumer({ exchange: 'state_accesses', - queueName: 'state-changes-test', - deadLetterExchange: 'state-changes-test_dlx', + queueName: 'state_changes_test', + deadLetterExchange: 'state_changes_test_dlx', }) - async consumeEvents(stateChanges: StateChangesRaw) { + async consumeEvents(blockWithStateChanges: BlockWithStateChangesRaw) { try { - console.time(`processing time shard ${stateChanges.shardID}`) - + console.time(`processing time shard ${blockWithStateChanges.shardID}`) console.time('decode time') - const finalStates = this.decodeStateChangesFinal(stateChanges); + const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates); + // console.dir(finalStates, { depth: null }) console.timeEnd('decode time') - await this.accountDetailsRepository.updateAccounts(transformedFinalStates); await this.cacheService.setRemote( - CacheInfo.LatestProcessedBlockTimestamp(stateChanges.shardID).key, - stateChanges.timestampMs, - CacheInfo.LatestProcessedBlockTimestamp(stateChanges.shardID).ttl, + CacheInfo.LatestProcessedBlockTimestamp(blockWithStateChanges.shardID).key, + blockWithStateChanges.timestampMs, + CacheInfo.LatestProcessedBlockTimestamp(blockWithStateChanges.shardID).ttl, ); - console.timeEnd(`processing time shard ${stateChanges.shardID}`) + console.timeEnd(`processing time shard ${blockWithStateChanges.shardID}`) } catch (error) { console.error(`Error consuming state changes:`, error); throw error; @@ -47,24 +46,41 @@ export class StateChangesConsumerService { // return decodeStateChangesRaw(stateChanges); // } - private decodeStateChangesFinal(stateChanges: StateChangesRaw) { - return decodeStateChangesFinal(stateChanges); + private decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { + return decodeStateChangesFinal(blockWithStateChanges); } private transformFinalStatesToDbFormat(finalStates: Record) { const transformed: AccountDetails[] = []; - for (const [_key, value] of Object.entries(finalStates)) { - const newAccountState = value.accountState; - const tokens = [...value.esdtState.Fungible]; // TODO: add other token types + for (const [_key, state] of Object.entries(finalStates)) { + const newAccountState = state.accountState; + const tokens = [ + ...state.esdtState.Fungible, + ...state.esdtState.SemiFungible, + ...state.esdtState.DynamicSFT, + ...state.esdtState.MetaFungible, + ...state.esdtState.DynamicMeta + ]; + + const nfts = [ + ...state.esdtState.NonFungible, + ...state.esdtState.NonFungibleV2, + ...state.esdtState.DynamicNFT + ]; if (newAccountState) { transformed.push(new AccountDetails({ ...newAccountState, - tokens: tokens.map(t => new TokenWithBalance({ - identifier: t.identifier, - nonce: parseInt(t.nonce), - balance: t.value, + tokens: tokens.map(token => new TokenWithBalance({ + identifier: token.identifier, + nonce: parseInt(token.nonce), + balance: token.value, })), + nfts: nfts.map(nft => new NftAccount({ + identifier: nft.identifier, + nonce: parseInt(nft.nonce), + // type: nft.type.toString(), + })) })); } diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 5e7f4fe8d..23a622254 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -3,21 +3,10 @@ import { UserAccountData } from "./user_account.pb"; import { ESDigitalToken } from "./esdt"; import { TrieLeafData } from "./trie_leaf_data"; import { TokenParser } from "./token.parser"; -import { AccountChanges, AccountState, EsdtState, ESDTType, StateChanges } from "../entities"; +import { AccountChanges, AccountChangesRaw, AccountState, BlockWithStateChangesRaw, EsdtState, ESDTType, StateAccessOperation, StateAccessPerAccountRaw, StateChanges } from "../entities"; import { CacheInfo } from "src/utils/cache.info"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; - -export enum StateAccessOperation { - NotSet = 0, - GetCode = 1, - SaveAccount = 2, - GetAccount = 4, - WriteCode = 8, - RemoveDataTrie = 16, - GetDataTrieValue = 32, -} - const bech32FromHex = (hex: any) => { const clean = hex.startsWith("0x") ? hex.slice(2) : hex; return Address.newFromHex(clean).toBech32(); @@ -86,11 +75,34 @@ function getDecodedUserAccountData(buf: any) { } } +export function decodeAccountChanges(flags: number | undefined): AccountChanges { + if (!flags) { + return new AccountChanges({ + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false, + }); + } + return new AccountChanges({ + nonceChanged: (flags & AccountChangesRaw.NonceChanged) !== 0, + balanceChanged: (flags & AccountChangesRaw.BalanceChanged) !== 0, + codeHashChanged: (flags & AccountChangesRaw.CodeHashChanged) !== 0, + rootHashChanged: (flags & AccountChangesRaw.RootHashChanged) !== 0, + developerRewardChanged: (flags & AccountChangesRaw.DeveloperRewardChanged) !== 0, + ownerAddressChanged: (flags & AccountChangesRaw.OwnerAddressChanged) !== 0, + userNameChanged: (flags & AccountChangesRaw.UserNameChanged) !== 0, + codeMetadataChanged: (flags & AccountChangesRaw.CodeMetadataChanged) !== 0, + }); +} function getDecodedEsdtData(buf: any) { try { const msgTrieLeafData: TrieLeafData = TrieLeafData.decode(buf); - // console.log(msgTrieLeafData) const bufEsdtData = msgTrieLeafData.value; const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufEsdtData); @@ -114,16 +126,16 @@ function getDecodedEsdtData(buf: any) { } } -export function decodeStateChangesRaw(stateChanges: any) { +export function decodeStateChangesRaw(blockWithStateChanges: BlockWithStateChangesRaw) { const allAccounts: Record = {}; - const accounts = stateChanges.stateAccessesPerAccounts || {}; + const accounts = blockWithStateChanges.stateAccessesPerAccounts || {}; for (const accountHex of Object.keys(accounts)) { const address = bech32FromHex(accountHex); const { stateAccess = [] } = accounts[accountHex] || {}; const allDecoded: Record = {}; - stateAccess.forEach((sa: any, i: any) => { + stateAccess.forEach((sa: StateAccessPerAccountRaw, i: number) => { const base64AccountData = sa.mainTrieVal; let decodedAccountData: any = null @@ -133,6 +145,7 @@ export function decodeStateChangesRaw(stateChanges: any) { } const dataTrieChanges = sa.dataTrieChanges; + let allDecodedEsdtData: any[] = []; if (dataTrieChanges) { for (const dataTrieChange of dataTrieChanges) { @@ -171,19 +184,9 @@ export function decodeStateChangesRaw(stateChanges: any) { } ); if (allDecoded[address] === undefined) allDecoded[address] = []; - const newAccount = sa.accountChanges && sa.operation === StateAccessOperation.SaveAccount ? false : true; - - const accountChanges = sa.accountChanges - || { - nonceChanged: false, - balanceChanged: false, - codeHashChanged: false, - rootHashChanged: false, - developerRewardChanged: false, - ownerAddressChanged: false, - userNameChanged: false, - codeMetadataChanged: false - }; + const newAccount = !sa.accountChanges && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; + + const accountChanges = decodeAccountChanges(sa.accountChanges); allDecoded[address].push({ entry: `Entry #${i}`, @@ -202,25 +205,16 @@ export function decodeStateChangesRaw(stateChanges: any) { return allAccounts; } -export function decodeStateChangesFinal(stateChanges: any) { - const accounts = stateChanges.stateAccessesPerAccounts || {}; +export function decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { + const accounts = blockWithStateChanges.stateAccessesPerAccounts; const finalStates: Record = {}; for (const accountHex of Object.keys(accounts)) { const address = bech32FromHex(accountHex); const esdtOccured: Record = {}; - const { stateAccess = [] } = accounts[accountHex] || {}; - const finalAccountChanges: AccountChanges = new AccountChanges({ - nonceChanged: false, - balanceChanged: false, - codeHashChanged: false, - rootHashChanged: false, - developerRewardChanged: false, - ownerAddressChanged: false, - userNameChanged: false, - codeMetadataChanged: false - }); + const { stateAccess } = accounts[accountHex] || {}; + let finalAccountChangesRaw: AccountChangesRaw = AccountChangesRaw.NoChange; let finalNewAccount = false; @@ -238,27 +232,13 @@ export function decodeStateChangesFinal(stateChanges: any) { for (let i = stateAccess.length - 1; i >= 0; i--) { const sa = stateAccess[i]; - - const currentAccountChanges = sa.accountChanges - || { - nonceChanged: false, - balanceChanged: false, - codeHashChanged: false, - rootHashChanged: false, - developerRewardChanged: false, - ownerAddressChanged: false, - userNameChanged: false, - codeMetadataChanged: false - }; - - (Object.entries(finalAccountChanges) as [keyof typeof finalAccountChanges, boolean][]).forEach( - ([key, value]) => { - finalAccountChanges[key] = value || currentAccountChanges[key]; - } - ); + const currentAccountChangesRaw = sa.accountChanges; + if (currentAccountChangesRaw) { + finalAccountChangesRaw = finalAccountChangesRaw | currentAccountChangesRaw; + } if (!finalNewAccount) { - const currentNewAccount = sa.accountChanges && sa.operation === StateAccessOperation.SaveAccount ? false : true; + const currentNewAccount = !sa.accountChanges && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; finalNewAccount = currentNewAccount || finalNewAccount; } @@ -277,7 +257,8 @@ export function decodeStateChangesFinal(stateChanges: any) { const dataTrieChange = dataTrieChanges[i]; if (dataTrieChange.version === 0) { console.warn(` Entry #${i}: unsupported dataTrieChanges version 0`); - } else { + } else if (dataTrieChange.val) { + const bufEsdtData = Buffer.from(dataTrieChange.val, "base64"); const decodedEsdtData = getDecodedEsdtData(bufEsdtData); @@ -300,7 +281,7 @@ export function decodeStateChangesFinal(stateChanges: any) { finalStates[address] = { accountState: finalAccountState, esdtState: finalEsdtStates, - accountChanges: finalAccountChanges, + accountChanges: decodeAccountChanges(finalAccountChangesRaw), isNewAccount: finalNewAccount, }; } @@ -347,7 +328,6 @@ export function getFinalStates(stateChanges: Record) { finalNewAccount = finalNewAccount ? finalNewAccount : currentNewAccount; finalAccountState = currentAccountState; - // console.log(entry); (Object.entries(finalAccountChanges) as [keyof typeof finalAccountChanges, boolean][]).forEach( ([key, value]) => { @@ -366,7 +346,6 @@ export function getFinalStates(stateChanges: Record) { } ); - } finalStates[address] = { From 65a883116128b48002cb27f6da8b4b4dd4acde72 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 16 Sep 2025 17:24:05 +0300 Subject: [PATCH 22/90] fix --- src/state-changes/utils/state-changes.utils.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 23a622254..11bbe3d7d 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -101,13 +101,21 @@ export function decodeAccountChanges(flags: number | undefined): AccountChanges } function getDecodedEsdtData(buf: any) { + const esdtPrefix = 'ELDRONDesdt'; try { const msgTrieLeafData: TrieLeafData = TrieLeafData.decode(buf); const bufEsdtData = msgTrieLeafData.value; const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufEsdtData); const valueBigInt: bigint = decodeMxSignMagBigInt(msgEsdtData.Value); - const key = Buffer.from(bytesToHex(msgTrieLeafData.key), "hex").toString().slice('ELRONDesdt'.length); + const keyRaw = Buffer.from(bytesToHex(msgTrieLeafData.key), "hex").toString(); + let key = keyRaw; + if (keyRaw.startsWith(esdtPrefix)) { + key = keyRaw.slice(esdtPrefix.length); + } else { + //TODO: handle if needed + return null; + } const [identifier, nonce] = TokenParser.extractTokenIDAndNonceFromTokenStorageKey(key); return { @@ -184,7 +192,7 @@ export function decodeStateChangesRaw(blockWithStateChanges: BlockWithStateChang } ); if (allDecoded[address] === undefined) allDecoded[address] = []; - const newAccount = !sa.accountChanges && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; + const newAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; const accountChanges = decodeAccountChanges(sa.accountChanges); @@ -238,7 +246,7 @@ export function decodeStateChangesFinal(blockWithStateChanges: BlockWithStateCha } if (!finalNewAccount) { - const currentNewAccount = !sa.accountChanges && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; + const currentNewAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; finalNewAccount = currentNewAccount || finalNewAccount; } From f77e1ea2f6ec0864c7a034ca9cb9d17197d05cd4 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 17 Sep 2025 10:26:15 +0300 Subject: [PATCH 23/90] fix typo --- src/state-changes/utils/state-changes.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 11bbe3d7d..0b8af92ce 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -101,7 +101,7 @@ export function decodeAccountChanges(flags: number | undefined): AccountChanges } function getDecodedEsdtData(buf: any) { - const esdtPrefix = 'ELDRONDesdt'; + const esdtPrefix = 'ELRONDesdt'; try { const msgTrieLeafData: TrieLeafData = TrieLeafData.decode(buf); const bufEsdtData = msgTrieLeafData.value; From 8c1b2842f1023329619806c5bbfb49b1c19389d5 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 17 Sep 2025 16:05:40 +0300 Subject: [PATCH 24/90] fix esdts nonce & identifier --- src/state-changes/utils/state-changes.utils.ts | 6 +++--- src/state-changes/utils/token.parser.ts | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 0b8af92ce..fc9229d00 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -116,12 +116,12 @@ function getDecodedEsdtData(buf: any) { //TODO: handle if needed return null; } - const [identifier, nonce] = TokenParser.extractTokenIDAndNonceFromTokenStorageKey(key); + const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(key); return { address: bech32FromHex(bytesToHex(msgTrieLeafData.address)), - identifier, - nonce, + identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, + nonce: parseInt(nonceHex, 16).toString(), type: msgEsdtData.Type, value: valueBigInt.toString(), propertiesHex: bytesToHex(msgEsdtData.Properties), diff --git a/src/state-changes/utils/token.parser.ts b/src/state-changes/utils/token.parser.ts index d49b78bb6..7be799e6c 100644 --- a/src/state-changes/utils/token.parser.ts +++ b/src/state-changes/utils/token.parser.ts @@ -13,14 +13,14 @@ export class TokenParser { * "ALC-1q2w3e" -> ["ALC-1q2w3e", "0"] (fungible, no nonce) * "ALC-2w3e4rX" -> ["ALC-2w3e4r", "X"] (non-fungible, nonce = "X") */ - public static extractTokenIDAndNonceFromTokenStorageKey( + public static extractTokenIDAndNonceHexFromTokenStorageKey( tokenKey: string ): [string, string] { const token = tokenKey; const indexOfFirstHyphen = token.indexOf(this.separatorChar); if (indexOfFirstHyphen < 0) { - return [tokenKey, "0"]; + return [tokenKey, "00"]; } const tokenTicker = token.slice(0, indexOfFirstHyphen); @@ -35,11 +35,11 @@ export class TokenParser { randomSequencePlusNonce.length === 0; if (areTickerAndRandomSequenceInvalid) { - return [tokenKey, "0"]; + return [tokenKey, "00"]; } if (randomSequencePlusNonce.length < this.esdtTickerNumRandChars + 1) { - return [tokenKey, "0"]; + return [tokenKey, "00"]; } // ALC-1q2w3eX -> X is the nonce @@ -47,7 +47,9 @@ export class TokenParser { const numCharsSinceNonce = token.length - nonceStr.length; const tokenID = token.slice(0, numCharsSinceNonce); - - return [tokenID, nonceStr || "0"]; + if (nonceStr) { + return [tokenID, Buffer.from(nonceStr).toString('hex')] + } + return [tokenID, "00"]; } } From 7fd1e89d2fe38a36430fbb392e380ed035623e22 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 19 Sep 2025 12:09:34 +0300 Subject: [PATCH 25/90] parse types/subtypes + delete esdt support --- .../account.details.repository.ts | 209 ++++++++++-------- src/common/rabbitmq/rabbitmq.module.ts | 1 - .../entities/state.changes.entity.ts | 7 +- .../state.changes.consumer.service.ts | 104 +++++++-- .../utils/state-changes.utils.ts | 28 +-- 5 files changed, 226 insertions(+), 123 deletions(-) diff --git a/src/common/indexer/db/repositories/account.details.repository.ts b/src/common/indexer/db/repositories/account.details.repository.ts index 404034611..55f3de85b 100644 --- a/src/common/indexer/db/repositories/account.details.repository.ts +++ b/src/common/indexer/db/repositories/account.details.repository.ts @@ -322,124 +322,157 @@ export class AccountDetailsRepository { // --- tokens --- if (accountDetailed.tokens?.length) { - updateOps.push({ - $set: { - tokens: { - $let: { - vars: { newTokens: accountDetailed.tokens }, - in: { - $concatArrays: [ - { - $map: { - input: { $ifNull: ["$tokens", []] }, // asigurăm array gol dacă tokens nu există - as: "t", - in: { - $let: { - vars: { - updated: { - $filter: { - input: "$$newTokens", - cond: { $eq: ["$$this.identifier", "$$t.identifier"] }, + // use reduce or partition loadash instead of 2 iterations over array with filter + const tokensToRemove = accountDetailed.tokens + .filter((t) => t.balance === '0') + .map((t) => t.identifier); + + if (tokensToRemove.length) { + updateOps.push({ + $pull: { + tokens: { identifier: { $in: tokensToRemove } }, + }, + }); + } + + const tokensToUpsert = accountDetailed.tokens.filter((t) => t.balance !== '0'); + if (tokensToUpsert.length) { + + updateOps.push({ + $set: { + tokens: { + $let: { + vars: { newTokens: tokensToUpsert }, + in: { + $concatArrays: [ + { + $map: { + input: { $ifNull: ["$tokens", []] }, // empty array if tokens array doesn't exist + as: "t", + in: { + $let: { + vars: { + updated: { + $filter: { + input: "$$newTokens", + cond: { $eq: ["$$this.identifier", "$$t.identifier"] }, + }, }, }, - }, - in: { - $cond: [ - { $gt: [{ $size: "$$updated" }, 0] }, - { $arrayElemAt: ["$$updated", 0] }, - "$$t", - ], + in: { + $cond: [ + { $gt: [{ $size: "$$updated" }, 0] }, + { $arrayElemAt: ["$$updated", 0] }, + "$$t", + ], + }, }, }, }, }, - }, - { - $filter: { - input: "$$newTokens", - cond: { - $not: { - $in: [ - "$$this.identifier", - { - $map: { - input: { $ifNull: ["$tokens", []] }, - as: "t", - in: "$$t.identifier", + { + $filter: { + input: "$$newTokens", + cond: { + $not: { + $in: [ + "$$this.identifier", + { + $map: { + input: { $ifNull: ["$tokens", []] }, + as: "t", + in: "$$t.identifier", + }, }, - }, - ], + ], + }, }, }, }, - }, - ], + ], + }, }, }, }, - }, - }); + }); + } } // --- nfts (analog tokens) --- if (accountDetailed.nfts?.length) { - updateOps.push({ - $set: { - nfts: { - $let: { - vars: { newNfts: accountDetailed.nfts }, - in: { - $concatArrays: [ - { - $map: { - input: { $ifNull: ["$nfts", []] }, // asigurăm array gol dacă nfts nu există - as: "n", - in: { - $let: { - vars: { - updated: { - $filter: { - input: "$$newNfts", - cond: { $eq: ["$$this.identifier", "$$n.identifier"] }, + // use reduce or partition loadash instead of 2 iterations over array with filter + const nftsToRemove = accountDetailed.nfts + .filter((n) => n.balance === '0') + .map((n) => n.identifier); + + if (nftsToRemove.length) { + updateOps.push({ + $pull: { + nfts: { identifier: { $in: nftsToRemove } }, + }, + }); + } + + const nftsToUpsert = accountDetailed.nfts.filter((n) => n.balance !== '0'); + if (nftsToUpsert.length) { + updateOps.push({ + $set: { + nfts: { + $let: { + vars: { newNfts: nftsToUpsert }, + in: { + $concatArrays: [ + { + $map: { + input: { $ifNull: ["$nfts", []] }, // empty array if nfts array doesn't exist + as: "n", + in: { + $let: { + vars: { + updated: { + $filter: { + input: "$$newNfts", + cond: { $eq: ["$$this.identifier", "$$n.identifier"] }, + }, }, }, - }, - in: { - $cond: [ - { $gt: [{ $size: "$$updated" }, 0] }, - { $arrayElemAt: ["$$updated", 0] }, - "$$n", - ], + in: { + $cond: [ + { $gt: [{ $size: "$$updated" }, 0] }, + { $arrayElemAt: ["$$updated", 0] }, + "$$n", + ], + }, }, }, }, }, - }, - { - $filter: { - input: "$$newNfts", - cond: { - $not: { - $in: [ - "$$this.identifier", - { - $map: { - input: { $ifNull: ["$nfts", []] }, - as: "n", - in: "$$n.identifier", + { + $filter: { + input: "$$newNfts", + cond: { + $not: { + $in: [ + "$$this.identifier", + { + $map: { + input: { $ifNull: ["$nfts", []] }, + as: "n", + in: "$$n.identifier", + }, }, - }, - ], + ], + }, }, }, }, - }, - ], + ], + }, }, }, }, - }, - }); + }); + } } totalOperations += updateOps.length; return { diff --git a/src/common/rabbitmq/rabbitmq.module.ts b/src/common/rabbitmq/rabbitmq.module.ts index ac5457a63..e85b77145 100644 --- a/src/common/rabbitmq/rabbitmq.module.ts +++ b/src/common/rabbitmq/rabbitmq.module.ts @@ -38,7 +38,6 @@ export class RabbitMqModule { type: 'fanout', options: {}, uri: apiConfigService.getEventsNotifierUrl(), - prefetchCount: 1, }; }, }), diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts index 307eb4500..430ea85eb 100644 --- a/src/state-changes/entities/state.changes.entity.ts +++ b/src/state-changes/entities/state.changes.entity.ts @@ -33,6 +33,7 @@ export class DataTrieChange { key!: string; val!: string; version!: number; + operation!: DataTrieChangeOperation; } export class BlockWithStateChangesRaw { @@ -64,7 +65,6 @@ export class AccountState { } export class EsdtState { - address!: string; identifier!: string; nonce!: string; type!: ESDTType; @@ -137,4 +137,9 @@ export enum StateAccessOperation { WriteCode = 1 << 3, RemoveDataTrie = 1 << 4, GetDataTrieValue = 1 << 5, +} + +export enum DataTrieChangeOperation { + NotDelete = 0, + Delete = 1, } \ No newline at end of file diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index e7b54a50e..7dffb920b 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -1,5 +1,5 @@ import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers"; -import { BlockWithStateChangesRaw, StateChanges } from "./entities"; +import { BlockWithStateChangesRaw, ESDTType, StateChanges } from "./entities"; import { decodeStateChangesFinal } from "./utils/state-changes.utils"; import { AccountDetails, AccountDetailsRepository } from "src/common/indexer/db"; import { Injectable } from "@nestjs/common"; @@ -7,6 +7,10 @@ import { TokenWithBalance } from "src/endpoints/tokens/entities/token.with.balan import { CacheService } from "@multiversx/sdk-nestjs-cache"; import { CacheInfo } from "src/utils/cache.info"; import { NftAccount } from "src/endpoints/nfts/entities/nft.account"; +import { TokenType } from "src/common/indexer/entities"; +import { NftType } from "src/endpoints/nfts/entities/nft.type"; +import { NftSubType } from "src/endpoints/nfts/entities/nft.sub.type"; + @Injectable() export class StateChangesConsumerService { constructor( @@ -17,17 +21,25 @@ export class StateChangesConsumerService { @CompetingRabbitConsumer({ exchange: 'state_accesses', - queueName: 'state_changes_test', - deadLetterExchange: 'state_changes_test_dlx', + queueName: 'state_changes_test-stefan', + deadLetterExchange: 'state_changes_test_dlx-stefan', }) async consumeEvents(blockWithStateChanges: BlockWithStateChangesRaw) { try { - console.time(`processing time shard ${blockWithStateChanges.shardID}`) - console.time('decode time') + const start = Date.now(); // ms la început + // console.time(`processing time shard ${blockWithStateChanges.shardID}`) + // console.time('decode time') + // console.dir(blockWithStateChanges, { depth: null }) + const startDecoding = start; const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); - const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates); - // console.dir(finalStates, { depth: null }) - console.timeEnd('decode time') + const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID); + + const endDecoding = Date.now(); + const decodingDuration = endDecoding - startDecoding; + + console.dir(finalStates, { depth: null }) + // console.timeEnd('decode time') + this.accountDetailsRepository await this.accountDetailsRepository.updateAccounts(transformedFinalStates); await this.cacheService.setRemote( @@ -35,7 +47,14 @@ export class StateChangesConsumerService { blockWithStateChanges.timestampMs, CacheInfo.LatestProcessedBlockTimestamp(blockWithStateChanges.shardID).ttl, ); - console.timeEnd(`processing time shard ${blockWithStateChanges.shardID}`) + // console.timeEnd(`processing time shard ${blockWithStateChanges.shardID}`) + const end = Date.now(); // ms la final + const duration = end - start; + if (duration > 10) { + // console.dir(finalStates, { depth: null }) + console.log(`decoding duration: ${decodingDuration}`) + console.log(`processing time shard ${blockWithStateChanges.shardID}: ${duration}ms`); + } } catch (error) { console.error(`Error consuming state changes:`, error); throw error; @@ -50,36 +69,44 @@ export class StateChangesConsumerService { return decodeStateChangesFinal(blockWithStateChanges); } - private transformFinalStatesToDbFormat(finalStates: Record) { + private transformFinalStatesToDbFormat(finalStates: Record, shardID: number) { const transformed: AccountDetails[] = []; for (const [_key, state] of Object.entries(finalStates)) { const newAccountState = state.accountState; + const tokens = [ ...state.esdtState.Fungible, - ...state.esdtState.SemiFungible, - ...state.esdtState.DynamicSFT, - ...state.esdtState.MetaFungible, - ...state.esdtState.DynamicMeta + ]; const nfts = [ ...state.esdtState.NonFungible, ...state.esdtState.NonFungibleV2, - ...state.esdtState.DynamicNFT + ...state.esdtState.DynamicNFT, + ...state.esdtState.SemiFungible, + ...state.esdtState.DynamicSFT, + ...state.esdtState.MetaFungible, + ...state.esdtState.DynamicMeta ]; if (newAccountState) { transformed.push(new AccountDetails({ ...newAccountState, + shard: shardID, tokens: tokens.map(token => new TokenWithBalance({ identifier: token.identifier, nonce: parseInt(token.nonce), balance: token.value, + type: this.parseEsdtType(token.type) as TokenType, + subType: NftSubType.None, })), nfts: nfts.map(nft => new NftAccount({ identifier: nft.identifier, nonce: parseInt(nft.nonce), - // type: nft.type.toString(), + type: this.parseEsdtType(nft.type) as NftType, + subType: this.parseEsdtSubtype(nft.type), + collection: nft.identifier.replace(/-[^-]*$/, ''), // delete everything after last `-` character inclusive + balance: nft.value, })) })); } @@ -88,4 +115,47 @@ export class StateChangesConsumerService { return transformed; } -} \ No newline at end of file + + private parseEsdtType(type: ESDTType): TokenType | NftType { + switch (type) { + case ESDTType.Fungible: + return TokenType.FungibleESDT; + + case ESDTType.NonFungible: + case ESDTType.DynamicNFT: + case ESDTType.NonFungibleV2: + return NftType.NonFungibleESDT; + + case ESDTType.SemiFungible: + case ESDTType.DynamicSFT: + return NftType.SemiFungibleESDT; + case ESDTType.MetaFungible: + case ESDTType.DynamicMeta: + return NftType.MetaESDT; + } + } + + private parseEsdtSubtype(type: ESDTType): NftSubType { + switch (type) { + case ESDTType.Fungible: + return NftSubType.None; + + case ESDTType.NonFungible: + return NftSubType.NonFungibleESDT; + case ESDTType.DynamicNFT: + return NftSubType.DynamicNonFungibleESDT; + case ESDTType.NonFungibleV2: + return NftSubType.NonFungibleESDTv2; + + case ESDTType.SemiFungible: + return NftSubType.SemiFungibleESDT; + case ESDTType.DynamicSFT: + return NftSubType.DynamicSemiFungibleESDT; + case ESDTType.MetaFungible: + return NftSubType.MetaESDT; + case ESDTType.DynamicMeta: + return NftSubType.DynamicMetaESDT; + } + } + +} diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index fc9229d00..0ccd25c26 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -1,9 +1,8 @@ import { Address } from "@multiversx/sdk-core/out"; import { UserAccountData } from "./user_account.pb"; import { ESDigitalToken } from "./esdt"; -import { TrieLeafData } from "./trie_leaf_data"; import { TokenParser } from "./token.parser"; -import { AccountChanges, AccountChangesRaw, AccountState, BlockWithStateChangesRaw, EsdtState, ESDTType, StateAccessOperation, StateAccessPerAccountRaw, StateChanges } from "../entities"; +import { AccountChanges, AccountChangesRaw, AccountState, BlockWithStateChangesRaw, DataTrieChangeOperation, EsdtState, ESDTType, StateAccessOperation, StateAccessPerAccountRaw, StateChanges } from "../entities"; import { CacheInfo } from "src/utils/cache.info"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; @@ -100,15 +99,13 @@ export function decodeAccountChanges(flags: number | undefined): AccountChanges }); } -function getDecodedEsdtData(buf: any) { +function getDecodedEsdtData(bufTrieLeafValue: any, trieLeafKey: string) { const esdtPrefix = 'ELRONDesdt'; try { - const msgTrieLeafData: TrieLeafData = TrieLeafData.decode(buf); - const bufEsdtData = msgTrieLeafData.value; - const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufEsdtData); + const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufTrieLeafValue); const valueBigInt: bigint = decodeMxSignMagBigInt(msgEsdtData.Value); - const keyRaw = Buffer.from(bytesToHex(msgTrieLeafData.key), "hex").toString(); + const keyRaw = Buffer.from(trieLeafKey, "base64").toString(); let key = keyRaw; if (keyRaw.startsWith(esdtPrefix)) { key = keyRaw.slice(esdtPrefix.length); @@ -119,7 +116,6 @@ function getDecodedEsdtData(buf: any) { const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(key); return { - address: bech32FromHex(bytesToHex(msgTrieLeafData.address)), identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, nonce: parseInt(nonceHex, 16).toString(), type: msgEsdtData.Type, @@ -160,9 +156,9 @@ export function decodeStateChangesRaw(blockWithStateChanges: BlockWithStateChang if (dataTrieChange.version === 0) { console.warn(`Entry #${i}: unsupported dataTrieChanges version 0`); } else { - const bufEsdtData = Buffer.from(dataTrieChange.val, "base64"); + const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); + const decodedEsdtData = getDecodedEsdtData(bufTrieLeafValue, dataTrieChange.key); - const decodedEsdtData = getDecodedEsdtData(bufEsdtData); if (decodedEsdtData) { allDecodedEsdtData.push(decodedEsdtData); } @@ -242,7 +238,7 @@ export function decodeStateChangesFinal(blockWithStateChanges: BlockWithStateCha const sa = stateAccess[i]; const currentAccountChangesRaw = sa.accountChanges; if (currentAccountChangesRaw) { - finalAccountChangesRaw = finalAccountChangesRaw | currentAccountChangesRaw; + finalAccountChangesRaw |= currentAccountChangesRaw; } if (!finalNewAccount) { @@ -266,20 +262,20 @@ export function decodeStateChangesFinal(blockWithStateChanges: BlockWithStateCha if (dataTrieChange.version === 0) { console.warn(` Entry #${i}: unsupported dataTrieChanges version 0`); } else if (dataTrieChange.val) { - - const bufEsdtData = Buffer.from(dataTrieChange.val, "base64"); - - const decodedEsdtData = getDecodedEsdtData(bufEsdtData); + const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); + const decodedEsdtData = getDecodedEsdtData(bufTrieLeafValue, dataTrieChange.key); if (decodedEsdtData) { const esdtId = decodedEsdtData.identifier; if (!esdtOccured[esdtId]) { const typeName = ESDTType[decodedEsdtData.type] as keyof typeof finalEsdtStates; // numeric -> string if (typeName) { + if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { + decodedEsdtData.value = '0'; + } finalEsdtStates[typeName].push(decodedEsdtData); esdtOccured[esdtId] = true; } } - } } } From ffb26a2bd566ec48860d71c5b181b77fd5bfdabb Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 19 Sep 2025 12:44:52 +0300 Subject: [PATCH 26/90] fix --- src/state-changes/utils/state-changes.utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 0ccd25c26..d952ad945 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -160,6 +160,9 @@ export function decodeStateChangesRaw(blockWithStateChanges: BlockWithStateChang const decodedEsdtData = getDecodedEsdtData(bufTrieLeafValue, dataTrieChange.key); if (decodedEsdtData) { + if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { + decodedEsdtData.value = '0'; + } allDecodedEsdtData.push(decodedEsdtData); } } From f921fec88ea88730004e226ae2ac1ab38cf82035 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Mon, 22 Sep 2025 10:07:01 +0300 Subject: [PATCH 27/90] fix remove esdt --- .../account.details.repository.ts | 118 +++++++++++------- 1 file changed, 72 insertions(+), 46 deletions(-) diff --git a/src/common/indexer/db/repositories/account.details.repository.ts b/src/common/indexer/db/repositories/account.details.repository.ts index 55f3de85b..8f694b3e2 100644 --- a/src/common/indexer/db/repositories/account.details.repository.ts +++ b/src/common/indexer/db/repositories/account.details.repository.ts @@ -303,42 +303,43 @@ export class AccountDetailsRepository { try { if (!accounts.length) return []; let totalOperations = 0; - const operations = accounts.map((accountDetailed) => { - const updateFields: any = {}; + + const operations: any[] = []; + + for (const accountDetailed of accounts) { + const updatePipeline: any[] = []; + const pulls: any[] = []; + + // --- helper --- const isValidValue = (value: any): boolean => value !== undefined && value !== null; + // --- simple fields --- + const updateFields: any = {}; Object.entries(accountDetailed).forEach(([key, value]) => { if (isValidValue(value) && key !== "tokens" && key !== "nfts") { updateFields[key as keyof AccountDetails] = value; } }); - - const updateOps: any[] = []; - if (Object.keys(updateFields).length > 0) { - updateOps.push({ $set: updateFields }); + updatePipeline.push({ $set: updateFields }); } // --- tokens --- - if (accountDetailed.tokens?.length) { - // use reduce or partition loadash instead of 2 iterations over array with filter - const tokensToRemove = accountDetailed.tokens - .filter((t) => t.balance === '0') - .map((t) => t.identifier); + let tokensToRemove: string[] = []; + let tokensToUpsert: any[] = []; - if (tokensToRemove.length) { - updateOps.push({ - $pull: { - tokens: { identifier: { $in: tokensToRemove } }, - }, - }); + if (accountDetailed.tokens?.length) { + for (const t of accountDetailed.tokens) { + if (t.balance === '0') { + tokensToRemove.push(t.identifier); + } else { + tokensToUpsert.push(t); + } } - const tokensToUpsert = accountDetailed.tokens.filter((t) => t.balance !== '0'); if (tokensToUpsert.length) { - - updateOps.push({ + updatePipeline.push({ $set: { tokens: { $let: { @@ -347,7 +348,7 @@ export class AccountDetailsRepository { $concatArrays: [ { $map: { - input: { $ifNull: ["$tokens", []] }, // empty array if tokens array doesn't exist + input: { $ifNull: ["$tokens", []] }, as: "t", in: { $let: { @@ -396,26 +397,32 @@ export class AccountDetailsRepository { }, }); } - } - // --- nfts (analog tokens) --- - if (accountDetailed.nfts?.length) { - // use reduce or partition loadash instead of 2 iterations over array with filter - const nftsToRemove = accountDetailed.nfts - .filter((n) => n.balance === '0') - .map((n) => n.identifier); - - if (nftsToRemove.length) { - updateOps.push({ - $pull: { - nfts: { identifier: { $in: nftsToRemove } }, + if (tokensToRemove.length) { + pulls.push({ + updateOne: { + filter: { address: accountDetailed.address }, + update: { $pull: { tokens: { identifier: { $in: tokensToRemove } } } }, }, }); } + } + + // --- nfts --- + let nftsToRemove: string[] = []; + let nftsToUpsert: any[] = []; + + if (accountDetailed.nfts?.length) { + for (const n of accountDetailed.nfts) { + if (n.balance === '0') { + nftsToRemove.push(n.identifier); + } else { + nftsToUpsert.push(n); + } + } - const nftsToUpsert = accountDetailed.nfts.filter((n) => n.balance !== '0'); if (nftsToUpsert.length) { - updateOps.push({ + updatePipeline.push({ $set: { nfts: { $let: { @@ -424,7 +431,7 @@ export class AccountDetailsRepository { $concatArrays: [ { $map: { - input: { $ifNull: ["$nfts", []] }, // empty array if nfts array doesn't exist + input: { $ifNull: ["$nfts", []] }, as: "n", in: { $let: { @@ -473,17 +480,37 @@ export class AccountDetailsRepository { }, }); } + + if (nftsToRemove.length) { + pulls.push({ + updateOne: { + filter: { address: accountDetailed.address }, + update: { $pull: { nfts: { identifier: { $in: nftsToRemove } } } }, + }, + }); + } } - totalOperations += updateOps.length; - return { - updateOne: { - filter: { address: accountDetailed.address }, - update: updateOps, - upsert: true, - }, - }; - }); + + if (updatePipeline.length > 0) { + operations.push({ + updateOne: { + filter: { address: accountDetailed.address }, + update: updatePipeline, // <<--- pipeline array + upsert: true, + }, + }); + totalOperations++; + } + + // --- push pulls --- + if (pulls.length > 0) { + operations.push(...pulls); + totalOperations += pulls.length; + } + } + console.log('number of write operations:', totalOperations); + const result = await this.accountDetailsModel.bulkWrite(operations, { ordered: true, }); @@ -494,5 +521,4 @@ export class AccountDetailsRepository { throw error; } } - } \ No newline at end of file From 855e10f832d910764ed0ec42aada57d70349337b Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Mon, 22 Sep 2025 15:35:08 +0300 Subject: [PATCH 28/90] fix decoder --- .../entities/state.changes.entity.ts | 2 +- .../utils/state-changes.utils.ts | 53 ++++++++++--------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts index 430ea85eb..a891deb48 100644 --- a/src/state-changes/entities/state.changes.entity.ts +++ b/src/state-changes/entities/state.changes.entity.ts @@ -31,7 +31,7 @@ export class StateAccessPerAccountRaw { export class DataTrieChange { type!: number; key!: string; - val!: string; + val!: any; version!: number; operation!: DataTrieChangeOperation; } diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index d952ad945..3fa6814ae 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -2,7 +2,7 @@ import { Address } from "@multiversx/sdk-core/out"; import { UserAccountData } from "./user_account.pb"; import { ESDigitalToken } from "./esdt"; import { TokenParser } from "./token.parser"; -import { AccountChanges, AccountChangesRaw, AccountState, BlockWithStateChangesRaw, DataTrieChangeOperation, EsdtState, ESDTType, StateAccessOperation, StateAccessPerAccountRaw, StateChanges } from "../entities"; +import { AccountChanges, AccountChangesRaw, AccountState, BlockWithStateChangesRaw, DataTrieChange, DataTrieChangeOperation, EsdtState, ESDTType, StateAccessOperation, StateAccessPerAccountRaw, StateChanges } from "../entities"; import { CacheInfo } from "src/utils/cache.info"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; @@ -13,6 +13,7 @@ const bech32FromHex = (hex: any) => { const bech32FromBytes = (u8: any) => (u8 && u8.length ? Address.newFromHex(bytesToHex(u8)).toBech32() : ""); const bytesToHex = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("hex") : ""); +const bytesToBase64 = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("base64") : ""); // const bytesToString = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("utf8") : ""); const longToString = (v: any) => v == null ? "" : (typeof v === "object" && typeof v.toString === "function" ? v.toString() : String(v)); @@ -63,8 +64,8 @@ function getDecodedUserAccountData(buf: any) { developerReward: devReward.toString(), address: address, ownerAddress: ownerAddress, - codeHash: bytesToHex(msg.CodeHash), - rootHash: bytesToHex(msg.RootHash), + codeHash: bytesToBase64(msg.CodeHash), + rootHash: bytesToBase64(msg.RootHash), userName: bytesToHex(msg.UserName), codeMetadata: bytesToHex(msg.CodeMetadata), }; @@ -99,33 +100,37 @@ export function decodeAccountChanges(flags: number | undefined): AccountChanges }); } -function getDecodedEsdtData(bufTrieLeafValue: any, trieLeafKey: string) { +function getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { + const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); const esdtPrefix = 'ELRONDesdt'; try { - const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufTrieLeafValue); - - const valueBigInt: bigint = decodeMxSignMagBigInt(msgEsdtData.Value); - const keyRaw = Buffer.from(trieLeafKey, "base64").toString(); + const keyRaw = Buffer.from(dataTrieChange.key, "base64").toString(); let key = keyRaw; if (keyRaw.startsWith(esdtPrefix)) { key = keyRaw.slice(esdtPrefix.length); + const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufTrieLeafValue as Uint8Array); + + const valueBigInt: bigint = decodeMxSignMagBigInt(msgEsdtData.Value); + const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(key); + + return { + identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, + nonce: parseInt(nonceHex, 16).toString(), + type: msgEsdtData.Type, + value: valueBigInt.toString(), + propertiesHex: bytesToHex(msgEsdtData.Properties), + reservedHex: bytesToHex(msgEsdtData.Reserved), + tokenMetaData: msgEsdtData.TokenMetaData ?? null, + }; } else { //TODO: handle if needed + return null; } - const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(key); - - return { - identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, - nonce: parseInt(nonceHex, 16).toString(), - type: msgEsdtData.Type, - value: valueBigInt.toString(), - propertiesHex: bytesToHex(msgEsdtData.Properties), - reservedHex: bytesToHex(msgEsdtData.Reserved), - tokenMetaData: msgEsdtData.TokenMetaData ?? null, - }; } catch (e: any) { console.warn(`Could not decode as EsdtData: ${e.message}`); + console.log(address, ':') + console.dir(dataTrieChange) return null; } } @@ -147,6 +152,7 @@ export function decodeStateChangesRaw(blockWithStateChanges: BlockWithStateChang const bufAccountData = Buffer.from(base64AccountData, "base64"); decodedAccountData = getDecodedUserAccountData(bufAccountData); } + const dataTrieChanges = sa.dataTrieChanges; @@ -156,8 +162,8 @@ export function decodeStateChangesRaw(blockWithStateChanges: BlockWithStateChang if (dataTrieChange.version === 0) { console.warn(`Entry #${i}: unsupported dataTrieChanges version 0`); } else { - const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); - const decodedEsdtData = getDecodedEsdtData(bufTrieLeafValue, dataTrieChange.key); + + const decodedEsdtData = getDecodedEsdtData(address, dataTrieChange); if (decodedEsdtData) { if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { @@ -263,10 +269,9 @@ export function decodeStateChangesFinal(blockWithStateChanges: BlockWithStateCha for (let i = dataTrieChanges.length - 1; i >= 0; i--) { const dataTrieChange = dataTrieChanges[i]; if (dataTrieChange.version === 0) { - console.warn(` Entry #${i}: unsupported dataTrieChanges version 0`); + console.warn(`Unsupported dataTrieChanges version 0`); } else if (dataTrieChange.val) { - const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); - const decodedEsdtData = getDecodedEsdtData(bufTrieLeafValue, dataTrieChange.key); + const decodedEsdtData = getDecodedEsdtData(address, dataTrieChange); if (decodedEsdtData) { const esdtId = decodedEsdtData.identifier; if (!esdtOccured[esdtId]) { From f42a1d9c73331100e853cdb090304d02c203d056 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Mon, 22 Sep 2025 16:30:57 +0300 Subject: [PATCH 29/90] parse sc code metadata --- .../state.changes.consumer.service.ts | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 7dffb920b..5144e2dfc 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -36,10 +36,6 @@ export class StateChangesConsumerService { const endDecoding = Date.now(); const decodingDuration = endDecoding - startDecoding; - - console.dir(finalStates, { depth: null }) - // console.timeEnd('decode time') - this.accountDetailsRepository await this.accountDetailsRepository.updateAccounts(transformedFinalStates); await this.cacheService.setRemote( @@ -52,7 +48,7 @@ export class StateChangesConsumerService { const duration = end - start; if (duration > 10) { // console.dir(finalStates, { depth: null }) - console.log(`decoding duration: ${decodingDuration}`) + console.log(`decoding duration: ${decodingDuration}ms`) console.log(`processing time shard ${blockWithStateChanges.shardID}: ${duration}ms`); } } catch (error) { @@ -71,12 +67,17 @@ export class StateChangesConsumerService { private transformFinalStatesToDbFormat(finalStates: Record, shardID: number) { const transformed: AccountDetails[] = []; - for (const [_key, state] of Object.entries(finalStates)) { + + for (const [_address, state] of Object.entries(finalStates)) { + // t1 + 0.5 + // const accountExists = await this.accountDetailsRepository.accountExists(address); + // if (!accountExists) { + // continue; + // } const newAccountState = state.accountState; const tokens = [ ...state.esdtState.Fungible, - ]; const nfts = [ @@ -93,6 +94,7 @@ export class StateChangesConsumerService { transformed.push(new AccountDetails({ ...newAccountState, shard: shardID, + ...this.parseCodeMetadata(newAccountState.codeMetadata), tokens: tokens.map(token => new TokenWithBalance({ identifier: token.identifier, nonce: parseInt(token.nonce), @@ -116,6 +118,26 @@ export class StateChangesConsumerService { return transformed; } + + + private parseCodeMetadata(hexStr?: string) { + const UPGRADEABLE = 0x01_00; // 256 + const READABLE = 0x04_00; // 1024 + const PAYABLE = 0x00_02; // 2 + const PAYABLE_BY_SC = 0x00_04; // 4 + if (!hexStr || hexStr === '') { + return {}; + } + const value = parseInt(hexStr, 16); + + return { + isUpgradeable: (value & UPGRADEABLE) !== 0, + isReadable: (value & READABLE) !== 0, + isPayable: (value & PAYABLE) !== 0, + isPayableBySmartContract: (value & PAYABLE_BY_SC) !== 0, + }; + } + private parseEsdtType(type: ESDTType): TokenType | NftType { switch (type) { case ESDTType.Fungible: @@ -157,5 +179,4 @@ export class StateChangesConsumerService { return NftSubType.DynamicMetaESDT; } } - } From 77be688a6cb6a0db8fc6db0c4bcce7d9abf9acf7 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 25 Sep 2025 13:27:18 +0300 Subject: [PATCH 30/90] fix esdts nonce --- config/config.devnet.yaml | 13 +++++-------- src/state-changes/utils/state-changes.utils.ts | 11 ++++------- src/state-changes/utils/token.parser.ts | 5 +++-- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 7c4f0e9d2..4166f189a 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -20,12 +20,9 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: - stateChanges: - enabled: true - port: 5675 - url: 'amqp://guest:guest@127.0.0.1:5672' - exchange: 'state_accesses' - queue: 'state-changes' + websocketSubscription: + enabled: false + port: 6002 eventsNotifier: enabled: false port: 5674 @@ -37,7 +34,7 @@ features: hitsThreshold: 100 ttl: 12 transactionPool: - enabled: false + enabled: true transactionPoolWarmer: enabled: false cronExpression: '*/5 * * * * *' @@ -136,7 +133,7 @@ urls: elastic: - 'https://devnet-index.multiversx.com' gateway: - - 'https://devnet-gateway.multiversx.com' + - 'https://k8s-devnet-gateway.multiversx.com' verifier: 'https://play-api.multiversx.com' redis: '127.0.0.1' rabbitmq: 'amqp://127.0.0.1:5672' diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 3fa6814ae..96088d352 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -104,14 +104,14 @@ function getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); const esdtPrefix = 'ELRONDesdt'; try { - const keyRaw = Buffer.from(dataTrieChange.key, "base64").toString(); - let key = keyRaw; + const keyRawBuf = Buffer.from(dataTrieChange.key, "base64"); + const keyRaw = keyRawBuf.toString(); if (keyRaw.startsWith(esdtPrefix)) { - key = keyRaw.slice(esdtPrefix.length); + const keyBuf = keyRawBuf.slice(esdtPrefix.length); const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufTrieLeafValue as Uint8Array); const valueBigInt: bigint = decodeMxSignMagBigInt(msgEsdtData.Value); - const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(key); + const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(keyBuf); return { identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, @@ -393,9 +393,6 @@ export async function isDbValid(cacheService: CacheService): Promise { } const diff = Date.now() - minTimestamp; - console.log('Min timestamp from cache:', minTimestamp); - console.log('Current timestamp:', Date.now()); - console.group('diff', diff); const blockTime = 6000; return diff < blockTime; } diff --git a/src/state-changes/utils/token.parser.ts b/src/state-changes/utils/token.parser.ts index 7be799e6c..392ab6d23 100644 --- a/src/state-changes/utils/token.parser.ts +++ b/src/state-changes/utils/token.parser.ts @@ -14,8 +14,9 @@ export class TokenParser { * "ALC-2w3e4rX" -> ["ALC-2w3e4r", "X"] (non-fungible, nonce = "X") */ public static extractTokenIDAndNonceHexFromTokenStorageKey( - tokenKey: string + tokenKeyRaw: Buffer ): [string, string] { + const tokenKey = tokenKeyRaw.toString(); const token = tokenKey; const indexOfFirstHyphen = token.indexOf(this.separatorChar); @@ -48,7 +49,7 @@ export class TokenParser { const numCharsSinceNonce = token.length - nonceStr.length; const tokenID = token.slice(0, numCharsSinceNonce); if (nonceStr) { - return [tokenID, Buffer.from(nonceStr).toString('hex')] + return [tokenID, Array.from(tokenKeyRaw.slice(tokenID.length)).map(byte => byte.toString(16).padStart(2, '0')).join('')]; } return [tokenID, "00"]; } From 232f3cda0bed180595a92bf9fa8379273fc60c9d Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 8 Oct 2025 17:53:10 +0300 Subject: [PATCH 31/90] caching + invalidation + store in db for account state --- .../account.details.repository.ts | 4 + .../db/schemas/account.details.schema.ts | 18 ++-- .../accounts-v2/account.controller.v2.ts | 42 ++++---- .../accounts-v2/account.service.v2.ts | 95 ++++++++++--------- .../entities/state.changes.entity.ts | 8 +- .../state.changes.consumer.service.ts | 92 ++++++++++++------ .../utils/state-changes.utils.ts | 34 ++++--- src/utils/cache.info.ts | 16 +++- 8 files changed, 189 insertions(+), 120 deletions(-) diff --git a/src/common/indexer/db/repositories/account.details.repository.ts b/src/common/indexer/db/repositories/account.details.repository.ts index 8f694b3e2..240490380 100644 --- a/src/common/indexer/db/repositories/account.details.repository.ts +++ b/src/common/indexer/db/repositories/account.details.repository.ts @@ -181,6 +181,8 @@ export class AccountDetailsRepository { "nfts.type": 1, "nfts.subType": 1, "nfts.name": 1, + "nfts.balance": 1, + "nfts.subtype": 1, } } ]).exec(); @@ -221,6 +223,8 @@ export class AccountDetailsRepository { "nfts.type": 1, "nfts.subType": 1, "nfts.name": 1, + "nfts.balance": 1, + "nfts.subtype": 1, } } ]).exec(); diff --git a/src/common/indexer/db/schemas/account.details.schema.ts b/src/common/indexer/db/schemas/account.details.schema.ts index 7430d7c7c..c7b572c5e 100644 --- a/src/common/indexer/db/schemas/account.details.schema.ts +++ b/src/common/indexer/db/schemas/account.details.schema.ts @@ -28,8 +28,8 @@ export class AccountDetails { @Prop({ required: true, type: Number }) shard: number = 0; - @Prop({ required: true, type: String }) - ownerAddress: string = ''; + @Prop({ required: false, type: String }) + ownerAddress?: string; @Prop({ type: Object, required: false }) assets?: AccountAssets; @@ -55,20 +55,20 @@ export class AccountDetails { @Prop({ required: false, type: Number }) transfersLast24h?: number; - @Prop({ required: true, type: String }) - code: string = ''; + @Prop({ required: false, type: String }) + code?: string; - @Prop({ required: true, type: String }) - codeHash: string = ''; + @Prop({ required: false, type: String }) + codeHash?: string; - @Prop({ required: true, type: String }) - rootHash: string = ''; + @Prop({ required: false, type: String }) + rootHash?: string; @Prop({ required: false, type: String }) username?: string; @Prop({ required: true, type: String }) - developerReward: string = ''; + developerReward: string = '0'; @Prop({ required: false, type: Boolean }) isUpgradeable?: boolean; diff --git a/src/endpoints/accounts-v2/account.controller.v2.ts b/src/endpoints/accounts-v2/account.controller.v2.ts index d3593bf73..d3a587922 100644 --- a/src/endpoints/accounts-v2/account.controller.v2.ts +++ b/src/endpoints/accounts-v2/account.controller.v2.ts @@ -65,7 +65,7 @@ export class AccountControllerV2 { private readonly logger = new OriginLogger(AccountControllerV2.name); constructor( - private readonly accountService: AccountServiceV2, + private readonly accountServiceV2: AccountServiceV2, private readonly tokenService: TokenService, private readonly nftService: NftService, private readonly delegationLegacyService: DelegationLegacyService, @@ -133,7 +133,7 @@ export class AccountControllerV2 { search, }); queryOptions.validate(size); - return this.accountService.getAccounts( + return this.accountServiceV2.getAccounts( new QueryPagination({ from, size }), queryOptions, ); @@ -158,7 +158,7 @@ export class AccountControllerV2 { @Query("hasAssets", ParseBoolPipe) hasAssets?: boolean, @Query("search") search?: string, ): Promise { - return await this.accountService.getAccountsCount( + return await this.accountServiceV2.getAccountsCount( new AccountQueryOptions( { ownerAddress, @@ -182,7 +182,7 @@ export class AccountControllerV2 { @Query("hasAssets", ParseBoolPipe) hasAssets?: boolean, @Query("search") search?: string, ): Promise { - return await this.accountService.getAccountsCount( + return await this.accountServiceV2.getAccountsCount( new AccountQueryOptions( { ownerAddress, @@ -215,7 +215,7 @@ export class AccountControllerV2 { @Query('withAssets', ParseBoolPipe) withAssets?: boolean, @Query('timestamp', ParseIntPipe) _timestamp?: number, ): Promise { - const account = await this.accountService.getAccountFromDb( + const account = await this.accountServiceV2.getAccount( address, new AccountFetchOptions({ withGuardianInfo, withTxCount, withScrCount, withTimestamp, withAssets }), ); @@ -231,7 +231,7 @@ export class AccountControllerV2 { @ApiOkResponse({ type: [AccountDeferred] }) async getAccountDeferred(@Param('address', ParseAddressPipe) address: string): Promise { try { - return await this.accountService.getDeferredAccount(address); + return await this.accountServiceV2.getDeferredAccount(address); } catch (error) { this.logger.error(`Error in getAccountDeferred for address ${address}`); this.logger.error(error); @@ -244,7 +244,7 @@ export class AccountControllerV2 { @ApiOkResponse({ type: AccountVerification }) async getAccountVerification(@Param('address', ParseAddressPipe) address: string): Promise { try { - return await this.accountService.getAccountVerification(address); + return await this.accountServiceV2.getAccountVerification(address); } catch (error) { this.logger.error(`Error in getAccountVerification for address ${address}`); this.logger.error(error); @@ -845,7 +845,7 @@ export class AccountControllerV2 { @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, @Query('status', new ParseEnumArrayPipe(NodeStatusRaw)) status?: NodeStatusRaw[], ): Promise { - return await this.accountService.getKeys( + return await this.accountServiceV2.getKeys( address, new AccountKeyFilter({ status }), new QueryPagination({ from, size })); @@ -1202,20 +1202,20 @@ export class AccountControllerV2 { @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); + return this.accountServiceV2.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); + return this.accountServiceV2.getAccountDeploysCount(address); } @Get("/accounts/:address/deploys/c") @ApiExcludeEndpoint() getAccountDeploysCountAlternative(@Param('address', ParseAddressPipe) address: string): Promise { - return this.accountService.getAccountDeploysCount(address); + return this.accountServiceV2.getAccountDeploysCount(address); } @Get("/accounts/:address/contracts") @@ -1228,20 +1228,20 @@ export class AccountControllerV2 { @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, ): Promise { - return this.accountService.getAccountContracts(new QueryPagination({ from, size }), address); + return this.accountServiceV2.getAccountContracts(new QueryPagination({ from, size }), address); } @Get("/accounts/:address/contracts/count") @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); + return this.accountServiceV2.getAccountContractsCount(address); } @Get("/accounts/:address/contracts/c") @ApiExcludeEndpoint() getAccountContractsCountAlternative(@Param('address', ParseAddressPipe) address: string): Promise { - return this.accountService.getAccountContractsCount(address); + return this.accountServiceV2.getAccountContractsCount(address); } @Get("/accounts/:address/upgrades") @@ -1254,7 +1254,7 @@ export class AccountControllerV2 { @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, ): Promise { - return this.accountService.getContractUpgrades(new QueryPagination({ from, size }), address); + return this.accountServiceV2.getContractUpgrades(new QueryPagination({ from, size }), address); } @Get("/accounts/:address/results") @@ -1308,7 +1308,7 @@ export class AccountControllerV2 { @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, ): Promise { - return this.accountService.getAccountHistory( + return this.accountServiceV2.getAccountHistory( address, new QueryPagination({ from, size }), new AccountHistoryFilter({ before, after })); @@ -1324,7 +1324,7 @@ export class AccountControllerV2 { @Query('before', ParseIntPipe) before?: number, @Query('after', ParseIntPipe) after?: number, ): Promise { - return this.accountService.getAccountHistoryCount( + return this.accountServiceV2.getAccountHistoryCount( address, new AccountHistoryFilter({ before, after })); } @@ -1344,7 +1344,7 @@ export class AccountControllerV2 { if (!isToken) { throw new NotFoundException(`Token '${tokenIdentifier}' not found`); } - return this.accountService.getAccountTokenHistoryCount( + return this.accountServiceV2.getAccountTokenHistoryCount( address, tokenIdentifier, new AccountHistoryFilter({ before, after })); @@ -1368,7 +1368,7 @@ export class AccountControllerV2 { @Query('identifier', ParseArrayPipe) identifier?: string[], @Query('token', ParseTokenPipe) token?: string, ): Promise { - return await this.accountService.getAccountEsdtHistory( + return await this.accountServiceV2.getAccountEsdtHistory( address, new QueryPagination({ from, size }), new AccountHistoryFilter({ before, after, identifiers: identifier, token })); @@ -1387,7 +1387,7 @@ export class AccountControllerV2 { @Query('identifier', ParseArrayPipe) identifier?: string[], @Query('token', ParseTokenPipe) token?: string, ): Promise { - return await this.accountService.getAccountEsdtHistoryCount( + return await this.accountServiceV2.getAccountEsdtHistoryCount( address, new AccountHistoryFilter({ before, after, identifiers: identifier, token })); } @@ -1412,7 +1412,7 @@ export class AccountControllerV2 { throw new NotFoundException(`Token '${tokenIdentifier}' not found`); } - return await this.accountService.getAccountTokenHistory( + return await this.accountServiceV2.getAccountTokenHistory( address, tokenIdentifier, new QueryPagination({ from, size }), new AccountHistoryFilter({ before, after })); diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts index a319cdb68..5cb6a2b9e 100644 --- a/src/endpoints/accounts-v2/account.service.v2.ts +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -77,66 +77,75 @@ export class AccountServiceV2 { return await this.indexerService.getAccountsCount(filter); } - async getAccount(address: string, options?: AccountFetchOptions): Promise { - if (!AddressUtils.isAddressValid(address)) { - return null; - } - - const account = await this.getAccountRaw(address, options?.withAssets); - if (!account) { - return null; - } - - if (options?.withTxCount === true) { - account.txCount = await this.getAccountTxCount(address); - } - - if (options?.withScrCount === true) { - account.scrCount = await this.getAccountScResults(address); - } - - if (options?.withGuardianInfo === true) { - await this.applyGuardianInfo(account); - } - - if (options?.withTimestamp) { - const elasticSearchAccount = await this.indexerService.getAccount(address); - account.timestamp = elasticSearchAccount.timestamp; - } - - if (AddressUtils.isSmartContractAddress(address)) { - const provider: Provider | undefined = await this.providerService.getProvider(address); - if (provider && provider.owner) { - account.ownerAddress = provider.owner; + private async getAccountWithFallBack(address: string, options?: AccountFetchOptions): Promise { + // First try to get account from MongoDB + const accountFromDb = await this.accountDetailsDepository.getAccount(address); + if (!accountFromDb) { + return await this.getAccountRaw(address, options?.withAssets); + } + if (options && options.withAssets === true) { + const assets = await this.assetsService.getAllAccountAssets(); + accountFromDb.assets = assets[address]; + if (accountFromDb.ownerAddress && accountFromDb.ownerAddress !== '') { + accountFromDb.ownerAssets = assets[accountFromDb.ownerAddress]; } } - - return account; + return accountFromDb; } - async getAccountFromDb(address: string, options?: AccountFetchOptions): Promise { + async getAccount(address: string, options?: AccountFetchOptions): Promise { if (!AddressUtils.isAddressValid(address)) { return null; } - + let account = null; try { const isDbUpToDate: boolean = await isDbValid(this.cachingService); if (isDbUpToDate === true) { - // First try to get account from MongoDB - const accountFromDb = await this.accountDetailsDepository.getAccount(address); + account = await this.cachingService.getOrSet( + CacheInfo.AccountState(address).key, + async () => this.getAccountWithFallBack(address, options), + CacheInfo.AccountState(address).ttl, + ) + } else { + account = await this.getAccountRaw(address, options?.withAssets); + } - if (accountFromDb) { - // console.log('Account found in DB:', accountFromDb); - return accountFromDb; + if (!account) { + return null; + } + + if (options?.withTxCount === true) { + account.txCount = await this.getAccountTxCount(address); + } + + if (options?.withScrCount === true) { + account.scrCount = await this.getAccountScResults(address); + } + + if (options?.withGuardianInfo === true) { + await this.applyGuardianInfo(account); + } + + if (options?.withTimestamp) { + if (!account.timestamp) { + const elasticSearchAccount = await this.indexerService.getAccount(address); + account.timestamp = elasticSearchAccount.timestamp; + } + } + + if (AddressUtils.isSmartContractAddress(address) && !account.ownerAddress) { + const provider: Provider | undefined = await this.providerService.getProvider(address); + if (provider && provider.owner) { + account.ownerAddress = provider.owner; } } - // If not found in DB, call getAccount with the same parameters - return await this.getAccount(address, options); } catch (error) { - this.logger.error(`Error when getting account from DB for address '${address}'`); + this.logger.error(`Error when getting account for address '${address}'`); this.logger.error(error); return null; } + + return account; } async applyGuardianInfo(account: AccountDetailed): Promise { diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts index a891deb48..8726ab6a1 100644 --- a/src/state-changes/entities/state.changes.entity.ts +++ b/src/state-changes/entities/state.changes.entity.ts @@ -53,11 +53,11 @@ export class AccountState { balance!: string; developerReward!: string; address!: string; - codeHash!: string; + codeHash?: string; rootHash!: string; - ownerAddress!: string; - userName!: string; - codeMetadata!: string; + ownerAddress?: string; + userName?: string; + codeMetadata?: string; constructor(init?: Partial) { Object.assign(this, init); diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 5144e2dfc..58a981238 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -2,7 +2,7 @@ import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers" import { BlockWithStateChangesRaw, ESDTType, StateChanges } from "./entities"; import { decodeStateChangesFinal } from "./utils/state-changes.utils"; import { AccountDetails, AccountDetailsRepository } from "src/common/indexer/db"; -import { Injectable } from "@nestjs/common"; +import { Inject, Injectable } from "@nestjs/common"; import { TokenWithBalance } from "src/endpoints/tokens/entities/token.with.balance"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; import { CacheInfo } from "src/utils/cache.info"; @@ -10,6 +10,7 @@ import { NftAccount } from "src/endpoints/nfts/entities/nft.account"; import { TokenType } from "src/common/indexer/entities"; import { NftType } from "src/endpoints/nfts/entities/nft.type"; import { NftSubType } from "src/endpoints/nfts/entities/nft.sub.type"; +import { ClientProxy } from "@nestjs/microservices"; @Injectable() export class StateChangesConsumerService { @@ -17,6 +18,7 @@ export class StateChangesConsumerService { // private readonly apiConfigService: ApiConfigService, private readonly cacheService: CacheService, private readonly accountDetailsRepository: AccountDetailsRepository, + @Inject('PUBSUB_SERVICE') private clientProxy: ClientProxy, ) { } @CompetingRabbitConsumer({ @@ -26,22 +28,21 @@ export class StateChangesConsumerService { }) async consumeEvents(blockWithStateChanges: BlockWithStateChangesRaw) { try { - const start = Date.now(); // ms la început - // console.time(`processing time shard ${blockWithStateChanges.shardID}`) - // console.time('decode time') - // console.dir(blockWithStateChanges, { depth: null }) + const start = Date.now(); + const startDecoding = start; const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); - const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID); + + const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs); const endDecoding = Date.now(); const decodingDuration = endDecoding - startDecoding; - await this.accountDetailsRepository.updateAccounts(transformedFinalStates); + await this.updateAccounts(transformedFinalStates); await this.cacheService.setRemote( - CacheInfo.LatestProcessedBlockTimestamp(blockWithStateChanges.shardID).key, + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(blockWithStateChanges.shardID).key, blockWithStateChanges.timestampMs, - CacheInfo.LatestProcessedBlockTimestamp(blockWithStateChanges.shardID).ttl, + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(blockWithStateChanges.shardID).ttl, ); // console.timeEnd(`processing time shard ${blockWithStateChanges.shardID}`) const end = Date.now(); // ms la final @@ -61,11 +62,33 @@ export class StateChangesConsumerService { // return decodeStateChangesRaw(stateChanges); // } + private async updateAccounts(transformedFinalStates: AccountDetails[]) { + const promisesToWaitFor = [this.accountDetailsRepository.updateAccounts(transformedFinalStates)]; + + // set as healthy + const cacheKeys = []; + const values = []; + for (const account of transformedFinalStates) { + cacheKeys.push(CacheInfo.AccountState(account.address).key); + const { tokens, nfts, ...accountWithoutAssets } = account; + values.push(accountWithoutAssets); + } + promisesToWaitFor.push( + this.cacheService.setManyRemote( + cacheKeys, + values, + CacheInfo.AccountState('any').ttl, + ) + ); + this.deleteLocalCache(cacheKeys); + + await Promise.all(promisesToWaitFor) + } private decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { return decodeStateChangesFinal(blockWithStateChanges); } - private transformFinalStatesToDbFormat(finalStates: Record, shardID: number) { + private transformFinalStatesToDbFormat(finalStates: Record, shardID: number, blockTimestampMs: number) { const transformed: AccountDetails[] = []; for (const [_address, state] of Object.entries(finalStates)) { @@ -91,26 +114,30 @@ export class StateChangesConsumerService { ]; if (newAccountState) { - transformed.push(new AccountDetails({ - ...newAccountState, - shard: shardID, - ...this.parseCodeMetadata(newAccountState.codeMetadata), - tokens: tokens.map(token => new TokenWithBalance({ - identifier: token.identifier, - nonce: parseInt(token.nonce), - balance: token.value, - type: this.parseEsdtType(token.type) as TokenType, - subType: NftSubType.None, - })), - nfts: nfts.map(nft => new NftAccount({ - identifier: nft.identifier, - nonce: parseInt(nft.nonce), - type: this.parseEsdtType(nft.type) as NftType, - subType: this.parseEsdtSubtype(nft.type), - collection: nft.identifier.replace(/-[^-]*$/, ''), // delete everything after last `-` character inclusive - balance: nft.value, - })) - })); + const parsedAccount = + new AccountDetails({ + ...newAccountState, + shard: shardID, + timestamp: blockTimestampMs, + ...this.parseCodeMetadata(newAccountState.codeMetadata), + tokens: tokens.map(token => new TokenWithBalance({ + identifier: token.identifier, + nonce: parseInt(token.nonce), + balance: token.value, + type: this.parseEsdtType(token.type) as TokenType, + subType: NftSubType.None, + })), + nfts: nfts.map(nft => new NftAccount({ + identifier: nft.identifier, + nonce: parseInt(nft.nonce), + type: this.parseEsdtType(nft.type) as NftType, + subType: this.parseEsdtSubtype(nft.type), + collection: nft.identifier.replace(/-[^-]*$/, ''), // delete everything after last `-` character inclusive + balance: nft.value, + })) + }); + transformed.push(parsedAccount); + console.log(parsedAccount) } } @@ -119,6 +146,9 @@ export class StateChangesConsumerService { } + private deleteLocalCache(cacheKeys: string[]) { + this.clientProxy.emit('deleteCacheKeys', cacheKeys); + } private parseCodeMetadata(hexStr?: string) { const UPGRADEABLE = 0x01_00; // 256 @@ -179,4 +209,4 @@ export class StateChangesConsumerService { return NftSubType.DynamicMetaESDT; } } -} +} \ No newline at end of file diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts index 96088d352..c751c2d6d 100644 --- a/src/state-changes/utils/state-changes.utils.ts +++ b/src/state-changes/utils/state-changes.utils.ts @@ -58,17 +58,23 @@ function getDecodedUserAccountData(buf: any) { const address = bech32FromBytes(msg.Address); const ownerAddress = bech32FromBytes(msg.OwnerAddress); - return { + const data: AccountState = { nonce: longToString(msg.Nonce), balance: balance.toString(), developerReward: devReward.toString(), - address: address, - ownerAddress: ownerAddress, + address, + ownerAddress, codeHash: bytesToBase64(msg.CodeHash), rootHash: bytesToBase64(msg.RootHash), userName: bytesToHex(msg.UserName), codeMetadata: bytesToHex(msg.CodeMetadata), }; + + const filteredData = Object.fromEntries( + Object.entries(data).filter(([_, v]) => v !== undefined && v !== null && v !== '') + ) as AccountState; + console.log(filteredData) + return filteredData; } catch (e: any) { console.warn(`Could not decode as UserAccountData: ${e.message}`); return null; @@ -100,7 +106,7 @@ export function decodeAccountChanges(flags: number | undefined): AccountChanges }); } -function getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { +export function getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); const esdtPrefix = 'ELRONDesdt'; try { @@ -112,7 +118,7 @@ function getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { const valueBigInt: bigint = decodeMxSignMagBigInt(msgEsdtData.Value); const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(keyBuf); - + console.log(`key: ${dataTrieChange.key}, identifier: ${identifier}, nonceHex: ${nonceHex}, type: ${msgEsdtData.Type}, value: ${valueBigInt.toString()}`); return { identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, nonce: parseInt(nonceHex, 16).toString(), @@ -372,12 +378,11 @@ export function getFinalStates(stateChanges: Record) { } export async function isDbValid(cacheService: CacheService): Promise { - // TODO: do not hardcode shard IDs const timestampsMs: (string | undefined)[] = await Promise.all([ - cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(0).key), - cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(1).key), - cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(2).key), - cacheService.get(CacheInfo.LatestProcessedBlockTimestamp(4294967295).key), + cacheService.get(CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(0).key), + cacheService.get(CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(1).key), + cacheService.get(CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(2).key), + cacheService.get(CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(4294967295).key), ]) as (string | undefined)[];; const numericValues = timestampsMs @@ -392,7 +397,14 @@ export async function isDbValid(cacheService: CacheService): Promise { return false; } + //@ts-ignore const diff = Date.now() - minTimestamp; + // console.log('Min timestamp from cache:', minTimestamp); + // console.log('Current timestamp:', Date.now()); + // console.log('diff: ', diff); const blockTime = 6000; - return diff < blockTime; + return diff <= blockTime; } + + +// key: RUxST05EZXNkdFNIQVJFUy0zMzc4ZGKT, identifier: SHARES-3378db, nonceHex: efbfbd \ No newline at end of file diff --git a/src/utils/cache.info.ts b/src/utils/cache.info.ts index e1166c2d8..f7356ccfc 100644 --- a/src/utils/cache.info.ts +++ b/src/utils/cache.info.ts @@ -711,10 +711,24 @@ export class CacheInfo { }; } - static LatestProcessedBlockTimestamp(shardId: number): CacheInfo { + static StateChangesConsumerLatestProcessedBlockTimestamp(shardId: number): CacheInfo { return { key: `latestProcessedBlock:${shardId}`, ttl: Constants.oneMinute(), }; } + + static AccountState(address: string): CacheInfo { + return { + key: `account-state:${address}`, + ttl: Constants.oneHour() * 12, + }; + } + + static AccountToken(address: string): CacheInfo { + return { + key: `account-token:${address}`, + ttl: Constants.oneHour() * 12, + }; + } } From b521a416e6dcb6c4440a44b416b2bb0ed54af2c0 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 9 Oct 2025 15:11:00 +0300 Subject: [PATCH 32/90] improvements --- .../account.details.repository.ts | 2 +- .../db/schemas/account.details.schema.ts | 4 +- .../accounts-v2/account.service.v2.ts | 6 +- src/endpoints/nfts/nft.service.ts | 7 +- src/endpoints/tokens/token.service.ts | 13 +- .../entities/state.changes.entity.ts | 2 +- .../state.changes.consumer.service.ts | 47 +- .../utils/state-changes.decoder.ts | 401 +++++++++++++++++ .../utils/state-changes.utils.ts | 410 ------------------ 9 files changed, 456 insertions(+), 436 deletions(-) create mode 100644 src/state-changes/utils/state-changes.decoder.ts delete mode 100644 src/state-changes/utils/state-changes.utils.ts diff --git a/src/common/indexer/db/repositories/account.details.repository.ts b/src/common/indexer/db/repositories/account.details.repository.ts index 240490380..ceb38473d 100644 --- a/src/common/indexer/db/repositories/account.details.repository.ts +++ b/src/common/indexer/db/repositories/account.details.repository.ts @@ -256,7 +256,7 @@ export class AccountDetailsRepository { if (!accountDb) { return null; } - return new AccountDetailed({ ...accountDb, nonce: parseInt(accountDb.nonce) }); + return new AccountDetailed({ ...accountDb, nonce: accountDb.nonce }); } catch (error) { console.error('Error fetching account:', error); return null; diff --git a/src/common/indexer/db/schemas/account.details.schema.ts b/src/common/indexer/db/schemas/account.details.schema.ts index c7b572c5e..69a142d19 100644 --- a/src/common/indexer/db/schemas/account.details.schema.ts +++ b/src/common/indexer/db/schemas/account.details.schema.ts @@ -19,8 +19,8 @@ export class AccountDetails { @Prop({ required: true, type: String }) balance: string = ''; - @Prop({ required: true, type: String }) - nonce: string = '0'; + @Prop({ required: true, type: Number }) + nonce: number = 0; @Prop({ required: true, type: Number }) timestamp: number = 0; diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts index 5cb6a2b9e..809ced4ea 100644 --- a/src/endpoints/accounts-v2/account.service.v2.ts +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -37,7 +37,7 @@ import { AccountContract } from './entities/account.contract'; import { AccountFetchOptions } from './entities/account.fetch.options'; import { Provider } from '../providers/entities/provider'; import { AccountDetailsRepository } from 'src/common/indexer/db'; -import { isDbValid } from 'src/state-changes/utils/state-changes.utils'; +import { StateChangesConsumerService } from 'src/state-changes/state.changes.consumer.service'; @Injectable() export class AccountServiceV2 { @@ -99,8 +99,8 @@ export class AccountServiceV2 { } let account = null; try { - const isDbUpToDate: boolean = await isDbValid(this.cachingService); - if (isDbUpToDate === true) { + const isStateChangesConsumerHealty: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); + if (isStateChangesConsumerHealty === true) { account = await this.cachingService.getOrSet( CacheInfo.AccountState(address).key, async () => this.getAccountWithFallBack(address, options), diff --git a/src/endpoints/nfts/nft.service.ts b/src/endpoints/nfts/nft.service.ts index 464b14685..07b03a9c5 100644 --- a/src/endpoints/nfts/nft.service.ts +++ b/src/endpoints/nfts/nft.service.ts @@ -32,7 +32,7 @@ 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"; import { AccountDetailsRepository } from "src/common/indexer/db"; -import { isDbValid } from "src/state-changes/utils/state-changes.utils"; +import { StateChangesConsumerService } from "src/state-changes/state.changes.consumer.service"; @Injectable() export class NftService { @@ -508,7 +508,7 @@ export class NftService { } async getNftsForAddressFromDb(address: string, queryPagination: QueryPagination, filter: NftFilter, fields?: string[], queryOptions?: NftQueryOptions, source?: EsdtDataSource): Promise { - const isDbUpToDate: boolean = await isDbValid(this.cachingService); + const isDbUpToDate: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); if (isDbUpToDate === true) { const nfts = await this.accountDetailsRepository.getNftsForAddress(address, queryPagination) as NftAccount[]; if (nfts && nfts.length > 0) { @@ -618,10 +618,11 @@ export class NftService { } async getNftForAddressFromDb(address: string, identifier: string, fields?: string[]): Promise { - const isDbUpToDate: boolean = await isDbValid(this.cachingService); + const isDbUpToDate: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); if (isDbUpToDate === true) { const nft = await this.accountDetailsRepository.getNftForAddress(address, identifier) as NftAccount; if (nft) { + console.log(`found in db nft: ${nft.identifier}`) return nft; } } diff --git a/src/endpoints/tokens/token.service.ts b/src/endpoints/tokens/token.service.ts index c8b2d5fd5..4dc91deeb 100644 --- a/src/endpoints/tokens/token.service.ts +++ b/src/endpoints/tokens/token.service.ts @@ -44,7 +44,7 @@ import { MexPairState } from "../mex/entities/mex.pair.state"; import { MexTokenType } from "../mex/entities/mex.token.type"; import { NftSubType } from "../nfts/entities/nft.sub.type"; import { AccountDetailsRepository } from "src/common/indexer/db"; -import { isDbValid } from "src/state-changes/utils/state-changes.utils"; +import { StateChangesConsumerService } from "src/state-changes/state.changes.consumer.service"; @Injectable() export class TokenService { @@ -254,7 +254,7 @@ export class TokenService { } async getTokensForAddressFromDb(address: string, queryPagination: QueryPagination, filter: TokenFilter): Promise { - const isDbUpToDate: boolean = await isDbValid(this.cachingService); + const isDbUpToDate: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000) if (isDbUpToDate === true) { const tokens = await this.accountDetailsRepository.getTokensForAddress(address, queryPagination) as TokenWithBalance[]; if (tokens && tokens.length > 0) { @@ -355,14 +355,19 @@ export class TokenService { } async getTokenForAddressFromDb(address: string, identifier: string): Promise { - const isDbUpToDate: boolean = await isDbValid(this.cachingService); + const isDbUpToDate: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); if (isDbUpToDate === true) { const token = await this.accountDetailsRepository.getTokenForAddress(address, identifier) as TokenDetailedWithBalance; if (token) { + console.log(`found in db token: ${token.identifier}`) return token; } } - return await this.getTokenForAddress(address, identifier); + const token = await this.getTokenForAddress(address, identifier); + // if (token) { + // this.dbWriterService.writeAccountToDb({ tokens: [token] }); + // } + return token; } diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts index 8726ab6a1..2ee7dcead 100644 --- a/src/state-changes/entities/state.changes.entity.ts +++ b/src/state-changes/entities/state.changes.entity.ts @@ -49,7 +49,7 @@ export class BlockWithStateChangesRaw { } export class AccountState { - nonce!: string; + nonce!: number; balance!: string; developerReward!: string; address!: string; diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 58a981238..8f7735c73 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -1,6 +1,5 @@ import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers"; import { BlockWithStateChangesRaw, ESDTType, StateChanges } from "./entities"; -import { decodeStateChangesFinal } from "./utils/state-changes.utils"; import { AccountDetails, AccountDetailsRepository } from "src/common/indexer/db"; import { Inject, Injectable } from "@nestjs/common"; import { TokenWithBalance } from "src/endpoints/tokens/entities/token.with.balance"; @@ -11,11 +10,11 @@ import { TokenType } from "src/common/indexer/entities"; import { NftType } from "src/endpoints/nfts/entities/nft.type"; import { NftSubType } from "src/endpoints/nfts/entities/nft.sub.type"; import { ClientProxy } from "@nestjs/microservices"; +import { StateChangesDecoder } from "./utils/state-changes.decoder"; @Injectable() export class StateChangesConsumerService { constructor( - // private readonly apiConfigService: ApiConfigService, private readonly cacheService: CacheService, private readonly accountDetailsRepository: AccountDetailsRepository, @Inject('PUBSUB_SERVICE') private clientProxy: ClientProxy, @@ -44,7 +43,7 @@ export class StateChangesConsumerService { blockWithStateChanges.timestampMs, CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(blockWithStateChanges.shardID).ttl, ); - // console.timeEnd(`processing time shard ${blockWithStateChanges.shardID}`) + const end = Date.now(); // ms la final const duration = end - start; if (duration > 10) { @@ -65,7 +64,6 @@ export class StateChangesConsumerService { private async updateAccounts(transformedFinalStates: AccountDetails[]) { const promisesToWaitFor = [this.accountDetailsRepository.updateAccounts(transformedFinalStates)]; - // set as healthy const cacheKeys = []; const values = []; for (const account of transformedFinalStates) { @@ -85,18 +83,13 @@ export class StateChangesConsumerService { await Promise.all(promisesToWaitFor) } private decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { - return decodeStateChangesFinal(blockWithStateChanges); + return StateChangesDecoder.decodeStateChangesFinal(blockWithStateChanges); } private transformFinalStatesToDbFormat(finalStates: Record, shardID: number, blockTimestampMs: number) { const transformed: AccountDetails[] = []; for (const [_address, state] of Object.entries(finalStates)) { - // t1 + 0.5 - // const accountExists = await this.accountDetailsRepository.accountExists(address); - // if (!accountExists) { - // continue; - // } const newAccountState = state.accountState; const tokens = [ @@ -114,9 +107,10 @@ export class StateChangesConsumerService { ]; if (newAccountState) { + const { codeMetadata, ...newAccountStateFiltered } = newAccountState; const parsedAccount = new AccountDetails({ - ...newAccountState, + ...newAccountStateFiltered, shard: shardID, timestamp: blockTimestampMs, ...this.parseCodeMetadata(newAccountState.codeMetadata), @@ -137,7 +131,6 @@ export class StateChangesConsumerService { })) }); transformed.push(parsedAccount); - console.log(parsedAccount) } } @@ -209,4 +202,34 @@ export class StateChangesConsumerService { return NftSubType.DynamicMetaESDT; } } + + static async isStateChangesConsumerHealthy(cacheService: CacheService, maxLastActivityDiffMs: number): Promise { + const keys = [ + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(0).key, + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(1).key, + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(2).key, + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(4294967295).key, + ]; + + let timestampsMs: (number | undefined | null)[] = cacheService.getManyLocal(keys); + + // check local + if (timestampsMs.some(t => t == null)) { + timestampsMs = await cacheService.getManyRemote(keys); + + // check remote + if (timestampsMs.some(t => t == null)) { + return false; + } + + // set only if it's valid + cacheService.setManyLocal(keys, timestampsMs, 0.6); + } + + const minTimestamp = Math.min(...(timestampsMs as number[])) + + const diff = Date.now() - minTimestamp; + + return diff <= maxLastActivityDiffMs; + } } \ No newline at end of file diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts new file mode 100644 index 000000000..6eacca94a --- /dev/null +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -0,0 +1,401 @@ +import { Address } from "@multiversx/sdk-core/out"; +import { UserAccountData } from "./user_account.pb"; +import { ESDigitalToken } from "./esdt"; +import { TokenParser } from "./token.parser"; +import { + AccountChanges, + AccountChangesRaw, + AccountState, + BlockWithStateChangesRaw, + DataTrieChange, + DataTrieChangeOperation, + EsdtState, + ESDTType, + StateAccessOperation, + StateAccessPerAccountRaw, + StateChanges +} from "../entities"; + +export class StateChangesDecoder { + static bech32FromHex(hex: any) { + const clean = hex.startsWith("0x") ? hex.slice(2) : hex; + return Address.newFromHex(clean).toBech32(); + } + + static bech32FromBytes(u8: any) { + return (u8 && u8.length ? Address.newFromHex(this.bytesToHex(u8)).toBech32() : ""); + } + + static bytesToHex(u8: any) { + return (u8 && u8.length ? Buffer.from(u8).toString("hex") : ""); + } + + static bytesToBase64(u8: any) { + return (u8 && u8.length ? Buffer.from(u8).toString("base64") : ""); + } + + // const bytesToString = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("utf8") : ""); + static longToString(v: any) { + return v == null ? "" : (typeof v === "object" && typeof v.toString === "function" ? v.toString() : String(v)); + } + + static bigEndianBytesToBigInt(u8: any) { + let v = BigInt(0); + for (const b of u8) { + v = (v << BigInt(8)) + BigInt(b); + } + return v; + } + + /** + * MultiversX custom sign & magnitude BigInt for proto: + * - zero => [0x00, 0x00] + * - positive non-zero => 0x00 || magnitude (big-endian) + * - (if present) negative => 0x01 || magnitude + * Fallback: if first byte is not a sign marker, treat whole buffer as positive magnitude. + */ + static decodeMxSignMagBigInt(u8: any) { + if (!u8 || u8.length === 0) return BigInt(0); + + // canonical zero used by the serializer + if (u8.length === 2 && u8[0] === 0x00 && u8[1] === 0x00) return BigInt(0); + + const sign = u8[0]; + if (sign === 0x00 || sign === 0x01) { + const mag = u8.slice(1); + const m = this.bigEndianBytesToBigInt(mag); + return sign === 0x01 ? -m : m; + } + + // fallback for legacy "magnitude only" encodings + return this.bigEndianBytesToBigInt(u8); + } + + static getDecodedUserAccountData(buf: any) { + try { + const msg = UserAccountData.decode(buf); + + const balance = this.decodeMxSignMagBigInt(msg.Balance); + const devReward = this.decodeMxSignMagBigInt(msg.DeveloperReward); + const address = this.bech32FromBytes(msg.Address); + const ownerAddress = this.bech32FromBytes(msg.OwnerAddress); + + const data: AccountState = { + nonce: parseInt(this.longToString(msg.Nonce)), + balance: balance.toString(), + developerReward: devReward.toString(), + address, + ownerAddress, + codeHash: this.bytesToBase64(msg.CodeHash), + rootHash: this.bytesToBase64(msg.RootHash), + userName: this.bytesToHex(msg.UserName), + codeMetadata: this.bytesToHex(msg.CodeMetadata), + }; + + const filteredData = Object.fromEntries( + Object.entries(data).filter(([_, v]) => v !== undefined && v !== null && v !== '') + ) as AccountState; + + return filteredData; + } catch (e: any) { + console.warn(`Could not decode as UserAccountData: ${e.message}`); + return null; + } + } + + static decodeAccountChanges(flags: number | undefined): AccountChanges { + if (!flags) { + return new AccountChanges({ + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false, + }); + } + return new AccountChanges({ + nonceChanged: (flags & AccountChangesRaw.NonceChanged) !== 0, + balanceChanged: (flags & AccountChangesRaw.BalanceChanged) !== 0, + codeHashChanged: (flags & AccountChangesRaw.CodeHashChanged) !== 0, + rootHashChanged: (flags & AccountChangesRaw.RootHashChanged) !== 0, + developerRewardChanged: (flags & AccountChangesRaw.DeveloperRewardChanged) !== 0, + ownerAddressChanged: (flags & AccountChangesRaw.OwnerAddressChanged) !== 0, + userNameChanged: (flags & AccountChangesRaw.UserNameChanged) !== 0, + codeMetadataChanged: (flags & AccountChangesRaw.CodeMetadataChanged) !== 0, + }); + } + + static getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { + const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); + const esdtPrefix = 'ELRONDesdt'; + try { + const keyRawBuf = Buffer.from(dataTrieChange.key, "base64"); + const keyRaw = keyRawBuf.toString(); + if (keyRaw.startsWith(esdtPrefix)) { + const keyBuf = keyRawBuf.slice(esdtPrefix.length); + const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufTrieLeafValue as Uint8Array); + + const valueBigInt: bigint = this.decodeMxSignMagBigInt(msgEsdtData.Value); + const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(keyBuf); + console.log(`key: ${dataTrieChange.key}, identifier: ${identifier}, nonceHex: ${nonceHex}, type: ${msgEsdtData.Type}, value: ${valueBigInt.toString()}`); + return { + identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, + nonce: parseInt(nonceHex, 16).toString(), + type: msgEsdtData.Type, + value: valueBigInt.toString(), + propertiesHex: this.bytesToHex(msgEsdtData.Properties), + reservedHex: this.bytesToHex(msgEsdtData.Reserved), + tokenMetaData: msgEsdtData.TokenMetaData ?? null, + }; + } else { + //TODO: handle if needed + + return null; + } + } catch (e: any) { + console.warn(`Could not decode as EsdtData: ${e.message}`); + console.log(address, ':') + console.dir(dataTrieChange) + return null; + } + } + + static decodeStateChangesRaw(blockWithStateChanges: BlockWithStateChangesRaw) { + const allAccounts: Record = {}; + const accounts = blockWithStateChanges.stateAccessesPerAccounts || {}; + + for (const accountHex of Object.keys(accounts)) { + const address = this.bech32FromHex(accountHex); + + const { stateAccess = [] } = accounts[accountHex] || {}; + const allDecoded: Record = {}; + stateAccess.forEach((sa: StateAccessPerAccountRaw, i: number) => { + + const base64AccountData = sa.mainTrieVal; + let decodedAccountData: any = null + if (base64AccountData) { + const bufAccountData = Buffer.from(base64AccountData, "base64"); + decodedAccountData = this.getDecodedUserAccountData(bufAccountData); + } + + const dataTrieChanges = sa.dataTrieChanges; + + + let allDecodedEsdtData: any[] = []; + if (dataTrieChanges) { + for (const dataTrieChange of dataTrieChanges) { + if (dataTrieChange.version === 0) { + console.warn(`Entry #${i}: unsupported dataTrieChanges version 0`); + } else { + + const decodedEsdtData = this.getDecodedEsdtData(address, dataTrieChange); + + if (decodedEsdtData) { + if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { + decodedEsdtData.value = '0'; + } + allDecodedEsdtData.push(decodedEsdtData); + } + } + } + } + + if (decodedAccountData || allDecodedEsdtData.length > 0) { + const groupedEsdtStates = allDecodedEsdtData.reduce>( + (acc, state) => { + const typeName = ESDTType[state.type]; // numeric -> string + if (typeName) { + acc[typeName].push(state); + } + + return acc; + }, + { + Fungible: [], + NonFungible: [], + NonFungibleV2: [], + SemiFungible: [], + MetaFungible: [], + DynamicNFT: [], + DynamicSFT: [], + DynamicMeta: [], + } + ); + if (allDecoded[address] === undefined) allDecoded[address] = []; + const newAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; + + const accountChanges = this.decodeAccountChanges(sa.accountChanges); + + allDecoded[address].push({ + entry: `Entry #${i}`, + accountState: decodedAccountData, + esdtStates: groupedEsdtStates, + accountChanges, + newAccount, + }); + } + }); + if (Object.keys(allDecoded).length === 0) continue; + + allAccounts[address] = [...(allAccounts[address] || []), ...Object.values(allDecoded).flat()]; + } + + return allAccounts; + } + + static decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { + const accounts = blockWithStateChanges.stateAccessesPerAccounts; + const finalStates: Record = {}; + + for (const accountHex of Object.keys(accounts)) { + const address = this.bech32FromHex(accountHex); + const esdtOccured: Record = {}; + + const { stateAccess } = accounts[accountHex] || {}; + let finalAccountChangesRaw: AccountChangesRaw = AccountChangesRaw.NoChange; + + let finalNewAccount = false; + + let finalAccountState: AccountState | undefined = undefined; + const finalEsdtStates = { + Fungible: [] as EsdtState[], + NonFungible: [] as EsdtState[], + NonFungibleV2: [] as EsdtState[], + SemiFungible: [] as EsdtState[], + MetaFungible: [] as EsdtState[], + DynamicNFT: [] as EsdtState[], + DynamicSFT: [] as EsdtState[], + DynamicMeta: [] as EsdtState[], + }; + + for (let i = stateAccess.length - 1; i >= 0; i--) { + const sa = stateAccess[i]; + const currentAccountChangesRaw = sa.accountChanges; + if (currentAccountChangesRaw) { + finalAccountChangesRaw |= currentAccountChangesRaw; + } + + if (!finalNewAccount) { + const currentNewAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; + finalNewAccount = currentNewAccount || finalNewAccount; + } + + const base64AccountData = sa.mainTrieVal; + if (base64AccountData && !finalAccountState) { + const bufAccountData = Buffer.from(base64AccountData, "base64"); + const decodedAccountData = this.getDecodedUserAccountData(bufAccountData); + if (decodedAccountData) { + finalAccountState = decodedAccountData; + } + } + + const dataTrieChanges = sa.dataTrieChanges; + if (dataTrieChanges) { + for (let i = dataTrieChanges.length - 1; i >= 0; i--) { + const dataTrieChange = dataTrieChanges[i]; + if (dataTrieChange.version === 0) { + console.warn(`Unsupported dataTrieChanges version 0`); + } else if (dataTrieChange.val) { + const decodedEsdtData = this.getDecodedEsdtData(address, dataTrieChange); + if (decodedEsdtData) { + const esdtId = decodedEsdtData.identifier; + if (!esdtOccured[esdtId]) { + const typeName = ESDTType[decodedEsdtData.type] as keyof typeof finalEsdtStates; // numeric -> string + if (typeName) { + if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { + decodedEsdtData.value = '0'; + } + finalEsdtStates[typeName].push(decodedEsdtData); + esdtOccured[esdtId] = true; + } + } + } + } + } + + } + } + finalStates[address] = { + accountState: finalAccountState, + esdtState: finalEsdtStates, + accountChanges: this.decodeAccountChanges(finalAccountChangesRaw), + isNewAccount: finalNewAccount, + }; + } + return finalStates; + } + + + static getFinalStates(stateChanges: Record) { + const finalStates: Record = {}; + + + for (const [address, entries] of Object.entries(stateChanges)) { + let finalAccountState: AccountState | undefined = undefined; + const finalEsdtStates = { + Fungible: [] as EsdtState[], + NonFungible: [] as EsdtState[], + NonFungibleV2: [] as EsdtState[], + SemiFungible: [] as EsdtState[], + MetaFungible: [] as EsdtState[], + DynamicNFT: [] as EsdtState[], + DynamicSFT: [] as EsdtState[], + DynamicMeta: [] as EsdtState[], + }; + const finalAccountChanges: AccountChanges = new AccountChanges({ + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false + }); + + let finalNewAccount = false; + + for (const entry of entries) { + const currentAccountState: AccountState = entry.accountState; + const currentEsdtStates = entry.esdtStates; + const currentAccountChanges = entry.accountChanges; + const currentNewAccount = entry.newAccount as boolean; + + + finalNewAccount = finalNewAccount ? finalNewAccount : currentNewAccount; + + finalAccountState = currentAccountState; + + (Object.entries(finalAccountChanges) as [keyof typeof finalAccountChanges, boolean][]).forEach( + ([key, value]) => { + finalAccountChanges[key] = value || currentAccountChanges[key]; + } + ); + + + (Object.entries(currentEsdtStates) as [keyof typeof finalEsdtStates, any[]][]).forEach( + ([tokenType, tokenChanges]) => { + for (const tokenChange of tokenChanges) { + let index = finalEsdtStates[tokenType].findIndex((item: any) => item.key === tokenChange.key); + index = index !== -1 ? index : finalEsdtStates[tokenType].length; + finalEsdtStates[tokenType][index] = tokenChange; + } + + } + ); + } + + finalStates[address] = { + accountState: finalAccountState, + esdtState: finalEsdtStates, + accountChanges: finalAccountChanges, + isNewAccount: finalNewAccount, + }; + } + + return finalStates; + } +} diff --git a/src/state-changes/utils/state-changes.utils.ts b/src/state-changes/utils/state-changes.utils.ts deleted file mode 100644 index c751c2d6d..000000000 --- a/src/state-changes/utils/state-changes.utils.ts +++ /dev/null @@ -1,410 +0,0 @@ -import { Address } from "@multiversx/sdk-core/out"; -import { UserAccountData } from "./user_account.pb"; -import { ESDigitalToken } from "./esdt"; -import { TokenParser } from "./token.parser"; -import { AccountChanges, AccountChangesRaw, AccountState, BlockWithStateChangesRaw, DataTrieChange, DataTrieChangeOperation, EsdtState, ESDTType, StateAccessOperation, StateAccessPerAccountRaw, StateChanges } from "../entities"; -import { CacheInfo } from "src/utils/cache.info"; -import { CacheService } from "@multiversx/sdk-nestjs-cache"; - -const bech32FromHex = (hex: any) => { - const clean = hex.startsWith("0x") ? hex.slice(2) : hex; - return Address.newFromHex(clean).toBech32(); -}; -const bech32FromBytes = (u8: any) => (u8 && u8.length ? Address.newFromHex(bytesToHex(u8)).toBech32() : ""); - -const bytesToHex = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("hex") : ""); -const bytesToBase64 = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("base64") : ""); -// const bytesToString = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("utf8") : ""); -const longToString = (v: any) => - v == null ? "" : (typeof v === "object" && typeof v.toString === "function" ? v.toString() : String(v)); -function bigEndianBytesToBigInt(u8: any) { - let v = BigInt(0); - for (const b of u8) { - v = (v << BigInt(8)) + BigInt(b); - } - return v; -} - -/** - * MultiversX custom sign & magnitude BigInt for proto: - * - zero => [0x00, 0x00] - * - positive non-zero => 0x00 || magnitude (big-endian) - * - (if present) negative => 0x01 || magnitude - * Fallback: if first byte is not a sign marker, treat whole buffer as positive magnitude. - */ -function decodeMxSignMagBigInt(u8: any) { - if (!u8 || u8.length === 0) return BigInt(0); - - // canonical zero used by the serializer - if (u8.length === 2 && u8[0] === 0x00 && u8[1] === 0x00) return BigInt(0); - - const sign = u8[0]; - if (sign === 0x00 || sign === 0x01) { - const mag = u8.slice(1); - const m = bigEndianBytesToBigInt(mag); - return sign === 0x01 ? -m : m; - } - - // fallback for legacy "magnitude only" encodings - return bigEndianBytesToBigInt(u8); -} - -function getDecodedUserAccountData(buf: any) { - try { - const msg = UserAccountData.decode(buf); - - const balance = decodeMxSignMagBigInt(msg.Balance); - const devReward = decodeMxSignMagBigInt(msg.DeveloperReward); - const address = bech32FromBytes(msg.Address); - const ownerAddress = bech32FromBytes(msg.OwnerAddress); - - const data: AccountState = { - nonce: longToString(msg.Nonce), - balance: balance.toString(), - developerReward: devReward.toString(), - address, - ownerAddress, - codeHash: bytesToBase64(msg.CodeHash), - rootHash: bytesToBase64(msg.RootHash), - userName: bytesToHex(msg.UserName), - codeMetadata: bytesToHex(msg.CodeMetadata), - }; - - const filteredData = Object.fromEntries( - Object.entries(data).filter(([_, v]) => v !== undefined && v !== null && v !== '') - ) as AccountState; - console.log(filteredData) - return filteredData; - } catch (e: any) { - console.warn(`Could not decode as UserAccountData: ${e.message}`); - return null; - } -} - -export function decodeAccountChanges(flags: number | undefined): AccountChanges { - if (!flags) { - return new AccountChanges({ - nonceChanged: false, - balanceChanged: false, - codeHashChanged: false, - rootHashChanged: false, - developerRewardChanged: false, - ownerAddressChanged: false, - userNameChanged: false, - codeMetadataChanged: false, - }); - } - return new AccountChanges({ - nonceChanged: (flags & AccountChangesRaw.NonceChanged) !== 0, - balanceChanged: (flags & AccountChangesRaw.BalanceChanged) !== 0, - codeHashChanged: (flags & AccountChangesRaw.CodeHashChanged) !== 0, - rootHashChanged: (flags & AccountChangesRaw.RootHashChanged) !== 0, - developerRewardChanged: (flags & AccountChangesRaw.DeveloperRewardChanged) !== 0, - ownerAddressChanged: (flags & AccountChangesRaw.OwnerAddressChanged) !== 0, - userNameChanged: (flags & AccountChangesRaw.UserNameChanged) !== 0, - codeMetadataChanged: (flags & AccountChangesRaw.CodeMetadataChanged) !== 0, - }); -} - -export function getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { - const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); - const esdtPrefix = 'ELRONDesdt'; - try { - const keyRawBuf = Buffer.from(dataTrieChange.key, "base64"); - const keyRaw = keyRawBuf.toString(); - if (keyRaw.startsWith(esdtPrefix)) { - const keyBuf = keyRawBuf.slice(esdtPrefix.length); - const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufTrieLeafValue as Uint8Array); - - const valueBigInt: bigint = decodeMxSignMagBigInt(msgEsdtData.Value); - const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(keyBuf); - console.log(`key: ${dataTrieChange.key}, identifier: ${identifier}, nonceHex: ${nonceHex}, type: ${msgEsdtData.Type}, value: ${valueBigInt.toString()}`); - return { - identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, - nonce: parseInt(nonceHex, 16).toString(), - type: msgEsdtData.Type, - value: valueBigInt.toString(), - propertiesHex: bytesToHex(msgEsdtData.Properties), - reservedHex: bytesToHex(msgEsdtData.Reserved), - tokenMetaData: msgEsdtData.TokenMetaData ?? null, - }; - } else { - //TODO: handle if needed - - return null; - } - } catch (e: any) { - console.warn(`Could not decode as EsdtData: ${e.message}`); - console.log(address, ':') - console.dir(dataTrieChange) - return null; - } -} - -export function decodeStateChangesRaw(blockWithStateChanges: BlockWithStateChangesRaw) { - const allAccounts: Record = {}; - const accounts = blockWithStateChanges.stateAccessesPerAccounts || {}; - - for (const accountHex of Object.keys(accounts)) { - const address = bech32FromHex(accountHex); - - const { stateAccess = [] } = accounts[accountHex] || {}; - const allDecoded: Record = {}; - stateAccess.forEach((sa: StateAccessPerAccountRaw, i: number) => { - - const base64AccountData = sa.mainTrieVal; - let decodedAccountData: any = null - if (base64AccountData) { - const bufAccountData = Buffer.from(base64AccountData, "base64"); - decodedAccountData = getDecodedUserAccountData(bufAccountData); - } - - const dataTrieChanges = sa.dataTrieChanges; - - - let allDecodedEsdtData: any[] = []; - if (dataTrieChanges) { - for (const dataTrieChange of dataTrieChanges) { - if (dataTrieChange.version === 0) { - console.warn(`Entry #${i}: unsupported dataTrieChanges version 0`); - } else { - - const decodedEsdtData = getDecodedEsdtData(address, dataTrieChange); - - if (decodedEsdtData) { - if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { - decodedEsdtData.value = '0'; - } - allDecodedEsdtData.push(decodedEsdtData); - } - } - } - } - - if (decodedAccountData || allDecodedEsdtData.length > 0) { - const groupedEsdtStates = allDecodedEsdtData.reduce>( - (acc, state) => { - const typeName = ESDTType[state.type]; // numeric -> string - if (typeName) { - acc[typeName].push(state); - } - - return acc; - }, - { - Fungible: [], - NonFungible: [], - NonFungibleV2: [], - SemiFungible: [], - MetaFungible: [], - DynamicNFT: [], - DynamicSFT: [], - DynamicMeta: [], - } - ); - if (allDecoded[address] === undefined) allDecoded[address] = []; - const newAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; - - const accountChanges = decodeAccountChanges(sa.accountChanges); - - allDecoded[address].push({ - entry: `Entry #${i}`, - accountState: decodedAccountData, - esdtStates: groupedEsdtStates, - accountChanges, - newAccount, - }); - } - }); - if (Object.keys(allDecoded).length === 0) continue; - - allAccounts[address] = [...(allAccounts[address] || []), ...Object.values(allDecoded).flat()]; - } - - return allAccounts; -} - -export function decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { - const accounts = blockWithStateChanges.stateAccessesPerAccounts; - const finalStates: Record = {}; - - for (const accountHex of Object.keys(accounts)) { - const address = bech32FromHex(accountHex); - const esdtOccured: Record = {}; - - const { stateAccess } = accounts[accountHex] || {}; - let finalAccountChangesRaw: AccountChangesRaw = AccountChangesRaw.NoChange; - - let finalNewAccount = false; - - let finalAccountState: AccountState | undefined = undefined; - const finalEsdtStates = { - Fungible: [] as EsdtState[], - NonFungible: [] as EsdtState[], - NonFungibleV2: [] as EsdtState[], - SemiFungible: [] as EsdtState[], - MetaFungible: [] as EsdtState[], - DynamicNFT: [] as EsdtState[], - DynamicSFT: [] as EsdtState[], - DynamicMeta: [] as EsdtState[], - }; - - for (let i = stateAccess.length - 1; i >= 0; i--) { - const sa = stateAccess[i]; - const currentAccountChangesRaw = sa.accountChanges; - if (currentAccountChangesRaw) { - finalAccountChangesRaw |= currentAccountChangesRaw; - } - - if (!finalNewAccount) { - const currentNewAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; - finalNewAccount = currentNewAccount || finalNewAccount; - } - - const base64AccountData = sa.mainTrieVal; - if (base64AccountData && !finalAccountState) { - const bufAccountData = Buffer.from(base64AccountData, "base64"); - const decodedAccountData = getDecodedUserAccountData(bufAccountData); - if (decodedAccountData) { - finalAccountState = decodedAccountData; - } - } - - const dataTrieChanges = sa.dataTrieChanges; - if (dataTrieChanges) { - for (let i = dataTrieChanges.length - 1; i >= 0; i--) { - const dataTrieChange = dataTrieChanges[i]; - if (dataTrieChange.version === 0) { - console.warn(`Unsupported dataTrieChanges version 0`); - } else if (dataTrieChange.val) { - const decodedEsdtData = getDecodedEsdtData(address, dataTrieChange); - if (decodedEsdtData) { - const esdtId = decodedEsdtData.identifier; - if (!esdtOccured[esdtId]) { - const typeName = ESDTType[decodedEsdtData.type] as keyof typeof finalEsdtStates; // numeric -> string - if (typeName) { - if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { - decodedEsdtData.value = '0'; - } - finalEsdtStates[typeName].push(decodedEsdtData); - esdtOccured[esdtId] = true; - } - } - } - } - } - - } - } - finalStates[address] = { - accountState: finalAccountState, - esdtState: finalEsdtStates, - accountChanges: decodeAccountChanges(finalAccountChangesRaw), - isNewAccount: finalNewAccount, - }; - } - return finalStates; -} - - -export function getFinalStates(stateChanges: Record) { - const finalStates: Record = {}; - - - for (const [address, entries] of Object.entries(stateChanges)) { - let finalAccountState: AccountState | undefined = undefined; - const finalEsdtStates = { - Fungible: [] as EsdtState[], - NonFungible: [] as EsdtState[], - NonFungibleV2: [] as EsdtState[], - SemiFungible: [] as EsdtState[], - MetaFungible: [] as EsdtState[], - DynamicNFT: [] as EsdtState[], - DynamicSFT: [] as EsdtState[], - DynamicMeta: [] as EsdtState[], - }; - const finalAccountChanges: AccountChanges = new AccountChanges({ - nonceChanged: false, - balanceChanged: false, - codeHashChanged: false, - rootHashChanged: false, - developerRewardChanged: false, - ownerAddressChanged: false, - userNameChanged: false, - codeMetadataChanged: false - }); - - let finalNewAccount = false; - - for (const entry of entries) { - const currentAccountState: AccountState = entry.accountState; - const currentEsdtStates = entry.esdtStates; - const currentAccountChanges = entry.accountChanges; - const currentNewAccount = entry.newAccount as boolean; - - - finalNewAccount = finalNewAccount ? finalNewAccount : currentNewAccount; - - finalAccountState = currentAccountState; - - (Object.entries(finalAccountChanges) as [keyof typeof finalAccountChanges, boolean][]).forEach( - ([key, value]) => { - finalAccountChanges[key] = value || currentAccountChanges[key]; - } - ); - - - (Object.entries(currentEsdtStates) as [keyof typeof finalEsdtStates, any[]][]).forEach( - ([tokenType, tokenChanges]) => { - for (const tokenChange of tokenChanges) { - let index = finalEsdtStates[tokenType].findIndex((item: any) => item.key === tokenChange.key); - index = index !== -1 ? index : finalEsdtStates[tokenType].length; - finalEsdtStates[tokenType][index] = tokenChange; - } - - } - ); - } - - finalStates[address] = { - accountState: finalAccountState, - esdtState: finalEsdtStates, - accountChanges: finalAccountChanges, - isNewAccount: finalNewAccount, - }; - } - - return finalStates; -} - -export async function isDbValid(cacheService: CacheService): Promise { - const timestampsMs: (string | undefined)[] = await Promise.all([ - cacheService.get(CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(0).key), - cacheService.get(CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(1).key), - cacheService.get(CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(2).key), - cacheService.get(CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(4294967295).key), - ]) as (string | undefined)[];; - - const numericValues = timestampsMs - .map((timestampMsRaw: string | null | undefined) => - timestampMsRaw !== null && timestampMsRaw !== undefined ? Number(timestampMsRaw) : undefined - ) - .filter((timestampMs: number | undefined): timestampMs is number => timestampMs !== undefined && !isNaN(timestampMs)); - - const minTimestamp = numericValues.length > 0 ? Math.min(...numericValues) : null; - - if (minTimestamp === null) { - return false; - } - - //@ts-ignore - const diff = Date.now() - minTimestamp; - // console.log('Min timestamp from cache:', minTimestamp); - // console.log('Current timestamp:', Date.now()); - // console.log('diff: ', diff); - const blockTime = 6000; - return diff <= blockTime; -} - - -// key: RUxST05EZXNkdFNIQVJFUy0zMzc4ZGKT, identifier: SHARES-3378db, nonceHex: efbfbd \ No newline at end of file From f65e29be07b59412a9bd2f31f957f4d12074db47 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 9 Oct 2025 18:04:47 +0300 Subject: [PATCH 33/90] fixes + clean --- .../accounts-v2/account.controller.v2.ts | 1387 +---------------- .../accounts-v2/account.service.v2.ts | 534 +------ .../state.changes.consumer.service.ts | 69 +- .../utils/state-changes.decoder.ts | 2 +- 4 files changed, 77 insertions(+), 1915 deletions(-) diff --git a/src/endpoints/accounts-v2/account.controller.v2.ts b/src/endpoints/accounts-v2/account.controller.v2.ts index d3a587922..ff4b32f56 100644 --- a/src/endpoints/accounts-v2/account.controller.v2.ts +++ b/src/endpoints/accounts-v2/account.controller.v2.ts @@ -1,201 +1,20 @@ -import { Controller, DefaultValuePipe, Get, HttpException, HttpStatus, NotFoundException, Param, Query, UseInterceptors } from '@nestjs/common'; -import { ApiExcludeEndpoint, ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { Controller, Get, NotFoundException, Param, Query, UseInterceptors } from '@nestjs/common'; +import { ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { AccountServiceV2 } from './account.service.v2'; import { AccountDetailed } from './entities/account.detailed'; -import { Account } from './entities/account'; -import { AccountDeferred } from './entities/account.deferred'; -import { TokenService } from '../tokens/token.service'; -import { TokenWithBalance } from '../tokens/entities/token.with.balance'; -import { DelegationLegacyService } from '../delegation.legacy/delegation.legacy.service'; -import { AccountDelegationLegacy } from '../delegation.legacy/entities/account.delegation.legacy'; -import { AccountKey } from './entities/account.key'; -import { NftAccount } from '../nfts/entities/nft.account'; -import { NftType } from '../nfts/entities/nft.type'; -import { WaitingList } from '../waiting-list/entities/waiting.list'; -import { WaitingListService } from '../waiting-list/waiting.list.service'; -import { StakeService } from '../stake/stake.service'; -import { NftService } from '../nfts/nft.service'; -import { TransactionStatus } from '../transactions/entities/transaction.status'; -import { TransactionService } from '../transactions/transaction.service'; -import { DeployedContract } from './entities/deployed.contract'; -import { SmartContractResult } from '../sc-results/entities/smart.contract.result'; -import { SmartContractResultService } from '../sc-results/scresult.service'; -import { CollectionService } from '../collections/collection.service'; -import { NftCollectionWithRoles } from '../collections/entities/nft.collection.with.roles'; -import { SortOrder } from 'src/common/entities/sort.order'; -import { AccountHistory } from "./entities/account.history"; -import { AccountEsdtHistory } from "./entities/account.esdt.history"; -import { EsdtDataSource } from '../esdt/entities/esdt.data.source'; -import { TransferService } from '../transfers/transfer.service'; -import { Transaction } from '../transactions/entities/transaction'; -import { ProviderStake } from '../stake/entities/provider.stake'; -import { TokenDetailedWithBalance } from '../tokens/entities/token.detailed.with.balance'; -import { NftCollectionAccount } from '../collections/entities/nft.collection.account'; -import { TokenWithRoles } from '../tokens/entities/token.with.roles'; -import { ParseAddressPipe, ParseTokenPipe, OriginLogger, ParseArrayPipe, ParseBlockHashPipe, ParseCollectionPipe, ParseNftPipe, ParseBoolPipe, ParseEnumArrayPipe, ParseEnumPipe, ParseIntPipe, ParseTokenOrNftPipe, ParseTransactionHashPipe, ParseAddressArrayPipe, ApplyComplexity, ParseNftArrayPipe } from '@multiversx/sdk-nestjs-common'; -import { QueryPagination } from 'src/common/entities/query.pagination'; -import { TransactionQueryOptions } from '../transactions/entities/transactions.query.options'; -import { TokenWithRolesFilter } from '../tokens/entities/token.with.roles.filter'; -import { CollectionFilter } from '../collections/entities/collection.filter'; -import { TokenFilter } from '../tokens/entities/token.filter'; -import { NftFilter } from '../nfts/entities/nft.filter'; -import { NftQueryOptions } from '../nfts/entities/nft.query.options'; -import { TransactionFilter } from '../transactions/entities/transaction.filter'; -import { TransactionDetailed } from '../transactions/entities/transaction.detailed'; -import { AccountDelegation } from '../stake/entities/account.delegation'; -import { DelegationService } from '../delegation/delegation.service'; -import { TokenType } from '../tokens/entities/token.type'; -import { ContractUpgrades } from './entities/contract.upgrades'; -import { AccountVerification } from './entities/account.verification'; -import { AccountQueryOptions } from './entities/account.query.options'; -import { AccountSort } from './entities/account.sort'; -import { AccountHistoryFilter } from './entities/account.history.filter'; -import { ParseArrayPipeOptions } from '@multiversx/sdk-nestjs-common/lib/pipes/entities/parse.array.options'; -import { NodeStatusRaw } from '../nodes/entities/node.status'; -import { AccountKeyFilter } from './entities/account.key.filter'; -import { ScamType } from 'src/common/entities/scam-type.enum'; +import { ParseAddressPipe, ParseBoolPipe, ParseIntPipe } from '@multiversx/sdk-nestjs-common'; import { DeepHistoryInterceptor } from 'src/interceptors/deep-history.interceptor'; -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('/v2') +import { NoCache } from '@multiversx/sdk-nestjs-cache'; +@Controller('') @ApiTags('accounts') export class AccountControllerV2 { - private readonly logger = new OriginLogger(AccountControllerV2.name); constructor( private readonly accountServiceV2: AccountServiceV2, - private readonly tokenService: TokenService, - private readonly nftService: NftService, - private readonly delegationLegacyService: DelegationLegacyService, - private readonly waitingListService: WaitingListService, - private readonly stakeService: StakeService, - private readonly transactionService: TransactionService, - private readonly scResultService: SmartContractResultService, - private readonly collectionService: CollectionService, - private readonly transferService: TransferService, - private readonly delegationService: DelegationService, ) { } - @Get("/accounts") - @ApiOperation({ summary: 'Accounts details', description: 'Returns all accounts available on blockchain. By default it returns 25 accounts' }) - @ApiOkResponse({ type: [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 }) - @ApiQuery({ name: 'ownerAddress', description: 'Search by owner address', required: false }) - @ApiQuery({ name: 'sort', description: 'Sort criteria (balance / timestamp)', required: false, enum: AccountSort }) - @ApiQuery({ name: 'order', description: 'Sort order (asc/desc)', required: false, enum: SortOrder }) - @ApiQuery({ name: 'isSmartContract', description: 'Filter accounts by whether they are smart contract or not', required: false }) - @ApiQuery({ name: 'withOwnerAssets', description: 'Return a list accounts with owner assets', required: false }) - @ApiQuery({ name: 'withDeployInfo', description: 'Include deployedAt and deployTxHash fields in the result', required: false }) - @ApiQuery({ name: 'withTxCount', description: 'Include txCount field in the result', required: false }) - @ApiQuery({ name: 'withScrCount', description: 'Include scrCount field in the result', required: false }) - @ApiQuery({ name: 'name', description: 'Filter accounts by assets name', required: false }) - @ApiQuery({ name: 'tags', description: 'Filter accounts by assets tags', required: false }) - @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", ParseArrayPipe) tags?: string[], - @Query('sort', new ParseEnumPipe(AccountSort)) sort?: AccountSort, - @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @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, - withOwnerAssets, - withDeployInfo, - withTxCount, - withScrCount, - name, - tags, - excludeTags, - hasAssets, - search, - }); - queryOptions.validate(size); - return this.accountServiceV2.getAccounts( - new QueryPagination({ from, size }), - queryOptions, - ); - } - - @Get("/accounts/count") - @ApiOperation({ summary: 'Total number of accounts', description: 'Returns total number of accounts available on blockchain' }) - @ApiOkResponse({ type: Number }) - @ApiQuery({ name: 'ownerAddress', description: 'Search by owner address', required: false }) - @ApiQuery({ name: 'isSmartContract', description: 'Return total smart contracts count', required: false }) - @ApiQuery({ name: 'name', description: 'Filter accounts by assets name', required: false }) - @ApiQuery({ name: 'tags', description: 'Filter accounts by assets tags', required: false }) - @ApiQuery({ name: 'search', description: 'Search by account address, assets name', required: false }) - @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 }) - async getAccountsCount( - @Query("ownerAddress", ParseAddressPipe) ownerAddress?: string, - @Query("isSmartContract", ParseBoolPipe) isSmartContract?: boolean, - @Query("name") name?: string, - @Query("tags", ParseArrayPipe) tags?: string[], - @Query("excludeTags", ParseArrayPipe) excludeTags?: string[], - @Query("hasAssets", ParseBoolPipe) hasAssets?: boolean, - @Query("search") search?: string, - ): Promise { - return await this.accountServiceV2.getAccountsCount( - new AccountQueryOptions( - { - ownerAddress, - isSmartContract, - name, - tags, - excludeTags, - hasAssets, - search, - })); - } - - @Get("/accounts/c") - @ApiExcludeEndpoint() - async getAccountsCountAlternative( - @Query("ownerAddress", ParseAddressPipe) ownerAddress?: string, - @Query("isSmartContract", ParseBoolPipe) isSmartContract?: boolean, - @Query("name") name?: string, - @Query("tags", ParseArrayPipe) tags?: string[], - @Query("excludeTags", ParseArrayPipe) excludeTags?: string[], - @Query("hasAssets", ParseBoolPipe) hasAssets?: boolean, - @Query("search") search?: string, - ): Promise { - return await this.accountServiceV2.getAccountsCount( - new AccountQueryOptions( - { - ownerAddress, - isSmartContract, - name, - tags, - excludeTags, - hasAssets, - search, - })); - } - - @Get("/accounts/:address") + @Get("/v2/accounts/:address") @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 }) @@ -205,7 +24,7 @@ export class AccountControllerV2 { @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 }) - // @NoCache() + @NoCache() async getAccountDetails( @Param('address', ParseAddressPipe) address: string, @Query('withGuardianInfo', ParseBoolPipe) withGuardianInfo?: boolean, @@ -225,1196 +44,4 @@ export class AccountControllerV2 { return account; } - - @Get("/accounts/:address/deferred") - @ApiOperation({ summary: 'Account deferred payment details', description: 'Returns deferred payments from legacy staking' }) - @ApiOkResponse({ type: [AccountDeferred] }) - async getAccountDeferred(@Param('address', ParseAddressPipe) address: string): Promise { - try { - return await this.accountServiceV2.getDeferredAccount(address); - } catch (error) { - this.logger.error(`Error in getAccountDeferred for address ${address}`); - this.logger.error(error); - throw new HttpException('Account not found', HttpStatus.NOT_FOUND); - } - } - - @Get("/accounts/:address/verification") - @ApiOperation({ summary: 'Account verification details', description: 'Returns contract verification details' }) - @ApiOkResponse({ type: AccountVerification }) - async getAccountVerification(@Param('address', ParseAddressPipe) address: string): Promise { - try { - return await this.accountServiceV2.getAccountVerification(address); - } catch (error) { - this.logger.error(`Error in getAccountVerification for address ${address}`); - this.logger.error(error); - throw new HttpException('Account verification not found', HttpStatus.NOT_FOUND); - } - } - - @Get("/accounts/:address/tokens") - @UseInterceptors(DeepHistoryInterceptor) - @ApiOperation({ summary: 'Account tokens', description: 'Returns a list of all available fungible tokens for a given address, together with their balance' }) - @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 }) - @ApiQuery({ name: 'type', description: 'Token type', required: false, enum: TokenType }) - @ApiQuery({ name: 'subType', description: 'Token sub type', required: false, enum: NftSubType }) - @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) - @ApiQuery({ name: 'name', description: 'Search by token name', required: false }) - @ApiQuery({ name: 'identifier', description: 'Search by token identifier', required: false }) - @ApiQuery({ name: 'identifiers', description: 'A comma-separated list of identifiers to filter by', required: false, type: String }) - @ApiQuery({ name: 'includeMetaESDT', description: 'Include MetaESDTs in response', required: false, type: Boolean }) - @ApiQuery({ name: 'timestamp', description: 'Retrieve entries from timestamp', required: false, type: Number }) - @ApiQuery({ name: 'mexPairType', description: 'Token Mex Pair', required: false, enum: MexPairType }) - @ApiOkResponse({ type: [TokenWithBalance] }) - // @NoCache() - async getAccountTokens( - @Param('address', ParseAddressPipe) address: string, - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - @Query('type', new ParseEnumPipe(TokenType)) type?: TokenType, - @Query('subType', new ParseEnumPipe(NftSubType)) subType?: NftSubType, - @Query('search') search?: string, - @Query('name') name?: string, - @Query('identifier') identifier?: string, - @Query('identifiers', ParseArrayPipe) identifiers?: string[], - @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, - @Query('timestamp', ParseIntPipe) _timestamp?: number, - @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], - ): Promise { - try { - return await this.tokenService.getTokensForAddressFromDb(address, new QueryPagination({ from, size }), new TokenFilter({ type, subType, search, name, identifier, identifiers, includeMetaESDT, mexPairType })); - } catch (error) { - this.logger.error(`Error in getAccountTokens for address ${address}`); - this.logger.error(error); - // throw new HttpException('Account not found', HttpStatus.NOT_FOUND); - return []; - } - } - - @Get("/accounts/:address/tokens/count") - @UseInterceptors(DeepHistoryInterceptor) - @ApiOperation({ summary: 'Account token count', description: 'Returns the total number of tokens for a given address' }) - @ApiQuery({ name: 'type', description: 'Token type', required: false, enum: TokenType }) - @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) - @ApiQuery({ name: 'name', description: 'Search by token name', required: false }) - @ApiQuery({ name: 'identifier', description: 'Search by token identifier', required: false }) - @ApiQuery({ name: 'identifiers', description: 'A comma-separated list of identifiers to filter by', required: false, type: String }) - @ApiQuery({ name: 'includeMetaESDT', description: 'Include MetaESDTs in response', required: false, type: Boolean }) - @ApiQuery({ name: 'timestamp', description: 'Retrieve entries from timestamp', required: false, type: Number }) - @ApiQuery({ name: 'mexPairType', description: 'Token Mex Pair', required: false, enum: MexPairType }) - @ApiOkResponse({ type: Number }) - async getTokenCount( - @Param('address', ParseAddressPipe) address: string, - @Query('type', new ParseEnumPipe(TokenType)) type?: TokenType, - @Query('search') search?: string, - @Query('name') name?: string, - @Query('identifier') identifier?: string, - @Query('identifiers', ParseArrayPipe) identifiers?: string[], - @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, - @Query('timestamp', ParseIntPipe) _timestamp?: number, - @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], - ): Promise { - try { - return await this.tokenService.getTokenCountForAddress(address, new TokenFilter({ type, search, name, identifier, identifiers, includeMetaESDT, mexPairType })); - } catch (error) { - this.logger.error(`Error in getTokenCount for address ${address}`); - this.logger.error(error); - // throw new HttpException('Account not found', HttpStatus.NOT_FOUND); - return 0; - } - } - - @Get("/accounts/:address/tokens/c") - @UseInterceptors(DeepHistoryInterceptor) - @ApiExcludeEndpoint() - async getTokenCountAlternative( - @Param('address', ParseAddressPipe) address: string, - @Query('type', new ParseEnumPipe(TokenType)) type?: TokenType, - @Query('search') search?: string, - @Query('name') name?: string, - @Query('identifier') identifier?: string, - @Query('identifiers', ParseArrayPipe) identifiers?: string[], - @Query('includeMetaESDT', ParseBoolPipe) includeMetaESDT?: boolean, - @Query('timestamp', ParseIntPipe) _timestamp?: number, - @Query('mexPairType', new ParseEnumArrayPipe(MexPairType)) mexPairType?: MexPairType[], - ): Promise { - try { - return await this.tokenService.getTokenCountForAddress(address, new TokenFilter({ type, search, name, identifier, identifiers, includeMetaESDT, mexPairType })); - } catch (error) { - this.logger.error(`Error in getTokenCount for address ${address}`); - this.logger.error(error); - // throw new HttpException('Account not found', HttpStatus.NOT_FOUND); - return 0; - } - } - - @Get("/accounts/:address/tokens/:token") - @UseInterceptors(DeepHistoryInterceptor) - @ApiOkResponse({ type: TokenWithBalance }) - @ApiOperation({ summary: 'Account token details', description: 'Returns details about a specific fungible token from a given address' }) - @ApiQuery({ name: 'timestamp', description: 'Retrieve entries from timestamp', required: false, type: Number }) - // @NoCache() - async getAccountToken( - @Param('address', ParseAddressPipe) address: string, - @Param('token', ParseTokenOrNftPipe) token: string, - @Query('timestamp', ParseIntPipe) _timestamp?: number, - ): Promise { - const result = await this.tokenService.getTokenForAddressFromDb(address, token); - if (!result) { - throw new HttpException('Token for given account not found', HttpStatus.NOT_FOUND); - } - - return result; - } - - @Get("/accounts/:address/roles/collections") - @ApiOperation({ summary: 'Account collections', description: 'Returns NFT/SFT/MetaESDT collections where the account is owner or has some special roles assigned to it' }) - @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 }) - @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) - @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) - @ApiQuery({ name: 'subType', description: 'Filter by type (NonFungibleESDTv2/DynamicNonFungibleESDT/DynamicSemiFungibleESDT)', required: false }) - @ApiQuery({ name: 'owner', description: 'Filter by collection owner', required: false }) - @ApiQuery({ name: 'canCreate', description: 'Filter by property canCreate (boolean)', required: false }) - @ApiQuery({ name: 'canBurn', description: 'Filter by property canBurn (boolean)', required: false }) - @ApiQuery({ name: 'canAddQuantity', description: 'Filter by property canAddQuantity (boolean)', required: false }) - @ApiQuery({ name: 'canUpdateAttributes', description: 'Filter by property canUpdateAttributes (boolean)', required: false }) - @ApiQuery({ name: 'canAddUri', description: 'Filter by property canAddUri (boolean)', required: false }) - @ApiQuery({ name: 'canTransferRole', description: 'Filter by property canTransferRole (boolean)', required: false }) - @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude collections of type "MetaESDT" in the response', required: false, type: Boolean }) - @ApiOkResponse({ type: [NftCollectionWithRoles] }) - async getAccountCollectionsWithRoles( - @Param('address', ParseAddressPipe) address: string, - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - @Query('search') search?: string, - @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], - @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('owner', ParseAddressPipe) owner?: string, - @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, - new CollectionFilter({ - search, - type, - subType, - owner, - canCreate, - canBurn, - canAddQuantity, - canUpdateAttributes, - canAddUri, - canTransferRole, - excludeMetaESDT, - }), new QueryPagination({ from, size })); - } - - @Get("/accounts/:address/roles/collections/count") - @ApiOperation({ summary: 'Account collection count', description: 'Returns the total number of NFT/SFT/MetaESDT collections where the account is owner or has some special roles assigned to it' }) - @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) - @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) - @ApiQuery({ name: 'subType', description: 'Filter by type (NonFungibleESDTv2/DynamicNonFungibleESDT/DynamicSemiFungibleESDT)', required: false }) - @ApiQuery({ name: 'owner', description: 'Filter by collection owner', required: false }) - @ApiQuery({ name: 'canCreate', description: 'Filter by property canCreate (boolean)', required: false }) - @ApiQuery({ name: 'canBurn', description: 'Filter by property canCreate (boolean)', required: false }) - @ApiQuery({ name: 'canAddQuantity', description: 'Filter by property canAddQuantity (boolean)', required: false }) - @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude collections of type "MetaESDT" in the response', required: false, type: Boolean }) - @ApiOkResponse({ type: Number }) - async getCollectionWithRolesCount( - @Param('address', ParseAddressPipe) address: string, - @Query('search') search?: string, - @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], - @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('owner', ParseAddressPipe) owner?: string, - @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 })); - } - - @Get("/accounts/:address/roles/collections/c") - @ApiExcludeEndpoint() - async getCollectionCountAlternative( - @Param('address', ParseAddressPipe) address: string, - @Query('search') search?: string, - @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], - @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('owner', ParseAddressPipe) owner?: string, - @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, - })); - } - - @Get("/accounts/:address/roles/collections/:collection") - @ApiOperation({ summary: 'Account collection details', description: 'Returns details about a specific NFT/SFT/MetaESDT collection from a given address' }) - @ApiOkResponse({ type: NftCollectionWithRoles }) - async getAccountCollection( - @Param('address', ParseAddressPipe) address: string, - @Param('collection', ParseCollectionPipe) collection: string, - ): Promise { - const result = await this.collectionService.getCollectionForAddressWithRole(address, collection); - if (!result) { - throw new NotFoundException('Collection for given account not found'); - } - - return result; - } - - @Get("/accounts/:address/roles/tokens") - @ApiOperation({ summary: 'Account token roles', description: 'Returns fungible token roles where the account is owner or has some special roles assigned to it' }) - @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 }) - @ApiQuery({ name: 'search', description: 'Search by token identifier or name', required: false }) - @ApiQuery({ name: 'owner', description: 'Filter by token owner', required: false }) - @ApiQuery({ name: 'canMint', description: 'Filter by property canMint (boolean)', required: false }) - @ApiQuery({ name: 'canBurn', description: 'Filter by property canBurn (boolean)', required: false }) - @ApiQuery({ name: 'includeMetaESDT', description: 'Include MetaESDTs in response', required: false, type: Boolean }) - @ApiOkResponse({ type: [TokenWithRoles] }) - async getAccountTokensWithRoles( - @Param('address', ParseAddressPipe) address: string, - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - @Query('search') search?: string, - @Query('owner', ParseAddressPipe) owner?: string, - @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 })); - } - - @Get("/accounts/:address/roles/tokens/count") - @ApiOperation({ summary: 'Account token roles count', description: 'Returns the total number of fungible token roles where the account is owner or has some special roles assigned to it' }) - @ApiQuery({ name: 'search', description: 'Search by token identifier or name', required: false }) - @ApiQuery({ name: 'owner', description: 'Filter by token owner', required: false }) - @ApiQuery({ name: 'canMint', description: 'Filter by property canMint (boolean)', required: false }) - @ApiQuery({ name: 'canBurn', description: 'Filter by property canCreate (boolean)', required: false }) - @ApiQuery({ name: 'includeMetaESDT', description: 'Include MetaESDTs in response', required: false, type: Boolean }) - @ApiOkResponse({ type: Number }) - async getTokensWithRolesCount( - @Param('address', ParseAddressPipe) address: string, - @Query('search') search?: string, - @Query('owner', ParseAddressPipe) owner?: string, - @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 })); - } - - @Get("/accounts/:address/roles/tokens/c") - @ApiExcludeEndpoint() - async getTokensWithRolesCountAlternative( - @Param('address', ParseAddressPipe) address: string, - @Query('search') search?: string, - @Query('owner', ParseAddressPipe) owner?: string, - @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 })); - } - - @Get("/accounts/:address/roles/tokens/:identifier") - @ApiOperation({ summary: 'Account token roles details', description: 'Returns details about fungible token roles where the account is owner or has some special roles assigned to it' }) - @ApiOkResponse({ type: TokenWithRoles }) - async getTokenWithRoles( - @Param('address', ParseAddressPipe) address: string, - @Param('identifier', ParseTokenPipe) identifier: string, - ): Promise { - const result = await this.tokenService.getTokenWithRolesForAddress(address, identifier); - if (!result) { - throw new NotFoundException('Token with roles for given account not found'); - } - - return result; - } - - @Get("/accounts/:address/collections") - @ApiOperation({ summary: 'Account collections', description: 'Returns NFT/SFT/MetaESDT collections where the account owns one or more NFTs' }) - @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 }) - @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) - @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) - @ApiQuery({ name: 'subType', description: 'Filter by type (NonFungibleESDTv2/DynamicNonFungibleESDT/DynamicSemiFungibleESDT)', required: false }) - @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude collections of type "MetaESDT" in the response', required: false, type: Boolean }) - @ApiOkResponse({ type: [NftCollectionAccount] }) - async getAccountNftCollections( - @Param('address', ParseAddressPipe) address: string, - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - @Query('search') search?: string, - @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], - @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, - ): Promise { - return await this.collectionService.getCollectionsForAddress( - address, - new CollectionFilter({ search, type, subType, excludeMetaESDT }), - new QueryPagination({ from, size })); - } - - @Get("/accounts/:address/collections/count") - @ApiOperation({ summary: 'Account collection count', description: 'Returns the total number of NFT/SFT/MetaESDT collections where the account is owner or has some special roles assigned to it' }) - @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) - @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) - @ApiQuery({ name: 'subType', description: 'Filter by type (NonFungibleESDTv2/DynamicNonFungibleESDT/DynamicSemiFungibleESDT)', required: false }) - @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude collections of type "MetaESDT" in the response', required: false, type: Boolean }) - @ApiOkResponse({ type: Number }) - async getNftCollectionCount( - @Param('address', ParseAddressPipe) address: string, - @Query('search') search?: string, - @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], - @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, - ): Promise { - return await this.collectionService.getCollectionCountForAddress(address, new CollectionFilter({ search, type, subType, excludeMetaESDT })); - } - - @Get("/accounts/:address/collections/c") - @ApiExcludeEndpoint() - async getNftCollectionCountAlternative( - @Param('address', ParseAddressPipe) address: string, - @Query('search') search?: string, - @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], - @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, - ): Promise { - return await this.collectionService.getCollectionCountForAddress(address, new CollectionFilter({ search, type, subType, excludeMetaESDT })); - } - - @Get("/accounts/:address/collections/:collection") - @ApiOperation({ summary: 'Account collection details', description: 'Returns details about a specific NFT/SFT/MetaESDT collection from a given address' }) - @ApiOkResponse({ type: NftCollectionAccount }) - async getAccountNftCollection( - @Param('address', ParseAddressPipe) address: string, - @Param('collection', ParseCollectionPipe) collection: string, - ): Promise { - const result = await this.collectionService.getCollectionForAddress(address, collection); - if (!result) { - throw new NotFoundException('Collection for given account not found'); - } - - return result; - } - - @Get("/accounts/:address/nfts") - @UseInterceptors(DeepHistoryInterceptor) - @ApiOkResponse({ type: [NftAccount] }) - @ApiOperation({ summary: 'Account NFTs', description: 'Returns a list of all available NFTs/SFTs/MetaESDTs owned by the provided address' }) - @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 }) - @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) - @ApiQuery({ name: 'identifiers', description: 'Filter by identifiers, comma-separated', required: false }) - @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) - @ApiQuery({ name: 'subType', description: 'Filter by type (NonFungibleESDTv2/DynamicNonFungibleESDT/DynamicSemiFungibleESDT)', required: false }) - @ApiQuery({ name: 'collection', description: 'Get all tokens by token collection. Deprecated, replaced by collections parameter', required: false, deprecated: true }) - @ApiQuery({ name: 'collections', description: 'Get all tokens by token collections, comma-separated', required: false }) - @ApiQuery({ name: 'name', description: 'Get all nfts by name', required: false }) - @ApiQuery({ name: 'tags', description: 'Filter by one or more comma-separated tags', required: false }) - @ApiQuery({ name: 'creator', description: 'Return all NFTs associated with a given creator', required: false }) - @ApiQuery({ name: 'hasUris', description: 'Return all NFTs that have one or more uris', required: false }) - @ApiQuery({ name: 'includeFlagged', description: 'Include NFTs that are flagged or not', required: false }) - @ApiQuery({ name: 'withSupply', description: 'Return supply where type = SemiFungibleESDT', required: false }) - @ApiQuery({ name: 'source', description: 'Data source of request', required: false }) - @ApiQuery({ name: 'withScamInfo', description: 'Include scam info in the response', required: false, type: Boolean }) - @ApiQuery({ name: 'computeScamInfo', description: 'Compute scam info in the response', required: false, type: Boolean }) - @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude NFTs of type "MetaESDT" in the response', required: false, type: Boolean }) - @ApiQuery({ name: 'fields', description: 'List of fields to filter by', required: false, isArray: true, style: 'form', explode: false }) - @ApiQuery({ name: 'isScam', description: 'Filter by scam status', required: false, type: Boolean }) - @ApiQuery({ name: 'scamType', description: 'Filter by type (scam/potentialScam)', required: false }) - @ApiQuery({ name: 'timestamp', description: 'Retrieve entry from timestamp', required: false, type: Number }) - // @NoCache() - async getAccountNfts( - @Param('address', ParseAddressPipe) address: string, - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - @Query('search') search?: string, - @Query('identifiers', ParseNftArrayPipe) identifiers?: string[], - @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], - @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('collection') collection?: string, - @Query('collections', ParseArrayPipe) collections?: string[], - @Query('name') name?: string, - @Query('tags', ParseArrayPipe) tags?: string[], - @Query('creator', ParseAddressPipe) creator?: string, - @Query('hasUris', ParseBoolPipe) hasUris?: boolean, - @Query('includeFlagged', ParseBoolPipe) includeFlagged?: boolean, - @Query('withSupply', ParseBoolPipe) withSupply?: boolean, - @Query('source', new ParseEnumPipe(EsdtDataSource)) source?: EsdtDataSource, - @Query('excludeMetaESDT', ParseBoolPipe) excludeMetaESDT?: boolean, - @Query('fields', ParseArrayPipe) fields?: string[], - @Query('isScam', ParseBoolPipe) isScam?: boolean, - @Query('scamType', new ParseEnumPipe(ScamType)) scamType?: ScamType, - @Query('timestamp', ParseIntPipe) _timestamp?: number, - ): Promise { - return await this.nftService.getNftsForAddressFromDb( - address, - new QueryPagination({ from, size }), - new NftFilter({ - search, - identifiers, - type, - subType, - collection, - name, - collections, - tags, - creator, - hasUris, - includeFlagged, - excludeMetaESDT, - isScam, - scamType, - }), - fields, - new NftQueryOptions({ withSupply }), - source - ); - } - - @Get("/accounts/:address/nfts/count") - @UseInterceptors(DeepHistoryInterceptor) - @ApiOperation({ summary: 'Account NFT/SFT tokens count', description: 'Returns the total number of NFT/SFT tokens from a given address, as well as the total number of a certain type of ESDT ' }) - @ApiQuery({ name: 'search', description: 'Search by collection identifier', required: false }) - @ApiQuery({ name: 'identifiers', description: 'Filter by identifiers, comma-separated', required: false }) - @ApiQuery({ name: 'type', description: 'Filter by type (NonFungibleESDT/SemiFungibleESDT/MetaESDT)', required: false }) - @ApiQuery({ name: 'subType', description: 'Filter by subType', required: false }) - @ApiQuery({ name: 'collection', description: 'Get all tokens by token collection', required: false }) - @ApiQuery({ name: 'collections', description: 'Get all tokens by token collections, comma-separated', required: false }) - @ApiQuery({ name: 'name', description: 'Get all nfts by name', required: false }) - @ApiQuery({ name: 'tags', description: 'Filter by one or more comma-separated tags', required: false }) - @ApiQuery({ name: 'creator', description: 'Return all NFTs associated with a given creator', required: false }) - @ApiQuery({ name: 'hasUris', description: 'Return all NFTs that have one or more uris', required: false }) - @ApiQuery({ name: 'includeFlagged', description: 'Include NFTs that are flagged or not', required: false }) - @ApiQuery({ name: 'excludeMetaESDT', description: 'Exclude NFTs of type "MetaESDT" in the response', required: false, type: Boolean }) - @ApiQuery({ name: 'isScam', description: 'Filter by scam status', required: false, type: Boolean }) - @ApiQuery({ name: 'scamType', description: 'Filter by type (scam/potentialScam)', required: false }) - @ApiQuery({ name: 'timestamp', description: 'Retrieve entry from timestamp', required: false, type: Number }) - @ApiOkResponse({ type: Number }) - // @NoCache() - async getNftCount( - @Param('address', ParseAddressPipe) address: string, - @Query('identifiers', ParseNftArrayPipe) identifiers?: string[], - @Query('search') search?: string, - @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], - @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('collection') collection?: string, - @Query('collections', ParseArrayPipe) collections?: string[], - @Query('name') name?: string, - @Query('tags', ParseArrayPipe) tags?: string[], - @Query('creator', ParseAddressPipe) creator?: string, - @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 { - return await this.nftService.getNftCountForAddress(address, new NftFilter({ - search, - identifiers, - type, - subType, - collection, - collections, - name, - tags, - creator, - hasUris, - includeFlagged, - excludeMetaESDT, - isScam, - scamType, - })); - } - - @Get("/accounts/:address/nfts/c") - @UseInterceptors(DeepHistoryInterceptor) - @ApiExcludeEndpoint() - async getNftCountAlternative( - @Param('address', ParseAddressPipe) address: string, - @Query('search') search?: string, - @Query('identifiers', ParseNftArrayPipe) identifiers?: string[], - @Query('type', new ParseEnumArrayPipe(NftType)) type?: NftType[], - @Query('subType', new ParseEnumArrayPipe(NftSubType)) subType?: NftSubType[], - @Query('collection') collection?: string, - @Query('collections', ParseArrayPipe) collections?: string[], - @Query('name') name?: string, - @Query('tags', ParseArrayPipe) tags?: string[], - @Query('creator', ParseAddressPipe) creator?: string, - @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 { - return await this.nftService.getNftCountForAddress(address, new NftFilter({ search, identifiers, type, subType, collection, collections, name, tags, creator, hasUris, includeFlagged, excludeMetaESDT, isScam, scamType })); - } - - @Get("/accounts/:address/nfts/:nft") - @UseInterceptors(DeepHistoryInterceptor) - @ApiOperation({ summary: 'Account NFT/SFT token details', description: 'Returns details about a specific fungible token for a given address' }) - @ApiQuery({ name: 'fields', description: 'List of fields to filter by', required: false, isArray: true, style: 'form', explode: false }) - @ApiQuery({ name: 'extract', description: 'Extract a specific field', required: false }) - @ApiQuery({ name: 'timestamp', description: 'Retrieve entry from timestamp', required: false, type: Number }) - @ApiOkResponse({ type: NftAccount }) - // @NoCache() - async getAccountNft( - @Param('address', ParseAddressPipe) address: string, - @Param('nft', ParseNftPipe) nft: string, - @Query('fields', ParseArrayPipe) fields?: string[], - @Query('extract') extract?: string, - @Query('timestamp', ParseIntPipe) _timestamp?: number, - ): Promise { - const actualFields = extract ? [extract] : fields; - - const result = await this.nftService.getNftForAddressFromDb(address, nft, actualFields); - if (!result) { - throw new HttpException('Token for given account not found', HttpStatus.NOT_FOUND); - } - - return result; - } - - @Get('/accounts/:address/stake') - @UseInterceptors(DeepHistoryInterceptor) - @ApiOperation({ - summary: 'Account stake details', - description: - 'Summarizes total staked amount for the given provider, as well as when and how much unbond will be performed', - }) - @ApiQuery({ - name: 'timestamp', - description: 'Retrieve entry from timestamp', - required: false, - type: Number, - }) - @ApiOkResponse({ type: ProviderStake }) - async getAccountStake( - @Param('address', ParseAddressPipe) address: string, - @Query('timestamp', ParseIntPipe) _timestamp?: number, - ): Promise { - return await this.stakeService.getStakeForAddress(address); - } - - @Get("/accounts/:address/delegation") - @ApiOperation({ summary: 'Account delegations with staking providers', description: 'Summarizes all delegation positions with staking providers, together with unDelegation positions' }) - @ApiOkResponse({ type: AccountDelegation, isArray: true }) - async getDelegationForAddress(@Param('address', ParseAddressPipe) address: string): Promise { - return await this.delegationService.getDelegationForAddress(address); - } - - @Get("/accounts/:address/delegation-legacy") - @UseInterceptors(DeepHistoryInterceptor) - @ApiOperation({ summary: 'Account legacy delegation details', description: 'Returns staking information related to the legacy delegation pool' }) - @ApiOkResponse({ type: AccountDelegationLegacy }) - @ApiQuery({ name: 'timestamp', description: 'Retrieve entry from timestamp', required: false, type: Number }) - async getAccountDelegationLegacy( - @Param('address', ParseAddressPipe) address: string, - @Query('timestamp', ParseIntPipe) _timestamp?: number, - ): Promise { - return await this.delegationLegacyService.getDelegationForAddress(address); - } - - @Get("/accounts/:address/keys") - @ApiOperation({ summary: 'Account nodes', description: 'Returns all active / queued nodes where the account is owner' }) - @ApiOkResponse({ type: [AccountKey] }) - @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 }) - @ApiQuery({ name: 'status', description: 'Key status', required: false, enum: NodeStatusRaw }) - async getAccountKeys( - @Param('address', ParseAddressPipe) address: string, - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - @Query('status', new ParseEnumArrayPipe(NodeStatusRaw)) status?: NodeStatusRaw[], - ): Promise { - return await this.accountServiceV2.getKeys( - address, - new AccountKeyFilter({ status }), - new QueryPagination({ from, size })); - } - - @Get("/accounts/:address/waiting-list") - @ApiOperation({ summary: 'Account queued nodes', description: 'Returns all nodes in the node queue where the account is owner' }) - @ApiOkResponse({ type: [WaitingList] }) - async getAccountWaitingList(@Param('address', ParseAddressPipe) address: string): Promise { - return await this.waitingListService.getWaitingListForAddress(address); - } - - @Get("/accounts/:address/transactions") - @ApiOperation({ summary: 'Account transaction list', description: 'Returns details of all transactions where the account is sender or receiver' }) - @ApplyComplexity({ target: TransactionDetailed }) - @ApiOkResponse({ type: [Transaction] }) - @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 }) - @ApiQuery({ name: 'sender', description: 'Address of the transaction sender', required: false }) - @ApiQuery({ name: 'receiver', description: 'Search by multiple receiver addresses, comma-separated', required: false }) - @ApiQuery({ name: 'token', description: 'Identifier of the token', required: false }) - @ApiQuery({ name: 'senderShard', description: 'Id of the shard the sender address belongs to', required: false }) - @ApiQuery({ name: 'receiverShard', description: 'Id of the shard the receiver address belongs to', required: false }) - @ApiQuery({ name: 'miniBlockHash', description: 'Filter by miniblock hash', required: false }) - @ApiQuery({ name: 'hashes', description: 'Filter by a comma-separated list of transaction hashes', required: false }) - @ApiQuery({ name: 'status', description: 'Status of the transaction (success / pending / invalid / fail)', required: false, enum: TransactionStatus }) - @ApiQuery({ name: 'function', description: 'Filter transactions by function name', required: false }) - @ApiQuery({ name: 'order', description: 'Sort order (asc/desc)', required: false, enum: SortOrder }) - @ApiQuery({ name: 'fields', description: 'List of fields to filter by', required: false, isArray: true, style: 'form', explode: false }) - @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) - @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) - @ApiQuery({ name: 'round', description: 'Round number', required: false }) - @ApiQuery({ name: 'withScResults', description: 'Return scResults for transactions. When "withScresults" parameter is applied, complexity estimation is 200', required: false }) - @ApiQuery({ name: 'withOperations', description: 'Return operations for transactions. When "withOperations" parameter is applied, complexity estimation is 200', required: false }) - @ApiQuery({ name: 'withLogs', description: 'Return logs for transactions. When "withLogs" parameter is applied, complexity estimation is 200', required: false }) - @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: 'withBlockInfo', description: 'Returns sender / receiver block details', required: false, type: Boolean }) - @ApiQuery({ name: 'computeScamInfo', required: false, type: Boolean }) - @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: 'isScCall', description: 'Returns sc call 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, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - @Query('sender', ParseAddressPipe) sender?: string, - @Query('receiver', ParseAddressArrayPipe) receiver?: string[], - @Query('token') token?: string, - @Query('senderShard', ParseIntPipe) senderShard?: number, - @Query('receiverShard', ParseIntPipe) receiverShard?: number, - @Query('miniBlockHash', ParseBlockHashPipe) miniBlockHash?: string, - @Query('hashes', ParseArrayPipe) hashes?: string[], - @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, - @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], - @Query('before', ParseIntPipe) before?: number, - @Query('after', ParseIntPipe) after?: number, - @Query('round', ParseIntPipe) round?: number, - @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @Query('fields', ParseArrayPipe) fields?: string[], - @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', ParseBoolPipe) isRelayed?: boolean, - @Query('isScCall', ParseBoolPipe) isScCall?: boolean, - @Query('withActionTransferValue', ParseBoolPipe) withActionTransferValue?: boolean, - @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, - ) { - const options = TransactionQueryOptions.applyDefaultOptions(size, { withScResults, withOperations, withLogs, withScamInfo, withUsername, withBlockInfo, withActionTransferValue }); - - const transactionFilter = new TransactionFilter({ - sender, - receivers: receiver, - token, - functions, - senderShard, - receiverShard, - miniBlockHash, - hashes, - status, - before, - after, - order, - senderOrReceiver, - isRelayed, - isScCall, - round, - withRelayedScresults, - }); - TransactionFilter.validate(transactionFilter, size); - return await this.transactionService.getTransactions(transactionFilter, new QueryPagination({ from, size }), options, address, fields); - } - - @Get("/accounts/:address/transactions/count") - @ApiOperation({ summary: 'Account transactions count', description: 'Returns total number of transactions for a given address where the account is sender or receiver, as well as total transactions count that have a certain status' }) - @ApiOkResponse({ type: Number }) - @ApiQuery({ name: 'sender', description: 'Address of the transaction sender', required: false }) - @ApiQuery({ name: 'receiver', description: 'Search by multiple receiver addresses, comma-separated', required: false }) - @ApiQuery({ name: 'token', description: 'Identifier of the token', required: false }) - @ApiQuery({ name: 'senderShard', description: 'Id of the shard the sender address belongs to', required: false }) - @ApiQuery({ name: 'receiverShard', description: 'Id of the shard the receiver address belongs to', required: false }) - @ApiQuery({ name: 'miniBlockHash', description: 'Filter by miniblock hash', required: false }) - @ApiQuery({ name: 'hashes', description: 'Filter by a comma-separated list of transaction hashes', required: false }) - @ApiQuery({ name: 'status', description: 'Status of the transaction (success / pending / invalid / fail)', required: false, enum: TransactionStatus }) - @ApiQuery({ name: 'function', description: 'Filter transactions by function name', required: false }) - @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) - @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: 'isRelayed', description: 'Returns isRelayed transactions details', required: false, type: Boolean }) - @ApiQuery({ name: 'isScCall', description: 'Returns sc call 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, - @Query('receiver', ParseAddressArrayPipe) receiver?: string[], - @Query('token') token?: string, - @Query('senderShard', ParseIntPipe) senderShard?: number, - @Query('receiverShard', ParseIntPipe) receiverShard?: number, - @Query('miniBlockHash', ParseBlockHashPipe) miniBlockHash?: string, - @Query('hashes', ParseArrayPipe) hashes?: string[], - @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, - @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], - @Query('before', ParseIntPipe) before?: number, - @Query('after', ParseIntPipe) after?: number, - @Query('round', ParseIntPipe) round?: number, - @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, - @Query('isRelayed', ParseBoolPipe) isRelayed?: boolean, - @Query('isScCall', ParseBoolPipe) isScCall?: boolean, - @Query('withRelayedScresults', ParseBoolPipe) withRelayedScresults?: boolean, - - ): Promise { - - return await this.transactionService.getTransactionCount(new TransactionFilter({ - sender, - receivers: receiver, - token, - functions, - senderShard, - receiverShard, - miniBlockHash, - hashes, - status, - before, - after, - senderOrReceiver, - isRelayed, - isScCall, - round, - withRelayedScresults, - }), address); - } - - @Get("/accounts/:address/transfers") - @ApiOperation({ summary: 'Account value transfers', description: 'Returns both transfers triggerred by a user account (type = Transaction), as well as transfers triggerred by smart contracts (type = SmartContractResult), thus providing a full picture of all in/out value transfers for a given account' }) - @ApiOkResponse({ type: [Transaction] }) - @ApplyComplexity({ target: TransactionDetailed }) - @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 }) - @ApiQuery({ name: 'sender', description: 'Address of the transfer sender', required: false }) - @ApiQuery({ name: 'receiver', description: 'Search by multiple receiver addresses, comma-separated', required: false }) - @ApiQuery({ name: 'token', description: 'Identifier of the token', required: false }) - @ApiQuery({ name: 'senderShard', description: 'Id of the shard the sender address belongs to', required: false }) - @ApiQuery({ name: 'receiverShard', description: 'Id of the shard the receiver address belongs to', required: false }) - @ApiQuery({ name: 'miniBlockHash', description: 'Filter by miniblock hash', required: false }) - @ApiQuery({ name: 'hashes', description: 'Filter by a comma-separated list of transfer hashes', required: false }) - @ApiQuery({ name: 'status', description: 'Status of the transaction (success / pending / invalid / fail)', required: false, enum: TransactionStatus }) - @ApiQuery({ name: 'function', description: 'Filter transactions by function name', required: false }) - @ApiQuery({ name: 'order', description: 'Sort order (asc/desc)', required: false, enum: SortOrder }) - @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) - @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) - @ApiQuery({ name: 'round', description: 'Round number', required: false }) - @ApiQuery({ name: 'fields', description: 'List of fields to filter by', required: false, isArray: true, style: 'form', explode: false }) - @ApiQuery({ name: 'relayer', description: 'Address of the relayer', required: false }) - @ApiQuery({ name: 'isScCall', description: 'Returns sc call transactions details', 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: 'withBlockInfo', description: 'Returns sender / receiver block details', required: false, type: Boolean }) - @ApiQuery({ name: 'senderOrReceiver', description: 'One address that current address interacted with', required: false }) - @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 }) - @ApiQuery({ name: 'withTxsRelayedByAddress', description: 'Include transactions that were relayed by the address', required: false }) - async getAccountTransfers( - @Param('address', ParseAddressPipe) address: string, - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - @Query('sender', ParseAddressArrayPipe) sender?: string[], - @Query('receiver', ParseAddressArrayPipe) receiver?: string[], - @Query('token') token?: string, - @Query('senderShard', ParseIntPipe) senderShard?: number, - @Query('receiverShard', ParseIntPipe) receiverShard?: number, - @Query('miniBlockHash', ParseBlockHashPipe) miniBlockHash?: string, - @Query('hashes', ParseArrayPipe) hashes?: string[], - @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, - @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], - @Query('before', ParseIntPipe) before?: number, - @Query('after', ParseIntPipe) after?: number, - @Query('round', ParseIntPipe) round?: number, - @Query('fields', ParseArrayPipe) fields?: string[], - @Query('order', new ParseEnumPipe(SortOrder)) order?: SortOrder, - @Query('relayer', ParseAddressPipe) relayer?: string, - @Query('withScamInfo', ParseBoolPipe) withScamInfo?: boolean, - @Query('withUsername', ParseBoolPipe) withUsername?: boolean, - @Query('withBlockInfo', ParseBoolPipe) withBlockInfo?: boolean, - @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, - @Query('isScCall', ParseBoolPipe) isScCall?: boolean, - @Query('withLogs', ParseBoolPipe) withLogs?: boolean, - @Query('withOperations', ParseBoolPipe) withOperations?: boolean, - @Query('withActionTransferValue', ParseBoolPipe) withActionTransferValue?: boolean, - @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, - @Query('withTxsRelayedByAddress', ParseBoolPipe) withTxsRelayedByAddress?: boolean, - ): Promise { - const options = TransactionQueryOptions.applyDefaultOptions( - size, { withScamInfo, withUsername, withBlockInfo, withOperations, withLogs, withActionTransferValue }); - - return await this.transferService.getTransfers(new TransactionFilter({ - address, - senders: sender, - receivers: receiver, - token, - functions, - senderShard, - receiverShard, - miniBlockHash, - hashes, - status, - before, - after, - order, - senderOrReceiver, - relayer, - round, - withRefunds, - withTxsRelayedByAddress, - isScCall, - }), - new QueryPagination({ from, size }), - options, - fields - ); - } - - @Get("/accounts/:address/transfers/count") - @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 }) - @ApiQuery({ name: 'token', description: 'Identifier of the token', required: false }) - @ApiQuery({ name: 'senderShard', description: 'Id of the shard the sender address belongs to', required: false }) - @ApiQuery({ name: 'receiverShard', description: 'Id of the shard the receiver address belongs to', required: false }) - @ApiQuery({ name: 'miniBlockHash', description: 'Filter by miniblock hash', required: false }) - @ApiQuery({ name: 'hashes', description: 'Filter by a comma-separated list of transfer hashes', required: false }) - @ApiQuery({ name: 'status', description: 'Status of the transaction (success / pending / invalid / fail)', required: false, enum: TransactionStatus }) - @ApiQuery({ name: 'function', description: 'Filter transfers by function name', required: false }) - @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) - @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) - @ApiQuery({ name: 'round', description: 'Round number', required: false }) - @ApiQuery({ name: 'isScCall', description: 'Returns sc call transactions details', required: false, type: Boolean }) - @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[], - @Query('receiver', ParseAddressArrayPipe) receiver?: string[], - @Query('token') token?: string, - @Query('senderShard', ParseIntPipe) senderShard?: number, - @Query('receiverShard', ParseIntPipe) receiverShard?: number, - @Query('miniBlockHash', ParseBlockHashPipe) miniBlockHash?: string, - @Query('hashes', ParseArrayPipe) hashes?: string[], - @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, - @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], - @Query('before', ParseIntPipe) before?: number, - @Query('after', ParseIntPipe) after?: number, - @Query('round', ParseIntPipe) round?: number, - @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, - @Query('isScCall', ParseBoolPipe) isScCall?: boolean, - @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, - ): Promise { - return await this.transferService.getTransfersCount(new TransactionFilter({ - address, - senders: sender, - receivers: receiver, - token, - functions, - senderShard, - receiverShard, - miniBlockHash, - hashes, - status, - before, - after, - senderOrReceiver, - round, - isScCall, - withRefunds, - })); - } - - @Get("/accounts/:address/transfers/c") - @ApiExcludeEndpoint() - async getAccountTransfersCountAlternative( - @Param('address', ParseAddressPipe) address: string, - @Query('sender', ParseAddressArrayPipe) sender?: string[], - @Query('receiver', ParseAddressArrayPipe) receiver?: string[], - @Query('token') token?: string, - @Query('senderShard', ParseIntPipe) senderShard?: number, - @Query('receiverShard', ParseIntPipe) receiverShard?: number, - @Query('miniBlockHash', ParseBlockHashPipe) miniBlockHash?: string, - @Query('hashes', ParseArrayPipe) hashes?: string[], - @Query('status', new ParseEnumPipe(TransactionStatus)) status?: TransactionStatus, - @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], - @Query('before', ParseIntPipe) before?: number, - @Query('after', ParseIntPipe) after?: number, - @Query('round', ParseIntPipe) round?: number, - @Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string, - @Query('withRefunds', ParseBoolPipe) withRefunds?: boolean, - @Query('isScCall', ParseBoolPipe) isScCall?: boolean, - ): Promise { - return await this.transferService.getTransfersCount(new TransactionFilter({ - address, - senders: sender, - receivers: receiver, - token, - functions, - senderShard, - receiverShard, - miniBlockHash, - hashes, - status, - before, - after, - senderOrReceiver, - round, - withRefunds, - isScCall, - })); - } - - @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.accountServiceV2.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.accountServiceV2.getAccountDeploysCount(address); - } - - @Get("/accounts/:address/deploys/c") - @ApiExcludeEndpoint() - getAccountDeploysCountAlternative(@Param('address', ParseAddressPipe) address: string): Promise { - return this.accountServiceV2.getAccountDeploysCount(address); - } - - @Get("/accounts/:address/contracts") - @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] }) - getAccountContracts( - @Param('address', ParseAddressPipe) address: string, - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - ): Promise { - return this.accountServiceV2.getAccountContracts(new QueryPagination({ from, size }), address); - } - - @Get("/accounts/:address/contracts/count") - @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.accountServiceV2.getAccountContractsCount(address); - } - - @Get("/accounts/:address/contracts/c") - @ApiExcludeEndpoint() - getAccountContractsCountAlternative(@Param('address', ParseAddressPipe) address: string): Promise { - return this.accountServiceV2.getAccountContractsCount(address); - } - - @Get("/accounts/:address/upgrades") - @ApiOperation({ summary: 'Account upgrades details', description: 'Returns all upgrades details for a specific contract address' }) - @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: ContractUpgrades }) - getContractUpgrades( - @Param('address', ParseAddressPipe) address: string, - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - ): Promise { - return this.accountServiceV2.getContractUpgrades(new QueryPagination({ from, size }), address); - } - - @Get("/accounts/:address/results") - @ApiOperation({ summary: 'Account smart contract results', description: 'Returns smart contract results where the account is sender or receiver' }) - @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: [SmartContractResult] }) - getAccountScResults( - @Param('address', ParseAddressPipe) address: string, - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - ): Promise { - return this.scResultService.getAccountScResults(address, new QueryPagination({ from, size })); - } - - @Get("/accounts/:address/results/count") - @ApiOperation({ summary: 'Account smart contracts results count', description: 'Returns number of smart contract results where the account is sender or receiver' }) - @ApiOkResponse({ type: Number }) - getAccountScResultsCount( - @Param('address', ParseAddressPipe) address: string, - ): Promise { - return this.scResultService.getAccountScResultsCount(address); - } - - @Get("/accounts/:address/results/:scHash") - @ApiOperation({ summary: 'Account smart contract result', description: 'Returns details of a smart contract result where the account is sender or receiver' }) - @ApiOkResponse({ type: SmartContractResult }) - async getAccountScResult( - @Param('address', ParseAddressPipe) address: string, - @Param('scHash', ParseTransactionHashPipe) scHash: string, - ): Promise { - const scResult = await this.scResultService.getScResult(scHash); - if (!scResult || (scResult.sender !== address && scResult.receiver !== address)) { - throw new NotFoundException('Smart contract result not found'); - } - - return scResult; - } - - @Get("/accounts/:address/history") - @ApiOperation({ summary: 'Account history', description: 'Return account EGLD balance history' }) - @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 }) - @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) - @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) - @ApiOkResponse({ type: [AccountHistory] }) - getAccountHistory( - @Param('address', ParseAddressPipe) address: string, - @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 { - return this.accountServiceV2.getAccountHistory( - address, - new QueryPagination({ from, size }), - new AccountHistoryFilter({ before, after })); - } - - @Get("/accounts/:address/history/count") - @ApiOperation({ summary: 'Account history count', description: 'Return account EGLD balance history count' }) - @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) - @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) - @ApiOkResponse({ type: Number }) - getAccountHistoryCount( - @Param('address', ParseAddressPipe) address: string, - @Query('before', ParseIntPipe) before?: number, - @Query('after', ParseIntPipe) after?: number, - ): Promise { - return this.accountServiceV2.getAccountHistoryCount( - address, - new AccountHistoryFilter({ before, after })); - } - - @Get("/accounts/:address/history/:tokenIdentifier/count") - @ApiOperation({ summary: 'Account token history count', description: 'Return account token balance history count' }) - @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) - @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) - @ApiOkResponse({ type: Number }) - async getAccountTokenHistoryCount( - @Param('address', ParseAddressPipe) address: string, - @Param('tokenIdentifier', ParseTokenOrNftPipe) tokenIdentifier: string, - @Query('before', ParseIntPipe) before?: number, - @Query('after', ParseIntPipe) after?: number, - ): Promise { - const isToken = await this.tokenService.isToken(tokenIdentifier) || await this.collectionService.isCollection(tokenIdentifier) || await this.nftService.isNft(tokenIdentifier); - if (!isToken) { - throw new NotFoundException(`Token '${tokenIdentifier}' not found`); - } - return this.accountServiceV2.getAccountTokenHistoryCount( - address, - tokenIdentifier, - new AccountHistoryFilter({ before, after })); - } - - @Get("/accounts/:address/esdthistory") - @ApiOperation({ summary: 'Account esdts history', description: 'Returns account esdts balance history' }) - @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 }) - @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) - @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) - @ApiQuery({ name: 'identifier', description: 'Filter by multiple esdt identifiers, comma-separated', required: false }) - @ApiQuery({ name: 'token', description: 'Token identifier', required: false }) - @ApiOkResponse({ type: [AccountEsdtHistory] }) - async getAccountEsdtHistory( - @Param('address', ParseAddressPipe) address: string, - @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, - @Query('identifier', ParseArrayPipe) identifier?: string[], - @Query('token', ParseTokenPipe) token?: string, - ): Promise { - return await this.accountServiceV2.getAccountEsdtHistory( - address, - new QueryPagination({ from, size }), - new AccountHistoryFilter({ before, after, identifiers: identifier, token })); - } - - @Get("/accounts/:address/esdthistory/count") - @ApiOperation({ summary: 'Account esdts history count', description: 'Returns account esdts balance history count' }) - @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) - @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) - @ApiQuery({ name: 'identifier', description: 'Filter by multiple esdt identifiers, comma-separated', required: false }) - @ApiQuery({ name: 'token', description: 'Token identifier', required: false }) - async getAccountEsdtHistoryCount( - @Param('address', ParseAddressPipe) address: string, - @Query('before', ParseIntPipe) before?: number, - @Query('after', ParseIntPipe) after?: number, - @Query('identifier', ParseArrayPipe) identifier?: string[], - @Query('token', ParseTokenPipe) token?: string, - ): Promise { - return await this.accountServiceV2.getAccountEsdtHistoryCount( - address, - new AccountHistoryFilter({ before, after, identifiers: identifier, token })); - } - - @Get("/accounts/:address/history/:tokenIdentifier") - @ApiOperation({ summary: 'Account token history', description: 'Returns account token balance history' }) - @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 }) - @ApiQuery({ name: 'before', description: 'Before timestamp', required: false }) - @ApiQuery({ name: 'after', description: 'After timestamp', required: false }) - @ApiOkResponse({ type: [AccountEsdtHistory] }) - async getAccountTokenHistory( - @Param('address', ParseAddressPipe) address: string, - @Param('tokenIdentifier', ParseTokenOrNftPipe) tokenIdentifier: string, - @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 { - const isToken = await this.tokenService.isToken(tokenIdentifier) || await this.collectionService.isCollection(tokenIdentifier) || await this.nftService.isNft(tokenIdentifier); - if (!isToken) { - throw new NotFoundException(`Token '${tokenIdentifier}' not found`); - } - - return await this.accountServiceV2.getAccountTokenHistory( - address, tokenIdentifier, - new QueryPagination({ from, size }), - new AccountHistoryFilter({ before, after })); - } } diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts index 809ced4ea..543d015c0 100644 --- a/src/endpoints/accounts-v2/account.service.v2.ts +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -1,39 +1,20 @@ import { forwardRef, HttpStatus, Inject, Injectable } from '@nestjs/common'; import { AccountDetailed } from './entities/account.detailed'; -import { Account } from './entities/account'; -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'; -import { QueryPagination } from 'src/common/entities/query.pagination'; -import { AccountKey } from './entities/account.key'; -import { DeployedContract } from './entities/deployed.contract'; import { PluginService } from 'src/common/plugins/plugin.service'; -import { AccountEsdtHistory } from "./entities/account.esdt.history"; -import { AccountHistory } from "./entities/account.history"; -import { StakeService } from '../stake/stake.service'; import { TransferService } from '../transfers/transfer.service'; import { TransactionType } from '../transactions/entities/transaction.type'; import { AssetsService } from 'src/common/assets/assets.service'; import { TransactionFilter } from '../transactions/entities/transaction.filter'; import { CacheService } from "@multiversx/sdk-nestjs-cache"; -import { AddressUtils, BinaryUtils, OriginLogger } from '@multiversx/sdk-nestjs-common'; -import { ApiService, ApiUtils } from "@multiversx/sdk-nestjs-http"; +import { AddressUtils, OriginLogger } from '@multiversx/sdk-nestjs-common'; +import { ApiService } from "@multiversx/sdk-nestjs-http"; import { GatewayService } from 'src/common/gateway/gateway.service'; import { IndexerService } from "src/common/indexer/indexer.service"; -import { AccountAssets } from 'src/common/assets/entities/account.assets'; import { CacheInfo } from 'src/utils/cache.info'; import { UsernameService } from '../usernames/username.service'; -import { ContractUpgrades } from './entities/contract.upgrades'; -import { AccountVerification } from './entities/account.verification'; -import { AccountQueryOptions } from './entities/account.query.options'; -import { AccountHistoryFilter } from './entities/account.history.filter'; import { ProtocolService } from 'src/common/protocol/protocol.service'; 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 { 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'; import { AccountDetailsRepository } from 'src/common/indexer/db'; @@ -47,12 +28,9 @@ export class AccountServiceV2 { private readonly indexerService: IndexerService, private readonly gatewayService: GatewayService, private readonly cachingService: CacheService, - private readonly vmQueryService: VmQueryService, private readonly apiConfigService: ApiConfigService, @Inject(forwardRef(() => PluginService)) private readonly pluginService: PluginService, - @Inject(forwardRef(() => StakeService)) - private readonly stakeService: StakeService, @Inject(forwardRef(() => TransferService)) private readonly transferService: TransferService, private readonly assetsService: AssetsService, @@ -61,26 +39,17 @@ export class AccountServiceV2 { private readonly protocolService: ProtocolService, @Inject(forwardRef(() => ProviderService)) private readonly providerService: ProviderService, - private readonly keysService: KeysService, private readonly accountDetailsDepository: AccountDetailsRepository, ) { } - async getAccountsCount(filter: AccountQueryOptions): Promise { - if (!filter.isSet()) { - return await this.cachingService.getOrSet( - CacheInfo.AccountsCount.key, - async () => await this.indexerService.getAccountsCount(filter), - CacheInfo.AccountsCount.ttl - ); - } - - return await this.indexerService.getAccountsCount(filter); - } - private async getAccountWithFallBack(address: string, options?: AccountFetchOptions): Promise { - // First try to get account from MongoDB - const accountFromDb = await this.accountDetailsDepository.getAccount(address); + // First try to get account from MongoDB if it is wallet address + let accountFromDb = null; + if (!AddressUtils.isSmartContractAddress(address)) { + accountFromDb = await this.accountDetailsDepository.getAccount(address); + } if (!accountFromDb) { + // Second use the legacy method return await this.getAccountRaw(address, options?.withAssets); } if (options && options.withAssets === true) { @@ -100,7 +69,7 @@ export class AccountServiceV2 { let account = null; try { const isStateChangesConsumerHealty: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); - if (isStateChangesConsumerHealty === true) { + if (isStateChangesConsumerHealty === true && !StateChangesConsumerService.isSystemContractAddress(address)) { account = await this.cachingService.getOrSet( CacheInfo.AccountState(address).key, async () => this.getAccountWithFallBack(address, options), @@ -175,28 +144,6 @@ export class AccountServiceV2 { } } - async getAccountVerification(address: string): Promise { - if (!AddressUtils.isAddressValid(address)) { - return null; - } - - const verificationResponse = await this.apiService.get(`${this.apiConfigService.getVerifierUrl()}/verifier/${address}`); - return verificationResponse.data; - } - - async getVerifiedAccounts(): Promise { - const verificationResponse = await this.apiService.get(`${this.apiConfigService.getVerifierUrl()}/verifier`); - return verificationResponse.data; - } - - async getAccountSimple(address: string): Promise { - if (!AddressUtils.isAddressValid(address)) { - return null; - } - - return await this.getAccountRaw(address); - } - async getAccountRaw(address: string, withAssets?: boolean): Promise { try { const { @@ -213,12 +160,12 @@ export class AccountServiceV2 { account.ownerAssets = assets[ownerAddress]; } - const codeAttributes = AddressUtils.decodeCodeMetadata(codeMetadata); - if (codeAttributes) { - account = { ...account, ...codeAttributes }; - } - if (AddressUtils.isSmartContractAddress(address) && account.code) { + const codeAttributes = AddressUtils.decodeCodeMetadata(codeMetadata); + if (codeAttributes) { + account = { ...account, ...codeAttributes }; + } + const deployTxHash = await this.getAccountDeployedTxHash(address); if (deployTxHash) { account.deployTxHash = deployTxHash; @@ -326,457 +273,4 @@ export class AccountServiceV2 { return null; } - - async getAccounts(queryPagination: QueryPagination, filter: AccountQueryOptions): Promise { - if (!filter.isSet()) { - return await this.cachingService.getOrSet( - CacheInfo.Accounts(queryPagination).key, - async () => await this.getAccountsRaw(queryPagination, filter), - CacheInfo.Accounts(queryPagination).ttl - ); - } - - return await this.getAccountsRaw(queryPagination, filter); - } - - public async getAccountsForAddresses(addresses: Array): Promise> { - const assets: { [key: string]: AccountAssets; } = await this.assetsService.getAllAccountAssets(); - - const accountsRaw = await this.indexerService.getAccountsForAddresses(addresses); - const accounts: Array = accountsRaw.map(account => ApiUtils.mergeObjects(new Account(), account)); - const shardCount = await this.protocolService.getShardCount(); - - for (const account of accounts) { - account.shard = AddressUtils.computeShard(AddressUtils.bech32Decode(account.address), shardCount); - account.assets = assets[account.address]; - } - - return accounts; - } - - async getAccountsRaw(queryPagination: QueryPagination, options: AccountQueryOptions): Promise { - const result = await this.indexerService.getAccounts(queryPagination, options); - const assets = await this.assetsService.getAllAccountAssets(); - const accounts: Account[] = result.map(item => { - const account = ApiUtils.mergeObjects(new Account(), item); - account.ownerAddress = item.currentOwner; - account.transfersLast24h = item.api_transfersLast24h; - - return account; - }); - - const shardCount = await this.protocolService.getShardCount(); - - 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]; - - if (options.withDeployInfo && AddressUtils.isSmartContractAddress(account.address)) { - const [deployedAt, deployTxHash] = await Promise.all([ - this.getAccountDeployedAt(account.address), - this.getAccountDeployedTxHash(account.address), - ]); - - account.deployedAt = deployedAt; - account.deployTxHash = deployTxHash; - } - - if (options.withTxCount) { - account.txCount = await this.getAccountTxCount(account.address); - } - - if (options.withScrCount) { - account.scrCount = await this.getAccountScResults(account.address); - } - - if (options.withOwnerAssets && account.ownerAddress) { - account.ownerAssets = assets[account.ownerAddress]; - } - - if (verifiedAccounts && verifiedAccounts.includes(account.address)) { - account.isVerified = true; - } - } - - return accounts; - } - - async getDeferredAccount(address: string): Promise { - const publicKey = AddressUtils.bech32Decode(address); - const delegationContractAddress = this.apiConfigService.getDelegationContractAddress(); - if (!delegationContractAddress) { - return []; - } - - const delegationContractShardId = AddressUtils.computeShard(AddressUtils.bech32Decode(delegationContractAddress), await this.protocolService.getShardCount()); - - const [ - encodedUserDeferredPaymentList, - [encodedNumBlocksBeforeUnBond], - { - erd_nonce, - }, - ] = await Promise.all([ - this.vmQueryService.vmQuery( - delegationContractAddress, - 'getUserDeferredPaymentList', - undefined, - [publicKey] - ), - this.vmQueryService.vmQuery( - delegationContractAddress, - 'getNumBlocksBeforeUnBond', - ), - this.gatewayService.getNetworkStatus(`${delegationContractShardId}`), - ]); - - const numBlocksBeforeUnBond = parseInt(BinaryUtils.base64ToBigInt(encodedNumBlocksBeforeUnBond).toString()); - const erdNonce = erd_nonce; - - const data: AccountDeferred[] = encodedUserDeferredPaymentList.reduce((result: AccountDeferred[], _, index, array) => { - if (index % 2 === 0) { - const [encodedDeferredPayment, encodedUnstakedNonce] = array.slice(index, index + 2); - - const deferredPayment = BinaryUtils.base64ToBigInt(encodedDeferredPayment).toString(); - const unstakedNonce = parseInt(BinaryUtils.base64ToBigInt(encodedUnstakedNonce).toString()); - const blocksLeft = Math.max(0, unstakedNonce + numBlocksBeforeUnBond - erdNonce); - const secondsLeft = blocksLeft * 6; // 6 seconds per block - - result.push({ deferredPayment, secondsLeft }); - } - - return result; - }, []); - - return data; - } - - private async getBlsKeysStatusForPublicKey(publicKey: string) { - const auctionContractAddress = this.apiConfigService.getAuctionContractAddress(); - if (!auctionContractAddress) { - return undefined; - } - - return await this.vmQueryService.vmQuery( - auctionContractAddress, - 'getBlsKeysStatus', - auctionContractAddress, - [publicKey], - ); - } - - private async getRewardAddressForNode(blsKey: string): Promise { - const stakingContractAddress = this.apiConfigService.getStakingContractAddress(); - if (!stakingContractAddress) { - return ''; - } - - const [encodedRewardsPublicKey] = await this.vmQueryService.vmQuery( - stakingContractAddress, - 'getRewardAddress', - undefined, - [blsKey], - ); - - const rewardsPublicKey = Buffer.from(encodedRewardsPublicKey, 'base64').toString(); - return AddressUtils.bech32Encode(rewardsPublicKey); - } - - private async getAllNodeStates(address: string) { - return await this.vmQueryService.vmQuery( - address, - 'getAllNodeStates' - ); - } - - async getKeys(address: string, filter: AccountKeyFilter, pagination: QueryPagination): Promise { - const { from, size } = pagination; - const publicKey = AddressUtils.bech32Decode(address); - const isStakingProvider = await this.providerService.isProvider(address); - - let notStakedNodes: AccountKey[] = []; - - if (isStakingProvider) { - const allNodeStates = await this.getAllNodeStates(address); - const inactiveNodesBuffers = this.getInactiveNodesBuffers(allNodeStates); - notStakedNodes = this.createNotStakedNodes(inactiveNodesBuffers); - } - - const blsKeysStatus = await this.getBlsKeysStatusForPublicKey(publicKey); - let nodes: AccountKey[] = []; - - if (blsKeysStatus) { - nodes = this.createAccountKeys(blsKeysStatus); - await this.applyRewardAddressAndTopUpToNodes(nodes, address); - await this.applyNodeUnbondingPeriods(nodes); - await this.updateQueuedNodes(nodes); - } - - let filteredNodes = [...notStakedNodes, ...nodes]; - - if (filter && filter.status && filter.status.length > 0) { - filteredNodes = filteredNodes.filter(node => filter.status.includes(node.status as NodeStatusRaw)); - filteredNodes = this.sortNodesByStatus(filteredNodes, filter.status); - } - - return filteredNodes.slice(from, from + size); - } - - getInactiveNodesBuffers(allNodeStates: string[]): string[] { - if (!allNodeStates) { - return []; - } - - const checkIfCurrentItemIsStatus = (currentNodeData: string) => - Object.values(NodeStatusRaw).includes( - currentNodeData as NodeStatusRaw - ); - - return allNodeStates.reduce( - (totalNodes: string[], currentNodeState, nodeIndex, allNodesDataArray) => { - const decodedData = Buffer.from(currentNodeState, 'base64').toString(); - const isNotStakedStatus = - decodedData === NodeStatusRaw.notStaked; - - const isCurrentItemTheStatus = checkIfCurrentItemIsStatus(decodedData); - - const nextStatusItemIndex = allNodesDataArray.findIndex( - (nodeData, nodeDataIndex) => - nodeIndex < nodeDataIndex - ? checkIfCurrentItemIsStatus(Buffer.from(nodeData, 'base64').toString()) - : false - ); - - if (isCurrentItemTheStatus && nextStatusItemIndex < 0 && isNotStakedStatus) { - return [...totalNodes, ...allNodesDataArray.slice(nodeIndex + 1)]; - } - - if (isCurrentItemTheStatus && isNotStakedStatus) { - return [...totalNodes, ...allNodesDataArray.slice(nodeIndex + 1, nextStatusItemIndex)]; - } - - return totalNodes; - }, - [] - ); - } - - createNotStakedNodes(inactiveNodesBuffers: string[]): AccountKey[] { - return inactiveNodesBuffers.map((inactiveNodeBuffer) => { - const accountKey: AccountKey = new AccountKey(); - accountKey.blsKey = BinaryUtils.padHex(Buffer.from(inactiveNodeBuffer, 'base64').toString('hex')); - accountKey.status = NodeStatusRaw.notStaked; - accountKey.stake = '2500000000000000000000'; - - return accountKey; - }); - } - - createAccountKeys(blsKeysStatus: string[]): AccountKey[] { - const nodes: AccountKey[] = []; - for (let index = 0; index < blsKeysStatus.length; index += 2) { - const [encodedBlsKey, encodedStatus] = blsKeysStatus.slice(index, index + 2); - - const accountKey: AccountKey = new AccountKey(); - accountKey.blsKey = BinaryUtils.padHex(Buffer.from(encodedBlsKey, 'base64').toString('hex')); - accountKey.status = Buffer.from(encodedStatus, 'base64').toString(); - accountKey.stake = '2500000000000000000000'; - - nodes.push(accountKey); - } - return nodes; - } - - private sortNodesByStatus(nodes: AccountKey[], status: NodeStatusRaw[]): AccountKey[] { - return nodes.sorted(node => { - const statusIndex = status.indexOf(node.status as NodeStatusRaw); - return statusIndex === -1 ? status.length : statusIndex; - }); - } - - async applyRewardAddressAndTopUpToNodes(nodes: AccountKey[], address: string) { - if (nodes.length) { - const rewardAddress = await this.getRewardAddressForNode(nodes[0].blsKey); - const { topUp } = await this.stakeService.getAllStakesForNode(address); - - for (const node of nodes) { - node.rewardAddress = rewardAddress; - node.topUp = topUp; - node.remainingUnBondPeriod = undefined; - } - } - } - - async updateQueuedNodes(nodes: AccountKey[]) { - const stakingContractAddress = this.apiConfigService.getStakingContractAddress(); - if (!stakingContractAddress) { - return; - } - - const queuedNodes: string[] = nodes - .filter((node: AccountKey) => node.status === 'queued') - .map(({ blsKey }) => blsKey); - - if (queuedNodes.length) { - const [queueSizeEncoded] = await this.vmQueryService.vmQuery( - stakingContractAddress, - 'getQueueSize', - ); - - if (queueSizeEncoded) { - const queueSize = Buffer.from(queueSizeEncoded, 'base64').toString(); - - const queueIndexes = await Promise.all( - queuedNodes.map((blsKey: string) => - this.vmQueryService.vmQuery( - stakingContractAddress, - 'getQueueIndex', - this.apiConfigService.getAuctionContractAddress(), - [blsKey], - ) - ), - ); - - let index = 0; - for (const queueIndexEncoded of queueIndexes) { - if (queueIndexEncoded) { - const [found] = nodes.filter((x: AccountKey) => x.blsKey === queuedNodes[index]); - - found.queueIndex = Buffer.from(queueIndexEncoded[0], 'base64').toString(); - found.queueSize = queueSize; - - index++; - } - } - } - } - } - - 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 => ({ - address: contract.contract, - deployTxHash: contract.deployTxHash, - timestamp: contract.timestamp, - assets: assets[contract.contract], - })); - - 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); - } - - async getContractUpgrades(queryPagination: QueryPagination, address: string): Promise { - const details = await this.indexerService.getScDeploy(address); - if (!details) { - return []; - } - - const upgrades = details.upgrades.map(item => ApiUtils.mergeObjects(new ContractUpgrades(), { - address: item.upgrader, - txHash: item.upgradeTxHash, - codeHash: item.codeHash, - timestamp: item.timestamp, - })).sortedDescending(item => item.timestamp); - - return upgrades.slice(queryPagination.from, queryPagination.from + queryPagination.size); - } - - async getAccountHistory(address: string, pagination: QueryPagination, filter: AccountHistoryFilter): Promise { - const elasticResult = await this.indexerService.getAccountHistory(address, pagination, filter); - return elasticResult.map(item => ApiUtils.mergeObjects(new AccountHistory(), item)); - } - - async getAccountHistoryCount(address: string, filter: AccountHistoryFilter): Promise { - return await this.indexerService.getAccountHistoryCount(address, filter); - } - - async getAccountTokenHistoryCount(address: string, tokenIdentifier: string, filter: AccountHistoryFilter): Promise { - return await this.indexerService.getAccountTokenHistoryCount(address, tokenIdentifier, filter); - } - - async getAccountTokenHistory(address: string, tokenIdentifier: string, pagination: QueryPagination, filter: AccountHistoryFilter): Promise { - const elasticResult = await this.indexerService.getAccountTokenHistory(address, tokenIdentifier, pagination, filter); - return elasticResult.map(item => ApiUtils.mergeObjects(new AccountEsdtHistory(), item)); - } - - async getAccountEsdtHistory(address: string, pagination: QueryPagination, filter: AccountHistoryFilter): Promise { - const elasticResult = await this.indexerService.getAccountEsdtHistory(address, pagination, filter); - return elasticResult.map(item => ApiUtils.mergeObjects(new AccountEsdtHistory(), item)); - } - - async getAccountEsdtHistoryCount(address: string, filter: AccountHistoryFilter): Promise { - return await this.indexerService.getAccountEsdtHistoryCount(address, filter); - } - - private async applyNodeUnbondingPeriods(nodes: AccountKey[]): Promise { - const leavingNodes = nodes.filter(node => node.status === 'unStaked'); - await Promise.all(leavingNodes.map(async node => { - const keyUnbondPeriod = await this.keysService.getKeyUnbondPeriod(node.blsKey); - node.remainingUnBondPeriod = keyUnbondPeriod?.remainingUnBondPeriod; - })); - } - - async getApplicationMostUsed(): Promise { - return await this.cachingService.getOrSet( - CacheInfo.ApplicationMostUsed.key, - async () => await this.getApplicationMostUsedRaw(), - CacheInfo.ApplicationMostUsed.ttl - ); - } - - async getApplicationMostUsedRaw(): Promise { - const transfersLast24hUrl = this.apiConfigService.getAccountExtraDetailsTransfersLast24hUrl(); - if (!transfersLast24hUrl) { - throw new Error('Transfers last 24h URL is not set'); - } - - const { data: mostUsedApplications } = await this.apiService.get(transfersLast24hUrl); - return mostUsedApplications.map((item: any) => new ApplicationMostUsed({ - address: item.key, - transfers24H: item.value, - })); - } } diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 8f7735c73..52fc8c4d7 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -11,9 +11,27 @@ import { NftType } from "src/endpoints/nfts/entities/nft.type"; import { NftSubType } from "src/endpoints/nfts/entities/nft.sub.type"; import { ClientProxy } from "@nestjs/microservices"; import { StateChangesDecoder } from "./utils/state-changes.decoder"; +import { AddressUtils } from "@multiversx/sdk-nestjs-common"; @Injectable() export class StateChangesConsumerService { + static SYSTEM_ACCOUNTS = [ + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqllls0lczs7", // stakingScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l", // validatorScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", // esdtScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", // governanceScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqrlllllllllllllllllllllllllsn60f0k", // jailingAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqlllllllllllllllllllllllllllsr9gav8", // endOfEpochAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", // delegationManagerScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0llllsqkarq6", // firstDelegationScAddress + "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu", // contractDeployScAdress + "erd17rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rcqqkhty3", // genesisMintingAddress + "erd1lllllllllllllllllllllllllllllllllllllllllllllllllllsckry7t", // systemAccountAddress + "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqq2m3f0f", // esdtGlobalSettingsAddresses[0] + "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqsl6e366", // esdtGlobalSettingsAddresses[1] + "erd1lllllllllllllllllllllllllllllllllllllllllllllllllupq9x7ny0", // esdtGlobalSettingsAddresses[2] + ] + constructor( private readonly cacheService: CacheService, private readonly accountDetailsRepository: AccountDetailsRepository, @@ -62,23 +80,39 @@ export class StateChangesConsumerService { // } private async updateAccounts(transformedFinalStates: AccountDetails[]) { - const promisesToWaitFor = [this.accountDetailsRepository.updateAccounts(transformedFinalStates)]; + const promisesToWaitFor = [this.accountDetailsRepository.updateAccounts(transformedFinalStates.filter(account => !AddressUtils.isSmartContractAddress(account.address)))]; - const cacheKeys = []; + const walletCacheKeys = []; + const contractCacheKeys = []; const values = []; for (const account of transformedFinalStates) { - cacheKeys.push(CacheInfo.AccountState(account.address).key); - const { tokens, nfts, ...accountWithoutAssets } = account; - values.push(accountWithoutAssets); + if (!AddressUtils.isSmartContractAddress(account.address)) { + walletCacheKeys.push(CacheInfo.AccountState(account.address).key); + const { tokens, nfts, ...accountWithoutAssets } = account; + values.push(accountWithoutAssets); + } else { + contractCacheKeys.push(CacheInfo.AccountState(account.address).key); + } + } + if (walletCacheKeys.length > 0) { + promisesToWaitFor.push( + this.cacheService.setManyRemote( + walletCacheKeys, + values, + CacheInfo.AccountState('any').ttl, + ) + ); } - promisesToWaitFor.push( - this.cacheService.setManyRemote( - cacheKeys, - values, - CacheInfo.AccountState('any').ttl, - ) - ); - this.deleteLocalCache(cacheKeys); + + if (contractCacheKeys.length > 0) { + promisesToWaitFor.push( + this.cacheService.deleteManyRemote( + contractCacheKeys, + ) + ); + } + + this.deleteLocalCache([...walletCacheKeys, ...contractCacheKeys]); await Promise.all(promisesToWaitFor) } @@ -89,7 +123,10 @@ export class StateChangesConsumerService { private transformFinalStatesToDbFormat(finalStates: Record, shardID: number, blockTimestampMs: number) { const transformed: AccountDetails[] = []; - for (const [_address, state] of Object.entries(finalStates)) { + for (const [address, state] of Object.entries(finalStates)) { + if (StateChangesConsumerService.isSystemContractAddress(address)) { + continue; + } const newAccountState = state.accountState; const tokens = [ @@ -232,4 +269,8 @@ export class StateChangesConsumerService { return diff <= maxLastActivityDiffMs; } + + static isSystemContractAddress(address: string) { + return StateChangesConsumerService.SYSTEM_ACCOUNTS.includes(address); + } } \ No newline at end of file diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts index 6eacca94a..ada8096dd 100644 --- a/src/state-changes/utils/state-changes.decoder.ts +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -140,7 +140,7 @@ export class StateChangesDecoder { const valueBigInt: bigint = this.decodeMxSignMagBigInt(msgEsdtData.Value); const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(keyBuf); - console.log(`key: ${dataTrieChange.key}, identifier: ${identifier}, nonceHex: ${nonceHex}, type: ${msgEsdtData.Type}, value: ${valueBigInt.toString()}`); + return { identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, nonce: parseInt(nonceHex, 16).toString(), From 4aec0f645273da8472fbec7069b706ec87ea57b1 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 10:14:18 +0300 Subject: [PATCH 34/90] fixes --- src/endpoints/accounts-v2/account.service.v2.ts | 3 +++ src/state-changes/state.changes.consumer.service.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts index 543d015c0..57f94aec0 100644 --- a/src/endpoints/accounts-v2/account.service.v2.ts +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -59,6 +59,9 @@ export class AccountServiceV2 { accountFromDb.ownerAssets = assets[accountFromDb.ownerAddress]; } } + + accountFromDb.username = await this.usernameService.getUsernameForAddress(address) ?? undefined; + await this.pluginService.processAccount(accountFromDb); return accountFromDb; } diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 52fc8c4d7..ced576e07 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -62,7 +62,7 @@ export class StateChangesConsumerService { CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(blockWithStateChanges.shardID).ttl, ); - const end = Date.now(); // ms la final + const end = Date.now(); const duration = end - start; if (duration > 10) { // console.dir(finalStates, { depth: null }) From 0ddcde314119f0caf1234948651c5a1eed242f1a Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 10:22:31 +0300 Subject: [PATCH 35/90] fixes --- .../account.details.repository.ts | 92 ++++++------------- src/common/indexer/db/repositories/index.ts | 2 +- .../db/schemas/account.details.schema.ts | 2 +- src/common/indexer/db/schemas/index.ts | 2 +- .../accounts-v2/account.service.v2.ts | 4 +- src/endpoints/nfts/nft.service.ts | 26 ------ src/endpoints/tokens/token.service.ts | 32 ------- src/state-changes/entities/index.ts | 2 +- .../entities/state.changes.entity.ts | 2 +- .../state.changes.consumer.service.ts | 21 +++-- src/state-changes/utils/esdt.d.ts | 25 ++--- .../utils/state-changes.decoder.ts | 12 +-- 12 files changed, 64 insertions(+), 158 deletions(-) diff --git a/src/common/indexer/db/repositories/account.details.repository.ts b/src/common/indexer/db/repositories/account.details.repository.ts index ceb38473d..eb69af846 100644 --- a/src/common/indexer/db/repositories/account.details.repository.ts +++ b/src/common/indexer/db/repositories/account.details.repository.ts @@ -47,7 +47,7 @@ export class AccountDetailsRepository { pendingGuardianAddress: 0, pendingGuardianServiceUid: 0, isGuarded: 0, - } + }; constructor( @InjectModel(AccountDetails.name) private readonly accountDetailsModel: Model @@ -63,9 +63,9 @@ export class AccountDetailsRepository { $project: { _id: 0, tokens: { - $slice: ["$tokens", queryPagination.from, queryPagination.size] + $slice: ["$tokens", queryPagination.from, queryPagination.size], }, - } + }, }, { $project: { @@ -77,22 +77,9 @@ export class AccountDetailsRepository { "tokens.nonce": 1, "tokens.decimals": 1, "tokens.balance": 1, - } - } + }, + }, ]).exec(); - // const result = await this.accountDetailsModel.findOne( - // { address }, - // { - // tokens: { - // $slice: [queryPagination.from, queryPagination.size], - // }, - // "tokens.balance": 0, // Exclude direct balance - // ...AccountDetailsRepository.exclusionFields, - // } - // ).lean(); - //@ts-ignore - // console.log('result', result); - // console.log('result', result); return result[0]?.tokens ?? []; } catch (error) { console.error(`Error fetching tokens for address: ${address}:`, error); @@ -114,10 +101,10 @@ export class AccountDetailsRepository { $filter: { input: "$tokens", as: "token", - cond: { $eq: ["$$token.identifier", identifier] } - } - } - } + cond: { $eq: ["$$token.identifier", identifier] }, + }, + }, + }, }, { $project: { @@ -129,24 +116,9 @@ export class AccountDetailsRepository { "tokens.nonce": 1, "tokens.decimals": 1, "tokens.balance": 1, - } - } + }, + }, ]).exec(); - // console.log('result', result); - // const result = await this.accountDetailsModel.findOne( - // { address }, - // { - // tokens: { - // $slice: [queryPagination.from, queryPagination.size], - // }, - // "tokens.balance": 0, // Exclude direct balance - // ...AccountDetailsRepository.exclusionFields, - // } - // ).lean(); - //@ts-ignore - // console.log('result', result); - // console.log('result', result); - // console.log(result[0].tokens) return result[0]?.tokens[0] ?? undefined; } catch (error) { console.error(`Error fetching token with identifier ${identifier} for address: ${address}:`, error); @@ -168,10 +140,10 @@ export class AccountDetailsRepository { $filter: { input: "$nfts", as: "nft", - cond: { $eq: ["$$nft.identifier", identifier] } - } - } - } + cond: { $eq: ["$$nft.identifier", identifier] }, + }, + }, + }, }, { $project: { @@ -183,19 +155,9 @@ export class AccountDetailsRepository { "nfts.name": 1, "nfts.balance": 1, "nfts.subtype": 1, - } - } + }, + }, ]).exec(); - // const result = await this.accountDetailsModel.findOne( - // { address }, - // { - // nfts: { - // $slice: [queryPagination.from, queryPagination.size] - // }, - // tokens: 0, - // ...AccountDetailsRepository.exclusionFields, - // } - // ).lean(); return result[0]?.nfts[0] ?? undefined; } catch (error) { console.error(`Error fetching nft with identifier ${identifier} for address: ${address}:`, error); @@ -212,8 +174,8 @@ export class AccountDetailsRepository { { $project: { _id: 0, - nfts: { $slice: ["$nfts", queryPagination.from, queryPagination.size] } - } + nfts: { $slice: ["$nfts", queryPagination.from, queryPagination.size] }, + }, }, { $project: { @@ -225,8 +187,8 @@ export class AccountDetailsRepository { "nfts.name": 1, "nfts.balance": 1, "nfts.subtype": 1, - } - } + }, + }, ]).exec(); // const result = await this.accountDetailsModel.findOne( // { address }, @@ -288,7 +250,7 @@ export class AccountDetailsRepository { new: true, // Return the updated document upsert: true, // Create if doesn't exist lean: true, // Return plain JavaScript object - projection: { __v: 0, __id: 0, updatedAt: 0 } // Exclude __v field + projection: { __v: 0, __id: 0, updatedAt: 0 }, // Exclude __v field } ); // console.log('updatedAccount', updatedAccount); @@ -330,8 +292,8 @@ export class AccountDetailsRepository { } // --- tokens --- - let tokensToRemove: string[] = []; - let tokensToUpsert: any[] = []; + const tokensToRemove: string[] = []; + const tokensToUpsert: any[] = []; if (accountDetailed.tokens?.length) { for (const t of accountDetailed.tokens) { @@ -413,8 +375,8 @@ export class AccountDetailsRepository { } // --- nfts --- - let nftsToRemove: string[] = []; - let nftsToUpsert: any[] = []; + const nftsToRemove: string[] = []; + const nftsToUpsert: any[] = []; if (accountDetailed.nfts?.length) { for (const n of accountDetailed.nfts) { @@ -525,4 +487,4 @@ export class AccountDetailsRepository { throw error; } } -} \ No newline at end of file +} diff --git a/src/common/indexer/db/repositories/index.ts b/src/common/indexer/db/repositories/index.ts index dc9908dae..eea469f4e 100644 --- a/src/common/indexer/db/repositories/index.ts +++ b/src/common/indexer/db/repositories/index.ts @@ -1 +1 @@ -export * from './account.details.repository'; \ No newline at end of file +export * from './account.details.repository'; diff --git a/src/common/indexer/db/schemas/account.details.schema.ts b/src/common/indexer/db/schemas/account.details.schema.ts index 69a142d19..352b74fb7 100644 --- a/src/common/indexer/db/schemas/account.details.schema.ts +++ b/src/common/indexer/db/schemas/account.details.schema.ts @@ -124,4 +124,4 @@ export const AccountDetailsSchema = SchemaFactory.createForClass(AccountDetails) AccountDetailsSchema.index({ address: 1 }, { unique: true }); AccountDetailsSchema.index({ "tokens.identifier": 1 }); -AccountDetailsSchema.index({ "nfts.identifier": 1 }); \ No newline at end of file +AccountDetailsSchema.index({ "nfts.identifier": 1 }); diff --git a/src/common/indexer/db/schemas/index.ts b/src/common/indexer/db/schemas/index.ts index 72b1aaabe..56f752654 100644 --- a/src/common/indexer/db/schemas/index.ts +++ b/src/common/indexer/db/schemas/index.ts @@ -1 +1 @@ -export * from './account.details.schema'; \ No newline at end of file +export * from './account.details.schema'; diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts index 57f94aec0..e364a647a 100644 --- a/src/endpoints/accounts-v2/account.service.v2.ts +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -75,9 +75,9 @@ export class AccountServiceV2 { if (isStateChangesConsumerHealty === true && !StateChangesConsumerService.isSystemContractAddress(address)) { account = await this.cachingService.getOrSet( CacheInfo.AccountState(address).key, - async () => this.getAccountWithFallBack(address, options), + async () => await this.getAccountWithFallBack(address, options), CacheInfo.AccountState(address).ttl, - ) + ); } else { account = await this.getAccountRaw(address, options?.withAssets); } diff --git a/src/endpoints/nfts/nft.service.ts b/src/endpoints/nfts/nft.service.ts index 07b03a9c5..f3703125d 100644 --- a/src/endpoints/nfts/nft.service.ts +++ b/src/endpoints/nfts/nft.service.ts @@ -31,8 +31,6 @@ 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"; -import { AccountDetailsRepository } from "src/common/indexer/db"; -import { StateChangesConsumerService } from "src/state-changes/state.changes.consumer.service"; @Injectable() export class NftService { @@ -53,7 +51,6 @@ export class NftService { private readonly esdtAddressService: EsdtAddressService, private readonly mexTokenService: MexTokenService, private readonly lockedAssetService: LockedAssetService, - private readonly accountDetailsRepository: AccountDetailsRepository, ) { this.NFT_THUMBNAIL_PREFIX = this.apiConfigService.getExternalMediaUrl() + '/nfts/asset'; this.DEFAULT_MEDIA = [ @@ -507,17 +504,6 @@ export class NftService { return await this.indexerService.getNftCount(filter); } - async getNftsForAddressFromDb(address: string, queryPagination: QueryPagination, filter: NftFilter, fields?: string[], queryOptions?: NftQueryOptions, source?: EsdtDataSource): Promise { - const isDbUpToDate: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); - if (isDbUpToDate === true) { - const nfts = await this.accountDetailsRepository.getNftsForAddress(address, queryPagination) as NftAccount[]; - if (nfts && nfts.length > 0) { - return nfts; - } - } - return await this.getNftsForAddress(address, queryPagination, filter, fields, queryOptions, source); - } - async getNftsForAddress(address: string, queryPagination: QueryPagination, filter: NftFilter, fields?: string[], queryOptions?: NftQueryOptions, source?: EsdtDataSource): Promise { let nfts = await this.esdtAddressService.getNftsForAddress(address, filter, queryPagination, source, queryOptions); for (const nft of nfts) { @@ -617,18 +603,6 @@ export class NftService { return await this.esdtAddressService.getNftCountForAddressFromElastic(address, filter); } - async getNftForAddressFromDb(address: string, identifier: string, fields?: string[]): Promise { - const isDbUpToDate: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); - if (isDbUpToDate === true) { - const nft = await this.accountDetailsRepository.getNftForAddress(address, identifier) as NftAccount; - if (nft) { - console.log(`found in db nft: ${nft.identifier}`) - return nft; - } - } - return await this.getNftForAddress(address, identifier, fields); - } - async getNftForAddress(address: string, identifier: string, fields?: string[]): Promise { const filter = new NftFilter(); filter.identifiers = [identifier]; diff --git a/src/endpoints/tokens/token.service.ts b/src/endpoints/tokens/token.service.ts index 4dc91deeb..52d1f77da 100644 --- a/src/endpoints/tokens/token.service.ts +++ b/src/endpoints/tokens/token.service.ts @@ -43,8 +43,6 @@ import { MexPairService } from "../mex/mex.pair.service"; import { MexPairState } from "../mex/entities/mex.pair.state"; import { MexTokenType } from "../mex/entities/mex.token.type"; import { NftSubType } from "../nfts/entities/nft.sub.type"; -import { AccountDetailsRepository } from "src/common/indexer/db"; -import { StateChangesConsumerService } from "src/state-changes/state.changes.consumer.service"; @Injectable() export class TokenService { @@ -70,7 +68,6 @@ export class TokenService { private readonly dataApiService: DataApiService, private readonly mexPairService: MexPairService, private readonly apiService: ApiService, - private readonly accountDetailsRepository: AccountDetailsRepository, ) { } async isToken(identifier: string): Promise { @@ -253,18 +250,6 @@ export class TokenService { return tokens.length; } - async getTokensForAddressFromDb(address: string, queryPagination: QueryPagination, filter: TokenFilter): Promise { - const isDbUpToDate: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000) - if (isDbUpToDate === true) { - const tokens = await this.accountDetailsRepository.getTokensForAddress(address, queryPagination) as TokenWithBalance[]; - if (tokens && tokens.length > 0) { - return tokens; - } - } - - return await this.getTokensForAddress(address, queryPagination, filter); - } - async getTokensForAddress(address: string, queryPagination: QueryPagination, filter: TokenFilter): Promise { let tokens: TokenWithBalance[]; if (AddressUtils.isSmartContractAddress(address)) { @@ -354,23 +339,6 @@ export class TokenService { return tokens; } - async getTokenForAddressFromDb(address: string, identifier: string): Promise { - const isDbUpToDate: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); - if (isDbUpToDate === true) { - const token = await this.accountDetailsRepository.getTokenForAddress(address, identifier) as TokenDetailedWithBalance; - if (token) { - console.log(`found in db token: ${token.identifier}`) - return token; - } - } - const token = await this.getTokenForAddress(address, identifier); - // if (token) { - // this.dbWriterService.writeAccountToDb({ tokens: [token] }); - // } - return token; - } - - async getTokenForAddress(address: string, identifier: string): Promise { const esdtIdentifier = identifier.split('-').slice(0, 2).join('-'); diff --git a/src/state-changes/entities/index.ts b/src/state-changes/entities/index.ts index e266c949a..5f4dc3633 100644 --- a/src/state-changes/entities/index.ts +++ b/src/state-changes/entities/index.ts @@ -1 +1 @@ -export * from './state.changes.entity'; \ No newline at end of file +export * from './state.changes.entity'; diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts index 2ee7dcead..25601b004 100644 --- a/src/state-changes/entities/state.changes.entity.ts +++ b/src/state-changes/entities/state.changes.entity.ts @@ -142,4 +142,4 @@ export enum StateAccessOperation { export enum DataTrieChangeOperation { NotDelete = 0, Delete = 1, -} \ No newline at end of file +} diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index ced576e07..8afa3b5c1 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -11,10 +11,11 @@ import { NftType } from "src/endpoints/nfts/entities/nft.type"; import { NftSubType } from "src/endpoints/nfts/entities/nft.sub.type"; import { ClientProxy } from "@nestjs/microservices"; import { StateChangesDecoder } from "./utils/state-changes.decoder"; -import { AddressUtils } from "@multiversx/sdk-nestjs-common"; +import { AddressUtils, OriginLogger } from "@multiversx/sdk-nestjs-common"; @Injectable() export class StateChangesConsumerService { + private readonly logger = new OriginLogger(StateChangesConsumerService.name); static SYSTEM_ACCOUNTS = [ "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqllls0lczs7", // stakingScAddress "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l", // validatorScAddress @@ -30,7 +31,7 @@ export class StateChangesConsumerService { "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqq2m3f0f", // esdtGlobalSettingsAddresses[0] "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqsl6e366", // esdtGlobalSettingsAddresses[1] "erd1lllllllllllllllllllllllllllllllllllllllllllllllllupq9x7ny0", // esdtGlobalSettingsAddresses[2] - ] + ]; constructor( private readonly cacheService: CacheService, @@ -66,11 +67,11 @@ export class StateChangesConsumerService { const duration = end - start; if (duration > 10) { // console.dir(finalStates, { depth: null }) - console.log(`decoding duration: ${decodingDuration}ms`) - console.log(`processing time shard ${blockWithStateChanges.shardID}: ${duration}ms`); + this.logger.log(`decoding duration: ${decodingDuration}ms`); + this.logger.log(`processing time shard ${blockWithStateChanges.shardID}: ${duration}ms`); } } catch (error) { - console.error(`Error consuming state changes:`, error); + this.logger.error(`Error consuming state changes from shard ${blockWithStateChanges.shardID}:`, error); throw error; } } @@ -114,7 +115,7 @@ export class StateChangesConsumerService { this.deleteLocalCache([...walletCacheKeys, ...contractCacheKeys]); - await Promise.all(promisesToWaitFor) + await Promise.all(promisesToWaitFor); } private decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { return StateChangesDecoder.decodeStateChangesFinal(blockWithStateChanges); @@ -140,7 +141,7 @@ export class StateChangesConsumerService { ...state.esdtState.SemiFungible, ...state.esdtState.DynamicSFT, ...state.esdtState.MetaFungible, - ...state.esdtState.DynamicMeta + ...state.esdtState.DynamicMeta, ]; if (newAccountState) { @@ -165,7 +166,7 @@ export class StateChangesConsumerService { subType: this.parseEsdtSubtype(nft.type), collection: nft.identifier.replace(/-[^-]*$/, ''), // delete everything after last `-` character inclusive balance: nft.value, - })) + })), }); transformed.push(parsedAccount); } @@ -263,7 +264,7 @@ export class StateChangesConsumerService { cacheService.setManyLocal(keys, timestampsMs, 0.6); } - const minTimestamp = Math.min(...(timestampsMs as number[])) + const minTimestamp = Math.min(...(timestampsMs as number[])); const diff = Date.now() - minTimestamp; @@ -273,4 +274,4 @@ export class StateChangesConsumerService { static isSystemContractAddress(address: string) { return StateChangesConsumerService.SYSTEM_ACCOUNTS.includes(address); } -} \ No newline at end of file +} diff --git a/src/state-changes/utils/esdt.d.ts b/src/state-changes/utils/esdt.d.ts index b6b878b27..ff78941a3 100644 --- a/src/state-changes/utils/esdt.d.ts +++ b/src/state-changes/utils/esdt.d.ts @@ -1,6 +1,7 @@ import * as $protobuf from "protobufjs"; import Long = require("long"); /** Properties of a MetaData. */ +//@ts-ignore export interface IMetaData { } @@ -44,7 +45,7 @@ export class MetaData implements IMetaData { * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): MetaData; + public static decode(reader: ($protobuf.Reader | Uint8Array), length?: number): MetaData; /** * Decodes a MetaData message from the specified reader or buffer, length delimited. @@ -53,14 +54,14 @@ export class MetaData implements IMetaData { * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): MetaData; + public static decodeDelimited(reader: ($protobuf.Reader | Uint8Array)): MetaData; /** * Verifies a MetaData message. * @param message Plain object to verify * @returns `null` if valid, otherwise the reason why it is not */ - public static verify(message: { [k: string]: any }): (string|null); + public static verify(message: { [k: string]: any }): (string | null); /** * Creates a MetaData message from a plain object. Also converts values to their respective internal types. @@ -95,19 +96,19 @@ export class MetaData implements IMetaData { export interface IESDigitalToken { /** ESDigitalToken Type */ - Type?: (number|null); + Type?: (number | null); /** ESDigitalToken Value */ - Value?: (Uint8Array|null); + Value?: (Uint8Array | null); /** ESDigitalToken Properties */ - Properties?: (Uint8Array|null); + Properties?: (Uint8Array | null); /** ESDigitalToken TokenMetaData */ - TokenMetaData?: (IMetaData|null); + TokenMetaData?: (IMetaData | null); /** ESDigitalToken Reserved */ - Reserved?: (Uint8Array|null); + Reserved?: (Uint8Array | null); } /** Represents a ESDigitalToken. */ @@ -129,7 +130,7 @@ export class ESDigitalToken implements IESDigitalToken { public Properties: Uint8Array; /** ESDigitalToken TokenMetaData. */ - public TokenMetaData?: (IMetaData|null); + public TokenMetaData?: (IMetaData | null); /** ESDigitalToken Reserved. */ public Reserved: Uint8Array; @@ -165,7 +166,7 @@ export class ESDigitalToken implements IESDigitalToken { * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): ESDigitalToken; + public static decode(reader: ($protobuf.Reader | Uint8Array), length?: number): ESDigitalToken; /** * Decodes a ESDigitalToken message from the specified reader or buffer, length delimited. @@ -174,14 +175,14 @@ export class ESDigitalToken implements IESDigitalToken { * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): ESDigitalToken; + public static decodeDelimited(reader: ($protobuf.Reader | Uint8Array)): ESDigitalToken; /** * Verifies a ESDigitalToken message. * @param message Plain object to verify * @returns `null` if valid, otherwise the reason why it is not */ - public static verify(message: { [k: string]: any }): (string|null); + public static verify(message: { [k: string]: any }): (string | null); /** * Creates a ESDigitalToken message from a plain object. Also converts values to their respective internal types. diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts index ada8096dd..0b594c1e3 100644 --- a/src/state-changes/utils/state-changes.decoder.ts +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -13,7 +13,7 @@ import { ESDTType, StateAccessOperation, StateAccessPerAccountRaw, - StateChanges + StateChanges, } from "../entities"; export class StateChangesDecoder { @@ -157,8 +157,8 @@ export class StateChangesDecoder { } } catch (e: any) { console.warn(`Could not decode as EsdtData: ${e.message}`); - console.log(address, ':') - console.dir(dataTrieChange) + console.log(address, ':'); + console.dir(dataTrieChange); return null; } } @@ -175,7 +175,7 @@ export class StateChangesDecoder { stateAccess.forEach((sa: StateAccessPerAccountRaw, i: number) => { const base64AccountData = sa.mainTrieVal; - let decodedAccountData: any = null + let decodedAccountData: any = null; if (base64AccountData) { const bufAccountData = Buffer.from(base64AccountData, "base64"); decodedAccountData = this.getDecodedUserAccountData(bufAccountData); @@ -184,7 +184,7 @@ export class StateChangesDecoder { const dataTrieChanges = sa.dataTrieChanges; - let allDecodedEsdtData: any[] = []; + const allDecodedEsdtData: any[] = []; if (dataTrieChanges) { for (const dataTrieChange of dataTrieChanges) { if (dataTrieChange.version === 0) { @@ -353,7 +353,7 @@ export class StateChangesDecoder { developerRewardChanged: false, ownerAddressChanged: false, userNameChanged: false, - codeMetadataChanged: false + codeMetadataChanged: false, }); let finalNewAccount = false; From 3863f146931c6e822965a464f483656b4c3f6db2 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 10:27:13 +0300 Subject: [PATCH 36/90] clean --- .../db/repositories/account.details.repository.ts | 15 +++------------ .../state.changes.consumer.service.ts | 5 ----- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/common/indexer/db/repositories/account.details.repository.ts b/src/common/indexer/db/repositories/account.details.repository.ts index eb69af846..fbe7af186 100644 --- a/src/common/indexer/db/repositories/account.details.repository.ts +++ b/src/common/indexer/db/repositories/account.details.repository.ts @@ -8,9 +8,11 @@ import { TokenWithBalance } from 'src/endpoints/tokens/entities/token.with.balan import { NftAccount } from 'src/endpoints/nfts/entities/nft.account'; import { Injectable } from '@nestjs/common'; import { AccountDetailed } from 'src/endpoints/accounts/entities/account.detailed'; +import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @Injectable() export class AccountDetailsRepository { + private readonly logger = new OriginLogger(AccountDetailsRepository.name); static readonly exclusionFields = { _id: 0, __v: 0, @@ -190,16 +192,6 @@ export class AccountDetailsRepository { }, }, ]).exec(); - // const result = await this.accountDetailsModel.findOne( - // { address }, - // { - // nfts: { - // $slice: [queryPagination.from, queryPagination.size] - // }, - // tokens: 0, - // ...AccountDetailsRepository.exclusionFields, - // } - // ).lean(); return result[0]?.nfts || []; } catch (error) { @@ -253,7 +245,6 @@ export class AccountDetailsRepository { projection: { __v: 0, __id: 0, updatedAt: 0 }, // Exclude __v field } ); - // console.log('updatedAccount', updatedAccount); return updatedAccount; } catch (error: any) { // Handle potential duplicate key errors @@ -475,7 +466,7 @@ export class AccountDetailsRepository { } } - console.log('number of write operations:', totalOperations); + this.logger.log(`number of write operations: ${totalOperations}`); const result = await this.accountDetailsModel.bulkWrite(operations, { ordered: true, diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 8afa3b5c1..f182df571 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -66,7 +66,6 @@ export class StateChangesConsumerService { const end = Date.now(); const duration = end - start; if (duration > 10) { - // console.dir(finalStates, { depth: null }) this.logger.log(`decoding duration: ${decodingDuration}ms`); this.logger.log(`processing time shard ${blockWithStateChanges.shardID}: ${duration}ms`); } @@ -76,10 +75,6 @@ export class StateChangesConsumerService { } } - // private decodeStateChanges(stateChanges: StateChangesRaw) { - // return decodeStateChangesRaw(stateChanges); - // } - private async updateAccounts(transformedFinalStates: AccountDetails[]) { const promisesToWaitFor = [this.accountDetailsRepository.updateAccounts(transformedFinalStates.filter(account => !AddressUtils.isSmartContractAddress(account.address)))]; From de5eb293b76c766a91457cc4b44b620875d08de3 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 10:30:10 +0300 Subject: [PATCH 37/90] lint --- src/state-changes/utils/esdt.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state-changes/utils/esdt.d.ts b/src/state-changes/utils/esdt.d.ts index ff78941a3..8ab4f6aa1 100644 --- a/src/state-changes/utils/esdt.d.ts +++ b/src/state-changes/utils/esdt.d.ts @@ -1,7 +1,7 @@ import * as $protobuf from "protobufjs"; import Long = require("long"); /** Properties of a MetaData. */ -//@ts-ignore +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface IMetaData { } From d0776bdb0e227ccba17c14b73aae1c0be8fec2be Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 10:34:18 +0300 Subject: [PATCH 38/90] devnet config example --- config/config.devnet.yaml | 6 ++++-- src/common/api-config/api.config.service.ts | 9 --------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 4166f189a..36e3e2dac 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -20,9 +20,11 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: - websocketSubscription: + stateChanges: enabled: false - port: 6002 + port: 5675 + url: 'amqp://guest:guest@127.0.0.1:5672' + exchange: 'state_accesses' eventsNotifier: enabled: false port: 5674 diff --git a/src/common/api-config/api.config.service.ts b/src/common/api-config/api.config.service.ts index 53a5d7e87..479fca15b 100644 --- a/src/common/api-config/api.config.service.ts +++ b/src/common/api-config/api.config.service.ts @@ -990,13 +990,4 @@ export class ApiConfigService { return exchange; } - - getStateChangesQueue(): string { - const queue = this.configService.get('features.stateChanges.queue'); - if (!queue) { - throw new Error('No state changes queue present'); - } - - return queue; - } } From 81d07dcc0e0627d46ad0bf0f16a0c1c6bced4329 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 10:50:28 +0300 Subject: [PATCH 39/90] fix --- config/config.devnet.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 36e3e2dac..2a4571eeb 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -135,7 +135,7 @@ urls: elastic: - 'https://devnet-index.multiversx.com' gateway: - - 'https://k8s-devnet-gateway.multiversx.com' + - 'https://devnet-gateway.multiversx.com' verifier: 'https://play-api.multiversx.com' redis: '127.0.0.1' rabbitmq: 'amqp://127.0.0.1:5672' From 256ad76447cf183299b45ce9aba49d2841d0fbcb Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 11:08:21 +0300 Subject: [PATCH 40/90] fix --- src/common/indexer/db/mongodb.module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/indexer/db/mongodb.module.ts b/src/common/indexer/db/mongodb.module.ts index d954041f5..36f157660 100644 --- a/src/common/indexer/db/mongodb.module.ts +++ b/src/common/indexer/db/mongodb.module.ts @@ -14,8 +14,8 @@ import { EventEmitterModule } from "@nestjs/event-emitter"; imports: [ApiConfigModule], inject: [ApiConfigService], useFactory: (apiConfigService: ApiConfigService) => ({ - uri: apiConfigService.getDatabaseUrl().replace(":27017", ''), // TODO: remove this hack - tls: false, + uri: apiConfigService.getDatabaseUrl(), + tls: true, tlsAllowInvalidCertificates: true, }), }), From a692a6d89e9a17abba6931ee4075f35ad9afce4e Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 11:39:41 +0300 Subject: [PATCH 41/90] test --- src/common/indexer/db/mongodb.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/indexer/db/mongodb.module.ts b/src/common/indexer/db/mongodb.module.ts index 36f157660..616e51243 100644 --- a/src/common/indexer/db/mongodb.module.ts +++ b/src/common/indexer/db/mongodb.module.ts @@ -14,7 +14,7 @@ import { EventEmitterModule } from "@nestjs/event-emitter"; imports: [ApiConfigModule], inject: [ApiConfigService], useFactory: (apiConfigService: ApiConfigService) => ({ - uri: apiConfigService.getDatabaseUrl(), + uri: apiConfigService.getDatabaseUrl().replace(":27017", ''), // TODO: remove this hack tls: true, tlsAllowInvalidCertificates: true, }), From 0f0147e68507dd2f58c1376ceb64759e0ceb1fe0 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 11:46:49 +0300 Subject: [PATCH 42/90] update configs --- config/config.devnet-old.yaml | 5 +++++ config/config.e2e-mocked.mainnet.yaml | 5 +++++ config/config.e2e.mainnet.yaml | 5 +++++ config/config.mainnet.yaml | 5 +++++ config/config.testnet.yaml | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/config/config.devnet-old.yaml b/config/config.devnet-old.yaml index 9bc4134b8..f0160dd15 100644 --- a/config/config.devnet-old.yaml +++ b/config/config.devnet-old.yaml @@ -24,6 +24,11 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: + stateChanges: + enabled: false + port: 5675 + url: 'amqp://guest:guest@127.0.0.1:5672' + exchange: 'state_accesses' eventsNotifier: enabled: false port: 5674 diff --git a/config/config.e2e-mocked.mainnet.yaml b/config/config.e2e-mocked.mainnet.yaml index ef1cd3eed..e7e574fff 100644 --- a/config/config.e2e-mocked.mainnet.yaml +++ b/config/config.e2e-mocked.mainnet.yaml @@ -5,6 +5,11 @@ api: private: true graphql: true features: + stateChanges: + enabled: false + port: 5675 + url: 'amqp://guest:guest@127.0.0.1:5672' + exchange: 'state_accesses' dataApi: enabled: false serviceUrl: 'https://data-api.multiversx.com' diff --git a/config/config.e2e.mainnet.yaml b/config/config.e2e.mainnet.yaml index b4c431ec0..531b8a66d 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -20,6 +20,11 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: + stateChanges: + enabled: false + port: 5675 + url: 'amqp://guest:guest@127.0.0.1:5672' + exchange: 'state_accesses' eventsNotifier: enabled: false port: 5674 diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index edc9c7a00..8b8ce1b3a 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -20,6 +20,11 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: + stateChanges: + enabled: false + port: 5675 + url: 'amqp://guest:guest@127.0.0.1:5672' + exchange: 'state_accesses' eventsNotifier: enabled: false port: 5674 diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 1f1887414..361ecff13 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -20,6 +20,11 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: + stateChanges: + enabled: false + port: 5675 + url: 'amqp://guest:guest@127.0.0.1:5672' + exchange: 'state_accesses' eventsNotifier: enabled: false port: 5674 From 5018ac5397687074a127d7201a163145abb60176 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 12:10:51 +0300 Subject: [PATCH 43/90] remove mongo module from token/nft modules --- src/endpoints/nfts/nft.module.ts | 2 -- src/endpoints/tokens/token.module.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/endpoints/nfts/nft.module.ts b/src/endpoints/nfts/nft.module.ts index 1e4733716..6242b358d 100644 --- a/src/endpoints/nfts/nft.module.ts +++ b/src/endpoints/nfts/nft.module.ts @@ -10,7 +10,6 @@ import { TokenModule } from "../tokens/token.module"; import { NftExtendedAttributesService } from "./nft.extendedattributes.service"; import { NftService } from "./nft.service"; import { LockedAssetModule } from "../../common/locked-asset/locked-asset.module"; -import { MongoDbModule } from "src/common/indexer/db"; @Module({ imports: [ @@ -23,7 +22,6 @@ import { MongoDbModule } from "src/common/indexer/db"; forwardRef(() => AssetsModule), forwardRef(() => LockedAssetModule), NftMediaModule, - MongoDbModule, ], providers: [ NftService, NftExtendedAttributesService, diff --git a/src/endpoints/tokens/token.module.ts b/src/endpoints/tokens/token.module.ts index 811a90d33..14669ed7b 100644 --- a/src/endpoints/tokens/token.module.ts +++ b/src/endpoints/tokens/token.module.ts @@ -9,7 +9,6 @@ import { MexModule } from "../mex/mex.module"; import { CollectionModule } from "../collections/collection.module"; import { PluginModule } from "src/plugins/plugin.module"; import { TransferModule } from "../transfers/transfer.module"; -import { MongoDbModule } from "src/common/indexer/db"; @Module({ imports: [ @@ -21,7 +20,6 @@ import { MongoDbModule } from "src/common/indexer/db"; forwardRef(() => MexModule.forRoot()), forwardRef(() => CollectionModule), forwardRef(() => PluginModule), - MongoDbModule, ], providers: [ TokenService, TokenTransferService, From dc9a3e4f4e7dfe44903ec476553b78dc535187e1 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 12:14:07 +0300 Subject: [PATCH 44/90] test --- src/common/indexer/db/mongodb.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/indexer/db/mongodb.module.ts b/src/common/indexer/db/mongodb.module.ts index 616e51243..d954041f5 100644 --- a/src/common/indexer/db/mongodb.module.ts +++ b/src/common/indexer/db/mongodb.module.ts @@ -15,7 +15,7 @@ import { EventEmitterModule } from "@nestjs/event-emitter"; inject: [ApiConfigService], useFactory: (apiConfigService: ApiConfigService) => ({ uri: apiConfigService.getDatabaseUrl().replace(":27017", ''), // TODO: remove this hack - tls: true, + tls: false, tlsAllowInvalidCertificates: true, }), }), From 1960552f027276a4f2af5047bd5243a893a6d90d Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 13:23:21 +0300 Subject: [PATCH 45/90] add mongo default user pass --- config/config.devnet-old.yaml | 2 +- config/config.devnet.yaml | 2 +- config/config.e2e-mocked.mainnet.yaml | 2 +- config/config.e2e.mainnet.yaml | 2 +- config/config.mainnet.yaml | 2 +- config/config.testnet.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/config.devnet-old.yaml b/config/config.devnet-old.yaml index f0160dd15..5c8c4e0ff 100644 --- a/config/config.devnet-old.yaml +++ b/config/config.devnet-old.yaml @@ -98,7 +98,7 @@ indexer: maxPagination: 10000 database: enabled: false - url: 'mongodb://127.0.0.1:27017/api?authSource=admin' + url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' type: 'mysql' host: 'localhost' port: 3306 diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 2a4571eeb..164f431d5 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -151,7 +151,7 @@ indexer: maxPagination: 10000 database: enabled: false - url: 'mongodb://127.0.0.1:27017/api?authSource=admin' + url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' type: 'mysql' host: 'localhost' port: 3306 diff --git a/config/config.e2e-mocked.mainnet.yaml b/config/config.e2e-mocked.mainnet.yaml index e7e574fff..706dcb53b 100644 --- a/config/config.e2e-mocked.mainnet.yaml +++ b/config/config.e2e-mocked.mainnet.yaml @@ -42,7 +42,7 @@ urls: maiarId: 'https://id-api.multiversx.com' database: enabled: false - url: 'mongodb://127.0.0.1:27017/api?authSource=admin' + url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' host: 'localhost' port: 3306 username: 'root' diff --git a/config/config.e2e.mainnet.yaml b/config/config.e2e.mainnet.yaml index 531b8a66d..0445ad0bf 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -155,7 +155,7 @@ indexer: maxPagination: 10000 database: enabled: false - url: 'mongodb://127.0.0.1:27017/api?authSource=admin' + url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' type: 'mysql' host: 'localhost' port: 3306 diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index 8b8ce1b3a..1efba8cad 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -155,7 +155,7 @@ indexer: maxPagination: 10000 database: enabled: false - url: 'mongodb://127.0.0.1:27017/api?authSource=admin' + url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' type: 'mysql' host: 'localhost' port: 3306 diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 361ecff13..8b9aa7fa6 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -151,7 +151,7 @@ urls: maiarId: 'https://testnet-id-api.multiversx.com' database: enabled: false - url: 'mongodb://127.0.0.1:27017/api?authSource=admin' + url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' type: 'mysql' host: 'localhost' port: 3306 From 0b4fdc6c0dc2aa3bcde751b540da9cbce7a08b45 Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Fri, 10 Oct 2025 13:25:09 +0300 Subject: [PATCH 46/90] fix mock --- src/test/unit/controllers/accounts.controller.spec.ts | 2 -- .../unit/controllers/services.mock/account.services.mock.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/unit/controllers/accounts.controller.spec.ts b/src/test/unit/controllers/accounts.controller.spec.ts index ab99de595..b163541fc 100644 --- a/src/test/unit/controllers/accounts.controller.spec.ts +++ b/src/test/unit/controllers/accounts.controller.spec.ts @@ -602,5 +602,3 @@ describe('AccountController', () => { return address.substring(0, desiredLength + 'erd1'.length); } }); - - diff --git a/src/test/unit/controllers/services.mock/account.services.mock.ts b/src/test/unit/controllers/services.mock/account.services.mock.ts index df379edfd..b3345e94b 100644 --- a/src/test/unit/controllers/services.mock/account.services.mock.ts +++ b/src/test/unit/controllers/services.mock/account.services.mock.ts @@ -78,6 +78,7 @@ export const mockApiConfigService = () => ({ getMediaUrl: jest.fn().mockReturnValue(''), isElasticCircuitBreakerEnabled: jest.fn().mockReturnValue(false), getElasticCircuitBreakerConfig: jest.fn().mockReturnValue({}), + getDatabaseUrl: jest.fn().mockReturnValue(''), getConfig: jest.fn(), }); From 0113ef3b97e3d1edc93466e582be7e7a011fed0a Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Fri, 10 Oct 2025 14:45:55 +0300 Subject: [PATCH 47/90] passthrough db in tests --- src/common/indexer/db/mongodb.module.ts | 57 ++++++++++++++++--------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/common/indexer/db/mongodb.module.ts b/src/common/indexer/db/mongodb.module.ts index d954041f5..5ba14b90c 100644 --- a/src/common/indexer/db/mongodb.module.ts +++ b/src/common/indexer/db/mongodb.module.ts @@ -5,30 +5,45 @@ import { ApiConfigService } from "src/common/api-config/api.config.service"; import { AccountDetails, AccountDetailsSchema } from "./schemas"; import { AccountDetailsRepository } from "./repositories"; import { EventEmitterModule } from "@nestjs/event-emitter"; +import configuration from "config/configuration"; -@Module({ - imports: [ - EventEmitterModule.forRoot({ maxListeners: 1 }), - MongooseModule.forRootAsync({ - imports: [ApiConfigModule], - inject: [ApiConfigService], - useFactory: (apiConfigService: ApiConfigService) => ({ - uri: apiConfigService.getDatabaseUrl().replace(":27017", ''), // TODO: remove this hack - tls: false, - tlsAllowInvalidCertificates: true, - }), +const isPassThrough = process.env.PERSISTENCE === 'passthrough' || configuration()?.database?.enabled === false; + +const mongoImports = isPassThrough ? [] : [ + EventEmitterModule.forRoot({ maxListeners: 1 }), + MongooseModule.forRootAsync({ + imports: [ApiConfigModule], + inject: [ApiConfigService], + useFactory: (apiConfigService: ApiConfigService) => ({ + uri: apiConfigService.getDatabaseUrl().replace(":27017", ''), // TODO: remove this hack + tls: false, + tlsAllowInvalidCertificates: true, }), - MongooseModule.forFeature([ - { name: AccountDetails.name, schema: AccountDetailsSchema }, + }), + MongooseModule.forFeature([ + { name: AccountDetails.name, schema: AccountDetailsSchema }, + ]), +]; - ]), - ], - providers: [ - AccountDetailsRepository, - ], - exports: [ - AccountDetailsRepository, - ], +const mongoProviders = isPassThrough ? [ + { + provide: AccountDetailsRepository, + useValue: { + getTokensForAddress: async () => [], + getTokenForAddress: async () => undefined, + getNftsForAddress: async () => [], + getNftForAddress: async () => undefined, + getAccount: async () => null, + updateAccount: async () => null, + updateAccounts: async () => [], + }, + }, +] : [AccountDetailsRepository]; + +@Module({ + imports: mongoImports, + providers: mongoProviders, + exports: [AccountDetailsRepository], }) export class MongoDbModule { } From 0556276fc8b84ee17adf0e0c2cad919d363b673d Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 14:53:59 +0300 Subject: [PATCH 48/90] add tls in config --- config/config.devnet-old.yaml | 1 + config/config.devnet.yaml | 1 + config/config.e2e-mocked.mainnet.yaml | 1 + config/config.e2e.mainnet.yaml | 1 + config/config.mainnet.yaml | 1 + config/config.testnet.yaml | 1 + 6 files changed, 6 insertions(+) diff --git a/config/config.devnet-old.yaml b/config/config.devnet-old.yaml index 5c8c4e0ff..5f4e58c3f 100644 --- a/config/config.devnet-old.yaml +++ b/config/config.devnet-old.yaml @@ -99,6 +99,7 @@ indexer: database: enabled: false url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' + tls: false type: 'mysql' host: 'localhost' port: 3306 diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 164f431d5..73b41a352 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -152,6 +152,7 @@ indexer: database: enabled: false url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' + tls: false type: 'mysql' host: 'localhost' port: 3306 diff --git a/config/config.e2e-mocked.mainnet.yaml b/config/config.e2e-mocked.mainnet.yaml index 706dcb53b..d7f5bd996 100644 --- a/config/config.e2e-mocked.mainnet.yaml +++ b/config/config.e2e-mocked.mainnet.yaml @@ -43,6 +43,7 @@ urls: database: enabled: false url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' + tls: false host: 'localhost' port: 3306 username: 'root' diff --git a/config/config.e2e.mainnet.yaml b/config/config.e2e.mainnet.yaml index 0445ad0bf..a75940f75 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -156,6 +156,7 @@ indexer: database: enabled: false url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' + tls: false type: 'mysql' host: 'localhost' port: 3306 diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index 1efba8cad..ef14b7fc3 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -156,6 +156,7 @@ indexer: database: enabled: false url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' + tls: false type: 'mysql' host: 'localhost' port: 3306 diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 8b9aa7fa6..099704700 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -152,6 +152,7 @@ urls: database: enabled: false url: 'mongodb://root:secret@127.0.0.1:27017/api?authSource=admin' + tls: false type: 'mysql' host: 'localhost' port: 3306 From cf4f4d6d4cedd08367eceeb55268d80ab1ac8e9d Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Fri, 10 Oct 2025 14:58:12 +0300 Subject: [PATCH 49/90] fix lint --- src/common/indexer/db/mongodb.module.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/common/indexer/db/mongodb.module.ts b/src/common/indexer/db/mongodb.module.ts index 5ba14b90c..13823fd82 100644 --- a/src/common/indexer/db/mongodb.module.ts +++ b/src/common/indexer/db/mongodb.module.ts @@ -30,13 +30,13 @@ const mongoProviders = isPassThrough ? [ { provide: AccountDetailsRepository, useValue: { - getTokensForAddress: async () => [], - getTokenForAddress: async () => undefined, - getNftsForAddress: async () => [], - getNftForAddress: async () => undefined, - getAccount: async () => null, - updateAccount: async () => null, - updateAccounts: async () => [], + getTokensForAddress: () => Promise.resolve([]), + getTokenForAddress: () => Promise.resolve(undefined), + getNftsForAddress: () => Promise.resolve([]), + getNftForAddress: () => Promise.resolve(undefined), + getAccount: () => Promise.resolve(null), + updateAccount: () => Promise.resolve(null), + updateAccounts: () => Promise.resolve([]), }, }, ] : [AccountDetailsRepository]; From b5a543be0423ec712756d5ffb3887ff016c2547b Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 15:06:34 +0300 Subject: [PATCH 50/90] add config --- config/config.devnet-old.yaml | 2 ++ config/config.devnet.yaml | 4 ++- config/config.e2e-mocked.mainnet.yaml | 2 ++ config/config.e2e.mainnet.yaml | 2 ++ config/config.mainnet.yaml | 2 ++ config/config.testnet.yaml | 2 ++ src/common/api-config/api.config.service.ts | 18 +++++++++++ src/common/indexer/db/mongodb.module.ts | 2 +- src/main.ts | 34 +++++++++++---------- 9 files changed, 50 insertions(+), 18 deletions(-) diff --git a/config/config.devnet-old.yaml b/config/config.devnet-old.yaml index 5f4e58c3f..ea1c72322 100644 --- a/config/config.devnet-old.yaml +++ b/config/config.devnet-old.yaml @@ -131,3 +131,5 @@ inflation: nftProcess: parallelism: 1 maxRetries: 3 +pubSubListener: + enabled: true diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 73b41a352..91b9709d4 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -189,4 +189,6 @@ compression: enabled: true level: 6 threshold: 1024 - chunkSize: 16384 \ No newline at end of file + chunkSize: 16384 +pubSubListener: + enabled: true \ No newline at end of file diff --git a/config/config.e2e-mocked.mainnet.yaml b/config/config.e2e-mocked.mainnet.yaml index d7f5bd996..8f49ca60f 100644 --- a/config/config.e2e-mocked.mainnet.yaml +++ b/config/config.e2e-mocked.mainnet.yaml @@ -82,3 +82,5 @@ test: transaction-action: mex: microServiceUrl: 'https://graph.xexchange.com/graphql' +pubSubListener: + enabled: true diff --git a/config/config.e2e.mainnet.yaml b/config/config.e2e.mainnet.yaml index a75940f75..5944ee221 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -189,3 +189,5 @@ inflation: nftProcess: parallelism: 1 maxRetries: 3 +pubSubListener: + enabled: true diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index ef14b7fc3..52e8fb986 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -194,3 +194,5 @@ compression: level: 6 threshold: 1024 chunkSize: 16384 +pubSubListener: + enabled: true diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 099704700..a8eda927c 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -193,3 +193,5 @@ compression: level: 6 threshold: 1024 chunkSize: 16384 +pubSubListener: + enabled: true diff --git a/src/common/api-config/api.config.service.ts b/src/common/api-config/api.config.service.ts index 479fca15b..07985f82c 100644 --- a/src/common/api-config/api.config.service.ts +++ b/src/common/api-config/api.config.service.ts @@ -481,6 +481,15 @@ export class ApiConfigService { return databaseUrl; } + isDatabaseTlsEnabled(): boolean { + const isDatabaseTlsEnabled = this.configService.get('database.tls'); + if (!isDatabaseTlsEnabled) { + throw new Error('No database.tls present'); + } + + return isDatabaseTlsEnabled; + } + getDatabaseConnection(): any { return { host: this.getDatabaseHost(), @@ -990,4 +999,13 @@ export class ApiConfigService { return exchange; } + + isPubSubListenerEnabled(): boolean { + const isPubSubListenerEnabled = this.configService.get('pubSubListener.enabled'); + if (isPubSubListenerEnabled == null) { + return true; + } + + return isPubSubListenerEnabled; + } } diff --git a/src/common/indexer/db/mongodb.module.ts b/src/common/indexer/db/mongodb.module.ts index 13823fd82..50d57f2b2 100644 --- a/src/common/indexer/db/mongodb.module.ts +++ b/src/common/indexer/db/mongodb.module.ts @@ -17,7 +17,7 @@ const mongoImports = isPassThrough ? [] : [ inject: [ApiConfigService], useFactory: (apiConfigService: ApiConfigService) => ({ uri: apiConfigService.getDatabaseUrl().replace(":27017", ''), // TODO: remove this hack - tls: false, + tls: apiConfigService.isDatabaseTlsEnabled(), tlsAllowInvalidCertificates: true, }), }), diff --git a/src/main.ts b/src/main.ts index 9c6b692a9..a150c2e75 100644 --- a/src/main.ts +++ b/src/main.ts @@ -146,23 +146,25 @@ async function bootstrap() { await stateChangesApp.listen(apiConfigService.getStateChangesFeaturePort()); } - const pubSubApp = await NestFactory.createMicroservice( - PubSubListenerModule, - { - transport: Transport.REDIS, - options: { - host: apiConfigService.getRedisUrl(), - port: 6379, - retryAttempts: 100, - retryDelay: 1000, - retryStrategy: () => 1000, + if (apiConfigService.isPubSubListenerEnabled()) { + const pubSubApp = await NestFactory.createMicroservice( + PubSubListenerModule, + { + transport: Transport.REDIS, + options: { + host: apiConfigService.getRedisUrl(), + port: 6379, + retryAttempts: 100, + retryDelay: 1000, + retryStrategy: () => 1000, + }, }, - }, - ); - pubSubApp.useLogger(pubSubApp.get(WINSTON_MODULE_NEST_PROVIDER)); - pubSubApp.useWebSocketAdapter(new SocketAdapter(pubSubApp)); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - pubSubApp.listen(); + ); + pubSubApp.useLogger(pubSubApp.get(WINSTON_MODULE_NEST_PROVIDER)); + pubSubApp.useWebSocketAdapter(new SocketAdapter(pubSubApp)); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + pubSubApp.listen(); + } logger.log(`Public API active: ${apiConfigService.getIsPublicApiActive()}`); logger.log(`Private API active: ${apiConfigService.getIsPrivateApiActive()}`); From c234b3abd09d823dd830b7d33d21c94e3f6130ae Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Fri, 10 Oct 2025 15:16:20 +0300 Subject: [PATCH 51/90] fix e2e config --- config/config.e2e.mainnet.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.e2e.mainnet.yaml b/config/config.e2e.mainnet.yaml index a75940f75..73cf2b92e 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -5,7 +5,7 @@ api: publicPort: 3001 private: true privatePort: 4001 - websocket: true + websocket: false cron: cacheWarmer: true fastWarm: false From 24d6c8a7b3f41e38c369132bbebe1805f20c8f0c Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 15:48:52 +0300 Subject: [PATCH 52/90] remove websockets subscription --- config/config.devnet.yaml | 2 +- config/config.e2e.mainnet.yaml | 2 +- src/crons/websocket/websocket.cron.service.ts | 32 ----- src/crons/websocket/websocket.crons.module.ts | 19 --- src/endpoints/blocks/block.module.ts | 5 +- src/endpoints/blocks/blocks.gateway.ts | 67 --------- .../blocks/entities/block.subscribe.ts | 50 ------- src/endpoints/network/network.gateway.ts | 35 ----- src/endpoints/network/network.module.ts | 5 +- .../entities/dtos/transaction.subscribe.ts | 130 ------------------ .../transactions/transaction.gateway.ts | 124 ----------------- src/main.ts | 2 - src/public.app.module.ts | 2 - 13 files changed, 6 insertions(+), 469 deletions(-) delete mode 100644 src/crons/websocket/websocket.cron.service.ts delete mode 100644 src/crons/websocket/websocket.crons.module.ts delete mode 100644 src/endpoints/blocks/blocks.gateway.ts delete mode 100644 src/endpoints/blocks/entities/block.subscribe.ts delete mode 100644 src/endpoints/network/network.gateway.ts delete mode 100644 src/endpoints/transactions/entities/dtos/transaction.subscribe.ts delete mode 100644 src/endpoints/transactions/transaction.gateway.ts diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 91b9709d4..8557994d4 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -5,7 +5,7 @@ api: publicPort: 3001 private: true privatePort: 4001 - websocket: false + websocket: true cron: cacheWarmer: true fastWarm: true diff --git a/config/config.e2e.mainnet.yaml b/config/config.e2e.mainnet.yaml index 194ef7edf..5944ee221 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -5,7 +5,7 @@ api: publicPort: 3001 private: true privatePort: 4001 - websocket: false + websocket: true cron: cacheWarmer: true fastWarm: false diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts deleted file mode 100644 index 5ebcc5e7c..000000000 --- a/src/crons/websocket/websocket.cron.service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Cron } from '@nestjs/schedule'; -import { TransactionsGateway } from '../../endpoints/transactions/transaction.gateway'; -import { BlocksGateway } from 'src/endpoints/blocks/blocks.gateway'; -import { NetworkGateway } from 'src/endpoints/network/network.gateway'; -import { Lock } from "@multiversx/sdk-nestjs-common"; -@Injectable() -export class WebsocketCronService { - constructor( - private readonly transactionsGateway: TransactionsGateway, - private readonly blocksGateway: BlocksGateway, - private readonly networkGateway: NetworkGateway, - ) { } - - @Cron('*/6 * * * * *') - @Lock({ name: 'Push transactions to subscribers', verbose: true }) - async handleTransactionsUpdate() { - await this.transactionsGateway.pushTransactions(); - } - - @Cron('*/6 * * * * *') - @Lock({ name: 'Push blocks to subscribers', verbose: true }) - async handleBlocksUpdate() { - await this.blocksGateway.pushBlocks(); - } - - @Cron('*/6 * * * * *') - @Lock({ name: 'Push stats to subscribers', verbose: true }) - async handleStatsUpdate() { - await this.networkGateway.pushStats(); - } -} diff --git a/src/crons/websocket/websocket.crons.module.ts b/src/crons/websocket/websocket.crons.module.ts deleted file mode 100644 index f3c9ddef3..000000000 --- a/src/crons/websocket/websocket.crons.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { TransactionModule } from 'src/endpoints/transactions/transaction.module'; -import { WebsocketCronService } from './websocket.cron.service'; -import { BlockModule } from 'src/endpoints/blocks/block.module'; -import { NetworkModule } from 'src/endpoints/network/network.module'; - -@Module({ - imports: [ - ScheduleModule.forRoot(), - TransactionModule, - BlockModule, - NetworkModule, - ], - providers: [ - WebsocketCronService, - ], -}) -export class WebSocketCronModule { } diff --git a/src/endpoints/blocks/block.module.ts b/src/endpoints/blocks/block.module.ts index df5a5ca87..24d9a8191 100644 --- a/src/endpoints/blocks/block.module.ts +++ b/src/endpoints/blocks/block.module.ts @@ -3,7 +3,6 @@ import { BlsModule } from "../bls/bls.module"; import { IdentitiesModule } from "../identities/identities.module"; import { NodeModule } from "../nodes/node.module"; import { BlockService } from "./block.service"; -import { BlocksGateway } from "./blocks.gateway"; @Module({ imports: [ @@ -12,10 +11,10 @@ import { BlocksGateway } from "./blocks.gateway"; forwardRef(() => IdentitiesModule), ], providers: [ - BlockService, BlocksGateway, + BlockService, ], exports: [ - BlockService, BlocksGateway, + BlockService, ], }) export class BlockModule { } diff --git a/src/endpoints/blocks/blocks.gateway.ts b/src/endpoints/blocks/blocks.gateway.ts deleted file mode 100644 index 518d730a4..000000000 --- a/src/endpoints/blocks/blocks.gateway.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect, MessageBody, ConnectedSocket } from '@nestjs/websockets'; -import { Server, Socket } from 'socket.io'; -import { BlockService } from './block.service'; -import { BlockFilter } from './entities/block.filter'; -import { QueryPagination } from 'src/common/entities/query.pagination'; -import { BlockSubscribePayload } from './entities/block.subscribe'; -import { UseFilters } from '@nestjs/common'; -import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; -import { WsValidationPipe } from 'src/utils/ws-validation.pipe'; -import { OriginLogger } from '@multiversx/sdk-nestjs-common'; - -@UseFilters(WebsocketExceptionsFilter) -@WebSocketGateway({ cors: { origin: '*' } }) -export class BlocksGateway implements OnGatewayDisconnect { - private readonly logger = new OriginLogger(BlocksGateway.name); - - @WebSocketServer() - server!: Server; - - constructor(private readonly blockService: BlockService) { } - - - @SubscribeMessage('subscribeBlocks') - async handleSubscription( - @ConnectedSocket() client: Socket, - @MessageBody(new WsValidationPipe()) payload: BlockSubscribePayload - ) { - const filterHash = JSON.stringify(payload); - await client.join(`block-${filterHash}`); - - return { status: 'success' }; - } - - async pushBlocks() { - for (const [roomName] of this.server.sockets.adapter.rooms) { - try { - if (!roomName.startsWith("block-")) continue; - - const filterHash = roomName.replace("block-", ""); - const filter = JSON.parse(filterHash); - - const blockFilter = new BlockFilter({ - shard: filter.shard, - proposer: filter.proposer, - validator: filter.validator, - epoch: filter.epoch, - nonce: filter.nonce, - hashes: filter.hashes, - order: filter.order, - }); - - const blocks = await this.blockService.getBlocks( - blockFilter, - new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), - filter.withProposerIdentity, - ); - - this.server.to(roomName).emit('blocksUpdate', blocks); - } catch (error) { - this.logger.error(error); - } - } - } - - handleDisconnect(_client: Socket) { } -} - diff --git a/src/endpoints/blocks/entities/block.subscribe.ts b/src/endpoints/blocks/entities/block.subscribe.ts deleted file mode 100644 index dec878829..000000000 --- a/src/endpoints/blocks/entities/block.subscribe.ts +++ /dev/null @@ -1,50 +0,0 @@ -// block-subscribe.dto.ts -import { IsOptional, IsString, IsNumber, IsArray, IsBoolean, Min, Max, IsEnum } from 'class-validator'; -import { SortOrder } from 'src/common/entities/sort.order'; - -export class BlockSubscribePayload { - @IsOptional() - @IsNumber() - @Min(0) - shard?: number; - - @IsOptional() - @IsString() - proposer?: string; - - @IsOptional() - @IsString() - validator?: string; - - @IsOptional() - @IsNumber() - epoch?: number; - - @IsOptional() - @IsNumber() - nonce?: number; - - @IsOptional() - @IsArray() - @IsString({ each: true }) - hashes?: string[]; - - @IsOptional() - @IsEnum(SortOrder) - order?: SortOrder; - - @IsOptional() - @IsNumber() - @Min(0) - from?: number; - - @IsOptional() - @IsNumber() - @Min(1) - @Max(100) - size?: number; - - @IsOptional() - @IsBoolean() - withProposerIdentity?: boolean; -} diff --git a/src/endpoints/network/network.gateway.ts b/src/endpoints/network/network.gateway.ts deleted file mode 100644 index 9db26d666..000000000 --- a/src/endpoints/network/network.gateway.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect } from '@nestjs/websockets'; -import { Server, Socket } from 'socket.io'; -import { NetworkService } from './network.service'; -import { UseFilters } from '@nestjs/common'; -import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; -import { OriginLogger } from '@multiversx/sdk-nestjs-common'; - -@UseFilters(WebsocketExceptionsFilter) -@WebSocketGateway({ cors: { origin: '*' } }) -export class NetworkGateway implements OnGatewayDisconnect { - private readonly logger = new OriginLogger(NetworkGateway.name); - - @WebSocketServer() - server!: Server; - - constructor(private readonly networkService: NetworkService) { } - - @SubscribeMessage('subscribeStats') - async handleSubscription(client: Socket) { - await client.join('statsRoom'); - } - - async pushStats() { - if (this.server.sockets.adapter.rooms.has('statsRoom')) { - try { - const stats = await this.networkService.getStats(); - this.server.to('statsRoom').emit('statsUpdate', stats); - } catch (error) { - this.logger.error(error); - } - } - } - - handleDisconnect(_client: Socket) { } -} diff --git a/src/endpoints/network/network.module.ts b/src/endpoints/network/network.module.ts index 246488f47..42671fdd0 100644 --- a/src/endpoints/network/network.module.ts +++ b/src/endpoints/network/network.module.ts @@ -8,7 +8,6 @@ import { TokenModule } from "../tokens/token.module"; import { TransactionModule } from "../transactions/transaction.module"; import { VmQueryModule } from "../vm.query/vm.query.module"; import { NetworkService } from "./network.service"; -import { NetworkGateway } from "./network.gateway"; @Module({ imports: [ @@ -22,10 +21,10 @@ import { NetworkGateway } from "./network.gateway"; forwardRef(() => SmartContractResultModule), ], providers: [ - NetworkService, NetworkGateway, + NetworkService, ], exports: [ - NetworkService, NetworkGateway, + NetworkService, ], }) export class NetworkModule { } diff --git a/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts b/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts deleted file mode 100644 index 2c1e0b45c..000000000 --- a/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { IsOptional, IsString, IsArray, IsBoolean, IsNumber, IsEnum } from 'class-validator'; -import { Type } from 'class-transformer'; -import { TransactionStatus } from '../transaction.status'; -import { QueryConditionOptions } from '@multiversx/sdk-nestjs-elastic'; -import { SortOrder } from 'src/common/entities/sort.order'; - -export class TransactionSubscribePayload { - @IsOptional() - @IsString() - sender?: string; - - @IsOptional() - @IsArray() - @IsString({ each: true }) - receiver?: string[]; - - @IsOptional() - @IsString() - token?: string; - - @IsOptional() - @IsArray() - @IsString({ each: true }) - functions?: string[]; - - @IsOptional() - @IsNumber() - @Type(() => Number) - senderShard?: number; - - @IsOptional() - @IsNumber() - @Type(() => Number) - receiverShard?: number; - - @IsOptional() - @IsString() - miniBlockHash?: string; - - @IsOptional() - @IsArray() - @IsString({ each: true }) - hashes?: string[]; - - @IsOptional() - @IsEnum(TransactionStatus) - status?: TransactionStatus; - - @IsOptional() - @IsNumber() - @Type(() => Number) - before?: number; - - @IsOptional() - @IsNumber() - @Type(() => Number) - after?: number; - - @IsOptional() - @IsEnum(QueryConditionOptions) - condition?: QueryConditionOptions; - - @IsOptional() - @IsEnum(SortOrder) - order?: SortOrder; - - @IsOptional() - @IsString() - relayer?: string; - - @IsOptional() - @IsBoolean() - isRelayed?: boolean; - - @IsOptional() - @IsBoolean() - isScCall?: boolean; - - @IsOptional() - @IsNumber() - @Type(() => Number) - round?: number; - - @IsOptional() - @IsBoolean() - withScResults?: boolean; - - @IsOptional() - @IsBoolean() - withRelayedScresults?: boolean; - - @IsOptional() - @IsBoolean() - withOperations?: boolean; - - @IsOptional() - @IsBoolean() - withLogs?: boolean; - - @IsOptional() - @IsBoolean() - withScamInfo?: boolean; - - @IsOptional() - @IsBoolean() - withUsername?: boolean; - - @IsOptional() - @IsBoolean() - withBlockInfo?: boolean; - - @IsOptional() - @IsBoolean() - withActionTransferValue?: boolean; - - @IsOptional() - @IsNumber() - @Type(() => Number) - from?: number; - - @IsOptional() - @IsNumber() - @Type(() => Number) - size?: number; - - @IsOptional() - @IsArray() - @IsString({ each: true }) - fields?: string[]; -} diff --git a/src/endpoints/transactions/transaction.gateway.ts b/src/endpoints/transactions/transaction.gateway.ts deleted file mode 100644 index 280e35d90..000000000 --- a/src/endpoints/transactions/transaction.gateway.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect, ConnectedSocket, MessageBody } from '@nestjs/websockets'; -import { Server, Socket } from 'socket.io'; -import { TransactionService } from './transaction.service'; -import { TransactionFilter } from './entities/transaction.filter'; -import { QueryPagination } from 'src/common/entities/query.pagination'; -import { TransactionQueryOptions } from './entities/transactions.query.options'; -import { WsValidationPipe } from 'src/utils/ws-validation.pipe'; -import { TransactionSubscribePayload } from './entities/dtos/transaction.subscribe'; -import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; -import { UseFilters } from '@nestjs/common'; -import { OriginLogger } from '@multiversx/sdk-nestjs-common'; - -@UseFilters(WebsocketExceptionsFilter) -@WebSocketGateway({ cors: { origin: '*' } }) -export class TransactionsGateway implements OnGatewayDisconnect { - private readonly logger = new OriginLogger(TransactionsGateway.name); - - @WebSocketServer() - server!: Server; - - constructor(private readonly transactionService: TransactionService) { } - - @SubscribeMessage('subscribeTransactions') - async handleSubscription( - @ConnectedSocket() client: Socket, - @MessageBody(new WsValidationPipe()) payload: TransactionSubscribePayload) { - // If one of these methods throw an exception then the subscription will not be successful - TransactionQueryOptions.applyDefaultOptions(payload.size || 25, { - withScResults: payload.withScResults, - withOperations: payload.withOperations, - withLogs: payload.withLogs, - withScamInfo: payload.withScamInfo, - withUsername: payload.withUsername, - withBlockInfo: payload.withBlockInfo, - withActionTransferValue: payload.withActionTransferValue, - }); - - const transactionFilter = new TransactionFilter({ - sender: payload.sender, - receivers: payload.receiver, - token: payload.token, - functions: payload.functions, - senderShard: payload.senderShard, - receiverShard: payload.receiverShard, - miniBlockHash: payload.miniBlockHash, - hashes: payload.hashes, - status: payload.status, - before: payload.before, - after: payload.after, - condition: payload.condition, - order: payload.order, - relayer: payload.relayer, - isRelayed: payload.isRelayed, - isScCall: payload.isScCall, - round: payload.round, - withRelayedScresults: payload.withRelayedScresults, - }); - - TransactionFilter.validate(transactionFilter, payload.size || 25); - - const filterHash = JSON.stringify(payload); - await client.join(`tx-${filterHash}`); - - return { status: 'success' }; - } - - async pushTransactions() { - for (const [roomName] of this.server.sockets.adapter.rooms) { - try { - if (!roomName.startsWith("tx-")) continue; - - const filterHash = roomName.replace("tx-", ""); - const filter = JSON.parse(filterHash); - - const options = TransactionQueryOptions.applyDefaultOptions(filter.size || 25, { - withScResults: filter.withScResults, - withOperations: filter.withOperations, - withLogs: filter.withLogs, - withScamInfo: filter.withScamInfo, - withUsername: filter.withUsername, - withBlockInfo: filter.withBlockInfo, - withActionTransferValue: filter.withActionTransferValue, - }); - - const transactionFilter = new TransactionFilter({ - sender: filter.sender, - receivers: filter.receiver, - token: filter.token, - functions: filter.functions, - senderShard: filter.senderShard, - receiverShard: filter.receiverShard, - miniBlockHash: filter.miniBlockHash, - hashes: filter.hashes, - status: filter.status, - before: filter.before, - after: filter.after, - condition: filter.condition, - order: filter.order, - relayer: filter.relayer, - isRelayed: filter.isRelayed, - isScCall: filter.isScCall, - round: filter.round, - withRelayedScresults: filter.withRelayedScresults, - }); - - TransactionFilter.validate(transactionFilter, filter.size || 25); - - const txs = await this.transactionService.getTransactions( - transactionFilter, - new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), - options, - undefined, - filter.fields || [], - ); - - this.server.to(roomName).emit('transactionUpdate', txs); - } catch (error) { - this.logger.error(error); - } - } - } - - handleDisconnect(_client: Socket) { } -} diff --git a/src/main.ts b/src/main.ts index a150c2e75..08fdfb712 100644 --- a/src/main.ts +++ b/src/main.ts @@ -36,7 +36,6 @@ import { NotWritableError } from './common/indexer/entities/not.writable.error'; import * as bodyParser from 'body-parser'; import * as requestIp from 'request-ip'; import compression from 'compression'; -import { IoAdapter } from '@nestjs/platform-socket.io'; import { StateChangesModule } from './state-changes/state.changes.module'; async function bootstrap() { @@ -54,7 +53,6 @@ async function bootstrap() { if (apiConfigService.getIsPublicApiActive()) { const publicApp = await NestFactory.create(PublicAppModule); - publicApp.useWebSocketAdapter(new IoAdapter(publicApp)); await configurePublicApp(publicApp, apiConfigService); diff --git a/src/public.app.module.ts b/src/public.app.module.ts index b28d53b7c..426717e6d 100644 --- a/src/public.app.module.ts +++ b/src/public.app.module.ts @@ -9,7 +9,6 @@ import { GuestCacheService } from '@multiversx/sdk-nestjs-cache'; import { LoggingModule } from '@multiversx/sdk-nestjs-common'; import { DynamicModuleUtils } from './utils/dynamic.module.utils'; import { LocalCacheController } from './endpoints/caching/local.cache.controller'; -import { WebSocketCronModule } from './crons/websocket/websocket.crons.module'; @Module({ imports: [ @@ -17,7 +16,6 @@ import { WebSocketCronModule } from './crons/websocket/websocket.crons.module'; EndpointsServicesModule, EndpointsControllersModule.forRoot(), DynamicModuleUtils.getRedisCacheModule(), - WebSocketCronModule, ], controllers: [ LocalCacheController, From b0984db7bf456772778758dedfb5dfb74df5f7a3 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 15:49:31 +0300 Subject: [PATCH 53/90] remove tx websocket --- src/endpoints/transactions/transaction.module.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/endpoints/transactions/transaction.module.ts b/src/endpoints/transactions/transaction.module.ts index b2eaa7f44..f5fbf6cfd 100644 --- a/src/endpoints/transactions/transaction.module.ts +++ b/src/endpoints/transactions/transaction.module.ts @@ -11,7 +11,6 @@ import { TransactionActionModule } from "./transaction-action/transaction.action import { TransactionGetService } from "./transaction.get.service"; import { TransactionPriceService } from "./transaction.price.service"; import { TransactionService } from "./transaction.service"; -import { TransactionsGateway } from "./transaction.gateway"; @Module({ imports: [ @@ -26,10 +25,10 @@ import { TransactionsGateway } from "./transaction.gateway"; DataApiModule, ], providers: [ - TransactionGetService, TransactionPriceService, TransactionService, TransactionsGateway, + TransactionGetService, TransactionPriceService, TransactionService, ], exports: [ - TransactionGetService, TransactionPriceService, TransactionService, TransactionsGateway, + TransactionGetService, TransactionPriceService, TransactionService, ], }) export class TransactionModule { } From 0345f88f2be1d2363745afe5c8cd839f12e03cd1 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 18:34:23 +0300 Subject: [PATCH 54/90] remove ws filters & parsers --- src/utils/ws-exceptions.filter.ts | 20 -------------------- src/utils/ws-validation.pipe.ts | 16 ---------------- 2 files changed, 36 deletions(-) delete mode 100644 src/utils/ws-exceptions.filter.ts delete mode 100644 src/utils/ws-validation.pipe.ts diff --git a/src/utils/ws-exceptions.filter.ts b/src/utils/ws-exceptions.filter.ts deleted file mode 100644 index bb864231b..000000000 --- a/src/utils/ws-exceptions.filter.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ArgumentsHost, Catch } from "@nestjs/common"; -import { BaseWsExceptionFilter, WsException } from "@nestjs/websockets"; -import { Socket } from "socket.io"; - -@Catch(WsException) -export class WebsocketExceptionsFilter extends BaseWsExceptionFilter { - catch(exception: WsException, host: ArgumentsHost) { - const client = host.switchToWs().getClient() as Socket; - - const pattern = host.switchToWs().getPattern(); - const data = host.switchToWs().getData(); - const error = exception.getError(); - - client.emit('error', { - pattern, - data, - error, - }); - } -} diff --git a/src/utils/ws-validation.pipe.ts b/src/utils/ws-validation.pipe.ts deleted file mode 100644 index 8a5a58010..000000000 --- a/src/utils/ws-validation.pipe.ts +++ /dev/null @@ -1,16 +0,0 @@ -// ws-validation.pipe.ts -import { Injectable, ValidationPipe, ValidationPipeOptions } from '@nestjs/common'; -import { WsException } from '@nestjs/websockets'; - -@Injectable() -export class WsValidationPipe extends ValidationPipe { - constructor(options?: ValidationPipeOptions) { - super({ - transform: true, - whitelist: true, - forbidNonWhitelisted: true, - exceptionFactory: (errors) => new WsException(errors), - ...options, - }); - } -} From 0b627f7513277f81793e900d7ac4b7411001441a Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 10 Oct 2025 18:40:07 +0300 Subject: [PATCH 55/90] remove not used entities --- .../accounts-v2/entities/account.contract.ts | 20 ------- .../accounts-v2/entities/account.deferred.ts | 14 ----- .../entities/account.esdt.history.ts | 15 ----- .../entities/account.history.filter.ts | 10 ---- .../accounts-v2/entities/account.history.ts | 20 ------- .../entities/account.key.filter.ts | 10 ---- .../accounts-v2/entities/account.key.ts | 33 ----------- .../entities/account.query.options.ts | 58 ------------------- .../entities/account.verification.source.ts | 13 ----- .../entities/account.verification.status.ts | 4 -- .../entities/account.verification.ts | 21 ------- .../entities/application.most.used.ts | 8 --- .../accounts-v2/entities/contract.upgrades.ts | 17 ------ .../accounts-v2/entities/deployed.contract.ts | 20 ------- 14 files changed, 263 deletions(-) delete mode 100644 src/endpoints/accounts-v2/entities/account.contract.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.deferred.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.esdt.history.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.history.filter.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.history.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.key.filter.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.key.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.query.options.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.verification.source.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.verification.status.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.verification.ts delete mode 100644 src/endpoints/accounts-v2/entities/application.most.used.ts delete mode 100644 src/endpoints/accounts-v2/entities/contract.upgrades.ts delete mode 100644 src/endpoints/accounts-v2/entities/deployed.contract.ts diff --git a/src/endpoints/accounts-v2/entities/account.contract.ts b/src/endpoints/accounts-v2/entities/account.contract.ts deleted file mode 100644 index e75cf782a..000000000 --- a/src/endpoints/accounts-v2/entities/account.contract.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { AccountAssets } from "src/common/assets/entities/account.assets"; - -export class AccountContract { - constructor(init?: Partial) { - Object.assign(this, init); - } - - @ApiProperty({ type: String }) - address: string = ""; - - @ApiProperty({ type: String }) - deployTxHash: string = ""; - - @ApiProperty({ type: Number }) - timestamp: number = 0; - - @ApiProperty({ type: AccountAssets, nullable: true, description: 'Contract assets' }) - assets: AccountAssets | undefined = undefined; -} diff --git a/src/endpoints/accounts-v2/entities/account.deferred.ts b/src/endpoints/accounts-v2/entities/account.deferred.ts deleted file mode 100644 index ce99c31e1..000000000 --- a/src/endpoints/accounts-v2/entities/account.deferred.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SwaggerUtils } from '@multiversx/sdk-nestjs-common'; -import { ApiProperty } from '@nestjs/swagger'; - -export class AccountDeferred { - constructor(init?: Partial) { - Object.assign(this, init); - } - - @ApiProperty(SwaggerUtils.amountPropertyOptions({ description: 'Deferred payment amount' })) - deferredPayment: string = ''; - - @ApiProperty({ description: 'Seconds left until unbonding time' }) - secondsLeft: number = 0; -} diff --git a/src/endpoints/accounts-v2/entities/account.esdt.history.ts b/src/endpoints/accounts-v2/entities/account.esdt.history.ts deleted file mode 100644 index ce48af609..000000000 --- a/src/endpoints/accounts-v2/entities/account.esdt.history.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { AccountHistory } from "./account.history"; - -export class AccountEsdtHistory extends AccountHistory { - constructor(init?: Partial) { - super(); - Object.assign(this, init); - } - - @ApiProperty({ type: String, example: 'WEGLD-bd4d79' }) - token: string = ''; - - @ApiProperty({ type: String, example: 'XPACHIEVE-5a0519-01' }) - identifier: string | undefined = undefined; -} diff --git a/src/endpoints/accounts-v2/entities/account.history.filter.ts b/src/endpoints/accounts-v2/entities/account.history.filter.ts deleted file mode 100644 index bed36d66a..000000000 --- a/src/endpoints/accounts-v2/entities/account.history.filter.ts +++ /dev/null @@ -1,10 +0,0 @@ - -export class AccountHistoryFilter { - constructor(init?: Partial) { - Object.assign(this, init); - } - before?: number; - after?: number; - identifiers?: string[]; - token?: string; -} diff --git a/src/endpoints/accounts-v2/entities/account.history.ts b/src/endpoints/accounts-v2/entities/account.history.ts deleted file mode 100644 index 4fe64333b..000000000 --- a/src/endpoints/accounts-v2/entities/account.history.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SwaggerUtils } from "@multiversx/sdk-nestjs-common"; -import { ApiProperty } from "@nestjs/swagger"; - -export class AccountHistory { - constructor(init?: Partial) { - Object.assign(this, init); - } - - @ApiProperty({ type: String, example: 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz' }) - address: string = ''; - - @ApiProperty(SwaggerUtils.amountPropertyOptions()) - balance: string = ''; - - @ApiProperty({ type: Number, example: 10000 }) - timestamp: number = 0; - - @ApiProperty({ type: Boolean, nullable: true, example: true, required: false }) - isSender?: boolean | undefined = undefined; -} diff --git a/src/endpoints/accounts-v2/entities/account.key.filter.ts b/src/endpoints/accounts-v2/entities/account.key.filter.ts deleted file mode 100644 index 67f73fade..000000000 --- a/src/endpoints/accounts-v2/entities/account.key.filter.ts +++ /dev/null @@ -1,10 +0,0 @@ - -import { NodeStatusRaw } from "src/endpoints/nodes/entities/node.status"; - -export class AccountKeyFilter { - constructor(init?: Partial) { - Object.assign(this, init); - } - - status: NodeStatusRaw[] = []; -} diff --git a/src/endpoints/accounts-v2/entities/account.key.ts b/src/endpoints/accounts-v2/entities/account.key.ts deleted file mode 100644 index 51a59f4fc..000000000 --- a/src/endpoints/accounts-v2/entities/account.key.ts +++ /dev/null @@ -1,33 +0,0 @@ - -import { SwaggerUtils } from "@multiversx/sdk-nestjs-common"; -import { ApiProperty } from "@nestjs/swagger"; - -export class AccountKey { - constructor(init?: Partial) { - Object.assign(this, init); - } - - @ApiProperty({ type: String, example: '2ef384d4d38bf3aad5cef34ce6eab047fba6d52b9735dbfdf7591289ed9c26ac7e816c9bb56ebf4f09129f045860f401275a91009befb4dc8ddc24ea4bc597290bd916b9f984c2a415ec9b2cfbc4a09de42c032314e6a21e69daf76302fcaa99' }) - blsKey: string = ''; - - @ApiProperty(SwaggerUtils.amountPropertyOptions()) - stake: string = ''; - - @ApiProperty(SwaggerUtils.amountPropertyOptions()) - topUp: string = ''; - - @ApiProperty({ type: String, example: 'online' }) - status: string = ''; - - @ApiProperty({ type: String, example: 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz' }) - rewardAddress: string = ''; - - @ApiProperty({ type: String, nullable: true, example: '2' }) - queueIndex: string | undefined = undefined; - - @ApiProperty({ type: String, nullable: true, example: '100' }) - queueSize: string | undefined = undefined; - - @ApiProperty({ type: Number, example: 10 }) - remainingUnBondPeriod: number | undefined = 0; -} diff --git a/src/endpoints/accounts-v2/entities/account.query.options.ts b/src/endpoints/accounts-v2/entities/account.query.options.ts deleted file mode 100644 index 91649d3c1..000000000 --- a/src/endpoints/accounts-v2/entities/account.query.options.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { SortOrder } from "src/common/entities/sort.order"; -import { AccountSort } from "./account.sort"; -import { BadRequestException } from "@nestjs/common"; - -export class AccountQueryOptions { - constructor(init?: Partial) { - Object.assign(this, init); - } - - addresses?: string[]; - ownerAddress?: string; - - sort?: AccountSort; - order?: SortOrder; - isSmartContract?: boolean; - withOwnerAssets?: boolean; - withDeployInfo?: boolean; - withTxCount?: boolean; - withScrCount?: boolean; - name?: string; - tags?: string[]; - excludeTags?: string[]; - hasAssets?: boolean; - search?: string; - - validate(size: number) { - if (this.withDeployInfo && size > 25) { - throw new BadRequestException('Size must be less than or equal to 25 when withDeployInfo is set'); - } - - if (this.withTxCount && size > 25) { - throw new BadRequestException('Size must be less than or equal to 25 when withTxCount is set'); - } - - 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 { - return this.ownerAddress !== undefined || - this.sort !== undefined || - this.order !== undefined || - this.isSmartContract !== undefined || - this.withOwnerAssets !== undefined || - this.withDeployInfo !== undefined || - this.name !== undefined || - this.tags !== undefined || - this.excludeTags !== undefined || - this.hasAssets !== undefined || - this.search !== undefined || - this.addresses !== undefined; - } -} diff --git a/src/endpoints/accounts-v2/entities/account.verification.source.ts b/src/endpoints/accounts-v2/entities/account.verification.source.ts deleted file mode 100644 index 040cfc251..000000000 --- a/src/endpoints/accounts-v2/entities/account.verification.source.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class AccountVerificationSource { - constructor(init?: Partial) { - Object.assign(this, init); - } - - @ApiProperty({ description: 'Abi file source' }) - abi: any = ''; - - @ApiProperty({ description: 'Contract source code' }) - contract: any = ''; -} diff --git a/src/endpoints/accounts-v2/entities/account.verification.status.ts b/src/endpoints/accounts-v2/entities/account.verification.status.ts deleted file mode 100644 index 996c4447d..000000000 --- a/src/endpoints/accounts-v2/entities/account.verification.status.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum AccountVerificationStatus { - success = 'success', - byteCodeChangedSinceLastVerification = 'byteCodeChangedSinceLastVerification' -} diff --git a/src/endpoints/accounts-v2/entities/account.verification.ts b/src/endpoints/accounts-v2/entities/account.verification.ts deleted file mode 100644 index 29e72590d..000000000 --- a/src/endpoints/accounts-v2/entities/account.verification.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { AccountVerificationSource } from './account.verification.source'; -import { AccountVerificationStatus } from './account.verification.status'; - -export class AccountVerification { - constructor(init?: Partial) { - Object.assign(this, init); - } - - @ApiProperty({ description: 'Source code hash' }) - codeHash?: string = ''; - - @ApiProperty({ description: 'Source code of contract', type: AccountVerificationSource, required: false }) - source?: any; - - @ApiProperty({ description: 'Verifier process status', enum: AccountVerificationStatus }) - status!: AccountVerificationStatus; - - @ApiProperty({ description: 'File hash for IPFS', required: false }) - ipfsFileHash?: string; -} diff --git a/src/endpoints/accounts-v2/entities/application.most.used.ts b/src/endpoints/accounts-v2/entities/application.most.used.ts deleted file mode 100644 index cf507a4b4..000000000 --- a/src/endpoints/accounts-v2/entities/application.most.used.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class ApplicationMostUsed { - constructor(init?: Partial) { - Object.assign(this, init); - } - - address: string = ''; - transfers24H: number = 0; -} diff --git a/src/endpoints/accounts-v2/entities/contract.upgrades.ts b/src/endpoints/accounts-v2/entities/contract.upgrades.ts deleted file mode 100644 index 2c7c7c601..000000000 --- a/src/endpoints/accounts-v2/entities/contract.upgrades.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -export class ContractUpgrades { - constructor(init?: Partial) { - Object.assign(this, init); - } - @ApiProperty({ type: String, nullable: true, example: 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz' }) - address: string = ''; - - @ApiProperty({ type: String, nullable: true, example: '1c8c6b2148f25621fa2c798a2c9a184df61fdd1991aa0af7ea01eb7b89025d2a' }) - txHash: string = ''; - - @ApiProperty({ type: String, nullable: true, example: '1c8c6b2148f25621fa2c798a2c9a184df61fdd1991aa0af7ea01eb7b89025d2a' }) - codeHash: string = ''; - - @ApiProperty({ type: Number, nullable: true, example: '1638577452' }) - timestamp: number = 0; -} diff --git a/src/endpoints/accounts-v2/entities/deployed.contract.ts b/src/endpoints/accounts-v2/entities/deployed.contract.ts deleted file mode 100644 index b2dce1663..000000000 --- a/src/endpoints/accounts-v2/entities/deployed.contract.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { AccountAssets } from "src/common/assets/entities/account.assets"; - -export class DeployedContract { - constructor(init?: Partial) { - Object.assign(this, init); - } - - @ApiProperty({ type: String }) - address: string = ""; - - @ApiProperty({ type: String }) - deployTxHash: string = ""; - - @ApiProperty({ type: Number }) - timestamp: number = 0; - - @ApiProperty({ type: AccountAssets, nullable: true, required: false, description: 'Contract assets' }) - assets: AccountAssets | undefined = undefined; -} From ddb419af8562948d4df86c8717625500f89c18e5 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Mon, 13 Oct 2025 11:20:59 +0300 Subject: [PATCH 56/90] rename queue --- src/state-changes/state.changes.consumer.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index f182df571..6566277ec 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -41,8 +41,8 @@ export class StateChangesConsumerService { @CompetingRabbitConsumer({ exchange: 'state_accesses', - queueName: 'state_changes_test-stefan', - deadLetterExchange: 'state_changes_test_dlx-stefan', + queueName: 'api_state_accesses_queue', + deadLetterExchange: 'api_state_accesses_queue_dlx', }) async consumeEvents(blockWithStateChanges: BlockWithStateChangesRaw) { try { From 7611306569909cf01e1239c7200302f8edaa3c08 Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Mon, 13 Oct 2025 13:00:46 +0300 Subject: [PATCH 57/90] fix db tls config fetch --- src/common/api-config/api.config.service.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/common/api-config/api.config.service.ts b/src/common/api-config/api.config.service.ts index 07985f82c..8f9195793 100644 --- a/src/common/api-config/api.config.service.ts +++ b/src/common/api-config/api.config.service.ts @@ -482,12 +482,7 @@ export class ApiConfigService { } isDatabaseTlsEnabled(): boolean { - const isDatabaseTlsEnabled = this.configService.get('database.tls'); - if (!isDatabaseTlsEnabled) { - throw new Error('No database.tls present'); - } - - return isDatabaseTlsEnabled; + return this.configService.get('database.tls') ?? false; } getDatabaseConnection(): any { From c4043a792ddea14d4b9d432d5138799848332b61 Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Fri, 17 Oct 2025 13:57:03 +0300 Subject: [PATCH 58/90] fix package lock after merge --- package-lock.json | 404 +--------------------------------------------- 1 file changed, 7 insertions(+), 397 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3e2410e6..dc8a498a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "@sendgrid/mail": "^8.1.5", "agentkeepalive": "^4.2.1", "amqp-connection-manager": "^4.1.3", - "amqplib": "^0.10.9", + "amqplib": "^0.10.0", "anchorme": "^3.0.8", "apollo-server-core": "^3.13.0", "apollo-server-express": "3.13.0", @@ -2209,16 +2209,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", @@ -2552,28 +2542,6 @@ "@img/sharp-libvips-darwin-arm64": "1.2.0" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", @@ -2590,364 +2558,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@ioredis/commands": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.1.tgz", @@ -6428,12 +6038,6 @@ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "license": "MIT" }, - "node_modules/@types/validator": { - "version": "13.15.2", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", - "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", - "license": "MIT" - }, "node_modules/@types/validator": { "version": "13.15.3", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", @@ -17472,6 +17076,12 @@ } } }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, "node_modules/xmlhttprequest-ssl": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", From 22cfb9872d9fe7eb1eeb711145846906d3257126 Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Fri, 17 Oct 2025 14:04:20 +0300 Subject: [PATCH 59/90] fix dependency --- package-lock.json | 488 +++++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- 2 files changed, 438 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index dc8a498a0..090303c9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,7 +71,7 @@ "request-ip": "^3.3.0", "rimraf": "^5.0.0", "rxjs": "^7.1.0", - "sharp": "^0.34.2", + "sharp": "^0.34.4", "simple-git": "^3.16.0", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", @@ -2209,6 +2209,16 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", @@ -2520,10 +2530,19 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", "cpu": [ "arm64" ], @@ -2539,13 +2558,35 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" + "@img/sharp-libvips-darwin-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.3" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", "cpu": [ "arm64" ], @@ -2558,6 +2599,364 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.5.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@ioredis/commands": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.1.tgz", @@ -8150,19 +8549,6 @@ "dev": true, "license": "MIT" }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -8781,9 +9167,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -14924,14 +15310,14 @@ } }, "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "engines": { @@ -14941,28 +15327,28 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" } }, "node_modules/shebang-command": { diff --git a/package.json b/package.json index c283aa582..495ed8a80 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "request-ip": "^3.3.0", "rimraf": "^5.0.0", "rxjs": "^7.1.0", - "sharp": "^0.34.2", + "sharp": "^0.34.4", "simple-git": "^3.16.0", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", From a053f2dd6a35a3a75c0ce17e60c8ecee52408dfc Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 28 Oct 2025 11:26:32 +0200 Subject: [PATCH 60/90] ignore metadata for user wallet addresses --- src/state-changes/state.changes.consumer.service.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 6566277ec..3e115ceac 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -146,7 +146,7 @@ export class StateChangesConsumerService { ...newAccountStateFiltered, shard: shardID, timestamp: blockTimestampMs, - ...this.parseCodeMetadata(newAccountState.codeMetadata), + ...this.parseCodeMetadata(newAccountState.address, codeMetadata), tokens: tokens.map(token => new TokenWithBalance({ identifier: token.identifier, nonce: parseInt(token.nonce), @@ -176,7 +176,12 @@ export class StateChangesConsumerService { this.clientProxy.emit('deleteCacheKeys', cacheKeys); } - private parseCodeMetadata(hexStr?: string) { + private parseCodeMetadata(address: string, hexStr?: string) { + // code metadata for user address refers to guardian data, should handle in the future if needed + if (!AddressUtils.isSmartContractAddress(address)) { + return {}; + } + const UPGRADEABLE = 0x01_00; // 256 const READABLE = 0x04_00; // 1024 const PAYABLE = 0x00_02; // 2 From 73172943633e56024612614bb941b392779887f9 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 28 Oct 2025 15:30:24 +0200 Subject: [PATCH 61/90] add support for timestamp/timestampMs --- src/common/indexer/db/schemas/account.details.schema.ts | 3 +++ src/common/indexer/entities/account.ts | 1 + src/endpoints/accounts-v2/account.service.v2.ts | 5 ++++- src/endpoints/accounts-v2/entities/account.ts | 5 ++++- src/endpoints/accounts/account.service.ts | 3 +++ src/endpoints/accounts/entities/account.ts | 6 +++++- src/state-changes/state.changes.consumer.service.ts | 3 ++- 7 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/common/indexer/db/schemas/account.details.schema.ts b/src/common/indexer/db/schemas/account.details.schema.ts index 352b74fb7..ac9f98e8e 100644 --- a/src/common/indexer/db/schemas/account.details.schema.ts +++ b/src/common/indexer/db/schemas/account.details.schema.ts @@ -22,6 +22,9 @@ export class AccountDetails { @Prop({ required: true, type: Number }) nonce: number = 0; + @Prop({ required: false, type: Number }) + timestampMs: number = 0; + @Prop({ required: true, type: Number }) timestamp: number = 0; diff --git a/src/common/indexer/entities/account.ts b/src/common/indexer/entities/account.ts index 94963eb91..136bda829 100644 --- a/src/common/indexer/entities/account.ts +++ b/src/common/indexer/entities/account.ts @@ -2,6 +2,7 @@ export interface Account { address: string; nonce: number; timestamp: number; + timestampMs: number; balance: string; balanceNum: number; totalBalanceWithStake: string; diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts index e364a647a..d977a5528 100644 --- a/src/endpoints/accounts-v2/account.service.v2.ts +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -99,9 +99,12 @@ export class AccountServiceV2 { } if (options?.withTimestamp) { - if (!account.timestamp) { + if (!account.timestamp || !account.timestampMs) { const elasticSearchAccount = await this.indexerService.getAccount(address); account.timestamp = elasticSearchAccount.timestamp; + if (elasticSearchAccount.timestampMs) { + account.timestampMs = elasticSearchAccount.timestampMs; + } } } diff --git a/src/endpoints/accounts-v2/entities/account.ts b/src/endpoints/accounts-v2/entities/account.ts index 6403d3cce..47ee0f63d 100644 --- a/src/endpoints/accounts-v2/entities/account.ts +++ b/src/endpoints/accounts-v2/entities/account.ts @@ -16,7 +16,10 @@ export class Account { @ApiProperty({ type: Number, description: 'Account current nonce', example: 42 }) nonce: number = 0; - @ApiProperty({ type: Number, description: 'Timestamp of the block where the account was first indexed', example: 1676979360 }) + @ApiProperty({ type: Number, description: 'Timestamp in miliseconds of the block where the account was first indexed', example: 1676979360000 }) + timestampMs: number = 0; + + @ApiProperty({ type: Number, description: 'Timestamp in seconds of the block where the account was first indexed', example: 1676979360 }) timestamp: number = 0; @ApiProperty({ type: Number, description: 'The shard ID allocated to the account', example: 0 }) diff --git a/src/endpoints/accounts/account.service.ts b/src/endpoints/accounts/account.service.ts index cde2c8564..104a1b5be 100644 --- a/src/endpoints/accounts/account.service.ts +++ b/src/endpoints/accounts/account.service.ts @@ -99,6 +99,9 @@ export class AccountService { if (options?.withTimestamp) { const elasticSearchAccount = await this.indexerService.getAccount(address); account.timestamp = elasticSearchAccount.timestamp; + if (elasticSearchAccount.timestampMs) { + account.timestampMs = elasticSearchAccount.timestampMs; + } } if (AddressUtils.isSmartContractAddress(address)) { diff --git a/src/endpoints/accounts/entities/account.ts b/src/endpoints/accounts/entities/account.ts index 6403d3cce..5ea3c9abd 100644 --- a/src/endpoints/accounts/entities/account.ts +++ b/src/endpoints/accounts/entities/account.ts @@ -16,7 +16,11 @@ export class Account { @ApiProperty({ type: Number, description: 'Account current nonce', example: 42 }) nonce: number = 0; - @ApiProperty({ type: Number, description: 'Timestamp of the block where the account was first indexed', example: 1676979360 }) + + @ApiProperty({ type: Number, description: 'Timestamp in milliseconds of the block where the account was first indexed', example: 1676979360000 }) + timestampMs: number = 0; + + @ApiProperty({ type: Number, description: 'Timestamp in seconds of the block where the account was first indexed', example: 1676979360 }) timestamp: number = 0; @ApiProperty({ type: Number, description: 'The shard ID allocated to the account', example: 0 }) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 3e115ceac..b980fb2cf 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -145,7 +145,8 @@ export class StateChangesConsumerService { new AccountDetails({ ...newAccountStateFiltered, shard: shardID, - timestamp: blockTimestampMs, + timestampMs: blockTimestampMs, + timestamp: Math.floor(blockTimestampMs / 1000), ...this.parseCodeMetadata(newAccountState.address, codeMetadata), tokens: tokens.map(token => new TokenWithBalance({ identifier: token.identifier, From 1388717de20d2038b00f3d2eb9c05a557b58447c Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 28 Oct 2025 15:46:47 +0200 Subject: [PATCH 62/90] fix unit tests --- src/endpoints/accounts/entities/account.ts | 1 - src/test/unit/services/accounts.spec.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/accounts/entities/account.ts b/src/endpoints/accounts/entities/account.ts index 5ea3c9abd..14df44e8a 100644 --- a/src/endpoints/accounts/entities/account.ts +++ b/src/endpoints/accounts/entities/account.ts @@ -16,7 +16,6 @@ export class Account { @ApiProperty({ type: Number, description: 'Account current nonce', example: 42 }) nonce: number = 0; - @ApiProperty({ type: Number, description: 'Timestamp in milliseconds of the block where the account was first indexed', example: 1676979360000 }) timestampMs: number = 0; diff --git a/src/test/unit/services/accounts.spec.ts b/src/test/unit/services/accounts.spec.ts index 7a7933537..96ae23554 100644 --- a/src/test/unit/services/accounts.spec.ts +++ b/src/test/unit/services/accounts.spec.ts @@ -283,6 +283,7 @@ describe('Account Service', () => { address: 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz', balance: '162486906126924046', nonce: 45, + timestampMs: 0, timestamp: 0, shard: 0, ownerAddress: '', From 8dbb737faf40919a8b08b3f7d44e7c68bbcb6581 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 28 Oct 2025 16:02:19 +0200 Subject: [PATCH 63/90] fix --- src/endpoints/accounts-v2/account.service.v2.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts index d977a5528..996063cff 100644 --- a/src/endpoints/accounts-v2/account.service.v2.ts +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -101,8 +101,10 @@ export class AccountServiceV2 { if (options?.withTimestamp) { if (!account.timestamp || !account.timestampMs) { const elasticSearchAccount = await this.indexerService.getAccount(address); - account.timestamp = elasticSearchAccount.timestamp; - if (elasticSearchAccount.timestampMs) { + if (!account.timestamp) { + account.timestamp = elasticSearchAccount.timestamp; + } + if (!account.timestampMs && elasticSearchAccount.timestampMs) { account.timestampMs = elasticSearchAccount.timestampMs; } } From 4066bed798c103d80cec10fe06ba512d583bf24e Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 29 Oct 2025 17:52:10 +0200 Subject: [PATCH 64/90] fix username --- src/state-changes/entities/state.changes.entity.ts | 2 +- src/state-changes/utils/state-changes.decoder.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts index 25601b004..c13fdc0af 100644 --- a/src/state-changes/entities/state.changes.entity.ts +++ b/src/state-changes/entities/state.changes.entity.ts @@ -56,7 +56,7 @@ export class AccountState { codeHash?: string; rootHash!: string; ownerAddress?: string; - userName?: string; + username?: string; codeMetadata?: string; constructor(init?: Partial) { diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts index 0b594c1e3..ebfba1a88 100644 --- a/src/state-changes/utils/state-changes.decoder.ts +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -88,7 +88,7 @@ export class StateChangesDecoder { ownerAddress, codeHash: this.bytesToBase64(msg.CodeHash), rootHash: this.bytesToBase64(msg.RootHash), - userName: this.bytesToHex(msg.UserName), + username: Buffer.from(this.bytesToHex(msg.UserName), 'hex').toString(), codeMetadata: this.bytesToHex(msg.CodeMetadata), }; From d0f44294713783e2c8d7b0382d56938afd0cb89f Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 31 Oct 2025 15:09:57 +0200 Subject: [PATCH 65/90] parse guardian data --- .../state.changes.consumer.service.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index b980fb2cf..4eb62b2e0 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -178,18 +178,23 @@ export class StateChangesConsumerService { } private parseCodeMetadata(address: string, hexStr?: string) { + if (!hexStr || hexStr === '') { + return {}; + } // code metadata for user address refers to guardian data, should handle in the future if needed if (!AddressUtils.isSmartContractAddress(address)) { - return {}; + const GUARDED = 0x08_00; + const value = parseInt(hexStr, 16); + return { + codeMetadata: hexStr, // TODO: debugging purpose, remove for production + isGuarded: (value & GUARDED) !== 0, + }; } const UPGRADEABLE = 0x01_00; // 256 const READABLE = 0x04_00; // 1024 const PAYABLE = 0x00_02; // 2 const PAYABLE_BY_SC = 0x00_04; // 4 - if (!hexStr || hexStr === '') { - return {}; - } const value = parseInt(hexStr, 16); return { @@ -197,6 +202,7 @@ export class StateChangesConsumerService { isReadable: (value & READABLE) !== 0, isPayable: (value & PAYABLE) !== 0, isPayableBySmartContract: (value & PAYABLE_BY_SC) !== 0, + codeMetadata: hexStr, // TODO: debugging purpose, remove for production }; } From c4987efb4063ec47eeacf53bfdafa6842fdd9032 Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Fri, 31 Oct 2025 15:34:18 +0200 Subject: [PATCH 66/90] debug print workflow --- .github/workflows/chain-simulator-e2e-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/chain-simulator-e2e-tests.yml b/.github/workflows/chain-simulator-e2e-tests.yml index b93ea059f..df1a7ffd5 100644 --- a/.github/workflows/chain-simulator-e2e-tests.yml +++ b/.github/workflows/chain-simulator-e2e-tests.yml @@ -47,6 +47,7 @@ jobs: - run: npm ci - run: npm run init + - run: cat src/common/indexer/entities/account.ts - name: Start API run: | From c8ee8c00aa31983aa4e25b2c600a8170d736eb0e Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Fri, 31 Oct 2025 15:38:35 +0200 Subject: [PATCH 67/90] force workflow branch --- .github/workflows/chain-simulator-e2e-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/chain-simulator-e2e-tests.yml b/.github/workflows/chain-simulator-e2e-tests.yml index df1a7ffd5..6b64ef320 100644 --- a/.github/workflows/chain-simulator-e2e-tests.yml +++ b/.github/workflows/chain-simulator-e2e-tests.yml @@ -15,6 +15,9 @@ jobs: steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ github.ref }} - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 From 87fc1de098c7b1e4ffe0f49db54ef1d1d022c418 Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Fri, 31 Oct 2025 15:55:11 +0200 Subject: [PATCH 68/90] remove duplicated field --- src/common/indexer/entities/account.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/common/indexer/entities/account.ts b/src/common/indexer/entities/account.ts index d29dcb20d..fc5ff3296 100644 --- a/src/common/indexer/entities/account.ts +++ b/src/common/indexer/entities/account.ts @@ -3,7 +3,6 @@ export interface Account { nonce: number; timestampMs: number; timestamp: number; - timestampMs: number; balance: string; balanceNum: number; totalBalanceWithStake: string; From f2768833739a0413f17b847312435eac3d21a00d Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Mon, 3 Nov 2025 12:28:17 +0200 Subject: [PATCH 69/90] add logs --- src/state-changes/state.changes.consumer.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 4eb62b2e0..24f2257f7 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -50,7 +50,8 @@ export class StateChangesConsumerService { const startDecoding = start; const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); - + console.log(blockWithStateChanges); + console.log(finalStates); const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs); const endDecoding = Date.now(); From 84f704960419408f51c20923251293b46db9d4e7 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 4 Nov 2025 10:26:35 +0200 Subject: [PATCH 70/90] fixes --- .../accounts-v2/account.service.v2.ts | 3 + .../state.changes.consumer.service.ts | 67 +++++++++---------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts index 996063cff..c1b89ec49 100644 --- a/src/endpoints/accounts-v2/account.service.v2.ts +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -78,6 +78,9 @@ export class AccountServiceV2 { async () => await this.getAccountWithFallBack(address, options), CacheInfo.AccountState(address).ttl, ); + if (account?.username) { + account.username = await this.usernameService.getUsernameForAddress(address) ?? undefined; + } } else { account = await this.getAccountRaw(address, options?.withAssets); } diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 24f2257f7..a4e8aaef4 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -2,10 +2,8 @@ import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers" import { BlockWithStateChangesRaw, ESDTType, StateChanges } from "./entities"; import { AccountDetails, AccountDetailsRepository } from "src/common/indexer/db"; import { Inject, Injectable } from "@nestjs/common"; -import { TokenWithBalance } from "src/endpoints/tokens/entities/token.with.balance"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; import { CacheInfo } from "src/utils/cache.info"; -import { NftAccount } from "src/endpoints/nfts/entities/nft.account"; import { TokenType } from "src/common/indexer/entities"; import { NftType } from "src/endpoints/nfts/entities/nft.type"; import { NftSubType } from "src/endpoints/nfts/entities/nft.sub.type"; @@ -41,7 +39,7 @@ export class StateChangesConsumerService { @CompetingRabbitConsumer({ exchange: 'state_accesses', - queueName: 'api_state_accesses_queue', + queueName: 'api_state_accesses_queue-test', deadLetterExchange: 'api_state_accesses_queue_dlx', }) async consumeEvents(blockWithStateChanges: BlockWithStateChangesRaw) { @@ -50,8 +48,7 @@ export class StateChangesConsumerService { const startDecoding = start; const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); - console.log(blockWithStateChanges); - console.log(finalStates); + const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs); const endDecoding = Date.now(); @@ -126,19 +123,19 @@ export class StateChangesConsumerService { } const newAccountState = state.accountState; - const tokens = [ - ...state.esdtState.Fungible, - ]; + // const tokens = [ + // ...state.esdtState.Fungible, + // ]; - const nfts = [ - ...state.esdtState.NonFungible, - ...state.esdtState.NonFungibleV2, - ...state.esdtState.DynamicNFT, - ...state.esdtState.SemiFungible, - ...state.esdtState.DynamicSFT, - ...state.esdtState.MetaFungible, - ...state.esdtState.DynamicMeta, - ]; + // const nfts = [ + // ...state.esdtState.NonFungible, + // ...state.esdtState.NonFungibleV2, + // ...state.esdtState.DynamicNFT, + // ...state.esdtState.SemiFungible, + // ...state.esdtState.DynamicSFT, + // ...state.esdtState.MetaFungible, + // ...state.esdtState.DynamicMeta, + // ]; if (newAccountState) { const { codeMetadata, ...newAccountStateFiltered } = newAccountState; @@ -149,21 +146,21 @@ export class StateChangesConsumerService { timestampMs: blockTimestampMs, timestamp: Math.floor(blockTimestampMs / 1000), ...this.parseCodeMetadata(newAccountState.address, codeMetadata), - tokens: tokens.map(token => new TokenWithBalance({ - identifier: token.identifier, - nonce: parseInt(token.nonce), - balance: token.value, - type: this.parseEsdtType(token.type) as TokenType, - subType: NftSubType.None, - })), - nfts: nfts.map(nft => new NftAccount({ - identifier: nft.identifier, - nonce: parseInt(nft.nonce), - type: this.parseEsdtType(nft.type) as NftType, - subType: this.parseEsdtSubtype(nft.type), - collection: nft.identifier.replace(/-[^-]*$/, ''), // delete everything after last `-` character inclusive - balance: nft.value, - })), + // tokens: tokens.map(token => new TokenWithBalance({ + // identifier: token.identifier, + // nonce: parseInt(token.nonce), + // balance: token.value, + // type: this.parseEsdtType(token.type) as TokenType, + // subType: NftSubType.None, + // })), + // nfts: nfts.map(nft => new NftAccount({ + // identifier: nft.identifier, + // nonce: parseInt(nft.nonce), + // type: this.parseEsdtType(nft.type) as NftType, + // subType: this.parseEsdtSubtype(nft.type), + // collection: nft.identifier.replace(/-[^-]*$/, ''), // delete everything after last `-` character inclusive + // balance: nft.value, + // })), }); transformed.push(parsedAccount); } @@ -187,7 +184,7 @@ export class StateChangesConsumerService { const GUARDED = 0x08_00; const value = parseInt(hexStr, 16); return { - codeMetadata: hexStr, // TODO: debugging purpose, remove for production + // codeMetadata: hexStr, // TODO: debugging purpose, remove for production isGuarded: (value & GUARDED) !== 0, }; } @@ -203,10 +200,11 @@ export class StateChangesConsumerService { isReadable: (value & READABLE) !== 0, isPayable: (value & PAYABLE) !== 0, isPayableBySmartContract: (value & PAYABLE_BY_SC) !== 0, - codeMetadata: hexStr, // TODO: debugging purpose, remove for production + // codeMetadata: hexStr, // TODO: debugging purpose, remove for production }; } + //@ts-ignore private parseEsdtType(type: ESDTType): TokenType | NftType { switch (type) { case ESDTType.Fungible: @@ -226,6 +224,7 @@ export class StateChangesConsumerService { } } + //@ts-ignore private parseEsdtSubtype(type: ESDTType): NftSubType { switch (type) { case ESDTType.Fungible: From 748c5acb23fba991ee5f2d79617b3ce98b627bc2 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 4 Nov 2025 10:39:24 +0200 Subject: [PATCH 71/90] add performance profiler --- .../state.changes.consumer.service.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index a4e8aaef4..5ee6344a4 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -10,6 +10,7 @@ import { NftSubType } from "src/endpoints/nfts/entities/nft.sub.type"; import { ClientProxy } from "@nestjs/microservices"; import { StateChangesDecoder } from "./utils/state-changes.decoder"; import { AddressUtils, OriginLogger } from "@multiversx/sdk-nestjs-common"; +import { PerformanceProfiler } from "@multiversx/sdk-nestjs-monitoring"; @Injectable() export class StateChangesConsumerService { @@ -44,15 +45,14 @@ export class StateChangesConsumerService { }) async consumeEvents(blockWithStateChanges: BlockWithStateChangesRaw) { try { - const start = Date.now(); - - const startDecoding = start; + const profiler = new PerformanceProfiler('BlockStateChangesProcessing'); + const decodingProfiler = new PerformanceProfiler('StateChangesDecoding'); const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs); + decodingProfiler.stop('StateChangesDecoding'); + this.logger.log(`Decoded state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID} in ${decodingProfiler.duration} ms`); - const endDecoding = Date.now(); - const decodingDuration = endDecoding - startDecoding; await this.updateAccounts(transformedFinalStates); await this.cacheService.setRemote( @@ -61,12 +61,8 @@ export class StateChangesConsumerService { CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(blockWithStateChanges.shardID).ttl, ); - const end = Date.now(); - const duration = end - start; - if (duration > 10) { - this.logger.log(`decoding duration: ${decodingDuration}ms`); - this.logger.log(`processing time shard ${blockWithStateChanges.shardID}: ${duration}ms`); - } + profiler.stop('BlockStateChangesProcessing'); + this.logger.log(`Processed state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID} in ${profiler.duration} ms`); } catch (error) { this.logger.error(`Error consuming state changes from shard ${blockWithStateChanges.shardID}:`, error); throw error; From 4ddc2d86bc984497129f93e20fd7d12178ed4885 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 4 Nov 2025 10:41:43 +0200 Subject: [PATCH 72/90] skip metashard --- src/state-changes/state.changes.consumer.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 5ee6344a4..14cda5d11 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -45,6 +45,10 @@ export class StateChangesConsumerService { }) async consumeEvents(blockWithStateChanges: BlockWithStateChangesRaw) { try { + if (blockWithStateChanges.shardID === 4294967295) { + return; // skip meta shard + } + const profiler = new PerformanceProfiler('BlockStateChangesProcessing'); const decodingProfiler = new PerformanceProfiler('StateChangesDecoding'); const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); From 901954c0d722d99bbfd7afcdb167f3a46c920fe4 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 4 Nov 2025 14:31:12 +0200 Subject: [PATCH 73/90] fixes after review --- config/config.devnet.yaml | 7 +- src/common/api-config/api.config.service.ts | 34 +- .../accounts-v2/account.controller.v2.ts | 1 + .../entities/state.changes.entity.ts | 226 ++-- .../state.changes.consumer.service.ts | 488 ++++----- src/state-changes/state.changes.module.ts | 58 +- src/state-changes/utils/esdt.d.ts | 378 +++---- src/state-changes/utils/esdt.js | 982 +++++++++--------- .../utils/state-changes.decoder.ts | 716 ++++++------- src/state-changes/utils/token.parser.ts | 108 +- src/state-changes/utils/trie_leaf_data.d.ts | 194 ++-- src/state-changes/utils/trie_leaf_data.js | 520 +++++----- src/state-changes/utils/user_account.pb.d.ts | 254 ++--- src/state-changes/utils/user_account.pb.js | 902 ++++++++-------- 14 files changed, 2445 insertions(+), 2423 deletions(-) diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 64355a2cb..eac143e39 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -26,8 +26,10 @@ features: stateChanges: enabled: false port: 5675 - url: 'amqp://guest:guest@127.0.0.1:5672' + rabbitUrl: 'amqp://guest:guest@127.0.0.1:5672' exchange: 'state_accesses' + queueName: 'api_state_accesses_queue-test' + deadLetterExchange: 'api_state_accesses_queue_dlx' eventsNotifier: enabled: false port: 5674 @@ -194,4 +196,5 @@ compression: threshold: 1024 chunkSize: 16384 pubSubListener: - enabled: true \ No newline at end of file + enabled: true + \ No newline at end of file diff --git a/src/common/api-config/api.config.service.ts b/src/common/api-config/api.config.service.ts index 54946e8c9..571eb831d 100644 --- a/src/common/api-config/api.config.service.ts +++ b/src/common/api-config/api.config.service.ts @@ -978,12 +978,7 @@ export class ApiConfigService { } isStateChangesFeatureActive(): boolean { - const isStateChangesActive = this.configService.get('features.stateChanges.enabled'); - if (isStateChangesActive === undefined) { - return false; - } - - return isStateChangesActive; + return this.configService.get('features.stateChanges.enabled') ?? false; } getStateChangesFeaturePort(): number { @@ -995,10 +990,13 @@ export class ApiConfigService { return stateChangesPort; } - getStateChangesUrl(): string { - const url = this.configService.get('features.stateChanges.url'); + getStateChangesRabbitUrl(): string { + let url = this.configService.get('features.stateChanges.rabbitUrl'); if (!url) { - throw new Error('No state changes url present'); + url = this.configService.get('features.stateChanges.url'); + if (!url) { + throw new Error('No state changes rabbit url present'); + } } return url; @@ -1013,6 +1011,24 @@ export class ApiConfigService { return exchange; } + getStateChangesQueueName(): string { + const queueName = this.configService.get('features.stateChanges.queueName'); + if (!queueName) { + throw new Error('No state changes queue name present'); + } + + return queueName; + } + + getStateChangesDeadLetterExchange(): string { + const deadLetterExchange = this.configService.get('features.stateChanges.deadLetterExchange'); + if (!deadLetterExchange) { + throw new Error('No state changes dead letter exchange present'); + } + + return deadLetterExchange; + } + isPubSubListenerEnabled(): boolean { const isPubSubListenerEnabled = this.configService.get('pubSubListener.enabled'); if (isPubSubListenerEnabled == null) { diff --git a/src/endpoints/accounts-v2/account.controller.v2.ts b/src/endpoints/accounts-v2/account.controller.v2.ts index ff4b32f56..79a61b277 100644 --- a/src/endpoints/accounts-v2/account.controller.v2.ts +++ b/src/endpoints/accounts-v2/account.controller.v2.ts @@ -6,6 +6,7 @@ import { ParseAddressPipe, ParseBoolPipe, ParseIntPipe } from '@multiversx/sdk-n import { DeepHistoryInterceptor } from 'src/interceptors/deep-history.interceptor'; import { AccountFetchOptions } from './entities/account.fetch.options'; import { NoCache } from '@multiversx/sdk-nestjs-cache'; + @Controller('') @ApiTags('accounts') export class AccountControllerV2 { diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts index c13fdc0af..f7580fbab 100644 --- a/src/state-changes/entities/state.changes.entity.ts +++ b/src/state-changes/entities/state.changes.entity.ts @@ -1,145 +1,145 @@ export class AccountChanges { - nonceChanged!: boolean; - balanceChanged!: boolean; - codeHashChanged!: boolean; - rootHashChanged!: boolean; - developerRewardChanged!: boolean; - ownerAddressChanged!: boolean; - userNameChanged!: boolean; - codeMetadataChanged!: boolean; - - constructor(init?: Partial) { - Object.assign(this, init); - } + nonceChanged!: boolean; + balanceChanged!: boolean; + codeHashChanged!: boolean; + rootHashChanged!: boolean; + developerRewardChanged!: boolean; + ownerAddressChanged!: boolean; + userNameChanged!: boolean; + codeMetadataChanged!: boolean; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export class StateAccessPerAccountRaw { - type!: number; - index!: number; - txHash!: string; - mainTrieKey!: string; - mainTrieVal!: string; - operation!: number; - dataTrieChanges?: DataTrieChange[]; - accountChanges?: number; - - constructor(init?: Partial) { - Object.assign(this, init); - } + type!: number; + index!: number; + txHash!: string; + mainTrieKey!: string; + mainTrieVal!: string; + operation!: number; + dataTrieChanges?: DataTrieChange[]; + accountChanges?: number; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export class DataTrieChange { - type!: number; - key!: string; - val!: any; - version!: number; - operation!: DataTrieChangeOperation; + type!: number; + key!: string; + val!: any; + version!: number; + operation!: DataTrieChangeOperation; } export class BlockWithStateChangesRaw { - hash!: string; - shardID!: number; - nonce!: number; - timestampMs!: number; - stateAccessesPerAccounts!: Record; - - constructor(init?: Partial) { - Object.assign(this, init); - } + hash!: string; + shardID!: number; + nonce!: number; + timestampMs!: number; + stateAccessesPerAccounts!: Record; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export class AccountState { - nonce!: number; - balance!: string; - developerReward!: string; - address!: string; - codeHash?: string; - rootHash!: string; - ownerAddress?: string; - username?: string; - codeMetadata?: string; - - constructor(init?: Partial) { - Object.assign(this, init); - } + nonce!: number; + balance!: string; + developerReward!: string; + address!: string; + codeHash?: string; + rootHash!: string; + ownerAddress?: string; + username?: string; + codeMetadata?: string; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export class EsdtState { - identifier!: string; - nonce!: string; - type!: ESDTType; - value!: string; - propertiesHex!: string; - reservedHex!: string; - tokenMetaData!: any; - - constructor(init?: Partial) { - Object.assign(this, init); - } + identifier!: string; + nonce!: string; + type!: ESDTType; + value!: string; + propertiesHex!: string; + reservedHex!: string; + tokenMetaData!: any; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export enum ESDTType { - // 0 - Fungible, - // 1 - NonFungible, - // 2 - NonFungibleV2, - // 3 - SemiFungible, - // 4 - MetaFungible, - // 5 - DynamicNFT, - // 6 - DynamicSFT, - // 7 - DynamicMeta, + // 0 + Fungible, + // 1 + NonFungible, + // 2 + NonFungibleV2, + // 3 + SemiFungible, + // 4 + MetaFungible, + // 5 + DynamicNFT, + // 6 + DynamicSFT, + // 7 + DynamicMeta, } export class StateChanges { - accountState!: AccountState | undefined; - esdtState!: { - 'Fungible': EsdtState[], - 'NonFungible': EsdtState[], - 'NonFungibleV2': EsdtState[], - 'SemiFungible': EsdtState[], - 'MetaFungible': EsdtState[], - 'DynamicNFT': EsdtState[], - 'DynamicSFT': EsdtState[], - 'DynamicMeta': EsdtState[], - }; - accountChanges!: AccountChanges; - isNewAccount!: boolean; - - constructor(init?: Partial) { - Object.assign(this, init); - } + accountState!: AccountState | undefined; + esdtState!: { + 'Fungible': EsdtState[], + 'NonFungible': EsdtState[], + 'NonFungibleV2': EsdtState[], + 'SemiFungible': EsdtState[], + 'MetaFungible': EsdtState[], + 'DynamicNFT': EsdtState[], + 'DynamicSFT': EsdtState[], + 'DynamicMeta': EsdtState[], + }; + accountChanges!: AccountChanges; + isNewAccount!: boolean; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export enum AccountChangesRaw { - NoChange = 0, - NonceChanged = 1 << 0, // 1 - BalanceChanged = 1 << 1, // 2 - CodeHashChanged = 1 << 2, // 4 - RootHashChanged = 1 << 3, // 8 - DeveloperRewardChanged = 1 << 4, // 16 - OwnerAddressChanged = 1 << 5, // 32 - UserNameChanged = 1 << 6, // 64 - CodeMetadataChanged = 1 << 7 // 128 + NoChange = 0, + NonceChanged = 1 << 0, // 1 + BalanceChanged = 1 << 1, // 2 + CodeHashChanged = 1 << 2, // 4 + RootHashChanged = 1 << 3, // 8 + DeveloperRewardChanged = 1 << 4, // 16 + OwnerAddressChanged = 1 << 5, // 32 + UserNameChanged = 1 << 6, // 64 + CodeMetadataChanged = 1 << 7 // 128 } export enum StateAccessOperation { - NotSet = 0, - GetCode = 1 << 0, - SaveAccount = 1 << 1, - GetAccount = 1 << 2, - WriteCode = 1 << 3, - RemoveDataTrie = 1 << 4, - GetDataTrieValue = 1 << 5, + NotSet = 0, + GetCode = 1 << 0, + SaveAccount = 1 << 1, + GetAccount = 1 << 2, + WriteCode = 1 << 3, + RemoveDataTrie = 1 << 4, + GetDataTrieValue = 1 << 5, } export enum DataTrieChangeOperation { - NotDelete = 0, - Delete = 1, + NotDelete = 0, + Delete = 1, } diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 14cda5d11..0f79bb5e1 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -11,274 +11,274 @@ import { ClientProxy } from "@nestjs/microservices"; import { StateChangesDecoder } from "./utils/state-changes.decoder"; import { AddressUtils, OriginLogger } from "@multiversx/sdk-nestjs-common"; import { PerformanceProfiler } from "@multiversx/sdk-nestjs-monitoring"; +import configuration from "config/configuration"; +import { ApiConfigService } from "src/common/api-config/api.config.service"; @Injectable() export class StateChangesConsumerService { - private readonly logger = new OriginLogger(StateChangesConsumerService.name); - static SYSTEM_ACCOUNTS = [ - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqllls0lczs7", // stakingScAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l", // validatorScAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", // esdtScAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", // governanceScAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqrlllllllllllllllllllllllllsn60f0k", // jailingAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqlllllllllllllllllllllllllllsr9gav8", // endOfEpochAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", // delegationManagerScAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0llllsqkarq6", // firstDelegationScAddress - "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu", // contractDeployScAdress - "erd17rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rcqqkhty3", // genesisMintingAddress - "erd1lllllllllllllllllllllllllllllllllllllllllllllllllllsckry7t", // systemAccountAddress - "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqq2m3f0f", // esdtGlobalSettingsAddresses[0] - "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqsl6e366", // esdtGlobalSettingsAddresses[1] - "erd1lllllllllllllllllllllllllllllllllllllllllllllllllupq9x7ny0", // esdtGlobalSettingsAddresses[2] - ]; - - constructor( - private readonly cacheService: CacheService, - private readonly accountDetailsRepository: AccountDetailsRepository, - @Inject('PUBSUB_SERVICE') private clientProxy: ClientProxy, - ) { } - - @CompetingRabbitConsumer({ - exchange: 'state_accesses', - queueName: 'api_state_accesses_queue-test', - deadLetterExchange: 'api_state_accesses_queue_dlx', - }) - async consumeEvents(blockWithStateChanges: BlockWithStateChangesRaw) { - try { - if (blockWithStateChanges.shardID === 4294967295) { - return; // skip meta shard - } - - const profiler = new PerformanceProfiler('BlockStateChangesProcessing'); - const decodingProfiler = new PerformanceProfiler('StateChangesDecoding'); - const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); - - const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs); - decodingProfiler.stop('StateChangesDecoding'); - this.logger.log(`Decoded state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID} in ${decodingProfiler.duration} ms`); - - await this.updateAccounts(transformedFinalStates); - - await this.cacheService.setRemote( - CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(blockWithStateChanges.shardID).key, - blockWithStateChanges.timestampMs, - CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(blockWithStateChanges.shardID).ttl, - ); - - profiler.stop('BlockStateChangesProcessing'); - this.logger.log(`Processed state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID} in ${profiler.duration} ms`); - } catch (error) { - this.logger.error(`Error consuming state changes from shard ${blockWithStateChanges.shardID}:`, error); - throw error; - } + private readonly logger = new OriginLogger(StateChangesConsumerService.name); + static SYSTEM_ACCOUNTS = [ + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqllls0lczs7", // stakingScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l", // validatorScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", // esdtScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", // governanceScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqrlllllllllllllllllllllllllsn60f0k", // jailingAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqlllllllllllllllllllllllllllsr9gav8", // endOfEpochAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", // delegationManagerScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0llllsqkarq6", // firstDelegationScAddress + "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu", // contractDeployScAdress + "erd17rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rcqqkhty3", // genesisMintingAddress + "erd1lllllllllllllllllllllllllllllllllllllllllllllllllllsckry7t", // systemAccountAddress + "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqq2m3f0f", // esdtGlobalSettingsAddresses[0] + "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqsl6e366", // esdtGlobalSettingsAddresses[1] + "erd1lllllllllllllllllllllllllllllllllllllllllllllllllupq9x7ny0", // esdtGlobalSettingsAddresses[2] + ]; + + constructor( + private readonly cacheService: CacheService, + private readonly accountDetailsRepository: AccountDetailsRepository, + private readonly apiConfigService: ApiConfigService, + @Inject('PUBSUB_SERVICE') private clientProxy: ClientProxy, + ) { } + + @CompetingRabbitConsumer({ + exchange: configuration().features.stateChanges.exchange ?? 'state_accesses', + queueName: configuration().features.stateChanges.queueName ?? 'api_state_accesses_queue', + deadLetterExchange: configuration().features.stateChanges.deadLetterExchange ?? 'api_state_accesses_queue_dlx', + }) + async consumeEvents(blockWithStateChanges: BlockWithStateChangesRaw) { + try { + if (blockWithStateChanges.shardID === this.apiConfigService.getMetaChainShardId()) { + return; // skip meta shard + } + + const profiler = new PerformanceProfiler('BlockStateChangesProcessing'); + const decodingProfiler = new PerformanceProfiler('StateChangesDecoding'); + const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); + + const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs); + decodingProfiler.stop('StateChangesDecoding'); + this.logger.log(`Decoded state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID} in ${decodingProfiler.duration} ms`); + + await this.updateAccounts(transformedFinalStates); + + await this.cacheService.setRemote( + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(blockWithStateChanges.shardID).key, + blockWithStateChanges.timestampMs, + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(blockWithStateChanges.shardID).ttl, + ); + + profiler.stop('BlockStateChangesProcessing'); + this.logger.log(`Processed state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID} in ${profiler.duration} ms`); + } catch (error) { + this.logger.error(`Error consuming state changes from shard ${blockWithStateChanges.shardID}:`, error); + throw error; } - - private async updateAccounts(transformedFinalStates: AccountDetails[]) { - const promisesToWaitFor = [this.accountDetailsRepository.updateAccounts(transformedFinalStates.filter(account => !AddressUtils.isSmartContractAddress(account.address)))]; - - const walletCacheKeys = []; - const contractCacheKeys = []; - const values = []; - for (const account of transformedFinalStates) { - if (!AddressUtils.isSmartContractAddress(account.address)) { - walletCacheKeys.push(CacheInfo.AccountState(account.address).key); - const { tokens, nfts, ...accountWithoutAssets } = account; - values.push(accountWithoutAssets); - } else { - contractCacheKeys.push(CacheInfo.AccountState(account.address).key); - } - } - if (walletCacheKeys.length > 0) { - promisesToWaitFor.push( - this.cacheService.setManyRemote( - walletCacheKeys, - values, - CacheInfo.AccountState('any').ttl, - ) - ); - } - - if (contractCacheKeys.length > 0) { - promisesToWaitFor.push( - this.cacheService.deleteManyRemote( - contractCacheKeys, - ) - ); - } - - this.deleteLocalCache([...walletCacheKeys, ...contractCacheKeys]); - - await Promise.all(promisesToWaitFor); + } + + private async updateAccounts(transformedFinalStates: AccountDetails[]) { + const promisesToWaitFor = [this.accountDetailsRepository.updateAccounts(transformedFinalStates.filter(account => !AddressUtils.isSmartContractAddress(account.address)))]; + + const walletCacheKeys = []; + const contractCacheKeys = []; + const values = []; + for (const account of transformedFinalStates) { + if (!AddressUtils.isSmartContractAddress(account.address)) { + walletCacheKeys.push(CacheInfo.AccountState(account.address).key); + const { tokens, nfts, ...accountWithoutAssets } = account; + values.push(accountWithoutAssets); + } else { + contractCacheKeys.push(CacheInfo.AccountState(account.address).key); + } } - private decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { - return StateChangesDecoder.decodeStateChangesFinal(blockWithStateChanges); + if (walletCacheKeys.length > 0) { + promisesToWaitFor.push( + this.cacheService.setManyRemote( + walletCacheKeys, + values, + CacheInfo.AccountState('any').ttl, + ) + ); } - private transformFinalStatesToDbFormat(finalStates: Record, shardID: number, blockTimestampMs: number) { - const transformed: AccountDetails[] = []; - - for (const [address, state] of Object.entries(finalStates)) { - if (StateChangesConsumerService.isSystemContractAddress(address)) { - continue; - } - const newAccountState = state.accountState; - - // const tokens = [ - // ...state.esdtState.Fungible, - // ]; - - // const nfts = [ - // ...state.esdtState.NonFungible, - // ...state.esdtState.NonFungibleV2, - // ...state.esdtState.DynamicNFT, - // ...state.esdtState.SemiFungible, - // ...state.esdtState.DynamicSFT, - // ...state.esdtState.MetaFungible, - // ...state.esdtState.DynamicMeta, - // ]; - - if (newAccountState) { - const { codeMetadata, ...newAccountStateFiltered } = newAccountState; - const parsedAccount = - new AccountDetails({ - ...newAccountStateFiltered, - shard: shardID, - timestampMs: blockTimestampMs, - timestamp: Math.floor(blockTimestampMs / 1000), - ...this.parseCodeMetadata(newAccountState.address, codeMetadata), - // tokens: tokens.map(token => new TokenWithBalance({ - // identifier: token.identifier, - // nonce: parseInt(token.nonce), - // balance: token.value, - // type: this.parseEsdtType(token.type) as TokenType, - // subType: NftSubType.None, - // })), - // nfts: nfts.map(nft => new NftAccount({ - // identifier: nft.identifier, - // nonce: parseInt(nft.nonce), - // type: this.parseEsdtType(nft.type) as NftType, - // subType: this.parseEsdtSubtype(nft.type), - // collection: nft.identifier.replace(/-[^-]*$/, ''), // delete everything after last `-` character inclusive - // balance: nft.value, - // })), - }); - transformed.push(parsedAccount); - } - - } - - return transformed; + if (contractCacheKeys.length > 0) { + promisesToWaitFor.push( + this.cacheService.deleteManyRemote( + contractCacheKeys, + ) + ); } + this.deleteLocalCache([...walletCacheKeys, ...contractCacheKeys]); + + await Promise.all(promisesToWaitFor); + } + private decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { + return StateChangesDecoder.decodeStateChangesFinal(blockWithStateChanges); + } + + private transformFinalStatesToDbFormat(finalStates: Record, shardID: number, blockTimestampMs: number) { + const transformed: AccountDetails[] = []; + + for (const [address, state] of Object.entries(finalStates)) { + if (StateChangesConsumerService.isSystemContractAddress(address)) { + continue; + } + const newAccountState = state.accountState; + + // const tokens = [ + // ...state.esdtState.Fungible, + // ]; + + // const nfts = [ + // ...state.esdtState.NonFungible, + // ...state.esdtState.NonFungibleV2, + // ...state.esdtState.DynamicNFT, + // ...state.esdtState.SemiFungible, + // ...state.esdtState.DynamicSFT, + // ...state.esdtState.MetaFungible, + // ...state.esdtState.DynamicMeta, + // ]; + + if (newAccountState) { + const { codeMetadata, ...newAccountStateFiltered } = newAccountState; + const parsedAccount = + new AccountDetails({ + ...newAccountStateFiltered, + shard: shardID, + timestampMs: blockTimestampMs, + timestamp: Math.floor(blockTimestampMs / 1000), + ...this.parseCodeMetadata(newAccountState.address, codeMetadata), + // tokens: tokens.map(token => new TokenWithBalance({ + // identifier: token.identifier, + // nonce: parseInt(token.nonce), + // balance: token.value, + // type: this.parseEsdtType(token.type) as TokenType, + // subType: NftSubType.None, + // })), + // nfts: nfts.map(nft => new NftAccount({ + // identifier: nft.identifier, + // nonce: parseInt(nft.nonce), + // type: this.parseEsdtType(nft.type) as NftType, + // subType: this.parseEsdtSubtype(nft.type), + // collection: nft.identifier.replace(/-[^-]*$/, ''), // delete everything after last `-` character inclusive + // balance: nft.value, + // })), + }); + transformed.push(parsedAccount); + } - private deleteLocalCache(cacheKeys: string[]) { - this.clientProxy.emit('deleteCacheKeys', cacheKeys); } - private parseCodeMetadata(address: string, hexStr?: string) { - if (!hexStr || hexStr === '') { - return {}; - } - // code metadata for user address refers to guardian data, should handle in the future if needed - if (!AddressUtils.isSmartContractAddress(address)) { - const GUARDED = 0x08_00; - const value = parseInt(hexStr, 16); - return { - // codeMetadata: hexStr, // TODO: debugging purpose, remove for production - isGuarded: (value & GUARDED) !== 0, - }; - } - - const UPGRADEABLE = 0x01_00; // 256 - const READABLE = 0x04_00; // 1024 - const PAYABLE = 0x00_02; // 2 - const PAYABLE_BY_SC = 0x00_04; // 4 - const value = parseInt(hexStr, 16); - - return { - isUpgradeable: (value & UPGRADEABLE) !== 0, - isReadable: (value & READABLE) !== 0, - isPayable: (value & PAYABLE) !== 0, - isPayableBySmartContract: (value & PAYABLE_BY_SC) !== 0, - // codeMetadata: hexStr, // TODO: debugging purpose, remove for production - }; + return transformed; + } + + + private deleteLocalCache(cacheKeys: string[]) { + this.clientProxy.emit('deleteCacheKeys', cacheKeys); + } + + private parseCodeMetadata(address: string, hexStr?: string) { + if (!hexStr || hexStr === '') { + return {}; } - //@ts-ignore - private parseEsdtType(type: ESDTType): TokenType | NftType { - switch (type) { - case ESDTType.Fungible: - return TokenType.FungibleESDT; - - case ESDTType.NonFungible: - case ESDTType.DynamicNFT: - case ESDTType.NonFungibleV2: - return NftType.NonFungibleESDT; - - case ESDTType.SemiFungible: - case ESDTType.DynamicSFT: - return NftType.SemiFungibleESDT; - case ESDTType.MetaFungible: - case ESDTType.DynamicMeta: - return NftType.MetaESDT; - } + if (!AddressUtils.isSmartContractAddress(address)) { + const GUARDED = 0x08_00; + const value = parseInt(hexStr, 16); + return { + isGuarded: (value & GUARDED) !== 0, + }; } - //@ts-ignore - private parseEsdtSubtype(type: ESDTType): NftSubType { - switch (type) { - case ESDTType.Fungible: - return NftSubType.None; - - case ESDTType.NonFungible: - return NftSubType.NonFungibleESDT; - case ESDTType.DynamicNFT: - return NftSubType.DynamicNonFungibleESDT; - case ESDTType.NonFungibleV2: - return NftSubType.NonFungibleESDTv2; - - case ESDTType.SemiFungible: - return NftSubType.SemiFungibleESDT; - case ESDTType.DynamicSFT: - return NftSubType.DynamicSemiFungibleESDT; - case ESDTType.MetaFungible: - return NftSubType.MetaESDT; - case ESDTType.DynamicMeta: - return NftSubType.DynamicMetaESDT; - } + const UPGRADEABLE = 0x01_00; // 256 + const READABLE = 0x04_00; // 1024 + const PAYABLE = 0x00_02; // 2 + const PAYABLE_BY_SC = 0x00_04; // 4 + const value = parseInt(hexStr, 16); + + return { + isUpgradeable: (value & UPGRADEABLE) !== 0, + isReadable: (value & READABLE) !== 0, + isPayable: (value & PAYABLE) !== 0, + isPayableBySmartContract: (value & PAYABLE_BY_SC) !== 0, + }; + } + + //@ts-ignore + private parseEsdtType(type: ESDTType): TokenType | NftType { + switch (type) { + case ESDTType.Fungible: + return TokenType.FungibleESDT; + + case ESDTType.NonFungible: + case ESDTType.DynamicNFT: + case ESDTType.NonFungibleV2: + return NftType.NonFungibleESDT; + + case ESDTType.SemiFungible: + case ESDTType.DynamicSFT: + return NftType.SemiFungibleESDT; + case ESDTType.MetaFungible: + case ESDTType.DynamicMeta: + return NftType.MetaESDT; } + } + + //@ts-ignore + private parseEsdtSubtype(type: ESDTType): NftSubType { + switch (type) { + case ESDTType.Fungible: + return NftSubType.None; + + case ESDTType.NonFungible: + return NftSubType.NonFungibleESDT; + case ESDTType.DynamicNFT: + return NftSubType.DynamicNonFungibleESDT; + case ESDTType.NonFungibleV2: + return NftSubType.NonFungibleESDTv2; + + case ESDTType.SemiFungible: + return NftSubType.SemiFungibleESDT; + case ESDTType.DynamicSFT: + return NftSubType.DynamicSemiFungibleESDT; + case ESDTType.MetaFungible: + return NftSubType.MetaESDT; + case ESDTType.DynamicMeta: + return NftSubType.DynamicMetaESDT; + } + } - static async isStateChangesConsumerHealthy(cacheService: CacheService, maxLastActivityDiffMs: number): Promise { - const keys = [ - CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(0).key, - CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(1).key, - CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(2).key, - CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(4294967295).key, - ]; + static async isStateChangesConsumerHealthy(cacheService: CacheService, maxLastActivityDiffMs: number): Promise { + const keys = [ + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(0).key, + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(1).key, + CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(2).key, + ]; - let timestampsMs: (number | undefined | null)[] = cacheService.getManyLocal(keys); + let timestampsMs: (number | undefined | null)[] = cacheService.getManyLocal(keys); - // check local - if (timestampsMs.some(t => t == null)) { - timestampsMs = await cacheService.getManyRemote(keys); + // check local + if (timestampsMs.some(t => t == null)) { + timestampsMs = await cacheService.getManyRemote(keys); - // check remote - if (timestampsMs.some(t => t == null)) { - return false; - } + // check remote + if (timestampsMs.some(t => t == null)) { + return false; + } - // set only if it's valid - cacheService.setManyLocal(keys, timestampsMs, 0.6); - } + // set only if it's valid + cacheService.setManyLocal(keys, timestampsMs, 0.6); + } - const minTimestamp = Math.min(...(timestampsMs as number[])); + const minTimestamp = Math.min(...(timestampsMs as number[])); - const diff = Date.now() - minTimestamp; + const diff = Date.now() - minTimestamp; - return diff <= maxLastActivityDiffMs; - } + return diff <= maxLastActivityDiffMs; + } - static isSystemContractAddress(address: string) { - return StateChangesConsumerService.SYSTEM_ACCOUNTS.includes(address); - } + static isSystemContractAddress(address: string) { + return StateChangesConsumerService.SYSTEM_ACCOUNTS.includes(address); + } } diff --git a/src/state-changes/state.changes.module.ts b/src/state-changes/state.changes.module.ts index 9149e3365..8427048b7 100644 --- a/src/state-changes/state.changes.module.ts +++ b/src/state-changes/state.changes.module.ts @@ -7,35 +7,35 @@ import { StateChangesConsumerService } from './state.changes.consumer.service'; import { MongoDbModule } from 'src/common/indexer/db'; @Module({ - imports: [ - ApiConfigModule, - MongoDbModule, - DynamicModuleUtils.getCacheModule(), - ], - providers: [ - StateChangesConsumerService, - DynamicModuleUtils.getPubSubService(), - ], + imports: [ + ApiConfigModule, + MongoDbModule, + DynamicModuleUtils.getCacheModule(), + ], + providers: [ + StateChangesConsumerService, + DynamicModuleUtils.getPubSubService(), + ], }) export class StateChangesModule { - static register(): DynamicModule { - return { - module: StateChangesModule, - imports: [ - RabbitMQModule.forRootAsync(RabbitMQModule, { - imports: [ApiConfigModule], - inject: [ApiConfigService], - useFactory: (apiConfigService: ApiConfigService) => { - return { - name: apiConfigService.getStateChangesExchange(), - type: 'fanout', - options: {}, - uri: apiConfigService.getStateChangesUrl(), - prefetchCount: 1, - }; - }, - }), - ], - }; - } + static register(): DynamicModule { + return { + module: StateChangesModule, + imports: [ + RabbitMQModule.forRootAsync(RabbitMQModule, { + imports: [ApiConfigModule], + inject: [ApiConfigService], + useFactory: (apiConfigService: ApiConfigService) => { + return { + name: apiConfigService.getStateChangesExchange(), + type: 'fanout', + options: {}, + uri: apiConfigService.getStateChangesRabbitUrl(), + prefetchCount: 1, + }; + }, + }), + ], + }; + } } diff --git a/src/state-changes/utils/esdt.d.ts b/src/state-changes/utils/esdt.d.ts index 8ab4f6aa1..410eed3c8 100644 --- a/src/state-changes/utils/esdt.d.ts +++ b/src/state-changes/utils/esdt.d.ts @@ -8,207 +8,207 @@ export interface IMetaData { /** Represents a MetaData. */ export class MetaData implements IMetaData { - /** - * Constructs a new MetaData. - * @param [properties] Properties to set - */ - constructor(properties?: IMetaData); - - /** - * Creates a new MetaData instance using the specified properties. - * @param [properties] Properties to set - * @returns MetaData instance - */ - public static create(properties?: IMetaData): MetaData; - - /** - * Encodes the specified MetaData message. Does not implicitly {@link MetaData.verify|verify} messages. - * @param message MetaData message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encode(message: IMetaData, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Encodes the specified MetaData message, length delimited. Does not implicitly {@link MetaData.verify|verify} messages. - * @param message MetaData message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encodeDelimited(message: IMetaData, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a MetaData message from the specified reader or buffer. - * @param reader Reader or buffer to decode from - * @param [length] Message length if known beforehand - * @returns MetaData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(reader: ($protobuf.Reader | Uint8Array), length?: number): MetaData; - - /** - * Decodes a MetaData message from the specified reader or buffer, length delimited. - * @param reader Reader or buffer to decode from - * @returns MetaData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decodeDelimited(reader: ($protobuf.Reader | Uint8Array)): MetaData; - - /** - * Verifies a MetaData message. - * @param message Plain object to verify - * @returns `null` if valid, otherwise the reason why it is not - */ - public static verify(message: { [k: string]: any }): (string | null); - - /** - * Creates a MetaData message from a plain object. Also converts values to their respective internal types. - * @param object Plain object - * @returns MetaData - */ - public static fromObject(object: { [k: string]: any }): MetaData; - - /** - * Creates a plain object from a MetaData message. Also converts values to other types if specified. - * @param message MetaData - * @param [options] Conversion options - * @returns Plain object - */ - public static toObject(message: MetaData, options?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this MetaData to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; - - /** - * Gets the default type url for MetaData - * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") - * @returns The default type url - */ - public static getTypeUrl(typeUrlPrefix?: string): string; + /** + * Constructs a new MetaData. + * @param [properties] Properties to set + */ + constructor(properties?: IMetaData); + + /** + * Creates a new MetaData instance using the specified properties. + * @param [properties] Properties to set + * @returns MetaData instance + */ + public static create(properties?: IMetaData): MetaData; + + /** + * Encodes the specified MetaData message. Does not implicitly {@link MetaData.verify|verify} messages. + * @param message MetaData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: IMetaData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified MetaData message, length delimited. Does not implicitly {@link MetaData.verify|verify} messages. + * @param message MetaData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: IMetaData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a MetaData message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns MetaData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader | Uint8Array), length?: number): MetaData; + + /** + * Decodes a MetaData message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns MetaData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader | Uint8Array)): MetaData; + + /** + * Verifies a MetaData message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string | null); + + /** + * Creates a MetaData message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns MetaData + */ + public static fromObject(object: { [k: string]: any }): MetaData; + + /** + * Creates a plain object from a MetaData message. Also converts values to other types if specified. + * @param message MetaData + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: MetaData, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this MetaData to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for MetaData + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; } /** Properties of a ESDigitalToken. */ export interface IESDigitalToken { - /** ESDigitalToken Type */ - Type?: (number | null); + /** ESDigitalToken Type */ + Type?: (number | null); - /** ESDigitalToken Value */ - Value?: (Uint8Array | null); + /** ESDigitalToken Value */ + Value?: (Uint8Array | null); - /** ESDigitalToken Properties */ - Properties?: (Uint8Array | null); + /** ESDigitalToken Properties */ + Properties?: (Uint8Array | null); - /** ESDigitalToken TokenMetaData */ - TokenMetaData?: (IMetaData | null); + /** ESDigitalToken TokenMetaData */ + TokenMetaData?: (IMetaData | null); - /** ESDigitalToken Reserved */ - Reserved?: (Uint8Array | null); + /** ESDigitalToken Reserved */ + Reserved?: (Uint8Array | null); } /** Represents a ESDigitalToken. */ export class ESDigitalToken implements IESDigitalToken { - /** - * Constructs a new ESDigitalToken. - * @param [properties] Properties to set - */ - constructor(properties?: IESDigitalToken); - - /** ESDigitalToken Type. */ - public Type: number; - - /** ESDigitalToken Value. */ - public Value: Uint8Array; - - /** ESDigitalToken Properties. */ - public Properties: Uint8Array; - - /** ESDigitalToken TokenMetaData. */ - public TokenMetaData?: (IMetaData | null); - - /** ESDigitalToken Reserved. */ - public Reserved: Uint8Array; - - /** - * Creates a new ESDigitalToken instance using the specified properties. - * @param [properties] Properties to set - * @returns ESDigitalToken instance - */ - public static create(properties?: IESDigitalToken): ESDigitalToken; - - /** - * Encodes the specified ESDigitalToken message. Does not implicitly {@link ESDigitalToken.verify|verify} messages. - * @param message ESDigitalToken message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encode(message: IESDigitalToken, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Encodes the specified ESDigitalToken message, length delimited. Does not implicitly {@link ESDigitalToken.verify|verify} messages. - * @param message ESDigitalToken message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encodeDelimited(message: IESDigitalToken, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a ESDigitalToken message from the specified reader or buffer. - * @param reader Reader or buffer to decode from - * @param [length] Message length if known beforehand - * @returns ESDigitalToken - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(reader: ($protobuf.Reader | Uint8Array), length?: number): ESDigitalToken; - - /** - * Decodes a ESDigitalToken message from the specified reader or buffer, length delimited. - * @param reader Reader or buffer to decode from - * @returns ESDigitalToken - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decodeDelimited(reader: ($protobuf.Reader | Uint8Array)): ESDigitalToken; - - /** - * Verifies a ESDigitalToken message. - * @param message Plain object to verify - * @returns `null` if valid, otherwise the reason why it is not - */ - public static verify(message: { [k: string]: any }): (string | null); - - /** - * Creates a ESDigitalToken message from a plain object. Also converts values to their respective internal types. - * @param object Plain object - * @returns ESDigitalToken - */ - public static fromObject(object: { [k: string]: any }): ESDigitalToken; - - /** - * Creates a plain object from a ESDigitalToken message. Also converts values to other types if specified. - * @param message ESDigitalToken - * @param [options] Conversion options - * @returns Plain object - */ - public static toObject(message: ESDigitalToken, options?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this ESDigitalToken to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; - - /** - * Gets the default type url for ESDigitalToken - * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") - * @returns The default type url - */ - public static getTypeUrl(typeUrlPrefix?: string): string; + /** + * Constructs a new ESDigitalToken. + * @param [properties] Properties to set + */ + constructor(properties?: IESDigitalToken); + + /** ESDigitalToken Type. */ + public Type: number; + + /** ESDigitalToken Value. */ + public Value: Uint8Array; + + /** ESDigitalToken Properties. */ + public Properties: Uint8Array; + + /** ESDigitalToken TokenMetaData. */ + public TokenMetaData?: (IMetaData | null); + + /** ESDigitalToken Reserved. */ + public Reserved: Uint8Array; + + /** + * Creates a new ESDigitalToken instance using the specified properties. + * @param [properties] Properties to set + * @returns ESDigitalToken instance + */ + public static create(properties?: IESDigitalToken): ESDigitalToken; + + /** + * Encodes the specified ESDigitalToken message. Does not implicitly {@link ESDigitalToken.verify|verify} messages. + * @param message ESDigitalToken message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: IESDigitalToken, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified ESDigitalToken message, length delimited. Does not implicitly {@link ESDigitalToken.verify|verify} messages. + * @param message ESDigitalToken message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: IESDigitalToken, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a ESDigitalToken message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns ESDigitalToken + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader | Uint8Array), length?: number): ESDigitalToken; + + /** + * Decodes a ESDigitalToken message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns ESDigitalToken + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader | Uint8Array)): ESDigitalToken; + + /** + * Verifies a ESDigitalToken message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string | null); + + /** + * Creates a ESDigitalToken message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns ESDigitalToken + */ + public static fromObject(object: { [k: string]: any }): ESDigitalToken; + + /** + * Creates a plain object from a ESDigitalToken message. Also converts values to other types if specified. + * @param message ESDigitalToken + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: ESDigitalToken, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this ESDigitalToken to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for ESDigitalToken + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; } diff --git a/src/state-changes/utils/esdt.js b/src/state-changes/utils/esdt.js index ecac3c922..8d6b67ce6 100644 --- a/src/state-changes/utils/esdt.js +++ b/src/state-changes/utils/esdt.js @@ -11,509 +11,509 @@ var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); $root.MetaData = (function () { - /** - * Properties of a MetaData. - * @exports IMetaData - * @interface IMetaData - */ - - /** - * Constructs a new MetaData. - * @exports MetaData - * @classdesc Represents a MetaData. - * @implements IMetaData - * @constructor - * @param {IMetaData=} [properties] Properties to set - */ - function MetaData(properties) { - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; + /** + * Properties of a MetaData. + * @exports IMetaData + * @interface IMetaData + */ + + /** + * Constructs a new MetaData. + * @exports MetaData + * @classdesc Represents a MetaData. + * @implements IMetaData + * @constructor + * @param {IMetaData=} [properties] Properties to set + */ + function MetaData(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * Creates a new MetaData instance using the specified properties. + * @function create + * @memberof MetaData + * @static + * @param {IMetaData=} [properties] Properties to set + * @returns {MetaData} MetaData instance + */ + MetaData.create = function create(properties) { + return new MetaData(properties); + }; + + /** + * Encodes the specified MetaData message. Does not implicitly {@link MetaData.verify|verify} messages. + * @function encode + * @memberof MetaData + * @static + * @param {IMetaData} message MetaData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + MetaData.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + return writer; + }; + + /** + * Encodes the specified MetaData message, length delimited. Does not implicitly {@link MetaData.verify|verify} messages. + * @function encodeDelimited + * @memberof MetaData + * @static + * @param {IMetaData} message MetaData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + MetaData.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a MetaData message from the specified reader or buffer. + * @function decode + * @memberof MetaData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {MetaData} MetaData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + MetaData.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.MetaData(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + default: + reader.skipType(tag & 7); + break; + } } + return message; + }; + + /** + * Decodes a MetaData message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof MetaData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {MetaData} MetaData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + MetaData.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a MetaData message. + * @function verify + * @memberof MetaData + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + MetaData.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + return null; + }; + + /** + * Creates a MetaData message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof MetaData + * @static + * @param {Object.} object Plain object + * @returns {MetaData} MetaData + */ + MetaData.fromObject = function fromObject(object) { + if (object instanceof $root.MetaData) + return object; + return new $root.MetaData(); + }; + + /** + * Creates a plain object from a MetaData message. Also converts values to other types if specified. + * @function toObject + * @memberof MetaData + * @static + * @param {MetaData} message MetaData + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + MetaData.toObject = function toObject() { + return {}; + }; + + /** + * Converts this MetaData to JSON. + * @function toJSON + * @memberof MetaData + * @instance + * @returns {Object.} JSON object + */ + MetaData.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for MetaData + * @function getTypeUrl + * @memberof MetaData + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + MetaData.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/MetaData"; + }; - /** - * Creates a new MetaData instance using the specified properties. - * @function create - * @memberof MetaData - * @static - * @param {IMetaData=} [properties] Properties to set - * @returns {MetaData} MetaData instance - */ - MetaData.create = function create(properties) { - return new MetaData(properties); - }; - - /** - * Encodes the specified MetaData message. Does not implicitly {@link MetaData.verify|verify} messages. - * @function encode - * @memberof MetaData - * @static - * @param {IMetaData} message MetaData message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - MetaData.encode = function encode(message, writer) { - if (!writer) - writer = $Writer.create(); - return writer; - }; - - /** - * Encodes the specified MetaData message, length delimited. Does not implicitly {@link MetaData.verify|verify} messages. - * @function encodeDelimited - * @memberof MetaData - * @static - * @param {IMetaData} message MetaData message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - MetaData.encodeDelimited = function encodeDelimited(message, writer) { - return this.encode(message, writer).ldelim(); - }; - - /** - * Decodes a MetaData message from the specified reader or buffer. - * @function decode - * @memberof MetaData - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @param {number} [length] Message length if known beforehand - * @returns {MetaData} MetaData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - MetaData.decode = function decode(reader, length, error) { - if (!(reader instanceof $Reader)) - reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.MetaData(); - while (reader.pos < end) { - var tag = reader.uint32(); - if (tag === error) - break; - switch (tag >>> 3) { - default: - reader.skipType(tag & 7); - break; - } - } - return message; - }; - - /** - * Decodes a MetaData message from the specified reader or buffer, length delimited. - * @function decodeDelimited - * @memberof MetaData - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {MetaData} MetaData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - MetaData.decodeDelimited = function decodeDelimited(reader) { - if (!(reader instanceof $Reader)) - reader = new $Reader(reader); - return this.decode(reader, reader.uint32()); - }; - - /** - * Verifies a MetaData message. - * @function verify - * @memberof MetaData - * @static - * @param {Object.} message Plain object to verify - * @returns {string|null} `null` if valid, otherwise the reason why it is not - */ - MetaData.verify = function verify(message) { - if (typeof message !== "object" || message === null) - return "object expected"; - return null; - }; - - /** - * Creates a MetaData message from a plain object. Also converts values to their respective internal types. - * @function fromObject - * @memberof MetaData - * @static - * @param {Object.} object Plain object - * @returns {MetaData} MetaData - */ - MetaData.fromObject = function fromObject(object) { - if (object instanceof $root.MetaData) - return object; - return new $root.MetaData(); - }; - - /** - * Creates a plain object from a MetaData message. Also converts values to other types if specified. - * @function toObject - * @memberof MetaData - * @static - * @param {MetaData} message MetaData - * @param {$protobuf.IConversionOptions} [options] Conversion options - * @returns {Object.} Plain object - */ - MetaData.toObject = function toObject() { - return {}; - }; - - /** - * Converts this MetaData to JSON. - * @function toJSON - * @memberof MetaData - * @instance - * @returns {Object.} JSON object - */ - MetaData.prototype.toJSON = function toJSON() { - return this.constructor.toObject(this, $protobuf.util.toJSONOptions); - }; - - /** - * Gets the default type url for MetaData - * @function getTypeUrl - * @memberof MetaData - * @static - * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") - * @returns {string} The default type url - */ - MetaData.getTypeUrl = function getTypeUrl(typeUrlPrefix) { - if (typeUrlPrefix === undefined) { - typeUrlPrefix = "type.googleapis.com"; - } - return typeUrlPrefix + "/MetaData"; - }; - - return MetaData; + return MetaData; })(); $root.ESDigitalToken = (function () { - /** - * Properties of a ESDigitalToken. - * @exports IESDigitalToken - * @interface IESDigitalToken - * @property {number|null} [Type] ESDigitalToken Type - * @property {Uint8Array|null} [Value] ESDigitalToken Value - * @property {Uint8Array|null} [Properties] ESDigitalToken Properties - * @property {IMetaData|null} [TokenMetaData] ESDigitalToken TokenMetaData - * @property {Uint8Array|null} [Reserved] ESDigitalToken Reserved - */ - - /** - * Constructs a new ESDigitalToken. - * @exports ESDigitalToken - * @classdesc Represents a ESDigitalToken. - * @implements IESDigitalToken - * @constructor - * @param {IESDigitalToken=} [properties] Properties to set - */ - function ESDigitalToken(properties) { - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } - - /** - * ESDigitalToken Type. - * @member {number} Type - * @memberof ESDigitalToken - * @instance - */ - ESDigitalToken.prototype.Type = 0; - - /** - * ESDigitalToken Value. - * @member {Uint8Array} Value - * @memberof ESDigitalToken - * @instance - */ - ESDigitalToken.prototype.Value = $util.newBuffer([]); - - /** - * ESDigitalToken Properties. - * @member {Uint8Array} Properties - * @memberof ESDigitalToken - * @instance - */ - ESDigitalToken.prototype.Properties = $util.newBuffer([]); - - /** - * ESDigitalToken TokenMetaData. - * @member {IMetaData|null|undefined} TokenMetaData - * @memberof ESDigitalToken - * @instance - */ - ESDigitalToken.prototype.TokenMetaData = null; - - /** - * ESDigitalToken Reserved. - * @member {Uint8Array} Reserved - * @memberof ESDigitalToken - * @instance - */ - ESDigitalToken.prototype.Reserved = $util.newBuffer([]); - - /** - * Creates a new ESDigitalToken instance using the specified properties. - * @function create - * @memberof ESDigitalToken - * @static - * @param {IESDigitalToken=} [properties] Properties to set - * @returns {ESDigitalToken} ESDigitalToken instance - */ - ESDigitalToken.create = function create(properties) { - return new ESDigitalToken(properties); - }; - - /** - * Encodes the specified ESDigitalToken message. Does not implicitly {@link ESDigitalToken.verify|verify} messages. - * @function encode - * @memberof ESDigitalToken - * @static - * @param {IESDigitalToken} message ESDigitalToken message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - ESDigitalToken.encode = function encode(message, writer) { - if (!writer) - writer = $Writer.create(); - if (message.Type != null && Object.hasOwnProperty.call(message, "Type")) - writer.uint32(/* id 1, wireType 0 =*/8).uint32(message.Type); - if (message.Value != null && Object.hasOwnProperty.call(message, "Value")) - writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.Value); - if (message.Properties != null && Object.hasOwnProperty.call(message, "Properties")) - writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.Properties); - if (message.TokenMetaData != null && Object.hasOwnProperty.call(message, "TokenMetaData")) - $root.MetaData.encode(message.TokenMetaData, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); - if (message.Reserved != null && Object.hasOwnProperty.call(message, "Reserved")) - writer.uint32(/* id 5, wireType 2 =*/42).bytes(message.Reserved); - return writer; - }; - - /** - * Encodes the specified ESDigitalToken message, length delimited. Does not implicitly {@link ESDigitalToken.verify|verify} messages. - * @function encodeDelimited - * @memberof ESDigitalToken - * @static - * @param {IESDigitalToken} message ESDigitalToken message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - ESDigitalToken.encodeDelimited = function encodeDelimited(message, writer) { - return this.encode(message, writer).ldelim(); - }; - - /** - * Decodes a ESDigitalToken message from the specified reader or buffer. - * @function decode - * @memberof ESDigitalToken - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @param {number} [length] Message length if known beforehand - * @returns {ESDigitalToken} ESDigitalToken - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - ESDigitalToken.decode = function decode(reader, length, error) { - if (!(reader instanceof $Reader)) - reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.ESDigitalToken(); - while (reader.pos < end) { - var tag = reader.uint32(); - if (tag === error) - break; - switch (tag >>> 3) { - case 1: { - message.Type = reader.uint32(); - break; - } - case 2: { - message.Value = reader.bytes(); - break; - } - case 3: { - message.Properties = reader.bytes(); - break; - } - case 4: { - message.TokenMetaData = $root.MetaData.decode(reader, reader.uint32()); - break; - } - case 5: { - message.Reserved = reader.bytes(); - break; - } - default: - reader.skipType(tag & 7); - break; - } + /** + * Properties of a ESDigitalToken. + * @exports IESDigitalToken + * @interface IESDigitalToken + * @property {number|null} [Type] ESDigitalToken Type + * @property {Uint8Array|null} [Value] ESDigitalToken Value + * @property {Uint8Array|null} [Properties] ESDigitalToken Properties + * @property {IMetaData|null} [TokenMetaData] ESDigitalToken TokenMetaData + * @property {Uint8Array|null} [Reserved] ESDigitalToken Reserved + */ + + /** + * Constructs a new ESDigitalToken. + * @exports ESDigitalToken + * @classdesc Represents a ESDigitalToken. + * @implements IESDigitalToken + * @constructor + * @param {IESDigitalToken=} [properties] Properties to set + */ + function ESDigitalToken(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * ESDigitalToken Type. + * @member {number} Type + * @memberof ESDigitalToken + * @instance + */ + ESDigitalToken.prototype.Type = 0; + + /** + * ESDigitalToken Value. + * @member {Uint8Array} Value + * @memberof ESDigitalToken + * @instance + */ + ESDigitalToken.prototype.Value = $util.newBuffer([]); + + /** + * ESDigitalToken Properties. + * @member {Uint8Array} Properties + * @memberof ESDigitalToken + * @instance + */ + ESDigitalToken.prototype.Properties = $util.newBuffer([]); + + /** + * ESDigitalToken TokenMetaData. + * @member {IMetaData|null|undefined} TokenMetaData + * @memberof ESDigitalToken + * @instance + */ + ESDigitalToken.prototype.TokenMetaData = null; + + /** + * ESDigitalToken Reserved. + * @member {Uint8Array} Reserved + * @memberof ESDigitalToken + * @instance + */ + ESDigitalToken.prototype.Reserved = $util.newBuffer([]); + + /** + * Creates a new ESDigitalToken instance using the specified properties. + * @function create + * @memberof ESDigitalToken + * @static + * @param {IESDigitalToken=} [properties] Properties to set + * @returns {ESDigitalToken} ESDigitalToken instance + */ + ESDigitalToken.create = function create(properties) { + return new ESDigitalToken(properties); + }; + + /** + * Encodes the specified ESDigitalToken message. Does not implicitly {@link ESDigitalToken.verify|verify} messages. + * @function encode + * @memberof ESDigitalToken + * @static + * @param {IESDigitalToken} message ESDigitalToken message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + ESDigitalToken.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.Type != null && Object.hasOwnProperty.call(message, "Type")) + writer.uint32(/* id 1, wireType 0 =*/8).uint32(message.Type); + if (message.Value != null && Object.hasOwnProperty.call(message, "Value")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.Value); + if (message.Properties != null && Object.hasOwnProperty.call(message, "Properties")) + writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.Properties); + if (message.TokenMetaData != null && Object.hasOwnProperty.call(message, "TokenMetaData")) + $root.MetaData.encode(message.TokenMetaData, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); + if (message.Reserved != null && Object.hasOwnProperty.call(message, "Reserved")) + writer.uint32(/* id 5, wireType 2 =*/42).bytes(message.Reserved); + return writer; + }; + + /** + * Encodes the specified ESDigitalToken message, length delimited. Does not implicitly {@link ESDigitalToken.verify|verify} messages. + * @function encodeDelimited + * @memberof ESDigitalToken + * @static + * @param {IESDigitalToken} message ESDigitalToken message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + ESDigitalToken.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a ESDigitalToken message from the specified reader or buffer. + * @function decode + * @memberof ESDigitalToken + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {ESDigitalToken} ESDigitalToken + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + ESDigitalToken.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.ESDigitalToken(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.Type = reader.uint32(); + break; } - return message; - }; - - /** - * Decodes a ESDigitalToken message from the specified reader or buffer, length delimited. - * @function decodeDelimited - * @memberof ESDigitalToken - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {ESDigitalToken} ESDigitalToken - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - ESDigitalToken.decodeDelimited = function decodeDelimited(reader) { - if (!(reader instanceof $Reader)) - reader = new $Reader(reader); - return this.decode(reader, reader.uint32()); - }; - - /** - * Verifies a ESDigitalToken message. - * @function verify - * @memberof ESDigitalToken - * @static - * @param {Object.} message Plain object to verify - * @returns {string|null} `null` if valid, otherwise the reason why it is not - */ - ESDigitalToken.verify = function verify(message) { - if (typeof message !== "object" || message === null) - return "object expected"; - if (message.Type != null && message.hasOwnProperty("Type")) - if (!$util.isInteger(message.Type)) - return "Type: integer expected"; - if (message.Value != null && message.hasOwnProperty("Value")) - if (!(message.Value && typeof message.Value.length === "number" || $util.isString(message.Value))) - return "Value: buffer expected"; - if (message.Properties != null && message.hasOwnProperty("Properties")) - if (!(message.Properties && typeof message.Properties.length === "number" || $util.isString(message.Properties))) - return "Properties: buffer expected"; - if (message.TokenMetaData != null && message.hasOwnProperty("TokenMetaData")) { - var error = $root.MetaData.verify(message.TokenMetaData); - if (error) - return "TokenMetaData." + error; + case 2: { + message.Value = reader.bytes(); + break; } - if (message.Reserved != null && message.hasOwnProperty("Reserved")) - if (!(message.Reserved && typeof message.Reserved.length === "number" || $util.isString(message.Reserved))) - return "Reserved: buffer expected"; - return null; - }; - - /** - * Creates a ESDigitalToken message from a plain object. Also converts values to their respective internal types. - * @function fromObject - * @memberof ESDigitalToken - * @static - * @param {Object.} object Plain object - * @returns {ESDigitalToken} ESDigitalToken - */ - ESDigitalToken.fromObject = function fromObject(object) { - if (object instanceof $root.ESDigitalToken) - return object; - var message = new $root.ESDigitalToken(); - if (object.Type != null) - message.Type = object.Type >>> 0; - if (object.Value != null) - if (typeof object.Value === "string") - $util.base64.decode(object.Value, message.Value = $util.newBuffer($util.base64.length(object.Value)), 0); - else if (object.Value.length >= 0) - message.Value = object.Value; - if (object.Properties != null) - if (typeof object.Properties === "string") - $util.base64.decode(object.Properties, message.Properties = $util.newBuffer($util.base64.length(object.Properties)), 0); - else if (object.Properties.length >= 0) - message.Properties = object.Properties; - if (object.TokenMetaData != null) { - if (typeof object.TokenMetaData !== "object") - throw TypeError(".ESDigitalToken.TokenMetaData: object expected"); - message.TokenMetaData = $root.MetaData.fromObject(object.TokenMetaData); + case 3: { + message.Properties = reader.bytes(); + break; } - if (object.Reserved != null) - if (typeof object.Reserved === "string") - $util.base64.decode(object.Reserved, message.Reserved = $util.newBuffer($util.base64.length(object.Reserved)), 0); - else if (object.Reserved.length >= 0) - message.Reserved = object.Reserved; - return message; - }; - - /** - * Creates a plain object from a ESDigitalToken message. Also converts values to other types if specified. - * @function toObject - * @memberof ESDigitalToken - * @static - * @param {ESDigitalToken} message ESDigitalToken - * @param {$protobuf.IConversionOptions} [options] Conversion options - * @returns {Object.} Plain object - */ - ESDigitalToken.toObject = function toObject(message, options) { - if (!options) - options = {}; - var object = {}; - if (options.defaults) { - object.Type = 0; - if (options.bytes === String) - object.Value = ""; - else { - object.Value = []; - if (options.bytes !== Array) - object.Value = $util.newBuffer(object.Value); - } - if (options.bytes === String) - object.Properties = ""; - else { - object.Properties = []; - if (options.bytes !== Array) - object.Properties = $util.newBuffer(object.Properties); - } - object.TokenMetaData = null; - if (options.bytes === String) - object.Reserved = ""; - else { - object.Reserved = []; - if (options.bytes !== Array) - object.Reserved = $util.newBuffer(object.Reserved); - } + case 4: { + message.TokenMetaData = $root.MetaData.decode(reader, reader.uint32()); + break; } - if (message.Type != null && message.hasOwnProperty("Type")) - object.Type = message.Type; - if (message.Value != null && message.hasOwnProperty("Value")) - object.Value = options.bytes === String ? $util.base64.encode(message.Value, 0, message.Value.length) : options.bytes === Array ? Array.prototype.slice.call(message.Value) : message.Value; - if (message.Properties != null && message.hasOwnProperty("Properties")) - object.Properties = options.bytes === String ? $util.base64.encode(message.Properties, 0, message.Properties.length) : options.bytes === Array ? Array.prototype.slice.call(message.Properties) : message.Properties; - if (message.TokenMetaData != null && message.hasOwnProperty("TokenMetaData")) - object.TokenMetaData = $root.MetaData.toObject(message.TokenMetaData, options); - if (message.Reserved != null && message.hasOwnProperty("Reserved")) - object.Reserved = options.bytes === String ? $util.base64.encode(message.Reserved, 0, message.Reserved.length) : options.bytes === Array ? Array.prototype.slice.call(message.Reserved) : message.Reserved; - return object; - }; - - /** - * Converts this ESDigitalToken to JSON. - * @function toJSON - * @memberof ESDigitalToken - * @instance - * @returns {Object.} JSON object - */ - ESDigitalToken.prototype.toJSON = function toJSON() { - return this.constructor.toObject(this, $protobuf.util.toJSONOptions); - }; - - /** - * Gets the default type url for ESDigitalToken - * @function getTypeUrl - * @memberof ESDigitalToken - * @static - * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") - * @returns {string} The default type url - */ - ESDigitalToken.getTypeUrl = function getTypeUrl(typeUrlPrefix) { - if (typeUrlPrefix === undefined) { - typeUrlPrefix = "type.googleapis.com"; + case 5: { + message.Reserved = reader.bytes(); + break; } - return typeUrlPrefix + "/ESDigitalToken"; - }; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a ESDigitalToken message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof ESDigitalToken + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {ESDigitalToken} ESDigitalToken + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + ESDigitalToken.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a ESDigitalToken message. + * @function verify + * @memberof ESDigitalToken + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + ESDigitalToken.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.Type != null && message.hasOwnProperty("Type")) + if (!$util.isInteger(message.Type)) + return "Type: integer expected"; + if (message.Value != null && message.hasOwnProperty("Value")) + if (!(message.Value && typeof message.Value.length === "number" || $util.isString(message.Value))) + return "Value: buffer expected"; + if (message.Properties != null && message.hasOwnProperty("Properties")) + if (!(message.Properties && typeof message.Properties.length === "number" || $util.isString(message.Properties))) + return "Properties: buffer expected"; + if (message.TokenMetaData != null && message.hasOwnProperty("TokenMetaData")) { + var error = $root.MetaData.verify(message.TokenMetaData); + if (error) + return "TokenMetaData." + error; + } + if (message.Reserved != null && message.hasOwnProperty("Reserved")) + if (!(message.Reserved && typeof message.Reserved.length === "number" || $util.isString(message.Reserved))) + return "Reserved: buffer expected"; + return null; + }; + + /** + * Creates a ESDigitalToken message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof ESDigitalToken + * @static + * @param {Object.} object Plain object + * @returns {ESDigitalToken} ESDigitalToken + */ + ESDigitalToken.fromObject = function fromObject(object) { + if (object instanceof $root.ESDigitalToken) + return object; + var message = new $root.ESDigitalToken(); + if (object.Type != null) + message.Type = object.Type >>> 0; + if (object.Value != null) + if (typeof object.Value === "string") + $util.base64.decode(object.Value, message.Value = $util.newBuffer($util.base64.length(object.Value)), 0); + else if (object.Value.length >= 0) + message.Value = object.Value; + if (object.Properties != null) + if (typeof object.Properties === "string") + $util.base64.decode(object.Properties, message.Properties = $util.newBuffer($util.base64.length(object.Properties)), 0); + else if (object.Properties.length >= 0) + message.Properties = object.Properties; + if (object.TokenMetaData != null) { + if (typeof object.TokenMetaData !== "object") + throw TypeError(".ESDigitalToken.TokenMetaData: object expected"); + message.TokenMetaData = $root.MetaData.fromObject(object.TokenMetaData); + } + if (object.Reserved != null) + if (typeof object.Reserved === "string") + $util.base64.decode(object.Reserved, message.Reserved = $util.newBuffer($util.base64.length(object.Reserved)), 0); + else if (object.Reserved.length >= 0) + message.Reserved = object.Reserved; + return message; + }; + + /** + * Creates a plain object from a ESDigitalToken message. Also converts values to other types if specified. + * @function toObject + * @memberof ESDigitalToken + * @static + * @param {ESDigitalToken} message ESDigitalToken + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + ESDigitalToken.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + object.Type = 0; + if (options.bytes === String) + object.Value = ""; + else { + object.Value = []; + if (options.bytes !== Array) + object.Value = $util.newBuffer(object.Value); + } + if (options.bytes === String) + object.Properties = ""; + else { + object.Properties = []; + if (options.bytes !== Array) + object.Properties = $util.newBuffer(object.Properties); + } + object.TokenMetaData = null; + if (options.bytes === String) + object.Reserved = ""; + else { + object.Reserved = []; + if (options.bytes !== Array) + object.Reserved = $util.newBuffer(object.Reserved); + } + } + if (message.Type != null && message.hasOwnProperty("Type")) + object.Type = message.Type; + if (message.Value != null && message.hasOwnProperty("Value")) + object.Value = options.bytes === String ? $util.base64.encode(message.Value, 0, message.Value.length) : options.bytes === Array ? Array.prototype.slice.call(message.Value) : message.Value; + if (message.Properties != null && message.hasOwnProperty("Properties")) + object.Properties = options.bytes === String ? $util.base64.encode(message.Properties, 0, message.Properties.length) : options.bytes === Array ? Array.prototype.slice.call(message.Properties) : message.Properties; + if (message.TokenMetaData != null && message.hasOwnProperty("TokenMetaData")) + object.TokenMetaData = $root.MetaData.toObject(message.TokenMetaData, options); + if (message.Reserved != null && message.hasOwnProperty("Reserved")) + object.Reserved = options.bytes === String ? $util.base64.encode(message.Reserved, 0, message.Reserved.length) : options.bytes === Array ? Array.prototype.slice.call(message.Reserved) : message.Reserved; + return object; + }; + + /** + * Converts this ESDigitalToken to JSON. + * @function toJSON + * @memberof ESDigitalToken + * @instance + * @returns {Object.} JSON object + */ + ESDigitalToken.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for ESDigitalToken + * @function getTypeUrl + * @memberof ESDigitalToken + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + ESDigitalToken.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/ESDigitalToken"; + }; - return ESDigitalToken; + return ESDigitalToken; })(); module.exports = $root; diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts index ebfba1a88..800097dfd 100644 --- a/src/state-changes/utils/state-changes.decoder.ts +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -3,399 +3,399 @@ import { UserAccountData } from "./user_account.pb"; import { ESDigitalToken } from "./esdt"; import { TokenParser } from "./token.parser"; import { - AccountChanges, - AccountChangesRaw, - AccountState, - BlockWithStateChangesRaw, - DataTrieChange, - DataTrieChangeOperation, - EsdtState, - ESDTType, - StateAccessOperation, - StateAccessPerAccountRaw, - StateChanges, + AccountChanges, + AccountChangesRaw, + AccountState, + BlockWithStateChangesRaw, + DataTrieChange, + DataTrieChangeOperation, + EsdtState, + ESDTType, + StateAccessOperation, + StateAccessPerAccountRaw, + StateChanges, } from "../entities"; export class StateChangesDecoder { - static bech32FromHex(hex: any) { - const clean = hex.startsWith("0x") ? hex.slice(2) : hex; - return Address.newFromHex(clean).toBech32(); + static bech32FromHex(hex: any) { + const clean = hex.startsWith("0x") ? hex.slice(2) : hex; + return Address.newFromHex(clean).toBech32(); + } + + static bech32FromBytes(u8: any) { + return (u8 && u8.length ? Address.newFromHex(this.bytesToHex(u8)).toBech32() : ""); + } + + static bytesToHex(u8: any) { + return (u8 && u8.length ? Buffer.from(u8).toString("hex") : ""); + } + + static bytesToBase64(u8: any) { + return (u8 && u8.length ? Buffer.from(u8).toString("base64") : ""); + } + + // const bytesToString = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("utf8") : ""); + static longToString(v: any) { + return v == null ? "" : (typeof v === "object" && typeof v.toString === "function" ? v.toString() : String(v)); + } + + static bigEndianBytesToBigInt(u8: any) { + let v = BigInt(0); + for (const b of u8) { + v = (v << BigInt(8)) + BigInt(b); } - - static bech32FromBytes(u8: any) { - return (u8 && u8.length ? Address.newFromHex(this.bytesToHex(u8)).toBech32() : ""); - } - - static bytesToHex(u8: any) { - return (u8 && u8.length ? Buffer.from(u8).toString("hex") : ""); + return v; + } + + /** + * MultiversX custom sign & magnitude BigInt for proto: + * - zero => [0x00, 0x00] + * - positive non-zero => 0x00 || magnitude (big-endian) + * - (if present) negative => 0x01 || magnitude + * Fallback: if first byte is not a sign marker, treat whole buffer as positive magnitude. + */ + static decodeMxSignMagBigInt(u8: any) { + if (!u8 || u8.length === 0) return BigInt(0); + + // canonical zero used by the serializer + if (u8.length === 2 && u8[0] === 0x00 && u8[1] === 0x00) return BigInt(0); + + const sign = u8[0]; + if (sign === 0x00 || sign === 0x01) { + const mag = u8.slice(1); + const m = this.bigEndianBytesToBigInt(mag); + return sign === 0x01 ? -m : m; } - static bytesToBase64(u8: any) { - return (u8 && u8.length ? Buffer.from(u8).toString("base64") : ""); + // fallback for legacy "magnitude only" encodings + return this.bigEndianBytesToBigInt(u8); + } + + static getDecodedUserAccountData(buf: any) { + try { + const msg = UserAccountData.decode(buf); + + const balance = this.decodeMxSignMagBigInt(msg.Balance); + const devReward = this.decodeMxSignMagBigInt(msg.DeveloperReward); + const address = this.bech32FromBytes(msg.Address); + const ownerAddress = this.bech32FromBytes(msg.OwnerAddress); + + const data: AccountState = { + nonce: parseInt(this.longToString(msg.Nonce)), + balance: balance.toString(), + developerReward: devReward.toString(), + address, + ownerAddress, + codeHash: this.bytesToBase64(msg.CodeHash), + rootHash: this.bytesToBase64(msg.RootHash), + username: Buffer.from(this.bytesToHex(msg.UserName), 'hex').toString(), + codeMetadata: this.bytesToHex(msg.CodeMetadata), + }; + + const filteredData = Object.fromEntries( + Object.entries(data).filter(([_, v]) => v !== undefined && v !== null && v !== '') + ) as AccountState; + + return filteredData; + } catch (e: any) { + console.warn(`Could not decode as UserAccountData: ${e.message}`); + return null; } - - // const bytesToString = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("utf8") : ""); - static longToString(v: any) { - return v == null ? "" : (typeof v === "object" && typeof v.toString === "function" ? v.toString() : String(v)); - } - - static bigEndianBytesToBigInt(u8: any) { - let v = BigInt(0); - for (const b of u8) { - v = (v << BigInt(8)) + BigInt(b); - } - return v; + } + + static decodeAccountChanges(flags: number | undefined): AccountChanges { + if (!flags) { + return new AccountChanges({ + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false, + }); } - - /** - * MultiversX custom sign & magnitude BigInt for proto: - * - zero => [0x00, 0x00] - * - positive non-zero => 0x00 || magnitude (big-endian) - * - (if present) negative => 0x01 || magnitude - * Fallback: if first byte is not a sign marker, treat whole buffer as positive magnitude. - */ - static decodeMxSignMagBigInt(u8: any) { - if (!u8 || u8.length === 0) return BigInt(0); - - // canonical zero used by the serializer - if (u8.length === 2 && u8[0] === 0x00 && u8[1] === 0x00) return BigInt(0); - - const sign = u8[0]; - if (sign === 0x00 || sign === 0x01) { - const mag = u8.slice(1); - const m = this.bigEndianBytesToBigInt(mag); - return sign === 0x01 ? -m : m; - } - - // fallback for legacy "magnitude only" encodings - return this.bigEndianBytesToBigInt(u8); + return new AccountChanges({ + nonceChanged: (flags & AccountChangesRaw.NonceChanged) !== 0, + balanceChanged: (flags & AccountChangesRaw.BalanceChanged) !== 0, + codeHashChanged: (flags & AccountChangesRaw.CodeHashChanged) !== 0, + rootHashChanged: (flags & AccountChangesRaw.RootHashChanged) !== 0, + developerRewardChanged: (flags & AccountChangesRaw.DeveloperRewardChanged) !== 0, + ownerAddressChanged: (flags & AccountChangesRaw.OwnerAddressChanged) !== 0, + userNameChanged: (flags & AccountChangesRaw.UserNameChanged) !== 0, + codeMetadataChanged: (flags & AccountChangesRaw.CodeMetadataChanged) !== 0, + }); + } + + static getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { + const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); + const esdtPrefix = 'ELRONDesdt'; + try { + const keyRawBuf = Buffer.from(dataTrieChange.key, "base64"); + const keyRaw = keyRawBuf.toString(); + if (keyRaw.startsWith(esdtPrefix)) { + const keyBuf = keyRawBuf.slice(esdtPrefix.length); + const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufTrieLeafValue as Uint8Array); + + const valueBigInt: bigint = this.decodeMxSignMagBigInt(msgEsdtData.Value); + const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(keyBuf); + + return { + identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, + nonce: parseInt(nonceHex, 16).toString(), + type: msgEsdtData.Type, + value: valueBigInt.toString(), + propertiesHex: this.bytesToHex(msgEsdtData.Properties), + reservedHex: this.bytesToHex(msgEsdtData.Reserved), + tokenMetaData: msgEsdtData.TokenMetaData ?? null, + }; + } else { + //TODO: handle if needed + + return null; + } + } catch (e: any) { + console.warn(`Could not decode as EsdtData: ${e.message}`); + console.log(address, ':'); + console.dir(dataTrieChange); + return null; } + } - static getDecodedUserAccountData(buf: any) { - try { - const msg = UserAccountData.decode(buf); - - const balance = this.decodeMxSignMagBigInt(msg.Balance); - const devReward = this.decodeMxSignMagBigInt(msg.DeveloperReward); - const address = this.bech32FromBytes(msg.Address); - const ownerAddress = this.bech32FromBytes(msg.OwnerAddress); - - const data: AccountState = { - nonce: parseInt(this.longToString(msg.Nonce)), - balance: balance.toString(), - developerReward: devReward.toString(), - address, - ownerAddress, - codeHash: this.bytesToBase64(msg.CodeHash), - rootHash: this.bytesToBase64(msg.RootHash), - username: Buffer.from(this.bytesToHex(msg.UserName), 'hex').toString(), - codeMetadata: this.bytesToHex(msg.CodeMetadata), - }; - - const filteredData = Object.fromEntries( - Object.entries(data).filter(([_, v]) => v !== undefined && v !== null && v !== '') - ) as AccountState; - - return filteredData; - } catch (e: any) { - console.warn(`Could not decode as UserAccountData: ${e.message}`); - return null; - } - } + static decodeStateChangesRaw(blockWithStateChanges: BlockWithStateChangesRaw) { + const allAccounts: Record = {}; + const accounts = blockWithStateChanges.stateAccessesPerAccounts || {}; - static decodeAccountChanges(flags: number | undefined): AccountChanges { - if (!flags) { - return new AccountChanges({ - nonceChanged: false, - balanceChanged: false, - codeHashChanged: false, - rootHashChanged: false, - developerRewardChanged: false, - ownerAddressChanged: false, - userNameChanged: false, - codeMetadataChanged: false, - }); - } - return new AccountChanges({ - nonceChanged: (flags & AccountChangesRaw.NonceChanged) !== 0, - balanceChanged: (flags & AccountChangesRaw.BalanceChanged) !== 0, - codeHashChanged: (flags & AccountChangesRaw.CodeHashChanged) !== 0, - rootHashChanged: (flags & AccountChangesRaw.RootHashChanged) !== 0, - developerRewardChanged: (flags & AccountChangesRaw.DeveloperRewardChanged) !== 0, - ownerAddressChanged: (flags & AccountChangesRaw.OwnerAddressChanged) !== 0, - userNameChanged: (flags & AccountChangesRaw.UserNameChanged) !== 0, - codeMetadataChanged: (flags & AccountChangesRaw.CodeMetadataChanged) !== 0, - }); - } + for (const accountHex of Object.keys(accounts)) { + const address = this.bech32FromHex(accountHex); - static getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { - const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); - const esdtPrefix = 'ELRONDesdt'; - try { - const keyRawBuf = Buffer.from(dataTrieChange.key, "base64"); - const keyRaw = keyRawBuf.toString(); - if (keyRaw.startsWith(esdtPrefix)) { - const keyBuf = keyRawBuf.slice(esdtPrefix.length); - const msgEsdtData: ESDigitalToken = ESDigitalToken.decode(bufTrieLeafValue as Uint8Array); - - const valueBigInt: bigint = this.decodeMxSignMagBigInt(msgEsdtData.Value); - const [identifier, nonceHex] = TokenParser.extractTokenIDAndNonceHexFromTokenStorageKey(keyBuf); - - return { - identifier: nonceHex !== '00' ? `${identifier}-${nonceHex}` : identifier, - nonce: parseInt(nonceHex, 16).toString(), - type: msgEsdtData.Type, - value: valueBigInt.toString(), - propertiesHex: this.bytesToHex(msgEsdtData.Properties), - reservedHex: this.bytesToHex(msgEsdtData.Reserved), - tokenMetaData: msgEsdtData.TokenMetaData ?? null, - }; - } else { - //TODO: handle if needed + const { stateAccess = [] } = accounts[accountHex] || {}; + const allDecoded: Record = {}; + stateAccess.forEach((sa: StateAccessPerAccountRaw, i: number) => { - return null; - } - } catch (e: any) { - console.warn(`Could not decode as EsdtData: ${e.message}`); - console.log(address, ':'); - console.dir(dataTrieChange); - return null; + const base64AccountData = sa.mainTrieVal; + let decodedAccountData: any = null; + if (base64AccountData) { + const bufAccountData = Buffer.from(base64AccountData, "base64"); + decodedAccountData = this.getDecodedUserAccountData(bufAccountData); } - } - - static decodeStateChangesRaw(blockWithStateChanges: BlockWithStateChangesRaw) { - const allAccounts: Record = {}; - const accounts = blockWithStateChanges.stateAccessesPerAccounts || {}; - for (const accountHex of Object.keys(accounts)) { - const address = this.bech32FromHex(accountHex); + const dataTrieChanges = sa.dataTrieChanges; - const { stateAccess = [] } = accounts[accountHex] || {}; - const allDecoded: Record = {}; - stateAccess.forEach((sa: StateAccessPerAccountRaw, i: number) => { - const base64AccountData = sa.mainTrieVal; - let decodedAccountData: any = null; - if (base64AccountData) { - const bufAccountData = Buffer.from(base64AccountData, "base64"); - decodedAccountData = this.getDecodedUserAccountData(bufAccountData); - } - - const dataTrieChanges = sa.dataTrieChanges; - - - const allDecodedEsdtData: any[] = []; - if (dataTrieChanges) { - for (const dataTrieChange of dataTrieChanges) { - if (dataTrieChange.version === 0) { - console.warn(`Entry #${i}: unsupported dataTrieChanges version 0`); - } else { + const allDecodedEsdtData: any[] = []; + if (dataTrieChanges) { + for (const dataTrieChange of dataTrieChanges) { + if (dataTrieChange.version === 0) { + console.warn(`Entry #${i}: unsupported dataTrieChanges version 0`); + } else { - const decodedEsdtData = this.getDecodedEsdtData(address, dataTrieChange); + const decodedEsdtData = this.getDecodedEsdtData(address, dataTrieChange); - if (decodedEsdtData) { - if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { - decodedEsdtData.value = '0'; - } - allDecodedEsdtData.push(decodedEsdtData); - } - } - } + if (decodedEsdtData) { + if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { + decodedEsdtData.value = '0'; } + allDecodedEsdtData.push(decodedEsdtData); + } + } + } + } - if (decodedAccountData || allDecodedEsdtData.length > 0) { - const groupedEsdtStates = allDecodedEsdtData.reduce>( - (acc, state) => { - const typeName = ESDTType[state.type]; // numeric -> string - if (typeName) { - acc[typeName].push(state); - } - - return acc; - }, - { - Fungible: [], - NonFungible: [], - NonFungibleV2: [], - SemiFungible: [], - MetaFungible: [], - DynamicNFT: [], - DynamicSFT: [], - DynamicMeta: [], - } - ); - if (allDecoded[address] === undefined) allDecoded[address] = []; - const newAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; - - const accountChanges = this.decodeAccountChanges(sa.accountChanges); - - allDecoded[address].push({ - entry: `Entry #${i}`, - accountState: decodedAccountData, - esdtStates: groupedEsdtStates, - accountChanges, - newAccount, - }); - } - }); - if (Object.keys(allDecoded).length === 0) continue; - - allAccounts[address] = [...(allAccounts[address] || []), ...Object.values(allDecoded).flat()]; + if (decodedAccountData || allDecodedEsdtData.length > 0) { + const groupedEsdtStates = allDecodedEsdtData.reduce>( + (acc, state) => { + const typeName = ESDTType[state.type]; // numeric -> string + if (typeName) { + acc[typeName].push(state); + } + + return acc; + }, + { + Fungible: [], + NonFungible: [], + NonFungibleV2: [], + SemiFungible: [], + MetaFungible: [], + DynamicNFT: [], + DynamicSFT: [], + DynamicMeta: [], + } + ); + if (allDecoded[address] === undefined) allDecoded[address] = []; + const newAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; + + const accountChanges = this.decodeAccountChanges(sa.accountChanges); + + allDecoded[address].push({ + entry: `Entry #${i}`, + accountState: decodedAccountData, + esdtStates: groupedEsdtStates, + accountChanges, + newAccount, + }); } + }); + if (Object.keys(allDecoded).length === 0) continue; - return allAccounts; + allAccounts[address] = [...(allAccounts[address] || []), ...Object.values(allDecoded).flat()]; } - static decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { - const accounts = blockWithStateChanges.stateAccessesPerAccounts; - const finalStates: Record = {}; - - for (const accountHex of Object.keys(accounts)) { - const address = this.bech32FromHex(accountHex); - const esdtOccured: Record = {}; - - const { stateAccess } = accounts[accountHex] || {}; - let finalAccountChangesRaw: AccountChangesRaw = AccountChangesRaw.NoChange; - - let finalNewAccount = false; - - let finalAccountState: AccountState | undefined = undefined; - const finalEsdtStates = { - Fungible: [] as EsdtState[], - NonFungible: [] as EsdtState[], - NonFungibleV2: [] as EsdtState[], - SemiFungible: [] as EsdtState[], - MetaFungible: [] as EsdtState[], - DynamicNFT: [] as EsdtState[], - DynamicSFT: [] as EsdtState[], - DynamicMeta: [] as EsdtState[], - }; - - for (let i = stateAccess.length - 1; i >= 0; i--) { - const sa = stateAccess[i]; - const currentAccountChangesRaw = sa.accountChanges; - if (currentAccountChangesRaw) { - finalAccountChangesRaw |= currentAccountChangesRaw; - } + return allAccounts; + } + + static decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { + const accounts = blockWithStateChanges.stateAccessesPerAccounts; + const finalStates: Record = {}; + + for (const accountHex of Object.keys(accounts)) { + const address = this.bech32FromHex(accountHex); + const esdtOccured: Record = {}; + + const { stateAccess } = accounts[accountHex] || {}; + let finalAccountChangesRaw: AccountChangesRaw = AccountChangesRaw.NoChange; + + let finalNewAccount = false; + + let finalAccountState: AccountState | undefined = undefined; + const finalEsdtStates = { + Fungible: [] as EsdtState[], + NonFungible: [] as EsdtState[], + NonFungibleV2: [] as EsdtState[], + SemiFungible: [] as EsdtState[], + MetaFungible: [] as EsdtState[], + DynamicNFT: [] as EsdtState[], + DynamicSFT: [] as EsdtState[], + DynamicMeta: [] as EsdtState[], + }; + + for (let i = stateAccess.length - 1; i >= 0; i--) { + const sa = stateAccess[i]; + const currentAccountChangesRaw = sa.accountChanges; + if (currentAccountChangesRaw) { + finalAccountChangesRaw |= currentAccountChangesRaw; + } - if (!finalNewAccount) { - const currentNewAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; - finalNewAccount = currentNewAccount || finalNewAccount; - } + if (!finalNewAccount) { + const currentNewAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; + finalNewAccount = currentNewAccount || finalNewAccount; + } - const base64AccountData = sa.mainTrieVal; - if (base64AccountData && !finalAccountState) { - const bufAccountData = Buffer.from(base64AccountData, "base64"); - const decodedAccountData = this.getDecodedUserAccountData(bufAccountData); - if (decodedAccountData) { - finalAccountState = decodedAccountData; - } - } + const base64AccountData = sa.mainTrieVal; + if (base64AccountData && !finalAccountState) { + const bufAccountData = Buffer.from(base64AccountData, "base64"); + const decodedAccountData = this.getDecodedUserAccountData(bufAccountData); + if (decodedAccountData) { + finalAccountState = decodedAccountData; + } + } - const dataTrieChanges = sa.dataTrieChanges; - if (dataTrieChanges) { - for (let i = dataTrieChanges.length - 1; i >= 0; i--) { - const dataTrieChange = dataTrieChanges[i]; - if (dataTrieChange.version === 0) { - console.warn(`Unsupported dataTrieChanges version 0`); - } else if (dataTrieChange.val) { - const decodedEsdtData = this.getDecodedEsdtData(address, dataTrieChange); - if (decodedEsdtData) { - const esdtId = decodedEsdtData.identifier; - if (!esdtOccured[esdtId]) { - const typeName = ESDTType[decodedEsdtData.type] as keyof typeof finalEsdtStates; // numeric -> string - if (typeName) { - if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { - decodedEsdtData.value = '0'; - } - finalEsdtStates[typeName].push(decodedEsdtData); - esdtOccured[esdtId] = true; - } - } - } - } + const dataTrieChanges = sa.dataTrieChanges; + if (dataTrieChanges) { + for (let i = dataTrieChanges.length - 1; i >= 0; i--) { + const dataTrieChange = dataTrieChanges[i]; + if (dataTrieChange.version === 0) { + console.warn(`Unsupported dataTrieChanges version 0`); + } else if (dataTrieChange.val) { + const decodedEsdtData = this.getDecodedEsdtData(address, dataTrieChange); + if (decodedEsdtData) { + const esdtId = decodedEsdtData.identifier; + if (!esdtOccured[esdtId]) { + const typeName = ESDTType[decodedEsdtData.type] as keyof typeof finalEsdtStates; // numeric -> string + if (typeName) { + if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { + decodedEsdtData.value = '0'; } - + finalEsdtStates[typeName].push(decodedEsdtData); + esdtOccured[esdtId] = true; + } } + } } - finalStates[address] = { - accountState: finalAccountState, - esdtState: finalEsdtStates, - accountChanges: this.decodeAccountChanges(finalAccountChangesRaw), - isNewAccount: finalNewAccount, - }; + } + } - return finalStates; + } + finalStates[address] = { + accountState: finalAccountState, + esdtState: finalEsdtStates, + accountChanges: this.decodeAccountChanges(finalAccountChangesRaw), + isNewAccount: finalNewAccount, + }; } - - - static getFinalStates(stateChanges: Record) { - const finalStates: Record = {}; - - - for (const [address, entries] of Object.entries(stateChanges)) { - let finalAccountState: AccountState | undefined = undefined; - const finalEsdtStates = { - Fungible: [] as EsdtState[], - NonFungible: [] as EsdtState[], - NonFungibleV2: [] as EsdtState[], - SemiFungible: [] as EsdtState[], - MetaFungible: [] as EsdtState[], - DynamicNFT: [] as EsdtState[], - DynamicSFT: [] as EsdtState[], - DynamicMeta: [] as EsdtState[], - }; - const finalAccountChanges: AccountChanges = new AccountChanges({ - nonceChanged: false, - balanceChanged: false, - codeHashChanged: false, - rootHashChanged: false, - developerRewardChanged: false, - ownerAddressChanged: false, - userNameChanged: false, - codeMetadataChanged: false, - }); - - let finalNewAccount = false; - - for (const entry of entries) { - const currentAccountState: AccountState = entry.accountState; - const currentEsdtStates = entry.esdtStates; - const currentAccountChanges = entry.accountChanges; - const currentNewAccount = entry.newAccount as boolean; - - - finalNewAccount = finalNewAccount ? finalNewAccount : currentNewAccount; - - finalAccountState = currentAccountState; - - (Object.entries(finalAccountChanges) as [keyof typeof finalAccountChanges, boolean][]).forEach( - ([key, value]) => { - finalAccountChanges[key] = value || currentAccountChanges[key]; - } - ); - - - (Object.entries(currentEsdtStates) as [keyof typeof finalEsdtStates, any[]][]).forEach( - ([tokenType, tokenChanges]) => { - for (const tokenChange of tokenChanges) { - let index = finalEsdtStates[tokenType].findIndex((item: any) => item.key === tokenChange.key); - index = index !== -1 ? index : finalEsdtStates[tokenType].length; - finalEsdtStates[tokenType][index] = tokenChange; - } - - } - ); + return finalStates; + } + + + static getFinalStates(stateChanges: Record) { + const finalStates: Record = {}; + + + for (const [address, entries] of Object.entries(stateChanges)) { + let finalAccountState: AccountState | undefined = undefined; + const finalEsdtStates = { + Fungible: [] as EsdtState[], + NonFungible: [] as EsdtState[], + NonFungibleV2: [] as EsdtState[], + SemiFungible: [] as EsdtState[], + MetaFungible: [] as EsdtState[], + DynamicNFT: [] as EsdtState[], + DynamicSFT: [] as EsdtState[], + DynamicMeta: [] as EsdtState[], + }; + const finalAccountChanges: AccountChanges = new AccountChanges({ + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false, + }); + + let finalNewAccount = false; + + for (const entry of entries) { + const currentAccountState: AccountState = entry.accountState; + const currentEsdtStates = entry.esdtStates; + const currentAccountChanges = entry.accountChanges; + const currentNewAccount = entry.newAccount as boolean; + + + finalNewAccount = finalNewAccount ? finalNewAccount : currentNewAccount; + + finalAccountState = currentAccountState; + + (Object.entries(finalAccountChanges) as [keyof typeof finalAccountChanges, boolean][]).forEach( + ([key, value]) => { + finalAccountChanges[key] = value || currentAccountChanges[key]; + } + ); + + + (Object.entries(currentEsdtStates) as [keyof typeof finalEsdtStates, any[]][]).forEach( + ([tokenType, tokenChanges]) => { + for (const tokenChange of tokenChanges) { + let index = finalEsdtStates[tokenType].findIndex((item: any) => item.key === tokenChange.key); + index = index !== -1 ? index : finalEsdtStates[tokenType].length; + finalEsdtStates[tokenType][index] = tokenChange; } - finalStates[address] = { - accountState: finalAccountState, - esdtState: finalEsdtStates, - accountChanges: finalAccountChanges, - isNewAccount: finalNewAccount, - }; - } + } + ); + } - return finalStates; + finalStates[address] = { + accountState: finalAccountState, + esdtState: finalEsdtStates, + accountChanges: finalAccountChanges, + isNewAccount: finalNewAccount, + }; } + + return finalStates; + } } diff --git a/src/state-changes/utils/token.parser.ts b/src/state-changes/utils/token.parser.ts index 392ab6d23..4f11b768a 100644 --- a/src/state-changes/utils/token.parser.ts +++ b/src/state-changes/utils/token.parser.ts @@ -1,56 +1,58 @@ export class TokenParser { - // Constants from Go - private static readonly esdtTickerNumRandChars: number = 6; - private static readonly separatorChar: string = "-"; - private static readonly minLengthForTickerName: number = 3; - private static readonly maxLengthForTickerName: number = 10; - - /** - * ExtractTokenIDAndNonceFromTokenStorageKey - * Parses the token's storage key and extracts the identifier and the nonce. - * - * Examples: - * "ALC-1q2w3e" -> ["ALC-1q2w3e", "0"] (fungible, no nonce) - * "ALC-2w3e4rX" -> ["ALC-2w3e4r", "X"] (non-fungible, nonce = "X") - */ - public static extractTokenIDAndNonceHexFromTokenStorageKey( - tokenKeyRaw: Buffer - ): [string, string] { - const tokenKey = tokenKeyRaw.toString(); - const token = tokenKey; - - const indexOfFirstHyphen = token.indexOf(this.separatorChar); - if (indexOfFirstHyphen < 0) { - return [tokenKey, "00"]; - } - - const tokenTicker = token.slice(0, indexOfFirstHyphen); - const randomSequencePlusNonce = token.slice(indexOfFirstHyphen + 1); - - const tokenTickerLen = tokenTicker.length; - - const areTickerAndRandomSequenceInvalid = - tokenTickerLen === 0 || - tokenTickerLen < this.minLengthForTickerName || - tokenTickerLen > this.maxLengthForTickerName || - randomSequencePlusNonce.length === 0; - - if (areTickerAndRandomSequenceInvalid) { - return [tokenKey, "00"]; - } - - if (randomSequencePlusNonce.length < this.esdtTickerNumRandChars + 1) { - return [tokenKey, "00"]; - } - - // ALC-1q2w3eX -> X is the nonce - const nonceStr = randomSequencePlusNonce.slice(this.esdtTickerNumRandChars); - - const numCharsSinceNonce = token.length - nonceStr.length; - const tokenID = token.slice(0, numCharsSinceNonce); - if (nonceStr) { - return [tokenID, Array.from(tokenKeyRaw.slice(tokenID.length)).map(byte => byte.toString(16).padStart(2, '0')).join('')]; - } - return [tokenID, "00"]; + // Constants from Go + private static readonly esdtTickerNumRandChars: number = 6; + private static readonly separatorChar: string = "-"; + private static readonly minLengthForTickerName: number = 3; + private static readonly maxLengthForTickerName: number = 10; + + /** + * ExtractTokenIDAndNonceFromTokenStorageKey + * Parses the token's storage key and extracts the identifier and the nonce. + * + * Examples: + * "ALC-1q2w3e" -> ["ALC-1q2w3e", "0"] (fungible, no nonce) + * "ALC-2w3e4rX" -> ["ALC-2w3e4r", "X"] (non-fungible, nonce = "X") + */ + + //TODO: add support for sovereign keys prefix + public static extractTokenIDAndNonceHexFromTokenStorageKey( + tokenKeyRaw: Buffer + ): [string, string] { + const tokenKey = tokenKeyRaw.toString(); + const token = tokenKey; + + const indexOfFirstHyphen = token.indexOf(this.separatorChar); + if (indexOfFirstHyphen < 0) { + return [tokenKey, "00"]; } + + const tokenTicker = token.slice(0, indexOfFirstHyphen); + const randomSequencePlusNonce = token.slice(indexOfFirstHyphen + 1); + + const tokenTickerLen = tokenTicker.length; + + const areTickerAndRandomSequenceInvalid = + tokenTickerLen === 0 || + tokenTickerLen < this.minLengthForTickerName || + tokenTickerLen > this.maxLengthForTickerName || + randomSequencePlusNonce.length === 0; + + if (areTickerAndRandomSequenceInvalid) { + return [tokenKey, "00"]; + } + + if (randomSequencePlusNonce.length < this.esdtTickerNumRandChars + 1) { + return [tokenKey, "00"]; + } + + // ALC-1q2w3eX -> X is the nonce + const nonceStr = randomSequencePlusNonce.slice(this.esdtTickerNumRandChars); + + const numCharsSinceNonce = token.length - nonceStr.length; + const tokenID = token.slice(0, numCharsSinceNonce); + if (nonceStr) { + return [tokenID, Array.from(tokenKeyRaw.slice(tokenID.length)).map(byte => byte.toString(16).padStart(2, '0')).join('')]; + } + return [tokenID, "00"]; + } } diff --git a/src/state-changes/utils/trie_leaf_data.d.ts b/src/state-changes/utils/trie_leaf_data.d.ts index 560a4180b..219357d40 100644 --- a/src/state-changes/utils/trie_leaf_data.d.ts +++ b/src/state-changes/utils/trie_leaf_data.d.ts @@ -3,108 +3,108 @@ import Long = require("long"); /** Properties of a TrieLeafData. */ export interface ITrieLeafData { - /** TrieLeafData value */ - value?: (Uint8Array|null); + /** TrieLeafData value */ + value?: (Uint8Array | null); - /** TrieLeafData key */ - key?: (Uint8Array|null); + /** TrieLeafData key */ + key?: (Uint8Array | null); - /** TrieLeafData address */ - address?: (Uint8Array|null); + /** TrieLeafData address */ + address?: (Uint8Array | null); } /** Represents a TrieLeafData. */ export class TrieLeafData implements ITrieLeafData { - /** - * Constructs a new TrieLeafData. - * @param [properties] Properties to set - */ - constructor(properties?: ITrieLeafData); - - /** TrieLeafData value. */ - public value: Uint8Array; - - /** TrieLeafData key. */ - public key: Uint8Array; - - /** TrieLeafData address. */ - public address: Uint8Array; - - /** - * Creates a new TrieLeafData instance using the specified properties. - * @param [properties] Properties to set - * @returns TrieLeafData instance - */ - public static create(properties?: ITrieLeafData): TrieLeafData; - - /** - * Encodes the specified TrieLeafData message. Does not implicitly {@link TrieLeafData.verify|verify} messages. - * @param message TrieLeafData message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encode(message: ITrieLeafData, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Encodes the specified TrieLeafData message, length delimited. Does not implicitly {@link TrieLeafData.verify|verify} messages. - * @param message TrieLeafData message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encodeDelimited(message: ITrieLeafData, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a TrieLeafData message from the specified reader or buffer. - * @param reader Reader or buffer to decode from - * @param [length] Message length if known beforehand - * @returns TrieLeafData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): TrieLeafData; - - /** - * Decodes a TrieLeafData message from the specified reader or buffer, length delimited. - * @param reader Reader or buffer to decode from - * @returns TrieLeafData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): TrieLeafData; - - /** - * Verifies a TrieLeafData message. - * @param message Plain object to verify - * @returns `null` if valid, otherwise the reason why it is not - */ - public static verify(message: { [k: string]: any }): (string|null); - - /** - * Creates a TrieLeafData message from a plain object. Also converts values to their respective internal types. - * @param object Plain object - * @returns TrieLeafData - */ - public static fromObject(object: { [k: string]: any }): TrieLeafData; - - /** - * Creates a plain object from a TrieLeafData message. Also converts values to other types if specified. - * @param message TrieLeafData - * @param [options] Conversion options - * @returns Plain object - */ - public static toObject(message: TrieLeafData, options?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this TrieLeafData to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; - - /** - * Gets the default type url for TrieLeafData - * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") - * @returns The default type url - */ - public static getTypeUrl(typeUrlPrefix?: string): string; + /** + * Constructs a new TrieLeafData. + * @param [properties] Properties to set + */ + constructor(properties?: ITrieLeafData); + + /** TrieLeafData value. */ + public value: Uint8Array; + + /** TrieLeafData key. */ + public key: Uint8Array; + + /** TrieLeafData address. */ + public address: Uint8Array; + + /** + * Creates a new TrieLeafData instance using the specified properties. + * @param [properties] Properties to set + * @returns TrieLeafData instance + */ + public static create(properties?: ITrieLeafData): TrieLeafData; + + /** + * Encodes the specified TrieLeafData message. Does not implicitly {@link TrieLeafData.verify|verify} messages. + * @param message TrieLeafData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: ITrieLeafData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified TrieLeafData message, length delimited. Does not implicitly {@link TrieLeafData.verify|verify} messages. + * @param message TrieLeafData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: ITrieLeafData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a TrieLeafData message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns TrieLeafData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader | Uint8Array), length?: number): TrieLeafData; + + /** + * Decodes a TrieLeafData message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns TrieLeafData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader | Uint8Array)): TrieLeafData; + + /** + * Verifies a TrieLeafData message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string | null); + + /** + * Creates a TrieLeafData message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns TrieLeafData + */ + public static fromObject(object: { [k: string]: any }): TrieLeafData; + + /** + * Creates a plain object from a TrieLeafData message. Also converts values to other types if specified. + * @param message TrieLeafData + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: TrieLeafData, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this TrieLeafData to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for TrieLeafData + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; } diff --git a/src/state-changes/utils/trie_leaf_data.js b/src/state-changes/utils/trie_leaf_data.js index 55a82bf56..03adf9033 100644 --- a/src/state-changes/utils/trie_leaf_data.js +++ b/src/state-changes/utils/trie_leaf_data.js @@ -9,283 +9,283 @@ var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.ut // Exported root namespace var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); -$root.TrieLeafData = (function() { +$root.TrieLeafData = (function () { - /** - * Properties of a TrieLeafData. - * @exports ITrieLeafData - * @interface ITrieLeafData - * @property {Uint8Array|null} [value] TrieLeafData value - * @property {Uint8Array|null} [key] TrieLeafData key - * @property {Uint8Array|null} [address] TrieLeafData address - */ + /** + * Properties of a TrieLeafData. + * @exports ITrieLeafData + * @interface ITrieLeafData + * @property {Uint8Array|null} [value] TrieLeafData value + * @property {Uint8Array|null} [key] TrieLeafData key + * @property {Uint8Array|null} [address] TrieLeafData address + */ - /** - * Constructs a new TrieLeafData. - * @exports TrieLeafData - * @classdesc Represents a TrieLeafData. - * @implements ITrieLeafData - * @constructor - * @param {ITrieLeafData=} [properties] Properties to set - */ - function TrieLeafData(properties) { - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } + /** + * Constructs a new TrieLeafData. + * @exports TrieLeafData + * @classdesc Represents a TrieLeafData. + * @implements ITrieLeafData + * @constructor + * @param {ITrieLeafData=} [properties] Properties to set + */ + function TrieLeafData(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } - /** - * TrieLeafData value. - * @member {Uint8Array} value - * @memberof TrieLeafData - * @instance - */ - TrieLeafData.prototype.value = $util.newBuffer([]); + /** + * TrieLeafData value. + * @member {Uint8Array} value + * @memberof TrieLeafData + * @instance + */ + TrieLeafData.prototype.value = $util.newBuffer([]); - /** - * TrieLeafData key. - * @member {Uint8Array} key - * @memberof TrieLeafData - * @instance - */ - TrieLeafData.prototype.key = $util.newBuffer([]); + /** + * TrieLeafData key. + * @member {Uint8Array} key + * @memberof TrieLeafData + * @instance + */ + TrieLeafData.prototype.key = $util.newBuffer([]); - /** - * TrieLeafData address. - * @member {Uint8Array} address - * @memberof TrieLeafData - * @instance - */ - TrieLeafData.prototype.address = $util.newBuffer([]); + /** + * TrieLeafData address. + * @member {Uint8Array} address + * @memberof TrieLeafData + * @instance + */ + TrieLeafData.prototype.address = $util.newBuffer([]); - /** - * Creates a new TrieLeafData instance using the specified properties. - * @function create - * @memberof TrieLeafData - * @static - * @param {ITrieLeafData=} [properties] Properties to set - * @returns {TrieLeafData} TrieLeafData instance - */ - TrieLeafData.create = function create(properties) { - return new TrieLeafData(properties); - }; + /** + * Creates a new TrieLeafData instance using the specified properties. + * @function create + * @memberof TrieLeafData + * @static + * @param {ITrieLeafData=} [properties] Properties to set + * @returns {TrieLeafData} TrieLeafData instance + */ + TrieLeafData.create = function create(properties) { + return new TrieLeafData(properties); + }; - /** - * Encodes the specified TrieLeafData message. Does not implicitly {@link TrieLeafData.verify|verify} messages. - * @function encode - * @memberof TrieLeafData - * @static - * @param {ITrieLeafData} message TrieLeafData message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - TrieLeafData.encode = function encode(message, writer) { - if (!writer) - writer = $Writer.create(); - if (message.value != null && Object.hasOwnProperty.call(message, "value")) - writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.value); - if (message.key != null && Object.hasOwnProperty.call(message, "key")) - writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.key); - if (message.address != null && Object.hasOwnProperty.call(message, "address")) - writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.address); - return writer; - }; + /** + * Encodes the specified TrieLeafData message. Does not implicitly {@link TrieLeafData.verify|verify} messages. + * @function encode + * @memberof TrieLeafData + * @static + * @param {ITrieLeafData} message TrieLeafData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + TrieLeafData.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.value != null && Object.hasOwnProperty.call(message, "value")) + writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.value); + if (message.key != null && Object.hasOwnProperty.call(message, "key")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.key); + if (message.address != null && Object.hasOwnProperty.call(message, "address")) + writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.address); + return writer; + }; - /** - * Encodes the specified TrieLeafData message, length delimited. Does not implicitly {@link TrieLeafData.verify|verify} messages. - * @function encodeDelimited - * @memberof TrieLeafData - * @static - * @param {ITrieLeafData} message TrieLeafData message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - TrieLeafData.encodeDelimited = function encodeDelimited(message, writer) { - return this.encode(message, writer).ldelim(); - }; + /** + * Encodes the specified TrieLeafData message, length delimited. Does not implicitly {@link TrieLeafData.verify|verify} messages. + * @function encodeDelimited + * @memberof TrieLeafData + * @static + * @param {ITrieLeafData} message TrieLeafData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + TrieLeafData.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; - /** - * Decodes a TrieLeafData message from the specified reader or buffer. - * @function decode - * @memberof TrieLeafData - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @param {number} [length] Message length if known beforehand - * @returns {TrieLeafData} TrieLeafData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - TrieLeafData.decode = function decode(reader, length, error) { - if (!(reader instanceof $Reader)) - reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.TrieLeafData(); - while (reader.pos < end) { - var tag = reader.uint32(); - if (tag === error) - break; - switch (tag >>> 3) { - case 1: { - message.value = reader.bytes(); - break; - } - case 2: { - message.key = reader.bytes(); - break; - } - case 3: { - message.address = reader.bytes(); - break; - } - default: - reader.skipType(tag & 7); - break; - } + /** + * Decodes a TrieLeafData message from the specified reader or buffer. + * @function decode + * @memberof TrieLeafData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {TrieLeafData} TrieLeafData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + TrieLeafData.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.TrieLeafData(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.value = reader.bytes(); + break; + } + case 2: { + message.key = reader.bytes(); + break; } - return message; - }; + case 3: { + message.address = reader.bytes(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; - /** - * Decodes a TrieLeafData message from the specified reader or buffer, length delimited. - * @function decodeDelimited - * @memberof TrieLeafData - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {TrieLeafData} TrieLeafData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - TrieLeafData.decodeDelimited = function decodeDelimited(reader) { - if (!(reader instanceof $Reader)) - reader = new $Reader(reader); - return this.decode(reader, reader.uint32()); - }; + /** + * Decodes a TrieLeafData message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof TrieLeafData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {TrieLeafData} TrieLeafData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + TrieLeafData.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; - /** - * Verifies a TrieLeafData message. - * @function verify - * @memberof TrieLeafData - * @static - * @param {Object.} message Plain object to verify - * @returns {string|null} `null` if valid, otherwise the reason why it is not - */ - TrieLeafData.verify = function verify(message) { - if (typeof message !== "object" || message === null) - return "object expected"; - if (message.value != null && message.hasOwnProperty("value")) - if (!(message.value && typeof message.value.length === "number" || $util.isString(message.value))) - return "value: buffer expected"; - if (message.key != null && message.hasOwnProperty("key")) - if (!(message.key && typeof message.key.length === "number" || $util.isString(message.key))) - return "key: buffer expected"; - if (message.address != null && message.hasOwnProperty("address")) - if (!(message.address && typeof message.address.length === "number" || $util.isString(message.address))) - return "address: buffer expected"; - return null; - }; + /** + * Verifies a TrieLeafData message. + * @function verify + * @memberof TrieLeafData + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + TrieLeafData.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.value != null && message.hasOwnProperty("value")) + if (!(message.value && typeof message.value.length === "number" || $util.isString(message.value))) + return "value: buffer expected"; + if (message.key != null && message.hasOwnProperty("key")) + if (!(message.key && typeof message.key.length === "number" || $util.isString(message.key))) + return "key: buffer expected"; + if (message.address != null && message.hasOwnProperty("address")) + if (!(message.address && typeof message.address.length === "number" || $util.isString(message.address))) + return "address: buffer expected"; + return null; + }; - /** - * Creates a TrieLeafData message from a plain object. Also converts values to their respective internal types. - * @function fromObject - * @memberof TrieLeafData - * @static - * @param {Object.} object Plain object - * @returns {TrieLeafData} TrieLeafData - */ - TrieLeafData.fromObject = function fromObject(object) { - if (object instanceof $root.TrieLeafData) - return object; - var message = new $root.TrieLeafData(); - if (object.value != null) - if (typeof object.value === "string") - $util.base64.decode(object.value, message.value = $util.newBuffer($util.base64.length(object.value)), 0); - else if (object.value.length >= 0) - message.value = object.value; - if (object.key != null) - if (typeof object.key === "string") - $util.base64.decode(object.key, message.key = $util.newBuffer($util.base64.length(object.key)), 0); - else if (object.key.length >= 0) - message.key = object.key; - if (object.address != null) - if (typeof object.address === "string") - $util.base64.decode(object.address, message.address = $util.newBuffer($util.base64.length(object.address)), 0); - else if (object.address.length >= 0) - message.address = object.address; - return message; - }; + /** + * Creates a TrieLeafData message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof TrieLeafData + * @static + * @param {Object.} object Plain object + * @returns {TrieLeafData} TrieLeafData + */ + TrieLeafData.fromObject = function fromObject(object) { + if (object instanceof $root.TrieLeafData) + return object; + var message = new $root.TrieLeafData(); + if (object.value != null) + if (typeof object.value === "string") + $util.base64.decode(object.value, message.value = $util.newBuffer($util.base64.length(object.value)), 0); + else if (object.value.length >= 0) + message.value = object.value; + if (object.key != null) + if (typeof object.key === "string") + $util.base64.decode(object.key, message.key = $util.newBuffer($util.base64.length(object.key)), 0); + else if (object.key.length >= 0) + message.key = object.key; + if (object.address != null) + if (typeof object.address === "string") + $util.base64.decode(object.address, message.address = $util.newBuffer($util.base64.length(object.address)), 0); + else if (object.address.length >= 0) + message.address = object.address; + return message; + }; - /** - * Creates a plain object from a TrieLeafData message. Also converts values to other types if specified. - * @function toObject - * @memberof TrieLeafData - * @static - * @param {TrieLeafData} message TrieLeafData - * @param {$protobuf.IConversionOptions} [options] Conversion options - * @returns {Object.} Plain object - */ - TrieLeafData.toObject = function toObject(message, options) { - if (!options) - options = {}; - var object = {}; - if (options.defaults) { - if (options.bytes === String) - object.value = ""; - else { - object.value = []; - if (options.bytes !== Array) - object.value = $util.newBuffer(object.value); - } - if (options.bytes === String) - object.key = ""; - else { - object.key = []; - if (options.bytes !== Array) - object.key = $util.newBuffer(object.key); - } - if (options.bytes === String) - object.address = ""; - else { - object.address = []; - if (options.bytes !== Array) - object.address = $util.newBuffer(object.address); - } - } - if (message.value != null && message.hasOwnProperty("value")) - object.value = options.bytes === String ? $util.base64.encode(message.value, 0, message.value.length) : options.bytes === Array ? Array.prototype.slice.call(message.value) : message.value; - if (message.key != null && message.hasOwnProperty("key")) - object.key = options.bytes === String ? $util.base64.encode(message.key, 0, message.key.length) : options.bytes === Array ? Array.prototype.slice.call(message.key) : message.key; - if (message.address != null && message.hasOwnProperty("address")) - object.address = options.bytes === String ? $util.base64.encode(message.address, 0, message.address.length) : options.bytes === Array ? Array.prototype.slice.call(message.address) : message.address; - return object; - }; + /** + * Creates a plain object from a TrieLeafData message. Also converts values to other types if specified. + * @function toObject + * @memberof TrieLeafData + * @static + * @param {TrieLeafData} message TrieLeafData + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + TrieLeafData.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + if (options.bytes === String) + object.value = ""; + else { + object.value = []; + if (options.bytes !== Array) + object.value = $util.newBuffer(object.value); + } + if (options.bytes === String) + object.key = ""; + else { + object.key = []; + if (options.bytes !== Array) + object.key = $util.newBuffer(object.key); + } + if (options.bytes === String) + object.address = ""; + else { + object.address = []; + if (options.bytes !== Array) + object.address = $util.newBuffer(object.address); + } + } + if (message.value != null && message.hasOwnProperty("value")) + object.value = options.bytes === String ? $util.base64.encode(message.value, 0, message.value.length) : options.bytes === Array ? Array.prototype.slice.call(message.value) : message.value; + if (message.key != null && message.hasOwnProperty("key")) + object.key = options.bytes === String ? $util.base64.encode(message.key, 0, message.key.length) : options.bytes === Array ? Array.prototype.slice.call(message.key) : message.key; + if (message.address != null && message.hasOwnProperty("address")) + object.address = options.bytes === String ? $util.base64.encode(message.address, 0, message.address.length) : options.bytes === Array ? Array.prototype.slice.call(message.address) : message.address; + return object; + }; - /** - * Converts this TrieLeafData to JSON. - * @function toJSON - * @memberof TrieLeafData - * @instance - * @returns {Object.} JSON object - */ - TrieLeafData.prototype.toJSON = function toJSON() { - return this.constructor.toObject(this, $protobuf.util.toJSONOptions); - }; + /** + * Converts this TrieLeafData to JSON. + * @function toJSON + * @memberof TrieLeafData + * @instance + * @returns {Object.} JSON object + */ + TrieLeafData.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; - /** - * Gets the default type url for TrieLeafData - * @function getTypeUrl - * @memberof TrieLeafData - * @static - * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") - * @returns {string} The default type url - */ - TrieLeafData.getTypeUrl = function getTypeUrl(typeUrlPrefix) { - if (typeUrlPrefix === undefined) { - typeUrlPrefix = "type.googleapis.com"; - } - return typeUrlPrefix + "/TrieLeafData"; - }; + /** + * Gets the default type url for TrieLeafData + * @function getTypeUrl + * @memberof TrieLeafData + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + TrieLeafData.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/TrieLeafData"; + }; - return TrieLeafData; + return TrieLeafData; })(); module.exports = $root; diff --git a/src/state-changes/utils/user_account.pb.d.ts b/src/state-changes/utils/user_account.pb.d.ts index 508e70625..7b0c21fc3 100644 --- a/src/state-changes/utils/user_account.pb.d.ts +++ b/src/state-changes/utils/user_account.pb.d.ts @@ -3,144 +3,144 @@ import Long = require("long"); /** Properties of a UserAccountData. */ export interface IUserAccountData { - /** UserAccountData Nonce */ - Nonce?: (number|Long|null); + /** UserAccountData Nonce */ + Nonce?: (number | Long | null); - /** UserAccountData Balance */ - Balance?: (Uint8Array|null); + /** UserAccountData Balance */ + Balance?: (Uint8Array | null); - /** UserAccountData CodeHash */ - CodeHash?: (Uint8Array|null); + /** UserAccountData CodeHash */ + CodeHash?: (Uint8Array | null); - /** UserAccountData RootHash */ - RootHash?: (Uint8Array|null); + /** UserAccountData RootHash */ + RootHash?: (Uint8Array | null); - /** UserAccountData Address */ - Address?: (Uint8Array|null); + /** UserAccountData Address */ + Address?: (Uint8Array | null); - /** UserAccountData DeveloperReward */ - DeveloperReward?: (Uint8Array|null); + /** UserAccountData DeveloperReward */ + DeveloperReward?: (Uint8Array | null); - /** UserAccountData OwnerAddress */ - OwnerAddress?: (Uint8Array|null); + /** UserAccountData OwnerAddress */ + OwnerAddress?: (Uint8Array | null); - /** UserAccountData UserName */ - UserName?: (Uint8Array|null); + /** UserAccountData UserName */ + UserName?: (Uint8Array | null); - /** UserAccountData CodeMetadata */ - CodeMetadata?: (Uint8Array|null); + /** UserAccountData CodeMetadata */ + CodeMetadata?: (Uint8Array | null); } /** Represents a UserAccountData. */ export class UserAccountData implements IUserAccountData { - /** - * Constructs a new UserAccountData. - * @param [properties] Properties to set - */ - constructor(properties?: IUserAccountData); - - /** UserAccountData Nonce. */ - public Nonce: (number|Long); - - /** UserAccountData Balance. */ - public Balance: Uint8Array; - - /** UserAccountData CodeHash. */ - public CodeHash: Uint8Array; - - /** UserAccountData RootHash. */ - public RootHash: Uint8Array; - - /** UserAccountData Address. */ - public Address: Uint8Array; - - /** UserAccountData DeveloperReward. */ - public DeveloperReward: Uint8Array; - - /** UserAccountData OwnerAddress. */ - public OwnerAddress: Uint8Array; - - /** UserAccountData UserName. */ - public UserName: Uint8Array; - - /** UserAccountData CodeMetadata. */ - public CodeMetadata: Uint8Array; - - /** - * Creates a new UserAccountData instance using the specified properties. - * @param [properties] Properties to set - * @returns UserAccountData instance - */ - public static create(properties?: IUserAccountData): UserAccountData; - - /** - * Encodes the specified UserAccountData message. Does not implicitly {@link UserAccountData.verify|verify} messages. - * @param message UserAccountData message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encode(message: IUserAccountData, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Encodes the specified UserAccountData message, length delimited. Does not implicitly {@link UserAccountData.verify|verify} messages. - * @param message UserAccountData message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encodeDelimited(message: IUserAccountData, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a UserAccountData message from the specified reader or buffer. - * @param reader Reader or buffer to decode from - * @param [length] Message length if known beforehand - * @returns UserAccountData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): UserAccountData; - - /** - * Decodes a UserAccountData message from the specified reader or buffer, length delimited. - * @param reader Reader or buffer to decode from - * @returns UserAccountData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): UserAccountData; - - /** - * Verifies a UserAccountData message. - * @param message Plain object to verify - * @returns `null` if valid, otherwise the reason why it is not - */ - public static verify(message: { [k: string]: any }): (string|null); - - /** - * Creates a UserAccountData message from a plain object. Also converts values to their respective internal types. - * @param object Plain object - * @returns UserAccountData - */ - public static fromObject(object: { [k: string]: any }): UserAccountData; - - /** - * Creates a plain object from a UserAccountData message. Also converts values to other types if specified. - * @param message UserAccountData - * @param [options] Conversion options - * @returns Plain object - */ - public static toObject(message: UserAccountData, options?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this UserAccountData to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; - - /** - * Gets the default type url for UserAccountData - * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") - * @returns The default type url - */ - public static getTypeUrl(typeUrlPrefix?: string): string; + /** + * Constructs a new UserAccountData. + * @param [properties] Properties to set + */ + constructor(properties?: IUserAccountData); + + /** UserAccountData Nonce. */ + public Nonce: (number | Long); + + /** UserAccountData Balance. */ + public Balance: Uint8Array; + + /** UserAccountData CodeHash. */ + public CodeHash: Uint8Array; + + /** UserAccountData RootHash. */ + public RootHash: Uint8Array; + + /** UserAccountData Address. */ + public Address: Uint8Array; + + /** UserAccountData DeveloperReward. */ + public DeveloperReward: Uint8Array; + + /** UserAccountData OwnerAddress. */ + public OwnerAddress: Uint8Array; + + /** UserAccountData UserName. */ + public UserName: Uint8Array; + + /** UserAccountData CodeMetadata. */ + public CodeMetadata: Uint8Array; + + /** + * Creates a new UserAccountData instance using the specified properties. + * @param [properties] Properties to set + * @returns UserAccountData instance + */ + public static create(properties?: IUserAccountData): UserAccountData; + + /** + * Encodes the specified UserAccountData message. Does not implicitly {@link UserAccountData.verify|verify} messages. + * @param message UserAccountData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: IUserAccountData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified UserAccountData message, length delimited. Does not implicitly {@link UserAccountData.verify|verify} messages. + * @param message UserAccountData message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: IUserAccountData, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a UserAccountData message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns UserAccountData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader | Uint8Array), length?: number): UserAccountData; + + /** + * Decodes a UserAccountData message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns UserAccountData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader | Uint8Array)): UserAccountData; + + /** + * Verifies a UserAccountData message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string | null); + + /** + * Creates a UserAccountData message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns UserAccountData + */ + public static fromObject(object: { [k: string]: any }): UserAccountData; + + /** + * Creates a plain object from a UserAccountData message. Also converts values to other types if specified. + * @param message UserAccountData + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: UserAccountData, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this UserAccountData to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for UserAccountData + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; } diff --git a/src/state-changes/utils/user_account.pb.js b/src/state-changes/utils/user_account.pb.js index 9dd4952fd..cdb6731d2 100644 --- a/src/state-changes/utils/user_account.pb.js +++ b/src/state-changes/utils/user_account.pb.js @@ -9,480 +9,480 @@ var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.ut // Exported root namespace var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); -$root.UserAccountData = (function() { +$root.UserAccountData = (function () { - /** - * Properties of a UserAccountData. - * @exports IUserAccountData - * @interface IUserAccountData - * @property {number|Long|null} [Nonce] UserAccountData Nonce - * @property {Uint8Array|null} [Balance] UserAccountData Balance - * @property {Uint8Array|null} [CodeHash] UserAccountData CodeHash - * @property {Uint8Array|null} [RootHash] UserAccountData RootHash - * @property {Uint8Array|null} [Address] UserAccountData Address - * @property {Uint8Array|null} [DeveloperReward] UserAccountData DeveloperReward - * @property {Uint8Array|null} [OwnerAddress] UserAccountData OwnerAddress - * @property {Uint8Array|null} [UserName] UserAccountData UserName - * @property {Uint8Array|null} [CodeMetadata] UserAccountData CodeMetadata - */ + /** + * Properties of a UserAccountData. + * @exports IUserAccountData + * @interface IUserAccountData + * @property {number|Long|null} [Nonce] UserAccountData Nonce + * @property {Uint8Array|null} [Balance] UserAccountData Balance + * @property {Uint8Array|null} [CodeHash] UserAccountData CodeHash + * @property {Uint8Array|null} [RootHash] UserAccountData RootHash + * @property {Uint8Array|null} [Address] UserAccountData Address + * @property {Uint8Array|null} [DeveloperReward] UserAccountData DeveloperReward + * @property {Uint8Array|null} [OwnerAddress] UserAccountData OwnerAddress + * @property {Uint8Array|null} [UserName] UserAccountData UserName + * @property {Uint8Array|null} [CodeMetadata] UserAccountData CodeMetadata + */ - /** - * Constructs a new UserAccountData. - * @exports UserAccountData - * @classdesc Represents a UserAccountData. - * @implements IUserAccountData - * @constructor - * @param {IUserAccountData=} [properties] Properties to set - */ - function UserAccountData(properties) { - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } + /** + * Constructs a new UserAccountData. + * @exports UserAccountData + * @classdesc Represents a UserAccountData. + * @implements IUserAccountData + * @constructor + * @param {IUserAccountData=} [properties] Properties to set + */ + function UserAccountData(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } - /** - * UserAccountData Nonce. - * @member {number|Long} Nonce - * @memberof UserAccountData - * @instance - */ - UserAccountData.prototype.Nonce = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + /** + * UserAccountData Nonce. + * @member {number|Long} Nonce + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.Nonce = $util.Long ? $util.Long.fromBits(0, 0, true) : 0; - /** - * UserAccountData Balance. - * @member {Uint8Array} Balance - * @memberof UserAccountData - * @instance - */ - UserAccountData.prototype.Balance = $util.newBuffer([]); + /** + * UserAccountData Balance. + * @member {Uint8Array} Balance + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.Balance = $util.newBuffer([]); - /** - * UserAccountData CodeHash. - * @member {Uint8Array} CodeHash - * @memberof UserAccountData - * @instance - */ - UserAccountData.prototype.CodeHash = $util.newBuffer([]); + /** + * UserAccountData CodeHash. + * @member {Uint8Array} CodeHash + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.CodeHash = $util.newBuffer([]); - /** - * UserAccountData RootHash. - * @member {Uint8Array} RootHash - * @memberof UserAccountData - * @instance - */ - UserAccountData.prototype.RootHash = $util.newBuffer([]); + /** + * UserAccountData RootHash. + * @member {Uint8Array} RootHash + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.RootHash = $util.newBuffer([]); - /** - * UserAccountData Address. - * @member {Uint8Array} Address - * @memberof UserAccountData - * @instance - */ - UserAccountData.prototype.Address = $util.newBuffer([]); + /** + * UserAccountData Address. + * @member {Uint8Array} Address + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.Address = $util.newBuffer([]); - /** - * UserAccountData DeveloperReward. - * @member {Uint8Array} DeveloperReward - * @memberof UserAccountData - * @instance - */ - UserAccountData.prototype.DeveloperReward = $util.newBuffer([]); + /** + * UserAccountData DeveloperReward. + * @member {Uint8Array} DeveloperReward + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.DeveloperReward = $util.newBuffer([]); - /** - * UserAccountData OwnerAddress. - * @member {Uint8Array} OwnerAddress - * @memberof UserAccountData - * @instance - */ - UserAccountData.prototype.OwnerAddress = $util.newBuffer([]); + /** + * UserAccountData OwnerAddress. + * @member {Uint8Array} OwnerAddress + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.OwnerAddress = $util.newBuffer([]); - /** - * UserAccountData UserName. - * @member {Uint8Array} UserName - * @memberof UserAccountData - * @instance - */ - UserAccountData.prototype.UserName = $util.newBuffer([]); + /** + * UserAccountData UserName. + * @member {Uint8Array} UserName + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.UserName = $util.newBuffer([]); - /** - * UserAccountData CodeMetadata. - * @member {Uint8Array} CodeMetadata - * @memberof UserAccountData - * @instance - */ - UserAccountData.prototype.CodeMetadata = $util.newBuffer([]); + /** + * UserAccountData CodeMetadata. + * @member {Uint8Array} CodeMetadata + * @memberof UserAccountData + * @instance + */ + UserAccountData.prototype.CodeMetadata = $util.newBuffer([]); - /** - * Creates a new UserAccountData instance using the specified properties. - * @function create - * @memberof UserAccountData - * @static - * @param {IUserAccountData=} [properties] Properties to set - * @returns {UserAccountData} UserAccountData instance - */ - UserAccountData.create = function create(properties) { - return new UserAccountData(properties); - }; + /** + * Creates a new UserAccountData instance using the specified properties. + * @function create + * @memberof UserAccountData + * @static + * @param {IUserAccountData=} [properties] Properties to set + * @returns {UserAccountData} UserAccountData instance + */ + UserAccountData.create = function create(properties) { + return new UserAccountData(properties); + }; - /** - * Encodes the specified UserAccountData message. Does not implicitly {@link UserAccountData.verify|verify} messages. - * @function encode - * @memberof UserAccountData - * @static - * @param {IUserAccountData} message UserAccountData message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - UserAccountData.encode = function encode(message, writer) { - if (!writer) - writer = $Writer.create(); - if (message.Nonce != null && Object.hasOwnProperty.call(message, "Nonce")) - writer.uint32(/* id 1, wireType 0 =*/8).uint64(message.Nonce); - if (message.Balance != null && Object.hasOwnProperty.call(message, "Balance")) - writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.Balance); - if (message.CodeHash != null && Object.hasOwnProperty.call(message, "CodeHash")) - writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.CodeHash); - if (message.RootHash != null && Object.hasOwnProperty.call(message, "RootHash")) - writer.uint32(/* id 4, wireType 2 =*/34).bytes(message.RootHash); - if (message.Address != null && Object.hasOwnProperty.call(message, "Address")) - writer.uint32(/* id 5, wireType 2 =*/42).bytes(message.Address); - if (message.DeveloperReward != null && Object.hasOwnProperty.call(message, "DeveloperReward")) - writer.uint32(/* id 6, wireType 2 =*/50).bytes(message.DeveloperReward); - if (message.OwnerAddress != null && Object.hasOwnProperty.call(message, "OwnerAddress")) - writer.uint32(/* id 7, wireType 2 =*/58).bytes(message.OwnerAddress); - if (message.UserName != null && Object.hasOwnProperty.call(message, "UserName")) - writer.uint32(/* id 8, wireType 2 =*/66).bytes(message.UserName); - if (message.CodeMetadata != null && Object.hasOwnProperty.call(message, "CodeMetadata")) - writer.uint32(/* id 9, wireType 2 =*/74).bytes(message.CodeMetadata); - return writer; - }; + /** + * Encodes the specified UserAccountData message. Does not implicitly {@link UserAccountData.verify|verify} messages. + * @function encode + * @memberof UserAccountData + * @static + * @param {IUserAccountData} message UserAccountData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + UserAccountData.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.Nonce != null && Object.hasOwnProperty.call(message, "Nonce")) + writer.uint32(/* id 1, wireType 0 =*/8).uint64(message.Nonce); + if (message.Balance != null && Object.hasOwnProperty.call(message, "Balance")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.Balance); + if (message.CodeHash != null && Object.hasOwnProperty.call(message, "CodeHash")) + writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.CodeHash); + if (message.RootHash != null && Object.hasOwnProperty.call(message, "RootHash")) + writer.uint32(/* id 4, wireType 2 =*/34).bytes(message.RootHash); + if (message.Address != null && Object.hasOwnProperty.call(message, "Address")) + writer.uint32(/* id 5, wireType 2 =*/42).bytes(message.Address); + if (message.DeveloperReward != null && Object.hasOwnProperty.call(message, "DeveloperReward")) + writer.uint32(/* id 6, wireType 2 =*/50).bytes(message.DeveloperReward); + if (message.OwnerAddress != null && Object.hasOwnProperty.call(message, "OwnerAddress")) + writer.uint32(/* id 7, wireType 2 =*/58).bytes(message.OwnerAddress); + if (message.UserName != null && Object.hasOwnProperty.call(message, "UserName")) + writer.uint32(/* id 8, wireType 2 =*/66).bytes(message.UserName); + if (message.CodeMetadata != null && Object.hasOwnProperty.call(message, "CodeMetadata")) + writer.uint32(/* id 9, wireType 2 =*/74).bytes(message.CodeMetadata); + return writer; + }; - /** - * Encodes the specified UserAccountData message, length delimited. Does not implicitly {@link UserAccountData.verify|verify} messages. - * @function encodeDelimited - * @memberof UserAccountData - * @static - * @param {IUserAccountData} message UserAccountData message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - UserAccountData.encodeDelimited = function encodeDelimited(message, writer) { - return this.encode(message, writer).ldelim(); - }; + /** + * Encodes the specified UserAccountData message, length delimited. Does not implicitly {@link UserAccountData.verify|verify} messages. + * @function encodeDelimited + * @memberof UserAccountData + * @static + * @param {IUserAccountData} message UserAccountData message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + UserAccountData.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; - /** - * Decodes a UserAccountData message from the specified reader or buffer. - * @function decode - * @memberof UserAccountData - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @param {number} [length] Message length if known beforehand - * @returns {UserAccountData} UserAccountData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - UserAccountData.decode = function decode(reader, length, error) { - if (!(reader instanceof $Reader)) - reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.UserAccountData(); - while (reader.pos < end) { - var tag = reader.uint32(); - if (tag === error) - break; - switch (tag >>> 3) { - case 1: { - message.Nonce = reader.uint64(); - break; - } - case 2: { - message.Balance = reader.bytes(); - break; - } - case 3: { - message.CodeHash = reader.bytes(); - break; - } - case 4: { - message.RootHash = reader.bytes(); - break; - } - case 5: { - message.Address = reader.bytes(); - break; - } - case 6: { - message.DeveloperReward = reader.bytes(); - break; - } - case 7: { - message.OwnerAddress = reader.bytes(); - break; - } - case 8: { - message.UserName = reader.bytes(); - break; - } - case 9: { - message.CodeMetadata = reader.bytes(); - break; - } - default: - reader.skipType(tag & 7); - break; - } + /** + * Decodes a UserAccountData message from the specified reader or buffer. + * @function decode + * @memberof UserAccountData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {UserAccountData} UserAccountData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + UserAccountData.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.UserAccountData(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.Nonce = reader.uint64(); + break; + } + case 2: { + message.Balance = reader.bytes(); + break; + } + case 3: { + message.CodeHash = reader.bytes(); + break; + } + case 4: { + message.RootHash = reader.bytes(); + break; + } + case 5: { + message.Address = reader.bytes(); + break; + } + case 6: { + message.DeveloperReward = reader.bytes(); + break; + } + case 7: { + message.OwnerAddress = reader.bytes(); + break; } - return message; - }; + case 8: { + message.UserName = reader.bytes(); + break; + } + case 9: { + message.CodeMetadata = reader.bytes(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; - /** - * Decodes a UserAccountData message from the specified reader or buffer, length delimited. - * @function decodeDelimited - * @memberof UserAccountData - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {UserAccountData} UserAccountData - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - UserAccountData.decodeDelimited = function decodeDelimited(reader) { - if (!(reader instanceof $Reader)) - reader = new $Reader(reader); - return this.decode(reader, reader.uint32()); - }; + /** + * Decodes a UserAccountData message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof UserAccountData + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {UserAccountData} UserAccountData + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + UserAccountData.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; - /** - * Verifies a UserAccountData message. - * @function verify - * @memberof UserAccountData - * @static - * @param {Object.} message Plain object to verify - * @returns {string|null} `null` if valid, otherwise the reason why it is not - */ - UserAccountData.verify = function verify(message) { - if (typeof message !== "object" || message === null) - return "object expected"; - if (message.Nonce != null && message.hasOwnProperty("Nonce")) - if (!$util.isInteger(message.Nonce) && !(message.Nonce && $util.isInteger(message.Nonce.low) && $util.isInteger(message.Nonce.high))) - return "Nonce: integer|Long expected"; - if (message.Balance != null && message.hasOwnProperty("Balance")) - if (!(message.Balance && typeof message.Balance.length === "number" || $util.isString(message.Balance))) - return "Balance: buffer expected"; - if (message.CodeHash != null && message.hasOwnProperty("CodeHash")) - if (!(message.CodeHash && typeof message.CodeHash.length === "number" || $util.isString(message.CodeHash))) - return "CodeHash: buffer expected"; - if (message.RootHash != null && message.hasOwnProperty("RootHash")) - if (!(message.RootHash && typeof message.RootHash.length === "number" || $util.isString(message.RootHash))) - return "RootHash: buffer expected"; - if (message.Address != null && message.hasOwnProperty("Address")) - if (!(message.Address && typeof message.Address.length === "number" || $util.isString(message.Address))) - return "Address: buffer expected"; - if (message.DeveloperReward != null && message.hasOwnProperty("DeveloperReward")) - if (!(message.DeveloperReward && typeof message.DeveloperReward.length === "number" || $util.isString(message.DeveloperReward))) - return "DeveloperReward: buffer expected"; - if (message.OwnerAddress != null && message.hasOwnProperty("OwnerAddress")) - if (!(message.OwnerAddress && typeof message.OwnerAddress.length === "number" || $util.isString(message.OwnerAddress))) - return "OwnerAddress: buffer expected"; - if (message.UserName != null && message.hasOwnProperty("UserName")) - if (!(message.UserName && typeof message.UserName.length === "number" || $util.isString(message.UserName))) - return "UserName: buffer expected"; - if (message.CodeMetadata != null && message.hasOwnProperty("CodeMetadata")) - if (!(message.CodeMetadata && typeof message.CodeMetadata.length === "number" || $util.isString(message.CodeMetadata))) - return "CodeMetadata: buffer expected"; - return null; - }; + /** + * Verifies a UserAccountData message. + * @function verify + * @memberof UserAccountData + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + UserAccountData.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.Nonce != null && message.hasOwnProperty("Nonce")) + if (!$util.isInteger(message.Nonce) && !(message.Nonce && $util.isInteger(message.Nonce.low) && $util.isInteger(message.Nonce.high))) + return "Nonce: integer|Long expected"; + if (message.Balance != null && message.hasOwnProperty("Balance")) + if (!(message.Balance && typeof message.Balance.length === "number" || $util.isString(message.Balance))) + return "Balance: buffer expected"; + if (message.CodeHash != null && message.hasOwnProperty("CodeHash")) + if (!(message.CodeHash && typeof message.CodeHash.length === "number" || $util.isString(message.CodeHash))) + return "CodeHash: buffer expected"; + if (message.RootHash != null && message.hasOwnProperty("RootHash")) + if (!(message.RootHash && typeof message.RootHash.length === "number" || $util.isString(message.RootHash))) + return "RootHash: buffer expected"; + if (message.Address != null && message.hasOwnProperty("Address")) + if (!(message.Address && typeof message.Address.length === "number" || $util.isString(message.Address))) + return "Address: buffer expected"; + if (message.DeveloperReward != null && message.hasOwnProperty("DeveloperReward")) + if (!(message.DeveloperReward && typeof message.DeveloperReward.length === "number" || $util.isString(message.DeveloperReward))) + return "DeveloperReward: buffer expected"; + if (message.OwnerAddress != null && message.hasOwnProperty("OwnerAddress")) + if (!(message.OwnerAddress && typeof message.OwnerAddress.length === "number" || $util.isString(message.OwnerAddress))) + return "OwnerAddress: buffer expected"; + if (message.UserName != null && message.hasOwnProperty("UserName")) + if (!(message.UserName && typeof message.UserName.length === "number" || $util.isString(message.UserName))) + return "UserName: buffer expected"; + if (message.CodeMetadata != null && message.hasOwnProperty("CodeMetadata")) + if (!(message.CodeMetadata && typeof message.CodeMetadata.length === "number" || $util.isString(message.CodeMetadata))) + return "CodeMetadata: buffer expected"; + return null; + }; - /** - * Creates a UserAccountData message from a plain object. Also converts values to their respective internal types. - * @function fromObject - * @memberof UserAccountData - * @static - * @param {Object.} object Plain object - * @returns {UserAccountData} UserAccountData - */ - UserAccountData.fromObject = function fromObject(object) { - if (object instanceof $root.UserAccountData) - return object; - var message = new $root.UserAccountData(); - if (object.Nonce != null) - if ($util.Long) - (message.Nonce = $util.Long.fromValue(object.Nonce)).unsigned = true; - else if (typeof object.Nonce === "string") - message.Nonce = parseInt(object.Nonce, 10); - else if (typeof object.Nonce === "number") - message.Nonce = object.Nonce; - else if (typeof object.Nonce === "object") - message.Nonce = new $util.LongBits(object.Nonce.low >>> 0, object.Nonce.high >>> 0).toNumber(true); - if (object.Balance != null) - if (typeof object.Balance === "string") - $util.base64.decode(object.Balance, message.Balance = $util.newBuffer($util.base64.length(object.Balance)), 0); - else if (object.Balance.length >= 0) - message.Balance = object.Balance; - if (object.CodeHash != null) - if (typeof object.CodeHash === "string") - $util.base64.decode(object.CodeHash, message.CodeHash = $util.newBuffer($util.base64.length(object.CodeHash)), 0); - else if (object.CodeHash.length >= 0) - message.CodeHash = object.CodeHash; - if (object.RootHash != null) - if (typeof object.RootHash === "string") - $util.base64.decode(object.RootHash, message.RootHash = $util.newBuffer($util.base64.length(object.RootHash)), 0); - else if (object.RootHash.length >= 0) - message.RootHash = object.RootHash; - if (object.Address != null) - if (typeof object.Address === "string") - $util.base64.decode(object.Address, message.Address = $util.newBuffer($util.base64.length(object.Address)), 0); - else if (object.Address.length >= 0) - message.Address = object.Address; - if (object.DeveloperReward != null) - if (typeof object.DeveloperReward === "string") - $util.base64.decode(object.DeveloperReward, message.DeveloperReward = $util.newBuffer($util.base64.length(object.DeveloperReward)), 0); - else if (object.DeveloperReward.length >= 0) - message.DeveloperReward = object.DeveloperReward; - if (object.OwnerAddress != null) - if (typeof object.OwnerAddress === "string") - $util.base64.decode(object.OwnerAddress, message.OwnerAddress = $util.newBuffer($util.base64.length(object.OwnerAddress)), 0); - else if (object.OwnerAddress.length >= 0) - message.OwnerAddress = object.OwnerAddress; - if (object.UserName != null) - if (typeof object.UserName === "string") - $util.base64.decode(object.UserName, message.UserName = $util.newBuffer($util.base64.length(object.UserName)), 0); - else if (object.UserName.length >= 0) - message.UserName = object.UserName; - if (object.CodeMetadata != null) - if (typeof object.CodeMetadata === "string") - $util.base64.decode(object.CodeMetadata, message.CodeMetadata = $util.newBuffer($util.base64.length(object.CodeMetadata)), 0); - else if (object.CodeMetadata.length >= 0) - message.CodeMetadata = object.CodeMetadata; - return message; - }; + /** + * Creates a UserAccountData message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof UserAccountData + * @static + * @param {Object.} object Plain object + * @returns {UserAccountData} UserAccountData + */ + UserAccountData.fromObject = function fromObject(object) { + if (object instanceof $root.UserAccountData) + return object; + var message = new $root.UserAccountData(); + if (object.Nonce != null) + if ($util.Long) + (message.Nonce = $util.Long.fromValue(object.Nonce)).unsigned = true; + else if (typeof object.Nonce === "string") + message.Nonce = parseInt(object.Nonce, 10); + else if (typeof object.Nonce === "number") + message.Nonce = object.Nonce; + else if (typeof object.Nonce === "object") + message.Nonce = new $util.LongBits(object.Nonce.low >>> 0, object.Nonce.high >>> 0).toNumber(true); + if (object.Balance != null) + if (typeof object.Balance === "string") + $util.base64.decode(object.Balance, message.Balance = $util.newBuffer($util.base64.length(object.Balance)), 0); + else if (object.Balance.length >= 0) + message.Balance = object.Balance; + if (object.CodeHash != null) + if (typeof object.CodeHash === "string") + $util.base64.decode(object.CodeHash, message.CodeHash = $util.newBuffer($util.base64.length(object.CodeHash)), 0); + else if (object.CodeHash.length >= 0) + message.CodeHash = object.CodeHash; + if (object.RootHash != null) + if (typeof object.RootHash === "string") + $util.base64.decode(object.RootHash, message.RootHash = $util.newBuffer($util.base64.length(object.RootHash)), 0); + else if (object.RootHash.length >= 0) + message.RootHash = object.RootHash; + if (object.Address != null) + if (typeof object.Address === "string") + $util.base64.decode(object.Address, message.Address = $util.newBuffer($util.base64.length(object.Address)), 0); + else if (object.Address.length >= 0) + message.Address = object.Address; + if (object.DeveloperReward != null) + if (typeof object.DeveloperReward === "string") + $util.base64.decode(object.DeveloperReward, message.DeveloperReward = $util.newBuffer($util.base64.length(object.DeveloperReward)), 0); + else if (object.DeveloperReward.length >= 0) + message.DeveloperReward = object.DeveloperReward; + if (object.OwnerAddress != null) + if (typeof object.OwnerAddress === "string") + $util.base64.decode(object.OwnerAddress, message.OwnerAddress = $util.newBuffer($util.base64.length(object.OwnerAddress)), 0); + else if (object.OwnerAddress.length >= 0) + message.OwnerAddress = object.OwnerAddress; + if (object.UserName != null) + if (typeof object.UserName === "string") + $util.base64.decode(object.UserName, message.UserName = $util.newBuffer($util.base64.length(object.UserName)), 0); + else if (object.UserName.length >= 0) + message.UserName = object.UserName; + if (object.CodeMetadata != null) + if (typeof object.CodeMetadata === "string") + $util.base64.decode(object.CodeMetadata, message.CodeMetadata = $util.newBuffer($util.base64.length(object.CodeMetadata)), 0); + else if (object.CodeMetadata.length >= 0) + message.CodeMetadata = object.CodeMetadata; + return message; + }; - /** - * Creates a plain object from a UserAccountData message. Also converts values to other types if specified. - * @function toObject - * @memberof UserAccountData - * @static - * @param {UserAccountData} message UserAccountData - * @param {$protobuf.IConversionOptions} [options] Conversion options - * @returns {Object.} Plain object - */ - UserAccountData.toObject = function toObject(message, options) { - if (!options) - options = {}; - var object = {}; - if (options.defaults) { - if ($util.Long) { - var long = new $util.Long(0, 0, true); - object.Nonce = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; - } else - object.Nonce = options.longs === String ? "0" : 0; - if (options.bytes === String) - object.Balance = ""; - else { - object.Balance = []; - if (options.bytes !== Array) - object.Balance = $util.newBuffer(object.Balance); - } - if (options.bytes === String) - object.CodeHash = ""; - else { - object.CodeHash = []; - if (options.bytes !== Array) - object.CodeHash = $util.newBuffer(object.CodeHash); - } - if (options.bytes === String) - object.RootHash = ""; - else { - object.RootHash = []; - if (options.bytes !== Array) - object.RootHash = $util.newBuffer(object.RootHash); - } - if (options.bytes === String) - object.Address = ""; - else { - object.Address = []; - if (options.bytes !== Array) - object.Address = $util.newBuffer(object.Address); - } - if (options.bytes === String) - object.DeveloperReward = ""; - else { - object.DeveloperReward = []; - if (options.bytes !== Array) - object.DeveloperReward = $util.newBuffer(object.DeveloperReward); - } - if (options.bytes === String) - object.OwnerAddress = ""; - else { - object.OwnerAddress = []; - if (options.bytes !== Array) - object.OwnerAddress = $util.newBuffer(object.OwnerAddress); - } - if (options.bytes === String) - object.UserName = ""; - else { - object.UserName = []; - if (options.bytes !== Array) - object.UserName = $util.newBuffer(object.UserName); - } - if (options.bytes === String) - object.CodeMetadata = ""; - else { - object.CodeMetadata = []; - if (options.bytes !== Array) - object.CodeMetadata = $util.newBuffer(object.CodeMetadata); - } - } - if (message.Nonce != null && message.hasOwnProperty("Nonce")) - if (typeof message.Nonce === "number") - object.Nonce = options.longs === String ? String(message.Nonce) : message.Nonce; - else - object.Nonce = options.longs === String ? $util.Long.prototype.toString.call(message.Nonce) : options.longs === Number ? new $util.LongBits(message.Nonce.low >>> 0, message.Nonce.high >>> 0).toNumber(true) : message.Nonce; - if (message.Balance != null && message.hasOwnProperty("Balance")) - object.Balance = options.bytes === String ? $util.base64.encode(message.Balance, 0, message.Balance.length) : options.bytes === Array ? Array.prototype.slice.call(message.Balance) : message.Balance; - if (message.CodeHash != null && message.hasOwnProperty("CodeHash")) - object.CodeHash = options.bytes === String ? $util.base64.encode(message.CodeHash, 0, message.CodeHash.length) : options.bytes === Array ? Array.prototype.slice.call(message.CodeHash) : message.CodeHash; - if (message.RootHash != null && message.hasOwnProperty("RootHash")) - object.RootHash = options.bytes === String ? $util.base64.encode(message.RootHash, 0, message.RootHash.length) : options.bytes === Array ? Array.prototype.slice.call(message.RootHash) : message.RootHash; - if (message.Address != null && message.hasOwnProperty("Address")) - object.Address = options.bytes === String ? $util.base64.encode(message.Address, 0, message.Address.length) : options.bytes === Array ? Array.prototype.slice.call(message.Address) : message.Address; - if (message.DeveloperReward != null && message.hasOwnProperty("DeveloperReward")) - object.DeveloperReward = options.bytes === String ? $util.base64.encode(message.DeveloperReward, 0, message.DeveloperReward.length) : options.bytes === Array ? Array.prototype.slice.call(message.DeveloperReward) : message.DeveloperReward; - if (message.OwnerAddress != null && message.hasOwnProperty("OwnerAddress")) - object.OwnerAddress = options.bytes === String ? $util.base64.encode(message.OwnerAddress, 0, message.OwnerAddress.length) : options.bytes === Array ? Array.prototype.slice.call(message.OwnerAddress) : message.OwnerAddress; - if (message.UserName != null && message.hasOwnProperty("UserName")) - object.UserName = options.bytes === String ? $util.base64.encode(message.UserName, 0, message.UserName.length) : options.bytes === Array ? Array.prototype.slice.call(message.UserName) : message.UserName; - if (message.CodeMetadata != null && message.hasOwnProperty("CodeMetadata")) - object.CodeMetadata = options.bytes === String ? $util.base64.encode(message.CodeMetadata, 0, message.CodeMetadata.length) : options.bytes === Array ? Array.prototype.slice.call(message.CodeMetadata) : message.CodeMetadata; - return object; - }; + /** + * Creates a plain object from a UserAccountData message. Also converts values to other types if specified. + * @function toObject + * @memberof UserAccountData + * @static + * @param {UserAccountData} message UserAccountData + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + UserAccountData.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + if ($util.Long) { + var long = new $util.Long(0, 0, true); + object.Nonce = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; + } else + object.Nonce = options.longs === String ? "0" : 0; + if (options.bytes === String) + object.Balance = ""; + else { + object.Balance = []; + if (options.bytes !== Array) + object.Balance = $util.newBuffer(object.Balance); + } + if (options.bytes === String) + object.CodeHash = ""; + else { + object.CodeHash = []; + if (options.bytes !== Array) + object.CodeHash = $util.newBuffer(object.CodeHash); + } + if (options.bytes === String) + object.RootHash = ""; + else { + object.RootHash = []; + if (options.bytes !== Array) + object.RootHash = $util.newBuffer(object.RootHash); + } + if (options.bytes === String) + object.Address = ""; + else { + object.Address = []; + if (options.bytes !== Array) + object.Address = $util.newBuffer(object.Address); + } + if (options.bytes === String) + object.DeveloperReward = ""; + else { + object.DeveloperReward = []; + if (options.bytes !== Array) + object.DeveloperReward = $util.newBuffer(object.DeveloperReward); + } + if (options.bytes === String) + object.OwnerAddress = ""; + else { + object.OwnerAddress = []; + if (options.bytes !== Array) + object.OwnerAddress = $util.newBuffer(object.OwnerAddress); + } + if (options.bytes === String) + object.UserName = ""; + else { + object.UserName = []; + if (options.bytes !== Array) + object.UserName = $util.newBuffer(object.UserName); + } + if (options.bytes === String) + object.CodeMetadata = ""; + else { + object.CodeMetadata = []; + if (options.bytes !== Array) + object.CodeMetadata = $util.newBuffer(object.CodeMetadata); + } + } + if (message.Nonce != null && message.hasOwnProperty("Nonce")) + if (typeof message.Nonce === "number") + object.Nonce = options.longs === String ? String(message.Nonce) : message.Nonce; + else + object.Nonce = options.longs === String ? $util.Long.prototype.toString.call(message.Nonce) : options.longs === Number ? new $util.LongBits(message.Nonce.low >>> 0, message.Nonce.high >>> 0).toNumber(true) : message.Nonce; + if (message.Balance != null && message.hasOwnProperty("Balance")) + object.Balance = options.bytes === String ? $util.base64.encode(message.Balance, 0, message.Balance.length) : options.bytes === Array ? Array.prototype.slice.call(message.Balance) : message.Balance; + if (message.CodeHash != null && message.hasOwnProperty("CodeHash")) + object.CodeHash = options.bytes === String ? $util.base64.encode(message.CodeHash, 0, message.CodeHash.length) : options.bytes === Array ? Array.prototype.slice.call(message.CodeHash) : message.CodeHash; + if (message.RootHash != null && message.hasOwnProperty("RootHash")) + object.RootHash = options.bytes === String ? $util.base64.encode(message.RootHash, 0, message.RootHash.length) : options.bytes === Array ? Array.prototype.slice.call(message.RootHash) : message.RootHash; + if (message.Address != null && message.hasOwnProperty("Address")) + object.Address = options.bytes === String ? $util.base64.encode(message.Address, 0, message.Address.length) : options.bytes === Array ? Array.prototype.slice.call(message.Address) : message.Address; + if (message.DeveloperReward != null && message.hasOwnProperty("DeveloperReward")) + object.DeveloperReward = options.bytes === String ? $util.base64.encode(message.DeveloperReward, 0, message.DeveloperReward.length) : options.bytes === Array ? Array.prototype.slice.call(message.DeveloperReward) : message.DeveloperReward; + if (message.OwnerAddress != null && message.hasOwnProperty("OwnerAddress")) + object.OwnerAddress = options.bytes === String ? $util.base64.encode(message.OwnerAddress, 0, message.OwnerAddress.length) : options.bytes === Array ? Array.prototype.slice.call(message.OwnerAddress) : message.OwnerAddress; + if (message.UserName != null && message.hasOwnProperty("UserName")) + object.UserName = options.bytes === String ? $util.base64.encode(message.UserName, 0, message.UserName.length) : options.bytes === Array ? Array.prototype.slice.call(message.UserName) : message.UserName; + if (message.CodeMetadata != null && message.hasOwnProperty("CodeMetadata")) + object.CodeMetadata = options.bytes === String ? $util.base64.encode(message.CodeMetadata, 0, message.CodeMetadata.length) : options.bytes === Array ? Array.prototype.slice.call(message.CodeMetadata) : message.CodeMetadata; + return object; + }; - /** - * Converts this UserAccountData to JSON. - * @function toJSON - * @memberof UserAccountData - * @instance - * @returns {Object.} JSON object - */ - UserAccountData.prototype.toJSON = function toJSON() { - return this.constructor.toObject(this, $protobuf.util.toJSONOptions); - }; + /** + * Converts this UserAccountData to JSON. + * @function toJSON + * @memberof UserAccountData + * @instance + * @returns {Object.} JSON object + */ + UserAccountData.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; - /** - * Gets the default type url for UserAccountData - * @function getTypeUrl - * @memberof UserAccountData - * @static - * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") - * @returns {string} The default type url - */ - UserAccountData.getTypeUrl = function getTypeUrl(typeUrlPrefix) { - if (typeUrlPrefix === undefined) { - typeUrlPrefix = "type.googleapis.com"; - } - return typeUrlPrefix + "/UserAccountData"; - }; + /** + * Gets the default type url for UserAccountData + * @function getTypeUrl + * @memberof UserAccountData + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + UserAccountData.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/UserAccountData"; + }; - return UserAccountData; + return UserAccountData; })(); module.exports = $root; From 91ea6a0aecb27bdf8647df7352eeff6f3d611d19 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 4 Nov 2025 15:21:57 +0200 Subject: [PATCH 74/90] fixes after review --- .../accounts-v2/account.controller.v2.ts | 7 +- .../accounts-v2/account.module.v2.ts | 3 + .../accounts-v2/account.service.v2.ts | 188 +----------------- .../accounts-v2/entities/account.detailed.ts | 74 ------- .../entities/account.fetch.options.ts | 11 - .../accounts-v2/entities/account.sort.ts | 23 --- src/endpoints/accounts-v2/entities/account.ts | 54 ----- 7 files changed, 16 insertions(+), 344 deletions(-) delete mode 100644 src/endpoints/accounts-v2/entities/account.detailed.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.fetch.options.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.sort.ts delete mode 100644 src/endpoints/accounts-v2/entities/account.ts diff --git a/src/endpoints/accounts-v2/account.controller.v2.ts b/src/endpoints/accounts-v2/account.controller.v2.ts index 79a61b277..34d4d4a93 100644 --- a/src/endpoints/accounts-v2/account.controller.v2.ts +++ b/src/endpoints/accounts-v2/account.controller.v2.ts @@ -1,11 +1,11 @@ import { Controller, Get, NotFoundException, Param, Query, UseInterceptors } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { AccountServiceV2 } from './account.service.v2'; -import { AccountDetailed } from './entities/account.detailed'; -import { ParseAddressPipe, ParseBoolPipe, ParseIntPipe } from '@multiversx/sdk-nestjs-common'; +import { ParseAddressPipe, ParseBoolPipe } from '@multiversx/sdk-nestjs-common'; import { DeepHistoryInterceptor } from 'src/interceptors/deep-history.interceptor'; -import { AccountFetchOptions } from './entities/account.fetch.options'; import { NoCache } from '@multiversx/sdk-nestjs-cache'; +import { AccountDetailed } from '../accounts/entities/account.detailed'; +import { AccountFetchOptions } from '../accounts/entities/account.fetch.options'; @Controller('') @ApiTags('accounts') @@ -33,7 +33,6 @@ export class AccountControllerV2 { @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.accountServiceV2.getAccount( address, diff --git a/src/endpoints/accounts-v2/account.module.v2.ts b/src/endpoints/accounts-v2/account.module.v2.ts index 4fd5545fe..63d56af21 100644 --- a/src/endpoints/accounts-v2/account.module.v2.ts +++ b/src/endpoints/accounts-v2/account.module.v2.ts @@ -16,6 +16,7 @@ import { AccountServiceV2 } from "./account.service.v2"; import { ProviderModule } from "../providers/provider.module"; import { KeysModule } from "../keys/keys.module"; import { MongoDbModule } from "src/common/indexer/db"; +import { AccountService } from "../accounts/account.service"; @Module({ imports: [ @@ -37,9 +38,11 @@ import { MongoDbModule } from "src/common/indexer/db"; MongoDbModule, ], providers: [ + AccountService, AccountServiceV2, ], exports: [ + AccountService, AccountServiceV2, ], }) diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts index c1b89ec49..3935bd822 100644 --- a/src/endpoints/accounts-v2/account.service.v2.ts +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -1,24 +1,18 @@ -import { forwardRef, HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { AccountDetailed } from './entities/account.detailed'; -import { ApiConfigService } from 'src/common/api-config/api.config.service'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { PluginService } from 'src/common/plugins/plugin.service'; -import { TransferService } from '../transfers/transfer.service'; -import { TransactionType } from '../transactions/entities/transaction.type'; import { AssetsService } from 'src/common/assets/assets.service'; -import { TransactionFilter } from '../transactions/entities/transaction.filter'; import { CacheService } from "@multiversx/sdk-nestjs-cache"; import { AddressUtils, OriginLogger } from '@multiversx/sdk-nestjs-common'; -import { ApiService } from "@multiversx/sdk-nestjs-http"; -import { GatewayService } from 'src/common/gateway/gateway.service'; import { IndexerService } from "src/common/indexer/indexer.service"; import { CacheInfo } from 'src/utils/cache.info'; import { UsernameService } from '../usernames/username.service'; -import { ProtocolService } from 'src/common/protocol/protocol.service'; import { ProviderService } from '../providers/provider.service'; -import { AccountFetchOptions } from './entities/account.fetch.options'; import { Provider } from '../providers/entities/provider'; import { AccountDetailsRepository } from 'src/common/indexer/db'; import { StateChangesConsumerService } from 'src/state-changes/state.changes.consumer.service'; +import { AccountService } from '../accounts/account.service'; +import { AccountDetailed } from '../accounts/entities/account.detailed'; +import { AccountFetchOptions } from '../accounts/entities/account.fetch.options'; @Injectable() export class AccountServiceV2 { @@ -26,20 +20,15 @@ export class AccountServiceV2 { constructor( private readonly indexerService: IndexerService, - private readonly gatewayService: GatewayService, private readonly cachingService: CacheService, - private readonly apiConfigService: ApiConfigService, @Inject(forwardRef(() => PluginService)) private readonly pluginService: PluginService, - @Inject(forwardRef(() => TransferService)) - private readonly transferService: TransferService, private readonly assetsService: AssetsService, private readonly usernameService: UsernameService, - private readonly apiService: ApiService, - private readonly protocolService: ProtocolService, @Inject(forwardRef(() => ProviderService)) private readonly providerService: ProviderService, private readonly accountDetailsDepository: AccountDetailsRepository, + private readonly accountServiceV1: AccountService, ) { } private async getAccountWithFallBack(address: string, options?: AccountFetchOptions): Promise { @@ -50,7 +39,7 @@ export class AccountServiceV2 { } if (!accountFromDb) { // Second use the legacy method - return await this.getAccountRaw(address, options?.withAssets); + return await this.accountServiceV1.getAccountRaw(address, options?.withAssets); } if (options && options.withAssets === true) { const assets = await this.assetsService.getAllAccountAssets(); @@ -82,7 +71,7 @@ export class AccountServiceV2 { account.username = await this.usernameService.getUsernameForAddress(address) ?? undefined; } } else { - account = await this.getAccountRaw(address, options?.withAssets); + account = await this.accountServiceV1.getAccountRaw(address, options?.withAssets); } if (!account) { @@ -90,15 +79,15 @@ export class AccountServiceV2 { } if (options?.withTxCount === true) { - account.txCount = await this.getAccountTxCount(address); + account.txCount = await this.accountServiceV1.getAccountTxCount(address); } if (options?.withScrCount === true) { - account.scrCount = await this.getAccountScResults(address); + account.scrCount = await this.accountServiceV1.getAccountScResults(address); } if (options?.withGuardianInfo === true) { - await this.applyGuardianInfo(account); + await this.accountServiceV1.applyGuardianInfo(account); } if (options?.withTimestamp) { @@ -127,161 +116,4 @@ export class AccountServiceV2 { return account; } - - async applyGuardianInfo(account: AccountDetailed): Promise { - try { - const guardianResult = await this.gatewayService.getGuardianData(account.address); - const guardianData = guardianResult?.guardianData; - if (guardianData) { - const activeGuardian = guardianData.activeGuardian; - if (activeGuardian) { - account.activeGuardianActivationEpoch = activeGuardian.activationEpoch; - account.activeGuardianAddress = activeGuardian.address; - account.activeGuardianServiceUid = activeGuardian.serviceUID; - } - - const pendingGuardian = guardianData.pendingGuardian; - if (pendingGuardian) { - account.pendingGuardianActivationEpoch = pendingGuardian.activationEpoch; - account.pendingGuardianAddress = pendingGuardian.address; - account.pendingGuardianServiceUid = pendingGuardian.serviceUID; - } - - account.isGuarded = guardianData.guarded; - } - } catch (error) { - this.logger.error(`Error when getting guardian data for address '${account.address}'`); - this.logger.error(error); - } - } - - async getAccountRaw(address: string, withAssets?: boolean): Promise { - try { - const { - account: { nonce, balance, code, codeHash, rootHash, developerReward, ownerAddress, codeMetadata }, - } = await this.gatewayService.getAddressDetails(address); - - const shardCount = await this.protocolService.getShardCount(); - const shard = AddressUtils.computeShard(AddressUtils.bech32Decode(address), shardCount); - 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]; - } - - if (AddressUtils.isSmartContractAddress(address) && account.code) { - const codeAttributes = AddressUtils.decodeCodeMetadata(codeMetadata); - if (codeAttributes) { - account = { ...account, ...codeAttributes }; - } - - const deployTxHash = await this.getAccountDeployedTxHash(address); - if (deployTxHash) { - account.deployTxHash = deployTxHash; - } - - const deployedAt = await this.getAccountDeployedAt(address); - if (deployedAt) { - account.deployedAt = deployedAt; - } - - const isVerified = await this.getAccountIsVerified(address, account.codeHash); - if (isVerified) { - account.isVerified = isVerified; - } - } - - if (!AddressUtils.isSmartContractAddress(address)) { - account.username = await this.usernameService.getUsernameForAddress(address) ?? undefined; - account.isPayableBySmartContract = undefined; - account.isUpgradeable = undefined; - account.isReadable = undefined; - account.isPayable = undefined; - } - - await this.pluginService.processAccount(account); - return account; - } catch (error) { - this.logger.error(error); - this.logger.error(`Error when getting account details for address '${address}'`); - return null; - } - } - - async getAccountTxCount(address: string): Promise { - return await this.transferService.getTransfersCount(new TransactionFilter({ address, type: TransactionType.Transaction })); - } - - async getAccountScResults(address: string): Promise { - return await this.transferService.getTransfersCount(new TransactionFilter({ address, type: TransactionType.SmartContractResult })); - } - - async getAccountDeployedAt(address: string): Promise { - return await this.cachingService.getOrSet( - CacheInfo.AccountDeployedAt(address).key, - async () => await this.getAccountDeployedAtRaw(address), - CacheInfo.AccountDeployedAt(address).ttl - ); - } - - async getAccountDeployedAtRaw(address: string): Promise { - const scDeploy = await this.indexerService.getScDeploy(address); - if (!scDeploy) { - return null; - } - - const txHash = scDeploy.deployTxHash; - if (!txHash) { - return null; - } - - const transaction = await this.indexerService.getTransaction(txHash); - if (!transaction) { - return null; - } - - return transaction.timestamp; - } - - async getAccountDeployedTxHash(address: string): Promise { - return await this.cachingService.getOrSet( - CacheInfo.AccountDeployTxHash(address).key, - async () => await this.getAccountDeployedTxHashRaw(address), - CacheInfo.AccountDeployTxHash(address).ttl, - ); - } - - async getAccountDeployedTxHashRaw(address: string): Promise { - const scDeploy = await this.indexerService.getScDeploy(address); - if (!scDeploy) { - return null; - } - - return scDeploy.deployTxHash; - } - - async getAccountIsVerified(address: string, codeHash: string): Promise { - return await this.cachingService.getOrSet( - CacheInfo.AccountIsVerified(address).key, - async () => await this.getAccountIsVerifiedRaw(address, codeHash), - CacheInfo.AccountIsVerified(address).ttl - ); - } - - async getAccountIsVerifiedRaw(address: string, codeHash: string): Promise { - try { - // eslint-disable-next-line require-await - const { data } = await this.apiService.get(`${this.apiConfigService.getVerifierUrl()}/verifier/${address}/codehash`, undefined, async (error) => error.response?.status === HttpStatus.NOT_FOUND); - - if (data.codeHash === Buffer.from(codeHash, 'base64').toString('hex')) { - return true; - } - } catch { - // ignore - } - - return null; - } } diff --git a/src/endpoints/accounts-v2/entities/account.detailed.ts b/src/endpoints/accounts-v2/entities/account.detailed.ts deleted file mode 100644 index 2c9f131ec..000000000 --- a/src/endpoints/accounts-v2/entities/account.detailed.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ComplexityEstimation } from "@multiversx/sdk-nestjs-common"; -import { ApiProperty } from "@nestjs/swagger"; -import { ScamInfo } from "src/common/entities/scam-info.dto"; -import { NftCollectionAccount } from "src/endpoints/collections/entities/nft.collection.account"; -import { NftAccount } from "src/endpoints/nfts/entities/nft.account"; -import { Account } from "./account"; - -export class AccountDetailed extends Account { - constructor(init?: Partial) { - super(); - Object.assign(this, init); - } - - @ApiProperty({ description: 'The source code in hex format', required: false }) - code: string = ''; - - @ApiProperty({ description: 'The hash of the source code', required: false }) - codeHash: string = ''; - - @ApiProperty({ description: 'The hash of the root node' }) - rootHash: string = ''; - - @ApiProperty({ description: 'The username specific for this account', nullable: true, required: false }) - username: string | undefined = undefined; - - @ApiProperty({ description: 'The developer reward' }) - developerReward: string = ''; - - @ApiProperty({ description: 'The address in bech 32 format of owner account', required: false }) - ownerAddress: string = ''; - - @ApiProperty({ description: 'Specific property flag for smart contract', type: Boolean, required: false }) - isUpgradeable?: boolean; - - @ApiProperty({ description: 'Specific property flag for smart contract', type: Boolean, required: false }) - isReadable?: boolean; - - @ApiProperty({ description: 'Specific property flag for smart contract', type: Boolean, required: false }) - isPayable?: boolean; - - @ApiProperty({ description: 'Specific property flag for smart contract', type: Boolean, nullable: true, required: false }) - isPayableBySmartContract?: boolean | undefined = undefined; - - @ApiProperty({ type: ScamInfo, nullable: true, required: false }) - scamInfo: ScamInfo | undefined = undefined; - - @ApiProperty({ description: 'Account nft collections', type: Boolean, nullable: true, required: false }) - nftCollections: NftCollectionAccount[] | undefined = undefined; - - @ApiProperty({ description: 'Account nfts', type: Boolean, nullable: true, required: false }) - @ComplexityEstimation({ group: 'nfts', value: 1000 }) - nfts: NftAccount[] | undefined = undefined; - - @ApiProperty({ type: Number, nullable: true, required: false }) - activeGuardianActivationEpoch?: number; - - @ApiProperty({ type: String, nullable: true, required: false }) - activeGuardianAddress?: string; - - @ApiProperty({ type: String, nullable: true, required: false }) - activeGuardianServiceUid?: string; - - @ApiProperty({ type: Number, nullable: true, required: false }) - pendingGuardianActivationEpoch?: number; - - @ApiProperty({ type: String, nullable: true, required: false }) - pendingGuardianAddress?: string; - - @ApiProperty({ type: String, nullable: true, required: false }) - pendingGuardianServiceUid?: string; - - @ApiProperty({ type: Boolean, nullable: true, required: false }) - isGuarded?: boolean; -} diff --git a/src/endpoints/accounts-v2/entities/account.fetch.options.ts b/src/endpoints/accounts-v2/entities/account.fetch.options.ts deleted file mode 100644 index 367686761..000000000 --- a/src/endpoints/accounts-v2/entities/account.fetch.options.ts +++ /dev/null @@ -1,11 +0,0 @@ -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-v2/entities/account.sort.ts b/src/endpoints/accounts-v2/entities/account.sort.ts deleted file mode 100644 index fbbdfddc4..000000000 --- a/src/endpoints/accounts-v2/entities/account.sort.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { registerEnumType } from "@nestjs/graphql"; - -export enum AccountSort { - balance = 'balance', - timestamp = 'timestamp', - transfersLast24h = 'transfersLast24h', -} - -registerEnumType(AccountSort, { - name: 'AccountSort', - description: 'Account Sort object.', - valuesMap: { - balance: { - description: 'Sort by balance.', - }, - timestamp: { - description: 'Sort by timestamp.', - }, - transfersLast24h: { - description: 'Sort by transfersLast24h.', - }, - }, -}); diff --git a/src/endpoints/accounts-v2/entities/account.ts b/src/endpoints/accounts-v2/entities/account.ts deleted file mode 100644 index 47ee0f63d..000000000 --- a/src/endpoints/accounts-v2/entities/account.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { SwaggerUtils } from "@multiversx/sdk-nestjs-common"; -import { ApiProperty } from "@nestjs/swagger"; -import { AccountAssets } from "src/common/assets/entities/account.assets"; - -export class Account { - constructor(init?: Partial) { - Object.assign(this, init); - } - - @ApiProperty({ type: String, description: 'Account bech32 address', example: 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz' }) - address: string = ''; - - @ApiProperty(SwaggerUtils.amountPropertyOptions({ description: 'Account current balance' })) - balance: string = ''; - - @ApiProperty({ type: Number, description: 'Account current nonce', example: 42 }) - nonce: number = 0; - - @ApiProperty({ type: Number, description: 'Timestamp in miliseconds of the block where the account was first indexed', example: 1676979360000 }) - timestampMs: number = 0; - - @ApiProperty({ type: Number, description: 'Timestamp in seconds of the block where the account was first indexed', example: 1676979360 }) - timestamp: number = 0; - - @ApiProperty({ type: Number, description: 'The shard ID allocated to the account', example: 0 }) - shard: number = 0; - - @ApiProperty({ type: String, description: 'Current owner address', required: false }) - ownerAddress: string | undefined = undefined; - - @ApiProperty({ type: AccountAssets, nullable: true, description: 'Account assets', required: false }) - assets: AccountAssets | undefined = undefined; - - @ApiProperty({ description: 'Specific property flag for smart contract', type: Number, required: false }) - deployedAt?: number | null; - - @ApiProperty({ description: 'The contract deploy transaction hash', required: false }) - deployTxHash?: string | null; - - @ApiProperty({ type: AccountAssets, nullable: true, description: 'Account assets', required: false }) - ownerAssets: AccountAssets | undefined = undefined; - - @ApiProperty({ description: 'Specific property flag for smart contract', type: Boolean, required: false }) - isVerified?: boolean; - - @ApiProperty({ description: 'The number of transactions performed on this account' }) - txCount?: number; - - @ApiProperty({ description: 'The number of smart contract results of this account' }) - scrCount?: number; - - @ApiProperty({ type: Number, description: 'Transfers in the last 24 hours', required: false }) - transfersLast24h: number | undefined = undefined; -} From a326e360785d641da033cd302a3427ea1526f7b2 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 4 Nov 2025 16:29:14 +0200 Subject: [PATCH 75/90] add configs + fixes --- .github/workflows/chain-simulator-e2e-tests.yml | 1 - config/config.devnet-old.yaml | 4 +++- config/config.e2e-mocked.mainnet.yaml | 4 +++- config/config.e2e.mainnet.yaml | 4 +++- config/config.mainnet.yaml | 7 ++++--- config/config.testnet.yaml | 4 +++- src/endpoints/accounts-v2/account.controller.v2.ts | 2 +- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/chain-simulator-e2e-tests.yml b/.github/workflows/chain-simulator-e2e-tests.yml index 6b64ef320..4d0131a48 100644 --- a/.github/workflows/chain-simulator-e2e-tests.yml +++ b/.github/workflows/chain-simulator-e2e-tests.yml @@ -50,7 +50,6 @@ jobs: - run: npm ci - run: npm run init - - run: cat src/common/indexer/entities/account.ts - name: Start API run: | diff --git a/config/config.devnet-old.yaml b/config/config.devnet-old.yaml index ea1c72322..7129c6e75 100644 --- a/config/config.devnet-old.yaml +++ b/config/config.devnet-old.yaml @@ -27,8 +27,10 @@ features: stateChanges: enabled: false port: 5675 - url: 'amqp://guest:guest@127.0.0.1:5672' + rabbitUrl: 'amqp://guest:guest@127.0.0.1:5672' exchange: 'state_accesses' + queueName: 'api_state_accesses_queue-test' + deadLetterExchange: 'api_state_accesses_queue_dlx' eventsNotifier: enabled: false port: 5674 diff --git a/config/config.e2e-mocked.mainnet.yaml b/config/config.e2e-mocked.mainnet.yaml index ca28b7cba..92db6ef92 100644 --- a/config/config.e2e-mocked.mainnet.yaml +++ b/config/config.e2e-mocked.mainnet.yaml @@ -11,8 +11,10 @@ features: stateChanges: enabled: false port: 5675 - url: 'amqp://guest:guest@127.0.0.1:5672' + rabbitUrl: 'amqp://guest:guest@127.0.0.1:5672' exchange: 'state_accesses' + queueName: 'api_state_accesses_queue-test' + deadLetterExchange: 'api_state_accesses_queue_dlx' dataApi: enabled: false serviceUrl: 'https://data-api.multiversx.com' diff --git a/config/config.e2e.mainnet.yaml b/config/config.e2e.mainnet.yaml index edc87487c..3d8f6e391 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -26,8 +26,10 @@ features: stateChanges: enabled: false port: 5675 - url: 'amqp://guest:guest@127.0.0.1:5672' + rabbitUrl: 'amqp://guest:guest@127.0.0.1:5672' exchange: 'state_accesses' + queueName: 'api_state_accesses_queue-test' + deadLetterExchange: 'api_state_accesses_queue_dlx' eventsNotifier: enabled: false port: 5674 diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index a18b7319e..8a713f398 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -31,9 +31,10 @@ features: eventsNotifier: enabled: false port: 5674 - url: 'amqp://guest:guest@127.0.0.1:5672' - exchange: 'all_events' - queue: 'api-process-logs-and-events' + rabbitUrl: 'amqp://guest:guest@127.0.0.1:5672' + exchange: 'state_accesses' + queueName: 'api_state_accesses_queue-test' + deadLetterExchange: 'api_state_accesses_queue_dlx' guestCaching: enabled: false hitsThreshold: 100 diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 8bbd373f2..7f732f0fb 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -26,8 +26,10 @@ features: stateChanges: enabled: false port: 5675 - url: 'amqp://guest:guest@127.0.0.1:5672' + rabbitUrl: 'amqp://guest:guest@127.0.0.1:5672' exchange: 'state_accesses' + queueName: 'api_state_accesses_queue-test' + deadLetterExchange: 'api_state_accesses_queue_dlx' eventsNotifier: enabled: false port: 5674 diff --git a/src/endpoints/accounts-v2/account.controller.v2.ts b/src/endpoints/accounts-v2/account.controller.v2.ts index 34d4d4a93..a408854dd 100644 --- a/src/endpoints/accounts-v2/account.controller.v2.ts +++ b/src/endpoints/accounts-v2/account.controller.v2.ts @@ -15,7 +15,7 @@ export class AccountControllerV2 { private readonly accountServiceV2: AccountServiceV2, ) { } - @Get("/v2/accounts/:address") + @Get("/accounts/v2/:address") @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 }) From 2689b9a5f235029a4f75d8ed077047de77cd38f4 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 4 Nov 2025 16:42:49 +0200 Subject: [PATCH 76/90] fix indentation --- src/state-changes/utils/esdt.proto | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/state-changes/utils/esdt.proto b/src/state-changes/utils/esdt.proto index 0fccddc7f..54eece29f 100644 --- a/src/state-changes/utils/esdt.proto +++ b/src/state-changes/utils/esdt.proto @@ -1,18 +1,18 @@ syntax = "proto3"; message MetaData { - // ex: string name = 1; - // optional uint64 id = 2; + // ex: string name = 1; + // optional uint64 id = 2; } message ESDigitalToken { - uint32 Type = 1; + uint32 Type = 1; - bytes Value = 2; + bytes Value = 2; - bytes Properties = 3; + bytes Properties = 3; - MetaData TokenMetaData = 4; + MetaData TokenMetaData = 4; - bytes Reserved = 5; + bytes Reserved = 5; } From f12e0bf19582b61255c11e19860f9c5984a4b2a0 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 4 Nov 2025 20:18:55 +0200 Subject: [PATCH 77/90] refactor entities --- .../entities/account-changes-raw.ts | 11 ++ src/state-changes/entities/account-changes.ts | 14 ++ src/state-changes/entities/account-state.ts | 15 ++ .../entities/block-with-state-changes-raw.ts | 13 ++ .../entities/data-trie-change-operation.ts | 4 + .../entities/data-trie-change.ts | 9 ++ src/state-changes/entities/esdt-state.ts | 15 ++ src/state-changes/entities/esdt-type.ts | 10 ++ src/state-changes/entities/index.ts | 15 +- .../entities/state-access-operation.ts | 9 ++ .../entities/state-access-per-account-raw.ts | 16 ++ src/state-changes/entities/state-changes.ts | 23 +++ .../entities/state.changes.entity.ts | 145 ------------------ 13 files changed, 153 insertions(+), 146 deletions(-) create mode 100644 src/state-changes/entities/account-changes-raw.ts create mode 100644 src/state-changes/entities/account-changes.ts create mode 100644 src/state-changes/entities/account-state.ts create mode 100644 src/state-changes/entities/block-with-state-changes-raw.ts create mode 100644 src/state-changes/entities/data-trie-change-operation.ts create mode 100644 src/state-changes/entities/data-trie-change.ts create mode 100644 src/state-changes/entities/esdt-state.ts create mode 100644 src/state-changes/entities/esdt-type.ts create mode 100644 src/state-changes/entities/state-access-operation.ts create mode 100644 src/state-changes/entities/state-access-per-account-raw.ts create mode 100644 src/state-changes/entities/state-changes.ts delete mode 100644 src/state-changes/entities/state.changes.entity.ts diff --git a/src/state-changes/entities/account-changes-raw.ts b/src/state-changes/entities/account-changes-raw.ts new file mode 100644 index 000000000..98c887a3a --- /dev/null +++ b/src/state-changes/entities/account-changes-raw.ts @@ -0,0 +1,11 @@ +export enum AccountChangesRaw { + NoChange = 0, + NonceChanged = 1 << 0, // 1 + BalanceChanged = 1 << 1, // 2 + CodeHashChanged = 1 << 2, // 4 + RootHashChanged = 1 << 3, // 8 + DeveloperRewardChanged = 1 << 4, // 16 + OwnerAddressChanged = 1 << 5, // 32 + UserNameChanged = 1 << 6, // 64 + CodeMetadataChanged = 1 << 7 // 128 +} diff --git a/src/state-changes/entities/account-changes.ts b/src/state-changes/entities/account-changes.ts new file mode 100644 index 000000000..ced774c91 --- /dev/null +++ b/src/state-changes/entities/account-changes.ts @@ -0,0 +1,14 @@ +export class AccountChanges { + nonceChanged!: boolean; + balanceChanged!: boolean; + codeHashChanged!: boolean; + rootHashChanged!: boolean; + developerRewardChanged!: boolean; + ownerAddressChanged!: boolean; + userNameChanged!: boolean; + codeMetadataChanged!: boolean; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/state-changes/entities/account-state.ts b/src/state-changes/entities/account-state.ts new file mode 100644 index 000000000..7f0bbabdc --- /dev/null +++ b/src/state-changes/entities/account-state.ts @@ -0,0 +1,15 @@ +export class AccountState { + nonce!: number; + balance!: string; + developerReward!: string; + address!: string; + codeHash?: string; + rootHash!: string; + ownerAddress?: string; + username?: string; + codeMetadata?: string; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/state-changes/entities/block-with-state-changes-raw.ts b/src/state-changes/entities/block-with-state-changes-raw.ts new file mode 100644 index 000000000..a4c5002f6 --- /dev/null +++ b/src/state-changes/entities/block-with-state-changes-raw.ts @@ -0,0 +1,13 @@ +import { StateAccessPerAccountRaw } from "./state-access-per-account-raw"; + +export class BlockWithStateChangesRaw { + hash!: string; + shardID!: number; + nonce!: number; + timestampMs!: number; + stateAccessesPerAccounts!: Record; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/state-changes/entities/data-trie-change-operation.ts b/src/state-changes/entities/data-trie-change-operation.ts new file mode 100644 index 000000000..d448b7ebc --- /dev/null +++ b/src/state-changes/entities/data-trie-change-operation.ts @@ -0,0 +1,4 @@ +export enum DataTrieChangeOperation { + NotDelete = 0, + Delete = 1, +} diff --git a/src/state-changes/entities/data-trie-change.ts b/src/state-changes/entities/data-trie-change.ts new file mode 100644 index 000000000..722fc6608 --- /dev/null +++ b/src/state-changes/entities/data-trie-change.ts @@ -0,0 +1,9 @@ +import { DataTrieChangeOperation } from "./data-trie-change-operation"; + +export class DataTrieChange { + type!: number; + key!: string; + val!: any; + version!: number; + operation!: DataTrieChangeOperation; +} diff --git a/src/state-changes/entities/esdt-state.ts b/src/state-changes/entities/esdt-state.ts new file mode 100644 index 000000000..614c3fc0e --- /dev/null +++ b/src/state-changes/entities/esdt-state.ts @@ -0,0 +1,15 @@ +import { ESDTType } from "./esdt-type"; + +export class EsdtState { + identifier!: string; + nonce!: string; + type!: ESDTType; + value!: string; + propertiesHex!: string; + reservedHex!: string; + tokenMetaData!: any; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/state-changes/entities/esdt-type.ts b/src/state-changes/entities/esdt-type.ts new file mode 100644 index 000000000..6dd0f4eb7 --- /dev/null +++ b/src/state-changes/entities/esdt-type.ts @@ -0,0 +1,10 @@ +export enum ESDTType { + Fungible = 0, + NonFungible = 1, + NonFungibleV2 = 2, + SemiFungible = 3, + MetaFungible = 4, + DynamicNFT = 5, + DynamicSFT = 6, + DynamicMeta = 7, +} diff --git a/src/state-changes/entities/index.ts b/src/state-changes/entities/index.ts index 5f4dc3633..79b691e9a 100644 --- a/src/state-changes/entities/index.ts +++ b/src/state-changes/entities/index.ts @@ -1 +1,14 @@ -export * from './state.changes.entity'; +// Entities +export * from "./account-changes"; +export * from "./state-access-per-account-raw"; +export * from "./data-trie-change"; +export * from "./block-with-state-changes-raw"; +export * from "./account-state"; +export * from "./esdt-state"; +export * from "./state-changes"; + +// Enums +export * from "./esdt-type"; +export * from "./account-changes-raw"; +export * from "./state-access-operation"; +export * from "./data-trie-change-operation"; diff --git a/src/state-changes/entities/state-access-operation.ts b/src/state-changes/entities/state-access-operation.ts new file mode 100644 index 000000000..97aaa749e --- /dev/null +++ b/src/state-changes/entities/state-access-operation.ts @@ -0,0 +1,9 @@ +export enum StateAccessOperation { + NotSet = 0, + GetCode = 1 << 0, + SaveAccount = 1 << 1, + GetAccount = 1 << 2, + WriteCode = 1 << 3, + RemoveDataTrie = 1 << 4, + GetDataTrieValue = 1 << 5, +} diff --git a/src/state-changes/entities/state-access-per-account-raw.ts b/src/state-changes/entities/state-access-per-account-raw.ts new file mode 100644 index 000000000..a65d829df --- /dev/null +++ b/src/state-changes/entities/state-access-per-account-raw.ts @@ -0,0 +1,16 @@ +import { DataTrieChange } from "./data-trie-change"; + +export class StateAccessPerAccountRaw { + type!: number; + index!: number; + txHash!: string; + mainTrieKey!: string; + mainTrieVal!: string; + operation!: number; + dataTrieChanges?: DataTrieChange[]; + accountChanges?: number; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/state-changes/entities/state-changes.ts b/src/state-changes/entities/state-changes.ts new file mode 100644 index 000000000..e0c712d28 --- /dev/null +++ b/src/state-changes/entities/state-changes.ts @@ -0,0 +1,23 @@ +import { AccountState } from "./account-state"; +import { EsdtState } from "./esdt-state"; +import { AccountChanges } from "./account-changes"; + +export class StateChanges { + accountState!: AccountState | undefined; + esdtState!: { + 'Fungible': EsdtState[], + 'NonFungible': EsdtState[], + 'NonFungibleV2': EsdtState[], + 'SemiFungible': EsdtState[], + 'MetaFungible': EsdtState[], + 'DynamicNFT': EsdtState[], + 'DynamicSFT': EsdtState[], + 'DynamicMeta': EsdtState[], + }; + accountChanges!: AccountChanges; + isNewAccount!: boolean; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/state-changes/entities/state.changes.entity.ts b/src/state-changes/entities/state.changes.entity.ts deleted file mode 100644 index f7580fbab..000000000 --- a/src/state-changes/entities/state.changes.entity.ts +++ /dev/null @@ -1,145 +0,0 @@ -export class AccountChanges { - nonceChanged!: boolean; - balanceChanged!: boolean; - codeHashChanged!: boolean; - rootHashChanged!: boolean; - developerRewardChanged!: boolean; - ownerAddressChanged!: boolean; - userNameChanged!: boolean; - codeMetadataChanged!: boolean; - - constructor(init?: Partial) { - Object.assign(this, init); - } -} - -export class StateAccessPerAccountRaw { - type!: number; - index!: number; - txHash!: string; - mainTrieKey!: string; - mainTrieVal!: string; - operation!: number; - dataTrieChanges?: DataTrieChange[]; - accountChanges?: number; - - constructor(init?: Partial) { - Object.assign(this, init); - } -} - -export class DataTrieChange { - type!: number; - key!: string; - val!: any; - version!: number; - operation!: DataTrieChangeOperation; -} - -export class BlockWithStateChangesRaw { - hash!: string; - shardID!: number; - nonce!: number; - timestampMs!: number; - stateAccessesPerAccounts!: Record; - - constructor(init?: Partial) { - Object.assign(this, init); - } -} - -export class AccountState { - nonce!: number; - balance!: string; - developerReward!: string; - address!: string; - codeHash?: string; - rootHash!: string; - ownerAddress?: string; - username?: string; - codeMetadata?: string; - - constructor(init?: Partial) { - Object.assign(this, init); - } -} - -export class EsdtState { - identifier!: string; - nonce!: string; - type!: ESDTType; - value!: string; - propertiesHex!: string; - reservedHex!: string; - tokenMetaData!: any; - - constructor(init?: Partial) { - Object.assign(this, init); - } -} - -export enum ESDTType { - // 0 - Fungible, - // 1 - NonFungible, - // 2 - NonFungibleV2, - // 3 - SemiFungible, - // 4 - MetaFungible, - // 5 - DynamicNFT, - // 6 - DynamicSFT, - // 7 - DynamicMeta, -} - -export class StateChanges { - accountState!: AccountState | undefined; - esdtState!: { - 'Fungible': EsdtState[], - 'NonFungible': EsdtState[], - 'NonFungibleV2': EsdtState[], - 'SemiFungible': EsdtState[], - 'MetaFungible': EsdtState[], - 'DynamicNFT': EsdtState[], - 'DynamicSFT': EsdtState[], - 'DynamicMeta': EsdtState[], - }; - accountChanges!: AccountChanges; - isNewAccount!: boolean; - - constructor(init?: Partial) { - Object.assign(this, init); - } -} - -export enum AccountChangesRaw { - NoChange = 0, - NonceChanged = 1 << 0, // 1 - BalanceChanged = 1 << 1, // 2 - CodeHashChanged = 1 << 2, // 4 - RootHashChanged = 1 << 3, // 8 - DeveloperRewardChanged = 1 << 4, // 16 - OwnerAddressChanged = 1 << 5, // 32 - UserNameChanged = 1 << 6, // 64 - CodeMetadataChanged = 1 << 7 // 128 -} - -export enum StateAccessOperation { - NotSet = 0, - GetCode = 1 << 0, - SaveAccount = 1 << 1, - GetAccount = 1 << 2, - WriteCode = 1 << 3, - RemoveDataTrie = 1 << 4, - GetDataTrieValue = 1 << 5, -} - -export enum DataTrieChangeOperation { - NotDelete = 0, - Delete = 1, -} From 1309708aefed72b11fe984b4a81da3b9618f91e5 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 5 Nov 2025 14:23:10 +0200 Subject: [PATCH 78/90] improvements --- .../state.changes.consumer.service.ts | 23 +-------- .../utils/state-changes.decoder.ts | 51 +++++++++++++++---- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 0f79bb5e1..3e3a36b7d 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -17,22 +17,6 @@ import { ApiConfigService } from "src/common/api-config/api.config.service"; @Injectable() export class StateChangesConsumerService { private readonly logger = new OriginLogger(StateChangesConsumerService.name); - static SYSTEM_ACCOUNTS = [ - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqllls0lczs7", // stakingScAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l", // validatorScAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", // esdtScAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", // governanceScAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqrlllllllllllllllllllllllllsn60f0k", // jailingAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqlllllllllllllllllllllllllllsr9gav8", // endOfEpochAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", // delegationManagerScAddress - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0llllsqkarq6", // firstDelegationScAddress - "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu", // contractDeployScAdress - "erd17rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rcqqkhty3", // genesisMintingAddress - "erd1lllllllllllllllllllllllllllllllllllllllllllllllllllsckry7t", // systemAccountAddress - "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqq2m3f0f", // esdtGlobalSettingsAddresses[0] - "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqsl6e366", // esdtGlobalSettingsAddresses[1] - "erd1lllllllllllllllllllllllllllllllllllllllllllllllllupq9x7ny0", // esdtGlobalSettingsAddresses[2] - ]; constructor( private readonly cacheService: CacheService, @@ -120,10 +104,7 @@ export class StateChangesConsumerService { private transformFinalStatesToDbFormat(finalStates: Record, shardID: number, blockTimestampMs: number) { const transformed: AccountDetails[] = []; - for (const [address, state] of Object.entries(finalStates)) { - if (StateChangesConsumerService.isSystemContractAddress(address)) { - continue; - } + for (const [_address, state] of Object.entries(finalStates)) { const newAccountState = state.accountState; // const tokens = [ @@ -279,6 +260,6 @@ export class StateChangesConsumerService { } static isSystemContractAddress(address: string) { - return StateChangesConsumerService.SYSTEM_ACCOUNTS.includes(address); + return StateChangesDecoder.isSystemContractAddress(address); } } diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts index 800097dfd..acd6dc6f1 100644 --- a/src/state-changes/utils/state-changes.decoder.ts +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -17,29 +17,49 @@ import { } from "../entities"; export class StateChangesDecoder { - static bech32FromHex(hex: any) { + private static SYSTEM_ACCOUNTS = new Set([ + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqllls0lczs7", // stakingScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l", // validatorScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", // esdtScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", // governanceScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqrlllllllllllllllllllllllllsn60f0k", // jailingAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqlllllllllllllllllllllllllllsr9gav8", // endOfEpochAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", // delegationManagerScAddress + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0llllsqkarq6", // firstDelegationScAddress + "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu", // contractDeployScAddress + "erd17rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rcqqkhty3", // genesisMintingAddress + "erd1lllllllllllllllllllllllllllllllllllllllllllllllllllsckry7t", // systemAccountAddress + "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqq2m3f0f", // esdtGlobalSettingsAddresses[0] + "erd1llllllllllllllllllllllllllllllllllllllllllllllllluqsl6e366", // esdtGlobalSettingsAddresses[1] + "erd1lllllllllllllllllllllllllllllllllllllllllllllllllupq9x7ny0", // esdtGlobalSettingsAddresses[2] + ]); + + static isSystemContractAddress(address: string) { + return this.SYSTEM_ACCOUNTS.has(address); + } + + private static bech32FromHex(hex: any) { const clean = hex.startsWith("0x") ? hex.slice(2) : hex; return Address.newFromHex(clean).toBech32(); } - static bech32FromBytes(u8: any) { + private static bech32FromBytes(u8: any) { return (u8 && u8.length ? Address.newFromHex(this.bytesToHex(u8)).toBech32() : ""); } - static bytesToHex(u8: any) { + private static bytesToHex(u8: any) { return (u8 && u8.length ? Buffer.from(u8).toString("hex") : ""); } - static bytesToBase64(u8: any) { + private static bytesToBase64(u8: any) { return (u8 && u8.length ? Buffer.from(u8).toString("base64") : ""); } - // const bytesToString = (u8: any) => (u8 && u8.length ? Buffer.from(u8).toString("utf8") : ""); - static longToString(v: any) { + private static longToString(v: any) { return v == null ? "" : (typeof v === "object" && typeof v.toString === "function" ? v.toString() : String(v)); } - static bigEndianBytesToBigInt(u8: any) { + private static bigEndianBytesToBigInt(u8: any) { let v = BigInt(0); for (const b of u8) { v = (v << BigInt(8)) + BigInt(b); @@ -54,7 +74,7 @@ export class StateChangesDecoder { * - (if present) negative => 0x01 || magnitude * Fallback: if first byte is not a sign marker, treat whole buffer as positive magnitude. */ - static decodeMxSignMagBigInt(u8: any) { + private static decodeMxSignMagBigInt(u8: any) { if (!u8 || u8.length === 0) return BigInt(0); // canonical zero used by the serializer @@ -71,7 +91,7 @@ export class StateChangesDecoder { return this.bigEndianBytesToBigInt(u8); } - static getDecodedUserAccountData(buf: any) { + private static getDecodedUserAccountData(buf: any) { try { const msg = UserAccountData.decode(buf); @@ -103,7 +123,7 @@ export class StateChangesDecoder { } } - static decodeAccountChanges(flags: number | undefined): AccountChanges { + private static decodeAccountChanges(flags: number | undefined): AccountChanges { if (!flags) { return new AccountChanges({ nonceChanged: false, @@ -128,7 +148,7 @@ export class StateChangesDecoder { }); } - static getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { + private static getDecodedEsdtData(address: string, dataTrieChange: DataTrieChange) { const bufTrieLeafValue = Buffer.from(dataTrieChange.val, "base64"); const esdtPrefix = 'ELRONDesdt'; try { @@ -170,6 +190,10 @@ export class StateChangesDecoder { for (const accountHex of Object.keys(accounts)) { const address = this.bech32FromHex(accountHex); + if (this.isSystemContractAddress(address)) { + continue; + } + const { stateAccess = [] } = accounts[accountHex] || {}; const allDecoded: Record = {}; stateAccess.forEach((sa: StateAccessPerAccountRaw, i: number) => { @@ -252,6 +276,11 @@ export class StateChangesDecoder { for (const accountHex of Object.keys(accounts)) { const address = this.bech32FromHex(accountHex); + + if (this.isSystemContractAddress(address)) { + continue; + } + const esdtOccured: Record = {}; const { stateAccess } = accounts[accountHex] || {}; From f7306293e51072112fe15531cc86d318a7335a02 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 5 Nov 2025 16:23:45 +0200 Subject: [PATCH 79/90] refactor --- .../utils/state-changes.decoder.ts | 155 +++++++++--------- 1 file changed, 77 insertions(+), 78 deletions(-) diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts index acd6dc6f1..5d1d4371f 100644 --- a/src/state-changes/utils/state-changes.decoder.ts +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -91,9 +91,13 @@ export class StateChangesDecoder { return this.bigEndianBytesToBigInt(u8); } - private static getDecodedUserAccountData(buf: any) { + private static getDecodedUserAccountData(base64Value: string | null): AccountState | null { try { - const msg = UserAccountData.decode(buf); + if (base64Value == null || base64Value === '') { + return null; + } + const buf = Buffer.from(base64Value, "base64"); + const msg = UserAccountData.decode(buf as Uint8Array); const balance = this.decodeMxSignMagBigInt(msg.Balance); const devReward = this.decodeMxSignMagBigInt(msg.DeveloperReward); @@ -172,7 +176,6 @@ export class StateChangesDecoder { }; } else { //TODO: handle if needed - return null; } } catch (e: any) { @@ -199,11 +202,7 @@ export class StateChangesDecoder { stateAccess.forEach((sa: StateAccessPerAccountRaw, i: number) => { const base64AccountData = sa.mainTrieVal; - let decodedAccountData: any = null; - if (base64AccountData) { - const bufAccountData = Buffer.from(base64AccountData, "base64"); - decodedAccountData = this.getDecodedUserAccountData(bufAccountData); - } + const decodedAccountData = this.getDecodedUserAccountData(base64AccountData); const dataTrieChanges = sa.dataTrieChanges; @@ -211,12 +210,8 @@ export class StateChangesDecoder { const allDecodedEsdtData: any[] = []; if (dataTrieChanges) { for (const dataTrieChange of dataTrieChanges) { - if (dataTrieChange.version === 0) { - console.warn(`Entry #${i}: unsupported dataTrieChanges version 0`); - } else { - + if (dataTrieChange.version === 0 && dataTrieChange.val) { const decodedEsdtData = this.getDecodedEsdtData(address, dataTrieChange); - if (decodedEsdtData) { if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { decodedEsdtData.value = '0'; @@ -249,15 +244,13 @@ export class StateChangesDecoder { } ); if (allDecoded[address] === undefined) allDecoded[address] = []; - const newAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; - - const accountChanges = this.decodeAccountChanges(sa.accountChanges); + const newAccount = this.isNewAccount(sa); allDecoded[address].push({ entry: `Entry #${i}`, accountState: decodedAccountData, esdtStates: groupedEsdtStates, - accountChanges, + accountChanges: this.decodeAccountChanges(sa.accountChanges), newAccount, }); } @@ -280,88 +273,94 @@ export class StateChangesDecoder { if (this.isSystemContractAddress(address)) { continue; } + const { stateAccess: accountStateAccesses } = accounts[accountHex] || {}; + finalStates[address] = this.getAccountFinalState(address, accountStateAccesses) + } - const esdtOccured: Record = {}; + return finalStates; + } - const { stateAccess } = accounts[accountHex] || {}; - let finalAccountChangesRaw: AccountChangesRaw = AccountChangesRaw.NoChange; - let finalNewAccount = false; + private static getAccountFinalState(address: string, accountStateAccesses: StateAccessPerAccountRaw[]) { + let finalAccountChangesRaw: AccountChangesRaw = AccountChangesRaw.NoChange; + const esdtOccured: Record = {}; + let finalNewAccount = false; + + let finalAccountState: AccountState | undefined = undefined; + const finalEsdtStates = { + Fungible: [] as EsdtState[], + NonFungible: [] as EsdtState[], + NonFungibleV2: [] as EsdtState[], + SemiFungible: [] as EsdtState[], + MetaFungible: [] as EsdtState[], + DynamicNFT: [] as EsdtState[], + DynamicSFT: [] as EsdtState[], + DynamicMeta: [] as EsdtState[], + }; + + for (let i = accountStateAccesses.length - 1; i >= 0; i--) { + const stateAccess = accountStateAccesses[i]; + const currentAccountChangesRaw = stateAccess.accountChanges; + if (currentAccountChangesRaw) { + finalAccountChangesRaw |= currentAccountChangesRaw; + } - let finalAccountState: AccountState | undefined = undefined; - const finalEsdtStates = { - Fungible: [] as EsdtState[], - NonFungible: [] as EsdtState[], - NonFungibleV2: [] as EsdtState[], - SemiFungible: [] as EsdtState[], - MetaFungible: [] as EsdtState[], - DynamicNFT: [] as EsdtState[], - DynamicSFT: [] as EsdtState[], - DynamicMeta: [] as EsdtState[], - }; + // if we already found it as new account, we skip the computation + if (!finalNewAccount) { + const currentNewAccount = this.isNewAccount(stateAccess) + finalNewAccount = currentNewAccount || finalNewAccount; + } - for (let i = stateAccess.length - 1; i >= 0; i--) { - const sa = stateAccess[i]; - const currentAccountChangesRaw = sa.accountChanges; - if (currentAccountChangesRaw) { - finalAccountChangesRaw |= currentAccountChangesRaw; - } + // the final state is the first state we find when iterating backwards on state accesses array + if (!finalAccountState) { + const base64AccountData = stateAccess.mainTrieVal; + const decodedAccountData = this.getDecodedUserAccountData(base64AccountData); - if (!finalNewAccount) { - const currentNewAccount = (sa.accountChanges === null || sa.accountChanges === undefined) && (sa.operation & StateAccessOperation.SaveAccount) ? true : false; - finalNewAccount = currentNewAccount || finalNewAccount; + if (decodedAccountData) { + finalAccountState = decodedAccountData; } + } - const base64AccountData = sa.mainTrieVal; - if (base64AccountData && !finalAccountState) { - const bufAccountData = Buffer.from(base64AccountData, "base64"); - const decodedAccountData = this.getDecodedUserAccountData(bufAccountData); - if (decodedAccountData) { - finalAccountState = decodedAccountData; - } - } + const dataTrieChanges = stateAccess.dataTrieChanges; + if (dataTrieChanges) { + for (let i = dataTrieChanges.length - 1; i >= 0; i--) { + const dataTrieChange = dataTrieChanges[i]; + if (dataTrieChange.version !== 0 && dataTrieChange.val) { + const decodedEsdtData = this.getDecodedEsdtData(address, dataTrieChange); - const dataTrieChanges = sa.dataTrieChanges; - if (dataTrieChanges) { - for (let i = dataTrieChanges.length - 1; i >= 0; i--) { - const dataTrieChange = dataTrieChanges[i]; - if (dataTrieChange.version === 0) { - console.warn(`Unsupported dataTrieChanges version 0`); - } else if (dataTrieChange.val) { - const decodedEsdtData = this.getDecodedEsdtData(address, dataTrieChange); - if (decodedEsdtData) { - const esdtId = decodedEsdtData.identifier; - if (!esdtOccured[esdtId]) { - const typeName = ESDTType[decodedEsdtData.type] as keyof typeof finalEsdtStates; // numeric -> string - if (typeName) { - if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { - decodedEsdtData.value = '0'; - } - finalEsdtStates[typeName].push(decodedEsdtData); - esdtOccured[esdtId] = true; - } + // last esdt state is the first appeareance in reverse order when iterating over account state accesses + if (decodedEsdtData && !esdtOccured[decodedEsdtData.identifier]) { + const typeName = ESDTType[decodedEsdtData.type] as keyof typeof finalEsdtStates; // numeric -> string + + // double check in case some unknown type passed through + if (typeName) { + if (dataTrieChange.operation === DataTrieChangeOperation.Delete) { + decodedEsdtData.value = '0'; } + finalEsdtStates[typeName].push(decodedEsdtData); + esdtOccured[decodedEsdtData.identifier] = true; } } } - } } - finalStates[address] = { - accountState: finalAccountState, - esdtState: finalEsdtStates, - accountChanges: this.decodeAccountChanges(finalAccountChangesRaw), - isNewAccount: finalNewAccount, - }; } - return finalStates; + return { + accountState: finalAccountState, + esdtState: finalEsdtStates, + accountChanges: this.decodeAccountChanges(finalAccountChangesRaw), + isNewAccount: finalNewAccount, + }; } + private static isNewAccount(stateAccess: StateAccessPerAccountRaw) { + return (stateAccess.accountChanges === null || stateAccess.accountChanges === undefined) && + (stateAccess.operation & StateAccessOperation.SaveAccount) ? true : false; + } - static getFinalStates(stateChanges: Record) { + static getFinalStatesFromStateChangesRaw(stateChanges: Record) { const finalStates: Record = {}; - for (const [address, entries] of Object.entries(stateChanges)) { let finalAccountState: AccountState | undefined = undefined; const finalEsdtStates = { From 5b28744cfd5ec2f7b4bc5757786fe43c9bd34ebc Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 6 Nov 2025 17:19:31 +0200 Subject: [PATCH 80/90] add unit tests + fixes --- .../state.changes.consumer.service.ts | 6 +- .../utils/state-changes.decoder.ts | 2 +- .../services/state-changes.consumer.spec.ts | 428 ++++++++++++++++++ .../unit/utils/state-changes.decoder.spec.ts | 218 +++++++++ 4 files changed, 650 insertions(+), 4 deletions(-) create mode 100644 src/test/unit/services/state-changes.consumer.spec.ts create mode 100644 src/test/unit/utils/state-changes.decoder.spec.ts diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 3e3a36b7d..626c7ff61 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -237,14 +237,14 @@ export class StateChangesConsumerService { CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(2).key, ]; - let timestampsMs: (number | undefined | null)[] = cacheService.getManyLocal(keys); + let timestampsMs: (number | undefined | null)[] | undefined = cacheService.getManyLocal(keys); // check local - if (timestampsMs.some(t => t == null)) { + if (timestampsMs == null || timestampsMs.some(t => t == null)) { timestampsMs = await cacheService.getManyRemote(keys); // check remote - if (timestampsMs.some(t => t == null)) { + if (timestampsMs == null || timestampsMs.some(t => t == null)) { return false; } diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts index 5d1d4371f..b8e3413de 100644 --- a/src/state-changes/utils/state-changes.decoder.ts +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -354,7 +354,7 @@ export class StateChangesDecoder { } private static isNewAccount(stateAccess: StateAccessPerAccountRaw) { - return (stateAccess.accountChanges === null || stateAccess.accountChanges === undefined) && + return (stateAccess.accountChanges == null) && (stateAccess.operation & StateAccessOperation.SaveAccount) ? true : false; } diff --git a/src/test/unit/services/state-changes.consumer.spec.ts b/src/test/unit/services/state-changes.consumer.spec.ts new file mode 100644 index 000000000..73f4e52bf --- /dev/null +++ b/src/test/unit/services/state-changes.consumer.spec.ts @@ -0,0 +1,428 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { StateChangesConsumerService } from 'src/state-changes/state.changes.consumer.service'; +import { CacheService } from '@multiversx/sdk-nestjs-cache'; +import { AccountDetailsRepository } from 'src/common/indexer/db'; +import { ApiConfigService } from 'src/common/api-config/api.config.service'; +import { ClientProxy } from '@nestjs/microservices'; +import { StateChangesDecoder } from 'src/state-changes/utils/state-changes.decoder'; +import { ESDTType } from 'src/state-changes/entities'; +import { TokenType } from 'src/common/indexer/entities'; +import { NftType } from 'src/endpoints/nfts/entities/nft.type'; +import { NftSubType } from 'src/endpoints/nfts/entities/nft.sub.type'; +import { AddressUtils } from '@multiversx/sdk-nestjs-common'; + +jest.mock('src/state-changes/utils/state-changes.decoder'); +jest.mock('@multiversx/sdk-nestjs-cache'); +jest.mock('src/common/indexer/db'); +jest.mock('@nestjs/microservices'); +jest.mock('@multiversx/sdk-nestjs-common', () => ({ + AddressUtils: { + isSmartContractAddress: jest.fn(), + }, + OriginLogger: jest.fn().mockImplementation(() => ({ + log: jest.fn(), + error: jest.fn(), + })), + ComplexityEstimation: jest.fn().mockImplementation(() => () => { }), + SwaggerUtils: { + amountPropertyOptions: jest.fn().mockReturnValue({}), + }, + Constants: { + oneSecond: jest.fn(() => 1000), + oneMinute: jest.fn(() => 60 * 1000), + oneHour: jest.fn(() => 60 * 60 * 1000), + oneDay: jest.fn(() => 24 * 60 * 60 * 1000), + oneWeek: jest.fn(() => 7 * 24 * 60 * 60 * 1000), + oneMonth: jest.fn(() => 30 * 24 * 60 * 60 * 1000), + }, +})); +jest.mock('src/common/indexer/db', () => ({ + AccountDetailsRepository: jest.fn(), + AccountDetails: jest.fn().mockImplementation((data) => data), +})); + +describe('StateChangesConsumerService', () => { + let service: StateChangesConsumerService; + let cacheService: jest.Mocked; + let accountRepo: jest.Mocked; + let clientProxy: jest.Mocked; + let apiConfig: jest.Mocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StateChangesConsumerService, + { + provide: CacheService, + useValue: { + setRemote: jest.fn(), + setManyRemote: jest.fn(), + deleteManyRemote: jest.fn(), + getManyLocal: jest.fn(), + getManyRemote: jest.fn(), + setManyLocal: jest.fn(), + }, + }, + { + provide: AccountDetailsRepository, + useValue: { + updateAccounts: jest.fn(), + }, + }, + { + provide: ApiConfigService, + useValue: { + getMetaChainShardId: jest.fn().mockReturnValue(4294967295), + }, + }, + { + provide: 'PUBSUB_SERVICE', + useValue: { + emit: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(StateChangesConsumerService); + cacheService = module.get(CacheService); + accountRepo = module.get(AccountDetailsRepository); + clientProxy = module.get('PUBSUB_SERVICE'); + apiConfig = module.get(ApiConfigService); + }); + + afterEach(() => jest.clearAllMocks()); + + describe('parseCodeMetadata', () => { + it('should return empty object if hexStr is missing', () => { + expect(service['parseCodeMetadata']('erd1...', undefined)).toEqual({}); + }); + + it('should parse non-smart contract guarded flag', () => { + (AddressUtils.isSmartContractAddress as jest.Mock).mockReturnValue(false); + const result = service['parseCodeMetadata']('erd1useraddress', '0800'); + expect(result).toEqual({ isGuarded: true }); + }); + + it('should parse smart contract flags', () => { + (AddressUtils.isSmartContractAddress as jest.Mock).mockReturnValue(true); + const result = service['parseCodeMetadata']('erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu', '0106'); + expect(result).toEqual({ + isUpgradeable: true, + isReadable: false, + isPayable: true, + isPayableBySmartContract: true, + }); + }); + }); + + describe('parseEsdtType', () => { + it('should map Fungible to TokenType.FungibleESDT', () => { + expect(service['parseEsdtType'](ESDTType.Fungible)).toBe(TokenType.FungibleESDT); + }); + + it('should map NonFungible to NftType.NonFungibleESDT', () => { + expect(service['parseEsdtType'](ESDTType.NonFungible)).toBe(NftType.NonFungibleESDT); + }); + + it('should map MetaFungible to NftType.MetaESDT', () => { + expect(service['parseEsdtType'](ESDTType.MetaFungible)).toBe(NftType.MetaESDT); + }); + }); + + describe('parseEsdtSubtype', () => { + it('should map NonFungibleV2 to NftSubType.NonFungibleESDTv2', () => { + expect(service['parseEsdtSubtype'](ESDTType.NonFungibleV2)).toBe(NftSubType.NonFungibleESDTv2); + }); + + it('should map DynamicSFT to NftSubType.DynamicSemiFungibleESDT', () => { + expect(service['parseEsdtSubtype'](ESDTType.DynamicSFT)).toBe(NftSubType.DynamicSemiFungibleESDT); + }); + }); + + describe('isStateChangesConsumerHealthy', () => { + it('should return false if any timestamp missing', async () => { + cacheService.getManyLocal.mockReturnValue([undefined, 1, 2]); + const result = await StateChangesConsumerService.isStateChangesConsumerHealthy(cacheService, 10000); + expect(result).toBe(false); + }); + + it('should return true if timestamps within threshold', async () => { + const now = Date.now(); + cacheService.getManyLocal.mockReturnValue([now - 5000, now - 3000, now - 4000]); + const result = await StateChangesConsumerService.isStateChangesConsumerHealthy(cacheService, 10000); + expect(result).toBe(true); + }); + + it('should return false if timestamps too old', async () => { + const now = Date.now(); + cacheService.getManyLocal.mockReturnValue([now - 60000, now - 70000, now - 80000]); + const result = await StateChangesConsumerService.isStateChangesConsumerHealthy(cacheService, 10000); + expect(result).toBe(false); + }); + + it('should fallback to remote cache if local missing and succeed', async () => { + const now = Date.now(); + cacheService.getManyLocal.mockReturnValue([null, null, null]); + cacheService.getManyRemote.mockResolvedValue([now - 2000, now - 3000, now - 4000]); + const result = await StateChangesConsumerService.isStateChangesConsumerHealthy(cacheService, 10000); + expect(result).toBe(true); + expect(cacheService.setManyLocal).toHaveBeenCalled(); + }); + }); + + describe('consumeEvents', () => { + it('should skip meta chain shard', async () => { + const mockBlock = { shardID: apiConfig.getMetaChainShardId(), hash: 'abc', timestampMs: Date.now() } as any; + await service.consumeEvents(mockBlock); + expect(StateChangesDecoder.decodeStateChangesFinal).not.toHaveBeenCalled(); + }); + + it('should decode and update accounts', async () => { + const mockBlock = { shardID: 1, hash: 'block1', timestampMs: Date.now(), stateAccessesPerAccounts: {} } as any; + (StateChangesDecoder.decodeStateChangesFinal as jest.Mock).mockReturnValue({ + erd1abc: { accountState: { address: 'erd1abc' }, esdtState: {}, accountChanges: {}, isNewAccount: false }, + }); + const spyTransform = jest.spyOn(service, 'transformFinalStatesToDbFormat'); + const spyUpdate = jest.spyOn(service, 'updateAccounts').mockResolvedValue(undefined); + await service.consumeEvents(mockBlock); + expect(StateChangesDecoder.decodeStateChangesFinal).toHaveBeenCalled(); + expect(spyTransform).toHaveBeenCalled(); + expect(spyUpdate).toHaveBeenCalled(); + expect(cacheService.setRemote).toHaveBeenCalled(); + }); + + it('should log and throw on error', async () => { + const mockBlock = { shardID: 1, hash: 'block1', timestampMs: Date.now() } as any; + jest.spyOn(service, 'decodeStateChangesFinal').mockImplementation(() => { throw new Error('test'); }); + await expect(service.consumeEvents(mockBlock)).rejects.toThrow('test'); + }); + }); + + describe('updateAccounts', () => { + it('should update non-contract accounts and set cache', async () => { + (AddressUtils.isSmartContractAddress as jest.Mock).mockReturnValue(false); + const mockAccounts = [{ address: 'erd1', tokens: [], nfts: [], balance: '10' }]; + await service['updateAccounts'](mockAccounts as any); + expect(accountRepo.updateAccounts).toHaveBeenCalled(); + expect(cacheService.setManyRemote).toHaveBeenCalled(); + expect(clientProxy.emit).toHaveBeenCalled(); + }); + + it('should delete contract cache keys', async () => { + (AddressUtils.isSmartContractAddress as jest.Mock).mockReturnValue(true); + const mockAccounts = [{ address: 'erd1sc', tokens: [], nfts: [], balance: '0' }]; + await service['updateAccounts'](mockAccounts as any); + expect(cacheService.deleteManyRemote).toHaveBeenCalled(); + expect(clientProxy.emit).toHaveBeenCalled(); + }); + }); + + describe('transformFinalStatesToDbFormat', () => { + it('should transform state changes into AccountDetails', () => { + const mockInput = + { + erd1qqqqqqqqqqqqqpgqvg8r5yavkyhu6rmmkgqzgsduzheg2fk7v5ysrypdex: { + accountState: { + nonce: 0, + balance: '6213923288818146107158', + developerReward: '7695868166885300000', + address: 'erd1qqqqqqqqqqqqqpgqvg8r5yavkyhu6rmmkgqzgsduzheg2fk7v5ysrypdex', + ownerAddress: 'erd1rc5p5drg26vggn6jx9puv6xlgka5n6ajm6cer554tzguwfm6v5ys2pr3pc', + codeHash: 'hRkRk4eX3AxvdJgLHruGrel+Zb0uHudVXBHqWvfvz4Q=', + rootHash: 'CKhf02sZIbMYtII8VbKqiLgTDEr0treTXEqIgHb1l4g=', + codeMetadata: '0500' + }, + esdtState: { + Fungible: [], + NonFungible: [], + NonFungibleV2: [], + SemiFungible: [], + MetaFungible: [], + DynamicNFT: [], + DynamicSFT: [], + DynamicMeta: [] + }, + accountChanges: { + nonceChanged: false, + balanceChanged: true, + codeHashChanged: false, + rootHashChanged: true, + developerRewardChanged: true, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false + }, + isNewAccount: false + }, + erd1qqqqqqqqqqqqqpgqjwls7l4jf9qwafnxual6nadaak66g5jjeyvs9dswkt: { + accountState: { + nonce: 0, + balance: '126502242682468246846', + developerReward: '2472015964236000000', + address: 'erd1qqqqqqqqqqqqqpgqjwls7l4jf9qwafnxual6nadaak66g5jjeyvs9dswkt', + ownerAddress: 'erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7', + codeHash: 'hcDhkiRq8PB5C3g5B7OcMZ020WOj+AuOOndgSDMBZ48=', + rootHash: 'bjwTHfuhx0bs3OJFnvcb6pcTWspG9cb370yuqVbKbQo=', + codeMetadata: '0100' + }, + esdtState: { + Fungible: [], + NonFungible: [], + NonFungibleV2: [], + SemiFungible: [], + MetaFungible: [], + DynamicNFT: [], + DynamicSFT: [], + DynamicMeta: [] + }, + accountChanges: { + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: true, + developerRewardChanged: true, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false + }, + isNewAccount: false + }, + erd1vt2qedvltqvrar072ny88wh7vgxq9xvxmyqm4nf8qkzpwj6ncy5sjtgj90: { + accountState: { + nonce: 43766, + balance: '59732371650000000000', + developerReward: '0', + address: 'erd1vt2qedvltqvrar072ny88wh7vgxq9xvxmyqm4nf8qkzpwj6ncy5sjtgj90', + rootHash: 's9P9Do2C9v8r3MqG3I3MkotuYpEA1qt8jNiRCE5ULBo=' + }, + esdtState: { + Fungible: [], + NonFungible: [], + NonFungibleV2: [], + SemiFungible: [], + MetaFungible: [], + DynamicNFT: [], + DynamicSFT: [], + DynamicMeta: [] + }, + accountChanges: { + nonceChanged: true, + balanceChanged: true, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false + }, + isNewAccount: false + }, + erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7: { + accountState: { + nonce: 306369, + balance: '391457061919320000000', + developerReward: '0', + address: 'erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7', + rootHash: 'fV3JuZDrwZ8TbnlawkHGYYp1bQX0fTgefUzU7xEYLh8=' + }, + esdtState: { + Fungible: [], + NonFungible: [], + NonFungibleV2: [], + SemiFungible: [], + MetaFungible: [], + DynamicNFT: [], + DynamicSFT: [], + DynamicMeta: [] + }, + accountChanges: { + nonceChanged: true, + balanceChanged: true, + codeHashChanged: false, + rootHashChanged: false, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false + }, + isNewAccount: false + } + } + const mockShardId = 1; + const mockBlockTimestampMs = 1762356608000; + const expectedResult = [ + { + address: 'erd1qqqqqqqqqqqqqpgqvg8r5yavkyhu6rmmkgqzgsduzheg2fk7v5ysrypdex', + balance: '6213923288818146107158', + nonce: 0, + timestampMs: 1762356608000, + timestamp: 1762356608, + shard: 1, + developerReward: '7695868166885300000', + ownerAddress: 'erd1rc5p5drg26vggn6jx9puv6xlgka5n6ajm6cer554tzguwfm6v5ys2pr3pc', + codeHash: 'hRkRk4eX3AxvdJgLHruGrel+Zb0uHudVXBHqWvfvz4Q=', + rootHash: 'CKhf02sZIbMYtII8VbKqiLgTDEr0treTXEqIgHb1l4g=', + isUpgradeable: true, + isReadable: true, + isPayable: false, + isPayableBySmartContract: false + }, + { + address: 'erd1qqqqqqqqqqqqqpgqjwls7l4jf9qwafnxual6nadaak66g5jjeyvs9dswkt', + balance: '126502242682468246846', + nonce: 0, + timestampMs: 1762356608000, + timestamp: 1762356608, + shard: 1, + developerReward: '2472015964236000000', + ownerAddress: 'erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7', + codeHash: 'hcDhkiRq8PB5C3g5B7OcMZ020WOj+AuOOndgSDMBZ48=', + rootHash: 'bjwTHfuhx0bs3OJFnvcb6pcTWspG9cb370yuqVbKbQo=', + isUpgradeable: true, + isReadable: false, + isPayable: false, + isPayableBySmartContract: false + }, + { + address: 'erd1vt2qedvltqvrar072ny88wh7vgxq9xvxmyqm4nf8qkzpwj6ncy5sjtgj90', + balance: '59732371650000000000', + nonce: 43766, + timestampMs: 1762356608000, + timestamp: 1762356608, + shard: 1, + developerReward: '0', + rootHash: 's9P9Do2C9v8r3MqG3I3MkotuYpEA1qt8jNiRCE5ULBo=' + }, + { + address: 'erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7', + balance: '391457061919320000000', + nonce: 306369, + timestampMs: 1762356608000, + timestamp: 1762356608, + shard: 1, + developerReward: '0', + rootHash: 'fV3JuZDrwZ8TbnlawkHGYYp1bQX0fTgefUzU7xEYLh8=' + } + ] + const result = service['transformFinalStatesToDbFormat']( + mockInput, + mockShardId, + mockBlockTimestampMs, + ); + expect(result).toEqual(expectedResult); + }); + + it('should skip if no accountState', () => { + const result = service['transformFinalStatesToDbFormat']({ erd1: {} } as any, 0, Date.now()); + expect(result.length).toBe(0); + }); + }); + + describe('deleteLocalCache', () => { + it('should emit deleteCacheKeys event', () => { + service['deleteLocalCache'](['key1', 'key2']); + expect(clientProxy.emit).toHaveBeenCalledWith('deleteCacheKeys', ['key1', 'key2']); + }); + }); +}); diff --git a/src/test/unit/utils/state-changes.decoder.spec.ts b/src/test/unit/utils/state-changes.decoder.spec.ts new file mode 100644 index 000000000..4956ab6c6 --- /dev/null +++ b/src/test/unit/utils/state-changes.decoder.spec.ts @@ -0,0 +1,218 @@ +import { StateChangesDecoder } from 'src/state-changes/utils/state-changes.decoder'; +import { AccountChangesRaw, DataTrieChangeOperation, StateAccessOperation } from 'src/state-changes/entities' + +describe('StateChangesDecoder', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('isSystemContractAddress', () => { + it('should detect system addresses correctly', () => { + const address = 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqllls0lczs7'; + expect(StateChangesDecoder.isSystemContractAddress(address)).toBe(true); + expect(StateChangesDecoder.isSystemContractAddress('erd1random')).toBe(false); + }); + }); + + describe('decodeMxSignMagBigInt', () => { + it('should decode zero buffer', () => { + expect(StateChangesDecoder['decodeMxSignMagBigInt'](new Uint8Array([0x00, 0x00]))).toBe(BigInt(0)); + }); + + it('should decode positive big int', () => { + const result = StateChangesDecoder['decodeMxSignMagBigInt'](new Uint8Array([0x00, 0x01, 0x02])); + expect(result).toBe(BigInt(258)); + }); + + it('should decode negative big int', () => { + const result = StateChangesDecoder['decodeMxSignMagBigInt'](new Uint8Array([0x01, 0x01])); + expect(result).toBe(BigInt(-1)); + }); + + it('should decode fallback magnitude', () => { + const result = StateChangesDecoder['decodeMxSignMagBigInt'](new Uint8Array([0x05])); + expect(result).toBe(BigInt(5)); + }); + }); + + describe('getDecodedUserAccountData', () => { + it('should return null for invalid base64', () => { + const result = StateChangesDecoder['getDecodedUserAccountData']('invalidbase64'); + expect(result).toBeNull(); + }); + + it('should decode valid UserAccountData', () => { + const base64Value = "EgoABtuSP3tqxyU+GiCFwOGSJGrw8HkLeDkHs5wxnTbRY6P4C446d2BIMwFnjyIgo7jh8DROSNY3jtZL6vVOdbo/PP+O+6q/7dB+r0GfmDwqIAAAAAAAAAAABQCTvw9+sklA7qZm53+p9b3ttaRSUskZMgkAIko5RBEBXgA6IN7lba/irh34wYBfK8KRIY7O4AQNJba8jH/P1hXXPckZSgIBAA==" + const result = StateChangesDecoder['getDecodedUserAccountData'](base64Value); + + const expectedResult = { + nonce: 0, + balance: '126502242682468246846', + developerReward: '2470850310072000000', + address: 'erd1qqqqqqqqqqqqqpgqjwls7l4jf9qwafnxual6nadaak66g5jjeyvs9dswkt', + ownerAddress: 'erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7', + codeHash: 'hcDhkiRq8PB5C3g5B7OcMZ020WOj+AuOOndgSDMBZ48=', + rootHash: 'o7jh8DROSNY3jtZL6vVOdbo/PP+O+6q/7dB+r0GfmDw=', + codeMetadata: '0100' + } + expect(result).toHaveProperty('nonce', expectedResult.nonce); + expect(result).toHaveProperty('balance', expectedResult.balance); + expect(result).toHaveProperty('developerReward', expectedResult.developerReward); + expect(result).toHaveProperty('address', expectedResult.address); + expect(result).toHaveProperty('ownerAddress', expectedResult.ownerAddress); + expect(result).toHaveProperty('codeHash', expectedResult.codeHash); + expect(result).toHaveProperty('rootHash', expectedResult.rootHash); + expect(result).toHaveProperty('codeMetadata', expectedResult.codeMetadata); + }); + }); + + describe('decodeAccountChanges', () => { + it('should decode all flags correctly', () => { + const result = StateChangesDecoder['decodeAccountChanges']( + AccountChangesRaw.BalanceChanged | AccountChangesRaw.RootHashChanged + ); + expect(result.balanceChanged).toBe(true); + expect(result.rootHashChanged).toBe(true); + expect(result.nonceChanged).toBe(false); + }); + + it('should handle undefined flags', () => { + const result = StateChangesDecoder['decodeAccountChanges'](undefined); + expect(result.balanceChanged).toBe(false); + }); + }); + + describe('getDecodedEsdtData', () => { + it('should return null for invalid key prefix', () => { + const dataTrieChange = { key: Buffer.from('BADKEY').toString('base64'), val: 'AA==', version: 1 }; + const result = StateChangesDecoder['getDecodedEsdtData']('erd1test', dataTrieChange as any); + expect(result).toBeNull(); + }); + + it('should decode valid ESDT data', () => { + const expectedResult = { + identifier: 'MEX-a659d0', + nonce: '0', + type: 0, + value: '135399426293137262324524632', + propertiesHex: '', + reservedHex: '', + tokenMetaData: null + } + + const dataTrieChange = { + type: 1, + key: 'RUxST05EZXNkdE1FWC1hNjU5ZDA=', + val: 'EgwAb//xm2Vec+YQplg=', + version: 1, + operation: DataTrieChangeOperation.NotDelete + } + + const result = StateChangesDecoder['getDecodedEsdtData']('erd150sh7scpm4q7tdtntte975kt0cgg3r4exf8mtwurfradguzxzuqsahzma8', dataTrieChange as any); + expect(result).toEqual(expectedResult); + }); + }); + + describe('isNewAccount', () => { + it('should detect new account correctly', () => { + const result = StateChangesDecoder['isNewAccount']({ + accountChanges: undefined, + operation: StateAccessOperation.SaveAccount, + } as any); + expect(result).toBe(true); + }); + + it('should detect existing account correctly', () => { + const result = StateChangesDecoder['isNewAccount']({ + accountChanges: 1, + operation: 0, + } as any); + expect(result).toBe(false); + }); + }); + + describe('getAccountFinalState', () => { + it('should get final account state from block state accesses', () => { + const mockAccountStateAccesses = [ + { + type: 1, + index: 1, + txHash: 'zvAPJGf0O/fbqGo5eq9kqs2AguViaEYvyEZmiTQDDwE=', + mainTrieKey: 'AAAAAAAAAAAFAHStkhZzzH/1idZo4AjPvzAQydYdiBc=', + mainTrieVal: 'EgIAABog9S1Hs2tTj7zA+JLDTnD29ncfZtRsxevT36irfG9yrWIiIBEmI7nptDIBxcYWKgu6Jou5jHnvmOIOj1vjVDHFPMX3KiAAAAAAAAAAAAUAdK2SFnPMf/WJ1mjgCM+/MBDJ1h2IFzIIACD1dWNCKQA6IE+qi6TxMzNnwNuQ8fhDq6VBoT9FgTUSb6bVix2DrIgXSgIFAA==', + operation: 2, + dataTrieChanges: [ + { + type: 1, + key: 'RUxST05EZXNkdEZPWFNZLTg2ZWNmZQ==', + val: 'EgkADeC2s6dkAAA=', + version: 1, + operation: 0 + }, + { + type: 1, + key: 'RUxST05EZXNkdEZPWFNZLTg2ZWNmZQ==', + val: 'EgkADeC2s6dkAAA=', + version: 1, + operation: 1 + }, + { + type: 1, + key: 'dG91cm5hbWVudHMAAAACnPg=', + val: 'AAAAApz4AAAAAGkLbEgAAAAAaQtuoAAAAAgN4Lazp2QAAAAAAAgN4Lazp2QAAAAAAAgpoiQa9iwAAABuAVv6I33/nQnW+3wmlLfavrSvstjnldid65XTjQ3nqgAAAAXwE1AptY3LtZdlBeh6mcP3eiC1CGvqqv/3e6SOTNq3cgAAAALsE8pgwEzgJ+Nk3sKxnZ6OQQ6s/5JhxCXnioUMFmdfpcqTAAAAAuwUQUO2CdnII2pbbgiyzP4TjqaZ99MUJtkS4/wxY6YkOxQAAAAC7BW8zrxROav1ST0TIld62AKhGxMYVMoJVvzDxYCXrvzY7gAAAALsFru2lwb0jw3/9QMkG67zHFB792NjLXypEOW/9DcOhiUMAAAAAuwX', + version: 1, + operation: 0 + } + ], + accountChanges: 24 + } + ] + + const expectedResult = { + accountState: { + nonce: 0, + balance: '0', + developerReward: '9277083780000000', + address: 'erd1qqqqqqqqqqqqqpgqwjkey9nne3lltzwkdrsq3nalxqgvn4sa3qtse7d6nx', + ownerAddress: 'erd1f74ghf83xvek0sxmjrclssat54q6z069sy63ymax6k93mqav3qtsp2rv0l', + codeHash: '9S1Hs2tTj7zA+JLDTnD29ncfZtRsxevT36irfG9yrWI=', + rootHash: 'ESYjuem0MgHFxhYqC7omi7mMee+Y4g6PW+NUMcU8xfc=', + codeMetadata: '0500' + }, + esdtState: { + Fungible: [ + { + identifier: 'FOXSY-86ecfe', + nonce: '0', + type: 0, + value: '0', + propertiesHex: '', + reservedHex: '', + tokenMetaData: null + } + ], + NonFungible: [], + NonFungibleV2: [], + SemiFungible: [], + MetaFungible: [], + DynamicNFT: [], + DynamicSFT: [], + DynamicMeta: [] + }, + accountChanges: { + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: true, + developerRewardChanged: true, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false + }, + isNewAccount: false + } + const result = StateChangesDecoder['getAccountFinalState']('erd1dwkr89z4mmqxxgrv0ks62pccmqsheqq3zjwpa7r7fh6v5dgnrmjs8a9wng', mockAccountStateAccesses); + expect(result).toEqual(expectedResult); + }); + }); +}); From adb9022ef04a6474d5c7d2e2b8cea7405e0f861c Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 7 Nov 2025 11:15:55 +0200 Subject: [PATCH 81/90] add flag for esdt computation --- config/config.devnet-old.yaml | 1 + config/config.devnet.yaml | 1 + config/config.e2e-mocked.mainnet.yaml | 1 + config/config.e2e.mainnet.yaml | 1 + config/config.mainnet.yaml | 1 + config/config.testnet.yaml | 1 + src/common/api-config/api.config.service.ts | 4 + .../state.changes.consumer.service.ts | 143 ++++++++++++------ .../utils/state-changes.decoder.ts | 11 +- .../services/state-changes.consumer.spec.ts | 1 + .../unit/utils/state-changes.decoder.spec.ts | 2 +- 11 files changed, 115 insertions(+), 52 deletions(-) diff --git a/config/config.devnet-old.yaml b/config/config.devnet-old.yaml index 7129c6e75..9e14d00bf 100644 --- a/config/config.devnet-old.yaml +++ b/config/config.devnet-old.yaml @@ -31,6 +31,7 @@ features: exchange: 'state_accesses' queueName: 'api_state_accesses_queue-test' deadLetterExchange: 'api_state_accesses_queue_dlx' + esdtEnabled: false eventsNotifier: enabled: false port: 5674 diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index eac143e39..1eba6f527 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -30,6 +30,7 @@ features: exchange: 'state_accesses' queueName: 'api_state_accesses_queue-test' deadLetterExchange: 'api_state_accesses_queue_dlx' + esdtEnabled: false eventsNotifier: enabled: false port: 5674 diff --git a/config/config.e2e-mocked.mainnet.yaml b/config/config.e2e-mocked.mainnet.yaml index 92db6ef92..562e5d00d 100644 --- a/config/config.e2e-mocked.mainnet.yaml +++ b/config/config.e2e-mocked.mainnet.yaml @@ -15,6 +15,7 @@ features: exchange: 'state_accesses' queueName: 'api_state_accesses_queue-test' deadLetterExchange: 'api_state_accesses_queue_dlx' + esdtEnabled: false dataApi: enabled: false serviceUrl: 'https://data-api.multiversx.com' diff --git a/config/config.e2e.mainnet.yaml b/config/config.e2e.mainnet.yaml index 3d8f6e391..775c3f5c9 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -30,6 +30,7 @@ features: exchange: 'state_accesses' queueName: 'api_state_accesses_queue-test' deadLetterExchange: 'api_state_accesses_queue_dlx' + esdtEnabled: false eventsNotifier: enabled: false port: 5674 diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index 8a713f398..0467b53d7 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -35,6 +35,7 @@ features: exchange: 'state_accesses' queueName: 'api_state_accesses_queue-test' deadLetterExchange: 'api_state_accesses_queue_dlx' + esdtEnabled: false guestCaching: enabled: false hitsThreshold: 100 diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 7f732f0fb..98e98d91e 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -30,6 +30,7 @@ features: exchange: 'state_accesses' queueName: 'api_state_accesses_queue-test' deadLetterExchange: 'api_state_accesses_queue_dlx' + esdtEnabled: false eventsNotifier: enabled: false port: 5674 diff --git a/src/common/api-config/api.config.service.ts b/src/common/api-config/api.config.service.ts index 571eb831d..3356f5003 100644 --- a/src/common/api-config/api.config.service.ts +++ b/src/common/api-config/api.config.service.ts @@ -1020,6 +1020,10 @@ export class ApiConfigService { return queueName; } + isEsdtComputationEnabled(): boolean { + return this.configService.get('features.stateChanges.esdtEnabled') ?? false; + } + getStateChangesDeadLetterExchange(): string { const deadLetterExchange = this.configService.get('features.stateChanges.deadLetterExchange'); if (!deadLetterExchange) { diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 626c7ff61..caedaa708 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -13,6 +13,8 @@ import { AddressUtils, OriginLogger } from "@multiversx/sdk-nestjs-common"; import { PerformanceProfiler } from "@multiversx/sdk-nestjs-monitoring"; import configuration from "config/configuration"; import { ApiConfigService } from "src/common/api-config/api.config.service"; +import { NftAccount } from "src/endpoints/nfts/entities/nft.account"; +import { TokenWithBalance } from "src/endpoints/tokens/entities/token.with.balance"; @Injectable() export class StateChangesConsumerService { @@ -38,9 +40,10 @@ export class StateChangesConsumerService { const profiler = new PerformanceProfiler('BlockStateChangesProcessing'); const decodingProfiler = new PerformanceProfiler('StateChangesDecoding'); - const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); + const isEsdtComputationEnabled = this.apiConfigService.isEsdtComputationEnabled(); + const finalStates = this.decodeStateChangesFinal(blockWithStateChanges, isEsdtComputationEnabled); - const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs); + const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs, isEsdtComputationEnabled); decodingProfiler.stop('StateChangesDecoding'); this.logger.log(`Decoded state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID} in ${decodingProfiler.duration} ms`); @@ -97,63 +100,109 @@ export class StateChangesConsumerService { await Promise.all(promisesToWaitFor); } - private decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { - return StateChangesDecoder.decodeStateChangesFinal(blockWithStateChanges); + private decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw, isEsdtComputationEnabled: boolean = false) { + return StateChangesDecoder.decodeStateChangesFinal(blockWithStateChanges, isEsdtComputationEnabled); } - private transformFinalStatesToDbFormat(finalStates: Record, shardID: number, blockTimestampMs: number) { + private transformFinalStatesToDbFormat( + finalStates: Record, + shardID: number, + blockTimestampMs: number, + isEsdtComputationEnabled: boolean = false + ) { const transformed: AccountDetails[] = []; for (const [_address, state] of Object.entries(finalStates)) { - const newAccountState = state.accountState; - - // const tokens = [ - // ...state.esdtState.Fungible, - // ]; - - // const nfts = [ - // ...state.esdtState.NonFungible, - // ...state.esdtState.NonFungibleV2, - // ...state.esdtState.DynamicNFT, - // ...state.esdtState.SemiFungible, - // ...state.esdtState.DynamicSFT, - // ...state.esdtState.MetaFungible, - // ...state.esdtState.DynamicMeta, - // ]; - - if (newAccountState) { - const { codeMetadata, ...newAccountStateFiltered } = newAccountState; - const parsedAccount = - new AccountDetails({ - ...newAccountStateFiltered, - shard: shardID, - timestampMs: blockTimestampMs, - timestamp: Math.floor(blockTimestampMs / 1000), - ...this.parseCodeMetadata(newAccountState.address, codeMetadata), - // tokens: tokens.map(token => new TokenWithBalance({ - // identifier: token.identifier, - // nonce: parseInt(token.nonce), - // balance: token.value, - // type: this.parseEsdtType(token.type) as TokenType, - // subType: NftSubType.None, - // })), - // nfts: nfts.map(nft => new NftAccount({ - // identifier: nft.identifier, - // nonce: parseInt(nft.nonce), - // type: this.parseEsdtType(nft.type) as NftType, - // subType: this.parseEsdtSubtype(nft.type), - // collection: nft.identifier.replace(/-[^-]*$/, ''), // delete everything after last `-` character inclusive - // balance: nft.value, - // })), - }); - transformed.push(parsedAccount); + const baseAccount = this.parseBaseAccount( + state, + shardID, + blockTimestampMs + ); + if (!baseAccount) continue; + + const parsedAccount = new AccountDetails({ + ...baseAccount, + }); + + if (isEsdtComputationEnabled) { + const tokens = this.transformTokens(state); + const nfts = this.transformNfts(state); + parsedAccount.tokens = tokens; + parsedAccount.nfts = nfts; } + transformed.push(parsedAccount); } return transformed; } + private parseBaseAccount( + state: StateChanges, + shardID: number, + blockTimestampMs: number + ) { + if (!state.accountState) return undefined; + const { codeMetadata, ...filteredState } = state.accountState; + + return { + ...filteredState, + shard: shardID, + timestampMs: blockTimestampMs, + timestamp: Math.floor(blockTimestampMs / 1000), + ...this.parseCodeMetadata(filteredState.address, codeMetadata), + }; + } + + //@ts-ignore + private transformTokens(state: StateChanges): TokenWithBalance[] { + const fungible = state.esdtState?.Fungible ?? []; + + return fungible.map(token => + new TokenWithBalance({ + identifier: token.identifier, + nonce: parseInt(token.nonce), + balance: token.value, + type: this.parseEsdtType(token.type) as TokenType, + subType: NftSubType.None, + }) + ); + } + + //@ts-ignore + private transformNfts(state: StateChanges): NftAccount[] { + const { + NonFungible, + NonFungibleV2, + DynamicNFT, + SemiFungible, + DynamicSFT, + MetaFungible, + DynamicMeta, + } = state.esdtState ?? {}; + + const allNfts = [ + ...(NonFungible ?? []), + ...(NonFungibleV2 ?? []), + ...(DynamicNFT ?? []), + ...(SemiFungible ?? []), + ...(DynamicSFT ?? []), + ...(MetaFungible ?? []), + ...(DynamicMeta ?? []), + ]; + + return allNfts.map(nft => + new NftAccount({ + identifier: nft.identifier, + nonce: parseInt(nft.nonce), + type: this.parseEsdtType(nft.type) as NftType, + subType: this.parseEsdtSubtype(nft.type), + collection: nft.identifier.replace(/-[^-]*$/, ''), + balance: nft.value, + }) + ); + } + private deleteLocalCache(cacheKeys: string[]) { this.clientProxy.emit('deleteCacheKeys', cacheKeys); diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts index b8e3413de..b8c886839 100644 --- a/src/state-changes/utils/state-changes.decoder.ts +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -263,7 +263,7 @@ export class StateChangesDecoder { return allAccounts; } - static decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { + static decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw, isEsdtComputationEnabled: boolean = false) { const accounts = blockWithStateChanges.stateAccessesPerAccounts; const finalStates: Record = {}; @@ -274,14 +274,14 @@ export class StateChangesDecoder { continue; } const { stateAccess: accountStateAccesses } = accounts[accountHex] || {}; - finalStates[address] = this.getAccountFinalState(address, accountStateAccesses) + finalStates[address] = this.getAccountFinalState(address, accountStateAccesses, isEsdtComputationEnabled) } return finalStates; } - private static getAccountFinalState(address: string, accountStateAccesses: StateAccessPerAccountRaw[]) { + private static getAccountFinalState(address: string, accountStateAccesses: StateAccessPerAccountRaw[], isEsdtComputationEnabled: boolean = false) { let finalAccountChangesRaw: AccountChangesRaw = AccountChangesRaw.NoChange; const esdtOccured: Record = {}; let finalNewAccount = false; @@ -318,11 +318,14 @@ export class StateChangesDecoder { if (decodedAccountData) { finalAccountState = decodedAccountData; + if (!isEsdtComputationEnabled) { + break; + } } } const dataTrieChanges = stateAccess.dataTrieChanges; - if (dataTrieChanges) { + if (dataTrieChanges && isEsdtComputationEnabled) { for (let i = dataTrieChanges.length - 1; i >= 0; i--) { const dataTrieChange = dataTrieChanges[i]; if (dataTrieChange.version !== 0 && dataTrieChange.val) { diff --git a/src/test/unit/services/state-changes.consumer.spec.ts b/src/test/unit/services/state-changes.consumer.spec.ts index 73f4e52bf..19afbac1d 100644 --- a/src/test/unit/services/state-changes.consumer.spec.ts +++ b/src/test/unit/services/state-changes.consumer.spec.ts @@ -73,6 +73,7 @@ describe('StateChangesConsumerService', () => { provide: ApiConfigService, useValue: { getMetaChainShardId: jest.fn().mockReturnValue(4294967295), + isEsdtComputationEnabled: jest.fn().mockReturnValue(false) }, }, { diff --git a/src/test/unit/utils/state-changes.decoder.spec.ts b/src/test/unit/utils/state-changes.decoder.spec.ts index 4956ab6c6..df2e487fa 100644 --- a/src/test/unit/utils/state-changes.decoder.spec.ts +++ b/src/test/unit/utils/state-changes.decoder.spec.ts @@ -211,7 +211,7 @@ describe('StateChangesDecoder', () => { }, isNewAccount: false } - const result = StateChangesDecoder['getAccountFinalState']('erd1dwkr89z4mmqxxgrv0ks62pccmqsheqq3zjwpa7r7fh6v5dgnrmjs8a9wng', mockAccountStateAccesses); + const result = StateChangesDecoder['getAccountFinalState']('erd1dwkr89z4mmqxxgrv0ks62pccmqsheqq3zjwpa7r7fh6v5dgnrmjs8a9wng', mockAccountStateAccesses, true); expect(result).toEqual(expectedResult); }); }); From b9b86f3258ddf528204f3c999ecaa4e131619c82 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 7 Nov 2025 11:38:31 +0200 Subject: [PATCH 82/90] run tests towards feat or features branches --- .github/workflows/chain-simulator-e2e-tests.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/load-tests.yml | 2 +- .github/workflows/unit.tests.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/chain-simulator-e2e-tests.yml b/.github/workflows/chain-simulator-e2e-tests.yml index 4d0131a48..5ae37f9a6 100644 --- a/.github/workflows/chain-simulator-e2e-tests.yml +++ b/.github/workflows/chain-simulator-e2e-tests.yml @@ -4,7 +4,7 @@ on: push: branches: [main, development] pull_request: - branches: [main, development] + branches: [main, development, feat/*, feature/*] jobs: test-chainsimulator-e2e: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bda7f1a37..b3083a236 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ on: push: branches: [main, development] pull_request: - branches: [main, development] + branches: [main, development, feat/*, feature/*] jobs: build: diff --git a/.github/workflows/load-tests.yml b/.github/workflows/load-tests.yml index 50568016a..ddda4c046 100644 --- a/.github/workflows/load-tests.yml +++ b/.github/workflows/load-tests.yml @@ -2,7 +2,7 @@ name: Load Tests on: pull_request: - branches: [main, development] + branches: [main, development, feat/*, feature/*] jobs: test-base: diff --git a/.github/workflows/unit.tests.yml b/.github/workflows/unit.tests.yml index d53e29e0e..c2bf4f559 100644 --- a/.github/workflows/unit.tests.yml +++ b/.github/workflows/unit.tests.yml @@ -7,7 +7,7 @@ on: push: branches: [main, development] pull_request: - branches: [main, development] + branches: [main, development, feat/*, feature/*] jobs: build: From 49bffbab485762badaea36fd3a062ca52e03582e Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 7 Nov 2025 11:40:16 +0200 Subject: [PATCH 83/90] fix lint --- .../utils/state-changes.decoder.ts | 4 +- .../services/state-changes.consumer.spec.ts | 50 +++++++++---------- .../unit/utils/state-changes.decoder.spec.ts | 44 ++++++++-------- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts index b8c886839..85abefc50 100644 --- a/src/state-changes/utils/state-changes.decoder.ts +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -274,7 +274,7 @@ export class StateChangesDecoder { continue; } const { stateAccess: accountStateAccesses } = accounts[accountHex] || {}; - finalStates[address] = this.getAccountFinalState(address, accountStateAccesses, isEsdtComputationEnabled) + finalStates[address] = this.getAccountFinalState(address, accountStateAccesses, isEsdtComputationEnabled); } return finalStates; @@ -307,7 +307,7 @@ export class StateChangesDecoder { // if we already found it as new account, we skip the computation if (!finalNewAccount) { - const currentNewAccount = this.isNewAccount(stateAccess) + const currentNewAccount = this.isNewAccount(stateAccess); finalNewAccount = currentNewAccount || finalNewAccount; } diff --git a/src/test/unit/services/state-changes.consumer.spec.ts b/src/test/unit/services/state-changes.consumer.spec.ts index 19afbac1d..b75906b03 100644 --- a/src/test/unit/services/state-changes.consumer.spec.ts +++ b/src/test/unit/services/state-changes.consumer.spec.ts @@ -73,7 +73,7 @@ describe('StateChangesConsumerService', () => { provide: ApiConfigService, useValue: { getMetaChainShardId: jest.fn().mockReturnValue(4294967295), - isEsdtComputationEnabled: jest.fn().mockReturnValue(false) + isEsdtComputationEnabled: jest.fn().mockReturnValue(false), }, }, { @@ -232,7 +232,7 @@ describe('StateChangesConsumerService', () => { ownerAddress: 'erd1rc5p5drg26vggn6jx9puv6xlgka5n6ajm6cer554tzguwfm6v5ys2pr3pc', codeHash: 'hRkRk4eX3AxvdJgLHruGrel+Zb0uHudVXBHqWvfvz4Q=', rootHash: 'CKhf02sZIbMYtII8VbKqiLgTDEr0treTXEqIgHb1l4g=', - codeMetadata: '0500' + codeMetadata: '0500', }, esdtState: { Fungible: [], @@ -242,7 +242,7 @@ describe('StateChangesConsumerService', () => { MetaFungible: [], DynamicNFT: [], DynamicSFT: [], - DynamicMeta: [] + DynamicMeta: [], }, accountChanges: { nonceChanged: false, @@ -252,9 +252,9 @@ describe('StateChangesConsumerService', () => { developerRewardChanged: true, ownerAddressChanged: false, userNameChanged: false, - codeMetadataChanged: false + codeMetadataChanged: false, }, - isNewAccount: false + isNewAccount: false, }, erd1qqqqqqqqqqqqqpgqjwls7l4jf9qwafnxual6nadaak66g5jjeyvs9dswkt: { accountState: { @@ -265,7 +265,7 @@ describe('StateChangesConsumerService', () => { ownerAddress: 'erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7', codeHash: 'hcDhkiRq8PB5C3g5B7OcMZ020WOj+AuOOndgSDMBZ48=', rootHash: 'bjwTHfuhx0bs3OJFnvcb6pcTWspG9cb370yuqVbKbQo=', - codeMetadata: '0100' + codeMetadata: '0100', }, esdtState: { Fungible: [], @@ -275,7 +275,7 @@ describe('StateChangesConsumerService', () => { MetaFungible: [], DynamicNFT: [], DynamicSFT: [], - DynamicMeta: [] + DynamicMeta: [], }, accountChanges: { nonceChanged: false, @@ -285,9 +285,9 @@ describe('StateChangesConsumerService', () => { developerRewardChanged: true, ownerAddressChanged: false, userNameChanged: false, - codeMetadataChanged: false + codeMetadataChanged: false, }, - isNewAccount: false + isNewAccount: false, }, erd1vt2qedvltqvrar072ny88wh7vgxq9xvxmyqm4nf8qkzpwj6ncy5sjtgj90: { accountState: { @@ -295,7 +295,7 @@ describe('StateChangesConsumerService', () => { balance: '59732371650000000000', developerReward: '0', address: 'erd1vt2qedvltqvrar072ny88wh7vgxq9xvxmyqm4nf8qkzpwj6ncy5sjtgj90', - rootHash: 's9P9Do2C9v8r3MqG3I3MkotuYpEA1qt8jNiRCE5ULBo=' + rootHash: 's9P9Do2C9v8r3MqG3I3MkotuYpEA1qt8jNiRCE5ULBo=', }, esdtState: { Fungible: [], @@ -305,7 +305,7 @@ describe('StateChangesConsumerService', () => { MetaFungible: [], DynamicNFT: [], DynamicSFT: [], - DynamicMeta: [] + DynamicMeta: [], }, accountChanges: { nonceChanged: true, @@ -315,9 +315,9 @@ describe('StateChangesConsumerService', () => { developerRewardChanged: false, ownerAddressChanged: false, userNameChanged: false, - codeMetadataChanged: false + codeMetadataChanged: false, }, - isNewAccount: false + isNewAccount: false, }, erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7: { accountState: { @@ -325,7 +325,7 @@ describe('StateChangesConsumerService', () => { balance: '391457061919320000000', developerReward: '0', address: 'erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7', - rootHash: 'fV3JuZDrwZ8TbnlawkHGYYp1bQX0fTgefUzU7xEYLh8=' + rootHash: 'fV3JuZDrwZ8TbnlawkHGYYp1bQX0fTgefUzU7xEYLh8=', }, esdtState: { Fungible: [], @@ -335,7 +335,7 @@ describe('StateChangesConsumerService', () => { MetaFungible: [], DynamicNFT: [], DynamicSFT: [], - DynamicMeta: [] + DynamicMeta: [], }, accountChanges: { nonceChanged: true, @@ -345,11 +345,11 @@ describe('StateChangesConsumerService', () => { developerRewardChanged: false, ownerAddressChanged: false, userNameChanged: false, - codeMetadataChanged: false + codeMetadataChanged: false, }, - isNewAccount: false - } - } + isNewAccount: false, + }, + }; const mockShardId = 1; const mockBlockTimestampMs = 1762356608000; const expectedResult = [ @@ -367,7 +367,7 @@ describe('StateChangesConsumerService', () => { isUpgradeable: true, isReadable: true, isPayable: false, - isPayableBySmartContract: false + isPayableBySmartContract: false, }, { address: 'erd1qqqqqqqqqqqqqpgqjwls7l4jf9qwafnxual6nadaak66g5jjeyvs9dswkt', @@ -383,7 +383,7 @@ describe('StateChangesConsumerService', () => { isUpgradeable: true, isReadable: false, isPayable: false, - isPayableBySmartContract: false + isPayableBySmartContract: false, }, { address: 'erd1vt2qedvltqvrar072ny88wh7vgxq9xvxmyqm4nf8qkzpwj6ncy5sjtgj90', @@ -393,7 +393,7 @@ describe('StateChangesConsumerService', () => { timestamp: 1762356608, shard: 1, developerReward: '0', - rootHash: 's9P9Do2C9v8r3MqG3I3MkotuYpEA1qt8jNiRCE5ULBo=' + rootHash: 's9P9Do2C9v8r3MqG3I3MkotuYpEA1qt8jNiRCE5ULBo=', }, { address: 'erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7', @@ -403,9 +403,9 @@ describe('StateChangesConsumerService', () => { timestamp: 1762356608, shard: 1, developerReward: '0', - rootHash: 'fV3JuZDrwZ8TbnlawkHGYYp1bQX0fTgefUzU7xEYLh8=' - } - ] + rootHash: 'fV3JuZDrwZ8TbnlawkHGYYp1bQX0fTgefUzU7xEYLh8=', + }, + ]; const result = service['transformFinalStatesToDbFormat']( mockInput, mockShardId, diff --git a/src/test/unit/utils/state-changes.decoder.spec.ts b/src/test/unit/utils/state-changes.decoder.spec.ts index df2e487fa..997e6b3a8 100644 --- a/src/test/unit/utils/state-changes.decoder.spec.ts +++ b/src/test/unit/utils/state-changes.decoder.spec.ts @@ -1,5 +1,5 @@ import { StateChangesDecoder } from 'src/state-changes/utils/state-changes.decoder'; -import { AccountChangesRaw, DataTrieChangeOperation, StateAccessOperation } from 'src/state-changes/entities' +import { AccountChangesRaw, DataTrieChangeOperation, StateAccessOperation } from 'src/state-changes/entities'; describe('StateChangesDecoder', () => { beforeEach(() => { @@ -42,7 +42,7 @@ describe('StateChangesDecoder', () => { }); it('should decode valid UserAccountData', () => { - const base64Value = "EgoABtuSP3tqxyU+GiCFwOGSJGrw8HkLeDkHs5wxnTbRY6P4C446d2BIMwFnjyIgo7jh8DROSNY3jtZL6vVOdbo/PP+O+6q/7dB+r0GfmDwqIAAAAAAAAAAABQCTvw9+sklA7qZm53+p9b3ttaRSUskZMgkAIko5RBEBXgA6IN7lba/irh34wYBfK8KRIY7O4AQNJba8jH/P1hXXPckZSgIBAA==" + const base64Value = "EgoABtuSP3tqxyU+GiCFwOGSJGrw8HkLeDkHs5wxnTbRY6P4C446d2BIMwFnjyIgo7jh8DROSNY3jtZL6vVOdbo/PP+O+6q/7dB+r0GfmDwqIAAAAAAAAAAABQCTvw9+sklA7qZm53+p9b3ttaRSUskZMgkAIko5RBEBXgA6IN7lba/irh34wYBfK8KRIY7O4AQNJba8jH/P1hXXPckZSgIBAA=="; const result = StateChangesDecoder['getDecodedUserAccountData'](base64Value); const expectedResult = { @@ -53,8 +53,8 @@ describe('StateChangesDecoder', () => { ownerAddress: 'erd1mmjkmtlz4cwl3svqtu4u9yfp3m8wqpqdykmterrleltpt4eaeyvsa68xa7', codeHash: 'hcDhkiRq8PB5C3g5B7OcMZ020WOj+AuOOndgSDMBZ48=', rootHash: 'o7jh8DROSNY3jtZL6vVOdbo/PP+O+6q/7dB+r0GfmDw=', - codeMetadata: '0100' - } + codeMetadata: '0100', + }; expect(result).toHaveProperty('nonce', expectedResult.nonce); expect(result).toHaveProperty('balance', expectedResult.balance); expect(result).toHaveProperty('developerReward', expectedResult.developerReward); @@ -97,16 +97,16 @@ describe('StateChangesDecoder', () => { value: '135399426293137262324524632', propertiesHex: '', reservedHex: '', - tokenMetaData: null - } + tokenMetaData: null, + }; const dataTrieChange = { type: 1, key: 'RUxST05EZXNkdE1FWC1hNjU5ZDA=', val: 'EgwAb//xm2Vec+YQplg=', version: 1, - operation: DataTrieChangeOperation.NotDelete - } + operation: DataTrieChangeOperation.NotDelete, + }; const result = StateChangesDecoder['getDecodedEsdtData']('erd150sh7scpm4q7tdtntte975kt0cgg3r4exf8mtwurfradguzxzuqsahzma8', dataTrieChange as any); expect(result).toEqual(expectedResult); @@ -147,26 +147,26 @@ describe('StateChangesDecoder', () => { key: 'RUxST05EZXNkdEZPWFNZLTg2ZWNmZQ==', val: 'EgkADeC2s6dkAAA=', version: 1, - operation: 0 + operation: 0, }, { type: 1, key: 'RUxST05EZXNkdEZPWFNZLTg2ZWNmZQ==', val: 'EgkADeC2s6dkAAA=', version: 1, - operation: 1 + operation: 1, }, { type: 1, key: 'dG91cm5hbWVudHMAAAACnPg=', val: 'AAAAApz4AAAAAGkLbEgAAAAAaQtuoAAAAAgN4Lazp2QAAAAAAAgN4Lazp2QAAAAAAAgpoiQa9iwAAABuAVv6I33/nQnW+3wmlLfavrSvstjnldid65XTjQ3nqgAAAAXwE1AptY3LtZdlBeh6mcP3eiC1CGvqqv/3e6SOTNq3cgAAAALsE8pgwEzgJ+Nk3sKxnZ6OQQ6s/5JhxCXnioUMFmdfpcqTAAAAAuwUQUO2CdnII2pbbgiyzP4TjqaZ99MUJtkS4/wxY6YkOxQAAAAC7BW8zrxROav1ST0TIld62AKhGxMYVMoJVvzDxYCXrvzY7gAAAALsFru2lwb0jw3/9QMkG67zHFB792NjLXypEOW/9DcOhiUMAAAAAuwX', version: 1, - operation: 0 - } + operation: 0, + }, ], - accountChanges: 24 - } - ] + accountChanges: 24, + }, + ]; const expectedResult = { accountState: { @@ -177,7 +177,7 @@ describe('StateChangesDecoder', () => { ownerAddress: 'erd1f74ghf83xvek0sxmjrclssat54q6z069sy63ymax6k93mqav3qtsp2rv0l', codeHash: '9S1Hs2tTj7zA+JLDTnD29ncfZtRsxevT36irfG9yrWI=', rootHash: 'ESYjuem0MgHFxhYqC7omi7mMee+Y4g6PW+NUMcU8xfc=', - codeMetadata: '0500' + codeMetadata: '0500', }, esdtState: { Fungible: [ @@ -188,8 +188,8 @@ describe('StateChangesDecoder', () => { value: '0', propertiesHex: '', reservedHex: '', - tokenMetaData: null - } + tokenMetaData: null, + }, ], NonFungible: [], NonFungibleV2: [], @@ -197,7 +197,7 @@ describe('StateChangesDecoder', () => { MetaFungible: [], DynamicNFT: [], DynamicSFT: [], - DynamicMeta: [] + DynamicMeta: [], }, accountChanges: { nonceChanged: false, @@ -207,10 +207,10 @@ describe('StateChangesDecoder', () => { developerRewardChanged: true, ownerAddressChanged: false, userNameChanged: false, - codeMetadataChanged: false + codeMetadataChanged: false, }, - isNewAccount: false - } + isNewAccount: false, + }; const result = StateChangesDecoder['getAccountFinalState']('erd1dwkr89z4mmqxxgrv0ks62pccmqsheqq3zjwpa7r7fh6v5dgnrmjs8a9wng', mockAccountStateAccesses, true); expect(result).toEqual(expectedResult); }); From 74678650e5b2e9fe6f5e011e55a20a903bbdcb78 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 7 Nov 2025 14:19:12 +0200 Subject: [PATCH 84/90] add todo --- src/state-changes/utils/state-changes.decoder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/state-changes/utils/state-changes.decoder.ts b/src/state-changes/utils/state-changes.decoder.ts index 85abefc50..a9593bd83 100644 --- a/src/state-changes/utils/state-changes.decoder.ts +++ b/src/state-changes/utils/state-changes.decoder.ts @@ -318,6 +318,7 @@ export class StateChangesDecoder { if (decodedAccountData) { finalAccountState = decodedAccountData; + //TODO: remove when we want to use accountChanges if (!isEsdtComputationEnabled) { break; } From 36a88ed621ef94277c6a463e148b295aa7871519 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Fri, 7 Nov 2025 14:23:36 +0200 Subject: [PATCH 85/90] fixes --- src/state-changes/state.changes.consumer.service.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index caedaa708..8f684bf11 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -40,10 +40,9 @@ export class StateChangesConsumerService { const profiler = new PerformanceProfiler('BlockStateChangesProcessing'); const decodingProfiler = new PerformanceProfiler('StateChangesDecoding'); - const isEsdtComputationEnabled = this.apiConfigService.isEsdtComputationEnabled(); - const finalStates = this.decodeStateChangesFinal(blockWithStateChanges, isEsdtComputationEnabled); - const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs, isEsdtComputationEnabled); + const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); + const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs); decodingProfiler.stop('StateChangesDecoding'); this.logger.log(`Decoded state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID} in ${decodingProfiler.duration} ms`); @@ -100,7 +99,8 @@ export class StateChangesConsumerService { await Promise.all(promisesToWaitFor); } - private decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw, isEsdtComputationEnabled: boolean = false) { + private decodeStateChangesFinal(blockWithStateChanges: BlockWithStateChangesRaw) { + const isEsdtComputationEnabled = this.apiConfigService.isEsdtComputationEnabled(); return StateChangesDecoder.decodeStateChangesFinal(blockWithStateChanges, isEsdtComputationEnabled); } @@ -108,8 +108,8 @@ export class StateChangesConsumerService { finalStates: Record, shardID: number, blockTimestampMs: number, - isEsdtComputationEnabled: boolean = false ) { + const isEsdtComputationEnabled = this.apiConfigService.isEsdtComputationEnabled(); const transformed: AccountDetails[] = []; for (const [_address, state] of Object.entries(finalStates)) { @@ -154,7 +154,6 @@ export class StateChangesConsumerService { }; } - //@ts-ignore private transformTokens(state: StateChanges): TokenWithBalance[] { const fungible = state.esdtState?.Fungible ?? []; @@ -169,7 +168,6 @@ export class StateChangesConsumerService { ); } - //@ts-ignore private transformNfts(state: StateChanges): NftAccount[] { const { NonFungible, From 0cb11d428559696ca4effcf99beab8ed0050d297 Mon Sep 17 00:00:00 2001 From: Gutica Stefan <123564494+stefangutica@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:17:45 +0200 Subject: [PATCH 86/90] esdts support (#1559) * esdts support * refactor esdts * fixes * fix unit tests * add more unit tests * fix lint * fixes * fix * lint --- src/common/indexer/db/index.ts | 1 - src/common/indexer/db/mongodb.module.ts | 82 ++-- .../indexer/db/mongodb.repositories.module.ts | 21 - .../account.details.repository.ts | 367 +----------------- .../repositories/esdt.details.repository.ts | 102 +++++ src/common/indexer/db/repositories/index.ts | 1 + .../db/schemas/account.details.schema.ts | 11 +- .../indexer/db/schemas/esdt.details.schema.ts | 30 ++ src/common/indexer/db/schemas/index.ts | 1 + .../accounts-v2/account.controller.v2.ts | 22 +- .../accounts-v2/account.service.v2.ts | 78 +++- .../state.changes.consumer.service.ts | 93 +++-- .../services/state-changes.consumer.spec.ts | 172 +++++++- src/utils/cache.info.ts | 4 +- 14 files changed, 512 insertions(+), 473 deletions(-) delete mode 100644 src/common/indexer/db/mongodb.repositories.module.ts create mode 100644 src/common/indexer/db/repositories/esdt.details.repository.ts create mode 100644 src/common/indexer/db/schemas/esdt.details.schema.ts diff --git a/src/common/indexer/db/index.ts b/src/common/indexer/db/index.ts index edbda0b95..37a3a8bd0 100644 --- a/src/common/indexer/db/index.ts +++ b/src/common/indexer/db/index.ts @@ -1,5 +1,4 @@ export * from './mongodb.module'; -export * from './mongodb.repositories.module'; export * from './repositories'; export * from './schemas'; export * from './utils'; diff --git a/src/common/indexer/db/mongodb.module.ts b/src/common/indexer/db/mongodb.module.ts index 50d57f2b2..4524742fe 100644 --- a/src/common/indexer/db/mongodb.module.ts +++ b/src/common/indexer/db/mongodb.module.ts @@ -1,49 +1,67 @@ -import { Module } from "@nestjs/common"; +import { Module, Provider } from "@nestjs/common"; import { MongooseModule } from "@nestjs/mongoose"; import { ApiConfigModule } from "src/common/api-config/api.config.module"; import { ApiConfigService } from "src/common/api-config/api.config.service"; -import { AccountDetails, AccountDetailsSchema } from "./schemas"; -import { AccountDetailsRepository } from "./repositories"; +import { AccountDetails, AccountDetailsSchema, EsdtDetails, EsdtDetailsSchema } from "./schemas"; +import { AccountDetailsRepository, EsdtDetailsRepository } from "./repositories"; import { EventEmitterModule } from "@nestjs/event-emitter"; import configuration from "config/configuration"; +const isPassThrough = + process.env.PERSISTENCE === 'passthrough' || + configuration()?.database?.enabled === false; -const isPassThrough = process.env.PERSISTENCE === 'passthrough' || configuration()?.database?.enabled === false; +const mongoImports = []; -const mongoImports = isPassThrough ? [] : [ - EventEmitterModule.forRoot({ maxListeners: 1 }), - MongooseModule.forRootAsync({ - imports: [ApiConfigModule], - inject: [ApiConfigService], - useFactory: (apiConfigService: ApiConfigService) => ({ - uri: apiConfigService.getDatabaseUrl().replace(":27017", ''), // TODO: remove this hack - tls: apiConfigService.isDatabaseTlsEnabled(), - tlsAllowInvalidCertificates: true, +if (!isPassThrough) { + mongoImports.push( + EventEmitterModule.forRoot({ maxListeners: 1 }), + MongooseModule.forRootAsync({ + imports: [ApiConfigModule], + inject: [ApiConfigService], + useFactory: (apiConfigService: ApiConfigService) => ({ + uri: apiConfigService.getDatabaseUrl().replace(":27017", ''), + tls: apiConfigService.isDatabaseTlsEnabled(), + tlsAllowInvalidCertificates: true, + }), }), - }), - MongooseModule.forFeature([ - { name: AccountDetails.name, schema: AccountDetailsSchema }, - ]), -]; + MongooseModule.forFeature([ + { name: AccountDetails.name, schema: AccountDetailsSchema }, + { name: EsdtDetails.name, schema: EsdtDetailsSchema }, + ]) + ); +} -const mongoProviders = isPassThrough ? [ - { - provide: AccountDetailsRepository, - useValue: { - getTokensForAddress: () => Promise.resolve([]), - getTokenForAddress: () => Promise.resolve(undefined), - getNftsForAddress: () => Promise.resolve([]), - getNftForAddress: () => Promise.resolve(undefined), - getAccount: () => Promise.resolve(null), - updateAccount: () => Promise.resolve(null), - updateAccounts: () => Promise.resolve([]), +const mongoProviders: Provider[] = []; + +if (isPassThrough) { + mongoProviders.push( + { + provide: AccountDetailsRepository, + useValue: { + getAccount: () => Promise.resolve(null), + updateAccount: () => Promise.resolve(null), + updateAccounts: () => Promise.resolve([]), + }, + }, + { + provide: EsdtDetailsRepository, + useValue: { + getEsdt: () => Promise.resolve(null), + updateEsdts: () => Promise.resolve([]), + }, }, - }, -] : [AccountDetailsRepository]; + ); +} else { + mongoProviders.push( + { provide: AccountDetailsRepository, useClass: AccountDetailsRepository }, + { provide: EsdtDetailsRepository, useClass: EsdtDetailsRepository }, + ); +} @Module({ imports: mongoImports, providers: mongoProviders, - exports: [AccountDetailsRepository], + exports: mongoProviders, }) export class MongoDbModule { } diff --git a/src/common/indexer/db/mongodb.repositories.module.ts b/src/common/indexer/db/mongodb.repositories.module.ts deleted file mode 100644 index ed2e75951..000000000 --- a/src/common/indexer/db/mongodb.repositories.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Global, Module } from "@nestjs/common"; -import { MongooseModule } from "@nestjs/mongoose"; -import { AccountDetails, AccountDetailsSchema } from "./schemas"; -import { AccountDetailsRepository } from "./repositories"; - - -@Global() -@Module({ - imports: [ - MongooseModule.forFeature([ - { name: AccountDetails.name, schema: AccountDetailsSchema }, - ]), - ], - providers: [ - AccountDetailsRepository, - ], - exports: [ - AccountDetailsRepository, - ], -}) -export class MongoDbRepositoriesModule { } diff --git a/src/common/indexer/db/repositories/account.details.repository.ts b/src/common/indexer/db/repositories/account.details.repository.ts index fbe7af186..b36b5b90e 100644 --- a/src/common/indexer/db/repositories/account.details.repository.ts +++ b/src/common/indexer/db/repositories/account.details.repository.ts @@ -3,9 +3,6 @@ import { LogPerformanceAsync } from 'src/utils/log.performance.decorator'; import { MetricsEvents } from 'src/utils/metrics-events.constants'; import { AccountDetails } from '../schemas'; import { InjectModel } from '@nestjs/mongoose'; -import { QueryPagination } from 'src/common/entities/query.pagination'; -import { TokenWithBalance } from 'src/endpoints/tokens/entities/token.with.balance'; -import { NftAccount } from 'src/endpoints/nfts/entities/nft.account'; import { Injectable } from '@nestjs/common'; import { AccountDetailed } from 'src/endpoints/accounts/entities/account.detailed'; import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @@ -13,193 +10,11 @@ import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @Injectable() export class AccountDetailsRepository { private readonly logger = new OriginLogger(AccountDetailsRepository.name); - static readonly exclusionFields = { - _id: 0, - __v: 0, - updatedAt: 0, - createdAt: 0, - address: 0, - balance: 0, - nonce: 0, - timestamp: 0, - shard: 0, - ownerAddress: 0, - assets: 0, - deployedAt: 0, - deployTxHash: 0, - ownerAssets: 0, - isVerified: 0, - txCount: 0, - scrCount: 0, - transfersLast24h: 0, - code: 0, - codeHash: 0, - rootHash: 0, - username: 0, - developerReward: 0, - isUpgradeable: 0, - isReadable: 0, isPayable: 0, - isPayableBySmartContract: 0, - scamInfo: 0, - nftCollections: 0, - activeGuardianActivationEpoch: 0, - activeGuardianAddress: 0, - activeGuardianServiceUid: 0, - pendingGuardianActivationEpoch: 0, - pendingGuardianAddress: 0, - pendingGuardianServiceUid: 0, - isGuarded: 0, - }; constructor( @InjectModel(AccountDetails.name) private readonly accountDetailsModel: Model ) { } - @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-tokens') - async getTokensForAddress(address: string, queryPagination: QueryPagination): Promise { - try { - // TODO: add more fields in project on demand - const result = await this.accountDetailsModel.aggregate([ - { $match: { address } }, - { - $project: { - _id: 0, - tokens: { - $slice: ["$tokens", queryPagination.from, queryPagination.size], - }, - }, - }, - { - $project: { - "tokens.type": 1, - "tokens.subType": 1, - "tokens.identifier": 1, - "tokens.collection": 1, - "tokens.name": 1, - "tokens.nonce": 1, - "tokens.decimals": 1, - "tokens.balance": 1, - }, - }, - ]).exec(); - return result[0]?.tokens ?? []; - } catch (error) { - console.error(`Error fetching tokens for address: ${address}:`, error); - return []; - } - } - - @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-tokens') - async getTokenForAddress(address: string, identifier: string): Promise { - try { - // TODO: add more fields in project on demand - // TODO: search for token efficiently: return first occurence and use index on identifier - const result = await this.accountDetailsModel.aggregate([ - { $match: { address } }, - { - $project: { - _id: 0, - tokens: { - $filter: { - input: "$tokens", - as: "token", - cond: { $eq: ["$$token.identifier", identifier] }, - }, - }, - }, - }, - { - $project: { - "tokens.type": 1, - "tokens.subType": 1, - "tokens.identifier": 1, - "tokens.collection": 1, - "tokens.name": 1, - "tokens.nonce": 1, - "tokens.decimals": 1, - "tokens.balance": 1, - }, - }, - ]).exec(); - return result[0]?.tokens[0] ?? undefined; - } catch (error) { - console.error(`Error fetching token with identifier ${identifier} for address: ${address}:`, error); - return undefined; - } - } - - @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-nfts') - async getNftForAddress(address: string, identifier: string): Promise { - try { - // TODO: add more fields in project on demand - // TODO: search for nft efficiently: return first occurence and use index on identifier - const result = await this.accountDetailsModel.aggregate([ - { $match: { address } }, - { - $project: { - _id: 0, - nfts: { - $filter: { - input: "$nfts", - as: "nft", - cond: { $eq: ["$$nft.identifier", identifier] }, - }, - }, - }, - }, - { - $project: { - "nfts.identifier": 1, - "nfts.collection": 1, - "nfts.nonce": 1, - "nfts.type": 1, - "nfts.subType": 1, - "nfts.name": 1, - "nfts.balance": 1, - "nfts.subtype": 1, - }, - }, - ]).exec(); - return result[0]?.nfts[0] ?? undefined; - } catch (error) { - console.error(`Error fetching nft with identifier ${identifier} for address: ${address}:`, error); - return undefined; - } - } - - @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-nfts') - async getNftsForAddress(address: string, queryPagination: QueryPagination): Promise { - try { - // TODO: add more fields in project on demand - const result = await this.accountDetailsModel.aggregate([ - { $match: { address } }, - { - $project: { - _id: 0, - nfts: { $slice: ["$nfts", queryPagination.from, queryPagination.size] }, - }, - }, - { - $project: { - "nfts.identifier": 1, - "nfts.collection": 1, - "nfts.nonce": 1, - "nfts.type": 1, - "nfts.subType": 1, - "nfts.name": 1, - "nfts.balance": 1, - "nfts.subtype": 1, - }, - }, - ]).exec(); - - return result[0]?.nfts || []; - } catch (error) { - console.error(`Error fetching nfts for address: ${address}:`, error); - return []; - } - } - @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'account-details') async getAccount(address: string): Promise { try { @@ -265,189 +80,22 @@ export class AccountDetailsRepository { for (const accountDetailed of accounts) { const updatePipeline: any[] = []; - const pulls: any[] = []; // --- helper --- const isValidValue = (value: any): boolean => - value !== undefined && value !== null; + value != null; // --- simple fields --- const updateFields: any = {}; Object.entries(accountDetailed).forEach(([key, value]) => { - if (isValidValue(value) && key !== "tokens" && key !== "nfts") { + if (isValidValue(value)) { updateFields[key as keyof AccountDetails] = value; } + }); if (Object.keys(updateFields).length > 0) { updatePipeline.push({ $set: updateFields }); } - - // --- tokens --- - const tokensToRemove: string[] = []; - const tokensToUpsert: any[] = []; - - if (accountDetailed.tokens?.length) { - for (const t of accountDetailed.tokens) { - if (t.balance === '0') { - tokensToRemove.push(t.identifier); - } else { - tokensToUpsert.push(t); - } - } - - if (tokensToUpsert.length) { - updatePipeline.push({ - $set: { - tokens: { - $let: { - vars: { newTokens: tokensToUpsert }, - in: { - $concatArrays: [ - { - $map: { - input: { $ifNull: ["$tokens", []] }, - as: "t", - in: { - $let: { - vars: { - updated: { - $filter: { - input: "$$newTokens", - cond: { $eq: ["$$this.identifier", "$$t.identifier"] }, - }, - }, - }, - in: { - $cond: [ - { $gt: [{ $size: "$$updated" }, 0] }, - { $arrayElemAt: ["$$updated", 0] }, - "$$t", - ], - }, - }, - }, - }, - }, - { - $filter: { - input: "$$newTokens", - cond: { - $not: { - $in: [ - "$$this.identifier", - { - $map: { - input: { $ifNull: ["$tokens", []] }, - as: "t", - in: "$$t.identifier", - }, - }, - ], - }, - }, - }, - }, - ], - }, - }, - }, - }, - }); - } - - if (tokensToRemove.length) { - pulls.push({ - updateOne: { - filter: { address: accountDetailed.address }, - update: { $pull: { tokens: { identifier: { $in: tokensToRemove } } } }, - }, - }); - } - } - - // --- nfts --- - const nftsToRemove: string[] = []; - const nftsToUpsert: any[] = []; - - if (accountDetailed.nfts?.length) { - for (const n of accountDetailed.nfts) { - if (n.balance === '0') { - nftsToRemove.push(n.identifier); - } else { - nftsToUpsert.push(n); - } - } - - if (nftsToUpsert.length) { - updatePipeline.push({ - $set: { - nfts: { - $let: { - vars: { newNfts: nftsToUpsert }, - in: { - $concatArrays: [ - { - $map: { - input: { $ifNull: ["$nfts", []] }, - as: "n", - in: { - $let: { - vars: { - updated: { - $filter: { - input: "$$newNfts", - cond: { $eq: ["$$this.identifier", "$$n.identifier"] }, - }, - }, - }, - in: { - $cond: [ - { $gt: [{ $size: "$$updated" }, 0] }, - { $arrayElemAt: ["$$updated", 0] }, - "$$n", - ], - }, - }, - }, - }, - }, - { - $filter: { - input: "$$newNfts", - cond: { - $not: { - $in: [ - "$$this.identifier", - { - $map: { - input: { $ifNull: ["$nfts", []] }, - as: "n", - in: "$$n.identifier", - }, - }, - ], - }, - }, - }, - }, - ], - }, - }, - }, - }, - }); - } - - if (nftsToRemove.length) { - pulls.push({ - updateOne: { - filter: { address: accountDetailed.address }, - update: { $pull: { nfts: { identifier: { $in: nftsToRemove } } } }, - }, - }); - } - } - if (updatePipeline.length > 0) { operations.push({ updateOne: { @@ -458,16 +106,9 @@ export class AccountDetailsRepository { }); totalOperations++; } - - // --- push pulls --- - if (pulls.length > 0) { - operations.push(...pulls); - totalOperations += pulls.length; - } } - this.logger.log(`number of write operations: ${totalOperations}`); - + this.logger.log(`number of accounts write operations: ${totalOperations}`); const result = await this.accountDetailsModel.bulkWrite(operations, { ordered: true, }); diff --git a/src/common/indexer/db/repositories/esdt.details.repository.ts b/src/common/indexer/db/repositories/esdt.details.repository.ts new file mode 100644 index 000000000..248c4c934 --- /dev/null +++ b/src/common/indexer/db/repositories/esdt.details.repository.ts @@ -0,0 +1,102 @@ +import { Model } from 'mongoose'; +import { LogPerformanceAsync } from 'src/utils/log.performance.decorator'; +import { MetricsEvents } from 'src/utils/metrics-events.constants'; +import { InjectModel } from '@nestjs/mongoose'; +import { Injectable } from '@nestjs/common'; +import { OriginLogger } from '@multiversx/sdk-nestjs-common'; +import { EsdtDetails } from '../schemas/esdt.details.schema'; + +@Injectable() +export class EsdtDetailsRepository { + private readonly logger = new OriginLogger(EsdtDetailsRepository.name); + constructor( + @InjectModel(EsdtDetails.name) + private readonly esdtDetailsModel: Model + ) { } + + @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'esdt-details') + async getEsdt(address: string, identifier: string): Promise { + try { + const esdtDb = await this.esdtDetailsModel.findOne( + { address, identifier }, + { _id: 0, __v: 0, tokens: 0, nfts: 0, updatedAt: 0, createdAt: 0, code: 0 } + ).lean(); + if (!esdtDb) { + return null; + } + return new EsdtDetails({ ...esdtDb }); + } catch (error) { + console.error('Error fetching esdt:', error); + return null; + } + } + + + @LogPerformanceAsync(MetricsEvents.SetPersistenceDuration, 'esdt-details') + async updateEsdts(esdts: EsdtDetails[]): Promise { + try { + if (!esdts.length) return []; + let totalOperations = 0; + + const operations: any[] = []; + + for (const esdtDetailed of esdts) { + if (esdtDetailed.balance === '0') { + operations.push({ + deleteOne: { + filter: { + address: esdtDetailed.address, + identifier: esdtDetailed.identifier, + }, + }, + }); + + totalOperations++; + continue; + } + + const updatePipeline: any[] = []; + + const isValidValue = (value: any): boolean => + value != null; + + const updateFields: any = {}; + Object.entries(esdtDetailed).forEach(([key, value]) => { + if (isValidValue(value)) { + updateFields[key as keyof EsdtDetails] = value; + } + }); + + if (Object.keys(updateFields).length > 0) { + updatePipeline.push({ $set: updateFields }); + } + + if (updatePipeline.length > 0) { + operations.push({ + updateOne: { + filter: { + address: esdtDetailed.address, + identifier: esdtDetailed.identifier, + }, + update: updatePipeline, + upsert: true, + }, + }); + totalOperations++; + } + } + + this.logger.log(`number of esdts write operations: ${totalOperations}`); + + const result = await this.esdtDetailsModel.bulkWrite(operations, { + ordered: true, + }); + + return result; + } catch (error: any) { + console.error('Error updating esdts:', error); + throw error; + } + } + +} diff --git a/src/common/indexer/db/repositories/index.ts b/src/common/indexer/db/repositories/index.ts index eea469f4e..6740a9e4e 100644 --- a/src/common/indexer/db/repositories/index.ts +++ b/src/common/indexer/db/repositories/index.ts @@ -1 +1,2 @@ export * from './account.details.repository'; +export * from './esdt.details.repository'; diff --git a/src/common/indexer/db/schemas/account.details.schema.ts b/src/common/indexer/db/schemas/account.details.schema.ts index ac9f98e8e..4b5bf43ae 100644 --- a/src/common/indexer/db/schemas/account.details.schema.ts +++ b/src/common/indexer/db/schemas/account.details.schema.ts @@ -3,8 +3,6 @@ import mongoose, { HydratedDocument } from 'mongoose'; import { AccountAssets } from 'src/common/assets/entities/account.assets'; import { ScamInfo } from 'src/common/entities/scam-info.dto'; import { NftCollectionAccount } from 'src/endpoints/collections/entities/nft.collection.account'; -import { NftAccount } from 'src/endpoints/nfts/entities/nft.account'; -import { TokenWithBalance } from 'src/endpoints/tokens/entities/token.with.balance'; export type AccountDetailsDocument = HydratedDocument; @@ -91,12 +89,6 @@ export class AccountDetails { @Prop({ type: Array, required: false }) nftCollections?: NftCollectionAccount[]; - @Prop({ type: Array, default: [] }) - nfts?: NftAccount[] = []; - - @Prop({ type: Array, default: [] }) - tokens?: TokenWithBalance[] = []; - @Prop({ required: false, type: Number }) activeGuardianActivationEpoch?: number; @@ -126,5 +118,4 @@ export class AccountDetails { export const AccountDetailsSchema = SchemaFactory.createForClass(AccountDetails); AccountDetailsSchema.index({ address: 1 }, { unique: true }); -AccountDetailsSchema.index({ "tokens.identifier": 1 }); -AccountDetailsSchema.index({ "nfts.identifier": 1 }); +AccountDetailsSchema.index({ "esdts.identifier": 1 }); diff --git a/src/common/indexer/db/schemas/esdt.details.schema.ts b/src/common/indexer/db/schemas/esdt.details.schema.ts new file mode 100644 index 000000000..7a5fa6e8c --- /dev/null +++ b/src/common/indexer/db/schemas/esdt.details.schema.ts @@ -0,0 +1,30 @@ +import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; +import mongoose, { HydratedDocument } from 'mongoose'; + +export type EsdtDetailsDocument = HydratedDocument; + + +@Schema({ collection: 'esdt-details', timestamps: true }) +export class EsdtDetails { + @Prop({ type: mongoose.Schema.Types.ObjectId, auto: true }) + _id!: string; + + @Prop({ required: true, type: String }) + address: string = ''; + + @Prop({ required: true, type: String }) + identifier: string = ''; + + @Prop({ required: true, type: String }) + balance: string = ''; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export const EsdtDetailsSchema = SchemaFactory.createForClass(EsdtDetails); + +EsdtDetailsSchema.index({ address: 1 }); +EsdtDetailsSchema.index({ identifier: 1 }); +EsdtDetailsSchema.index({ address: 1, identifier: 1 }, { unique: true }); diff --git a/src/common/indexer/db/schemas/index.ts b/src/common/indexer/db/schemas/index.ts index 56f752654..ddb5d941f 100644 --- a/src/common/indexer/db/schemas/index.ts +++ b/src/common/indexer/db/schemas/index.ts @@ -1 +1,2 @@ export * from './account.details.schema'; +export * from './esdt.details.schema'; diff --git a/src/endpoints/accounts-v2/account.controller.v2.ts b/src/endpoints/accounts-v2/account.controller.v2.ts index a408854dd..453cd578c 100644 --- a/src/endpoints/accounts-v2/account.controller.v2.ts +++ b/src/endpoints/accounts-v2/account.controller.v2.ts @@ -1,11 +1,12 @@ -import { Controller, Get, NotFoundException, Param, Query, UseInterceptors } from '@nestjs/common'; +import { Controller, Get, HttpException, HttpStatus, NotFoundException, Param, Query, UseInterceptors } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { AccountServiceV2 } from './account.service.v2'; -import { ParseAddressPipe, ParseBoolPipe } from '@multiversx/sdk-nestjs-common'; +import { ParseAddressPipe, ParseBoolPipe, ParseTokenOrNftPipe } from '@multiversx/sdk-nestjs-common'; import { DeepHistoryInterceptor } from 'src/interceptors/deep-history.interceptor'; import { NoCache } from '@multiversx/sdk-nestjs-cache'; import { AccountDetailed } from '../accounts/entities/account.detailed'; import { AccountFetchOptions } from '../accounts/entities/account.fetch.options'; +import { TokenDetailedWithBalance } from '../tokens/entities/token.detailed.with.balance'; @Controller('') @ApiTags('accounts') @@ -44,4 +45,21 @@ export class AccountControllerV2 { return account; } + + @Get("/accounts/v2/:address/tokens/:token") + @UseInterceptors(DeepHistoryInterceptor) + @ApiOkResponse({ type: TokenDetailedWithBalance }) + @ApiOperation({ summary: 'Account token details', description: 'Returns details about a specific fungible token from a given address' }) + @NoCache() + async getAccountToken( + @Param('address', ParseAddressPipe) address: string, + @Param('token', ParseTokenOrNftPipe) token: string, + ): Promise { + const result = await this.accountServiceV2.getTokenForAddress(address, token); + if (!result) { + throw new HttpException('Token for given account not found', HttpStatus.NOT_FOUND); + } + + return result; + } } diff --git a/src/endpoints/accounts-v2/account.service.v2.ts b/src/endpoints/accounts-v2/account.service.v2.ts index 3935bd822..b7c4b5da0 100644 --- a/src/endpoints/accounts-v2/account.service.v2.ts +++ b/src/endpoints/accounts-v2/account.service.v2.ts @@ -2,7 +2,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { PluginService } from 'src/common/plugins/plugin.service'; import { AssetsService } from 'src/common/assets/assets.service'; import { CacheService } from "@multiversx/sdk-nestjs-cache"; -import { AddressUtils, OriginLogger } from '@multiversx/sdk-nestjs-common'; +import { AddressUtils, OriginLogger, TokenUtils } from '@multiversx/sdk-nestjs-common'; import { IndexerService } from "src/common/indexer/indexer.service"; import { CacheInfo } from 'src/utils/cache.info'; import { UsernameService } from '../usernames/username.service'; @@ -13,6 +13,11 @@ import { StateChangesConsumerService } from 'src/state-changes/state.changes.con import { AccountService } from '../accounts/account.service'; import { AccountDetailed } from '../accounts/entities/account.detailed'; import { AccountFetchOptions } from '../accounts/entities/account.fetch.options'; +import { TokenService } from '../tokens/token.service'; +import { TokenDetailedWithBalance } from '../tokens/entities/token.detailed.with.balance'; +import { GatewayService } from 'src/common/gateway/gateway.service'; +import { EsdtDetailsRepository } from 'src/common/indexer/db/repositories/esdt.details.repository'; +import { EsdtDetails } from 'src/common/indexer/db/schemas/esdt.details.schema'; @Injectable() export class AccountServiceV2 { @@ -28,7 +33,10 @@ export class AccountServiceV2 { @Inject(forwardRef(() => ProviderService)) private readonly providerService: ProviderService, private readonly accountDetailsDepository: AccountDetailsRepository, + private readonly esdtDetailsRepository: EsdtDetailsRepository, private readonly accountServiceV1: AccountService, + private readonly tokenService: TokenService, + private readonly gatewayService: GatewayService, ) { } private async getAccountWithFallBack(address: string, options?: AccountFetchOptions): Promise { @@ -60,8 +68,8 @@ export class AccountServiceV2 { } let account = null; try { - const isStateChangesConsumerHealty: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); - if (isStateChangesConsumerHealty === true && !StateChangesConsumerService.isSystemContractAddress(address)) { + const isStateChangesConsumerHealthy: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); + if (isStateChangesConsumerHealthy === true && !StateChangesConsumerService.isSystemContractAddress(address)) { account = await this.cachingService.getOrSet( CacheInfo.AccountState(address).key, async () => await this.getAccountWithFallBack(address, options), @@ -116,4 +124,68 @@ export class AccountServiceV2 { return account; } + + async getTokenForAddress(address: string, identifier: string): Promise { + if (!TokenUtils.isToken(identifier) && !TokenUtils.isNft(identifier)) { + return undefined; + } + try { + let tokenDetailedWithBalance: TokenDetailedWithBalance | undefined; + const isStateChangesConsumerHealthy: boolean = await StateChangesConsumerService.isStateChangesConsumerHealthy(this.cachingService, 6000); + if (isStateChangesConsumerHealthy === true && !StateChangesConsumerService.isSystemContractAddress(address)) { + tokenDetailedWithBalance = await this.getTokenForAddressWithFallback(address, identifier); + } else { + tokenDetailedWithBalance = await this.tokenService.getTokenForAddress(address, identifier); + } + return tokenDetailedWithBalance; + } catch (err) { + return undefined; + } + } + + async getTokenForAddressWithFallback(address: string, identifier: string): Promise { + const tokenRaw = await this.cachingService.getOrSet( + CacheInfo.AccountEsdt(address, identifier).key, + async () => { + const token = await this.esdtDetailsRepository.getEsdt(address, identifier); + if (token) { + return new EsdtDetails({ + identifier, + balance: token.balance, + }); + } + + const tokenFromGateway = await this.gatewayService.getAddressEsdt(address, identifier); + if (tokenFromGateway) { + return new EsdtDetails({ + identifier, + balance: tokenFromGateway.balance, + }); + } + return undefined; + }, + CacheInfo.AccountEsdt(address, identifier).ttl, + ); + + if (!tokenRaw) { + return await this.getTokenForAddress(address, identifier); + } + const { balance } = tokenRaw; + const esdtIdentifier = identifier.split('-').slice(0, 2).join('-'); + const tokens = await this.tokenService.getFilteredTokens({ identifier: esdtIdentifier, includeMetaESDT: true }); + if (!tokens.length) { + this.logger.log(`Error when fetching token ${identifier} details for address ${address}`); + return undefined; + } + + const tokenData = tokens[0]; + + const tokenDetailedWithBalance = new TokenDetailedWithBalance({ ...tokenData, balance }); + + this.tokenService.applyValueUsd(tokenDetailedWithBalance); + this.tokenService.applyTickerFromAssets(tokenDetailedWithBalance); + await this.tokenService.applySupply(tokenDetailedWithBalance); + + return tokenDetailedWithBalance; + } } diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 8f684bf11..669fea4bd 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -1,6 +1,6 @@ import { CompetingRabbitConsumer } from "src/common/rabbitmq/rabbitmq.consumers"; import { BlockWithStateChangesRaw, ESDTType, StateChanges } from "./entities"; -import { AccountDetails, AccountDetailsRepository } from "src/common/indexer/db"; +import { AccountDetails, AccountDetailsRepository, EsdtDetails, EsdtDetailsRepository } from "src/common/indexer/db"; import { Inject, Injectable } from "@nestjs/common"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; import { CacheInfo } from "src/utils/cache.info"; @@ -9,7 +9,7 @@ import { NftType } from "src/endpoints/nfts/entities/nft.type"; import { NftSubType } from "src/endpoints/nfts/entities/nft.sub.type"; import { ClientProxy } from "@nestjs/microservices"; import { StateChangesDecoder } from "./utils/state-changes.decoder"; -import { AddressUtils, OriginLogger } from "@multiversx/sdk-nestjs-common"; +import { AddressUtils, OriginLogger, TokenUtils } from "@multiversx/sdk-nestjs-common"; import { PerformanceProfiler } from "@multiversx/sdk-nestjs-monitoring"; import configuration from "config/configuration"; import { ApiConfigService } from "src/common/api-config/api.config.service"; @@ -23,6 +23,7 @@ export class StateChangesConsumerService { constructor( private readonly cacheService: CacheService, private readonly accountDetailsRepository: AccountDetailsRepository, + private readonly esdtDetailsRepository: EsdtDetailsRepository, private readonly apiConfigService: ApiConfigService, @Inject('PUBSUB_SERVICE') private clientProxy: ClientProxy, ) { } @@ -42,11 +43,11 @@ export class StateChangesConsumerService { const decodingProfiler = new PerformanceProfiler('StateChangesDecoding'); const finalStates = this.decodeStateChangesFinal(blockWithStateChanges); - const transformedFinalStates = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs); + const { transformedAccounts, transformedEsdts } = this.transformFinalStatesToDbFormat(finalStates, blockWithStateChanges.shardID, blockWithStateChanges.timestampMs); decodingProfiler.stop('StateChangesDecoding'); this.logger.log(`Decoded state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID} in ${decodingProfiler.duration} ms`); - await this.updateAccounts(transformedFinalStates); + await this.updateAccounts(transformedAccounts, transformedEsdts); await this.cacheService.setRemote( CacheInfo.StateChangesConsumerLatestProcessedBlockTimestamp(blockWithStateChanges.shardID).key, @@ -62,40 +63,56 @@ export class StateChangesConsumerService { } } - private async updateAccounts(transformedFinalStates: AccountDetails[]) { - const promisesToWaitFor = [this.accountDetailsRepository.updateAccounts(transformedFinalStates.filter(account => !AddressUtils.isSmartContractAddress(account.address)))]; + private async updateAccounts(transformedAccounts: AccountDetails[], transformedEsdts: EsdtDetails[]) { + const promisesToWaitFor = [ + this.accountDetailsRepository.updateAccounts(transformedAccounts.filter(account => !AddressUtils.isSmartContractAddress(account.address))), + this.esdtDetailsRepository.updateEsdts(transformedEsdts.filter(esdt => !AddressUtils.isSmartContractAddress(esdt.address) && TokenUtils.isToken(esdt.identifier))), + ]; - const walletCacheKeys = []; + const esdtsUpdateCacheKeys = []; + const esdtsDeleteCacheKeys = []; + const walletAccountCacheKeys = []; const contractCacheKeys = []; - const values = []; - for (const account of transformedFinalStates) { + const walletAccountValues = []; + const esdtsUpdateValues = []; + for (const account of transformedAccounts) { + if (!AddressUtils.isSmartContractAddress(account.address)) { - walletCacheKeys.push(CacheInfo.AccountState(account.address).key); - const { tokens, nfts, ...accountWithoutAssets } = account; - values.push(accountWithoutAssets); + walletAccountCacheKeys.push(CacheInfo.AccountState(account.address).key); + walletAccountValues.push(account); } else { contractCacheKeys.push(CacheInfo.AccountState(account.address).key); } } - if (walletCacheKeys.length > 0) { + + for (const esdt of transformedEsdts) { + if (esdt.balance === '0') { + esdtsDeleteCacheKeys.push(CacheInfo.AccountEsdt(esdt.address, esdt.identifier).key); + } else { + esdtsUpdateCacheKeys.push(CacheInfo.AccountEsdt(esdt.address, esdt.identifier).key); + esdtsUpdateValues.push(esdt); + } + } + + if (walletAccountCacheKeys.length > 0 || esdtsUpdateCacheKeys.length > 0) { promisesToWaitFor.push( this.cacheService.setManyRemote( - walletCacheKeys, - values, - CacheInfo.AccountState('any').ttl, - ) + [...walletAccountCacheKeys, ...esdtsUpdateCacheKeys], + [...walletAccountValues, ...esdtsUpdateValues], + CacheInfo.AccountEsdt('any', 'any').ttl, + ), ); } - if (contractCacheKeys.length > 0) { + if (contractCacheKeys.length > 0 || esdtsDeleteCacheKeys.length > 0) { promisesToWaitFor.push( this.cacheService.deleteManyRemote( - contractCacheKeys, + [...contractCacheKeys, ...esdtsDeleteCacheKeys], ) ); } - this.deleteLocalCache([...walletCacheKeys, ...contractCacheKeys]); + this.deleteLocalCache([...walletAccountCacheKeys, ...contractCacheKeys, ...esdtsUpdateCacheKeys, ...esdtsDeleteCacheKeys]); await Promise.all(promisesToWaitFor); } @@ -110,8 +127,8 @@ export class StateChangesConsumerService { blockTimestampMs: number, ) { const isEsdtComputationEnabled = this.apiConfigService.isEsdtComputationEnabled(); - const transformed: AccountDetails[] = []; - + const transformedAccounts: AccountDetails[] = []; + const transformedEsdts: EsdtDetails[] = []; for (const [_address, state] of Object.entries(finalStates)) { const baseAccount = this.parseBaseAccount( state, @@ -123,18 +140,15 @@ export class StateChangesConsumerService { const parsedAccount = new AccountDetails({ ...baseAccount, }); + transformedAccounts.push(parsedAccount); if (isEsdtComputationEnabled) { - const tokens = this.transformTokens(state); - const nfts = this.transformNfts(state); - parsedAccount.tokens = tokens; - parsedAccount.nfts = nfts; + const esdts = this.transformEsdts(state); + transformedEsdts.push(...esdts); } - - transformed.push(parsedAccount); } - return transformed; + return { transformedAccounts, transformedEsdts }; } private parseBaseAccount( @@ -154,6 +168,26 @@ export class StateChangesConsumerService { }; } + private transformEsdts(state: StateChanges) { + const allEsdts = [ + ...(state.esdtState.Fungible ?? []), + ...(state.esdtState.NonFungible ?? []), + ...(state.esdtState.NonFungibleV2 ?? []), + ...(state.esdtState.DynamicNFT ?? []), + ...(state.esdtState.SemiFungible ?? []), + ...(state.esdtState.DynamicSFT ?? []), + ...(state.esdtState.MetaFungible ?? []), + ...(state.esdtState.DynamicMeta ?? []), + ]; + return allEsdts.map(esdt => new EsdtDetails({ + address: state.accountState?.address, + identifier: esdt.identifier, + balance: esdt.value, + }) + ); + } + + //@ts-ignore private transformTokens(state: StateChanges): TokenWithBalance[] { const fungible = state.esdtState?.Fungible ?? []; @@ -168,6 +202,7 @@ export class StateChangesConsumerService { ); } + //@ts-ignore private transformNfts(state: StateChanges): NftAccount[] { const { NonFungible, diff --git a/src/test/unit/services/state-changes.consumer.spec.ts b/src/test/unit/services/state-changes.consumer.spec.ts index b75906b03..e67fdbbe0 100644 --- a/src/test/unit/services/state-changes.consumer.spec.ts +++ b/src/test/unit/services/state-changes.consumer.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { StateChangesConsumerService } from 'src/state-changes/state.changes.consumer.service'; import { CacheService } from '@multiversx/sdk-nestjs-cache'; -import { AccountDetailsRepository } from 'src/common/indexer/db'; +import { AccountDetailsRepository, EsdtDetailsRepository } from 'src/common/indexer/db'; import { ApiConfigService } from 'src/common/api-config/api.config.service'; import { ClientProxy } from '@nestjs/microservices'; import { StateChangesDecoder } from 'src/state-changes/utils/state-changes.decoder'; @@ -19,6 +19,9 @@ jest.mock('@multiversx/sdk-nestjs-common', () => ({ AddressUtils: { isSmartContractAddress: jest.fn(), }, + TokenUtils: { + isToken: jest.fn(), + }, OriginLogger: jest.fn().mockImplementation(() => ({ log: jest.fn(), error: jest.fn(), @@ -39,17 +42,21 @@ jest.mock('@multiversx/sdk-nestjs-common', () => ({ jest.mock('src/common/indexer/db', () => ({ AccountDetailsRepository: jest.fn(), AccountDetails: jest.fn().mockImplementation((data) => data), + EsdtDetailsRepository: jest.fn(), + EsdtDetails: jest.fn().mockImplementation((data) => data), })); describe('StateChangesConsumerService', () => { let service: StateChangesConsumerService; let cacheService: jest.Mocked; let accountRepo: jest.Mocked; + let esdtRepo: jest.Mocked; let clientProxy: jest.Mocked; let apiConfig: jest.Mocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + //@ts-ignore providers: [ StateChangesConsumerService, { @@ -69,6 +76,12 @@ describe('StateChangesConsumerService', () => { updateAccounts: jest.fn(), }, }, + { + provide: EsdtDetailsRepository, + useValue: { + updateEsdts: jest.fn(), + }, + }, { provide: ApiConfigService, useValue: { @@ -88,6 +101,7 @@ describe('StateChangesConsumerService', () => { service = module.get(StateChangesConsumerService); cacheService = module.get(CacheService); accountRepo = module.get(AccountDetailsRepository); + esdtRepo = module.get(EsdtDetailsRepository); clientProxy = module.get('PUBSUB_SERVICE'); apiConfig = module.get(ApiConfigService); }); @@ -203,24 +217,27 @@ describe('StateChangesConsumerService', () => { describe('updateAccounts', () => { it('should update non-contract accounts and set cache', async () => { (AddressUtils.isSmartContractAddress as jest.Mock).mockReturnValue(false); - const mockAccounts = [{ address: 'erd1', tokens: [], nfts: [], balance: '10' }]; - await service['updateAccounts'](mockAccounts as any); + const mockAccounts = [{ address: 'erd1', balance: '10' }]; + const mockEsdts = [{ identifier: 'id-123456', balance: '10' }]; + await service['updateAccounts'](mockAccounts as any, mockEsdts as any); expect(accountRepo.updateAccounts).toHaveBeenCalled(); + expect(esdtRepo.updateEsdts).toHaveBeenCalled(); expect(cacheService.setManyRemote).toHaveBeenCalled(); expect(clientProxy.emit).toHaveBeenCalled(); }); it('should delete contract cache keys', async () => { (AddressUtils.isSmartContractAddress as jest.Mock).mockReturnValue(true); - const mockAccounts = [{ address: 'erd1sc', tokens: [], nfts: [], balance: '0' }]; - await service['updateAccounts'](mockAccounts as any); + const mockAccounts = [{ address: 'erd1sc', balance: '0' }]; + const mockEsdts = [{ identifier: 'id-123456', balance: '10' }]; + await service['updateAccounts'](mockAccounts as any, mockEsdts as any); expect(cacheService.deleteManyRemote).toHaveBeenCalled(); expect(clientProxy.emit).toHaveBeenCalled(); }); }); describe('transformFinalStatesToDbFormat', () => { - it('should transform state changes into AccountDetails', () => { + it('should transform state changes into AccountDetails and ignore ESDTs', () => { const mockInput = { erd1qqqqqqqqqqqqqpgqvg8r5yavkyhu6rmmkgqzgsduzheg2fk7v5ysrypdex: { @@ -406,17 +423,152 @@ describe('StateChangesConsumerService', () => { rootHash: 'fV3JuZDrwZ8TbnlawkHGYYp1bQX0fTgefUzU7xEYLh8=', }, ]; - const result = service['transformFinalStatesToDbFormat']( + const { transformedAccounts, transformedEsdts } = service['transformFinalStatesToDbFormat']( + mockInput, + mockShardId, + mockBlockTimestampMs, + ); + expect(transformedAccounts).toEqual(expectedResult); + expect(transformedEsdts.length).toBe(0); + }); + + it('should transform state changes into AccountDetails and ESDT details', () => { + (apiConfig.isEsdtComputationEnabled as jest.Mock).mockReturnValue(true); + const mockInput = + { + erd1vhfuv9qznn59vlasthdgsp7pzc99snzvchvcrjzhgn3cdequ7jxsvwtu50: { + accountState: { + nonce: 163, + balance: '1022184909233299999998', + developerReward: '0', + address: 'erd1vhfuv9qznn59vlasthdgsp7pzc99snzvchvcrjzhgn3cdequ7jxsvwtu50', + rootHash: 'i6mdSsTadb0E0H17P7rCPCTmQrtu8b4WDT20ncfVRz4=', + }, + esdtState: { + Fungible: [ + { + identifier: 'ACCEPTED-bc0f6e', + nonce: '0', + type: 0, + value: '9920000000', + propertiesHex: '', + reservedHex: '', + tokenMetaData: null, + }, + ], + NonFungible: [], + NonFungibleV2: [], + SemiFungible: [], + MetaFungible: [], + DynamicNFT: [], + DynamicSFT: [], + DynamicMeta: [], + }, + accountChanges: { + nonceChanged: false, + balanceChanged: false, + codeHashChanged: false, + rootHashChanged: true, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false, + }, + isNewAccount: false, + }, + erd107uaynrvf80g4zuym4fqqh5pqzvaczdryj49zr2qew57wqe3mvusupj8xh: { + accountState: { + nonce: 137, + balance: '1050718027978784480311', + developerReward: '0', + address: 'erd107uaynrvf80g4zuym4fqqh5pqzvaczdryj49zr2qew57wqe3mvusupj8xh', + rootHash: 'U0Q3PEXqxQNGQZ07/NVNVfFlxRdyzzY04/mzEWc5czs=', + }, + esdtState: { + Fungible: [ + { + identifier: 'ACCEPTED-bc0f6e', + nonce: '0', + type: 0, + value: '49980000000', + propertiesHex: '', + reservedHex: '', + tokenMetaData: null, + }, + ], + NonFungible: [], + NonFungibleV2: [], + SemiFungible: [], + MetaFungible: [], + DynamicNFT: [], + DynamicSFT: [], + DynamicMeta: [], + }, + accountChanges: { + nonceChanged: true, + balanceChanged: true, + codeHashChanged: false, + rootHashChanged: true, + developerRewardChanged: false, + ownerAddressChanged: false, + userNameChanged: false, + codeMetadataChanged: false, + }, + isNewAccount: false, + }, + }; + + const mockShardId = 1; + const mockBlockTimestampMs = 1763379962000; + const accountsExpectedResults = [ + { + address: 'erd1vhfuv9qznn59vlasthdgsp7pzc99snzvchvcrjzhgn3cdequ7jxsvwtu50', + balance: '1022184909233299999998', + nonce: 163, + timestampMs: 1763379962000, + timestamp: 1763379962, + shard: 1, + developerReward: '0', + rootHash: 'i6mdSsTadb0E0H17P7rCPCTmQrtu8b4WDT20ncfVRz4=', + }, + { + address: 'erd107uaynrvf80g4zuym4fqqh5pqzvaczdryj49zr2qew57wqe3mvusupj8xh', + balance: '1050718027978784480311', + nonce: 137, + timestampMs: 1763379962000, + timestamp: 1763379962, + shard: 1, + developerReward: '0', + rootHash: 'U0Q3PEXqxQNGQZ07/NVNVfFlxRdyzzY04/mzEWc5czs=', + }, + ]; + + const esdtsExpectedResults = [ + { + address: 'erd1vhfuv9qznn59vlasthdgsp7pzc99snzvchvcrjzhgn3cdequ7jxsvwtu50', + identifier: 'ACCEPTED-bc0f6e', + balance: '9920000000', + }, + { + address: 'erd107uaynrvf80g4zuym4fqqh5pqzvaczdryj49zr2qew57wqe3mvusupj8xh', + identifier: 'ACCEPTED-bc0f6e', + balance: '49980000000', + }, + ]; + + const { transformedAccounts, transformedEsdts } = service['transformFinalStatesToDbFormat']( mockInput, mockShardId, mockBlockTimestampMs, ); - expect(result).toEqual(expectedResult); + expect(transformedAccounts).toEqual(accountsExpectedResults); + expect(transformedEsdts).toEqual(esdtsExpectedResults); }); it('should skip if no accountState', () => { - const result = service['transformFinalStatesToDbFormat']({ erd1: {} } as any, 0, Date.now()); - expect(result.length).toBe(0); + const { transformedAccounts, transformedEsdts } = service['transformFinalStatesToDbFormat']({ erd1: {} } as any, 0, Date.now()); + expect(transformedAccounts.length).toBe(0); + expect(transformedEsdts.length).toBe(0); }); }); diff --git a/src/utils/cache.info.ts b/src/utils/cache.info.ts index d44ecbc7a..83f90372d 100644 --- a/src/utils/cache.info.ts +++ b/src/utils/cache.info.ts @@ -725,9 +725,9 @@ export class CacheInfo { }; } - static AccountToken(address: string): CacheInfo { + static AccountEsdt(address: string, identifier: string): CacheInfo { return { - key: `account-token:${address}`, + key: `account-esdt:${address}:${identifier}`, ttl: Constants.oneHour() * 12, }; } From ae35871eaf29b7f4b0c78263c5e8dce0214f4e75 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 21 Jan 2026 16:20:03 +0200 Subject: [PATCH 87/90] add logs --- src/state-changes/state.changes.consumer.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 669fea4bd..6724c5bbf 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -38,6 +38,8 @@ export class StateChangesConsumerService { if (blockWithStateChanges.shardID === this.apiConfigService.getMetaChainShardId()) { return; // skip meta shard } + const stateChangesDebugging = StateChangesDecoder.decodeStateChangesRaw(blockWithStateChanges); + console.dir(stateChangesDebugging, { depth: null }); const profiler = new PerformanceProfiler('BlockStateChangesProcessing'); const decodingProfiler = new PerformanceProfiler('StateChangesDecoding'); From 58a048020043c7a6798dc67a1b30952243f81fba Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 21 Jan 2026 16:22:42 +0200 Subject: [PATCH 88/90] add logs --- src/state-changes/state.changes.consumer.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 6724c5bbf..5a0e6bda7 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -38,6 +38,7 @@ export class StateChangesConsumerService { if (blockWithStateChanges.shardID === this.apiConfigService.getMetaChainShardId()) { return; // skip meta shard } + console.log(`Consuming state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID}`); const stateChangesDebugging = StateChangesDecoder.decodeStateChangesRaw(blockWithStateChanges); console.dir(stateChangesDebugging, { depth: null }); From 5049cfabc56f9e97548779af8e4962a3a0ba622e Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 28 Jan 2026 10:37:46 +0200 Subject: [PATCH 89/90] remove newline --- src/endpoints/network/network.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/endpoints/network/network.controller.ts b/src/endpoints/network/network.controller.ts index f1fb81f55..719f83aff 100644 --- a/src/endpoints/network/network.controller.ts +++ b/src/endpoints/network/network.controller.ts @@ -41,7 +41,6 @@ export class NetworkController { } } - @Get("/about") @ApiOperation({ summary: 'About', description: 'Returns general information about API deployment' }) @ApiOkResponse({ type: About }) From d1c7e4b7544a7c7b78400fbb1a6e14577bd48b0f Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 28 Jan 2026 10:38:59 +0200 Subject: [PATCH 90/90] remove logs --- src/state-changes/state.changes.consumer.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/state-changes/state.changes.consumer.service.ts b/src/state-changes/state.changes.consumer.service.ts index 5a0e6bda7..669fea4bd 100644 --- a/src/state-changes/state.changes.consumer.service.ts +++ b/src/state-changes/state.changes.consumer.service.ts @@ -38,9 +38,6 @@ export class StateChangesConsumerService { if (blockWithStateChanges.shardID === this.apiConfigService.getMetaChainShardId()) { return; // skip meta shard } - console.log(`Consuming state changes for block ${blockWithStateChanges.hash} on shard ${blockWithStateChanges.shardID}`); - const stateChangesDebugging = StateChangesDecoder.decodeStateChangesRaw(blockWithStateChanges); - console.dir(stateChangesDebugging, { depth: null }); const profiler = new PerformanceProfiler('BlockStateChangesProcessing'); const decodingProfiler = new PerformanceProfiler('StateChangesDecoding');