From 0560b874dc4c82c9789834d978811a968f7d76db Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 8 Jun 2026 01:32:19 -0400 Subject: [PATCH] Use current round best share for dashboard --- ...AccountingDashboardIndexes1780867200000.ts | 6 +-- .../share-accounting.module.ts | 3 +- .../share-accounting.service.spec.ts | 47 +++++++++++++++++++ .../share-accounting.service.ts | 35 +++++++++----- test/timescale-redis.integration-spec.ts | 2 + 5 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/ORM/_migrations/PoolAccountingDashboardIndexes1780867200000.ts b/src/ORM/_migrations/PoolAccountingDashboardIndexes1780867200000.ts index 1811290..727d0ed 100644 --- a/src/ORM/_migrations/PoolAccountingDashboardIndexes1780867200000.ts +++ b/src/ORM/_migrations/PoolAccountingDashboardIndexes1780867200000.ts @@ -10,13 +10,13 @@ export class PoolAccountingDashboardIndexes1780867200000 implements MigrationInt ON "accepted_share_entity" ("acceptedAt" DESC) `); await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_address_settings_best_difficulty" - ON "address_settings_entity" ("bestDifficulty" DESC) + CREATE INDEX IF NOT EXISTS "IDX_accepted_share_round_best" + ON "accepted_share_entity" ("submissionDifficulty" DESC, "acceptedAt" DESC, "blockHeight") `); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS "IDX_address_settings_best_difficulty"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_accepted_share_round_best"`); await queryRunner.query(`DROP INDEX IF EXISTS "IDX_accepted_share_accepted_at"`); } } diff --git a/src/ORM/share-accounting/share-accounting.module.ts b/src/ORM/share-accounting/share-accounting.module.ts index c752dd4..f2d688d 100644 --- a/src/ORM/share-accounting/share-accounting.module.ts +++ b/src/ORM/share-accounting/share-accounting.module.ts @@ -2,12 +2,11 @@ import { Global, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AcceptedShareEntity } from '../accepted-share/accepted-share.entity'; -import { AddressSettingsEntity } from '../address-settings/address-settings.entity'; import { ShareAccountingService } from './share-accounting.service'; @Global() @Module({ - imports: [TypeOrmModule.forFeature([AcceptedShareEntity, AddressSettingsEntity])], + imports: [TypeOrmModule.forFeature([AcceptedShareEntity])], providers: [ShareAccountingService], exports: [TypeOrmModule, ShareAccountingService], }) diff --git a/src/ORM/share-accounting/share-accounting.service.spec.ts b/src/ORM/share-accounting/share-accounting.service.spec.ts index 67331a8..f5d65cd 100644 --- a/src/ORM/share-accounting/share-accounting.service.spec.ts +++ b/src/ORM/share-accounting/share-accounting.service.spec.ts @@ -152,6 +152,7 @@ describe('ShareAccountingService', () => { hashRateLast10Minutes: 458129844.9, hashRateLastHour: 114532461.2, bestSubmissionDifficulty: 0, + bestSubmissionDifficultyAt: null, blockCandidateCount: 0, latestShareAt: '2026-06-07T12:10:00.000Z', protocolBreakdown: [], @@ -164,6 +165,52 @@ describe('ShareAccountingService', () => { expect(repository.query).toHaveBeenCalledTimes(1); }); + it('should overlay live pool data and best share from the current round', async () => { + const redis = { + setJsonCache: jest.fn().mockResolvedValue(undefined), + }; + const repository = { + query: jest.fn() + .mockResolvedValueOnce([{ + totalAcceptedShares: '3', + totalCreditedDifficulty: '96', + acceptedSharesLast10Minutes: '0', + creditedDifficultyLast10Minutes: '0', + acceptedSharesLastHour: '3', + creditedDifficultyLastHour: '96', + acceptedSharesLastDay: '3', + creditedDifficultyLastDay: '96', + hashRateLast10Minutes: '0', + hashRateLastHour: '114532461.2', + latestShareAt: new Date('2026-06-07T12:10:00Z'), + }]) + .mockResolvedValueOnce([{ + acceptedSharesLast10Minutes: '7', + creditedDifficultyLast10Minutes: '224', + hashRateLast10Minutes: '1603451170.77', + latestShareAt: new Date('2026-06-07T12:20:00Z'), + }]) + .mockResolvedValueOnce([{ + bestSubmissionDifficulty: '4096', + bestSubmissionDifficultyAt: new Date('2026-06-07T12:19:00Z'), + }]), + }; + const service = new ShareAccountingService(repository as any, redis as any); + + await expect(service.refreshPoolSummary()).resolves.toEqual(expect.objectContaining({ + acceptedSharesLast10Minutes: 7, + creditedDifficultyLast10Minutes: 224, + hashRateLast10Minutes: 1603451170.77, + bestSubmissionDifficulty: 4096, + bestSubmissionDifficultyAt: '2026-06-07T12:19:00.000Z', + latestShareAt: '2026-06-07T12:20:00.000Z', + })); + expect(repository.query).toHaveBeenNthCalledWith( + 3, + expect.stringContaining('WHERE "blockHeight" > latest_found_block."height"'), + ); + }); + it('should cache accounting summaries briefly to protect hot dashboard endpoints', async () => { process.env.SHARE_ACCOUNTING_SUMMARY_CACHE_MS = '1000'; const repository = { diff --git a/src/ORM/share-accounting/share-accounting.service.ts b/src/ORM/share-accounting/share-accounting.service.ts index 6389fe3..07a0a9b 100644 --- a/src/ORM/share-accounting/share-accounting.service.ts +++ b/src/ORM/share-accounting/share-accounting.service.ts @@ -1,9 +1,8 @@ -import { Injectable, OnModuleDestroy, Optional } from '@nestjs/common'; +import { Injectable, OnModuleDestroy } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { AcceptedShareEntity } from '../accepted-share/accepted-share.entity'; -import { AddressSettingsEntity } from '../address-settings/address-settings.entity'; import { RedisMessagingService } from '../../services/redis-messaging.service'; import { timeAsync } from '../../utils/timing.utils'; @@ -40,6 +39,7 @@ export interface ShareAccountingSummary { hashRateLast10Minutes: number; hashRateLastHour: number; bestSubmissionDifficulty: number; + bestSubmissionDifficultyAt: string | null; blockCandidateCount: number; latestShareAt: string | null; protocolBreakdown: { @@ -86,9 +86,6 @@ export class ShareAccountingService implements OnModuleDestroy { constructor( @InjectRepository(AcceptedShareEntity) private readonly acceptedShareRepository: Repository, - @Optional() - @InjectRepository(AddressSettingsEntity) - private readonly addressSettingsRepository?: Repository, private readonly redisMessagingService?: RedisMessagingService, ) { } @@ -175,15 +172,22 @@ export class ShareAccountingService implements OnModuleDestroy { COALESCE(SUM("creditedDifficulty"), 0)::float AS "creditedDifficultyLast10Minutes", COALESCE((SUM("creditedDifficulty") * ${HASHES_PER_DIFFICULTY}) / 600, 0)::float AS "hashRateLast10Minutes", MAX("acceptedAt") AS "latestShareAt" - FROM "accepted_share_entity" - WHERE "acceptedAt" > NOW() - INTERVAL '10 minutes' - `)), - this.addressSettingsRepository == null - ? Promise.resolve([{ bestSubmissionDifficulty: 0 }]) - : timeAsync('share accounting best submitted share query', () => this.addressSettingsRepository.query(` - SELECT COALESCE(MAX("bestDifficulty"), 0)::float AS "bestSubmissionDifficulty" - FROM "address_settings_entity" + FROM "accepted_share_entity" + WHERE "acceptedAt" > NOW() - INTERVAL '10 minutes' `)), + timeAsync('share accounting current round best share query', () => this.acceptedShareRepository.query(` + WITH latest_found_block AS ( + SELECT COALESCE(MAX("height"), 0) AS "height" + FROM "blocks_entity" + ) + SELECT + COALESCE("submissionDifficulty", 0)::float AS "bestSubmissionDifficulty", + "acceptedAt" AS "bestSubmissionDifficultyAt" + FROM "accepted_share_entity", latest_found_block + WHERE "blockHeight" > latest_found_block."height" + ORDER BY "submissionDifficulty" DESC, "acceptedAt" DESC + LIMIT 1 + `)), ]); return { @@ -192,6 +196,9 @@ export class ShareAccountingService implements OnModuleDestroy { creditedDifficultyLast10Minutes: this.toNumber(liveWindow?.creditedDifficultyLast10Minutes), hashRateLast10Minutes: this.toNumber(liveWindow?.hashRateLast10Minutes), bestSubmissionDifficulty: this.toNumber(bestDifficultyRow?.bestSubmissionDifficulty), + bestSubmissionDifficultyAt: bestDifficultyRow?.bestSubmissionDifficultyAt == null + ? null + : new Date(bestDifficultyRow.bestSubmissionDifficultyAt).toISOString(), latestShareAt: liveWindow?.latestShareAt == null ? summary.latestShareAt : new Date(liveWindow.latestShareAt).toISOString(), @@ -211,6 +218,7 @@ export class ShareAccountingService implements OnModuleDestroy { hashRateLast10Minutes: 0, hashRateLastHour: 0, bestSubmissionDifficulty: 0, + bestSubmissionDifficultyAt: null, blockCandidateCount: 0, latestShareAt: null, protocolBreakdown: [], @@ -320,6 +328,7 @@ export class ShareAccountingService implements OnModuleDestroy { hashRateLast10Minutes: this.toNumber(summary?.hashRateLast10Minutes), hashRateLastHour: this.toNumber(summary?.hashRateLastHour), bestSubmissionDifficulty: 0, + bestSubmissionDifficultyAt: null, blockCandidateCount: 0, latestShareAt: summary?.latestShareAt == null ? null diff --git a/test/timescale-redis.integration-spec.ts b/test/timescale-redis.integration-spec.ts index f97fd8e..4cf70a0 100644 --- a/test/timescale-redis.integration-spec.ts +++ b/test/timescale-redis.integration-spec.ts @@ -45,6 +45,8 @@ describe('TimescaleDB and Redis integration', () => { await dataSource.query(`DELETE FROM accepted_share_entity`); await dataSource.query(`DELETE FROM client_entity`); await dataSource.query(`REFRESH MATERIALIZED VIEW user_agent_report_view`); + await redisMessagingService.clearClientPresence(); + await (redisMessagingService as any).publisher.del('json-cache:presence:user-agent-report'); }); it('should create Timescale extension, hypertable, continuous aggregates, and operational policies', async () => {