Use current round best share for dashboard

This commit is contained in:
Ben
2026-06-08 01:32:19 -04:00
parent 7f6f42c5c9
commit 0560b874dc
5 changed files with 75 additions and 18 deletions
@@ -10,13 +10,13 @@ export class PoolAccountingDashboardIndexes1780867200000 implements MigrationInt
ON "accepted_share_entity" ("acceptedAt" DESC) ON "accepted_share_entity" ("acceptedAt" DESC)
`); `);
await queryRunner.query(` await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_address_settings_best_difficulty" CREATE INDEX IF NOT EXISTS "IDX_accepted_share_round_best"
ON "address_settings_entity" ("bestDifficulty" DESC) ON "accepted_share_entity" ("submissionDifficulty" DESC, "acceptedAt" DESC, "blockHeight")
`); `);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
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"`); await queryRunner.query(`DROP INDEX IF EXISTS "IDX_accepted_share_accepted_at"`);
} }
} }
@@ -2,12 +2,11 @@ import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AcceptedShareEntity } from '../accepted-share/accepted-share.entity'; import { AcceptedShareEntity } from '../accepted-share/accepted-share.entity';
import { AddressSettingsEntity } from '../address-settings/address-settings.entity';
import { ShareAccountingService } from './share-accounting.service'; import { ShareAccountingService } from './share-accounting.service';
@Global() @Global()
@Module({ @Module({
imports: [TypeOrmModule.forFeature([AcceptedShareEntity, AddressSettingsEntity])], imports: [TypeOrmModule.forFeature([AcceptedShareEntity])],
providers: [ShareAccountingService], providers: [ShareAccountingService],
exports: [TypeOrmModule, ShareAccountingService], exports: [TypeOrmModule, ShareAccountingService],
}) })
@@ -152,6 +152,7 @@ describe('ShareAccountingService', () => {
hashRateLast10Minutes: 458129844.9, hashRateLast10Minutes: 458129844.9,
hashRateLastHour: 114532461.2, hashRateLastHour: 114532461.2,
bestSubmissionDifficulty: 0, bestSubmissionDifficulty: 0,
bestSubmissionDifficultyAt: null,
blockCandidateCount: 0, blockCandidateCount: 0,
latestShareAt: '2026-06-07T12:10:00.000Z', latestShareAt: '2026-06-07T12:10:00.000Z',
protocolBreakdown: [], protocolBreakdown: [],
@@ -164,6 +165,52 @@ describe('ShareAccountingService', () => {
expect(repository.query).toHaveBeenCalledTimes(1); 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 () => { it('should cache accounting summaries briefly to protect hot dashboard endpoints', async () => {
process.env.SHARE_ACCOUNTING_SUMMARY_CACHE_MS = '1000'; process.env.SHARE_ACCOUNTING_SUMMARY_CACHE_MS = '1000';
const repository = { const repository = {
@@ -1,9 +1,8 @@
import { Injectable, OnModuleDestroy, Optional } from '@nestjs/common'; import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { AcceptedShareEntity } from '../accepted-share/accepted-share.entity'; import { AcceptedShareEntity } from '../accepted-share/accepted-share.entity';
import { AddressSettingsEntity } from '../address-settings/address-settings.entity';
import { RedisMessagingService } from '../../services/redis-messaging.service'; import { RedisMessagingService } from '../../services/redis-messaging.service';
import { timeAsync } from '../../utils/timing.utils'; import { timeAsync } from '../../utils/timing.utils';
@@ -40,6 +39,7 @@ export interface ShareAccountingSummary {
hashRateLast10Minutes: number; hashRateLast10Minutes: number;
hashRateLastHour: number; hashRateLastHour: number;
bestSubmissionDifficulty: number; bestSubmissionDifficulty: number;
bestSubmissionDifficultyAt: string | null;
blockCandidateCount: number; blockCandidateCount: number;
latestShareAt: string | null; latestShareAt: string | null;
protocolBreakdown: { protocolBreakdown: {
@@ -86,9 +86,6 @@ export class ShareAccountingService implements OnModuleDestroy {
constructor( constructor(
@InjectRepository(AcceptedShareEntity) @InjectRepository(AcceptedShareEntity)
private readonly acceptedShareRepository: Repository<AcceptedShareEntity>, private readonly acceptedShareRepository: Repository<AcceptedShareEntity>,
@Optional()
@InjectRepository(AddressSettingsEntity)
private readonly addressSettingsRepository?: Repository<AddressSettingsEntity>,
private readonly redisMessagingService?: RedisMessagingService, private readonly redisMessagingService?: RedisMessagingService,
) { } ) { }
@@ -178,11 +175,18 @@ export class ShareAccountingService implements OnModuleDestroy {
FROM "accepted_share_entity" FROM "accepted_share_entity"
WHERE "acceptedAt" > NOW() - INTERVAL '10 minutes' WHERE "acceptedAt" > NOW() - INTERVAL '10 minutes'
`)), `)),
this.addressSettingsRepository == null timeAsync('share accounting current round best share query', () => this.acceptedShareRepository.query(`
? Promise.resolve([{ bestSubmissionDifficulty: 0 }]) WITH latest_found_block AS (
: timeAsync('share accounting best submitted share query', () => this.addressSettingsRepository.query(` SELECT COALESCE(MAX("height"), 0) AS "height"
SELECT COALESCE(MAX("bestDifficulty"), 0)::float AS "bestSubmissionDifficulty" FROM "blocks_entity"
FROM "address_settings_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
`)), `)),
]); ]);
@@ -192,6 +196,9 @@ export class ShareAccountingService implements OnModuleDestroy {
creditedDifficultyLast10Minutes: this.toNumber(liveWindow?.creditedDifficultyLast10Minutes), creditedDifficultyLast10Minutes: this.toNumber(liveWindow?.creditedDifficultyLast10Minutes),
hashRateLast10Minutes: this.toNumber(liveWindow?.hashRateLast10Minutes), hashRateLast10Minutes: this.toNumber(liveWindow?.hashRateLast10Minutes),
bestSubmissionDifficulty: this.toNumber(bestDifficultyRow?.bestSubmissionDifficulty), bestSubmissionDifficulty: this.toNumber(bestDifficultyRow?.bestSubmissionDifficulty),
bestSubmissionDifficultyAt: bestDifficultyRow?.bestSubmissionDifficultyAt == null
? null
: new Date(bestDifficultyRow.bestSubmissionDifficultyAt).toISOString(),
latestShareAt: liveWindow?.latestShareAt == null latestShareAt: liveWindow?.latestShareAt == null
? summary.latestShareAt ? summary.latestShareAt
: new Date(liveWindow.latestShareAt).toISOString(), : new Date(liveWindow.latestShareAt).toISOString(),
@@ -211,6 +218,7 @@ export class ShareAccountingService implements OnModuleDestroy {
hashRateLast10Minutes: 0, hashRateLast10Minutes: 0,
hashRateLastHour: 0, hashRateLastHour: 0,
bestSubmissionDifficulty: 0, bestSubmissionDifficulty: 0,
bestSubmissionDifficultyAt: null,
blockCandidateCount: 0, blockCandidateCount: 0,
latestShareAt: null, latestShareAt: null,
protocolBreakdown: [], protocolBreakdown: [],
@@ -320,6 +328,7 @@ export class ShareAccountingService implements OnModuleDestroy {
hashRateLast10Minutes: this.toNumber(summary?.hashRateLast10Minutes), hashRateLast10Minutes: this.toNumber(summary?.hashRateLast10Minutes),
hashRateLastHour: this.toNumber(summary?.hashRateLastHour), hashRateLastHour: this.toNumber(summary?.hashRateLastHour),
bestSubmissionDifficulty: 0, bestSubmissionDifficulty: 0,
bestSubmissionDifficultyAt: null,
blockCandidateCount: 0, blockCandidateCount: 0,
latestShareAt: summary?.latestShareAt == null latestShareAt: summary?.latestShareAt == null
? null ? null
+2
View File
@@ -45,6 +45,8 @@ describe('TimescaleDB and Redis integration', () => {
await dataSource.query(`DELETE FROM accepted_share_entity`); await dataSource.query(`DELETE FROM accepted_share_entity`);
await dataSource.query(`DELETE FROM client_entity`); await dataSource.query(`DELETE FROM client_entity`);
await dataSource.query(`REFRESH MATERIALIZED VIEW user_agent_report_view`); 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 () => { it('should create Timescale extension, hypertable, continuous aggregates, and operational policies', async () => {