From 5239843c0b6b89b8c1176501acf637ef467b01e6 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 20 Aug 2025 16:47:55 +0300 Subject: [PATCH 01/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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 cb92d2b06152f13522cd28dad7799ef437bdfc20 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 2 Sep 2025 11:22:46 +0300 Subject: [PATCH 11/33] add pool subscription + reduce filters combinations for subscriptions --- src/crons/websocket/websocket.cron.service.ts | 8 ++ src/crons/websocket/websocket.crons.module.ts | 2 + src/endpoints/blocks/blocks.gateway.ts | 9 +-- .../blocks/entities/block.subscribe.ts | 33 ++------- src/endpoints/pool/entities/pool.subscribe.ts | 20 +++++ src/endpoints/pool/pool.gateway.ts | 61 +++++++++++++++ src/endpoints/pool/pool.module.ts | 5 +- .../entities/dtos/transaction.subscribe.ts | 74 ++----------------- .../transactions/transaction.gateway.ts | 14 ---- 9 files changed, 109 insertions(+), 117 deletions(-) create mode 100644 src/endpoints/pool/entities/pool.subscribe.ts create mode 100644 src/endpoints/pool/pool.gateway.ts diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index 5ebcc5e7c..5a012e32e 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -4,12 +4,14 @@ import { TransactionsGateway } from '../../endpoints/transactions/transaction.ga import { BlocksGateway } from 'src/endpoints/blocks/blocks.gateway'; import { NetworkGateway } from 'src/endpoints/network/network.gateway'; import { Lock } from "@multiversx/sdk-nestjs-common"; +import { PoolGateway } from 'src/endpoints/pool/pool.gateway'; @Injectable() export class WebsocketCronService { constructor( private readonly transactionsGateway: TransactionsGateway, private readonly blocksGateway: BlocksGateway, private readonly networkGateway: NetworkGateway, + private readonly poolGateway: PoolGateway, ) { } @Cron('*/6 * * * * *') @@ -29,4 +31,10 @@ export class WebsocketCronService { async handleStatsUpdate() { await this.networkGateway.pushStats(); } + + @Cron('*/6 * * * * *') + @Lock({ name: 'Push pool transactions to subscribers', verbose: true }) + async handlePoolTransactions() { + await this.poolGateway.pushPool(); + } } diff --git a/src/crons/websocket/websocket.crons.module.ts b/src/crons/websocket/websocket.crons.module.ts index f3c9ddef3..fb8eae363 100644 --- a/src/crons/websocket/websocket.crons.module.ts +++ b/src/crons/websocket/websocket.crons.module.ts @@ -4,6 +4,7 @@ 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'; +import { PoolModule } from 'src/endpoints/pool/pool.module'; @Module({ imports: [ @@ -11,6 +12,7 @@ import { NetworkModule } from 'src/endpoints/network/network.module'; TransactionModule, BlockModule, NetworkModule, + PoolModule, ], providers: [ WebsocketCronService, diff --git a/src/endpoints/blocks/blocks.gateway.ts b/src/endpoints/blocks/blocks.gateway.ts index 518d730a4..fe7098d59 100644 --- a/src/endpoints/blocks/blocks.gateway.ts +++ b/src/endpoints/blocks/blocks.gateway.ts @@ -37,21 +37,16 @@ export class BlocksGateway implements OnGatewayDisconnect { if (!roomName.startsWith("block-")) continue; const filterHash = roomName.replace("block-", ""); - const filter = JSON.parse(filterHash); + const filter: BlockSubscribePayload = 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 }), + new QueryPagination({ from: filter.from, size: filter.size }), filter.withProposerIdentity, ); diff --git a/src/endpoints/blocks/entities/block.subscribe.ts b/src/endpoints/blocks/entities/block.subscribe.ts index dec878829..19e379e23 100644 --- a/src/endpoints/blocks/entities/block.subscribe.ts +++ b/src/endpoints/blocks/entities/block.subscribe.ts @@ -1,5 +1,5 @@ // block-subscribe.dto.ts -import { IsOptional, IsString, IsNumber, IsArray, IsBoolean, Min, Max, IsEnum } from 'class-validator'; +import { IsOptional, IsNumber, IsBoolean, Min, Max, IsEnum, IsIn } from 'class-validator'; import { SortOrder } from 'src/common/entities/sort.order'; export class BlockSubscribePayload { @@ -8,41 +8,20 @@ export class BlockSubscribePayload { @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; + @IsIn([0], { message: 'from can only be 0' }) + from?: number = 0; @IsOptional() @IsNumber() - @Min(1) - @Max(100) - size?: number; + @Min(1, { message: 'minimum size is 1' }) + @Max(50, { message: 'maximum size is 50' }) + size?: number = 25; @IsOptional() @IsBoolean() diff --git a/src/endpoints/pool/entities/pool.subscribe.ts b/src/endpoints/pool/entities/pool.subscribe.ts new file mode 100644 index 000000000..704e2e954 --- /dev/null +++ b/src/endpoints/pool/entities/pool.subscribe.ts @@ -0,0 +1,20 @@ +// block-subscribe.dto.ts +import { IsOptional, IsNumber, Min, Max, IsEnum, IsIn } from 'class-validator'; +import { TransactionType } from 'src/endpoints/transactions/entities/transaction.type'; + +export class PoolSubscribePayload { + @IsOptional() + @IsEnum(TransactionType) + type?: TransactionType; + + @IsOptional() + @IsNumber() + @IsIn([0], { message: 'from can only be 0' }) + from?: number = 0; + + @IsOptional() + @IsNumber() + @Min(1, { message: 'minimum size is 1' }) + @Max(50, { message: 'maximum size is 50' }) + size?: number = 25; +} diff --git a/src/endpoints/pool/pool.gateway.ts b/src/endpoints/pool/pool.gateway.ts new file mode 100644 index 000000000..2a064b8dd --- /dev/null +++ b/src/endpoints/pool/pool.gateway.ts @@ -0,0 +1,61 @@ +import { + WebSocketGateway, + WebSocketServer, + SubscribeMessage, + OnGatewayDisconnect, + MessageBody, + ConnectedSocket +} from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; +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'; + +import { PoolService } from './pool.service'; +import { PoolFilter } from './entities/pool.filter'; +import { QueryPagination } from 'src/common/entities/query.pagination'; +import { PoolSubscribePayload } from './entities/pool.subscribe'; + +@UseFilters(WebsocketExceptionsFilter) +@WebSocketGateway({ cors: { origin: '*' } }) +export class PoolGateway implements OnGatewayDisconnect { + private readonly logger = new OriginLogger(PoolGateway.name); + + @WebSocketServer() + server!: Server; + + constructor(private readonly poolService: PoolService) { } + + @SubscribeMessage('subscribePool') + async handleSubscription( + @ConnectedSocket() client: Socket, + @MessageBody(new WsValidationPipe()) payload: PoolSubscribePayload, + ) { + const filterHash = JSON.stringify(payload); + await client.join(`pool-${filterHash}`); + + return { status: 'success' }; + } + + async pushPool() { + for (const [roomName] of this.server.sockets.adapter.rooms) { + try { + if (!roomName.startsWith("pool-")) continue; + + const filterHash = roomName.replace("pool-", ""); + const filter: PoolSubscribePayload = JSON.parse(filterHash); + + const pool = await this.poolService.getPool(new QueryPagination({ from: filter.from, size: filter.size }), new PoolFilter({ + type: filter.type, + })); + + this.server.to(roomName).emit('poolUpdate', pool); + } catch (error) { + this.logger.error(error); + } + } + } + + handleDisconnect(_client: Socket) { } +} diff --git a/src/endpoints/pool/pool.module.ts b/src/endpoints/pool/pool.module.ts index 88c7a780e..2d891f606 100644 --- a/src/endpoints/pool/pool.module.ts +++ b/src/endpoints/pool/pool.module.ts @@ -1,16 +1,17 @@ import { Module } from "@nestjs/common"; import { PoolService } from "./pool.service"; import { TransactionActionModule } from "../transactions/transaction-action/transaction.action.module"; +import { PoolGateway } from "./pool.gateway"; @Module({ imports: [ TransactionActionModule, ], providers: [ - PoolService, + PoolService, PoolGateway, ], exports: [ - PoolService, + PoolService, PoolGateway, ], }) diff --git a/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts b/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts index 2c1e0b45c..3ea3c602c 100644 --- a/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts +++ b/src/endpoints/transactions/entities/dtos/transaction.subscribe.ts @@ -1,73 +1,16 @@ -import { IsOptional, IsString, IsArray, IsBoolean, IsNumber, IsEnum } from 'class-validator'; -import { Type } from 'class-transformer'; +import { IsOptional, IsString, IsArray, IsBoolean, IsNumber, IsEnum, Min, Max, IsIn } from 'class-validator'; 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; @@ -76,11 +19,6 @@ export class TransactionSubscribePayload { @IsBoolean() isScCall?: boolean; - @IsOptional() - @IsNumber() - @Type(() => Number) - round?: number; - @IsOptional() @IsBoolean() withScResults?: boolean; @@ -115,13 +53,15 @@ export class TransactionSubscribePayload { @IsOptional() @IsNumber() - @Type(() => Number) - from?: number; + @IsIn([0], { message: 'from can only be 0' }) + from?: number = 0; @IsOptional() @IsNumber() - @Type(() => Number) - size?: number; + @Min(1, { message: 'minimum size is 1' }) + @Max(50, { message: 'maximum size is 50' }) + size?: number = 25; + @IsOptional() @IsArray() diff --git a/src/endpoints/transactions/transaction.gateway.ts b/src/endpoints/transactions/transaction.gateway.ts index 280e35d90..8005f5856 100644 --- a/src/endpoints/transactions/transaction.gateway.ts +++ b/src/endpoints/transactions/transaction.gateway.ts @@ -36,23 +36,9 @@ export class TransactionsGateway implements OnGatewayDisconnect { }); 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, }); From 344330e39dfc622749f5f11107b623506376b4d4 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 2 Sep 2025 11:27:23 +0300 Subject: [PATCH 12/33] lint --- src/endpoints/pool/pool.gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/pool/pool.gateway.ts b/src/endpoints/pool/pool.gateway.ts index 2a064b8dd..3c1b6ded1 100644 --- a/src/endpoints/pool/pool.gateway.ts +++ b/src/endpoints/pool/pool.gateway.ts @@ -4,7 +4,7 @@ import { SubscribeMessage, OnGatewayDisconnect, MessageBody, - ConnectedSocket + ConnectedSocket, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { UseFilters } from '@nestjs/common'; From 404b18f0301a7760cf906a26cde200fbb6c4ec3c Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 2 Sep 2025 15:03:09 +0300 Subject: [PATCH 13/33] add support for events subscription --- src/crons/websocket/websocket.cron.service.ts | 8 +++ src/crons/websocket/websocket.crons.module.ts | 2 + .../events/entities/events.subscribe.ts | 19 ++++++ src/endpoints/events/events.gateway.ts | 66 +++++++++++++++++++ src/endpoints/events/events.module.ts | 5 +- 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/endpoints/events/entities/events.subscribe.ts create mode 100644 src/endpoints/events/events.gateway.ts diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index 5a012e32e..396a0df08 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -5,6 +5,7 @@ import { BlocksGateway } from 'src/endpoints/blocks/blocks.gateway'; import { NetworkGateway } from 'src/endpoints/network/network.gateway'; import { Lock } from "@multiversx/sdk-nestjs-common"; import { PoolGateway } from 'src/endpoints/pool/pool.gateway'; +import { EventsGateway } from 'src/endpoints/events/events.gateway'; @Injectable() export class WebsocketCronService { constructor( @@ -12,6 +13,7 @@ export class WebsocketCronService { private readonly blocksGateway: BlocksGateway, private readonly networkGateway: NetworkGateway, private readonly poolGateway: PoolGateway, + private readonly eventsGateway: EventsGateway, ) { } @Cron('*/6 * * * * *') @@ -37,4 +39,10 @@ export class WebsocketCronService { async handlePoolTransactions() { await this.poolGateway.pushPool(); } + + @Cron('*/6 * * * * *') + @Lock({ name: 'Push events to subscribers', verbose: true }) + async handleEventsUpdate() { + await this.eventsGateway.pushEvents(); + } } diff --git a/src/crons/websocket/websocket.crons.module.ts b/src/crons/websocket/websocket.crons.module.ts index fb8eae363..344320d62 100644 --- a/src/crons/websocket/websocket.crons.module.ts +++ b/src/crons/websocket/websocket.crons.module.ts @@ -5,6 +5,7 @@ import { WebsocketCronService } from './websocket.cron.service'; import { BlockModule } from 'src/endpoints/blocks/block.module'; import { NetworkModule } from 'src/endpoints/network/network.module'; import { PoolModule } from 'src/endpoints/pool/pool.module'; +import { EventsModule } from 'src/endpoints/events/events.module'; @Module({ imports: [ @@ -13,6 +14,7 @@ import { PoolModule } from 'src/endpoints/pool/pool.module'; BlockModule, NetworkModule, PoolModule, + EventsModule, ], providers: [ WebsocketCronService, diff --git a/src/endpoints/events/entities/events.subscribe.ts b/src/endpoints/events/entities/events.subscribe.ts new file mode 100644 index 000000000..6adffbd67 --- /dev/null +++ b/src/endpoints/events/entities/events.subscribe.ts @@ -0,0 +1,19 @@ +import { IsOptional, IsNumber, Min, Max, IsIn } from 'class-validator'; + +export class EventsSubscribePayload { + @IsOptional() + @IsNumber() + @Min(0) + shard?: number; + + @IsOptional() + @IsNumber() + @IsIn([0], { message: 'from can only be 0' }) + from?: number = 0; + + @IsOptional() + @IsNumber() + @Min(1, { message: 'minimum size is 1' }) + @Max(50, { message: 'maximum size is 50' }) + size?: number = 25; +} diff --git a/src/endpoints/events/events.gateway.ts b/src/endpoints/events/events.gateway.ts new file mode 100644 index 000000000..0b0c2b857 --- /dev/null +++ b/src/endpoints/events/events.gateway.ts @@ -0,0 +1,66 @@ +import { + WebSocketGateway, + WebSocketServer, + SubscribeMessage, + OnGatewayDisconnect, + MessageBody, + ConnectedSocket +} from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; +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'; + +import { EventsService } from './events.service'; +import { EventsFilter } from './entities/events.filter'; +import { EventsSubscribePayload } from './entities/events.subscribe'; +import { QueryPagination } from 'src/common/entities/query.pagination'; + +@UseFilters(WebsocketExceptionsFilter) +@WebSocketGateway({ cors: { origin: '*' } }) +export class EventsGateway implements OnGatewayDisconnect { + private readonly logger = new OriginLogger(EventsGateway.name); + + @WebSocketServer() + server!: Server; + + constructor(private readonly eventsService: EventsService) { } + + @SubscribeMessage('subscribeEvents') + async handleSubscription( + @ConnectedSocket() client: Socket, + @MessageBody(new WsValidationPipe()) payload: EventsSubscribePayload, + ) { + const filterHash = JSON.stringify(payload); + await client.join(`events-${filterHash}`); + + return { status: 'success' }; + } + + async pushEvents() { + for (const [roomName] of this.server.sockets.adapter.rooms) { + try { + if (!roomName.startsWith("events-")) continue; + + const filterHash = roomName.replace("events-", ""); + const filter: EventsSubscribePayload = JSON.parse(filterHash); + + const eventsFilter = new EventsFilter({ + shard: filter.shard, + }); + + const events = await this.eventsService.getEvents( + new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), + eventsFilter, + ); + + this.server.to(roomName).emit('eventsUpdate', events); + } catch (error) { + this.logger.error(error); + } + } + } + + handleDisconnect(_client: Socket) { } +} diff --git a/src/endpoints/events/events.module.ts b/src/endpoints/events/events.module.ts index 9cdf3f729..cfbfc300d 100644 --- a/src/endpoints/events/events.module.ts +++ b/src/endpoints/events/events.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; import { EventsService } from './events.service'; +import { EventsGateway } from './events.gateway'; @Module({ - providers: [EventsService], - exports: [EventsService], + providers: [EventsService, EventsGateway], + exports: [EventsService, EventsGateway], }) export class EventsModule { } From d12208d703096bfc3f97389ac212c1c26706b9c1 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Tue, 2 Sep 2025 15:04:22 +0300 Subject: [PATCH 14/33] lint --- src/endpoints/events/events.gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/events/events.gateway.ts b/src/endpoints/events/events.gateway.ts index 0b0c2b857..7b9600ea4 100644 --- a/src/endpoints/events/events.gateway.ts +++ b/src/endpoints/events/events.gateway.ts @@ -4,7 +4,7 @@ import { SubscribeMessage, OnGatewayDisconnect, MessageBody, - ConnectedSocket + ConnectedSocket, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { UseFilters } from '@nestjs/common'; From c0833a7ffc1cb5cce9510d8778f49718a152ef09 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 3 Sep 2025 16:06:13 +0300 Subject: [PATCH 15/33] separate subscription websocket into separate app --- src/common/api-config/api.config.service.ts | 18 ++++++++++++++++++ .../websocket}/blocks.gateway.ts | 6 +++--- .../websocket}/network.gateway.ts | 2 +- .../pool => crons/websocket}/pool.gateway.ts | 6 +++--- .../websocket}/transaction.gateway.ts | 8 ++++---- src/crons/websocket/websocket.cron.service.ts | 8 ++++---- ...ule.ts => websocket.subscription.module.ts} | 10 +++++++++- src/endpoints/blocks/block.module.ts | 5 ++--- src/endpoints/network/network.module.ts | 5 ++--- src/endpoints/pool/pool.module.ts | 5 ++--- .../transactions/transaction.module.ts | 5 ++--- src/main.ts | 10 +++++++++- src/public.app.module.ts | 2 -- 13 files changed, 59 insertions(+), 31 deletions(-) rename src/{endpoints/blocks => crons/websocket}/blocks.gateway.ts (89%) rename src/{endpoints/network => crons/websocket}/network.gateway.ts (93%) rename src/{endpoints/pool => crons/websocket}/pool.gateway.ts (89%) rename src/{endpoints/transactions => crons/websocket}/transaction.gateway.ts (90%) rename src/crons/websocket/{websocket.crons.module.ts => websocket.subscription.module.ts} (67%) diff --git a/src/common/api-config/api.config.service.ts b/src/common/api-config/api.config.service.ts index 77c400332..e157c943a 100644 --- a/src/common/api-config/api.config.service.ts +++ b/src/common/api-config/api.config.service.ts @@ -954,4 +954,22 @@ export class ApiConfigService { getCompressionChunkSize(): number { return this.configService.get('compression.chunkSize') ?? 16384; } + + getIsWebsocketSubscriptionActive(): boolean { + const isWebsocketSubscriptionActive = this.configService.get('features.websocketSubscription.enabled'); + if (isWebsocketSubscriptionActive === undefined) { + throw new Error('No features.websocketSubscription.enabled flag present'); + } + + return isWebsocketSubscriptionActive; + } + + getWebsocketSubscriptionPort(): number { + const port = this.configService.get('features.websocketSubscription.port'); + if (port === undefined) { + throw new Error('No features.websocketSubscription.port present'); + } + + return port; + } } diff --git a/src/endpoints/blocks/blocks.gateway.ts b/src/crons/websocket/blocks.gateway.ts similarity index 89% rename from src/endpoints/blocks/blocks.gateway.ts rename to src/crons/websocket/blocks.gateway.ts index fe7098d59..869cd9fea 100644 --- a/src/endpoints/blocks/blocks.gateway.ts +++ b/src/crons/websocket/blocks.gateway.ts @@ -1,9 +1,9 @@ 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 { BlockService } from '../../endpoints/blocks/block.service'; +import { BlockFilter } from '../../endpoints/blocks/entities/block.filter'; import { QueryPagination } from 'src/common/entities/query.pagination'; -import { BlockSubscribePayload } from './entities/block.subscribe'; +import { BlockSubscribePayload } from '../../endpoints/blocks/entities/block.subscribe'; import { UseFilters } from '@nestjs/common'; import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; import { WsValidationPipe } from 'src/utils/ws-validation.pipe'; diff --git a/src/endpoints/network/network.gateway.ts b/src/crons/websocket/network.gateway.ts similarity index 93% rename from src/endpoints/network/network.gateway.ts rename to src/crons/websocket/network.gateway.ts index 9db26d666..ebe209f45 100644 --- a/src/endpoints/network/network.gateway.ts +++ b/src/crons/websocket/network.gateway.ts @@ -1,6 +1,6 @@ import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; -import { NetworkService } from './network.service'; +import { NetworkService } from '../../endpoints/network/network.service'; import { UseFilters } from '@nestjs/common'; import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; import { OriginLogger } from '@multiversx/sdk-nestjs-common'; diff --git a/src/endpoints/pool/pool.gateway.ts b/src/crons/websocket/pool.gateway.ts similarity index 89% rename from src/endpoints/pool/pool.gateway.ts rename to src/crons/websocket/pool.gateway.ts index 3c1b6ded1..537cf7412 100644 --- a/src/endpoints/pool/pool.gateway.ts +++ b/src/crons/websocket/pool.gateway.ts @@ -12,10 +12,10 @@ import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; import { WsValidationPipe } from 'src/utils/ws-validation.pipe'; import { OriginLogger } from '@multiversx/sdk-nestjs-common'; -import { PoolService } from './pool.service'; -import { PoolFilter } from './entities/pool.filter'; +import { PoolService } from '../../endpoints/pool/pool.service'; +import { PoolFilter } from '../../endpoints/pool/entities/pool.filter'; import { QueryPagination } from 'src/common/entities/query.pagination'; -import { PoolSubscribePayload } from './entities/pool.subscribe'; +import { PoolSubscribePayload } from '../../endpoints/pool/entities/pool.subscribe'; @UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' } }) diff --git a/src/endpoints/transactions/transaction.gateway.ts b/src/crons/websocket/transaction.gateway.ts similarity index 90% rename from src/endpoints/transactions/transaction.gateway.ts rename to src/crons/websocket/transaction.gateway.ts index 8005f5856..7dca5498a 100644 --- a/src/endpoints/transactions/transaction.gateway.ts +++ b/src/crons/websocket/transaction.gateway.ts @@ -1,11 +1,11 @@ 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 { TransactionService } from '../../endpoints/transactions/transaction.service'; +import { TransactionFilter } from '../../endpoints/transactions/entities/transaction.filter'; import { QueryPagination } from 'src/common/entities/query.pagination'; -import { TransactionQueryOptions } from './entities/transactions.query.options'; +import { TransactionQueryOptions } from '../../endpoints/transactions/entities/transactions.query.options'; import { WsValidationPipe } from 'src/utils/ws-validation.pipe'; -import { TransactionSubscribePayload } from './entities/dtos/transaction.subscribe'; +import { TransactionSubscribePayload } from '../../endpoints/transactions/entities/dtos/transaction.subscribe'; import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; import { UseFilters } from '@nestjs/common'; import { OriginLogger } from '@multiversx/sdk-nestjs-common'; diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index 396a0df08..c498d9228 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -1,10 +1,10 @@ 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 { TransactionsGateway } from './transaction.gateway'; +import { BlocksGateway } from 'src/crons/websocket/blocks.gateway'; +import { NetworkGateway } from 'src/crons/websocket/network.gateway'; import { Lock } from "@multiversx/sdk-nestjs-common"; -import { PoolGateway } from 'src/endpoints/pool/pool.gateway'; +import { PoolGateway } from 'src/crons/websocket/pool.gateway'; import { EventsGateway } from 'src/endpoints/events/events.gateway'; @Injectable() export class WebsocketCronService { diff --git a/src/crons/websocket/websocket.crons.module.ts b/src/crons/websocket/websocket.subscription.module.ts similarity index 67% rename from src/crons/websocket/websocket.crons.module.ts rename to src/crons/websocket/websocket.subscription.module.ts index 344320d62..f8949ee77 100644 --- a/src/crons/websocket/websocket.crons.module.ts +++ b/src/crons/websocket/websocket.subscription.module.ts @@ -6,6 +6,10 @@ import { BlockModule } from 'src/endpoints/blocks/block.module'; import { NetworkModule } from 'src/endpoints/network/network.module'; import { PoolModule } from 'src/endpoints/pool/pool.module'; import { EventsModule } from 'src/endpoints/events/events.module'; +import { BlocksGateway } from './blocks.gateway'; +import { NetworkGateway } from './network.gateway'; +import { TransactionsGateway } from './transaction.gateway'; +import { PoolGateway } from './pool.gateway'; @Module({ imports: [ @@ -18,6 +22,10 @@ import { EventsModule } from 'src/endpoints/events/events.module'; ], providers: [ WebsocketCronService, + BlocksGateway, + NetworkGateway, + TransactionsGateway, + PoolGateway, ], }) -export class WebSocketCronModule { } +export class WebsocketSubscriptionModule { } 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/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/pool/pool.module.ts b/src/endpoints/pool/pool.module.ts index 2d891f606..88c7a780e 100644 --- a/src/endpoints/pool/pool.module.ts +++ b/src/endpoints/pool/pool.module.ts @@ -1,17 +1,16 @@ import { Module } from "@nestjs/common"; import { PoolService } from "./pool.service"; import { TransactionActionModule } from "../transactions/transaction-action/transaction.action.module"; -import { PoolGateway } from "./pool.gateway"; @Module({ imports: [ TransactionActionModule, ], providers: [ - PoolService, PoolGateway, + PoolService, ], exports: [ - PoolService, PoolGateway, + PoolService, ], }) 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 { } diff --git a/src/main.ts b/src/main.ts index b31cb34a6..aca22383e 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 { WebsocketSubscriptionModule } from './crons/websocket/websocket.subscription.module'; async function bootstrap() { const logger = new Logger('Bootstrapper'); @@ -53,7 +54,6 @@ async function bootstrap() { if (apiConfigService.getIsPublicApiActive()) { const publicApp = await NestFactory.create(PublicAppModule); - publicApp.useWebSocketAdapter(new IoAdapter(publicApp)); await configurePublicApp(publicApp, apiConfigService); @@ -89,6 +89,13 @@ async function bootstrap() { await processorApp.listen(5001); } + + if (apiConfigService.getIsWebsocketSubscriptionActive()) { + const websocketSubscriptionApp = await NestFactory.create(WebsocketSubscriptionModule); + websocketSubscriptionApp.useWebSocketAdapter(new IoAdapter(websocketSubscriptionApp)); + await websocketSubscriptionApp.listen(apiConfigService.getWebsocketSubscriptionPort()); + } + if (apiConfigService.getIsCacheWarmerCronActive()) { const cacheWarmerApp = await NestFactory.create(CacheWarmerModule); await configureCacheWarmerApp(cacheWarmerApp, apiConfigService); @@ -170,6 +177,7 @@ async function bootstrap() { logger.log(`Exchange feature active: ${apiConfigService.isExchangeEnabled()}`); logger.log(`Marketplace feature active: ${apiConfigService.isMarketplaceFeatureEnabled()}`); logger.log(`Auth active: ${apiConfigService.getIsAuthActive()}`); + logger.log(`WebSocket subscription active: ${apiConfigService.getIsWebsocketSubscriptionActive()}`); logger.log(`Use tracing: ${apiConfigService.getUseTracingFlag()}`); logger.log(`Process NFTs flag: ${apiConfigService.getIsProcessNftsFlagActive()}`); 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 4f9741d47438f8cc8c72f4dc5ff4988a6a8ccbcf Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 3 Sep 2025 16:07:10 +0300 Subject: [PATCH 16/33] add config --- config/config.devnet.yaml | 9 ++++++--- config/config.mainnet.yaml | 3 +++ config/config.testnet.yaml | 3 +++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 7d42d5d61..3a9e7ccef 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -7,9 +7,9 @@ api: privatePort: 4001 websocket: false cron: - cacheWarmer: true - fastWarm: true - queueWorker: true + cacheWarmer: false + fastWarm: false + queueWorker: false elasticUpdater: false flags: useRequestCaching: true @@ -20,6 +20,9 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: + websocketSubscription: + enabled: true + port: 6002 eventsNotifier: enabled: false port: 5674 diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index edc9c7a00..cdfec6316 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -20,6 +20,9 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: + websocketSubscription: + enabled: true + port: 6002 eventsNotifier: enabled: false port: 5674 diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 1f1887414..1232436d1 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -20,6 +20,9 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: + websocketSubscription: + enabled: true + port: 6002 eventsNotifier: enabled: false port: 5674 From 889a75b49d58220d24c3fc09fef5dd15e7e03ac5 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 3 Sep 2025 16:39:08 +0300 Subject: [PATCH 17/33] add path --- src/crons/websocket/blocks.gateway.ts | 2 +- src/crons/websocket/network.gateway.ts | 2 +- src/crons/websocket/pool.gateway.ts | 2 +- src/crons/websocket/transaction.gateway.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/crons/websocket/blocks.gateway.ts b/src/crons/websocket/blocks.gateway.ts index 869cd9fea..7d7b874f8 100644 --- a/src/crons/websocket/blocks.gateway.ts +++ b/src/crons/websocket/blocks.gateway.ts @@ -10,7 +10,7 @@ import { WsValidationPipe } from 'src/utils/ws-validation.pipe'; import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @UseFilters(WebsocketExceptionsFilter) -@WebSocketGateway({ cors: { origin: '*' } }) +@WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) export class BlocksGateway implements OnGatewayDisconnect { private readonly logger = new OriginLogger(BlocksGateway.name); diff --git a/src/crons/websocket/network.gateway.ts b/src/crons/websocket/network.gateway.ts index ebe209f45..6ac0242f2 100644 --- a/src/crons/websocket/network.gateway.ts +++ b/src/crons/websocket/network.gateway.ts @@ -6,7 +6,7 @@ import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @UseFilters(WebsocketExceptionsFilter) -@WebSocketGateway({ cors: { origin: '*' } }) +@WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) export class NetworkGateway implements OnGatewayDisconnect { private readonly logger = new OriginLogger(NetworkGateway.name); diff --git a/src/crons/websocket/pool.gateway.ts b/src/crons/websocket/pool.gateway.ts index 537cf7412..b7386caec 100644 --- a/src/crons/websocket/pool.gateway.ts +++ b/src/crons/websocket/pool.gateway.ts @@ -18,7 +18,7 @@ import { QueryPagination } from 'src/common/entities/query.pagination'; import { PoolSubscribePayload } from '../../endpoints/pool/entities/pool.subscribe'; @UseFilters(WebsocketExceptionsFilter) -@WebSocketGateway({ cors: { origin: '*' } }) +@WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) export class PoolGateway implements OnGatewayDisconnect { private readonly logger = new OriginLogger(PoolGateway.name); diff --git a/src/crons/websocket/transaction.gateway.ts b/src/crons/websocket/transaction.gateway.ts index 7dca5498a..82d025aea 100644 --- a/src/crons/websocket/transaction.gateway.ts +++ b/src/crons/websocket/transaction.gateway.ts @@ -11,7 +11,7 @@ import { UseFilters } from '@nestjs/common'; import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @UseFilters(WebsocketExceptionsFilter) -@WebSocketGateway({ cors: { origin: '*' } }) +@WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) export class TransactionsGateway implements OnGatewayDisconnect { private readonly logger = new OriginLogger(TransactionsGateway.name); From cdfc7117ba7b50bc39c887d602af7d25a06c41af Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 3 Sep 2025 17:06:38 +0300 Subject: [PATCH 18/33] fix --- .../events => crons/websocket}/events.gateway.ts | 7 +++---- src/crons/websocket/websocket.cron.service.ts | 2 +- src/crons/websocket/websocket.subscription.module.ts | 2 ++ src/endpoints/events/events.module.ts | 6 ++---- 4 files changed, 8 insertions(+), 9 deletions(-) rename src/{endpoints/events => crons/websocket}/events.gateway.ts (89%) diff --git a/src/endpoints/events/events.gateway.ts b/src/crons/websocket/events.gateway.ts similarity index 89% rename from src/endpoints/events/events.gateway.ts rename to src/crons/websocket/events.gateway.ts index 7b9600ea4..d0190f2c7 100644 --- a/src/endpoints/events/events.gateway.ts +++ b/src/crons/websocket/events.gateway.ts @@ -11,10 +11,9 @@ 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'; - -import { EventsService } from './events.service'; -import { EventsFilter } from './entities/events.filter'; -import { EventsSubscribePayload } from './entities/events.subscribe'; +import { EventsService } from '../../endpoints/events/events.service'; +import { EventsFilter } from '../../endpoints/events/entities/events.filter'; +import { EventsSubscribePayload } from '../../endpoints/events/entities/events.subscribe'; import { QueryPagination } from 'src/common/entities/query.pagination'; @UseFilters(WebsocketExceptionsFilter) diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index c498d9228..8befa9367 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -5,7 +5,7 @@ import { BlocksGateway } from 'src/crons/websocket/blocks.gateway'; import { NetworkGateway } from 'src/crons/websocket/network.gateway'; import { Lock } from "@multiversx/sdk-nestjs-common"; import { PoolGateway } from 'src/crons/websocket/pool.gateway'; -import { EventsGateway } from 'src/endpoints/events/events.gateway'; +import { EventsGateway } from 'src/crons/websocket/events.gateway'; @Injectable() export class WebsocketCronService { constructor( diff --git a/src/crons/websocket/websocket.subscription.module.ts b/src/crons/websocket/websocket.subscription.module.ts index f8949ee77..284e57621 100644 --- a/src/crons/websocket/websocket.subscription.module.ts +++ b/src/crons/websocket/websocket.subscription.module.ts @@ -10,6 +10,7 @@ import { BlocksGateway } from './blocks.gateway'; import { NetworkGateway } from './network.gateway'; import { TransactionsGateway } from './transaction.gateway'; import { PoolGateway } from './pool.gateway'; +import { EventsGateway } from './events.gateway'; @Module({ imports: [ @@ -26,6 +27,7 @@ import { PoolGateway } from './pool.gateway'; NetworkGateway, TransactionsGateway, PoolGateway, + EventsGateway, ], }) export class WebsocketSubscriptionModule { } diff --git a/src/endpoints/events/events.module.ts b/src/endpoints/events/events.module.ts index cfbfc300d..4e7ae6221 100644 --- a/src/endpoints/events/events.module.ts +++ b/src/endpoints/events/events.module.ts @@ -1,9 +1,7 @@ import { Module } from '@nestjs/common'; import { EventsService } from './events.service'; -import { EventsGateway } from './events.gateway'; - @Module({ - providers: [EventsService, EventsGateway], - exports: [EventsService, EventsGateway], + providers: [EventsService], + exports: [EventsService], }) export class EventsModule { } From 788284bc666e827b9d5c6c959db158cfca162d42 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 3 Sep 2025 17:20:49 +0300 Subject: [PATCH 19/33] add path for events + config default settings --- config/config.devnet.yaml | 8 ++++---- config/config.mainnet.yaml | 2 +- config/config.testnet.yaml | 2 +- src/crons/websocket/events.gateway.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 3a9e7ccef..4f9db17de 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -5,11 +5,11 @@ api: publicPort: 3001 private: true privatePort: 4001 - websocket: false + websocket: true cron: - cacheWarmer: false + cacheWarmer: true fastWarm: false - queueWorker: false + queueWorker: true elasticUpdater: false flags: useRequestCaching: true @@ -21,7 +21,7 @@ flags: collectionPropertiesFromGateway: false features: websocketSubscription: - enabled: true + enabled: false port: 6002 eventsNotifier: enabled: false diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index cdfec6316..3ceb51cd0 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -21,7 +21,7 @@ flags: collectionPropertiesFromGateway: false features: websocketSubscription: - enabled: true + enabled: false port: 6002 eventsNotifier: enabled: false diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 1232436d1..a5305a200 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -21,7 +21,7 @@ flags: collectionPropertiesFromGateway: false features: websocketSubscription: - enabled: true + enabled: false port: 6002 eventsNotifier: enabled: false diff --git a/src/crons/websocket/events.gateway.ts b/src/crons/websocket/events.gateway.ts index d0190f2c7..fd0187a2d 100644 --- a/src/crons/websocket/events.gateway.ts +++ b/src/crons/websocket/events.gateway.ts @@ -17,7 +17,7 @@ import { EventsSubscribePayload } from '../../endpoints/events/entities/events.s import { QueryPagination } from 'src/common/entities/query.pagination'; @UseFilters(WebsocketExceptionsFilter) -@WebSocketGateway({ cors: { origin: '*' } }) +@WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) export class EventsGateway implements OnGatewayDisconnect { private readonly logger = new OriginLogger(EventsGateway.name); From 207ef83b65e43956dedfaa55069a25fd4548be5f Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Thu, 4 Sep 2025 10:40:03 +0300 Subject: [PATCH 20/33] temp logs --- src/test/chain-simulator/utils/test.utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/chain-simulator/utils/test.utils.ts b/src/test/chain-simulator/utils/test.utils.ts index 41e20f9fa..a7e153a35 100644 --- a/src/test/chain-simulator/utils/test.utils.ts +++ b/src/test/chain-simulator/utils/test.utils.ts @@ -14,6 +14,7 @@ export class ChainSimulatorUtils { while (retries < maxRetries) { try { const networkStatus = await axios.get(`${config.chainSimulatorUrl}/network/status/4294967295`); + console.log(`Network status: ${JSON.stringify(networkStatus)}`); const currentEpoch = networkStatus.data.erd_epoch_number; if (currentEpoch >= targetEpoch) { @@ -27,6 +28,7 @@ export class ChainSimulatorUtils { // Verify we reached the target epoch const stats = await axios.get(`${config.apiServiceUrl}/stats`); + console.log(`API stats: ${JSON.stringify(stats)}`); const newEpoch = stats.data.epoch; if (newEpoch >= targetEpoch) { From 524fd9ed7ed0949b6d4d95a9c34c47699a836f6b Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Thu, 4 Sep 2025 10:44:59 +0300 Subject: [PATCH 21/33] temp logs 2 --- src/test/chain-simulator/utils/test.utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/chain-simulator/utils/test.utils.ts b/src/test/chain-simulator/utils/test.utils.ts index a7e153a35..757584410 100644 --- a/src/test/chain-simulator/utils/test.utils.ts +++ b/src/test/chain-simulator/utils/test.utils.ts @@ -14,7 +14,7 @@ export class ChainSimulatorUtils { while (retries < maxRetries) { try { const networkStatus = await axios.get(`${config.chainSimulatorUrl}/network/status/4294967295`); - console.log(`Network status: ${JSON.stringify(networkStatus)}`); + console.log(`Network status: ${JSON.stringify(networkStatus.data)}`); const currentEpoch = networkStatus.data.erd_epoch_number; if (currentEpoch >= targetEpoch) { @@ -28,7 +28,7 @@ export class ChainSimulatorUtils { // Verify we reached the target epoch const stats = await axios.get(`${config.apiServiceUrl}/stats`); - console.log(`API stats: ${JSON.stringify(stats)}`); + console.log(`API stats: ${JSON.stringify(stats.data)}`); const newEpoch = stats.data.epoch; if (newEpoch >= targetEpoch) { @@ -59,6 +59,7 @@ export class ChainSimulatorUtils { while (retries < maxRetries) { try { const response = await axios.get(`${config.chainSimulatorUrl}/simulator/observers`); + console.log(`Simulator observers: ${JSON.stringify(response.data)}`); if (response.status === 200) { return true; } From e6447944b2d8e7945caab619a81feea68756e6dd Mon Sep 17 00:00:00 2001 From: bogdan-rosianu Date: Thu, 4 Sep 2025 10:48:26 +0300 Subject: [PATCH 22/33] added missing configs + remove temp logs --- config/config.e2e-mocked.mainnet.yaml | 3 +++ config/config.e2e.mainnet.yaml | 3 +++ src/test/chain-simulator/utils/test.utils.ts | 3 --- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/config/config.e2e-mocked.mainnet.yaml b/config/config.e2e-mocked.mainnet.yaml index ef1cd3eed..83d35ceac 100644 --- a/config/config.e2e-mocked.mainnet.yaml +++ b/config/config.e2e-mocked.mainnet.yaml @@ -5,6 +5,9 @@ api: private: true graphql: true features: + websocketSubscription: + enabled: false + port: 6002 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..03e4998b1 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -20,6 +20,9 @@ flags: processNfts: true collectionPropertiesFromGateway: false features: + websocketSubscription: + enabled: false + port: 6002 eventsNotifier: enabled: false port: 5674 diff --git a/src/test/chain-simulator/utils/test.utils.ts b/src/test/chain-simulator/utils/test.utils.ts index 757584410..41e20f9fa 100644 --- a/src/test/chain-simulator/utils/test.utils.ts +++ b/src/test/chain-simulator/utils/test.utils.ts @@ -14,7 +14,6 @@ export class ChainSimulatorUtils { while (retries < maxRetries) { try { const networkStatus = await axios.get(`${config.chainSimulatorUrl}/network/status/4294967295`); - console.log(`Network status: ${JSON.stringify(networkStatus.data)}`); const currentEpoch = networkStatus.data.erd_epoch_number; if (currentEpoch >= targetEpoch) { @@ -28,7 +27,6 @@ export class ChainSimulatorUtils { // Verify we reached the target epoch const stats = await axios.get(`${config.apiServiceUrl}/stats`); - console.log(`API stats: ${JSON.stringify(stats.data)}`); const newEpoch = stats.data.epoch; if (newEpoch >= targetEpoch) { @@ -59,7 +57,6 @@ export class ChainSimulatorUtils { while (retries < maxRetries) { try { const response = await axios.get(`${config.chainSimulatorUrl}/simulator/observers`); - console.log(`Simulator observers: ${JSON.stringify(response.data)}`); if (response.status === 200) { return true; } From c5dc598d445d4255296cfd19fd446141509f9c1c Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 4 Sep 2025 13:35:33 +0300 Subject: [PATCH 23/33] enable andromeda in config --- config/config.devnet.yaml | 2 +- config/config.e2e.mainnet.yaml | 2 +- config/config.mainnet.yaml | 2 +- config/config.testnet.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 4f9db17de..de8480d00 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -68,7 +68,7 @@ features: cronExpression: '*/5 * * * * *' activationEpoch: 1043 chainAndromeda: - enabled: false + enabled: true activationEpoch: 4 nodeEpochsLeft: enabled: false diff --git a/config/config.e2e.mainnet.yaml b/config/config.e2e.mainnet.yaml index 03e4998b1..9cdb71bc6 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -66,7 +66,7 @@ features: cronExpression: '*/5 * * * * *' activationEpoch: 1391 chainAndromeda: - enabled: false + enabled: true activationEpoch: 4 nodeEpochsLeft: enabled: false diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index 3ceb51cd0..0f10dacba 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -66,7 +66,7 @@ features: cronExpression: '*/5 * * * * *' activationEpoch: 1391 chainAndromeda: - enabled: false + enabled: true activationEpoch: 4 nodeEpochsLeft: enabled: false diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index a5305a200..e0353f514 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -65,7 +65,7 @@ features: cronExpression: '*/5 * * * * *' activationEpoch: 1043 chainAndromeda: - enabled: false + enabled: true activationEpoch: 4 nodeEpochsLeft: enabled: false From cad49bc397a60f913c295508fa9e47b62458144a Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Mon, 8 Sep 2025 15:10:32 +0300 Subject: [PATCH 24/33] add metrics on subscription --- src/common/metrics/api.metrics.service.ts | 16 +++++++++++++ src/crons/websocket/websocket.cron.service.ts | 24 +++++++++++++++++++ src/utils/metrics-events.constants.ts | 1 + 3 files changed, 41 insertions(+) diff --git a/src/common/metrics/api.metrics.service.ts b/src/common/metrics/api.metrics.service.ts index eea34676a..ff2d24260 100644 --- a/src/common/metrics/api.metrics.service.ts +++ b/src/common/metrics/api.metrics.service.ts @@ -22,6 +22,7 @@ export class ApiMetricsService { private static transactionsCompletedCounter: Counter; private static transactionsPendingResultsCounter: Counter; private static batchUpdatesCounter: Counter; + private static subscriptionsConnectionsGauge: Gauge; constructor( private readonly apiConfigService: ApiConfigService, @@ -32,6 +33,13 @@ export class ApiMetricsService { private readonly metricsService: MetricsService, ) { + if (!ApiMetricsService.subscriptionsConnectionsGauge) { + ApiMetricsService.subscriptionsConnectionsGauge = new Gauge({ + name: 'websocket_subscriptions_connections', + help: 'Number of websocket connections for subscriptions', + }); + } + if (!ApiMetricsService.vmQueriesHistogram) { ApiMetricsService.vmQueriesHistogram = new Histogram({ name: 'vm_query', @@ -182,6 +190,14 @@ export class ApiMetricsService { ApiMetricsService.lastProcessedTransactionCompletedProcessorNonce.set({ shardId }, nonce); } + @OnEvent(MetricsEvents.SetWebsocketMetrics) + setWebsocketSubscriptionsMetrics(payload: { connectedClients: number }) { + const { connectedClients } = payload; + + ApiMetricsService.subscriptionsConnectionsGauge.set(connectedClients); + } + + @OnEvent(MetricsEvents.SetTransactionsCompleted) recordTransactionsCompleted(payload: { transactions: any[] }) { ApiMetricsService.transactionsCompletedCounter.inc(payload.transactions.length); diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index 8befa9367..2dd105360 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -6,16 +6,40 @@ import { NetworkGateway } from 'src/crons/websocket/network.gateway'; import { Lock } from "@multiversx/sdk-nestjs-common"; import { PoolGateway } from 'src/crons/websocket/pool.gateway'; import { EventsGateway } from 'src/crons/websocket/events.gateway'; +import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { MetricsEvents } from 'src/utils/metrics-events.constants'; +import { Server } from 'socket.io'; @Injectable() +@WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) export class WebsocketCronService { + @WebSocketServer() + server!: Server; + constructor( private readonly transactionsGateway: TransactionsGateway, private readonly blocksGateway: BlocksGateway, private readonly networkGateway: NetworkGateway, private readonly poolGateway: PoolGateway, private readonly eventsGateway: EventsGateway, + private readonly eventEmitter: EventEmitter2, ) { } + @Cron('*/6 * * * * *') + async handleWebsocketMetrics() { + const connectedClients = this.server.sockets.sockets.size ?? 0; + // TODO: add more metrics in the future + // const subscriptions: Record = {}; + + // this.server.sockets.adapter.rooms.forEach((socketsSet, roomName) => { + // subscriptions[roomName] = socketsSet.size; + // }); + + this.eventEmitter.emit(MetricsEvents.SetWebsocketMetrics, { + connectedClients, + }); + } + @Cron('*/6 * * * * *') @Lock({ name: 'Push transactions to subscribers', verbose: true }) async handleTransactionsUpdate() { diff --git a/src/utils/metrics-events.constants.ts b/src/utils/metrics-events.constants.ts index a2e480638..2a5cb12ba 100644 --- a/src/utils/metrics-events.constants.ts +++ b/src/utils/metrics-events.constants.ts @@ -10,4 +10,5 @@ export enum MetricsEvents { SetTransactionsCompleted = "setTransactionsCompleted", SetTransactionsPendingResults = "setTransactionsPendingResults", SetBatchUpdated = "setBatchUpdated", + SetWebsocketMetrics = "setWebsocketMetrics", } From 8e92d63c01daac620573ec5fd0a3fe559965ebaa Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Mon, 8 Sep 2025 15:12:50 +0300 Subject: [PATCH 25/33] remove async + reschedule --- src/crons/websocket/websocket.cron.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index 2dd105360..86f8b2549 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -25,8 +25,8 @@ export class WebsocketCronService { private readonly eventEmitter: EventEmitter2, ) { } - @Cron('*/6 * * * * *') - async handleWebsocketMetrics() { + @Cron('*/5 * * * * *') + handleWebsocketMetrics() { const connectedClients = this.server.sockets.sockets.size ?? 0; // TODO: add more metrics in the future // const subscriptions: Record = {}; From 578303a82738427cca0269f2821eb32df24a2c6d Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Mon, 8 Sep 2025 15:18:36 +0300 Subject: [PATCH 26/33] refresh metrics every second --- src/crons/websocket/websocket.cron.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crons/websocket/websocket.cron.service.ts b/src/crons/websocket/websocket.cron.service.ts index 86f8b2549..9aae69097 100644 --- a/src/crons/websocket/websocket.cron.service.ts +++ b/src/crons/websocket/websocket.cron.service.ts @@ -25,7 +25,7 @@ export class WebsocketCronService { private readonly eventEmitter: EventEmitter2, ) { } - @Cron('*/5 * * * * *') + @Cron('*/1 * * * * *') handleWebsocketMetrics() { const connectedClients = this.server.sockets.sockets.size ?? 0; // TODO: add more metrics in the future From 6679b1fe0926bed333955c6aff660587f1d9d0be Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Wed, 10 Sep 2025 16:15:26 +0300 Subject: [PATCH 27/33] set max listeners to 12 --- src/crons/websocket/blocks.gateway.ts | 7 ++----- src/crons/websocket/connection.handler.ts | 20 +++++++++++++++++++ src/crons/websocket/events.gateway.ts | 13 ++---------- src/crons/websocket/network.gateway.ts | 6 ++---- src/crons/websocket/pool.gateway.ts | 13 ++---------- src/crons/websocket/transaction.gateway.ts | 6 ++---- .../websocket.subscription.module.ts | 2 ++ 7 files changed, 32 insertions(+), 35 deletions(-) create mode 100644 src/crons/websocket/connection.handler.ts diff --git a/src/crons/websocket/blocks.gateway.ts b/src/crons/websocket/blocks.gateway.ts index 7d7b874f8..281f49d65 100644 --- a/src/crons/websocket/blocks.gateway.ts +++ b/src/crons/websocket/blocks.gateway.ts @@ -1,4 +1,4 @@ -import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect, MessageBody, ConnectedSocket } from '@nestjs/websockets'; +import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { BlockService } from '../../endpoints/blocks/block.service'; import { BlockFilter } from '../../endpoints/blocks/entities/block.filter'; @@ -11,7 +11,7 @@ import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) -export class BlocksGateway implements OnGatewayDisconnect { +export class BlocksGateway { private readonly logger = new OriginLogger(BlocksGateway.name); @WebSocketServer() @@ -19,7 +19,6 @@ export class BlocksGateway implements OnGatewayDisconnect { constructor(private readonly blockService: BlockService) { } - @SubscribeMessage('subscribeBlocks') async handleSubscription( @ConnectedSocket() client: Socket, @@ -56,7 +55,5 @@ export class BlocksGateway implements OnGatewayDisconnect { } } } - - handleDisconnect(_client: Socket) { } } diff --git a/src/crons/websocket/connection.handler.ts b/src/crons/websocket/connection.handler.ts new file mode 100644 index 000000000..079cd40b7 --- /dev/null +++ b/src/crons/websocket/connection.handler.ts @@ -0,0 +1,20 @@ +import { UseFilters } from "@nestjs/common"; +import { OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, WebSocketGateway, WebSocketServer } from "@nestjs/websockets"; +import { Socket, Server } from "socket.io"; +import { WebsocketExceptionsFilter } from "src/utils/ws-exceptions.filter"; + +@UseFilters(WebsocketExceptionsFilter) +@WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) +export class ConnectionHandler implements OnGatewayDisconnect, OnGatewayConnection, OnGatewayInit { + + @WebSocketServer() + server!: Server; + + afterInit(__server: Server) { } + + handleDisconnect(_client: Socket) { } + + handleConnection(client: Socket, ..._args: any[]) { + client.setMaxListeners(12); + } +} \ No newline at end of file diff --git a/src/crons/websocket/events.gateway.ts b/src/crons/websocket/events.gateway.ts index fd0187a2d..9834cf91e 100644 --- a/src/crons/websocket/events.gateway.ts +++ b/src/crons/websocket/events.gateway.ts @@ -1,11 +1,4 @@ -import { - WebSocketGateway, - WebSocketServer, - SubscribeMessage, - OnGatewayDisconnect, - MessageBody, - ConnectedSocket, -} from '@nestjs/websockets'; +import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { UseFilters } from '@nestjs/common'; import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; @@ -18,7 +11,7 @@ import { QueryPagination } from 'src/common/entities/query.pagination'; @UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) -export class EventsGateway implements OnGatewayDisconnect { +export class EventsGateway { private readonly logger = new OriginLogger(EventsGateway.name); @WebSocketServer() @@ -60,6 +53,4 @@ export class EventsGateway implements OnGatewayDisconnect { } } } - - handleDisconnect(_client: Socket) { } } diff --git a/src/crons/websocket/network.gateway.ts b/src/crons/websocket/network.gateway.ts index 6ac0242f2..5618de162 100644 --- a/src/crons/websocket/network.gateway.ts +++ b/src/crons/websocket/network.gateway.ts @@ -1,4 +1,4 @@ -import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect } from '@nestjs/websockets'; +import { WebSocketGateway, WebSocketServer, SubscribeMessage } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { NetworkService } from '../../endpoints/network/network.service'; import { UseFilters } from '@nestjs/common'; @@ -7,7 +7,7 @@ import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) -export class NetworkGateway implements OnGatewayDisconnect { +export class NetworkGateway { private readonly logger = new OriginLogger(NetworkGateway.name); @WebSocketServer() @@ -30,6 +30,4 @@ export class NetworkGateway implements OnGatewayDisconnect { } } } - - handleDisconnect(_client: Socket) { } } diff --git a/src/crons/websocket/pool.gateway.ts b/src/crons/websocket/pool.gateway.ts index b7386caec..feb9db5aa 100644 --- a/src/crons/websocket/pool.gateway.ts +++ b/src/crons/websocket/pool.gateway.ts @@ -1,11 +1,4 @@ -import { - WebSocketGateway, - WebSocketServer, - SubscribeMessage, - OnGatewayDisconnect, - MessageBody, - ConnectedSocket, -} from '@nestjs/websockets'; +import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { UseFilters } from '@nestjs/common'; import { WebsocketExceptionsFilter } from 'src/utils/ws-exceptions.filter'; @@ -19,7 +12,7 @@ import { PoolSubscribePayload } from '../../endpoints/pool/entities/pool.subscri @UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) -export class PoolGateway implements OnGatewayDisconnect { +export class PoolGateway { private readonly logger = new OriginLogger(PoolGateway.name); @WebSocketServer() @@ -56,6 +49,4 @@ export class PoolGateway implements OnGatewayDisconnect { } } } - - handleDisconnect(_client: Socket) { } } diff --git a/src/crons/websocket/transaction.gateway.ts b/src/crons/websocket/transaction.gateway.ts index 82d025aea..bca4054d2 100644 --- a/src/crons/websocket/transaction.gateway.ts +++ b/src/crons/websocket/transaction.gateway.ts @@ -1,4 +1,4 @@ -import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayDisconnect, ConnectedSocket, MessageBody } from '@nestjs/websockets'; +import { WebSocketGateway, WebSocketServer, SubscribeMessage, ConnectedSocket, MessageBody } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { TransactionService } from '../../endpoints/transactions/transaction.service'; import { TransactionFilter } from '../../endpoints/transactions/entities/transaction.filter'; @@ -12,7 +12,7 @@ import { OriginLogger } from '@multiversx/sdk-nestjs-common'; @UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) -export class TransactionsGateway implements OnGatewayDisconnect { +export class TransactionsGateway { private readonly logger = new OriginLogger(TransactionsGateway.name); @WebSocketServer() @@ -105,6 +105,4 @@ export class TransactionsGateway implements OnGatewayDisconnect { } } } - - handleDisconnect(_client: Socket) { } } diff --git a/src/crons/websocket/websocket.subscription.module.ts b/src/crons/websocket/websocket.subscription.module.ts index 284e57621..89276d50d 100644 --- a/src/crons/websocket/websocket.subscription.module.ts +++ b/src/crons/websocket/websocket.subscription.module.ts @@ -11,6 +11,7 @@ import { NetworkGateway } from './network.gateway'; import { TransactionsGateway } from './transaction.gateway'; import { PoolGateway } from './pool.gateway'; import { EventsGateway } from './events.gateway'; +import { ConnectionHandler } from './connection.handler'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { EventsGateway } from './events.gateway'; ], providers: [ WebsocketCronService, + ConnectionHandler, BlocksGateway, NetworkGateway, TransactionsGateway, From f6f747bd905756bed15bb791a3723d0c7ab47718 Mon Sep 17 00:00:00 2001 From: cfaur09 Date: Tue, 16 Sep 2025 11:44:58 +0300 Subject: [PATCH 28/33] add EOL --- src/crons/websocket/connection.handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crons/websocket/connection.handler.ts b/src/crons/websocket/connection.handler.ts index 079cd40b7..b8f50ed53 100644 --- a/src/crons/websocket/connection.handler.ts +++ b/src/crons/websocket/connection.handler.ts @@ -17,4 +17,4 @@ export class ConnectionHandler implements OnGatewayDisconnect, OnGatewayConnecti handleConnection(client: Socket, ..._args: any[]) { client.setMaxListeners(12); } -} \ No newline at end of file +} From d86907fa9150199b67f3d3fc8a3c67ea4181aac8 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 25 Sep 2025 16:48:29 +0300 Subject: [PATCH 29/33] add count on update + parallel broadcast to rooms --- src/crons/websocket/blocks.gateway.ts | 45 +++++++---- src/crons/websocket/events.gateway.ts | 46 +++++++---- src/crons/websocket/pool.gateway.ts | 46 +++++++---- src/crons/websocket/transaction.gateway.ts | 91 +++++++++++++--------- 4 files changed, 144 insertions(+), 84 deletions(-) diff --git a/src/crons/websocket/blocks.gateway.ts b/src/crons/websocket/blocks.gateway.ts index 281f49d65..83e79759b 100644 --- a/src/crons/websocket/blocks.gateway.ts +++ b/src/crons/websocket/blocks.gateway.ts @@ -25,35 +25,46 @@ export class BlocksGateway { @MessageBody(new WsValidationPipe()) payload: BlockSubscribePayload ) { const filterHash = JSON.stringify(payload); - await client.join(`block-${filterHash}`); + await client.join(`blocks-${filterHash}`); return { status: 'success' }; } - async pushBlocks() { - for (const [roomName] of this.server.sockets.adapter.rooms) { - try { - if (!roomName.startsWith("block-")) continue; + async pushBlocksForRoom(roomName: string): Promise { + if (!roomName.startsWith("blocks-")) return; - const filterHash = roomName.replace("block-", ""); - const filter: BlockSubscribePayload = JSON.parse(filterHash); + try { + const filterHash = roomName.replace("blocks-", ""); + const filter: BlockSubscribePayload = JSON.parse(filterHash); - const blockFilter = new BlockFilter({ - shard: filter.shard, - order: filter.order, - }); + const blockFilter = new BlockFilter({ + shard: filter.shard, + order: filter.order, + }); - const blocks = await this.blockService.getBlocks( + const [blocks, blocksCount] = await Promise.all([ + this.blockService.getBlocks( blockFilter, new QueryPagination({ from: filter.from, size: filter.size }), filter.withProposerIdentity, - ); + ), + this.blockService.getBlocksCount(blockFilter), + ]); - this.server.to(roomName).emit('blocksUpdate', blocks); - } catch (error) { - this.logger.error(error); - } + this.server.to(roomName).emit("blocksUpdate", { blocks, blocksCount }); + } catch (error) { + this.logger.error(error); } } + + async pushBlocks(): Promise { + const promises: Promise[] = []; + + for (const [roomName] of this.server.sockets.adapter.rooms) { + promises.push(this.pushBlocksForRoom(roomName)); + } + + await Promise.all(promises); + } } diff --git a/src/crons/websocket/events.gateway.ts b/src/crons/websocket/events.gateway.ts index 9834cf91e..14c6b510d 100644 --- a/src/crons/websocket/events.gateway.ts +++ b/src/crons/websocket/events.gateway.ts @@ -30,27 +30,41 @@ export class EventsGateway { return { status: 'success' }; } - async pushEvents() { - for (const [roomName] of this.server.sockets.adapter.rooms) { - try { - if (!roomName.startsWith("events-")) continue; + async pushEventsForRoom(roomName: string): Promise { + if (!roomName.startsWith("events-")) return; - const filterHash = roomName.replace("events-", ""); - const filter: EventsSubscribePayload = JSON.parse(filterHash); + try { + const filterHash = roomName.replace("events-", ""); + const filter: EventsSubscribePayload = JSON.parse(filterHash); - const eventsFilter = new EventsFilter({ - shard: filter.shard, - }); + const eventsFilter = new EventsFilter({ + shard: filter.shard, + }); - const events = await this.eventsService.getEvents( - new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), + const [events, eventsCount] = await Promise.all([ + this.eventsService.getEvents( + new QueryPagination({ + from: filter.from || 0, + size: filter.size || 25, + }), eventsFilter, - ); + ), + this.eventsService.getEventsCount(eventsFilter), + ]); - this.server.to(roomName).emit('eventsUpdate', events); - } catch (error) { - this.logger.error(error); - } + this.server.to(roomName).emit("eventsUpdate", { events, eventsCount }); + } catch (error) { + this.logger.error(error); } } + + async pushEvents(): Promise { + const promises: Promise[] = []; + + for (const [roomName] of this.server.sockets.adapter.rooms) { + promises.push(this.pushEventsForRoom(roomName)); + } + + await Promise.all(promises); + } } diff --git a/src/crons/websocket/pool.gateway.ts b/src/crons/websocket/pool.gateway.ts index feb9db5aa..3e91eab6c 100644 --- a/src/crons/websocket/pool.gateway.ts +++ b/src/crons/websocket/pool.gateway.ts @@ -31,22 +31,42 @@ export class PoolGateway { return { status: 'success' }; } - async pushPool() { - for (const [roomName] of this.server.sockets.adapter.rooms) { - try { - if (!roomName.startsWith("pool-")) continue; + async pushPoolForRoom(roomName: string): Promise { + if (!roomName.startsWith("pool-")) return; + + try { + const filterHash = roomName.replace("pool-", ""); + const filter: PoolSubscribePayload = JSON.parse(filterHash); - const filterHash = roomName.replace("pool-", ""); - const filter: PoolSubscribePayload = JSON.parse(filterHash); + const poolFilter = new PoolFilter({ + type: filter.type, + }); - const pool = await this.poolService.getPool(new QueryPagination({ from: filter.from, size: filter.size }), new PoolFilter({ - type: filter.type, - })); + const [pool, poolCount] = await Promise.all([ + this.poolService.getPool( + new QueryPagination({ + from: filter.from, + size: filter.size, + }), + poolFilter, + ), + this.poolService.getPoolCount(poolFilter), + ]); - this.server.to(roomName).emit('poolUpdate', pool); - } catch (error) { - this.logger.error(error); - } + this.server.to(roomName).emit("poolUpdate", { pool, poolCount }); + } catch (error) { + this.logger.error(error); } } + + async pushPool(): Promise { + const promises: Promise[] = []; + + for (const [roomName] of this.server.sockets.adapter.rooms) { + promises.push(this.pushPoolForRoom(roomName)); + } + + await Promise.all(promises); + } + } diff --git a/src/crons/websocket/transaction.gateway.ts b/src/crons/websocket/transaction.gateway.ts index bca4054d2..20ef6e53a 100644 --- a/src/crons/websocket/transaction.gateway.ts +++ b/src/crons/websocket/transaction.gateway.ts @@ -50,15 +50,16 @@ export class TransactionsGateway { return { status: 'success' }; } - async pushTransactions() { - for (const [roomName] of this.server.sockets.adapter.rooms) { - try { - if (!roomName.startsWith("tx-")) continue; + async pushTransactionsForRoom(roomName: string): Promise { + if (!roomName.startsWith("tx-")) return; - const filterHash = roomName.replace("tx-", ""); - const filter = JSON.parse(filterHash); + try { + const filterHash = roomName.replace("tx-", ""); + const filter = JSON.parse(filterHash); - const options = TransactionQueryOptions.applyDefaultOptions(filter.size || 25, { + const options = TransactionQueryOptions.applyDefaultOptions( + filter.size || 25, + { withScResults: filter.withScResults, withOperations: filter.withOperations, withLogs: filter.withLogs, @@ -66,43 +67,57 @@ export class TransactionsGateway { 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( + }, + ); + + 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 [transactions, transactionsCount] = await Promise.all([ + this.transactionService.getTransactions( transactionFilter, new QueryPagination({ from: filter.from || 0, size: filter.size || 25 }), options, undefined, filter.fields || [], - ); + ), + this.transactionService.getTransactionCount(transactionFilter), + ]); - this.server.to(roomName).emit('transactionUpdate', txs); - } catch (error) { - this.logger.error(error); - } + this.server.to(roomName).emit("transactionUpdate", { transactions, transactionsCount }); + } catch (error) { + this.logger.error(error); } } + + async pushTransactions(): Promise { + const promises: Promise[] = []; + + for (const [roomName] of this.server.sockets.adapter.rooms) { + promises.push(this.pushTransactionsForRoom(roomName)); + } + + await Promise.all(promises); + } + } From bdb82de681f6c399d68a5663ae2b5169d40ff5b7 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 16 Oct 2025 09:52:20 +0300 Subject: [PATCH 30/33] lower ttl for blocks count cache --- src/utils/cache.info.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/cache.info.ts b/src/utils/cache.info.ts index 6ca7e69ac..00059ae49 100644 --- a/src/utils/cache.info.ts +++ b/src/utils/cache.info.ts @@ -469,7 +469,7 @@ export class CacheInfo { static BlocksCount(filter: BlockFilter): CacheInfo { return { key: `blocks:count:${JSON.stringify(filter)}`, - ttl: Constants.oneMinute(), + ttl: Constants.oneSecond() * 6, }; } From 341cbe34a67874651b8951d1e6dc7cf1a4b77fa3 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 16 Oct 2025 09:58:01 +0300 Subject: [PATCH 31/33] remove comments --- src/endpoints/blocks/entities/block.subscribe.ts | 1 - src/endpoints/pool/entities/pool.subscribe.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/endpoints/blocks/entities/block.subscribe.ts b/src/endpoints/blocks/entities/block.subscribe.ts index 19e379e23..e60e9c8dc 100644 --- a/src/endpoints/blocks/entities/block.subscribe.ts +++ b/src/endpoints/blocks/entities/block.subscribe.ts @@ -1,4 +1,3 @@ -// block-subscribe.dto.ts import { IsOptional, IsNumber, IsBoolean, Min, Max, IsEnum, IsIn } from 'class-validator'; import { SortOrder } from 'src/common/entities/sort.order'; diff --git a/src/endpoints/pool/entities/pool.subscribe.ts b/src/endpoints/pool/entities/pool.subscribe.ts index 704e2e954..e9ef35074 100644 --- a/src/endpoints/pool/entities/pool.subscribe.ts +++ b/src/endpoints/pool/entities/pool.subscribe.ts @@ -1,4 +1,3 @@ -// block-subscribe.dto.ts import { IsOptional, IsNumber, Min, Max, IsEnum, IsIn } from 'class-validator'; import { TransactionType } from 'src/endpoints/transactions/entities/transaction.type'; From e0c22950475e3dcb291d38222bec47c2f7236929 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 16 Oct 2025 09:58:30 +0300 Subject: [PATCH 32/33] remove comments --- src/utils/ws-validation.pipe.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/ws-validation.pipe.ts b/src/utils/ws-validation.pipe.ts index 8a5a58010..ddeb25994 100644 --- a/src/utils/ws-validation.pipe.ts +++ b/src/utils/ws-validation.pipe.ts @@ -1,4 +1,3 @@ -// ws-validation.pipe.ts import { Injectable, ValidationPipe, ValidationPipeOptions } from '@nestjs/common'; import { WsException } from '@nestjs/websockets'; From a4dfb219ef5f5a576e1add8b513fe0bbbc1e0174 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 16 Oct 2025 10:03:12 +0300 Subject: [PATCH 33/33] renaming --- src/crons/websocket/blocks.gateway.ts | 8 ++++---- src/crons/websocket/events.gateway.ts | 8 ++++---- src/crons/websocket/pool.gateway.ts | 8 ++++---- src/crons/websocket/transaction.gateway.ts | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/crons/websocket/blocks.gateway.ts b/src/crons/websocket/blocks.gateway.ts index 83e79759b..40cfd5038 100644 --- a/src/crons/websocket/blocks.gateway.ts +++ b/src/crons/websocket/blocks.gateway.ts @@ -24,8 +24,8 @@ export class BlocksGateway { @ConnectedSocket() client: Socket, @MessageBody(new WsValidationPipe()) payload: BlockSubscribePayload ) { - const filterHash = JSON.stringify(payload); - await client.join(`blocks-${filterHash}`); + const filterIdentifier = JSON.stringify(payload); + await client.join(`blocks-${filterIdentifier}`); return { status: 'success' }; } @@ -34,8 +34,8 @@ export class BlocksGateway { if (!roomName.startsWith("blocks-")) return; try { - const filterHash = roomName.replace("blocks-", ""); - const filter: BlockSubscribePayload = JSON.parse(filterHash); + const filterIdentifier = roomName.replace("blocks-", ""); + const filter: BlockSubscribePayload = JSON.parse(filterIdentifier); const blockFilter = new BlockFilter({ shard: filter.shard, diff --git a/src/crons/websocket/events.gateway.ts b/src/crons/websocket/events.gateway.ts index 14c6b510d..c95e982e0 100644 --- a/src/crons/websocket/events.gateway.ts +++ b/src/crons/websocket/events.gateway.ts @@ -24,8 +24,8 @@ export class EventsGateway { @ConnectedSocket() client: Socket, @MessageBody(new WsValidationPipe()) payload: EventsSubscribePayload, ) { - const filterHash = JSON.stringify(payload); - await client.join(`events-${filterHash}`); + const filterIdentifier = JSON.stringify(payload); + await client.join(`events-${filterIdentifier}`); return { status: 'success' }; } @@ -34,8 +34,8 @@ export class EventsGateway { if (!roomName.startsWith("events-")) return; try { - const filterHash = roomName.replace("events-", ""); - const filter: EventsSubscribePayload = JSON.parse(filterHash); + const filterIdentifier = roomName.replace("events-", ""); + const filter: EventsSubscribePayload = JSON.parse(filterIdentifier); const eventsFilter = new EventsFilter({ shard: filter.shard, diff --git a/src/crons/websocket/pool.gateway.ts b/src/crons/websocket/pool.gateway.ts index 3e91eab6c..426e14e10 100644 --- a/src/crons/websocket/pool.gateway.ts +++ b/src/crons/websocket/pool.gateway.ts @@ -25,8 +25,8 @@ export class PoolGateway { @ConnectedSocket() client: Socket, @MessageBody(new WsValidationPipe()) payload: PoolSubscribePayload, ) { - const filterHash = JSON.stringify(payload); - await client.join(`pool-${filterHash}`); + const filterIdentifier = JSON.stringify(payload); + await client.join(`pool-${filterIdentifier}`); return { status: 'success' }; } @@ -35,8 +35,8 @@ export class PoolGateway { if (!roomName.startsWith("pool-")) return; try { - const filterHash = roomName.replace("pool-", ""); - const filter: PoolSubscribePayload = JSON.parse(filterHash); + const filterIdentifier = roomName.replace("pool-", ""); + const filter: PoolSubscribePayload = JSON.parse(filterIdentifier); const poolFilter = new PoolFilter({ type: filter.type, diff --git a/src/crons/websocket/transaction.gateway.ts b/src/crons/websocket/transaction.gateway.ts index 20ef6e53a..85c3a76d1 100644 --- a/src/crons/websocket/transaction.gateway.ts +++ b/src/crons/websocket/transaction.gateway.ts @@ -44,8 +44,8 @@ export class TransactionsGateway { TransactionFilter.validate(transactionFilter, payload.size || 25); - const filterHash = JSON.stringify(payload); - await client.join(`tx-${filterHash}`); + const filterIdentifier = JSON.stringify(payload); + await client.join(`tx-${filterIdentifier}`); return { status: 'success' }; } @@ -54,8 +54,8 @@ export class TransactionsGateway { if (!roomName.startsWith("tx-")) return; try { - const filterHash = roomName.replace("tx-", ""); - const filter = JSON.parse(filterHash); + const filterIdentifier = roomName.replace("tx-", ""); + const filter = JSON.parse(filterIdentifier); const options = TransactionQueryOptions.applyDefaultOptions( filter.size || 25,