diff --git a/config/dev.config.yaml b/config/dev.config.yaml index ccc87f8..a8a43de 100644 --- a/config/dev.config.yaml +++ b/config/dev.config.yaml @@ -17,4 +17,3 @@ bitcoinCore: rpcPass: polarpass rpcUser: polaruser rpcPort: 18445 - diff --git a/package-lock.json b/package-lock.json index 47adfb3..c0b2e37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@nestjs/common": "^10.4.1", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.4.1", + "@nestjs/event-emitter": "^2.0.4", "@nestjs/microservices": "^10.4.1", "@nestjs/passport": "^10.0.0", "@nestjs/platform-express": "^10.3.7", @@ -31,6 +32,7 @@ }, "devDependencies": { "@nestjs/cli": "^10.4.5", + "@nestjs/platform-ws": "^10.4.4", "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^10.3.7", "@types/express": "^4.17.13", @@ -39,6 +41,7 @@ "@types/node": "18.15.11", "@types/secp256k1": "^4.0.6", "@types/supertest": "^2.0.11", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "class-transformer": "^0.5.1", @@ -1892,6 +1895,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, + "node_modules/@nestjs/event-emitter": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz", + "integrity": "sha512-quMiw8yOwoSul0pp3mOonGz8EyXWHSBTqBy8B0TbYYgpnG1Ix2wGUnuTksLWaaBiiOTDhciaZ41Y5fJZsSJE1Q==", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", @@ -2025,6 +2040,52 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/@nestjs/platform-ws": { + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/platform-ws/-/platform-ws-10.4.4.tgz", + "integrity": "sha512-6E476YvfO14uQUT6FzWFpGnwp58fpGq2aeGxHFdRMMptOQ5suKqD+3LsZgPiGF1elgVhQcI5uqM15qL3yH+OqQ==", + "dev": true, + "dependencies": { + "tslib": "2.7.0", + "ws": "8.18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/platform-ws/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, + "node_modules/@nestjs/platform-ws/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "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/@nestjs/schedule": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-3.0.0.tgz", @@ -2626,6 +2687,15 @@ "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==", "devOptional": true }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -5529,6 +5599,11 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", diff --git a/package.json b/package.json index 0af3aa6..f6d79f7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@nestjs/common": "^10.4.1", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.4.1", + "@nestjs/event-emitter": "^2.0.4", "@nestjs/microservices": "^10.4.1", "@nestjs/passport": "^10.0.0", "@nestjs/platform-express": "^10.3.7", @@ -36,6 +37,7 @@ "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.7", + "@nestjs/platform-ws": "^10.4.4", "axios": "^1.7.2", "currency.js": "^2.0.4", "js-yaml": "^4.1.0", @@ -55,6 +57,7 @@ "@types/node": "18.15.11", "@types/secp256k1": "^4.0.6", "@types/supertest": "^2.0.11", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "class-transformer": "^0.5.1", diff --git a/src/app.module.ts b/src/app.module.ts index 623fc86..74f21e1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,10 +9,12 @@ import { SilentBlocksModule } from '@/silent-blocks/silent-blocks.module'; import { OperationStateModule } from '@/operation-state/operation-state.module'; import { ScheduleModule } from '@nestjs/schedule'; import { BlockProviderModule } from '@/block-data-providers/block-provider.module'; +import { EventEmitterModule } from '@nestjs/event-emitter'; @Module({ imports: [ ScheduleModule.forRoot(), + EventEmitterModule.forRoot(), ConfigModule.forRoot({ ignoreEnvFile: true, load: [configuration], diff --git a/src/block-data-providers/bitcoin-core/provider.ts b/src/block-data-providers/bitcoin-core/provider.ts index 3ca101e..e098965 100644 --- a/src/block-data-providers/bitcoin-core/provider.ts +++ b/src/block-data-providers/bitcoin-core/provider.ts @@ -28,6 +28,8 @@ import { import { AxiosRequestConfig } from 'axios'; import * as currency from 'currency.js'; import { AxiosRetryConfig, makeRequest } from '@/common/request'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { INDEXED_BLOCK_EVENT } from '@/common/events'; @Injectable() export class BitcoinCoreProvider @@ -44,6 +46,7 @@ export class BitcoinCoreProvider private readonly configService: ConfigService, indexerService: IndexerService, operationStateService: OperationStateService, + private eventEmitter: EventEmitter2, ) { super(indexerService, operationStateService); @@ -123,6 +126,8 @@ export class BitcoinCoreProvider state.indexedBlockHeight = height; await this.setState(state); + + this.eventEmitter.emit(INDEXED_BLOCK_EVENT, height); } } finally { this.isSyncing = false; diff --git a/src/block-data-providers/block-provider.module.ts b/src/block-data-providers/block-provider.module.ts index 24142f5..5898a9b 100644 --- a/src/block-data-providers/block-provider.module.ts +++ b/src/block-data-providers/block-provider.module.ts @@ -7,18 +7,30 @@ import { IndexerService } from '@/indexer/indexer.service'; import { ProviderType } from '@/common/enum'; import { BitcoinCoreProvider } from '@/block-data-providers/bitcoin-core/provider'; import { EsploraProvider } from '@/block-data-providers/esplora/provider'; +import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; @Module({ - imports: [OperationStateModule, IndexerModule, ConfigModule], + imports: [ + OperationStateModule, + IndexerModule, + ConfigModule, + EventEmitterModule, + ], controllers: [], providers: [ { provide: 'BlockDataProvider', - inject: [ConfigService, IndexerService, OperationStateService], + inject: [ + ConfigService, + IndexerService, + OperationStateService, + EventEmitter2, + ], useFactory: ( configService: ConfigService, indexerService: IndexerService, operationStateService: OperationStateService, + eventEmitter: EventEmitter2, ) => { switch (configService.get('providerType')) { case ProviderType.ESPLORA: @@ -26,12 +38,14 @@ import { EsploraProvider } from '@/block-data-providers/esplora/provider'; configService, indexerService, operationStateService, + eventEmitter, ); case ProviderType.BITCOIN_CORE_RPC: return new BitcoinCoreProvider( configService, indexerService, operationStateService, + eventEmitter, ); default: throw Error('unrecognised provider type in config'); diff --git a/src/block-data-providers/esplora/provider.ts b/src/block-data-providers/esplora/provider.ts index a27021d..9919de1 100644 --- a/src/block-data-providers/esplora/provider.ts +++ b/src/block-data-providers/esplora/provider.ts @@ -12,6 +12,7 @@ import { } from '@/block-data-providers/esplora/interface'; import { TAPROOT_ACTIVATION_HEIGHT } from '@/common/constants'; import { Cron, CronExpression } from '@nestjs/schedule'; +import { EventEmitter2 } from '@nestjs/event-emitter'; @Injectable() export class EsploraProvider @@ -29,6 +30,8 @@ export class EsploraProvider private readonly configService: ConfigService, indexerService: IndexerService, operationStateService: OperationStateService, + private eventEmitter: EventEmitter2, + ) { super(indexerService, operationStateService); diff --git a/src/common/common.ts b/src/common/common.ts index ac8a415..2373694 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -147,3 +147,7 @@ export const varIntSize = (value: number): number => { else if (value <= 0xffffffff) return 5; else return 9; }; + +export const delay = (ms: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; diff --git a/src/common/events.ts b/src/common/events.ts new file mode 100644 index 0000000..1b78d98 --- /dev/null +++ b/src/common/events.ts @@ -0,0 +1 @@ +export const INDEXED_BLOCK_EVENT = 'INDEXED_BLOCK_EVENT'; diff --git a/src/main.ts b/src/main.ts index 874a2ad..7dfc7ff 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,13 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from '@/app.module'; import { ConfigService } from '@nestjs/config'; +import { WsAdapter } from '@nestjs/platform-ws'; declare const module: any; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useWebSocketAdapter(new WsAdapter(app)); const configService = app.get(ConfigService); const port = configService.get('app.port'); diff --git a/src/silent-blocks/silent-blocks.gateway.ts b/src/silent-blocks/silent-blocks.gateway.ts new file mode 100644 index 0000000..4c6210a --- /dev/null +++ b/src/silent-blocks/silent-blocks.gateway.ts @@ -0,0 +1,37 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { + WebSocketGateway, + WebSocketServer, + OnGatewayConnection, + OnGatewayDisconnect, +} from '@nestjs/websockets'; +import { Server, WebSocket } from 'ws'; + +@Injectable() +@WebSocketGateway() +export class SilentBlocksGateway + implements OnGatewayConnection, OnGatewayDisconnect +{ + private readonly logger = new Logger(SilentBlocksGateway.name); + + @WebSocketServer() server: Server; + + handleConnection(client: WebSocket) { + const remoteAddress = (client as any)._socket.remoteAddress; + this.logger.debug(`Client connected: ${remoteAddress}`); + } + + handleDisconnect(client: WebSocket) { + const remoteAddress = (client as any)._socket.remoteAddress; + this.logger.debug(`Client disconnected: ${remoteAddress}`); + } + + // Method to broadcast silent block to all connected clients + broadcastSilentBlock(silentBlock: Buffer) { + this.server.clients.forEach((client: WebSocket) => { + if (client.readyState === WebSocket.OPEN) { + client.send(silentBlock.toString('hex')); // Send silent block as hex string + } + }); + } +} diff --git a/src/silent-blocks/silent-blocks.module.ts b/src/silent-blocks/silent-blocks.module.ts index 3e9d263..9127094 100644 --- a/src/silent-blocks/silent-blocks.module.ts +++ b/src/silent-blocks/silent-blocks.module.ts @@ -4,10 +4,11 @@ import { Transaction } from '@/transactions/transaction.entity'; import { TransactionsService } from '@/transactions/transactions.service'; import { SilentBlocksController } from '@/silent-blocks/silent-blocks.controller'; import { SilentBlocksService } from '@/silent-blocks/silent-blocks.service'; +import { SilentBlocksGateway } from '@/silent-blocks/silent-blocks.gateway'; @Module({ imports: [TypeOrmModule.forFeature([Transaction])], - providers: [TransactionsService, SilentBlocksService], + providers: [TransactionsService, SilentBlocksService, SilentBlocksGateway], controllers: [SilentBlocksController], exports: [SilentBlocksService], }) diff --git a/src/silent-blocks/silent-blocks.service.ts b/src/silent-blocks/silent-blocks.service.ts index 1da1d7c..779e9cc 100644 --- a/src/silent-blocks/silent-blocks.service.ts +++ b/src/silent-blocks/silent-blocks.service.ts @@ -1,12 +1,28 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { Transaction } from '@/transactions/transaction.entity'; import { TransactionsService } from '@/transactions/transactions.service'; import { SILENT_PAYMENT_BLOCK_TYPE } from '@/common/constants'; -import { encodeVarInt, varIntSize } from '@/common/common'; +import { encodeVarInt, varIntSize, delay } from '@/common/common'; +import { SilentBlocksGateway } from '@/silent-blocks/silent-blocks.gateway'; +import { OnEvent } from '@nestjs/event-emitter'; +import { INDEXED_BLOCK_EVENT } from '@/common/events'; @Injectable() export class SilentBlocksService { - constructor(private readonly transactionsService: TransactionsService) {} + private readonly logger = new Logger(SilentBlocksService.name); + + constructor( + private readonly transactionsService: TransactionsService, + private readonly silentBlocksGateway: SilentBlocksGateway, + ) {} + + @OnEvent(INDEXED_BLOCK_EVENT) + async handleBlockIndexedEvent(blockHeight: number) { + this.logger.debug(`New block indexed: ${blockHeight}`); + await delay(1000); + const silentBlock = await this.getSilentBlockByHeight(blockHeight); + this.silentBlocksGateway.broadcastSilentBlock(silentBlock); + } private getSilentBlockLength(transactions: Transaction[]): number { let length = 1 + varIntSize(transactions.length); // 1 byte for type + varint for transactions count