mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
126b05224f | ||
|
|
12f3983c8f |
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user