Add payout modes and production switchover schema

This commit is contained in:
Ben
2026-06-20 13:59:05 -04:00
parent a33c530268
commit a9d425f32f
61 changed files with 4164 additions and 1029 deletions
@@ -2,11 +2,20 @@ import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { normalizePayoutMode, PayoutMode } from '../../types/payout-mode';
const HASHES_PER_DIFFICULTY = 4294967296;
const CHART_BUCKET_SECONDS = 600;
const CHART_WINDOW = '24 hours';
const SITE_CHART_WINDOW = '7 days';
interface AcceptedShareChartFilter {
address?: string;
clientName?: string;
clientId?: string;
payoutMode?: PayoutMode;
}
@Injectable()
export class ClientStatisticsService {
@@ -21,6 +30,10 @@ export class ClientStatisticsService {
return this.getAcceptedShareChartData('', [], limit, SITE_CHART_WINDOW);
}
public async getChartDataForSiteByPayoutMode(payoutMode?: PayoutMode, limit: number = 144 * 7) {
return this.getAcceptedShareChartDataByPayoutMode({ payoutMode }, limit, SITE_CHART_WINDOW);
}
public async getChartDataForAddress(address: string) {
return this.getAcceptedShareChartData(
'AND "address" = $1',
@@ -30,14 +43,33 @@ export class ClientStatisticsService {
);
}
public async getChartDataForAddressByPayoutMode(address: string, payoutMode?: PayoutMode) {
return this.getAcceptedShareChartDataByPayoutMode({ address, payoutMode }, 144, CHART_WINDOW);
}
public async getHashRateForGroup(address: string, clientName: string) {
const result = await this.dataSource.query(`
WITH bounds AS (
SELECT time_bucket(INTERVAL '10 minutes', NOW()) AS current_bucket
),
completed AS (
SELECT COALESCE(SUM("shares"), 0) AS "creditedDifficulty"
FROM "accepted_share_10m", bounds
WHERE "address" = $1
AND "clientName" = $2
AND "bucket" > bounds.current_bucket - INTERVAL '1 hour'
AND "bucket" < bounds.current_bucket
),
live AS (
SELECT COALESCE(SUM("creditedDifficulty"), 0) AS "creditedDifficulty"
FROM "accepted_share_entity", bounds
WHERE "address" = $1
AND "clientName" = $2
AND "acceptedAt" >= bounds.current_bucket
)
SELECT
COALESCE((SUM("creditedDifficulty") * ${HASHES_PER_DIFFICULTY}) / ${CHART_BUCKET_SECONDS}, 0) AS "hashRate"
FROM "accepted_share_entity"
WHERE "address" = $1
AND "clientName" = $2
AND "acceptedAt" > NOW() - INTERVAL '1 hour'
COALESCE(((completed."creditedDifficulty" + live."creditedDifficulty") * ${HASHES_PER_DIFFICULTY}) / ${CHART_BUCKET_SECONDS}, 0) AS "hashRate"
FROM completed, live
`, [address, clientName]);
return parseFloat(result[0]?.hashRate ?? '0');
@@ -52,6 +84,10 @@ export class ClientStatisticsService {
);
}
public async getChartDataForGroupByPayoutMode(address: string, clientName: string, payoutMode?: PayoutMode) {
return this.getAcceptedShareChartDataByPayoutMode({ address, clientName, payoutMode }, 144, CHART_WINDOW);
}
public async getChartDataForSession(clientId: string) {
return this.getAcceptedShareChartData(
'AND "clientId" = $1',
@@ -61,6 +97,10 @@ export class ClientStatisticsService {
);
}
public async getChartDataForSessionByPayoutMode(clientId: string, payoutMode?: PayoutMode) {
return this.getAcceptedShareChartDataByPayoutMode({ clientId, payoutMode }, 144, CHART_WINDOW);
}
private async getAcceptedShareChartData(filterSql: string, params: unknown[], limit: number, windowSql: string) {
const query = `
WITH bounds AS (
@@ -101,4 +141,80 @@ export class ClientStatisticsService {
};
});
}
private async getAcceptedShareChartDataByPayoutMode(
filter: AcceptedShareChartFilter,
limit: number,
windowSql: string,
) {
const { whereSql, params } = this.buildModeChartWhere(filter);
const query = `
WITH bounds AS (
SELECT
NOW() - INTERVAL '${windowSql}' AS since,
time_bucket(INTERVAL '10 minutes', NOW()) AS current_bucket
)
SELECT
"label",
"payoutMode",
"data",
"shares",
"acceptedCount"
FROM (
SELECT
"bucket" AS "label",
"payoutMode",
ROUND((SUM("shares") * ${HASHES_PER_DIFFICULTY}) / ${CHART_BUCKET_SECONDS}) AS "data",
SUM("shares") AS "shares",
SUM("acceptedCount") AS "acceptedCount"
FROM "accepted_share_10m", bounds
WHERE "bucket" > bounds.since
AND "bucket" < bounds.current_bucket
${whereSql}
GROUP BY "bucket", "payoutMode"
ORDER BY "bucket" DESC
LIMIT ${limit * 2}
) AS limited_rows
ORDER BY "label", "payoutMode"
`;
const result = await this.dataSource.query(query, params);
return result.map(res => {
return {
label: new Date(res.label).toISOString(),
payoutMode: res.payoutMode,
data: res.data,
shares: Number(res.shares ?? 0),
acceptedCount: Number(res.acceptedCount ?? 0),
};
});
}
private buildModeChartWhere(filter: AcceptedShareChartFilter): { whereSql: string; params: string[] } {
const where: string[] = [];
const params: string[] = [];
if (filter.address != null) {
params.push(filter.address);
where.push(`"address" = $${params.length}`);
}
if (filter.clientName != null) {
params.push(filter.clientName);
where.push(`"clientName" = $${params.length}`);
}
if (filter.clientId != null) {
params.push(filter.clientId);
where.push(`"clientId" = $${params.length}`);
}
if (filter.payoutMode != null) {
params.push(normalizePayoutMode(filter.payoutMode));
where.push(`"payoutMode" = $${params.length}`);
}
return {
whereSql: where.length > 0 ? `AND ${where.join(' AND ')}` : '',
params,
};
}
}