mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
Use current round best share for dashboard
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user