Compare commits

2 Commits
Author SHA1 Message Date
Ben 126b05224f Read best difficulty in API workers 2026-06-21 17:45:00 -04:00
Ben 12f3983c8f Filter stale address workers 2026-06-21 17:30:06 -04:00
3 changed files with 103 additions and 14 deletions
+19 -9
View File
@@ -4,10 +4,11 @@ import { In, Repository } from 'typeorm';
import { ClientEntity } from './client.entity'; import { ClientEntity } from './client.entity';
const DEFAULT_CLIENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
@Injectable() @Injectable()
export class ClientService { export class ClientService {
private readonly activeWindowMs = this.readPositiveInt('CLIENT_REPORT_ACTIVE_WINDOW_MS', DEFAULT_CLIENT_ACTIVE_WINDOW_MS);
constructor( constructor(
@InjectRepository(ClientEntity) @InjectRepository(ClientEntity)
@@ -84,14 +85,15 @@ export class ClientService {
} }
public async getByAddress(address: string): Promise<ClientEntity[]> { public async getByAddress(address: string): Promise<ClientEntity[]> {
return await this.clientRepository.find({ const activeSince = new Date(Date.now() - this.activeWindowMs);
where: { return await this.clientRepository
address .createQueryBuilder('client')
}, .where('client.address = :address', { address })
order: { .andWhere('client.deletedAt IS NULL')
updatedAt: 'DESC', .andWhere('client.updatedAt > :activeSince', { activeSince })
}, .andWhere('client.hashRate > 0')
}) .orderBy('client.updatedAt', 'DESC')
.getMany();
} }
@@ -135,4 +137,12 @@ export class ClientService {
// return result; // return result;
// } // }
private readPositiveInt(name: string, defaultValue: number): number {
const value = Number(process.env[name]);
if (Number.isInteger(value) && value > 0) {
return value;
}
return defaultValue;
}
} }
@@ -105,6 +105,34 @@ describe('ClientController', () => {
})); }));
}); });
it('should read address best difficulty in API-only mode', async () => {
const originalApiOnly = process.env.API_ONLY;
process.env.API_ONLY = 'true';
try {
addressSettingsService.getSettings.mockResolvedValue({ bestDifficulty: 8192 });
shareAccountingService.getSessionSummaries.mockResolvedValue(new Map());
shareAccountingService.getAddressSummary.mockResolvedValue({
totalAcceptedShares: 10,
totalCreditedDifficulty: 100,
bestSubmissionDifficulty: 0,
});
await expect(controller.getClientInfo('bc1qtest')).resolves.toEqual(expect.objectContaining({
bestDifficulty: 8192,
accounting: expect.objectContaining({
bestSubmissionDifficulty: 8192,
}),
}));
expect(addressSettingsService.getSettings).toHaveBeenCalledWith('bc1qtest', false);
} finally {
if (originalApiOnly == null) {
delete process.env.API_ONLY;
} else {
process.env.API_ONLY = originalApiOnly;
}
}
});
it('should expose the latest current PPLNS expected payout for an address', async () => { it('should expose the latest current PPLNS expected payout for an address', async () => {
addressSettingsService.getSettings.mockResolvedValue(null); addressSettingsService.getSettings.mockResolvedValue(null);
shareAccountingService.getAddressSummary.mockResolvedValue({ shareAccountingService.getAddressSummary.mockResolvedValue({
@@ -145,6 +173,8 @@ describe('ClientController', () => {
}); });
it('should expose active database workers for an address', async () => { it('should expose active database workers for an address', async () => {
const recent = new Date(Date.now() - 60 * 1000).toISOString();
const stale = new Date(Date.now() - 60 * 60 * 1000).toISOString();
clientService.getByAddress.mockResolvedValue([ clientService.getByAddress.mockResolvedValue([
{ {
id: 'active-client', id: 'active-client',
@@ -155,7 +185,7 @@ describe('ClientController', () => {
bestDifficulty: 64, bestDifficulty: 64,
hashRate: 1024, hashRate: 1024,
startTime: '2026-06-08T12:00:00.000Z', startTime: '2026-06-08T12:00:00.000Z',
updatedAt: '2026-06-08T12:10:00.000Z', updatedAt: recent,
}, },
{ {
id: 'solo-client', id: 'solo-client',
@@ -166,7 +196,41 @@ describe('ClientController', () => {
bestDifficulty: 128, bestDifficulty: 128,
hashRate: 2048, hashRate: 2048,
startTime: '2026-06-08T12:00:00.000Z', startTime: '2026-06-08T12:00:00.000Z',
updatedAt: '2026-06-08T12:10:00.000Z', updatedAt: recent,
},
{
id: 'stale-client',
address: 'bc1qtest',
sessionId: 'stale1',
clientName: 'stale-worker',
payoutMode: 'pplns',
bestDifficulty: 256,
hashRate: 4096,
startTime: '2026-06-08T12:00:00.000Z',
updatedAt: stale,
},
{
id: 'idle-client',
address: 'bc1qtest',
sessionId: 'idle1',
clientName: 'idle-worker',
payoutMode: 'pplns',
bestDifficulty: 512,
hashRate: 0,
startTime: '2026-06-08T12:00:00.000Z',
updatedAt: recent,
},
{
id: 'deleted-client',
address: 'bc1qtest',
sessionId: 'deleted1',
clientName: 'deleted-worker',
payoutMode: 'pplns',
bestDifficulty: 1024,
hashRate: 8192,
startTime: '2026-06-08T12:00:00.000Z',
updatedAt: recent,
deletedAt: recent,
}, },
]); ]);
addressSettingsService.getSettings.mockResolvedValue(null); addressSettingsService.getSettings.mockResolvedValue(null);
+18 -3
View File
@@ -7,9 +7,11 @@ import { PayoutSnapshotService } from '../../ORM/payout-snapshot/payout-snapshot
import { ShareAccountingService } from '../../ORM/share-accounting/share-accounting.service'; import { ShareAccountingService } from '../../ORM/share-accounting/share-accounting.service';
import { normalizePayoutMode, PayoutMode } from '../../types/payout-mode'; import { normalizePayoutMode, PayoutMode } from '../../types/payout-mode';
const DEFAULT_CLIENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
@Controller('client') @Controller('client')
export class ClientController { export class ClientController {
private readonly activeWindowMs = this.readPositiveInt('CLIENT_REPORT_ACTIVE_WINDOW_MS', DEFAULT_CLIENT_ACTIVE_WINDOW_MS);
constructor( constructor(
private readonly clientService: ClientService, private readonly clientService: ClientService,
@@ -26,9 +28,7 @@ export class ClientController {
const workers = await this.getActiveAddressWorkers(address, mode); const workers = await this.getActiveAddressWorkers(address, mode);
const sessionSummaries = await this.shareAccountingService.getSessionSummaries(workers.map(worker => worker.clientId)); const sessionSummaries = await this.shareAccountingService.getSessionSummaries(workers.map(worker => worker.clientId));
const addressSettings = process.env.API_ONLY === 'true' const addressSettings = await this.addressSettingsService.getSettings(address, false);
? null
: await this.addressSettingsService.getSettings(address, false);
const bestDifficulty = addressSettings?.bestDifficulty ?? workers.reduce((best, worker) => { const bestDifficulty = addressSettings?.bestDifficulty ?? workers.reduce((best, worker) => {
return Math.max(best, Number(worker.bestDifficulty ?? 0)); return Math.max(best, Number(worker.bestDifficulty ?? 0));
}, 0); }, 0);
@@ -178,8 +178,15 @@ export class ClientController {
private async getActiveAddressWorkers(address: string, payoutMode?: PayoutMode) { private async getActiveAddressWorkers(address: string, payoutMode?: PayoutMode) {
const workers = await this.clientService.getByAddress(address); const workers = await this.clientService.getByAddress(address);
const activeSince = Date.now() - this.activeWindowMs;
return workers return workers
.filter(worker => payoutMode == null || worker.payoutMode === payoutMode) .filter(worker => payoutMode == null || worker.payoutMode === payoutMode)
.filter(worker => worker.deletedAt == null)
.filter(worker => Number(worker.hashRate ?? 0) > 0)
.filter(worker => {
const updatedAt = worker.updatedAt == null ? 0 : new Date(worker.updatedAt).getTime();
return Number.isFinite(updatedAt) && updatedAt > activeSince;
})
.map(worker => ({ .map(worker => ({
clientId: worker.id, clientId: worker.id,
address: worker.address, address: worker.address,
@@ -193,4 +200,12 @@ export class ClientController {
bestDifficulty: Number(worker.bestDifficulty ?? 0), bestDifficulty: Number(worker.bestDifficulty ?? 0),
})); }));
} }
private readPositiveInt(name: string, defaultValue: number): number {
const value = Number(process.env[name]);
if (Number.isInteger(value) && value > 0) {
return value;
}
return defaultValue;
}
} }