From 3cb4cbfbdf8c0a5ddca1a556a32ea150c8ec99c6 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 3 Aug 2026 10:27:03 -0400 Subject: [PATCH] Isolate API worker module --- src/api.module.ts | 62 +++++++++++++++++++++++++++ src/app.controller.spec.ts | 4 +- src/app.controller.ts | 6 +-- src/app.module.ts | 2 + src/main.ts | 6 ++- src/services/sv2-authority.service.ts | 37 ++++++++++++++++ 6 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 src/api.module.ts create mode 100644 src/services/sv2-authority.service.ts diff --git a/src/api.module.ts b/src/api.module.ts new file mode 100644 index 0000000..b48874d --- /dev/null +++ b/src/api.module.ts @@ -0,0 +1,62 @@ +import { CacheModule } from '@nestjs/cache-manager'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AppController } from './app.controller'; +import { AddressController } from './controllers/address/address.controller'; +import { ClientController } from './controllers/client/client.controller'; +import { UserAgentReportModule } from './ORM/_views/user-agent-report/user-agent-report.module'; +import { AddressSettingsModule } from './ORM/address-settings/address-settings.module'; +import { BlocksModule } from './ORM/blocks/blocks.module'; +import { ClientStatisticsModule } from './ORM/client-statistics/client-statistics.module'; +import { ClientModule } from './ORM/client/client.module'; +import { PayoutSnapshotModule } from './ORM/payout-snapshot/payout-snapshot.module'; +import { RpcBlocksModule } from './ORM/rpc-block/rpc-block.module'; +import { ShareAccountingModule } from './ORM/share-accounting/share-accounting.module'; +import { createDatabaseOptions } from './database.config'; +import { BitcoinAddressValidator } from './models/validators/bitcoin-address.validator'; +import { BitcoinRpcService } from './services/bitcoin-rpc.service'; +import { RedisMessagingModule } from './services/redis-messaging.module'; +import { Sv2AuthorityService } from './services/sv2-authority.service'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forRootAsync({ + useFactory: (configService: ConfigService) => createDatabaseOptions({ + ...process.env, + DB_HOST: configService.get('DB_HOST'), + DB_PORT: configService.get('DB_PORT'), + DB_USERNAME: configService.get('DB_USERNAME'), + DB_PASSWORD: configService.get('DB_PASSWORD'), + DB_DATABASE: configService.get('DB_DATABASE'), + DB_LOGGING: configService.get('DB_LOGGING'), + DB_POOL_SIZE: configService.get('DB_POOL_SIZE'), + }), + imports: [ConfigModule], + inject: [ConfigService], + }), + CacheModule.register(), + RedisMessagingModule, + ClientStatisticsModule, + ClientModule, + AddressSettingsModule, + BlocksModule, + RpcBlocksModule, + UserAgentReportModule, + ShareAccountingModule, + PayoutSnapshotModule, + ], + controllers: [ + AppController, + ClientController, + AddressController, + ], + providers: [ + BitcoinRpcService, + BitcoinAddressValidator, + Sv2AuthorityService, + ], +}) +export class ApiModule {} diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index 55b5af9..f9e55dc 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -6,7 +6,7 @@ describe('AppController', () => { blocksService?: any; addressSettingsService?: any; userAgentReportService?: any; - stratumV2Service?: any; + sv2AuthorityService?: any; redisMessagingService?: any; } = {}) => { return new AppController( @@ -26,7 +26,7 @@ describe('AppController', () => { overrides.userAgentReportService ?? { getReport: jest.fn().mockResolvedValue([]), }, - overrides.stratumV2Service ?? { + overrides.sv2AuthorityService ?? { getPoolAuthorityPublicKey: jest.fn().mockResolvedValue({ publicKey: '', configured: false, diff --git a/src/app.controller.ts b/src/app.controller.ts index 42c8a56..19d3499 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -10,9 +10,9 @@ import { ClientStatisticsService } from './ORM/client-statistics/client-statisti import { ClientService } from './ORM/client/client.service'; import { BitcoinRpcService } from './services/bitcoin-rpc.service'; import { UserAgentReportView } from './ORM/_views/user-agent-report/user-agent-report.view'; -import { StratumV2Service } from './services/stratum-v2.service'; import { ShareAccountingService } from './ORM/share-accounting/share-accounting.service'; import { RedisMessagingService } from './services/redis-messaging.service'; +import { Sv2AuthorityService } from './services/sv2-authority.service'; import { normalizePayoutMode } from './types/payout-mode'; @Controller() @@ -30,7 +30,7 @@ export class AppController { private readonly bitcoinRpcService: BitcoinRpcService, private readonly addressSettingsService: AddressSettingsService, private readonly userAgentReportService: UserAgentReportService, - private readonly stratumV2Service: StratumV2Service, + private readonly sv2AuthorityService: Sv2AuthorityService, private readonly shareAccountingService: ShareAccountingService, private readonly redisMessagingService: RedisMessagingService ) { } @@ -78,7 +78,7 @@ export class AppController { const [blockData, highScores, poolAuthority, userAgentReport] = await Promise.all([ withInfoTimeout('found blocks', this.blocksService.getFoundBlocks(), staleInfo?.blockData ?? []), withInfoTimeout('high scores', this.addressSettingsService.getHighScores(), staleInfo?.highScores ?? []), - withInfoTimeout('SV2 authority', this.stratumV2Service.getPoolAuthorityPublicKey(), { + withInfoTimeout('SV2 authority', this.sv2AuthorityService.getPoolAuthorityPublicKey(), { publicKey: staleInfo?.sv2?.poolAuthorityPublicKey ?? '', configured: staleInfo?.sv2?.authorityKeyConfigured ?? false }), diff --git a/src/app.module.ts b/src/app.module.ts index c65fbf7..ae2ffcb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -33,6 +33,7 @@ import { StratumV1Service } from './services/stratum-v1.service'; import { Sv2JobDeclarationRegistryService } from './services/sv2-job-declaration-registry.service'; import { Sv2JobDeclarationService } from './services/sv2-job-declaration.service'; import { Sv2TemplateDistributionService } from './services/sv2-template-distribution.service'; +import { Sv2AuthorityService } from './services/sv2-authority.service'; import { StratumV2Service } from './services/stratum-v2.service'; import { TelegramService } from './services/telegram.service'; import { TemplateProviderService } from './services/template-provider.service'; @@ -94,6 +95,7 @@ const ORMModules = [ Sv2JobDeclarationRegistryService, Sv2JobDeclarationService, Sv2TemplateDistributionService, + Sv2AuthorityService, CustomWorkService, DatumService, BTCPayService, diff --git a/src/main.ts b/src/main.ts index 048749c..522320b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { readFileSync, watch } from 'fs'; import * as path from 'path'; import * as ecc from 'tiny-secp256k1'; +import { ApiModule } from './api.module'; import { AppModule } from './app.module'; async function bootstrap() { @@ -32,7 +33,8 @@ async function bootstrap() { }; } - const app = await NestFactory.create(AppModule, new FastifyAdapter(options)); + const appModule = process.env.API_ONLY === 'true' ? ApiModule : AppModule; + const app = await NestFactory.create(appModule, new FastifyAdapter(options)); app.setGlobalPrefix('api'); app.useGlobalPipes( new ValidationPipe({ @@ -54,7 +56,7 @@ async function bootstrap() { }); app.enableCors(); - useContainer(app.select(AppModule), { fallbackOnErrors: true }); + useContainer(app.select(appModule), { fallbackOnErrors: true }); // Taproot bitcoinjs.initEccLib(ecc); diff --git a/src/services/sv2-authority.service.ts b/src/services/sv2-authority.service.ts new file mode 100644 index 0000000..8809b50 --- /dev/null +++ b/src/services/sv2-authority.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as crypto from 'crypto'; + +import { encodeSv2AuthorityPublicKey } from '../models/sv2/sv2-authority-key'; +import { xOnlyPubKeyFromPriv } from '../models/sv2/sv2-noise'; + +@Injectable() +export class Sv2AuthorityService { + private authorityPublicKey: string | null = null; + private authorityKeyConfigured = false; + + constructor( + private readonly configService: ConfigService, + ) {} + + public async getPoolAuthorityPublicKey(): Promise<{ publicKey: string; configured: boolean }> { + if (this.authorityPublicKey != null) { + return { + publicKey: this.authorityPublicKey, + configured: this.authorityKeyConfigured, + }; + } + + const configuredAuthorityKey = this.configService.get('SV2_AUTHORITY_PRIVKEY'); + this.authorityKeyConfigured = configuredAuthorityKey?.length === 64; + const authorityPrivKey = configuredAuthorityKey?.length === 64 + ? Buffer.from(configuredAuthorityKey, 'hex') + : crypto.randomBytes(32); + this.authorityPublicKey = encodeSv2AuthorityPublicKey(xOnlyPubKeyFromPriv(authorityPrivKey)); + + return { + publicKey: this.authorityPublicKey, + configured: this.authorityKeyConfigured, + }; + } +}