mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
db refactor
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class UniqueNonceIndex implements MigrationInterface {
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX "IDX_unique_nonce" ON "client_entity" ("sessionId") WHERE "deletedAt" IS NOT NULL`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_unique_nonce"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -14,6 +14,9 @@ export class AddressSettingsEntity extends TrackedEntity {
|
|||||||
@Column({ type: 'decimal', default: 0 })
|
@Column({ type: 'decimal', default: 0 })
|
||||||
bestDifficulty: number;
|
bestDifficulty: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
bestDifficultyUserAgent: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
miscCoinbaseScriptData: string;
|
miscCoinbaseScriptData: string;
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,18 @@ export class AddressSettingsService {
|
|||||||
public async getSettings(address: string, createIfNotFound: boolean) {
|
public async getSettings(address: string, createIfNotFound: boolean) {
|
||||||
const settings = await this.addressSettingsRepository.findOne({ where: { address } });
|
const settings = await this.addressSettingsRepository.findOne({ where: { address } });
|
||||||
if (createIfNotFound == true && settings == null) {
|
if (createIfNotFound == true && settings == null) {
|
||||||
return await this.createNew(address);
|
// It's possible to have a race condition here so if we get a PK violation, fetch it
|
||||||
|
try {
|
||||||
|
return await this.createNew(address);
|
||||||
|
} catch (e) {
|
||||||
|
return await this.addressSettingsRepository.findOne({ where: { address } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateBestDifficulty(address: string, bestDifficulty: number) {
|
public async updateBestDifficulty(address: string, bestDifficulty: number, bestDifficultyUserAgent: string) {
|
||||||
return await this.addressSettingsRepository.update({ address }, { bestDifficulty });
|
return await this.addressSettingsRepository.update({ address }, { bestDifficulty, bestDifficultyUserAgent });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createNew(address: string) {
|
public async createNew(address: string) {
|
||||||
@@ -46,4 +51,12 @@ export class AddressSettingsService {
|
|||||||
bestDifficulty: 0
|
bestDifficulty: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getHighScores() {
|
||||||
|
return await this.addressSettingsRepository.createQueryBuilder()
|
||||||
|
.select('"updatedAt", "bestDifficulty", "bestDifficultyUserAgent"')
|
||||||
|
.orderBy('"bestDifficulty"', 'DESC')
|
||||||
|
.limit(10)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
import { ClientEntity } from '../client/client.entity';
|
||||||
import { TrackedEntity } from '../utils/TrackedEntity.entity';
|
import { TrackedEntity } from '../utils/TrackedEntity.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
//Index for getHashRateForSession
|
|
||||||
@Index(["address", "clientName", "sessionId"])
|
|
||||||
//Index for statistics save
|
//Index for statistics save
|
||||||
@Index(["address", "clientName", "sessionId", "time"])
|
@Index(["clientId", "time"])
|
||||||
export class ClientStatisticsEntity extends TrackedEntity {
|
export class ClientStatisticsEntity extends TrackedEntity {
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
@@ -32,4 +31,17 @@ export class ClientStatisticsEntity extends TrackedEntity {
|
|||||||
acceptedCount: number;
|
acceptedCount: number;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ManyToOne(
|
||||||
|
() => ClientEntity,
|
||||||
|
clientEntity => clientEntity.statistics,
|
||||||
|
{ nullable: false, }
|
||||||
|
)
|
||||||
|
client: ClientEntity;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({ name: 'clientId' })
|
||||||
|
public clientId: string;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -19,25 +19,15 @@ export class ClientStatisticsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async save(clientStatistic: Partial<ClientStatisticsEntity>) {
|
public async save(clientStatistic: Partial<ClientStatisticsEntity>) {
|
||||||
// Attempt to update the existing record
|
// // Attempt to update the existing record
|
||||||
const updateResult = await this.clientStatisticsRepository
|
const result = await this.clientStatisticsRepository.update({ clientId: clientStatistic.clientId, time: clientStatistic.time },
|
||||||
.createQueryBuilder()
|
{
|
||||||
.update(ClientStatisticsEntity)
|
shares: clientStatistic.shares,
|
||||||
.set({
|
acceptedCount: clientStatistic.acceptedCount
|
||||||
shares: () => `"shares" + :sharesIncrement`,
|
|
||||||
acceptedCount: () => `"acceptedCount" + 1`
|
|
||||||
})
|
})
|
||||||
.where('address = :address AND clientName = :clientName AND sessionId = :sessionId AND time = :time', {
|
|
||||||
address: clientStatistic.address,
|
|
||||||
clientName: clientStatistic.clientName,
|
|
||||||
sessionId: clientStatistic.sessionId,
|
|
||||||
time: clientStatistic.time,
|
|
||||||
sharesIncrement: clientStatistic.shares
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
// Check if the update affected any rows
|
// Check if the update affected any rows
|
||||||
if (updateResult.affected === 0) {
|
if (result.affected === 0) {
|
||||||
// If no rows were updated, insert a new record
|
// If no rows were updated, insert a new record
|
||||||
await this.clientStatisticsRepository.insert(clientStatistic);
|
await this.clientStatisticsRepository.insert(clientStatistic);
|
||||||
}
|
}
|
||||||
@@ -139,7 +129,7 @@ export class ClientStatisticsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async getHashRateForSession(address: string, clientName: string, sessionId: string) {
|
public async getHashRateForSession(clientId: string) {
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -149,12 +139,12 @@ export class ClientStatisticsService {
|
|||||||
FROM
|
FROM
|
||||||
client_statistics_entity AS entry
|
client_statistics_entity AS entry
|
||||||
WHERE
|
WHERE
|
||||||
entry.address = $1 AND entry."clientName" = $2 AND entry."sessionId" = $3
|
entry."clientId" = $1
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
LIMIT 2;
|
LIMIT 2;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await this.clientStatisticsRepository.query(query, [address, clientName, sessionId]);
|
const result = await this.clientStatisticsRepository.query(query, [clientId]);
|
||||||
|
|
||||||
if (result.length < 1) {
|
if (result.length < 1) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -179,7 +169,7 @@ export class ClientStatisticsService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getChartDataForSession(address: string, clientName: string, sessionId: string) {
|
public async getChartDataForSession(clientId: string) {
|
||||||
var yesterday = new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
|
var yesterday = new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
@@ -189,7 +179,7 @@ export class ClientStatisticsService {
|
|||||||
FROM
|
FROM
|
||||||
client_statistics_entity AS entry
|
client_statistics_entity AS entry
|
||||||
WHERE
|
WHERE
|
||||||
entry.address = $1 AND entry."clientName" = $2 AND entry."sessionId" = $3 AND entry.time > ${yesterday.getTime()}
|
entry."clientId" = $1 AND entry.time > ${yesterday.getTime()}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
time
|
time
|
||||||
ORDER BY
|
ORDER BY
|
||||||
@@ -197,7 +187,7 @@ export class ClientStatisticsService {
|
|||||||
LIMIT 144;
|
LIMIT 144;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await this.clientStatisticsRepository.query(query, [address, clientName, sessionId]);
|
const result = await this.clientStatisticsRepository.query(query, [clientId]);
|
||||||
|
|
||||||
return result.map(res => {
|
return result.map(res => {
|
||||||
res.label = new Date(parseInt(res.label)).toISOString();
|
res.label = new Date(parseInt(res.label)).toISOString();
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
import { ClientStatisticsEntity } from '../client-statistics/client-statistics.entity';
|
||||||
import { TrackedEntity } from '../utils/TrackedEntity.entity';
|
import { TrackedEntity } from '../utils/TrackedEntity.entity';
|
||||||
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Index(['address', 'clientName', 'sessionId'], { unique: true })
|
@Index("IDX_unique_nonce", { synchronize: false })
|
||||||
export class ClientEntity extends TrackedEntity {
|
export class ClientEntity extends TrackedEntity {
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@PrimaryColumn({ length: 62, type: 'varchar' })
|
id: string;
|
||||||
address: string;
|
|
||||||
|
|
||||||
@PrimaryColumn({ length: 64, type: 'varchar' })
|
|
||||||
clientName: string;
|
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@PrimaryColumn({ length: 8, type: 'varchar' })
|
@Column({ length: 62, type: 'varchar' })
|
||||||
|
address: string;
|
||||||
|
|
||||||
|
@Column({ length: 64, type: 'varchar' })
|
||||||
|
clientName: string;
|
||||||
|
|
||||||
|
@Column({ length: 8, type: 'varchar', })
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
|
||||||
|
|
||||||
@@ -23,7 +26,6 @@ export class ClientEntity extends TrackedEntity {
|
|||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp' })
|
@Column({ type: 'timestamp' })
|
||||||
startTime: Date;
|
startTime: Date;
|
||||||
|
|
||||||
@@ -33,5 +35,11 @@ export class ClientEntity extends TrackedEntity {
|
|||||||
@Column({ default: 0, type: 'decimal' })
|
@Column({ default: 0, type: 'decimal' })
|
||||||
hashRate: number;
|
hashRate: number;
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => ClientStatisticsEntity,
|
||||||
|
clientStatisticsEntity => clientStatisticsEntity.client
|
||||||
|
)
|
||||||
|
statistics: ClientStatisticsEntity[]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ export class ClientService {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async heartbeat(address: string, clientName: string, sessionId: string, hashRate: number, updatedAt: Date) {
|
public async heartbeat(id, hashRate: number, updatedAt: Date) {
|
||||||
return await this.clientRepository.update({ address, clientName, sessionId }, { hashRate, deletedAt: null, updatedAt });
|
return await this.clientRepository.update({ id }, { hashRate, deletedAt: null, updatedAt });
|
||||||
}
|
}
|
||||||
|
|
||||||
// public async save(client: Partial<ClientEntity>) {
|
// public async save(client: Partial<ClientEntity>) {
|
||||||
@@ -48,8 +48,8 @@ export class ClientService {
|
|||||||
return client as ClientEntity;
|
return client as ClientEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete(sessionId: string) {
|
public async delete(id: string) {
|
||||||
return await this.clientRepository.softDelete({ sessionId });
|
return await this.clientRepository.softDelete({ id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteOldClients() {
|
public async deleteOldClients() {
|
||||||
@@ -65,8 +65,8 @@ export class ClientService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateBestDifficulty(sessionId: string, bestDifficulty: number) {
|
public async updateBestDifficulty(id: string, bestDifficulty: number) {
|
||||||
return await this.clientRepository.update({ sessionId }, { bestDifficulty });
|
return await this.clientRepository.update({ id }, { bestDifficulty });
|
||||||
}
|
}
|
||||||
public async connectedClientCount(): Promise<number> {
|
public async connectedClientCount(): Promise<number> {
|
||||||
return await this.clientRepository.count();
|
return await this.clientRepository.count();
|
||||||
@@ -111,7 +111,7 @@ export class ClientService {
|
|||||||
.addSelect('MAX(client.bestDifficulty)', 'bestDifficulty')
|
.addSelect('MAX(client.bestDifficulty)', 'bestDifficulty')
|
||||||
.addSelect('SUM(client.hashRate)', 'totalHashRate')
|
.addSelect('SUM(client.hashRate)', 'totalHashRate')
|
||||||
.groupBy('client.userAgent')
|
.groupBy('client.userAgent')
|
||||||
.orderBy('count', 'DESC')
|
.orderBy('"totalHashRate"', 'DESC')
|
||||||
.getRawMany();
|
.getRawMany();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Controller, Get, Inject } from '@nestjs/common';
|
|||||||
import { Cache } from 'cache-manager';
|
import { Cache } from 'cache-manager';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import { AddressSettingsService } from './ORM/address-settings/address-settings.service';
|
||||||
import { BlocksService } from './ORM/blocks/blocks.service';
|
import { BlocksService } from './ORM/blocks/blocks.service';
|
||||||
import { ClientStatisticsService } from './ORM/client-statistics/client-statistics.service';
|
import { ClientStatisticsService } from './ORM/client-statistics/client-statistics.service';
|
||||||
import { ClientService } from './ORM/client/client.service';
|
import { ClientService } from './ORM/client/client.service';
|
||||||
@@ -20,7 +21,8 @@ export class AppController {
|
|||||||
private readonly clientStatisticsService: ClientStatisticsService,
|
private readonly clientStatisticsService: ClientStatisticsService,
|
||||||
private readonly blocksService: BlocksService,
|
private readonly blocksService: BlocksService,
|
||||||
private readonly bitcoinRpcService: BitcoinRpcService,
|
private readonly bitcoinRpcService: BitcoinRpcService,
|
||||||
private readonly homeGraphService: HomeGraphService
|
private readonly homeGraphService: HomeGraphService,
|
||||||
|
private readonly addressSettingsService: AddressSettingsService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@Get('info')
|
@Get('info')
|
||||||
@@ -37,10 +39,12 @@ export class AppController {
|
|||||||
|
|
||||||
const blockData = await this.blocksService.getFoundBlocks();
|
const blockData = await this.blocksService.getFoundBlocks();
|
||||||
const userAgents = await this.clientService.getUserAgents();
|
const userAgents = await this.clientService.getUserAgents();
|
||||||
|
const highScores = await this.addressSettingsService.getHighScores();
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
blockData,
|
blockData,
|
||||||
userAgents,
|
userAgents,
|
||||||
|
highScores,
|
||||||
uptime: this.uptime
|
uptime: this.uptime
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -9,6 +9,7 @@ import { AppController } from './app.controller';
|
|||||||
import { AddressController } from './controllers/address/address.controller';
|
import { AddressController } from './controllers/address/address.controller';
|
||||||
import { ClientController } from './controllers/client/client.controller';
|
import { ClientController } from './controllers/client/client.controller';
|
||||||
import { BitcoinAddressValidator } from './models/validators/bitcoin-address.validator';
|
import { BitcoinAddressValidator } from './models/validators/bitcoin-address.validator';
|
||||||
|
import { UniqueNonceIndex } from './ORM/_migrations/UniqueNonceIndex';
|
||||||
import { AddressSettingsEntity } from './ORM/address-settings/address-settings.entity';
|
import { AddressSettingsEntity } from './ORM/address-settings/address-settings.entity';
|
||||||
import { AddressSettingsModule } from './ORM/address-settings/address-settings.module';
|
import { AddressSettingsModule } from './ORM/address-settings/address-settings.module';
|
||||||
import { BlocksEntity } from './ORM/blocks/blocks.entity';
|
import { BlocksEntity } from './ORM/blocks/blocks.entity';
|
||||||
@@ -68,7 +69,10 @@ const ORMModules = [
|
|||||||
],
|
],
|
||||||
synchronize: configService.get('PRODUCTION') != 'true',
|
synchronize: configService.get('PRODUCTION') != 'true',
|
||||||
logging: false,
|
logging: false,
|
||||||
poolSize: 30
|
poolSize: 30,
|
||||||
|
migrations: [
|
||||||
|
UniqueNonceIndex
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class ClientController {
|
|||||||
if (worker == null) {
|
if (worker == null) {
|
||||||
return new NotFoundException();
|
return new NotFoundException();
|
||||||
}
|
}
|
||||||
const chartData = await this.clientStatisticsService.getChartDataForSession(worker.address, worker.clientName, worker.sessionId);
|
const chartData = await this.clientStatisticsService.getChartDataForSession(worker.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId: worker.sessionId,
|
sessionId: worker.sessionId,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class StratumV1Client {
|
|||||||
private usedSuggestedDifficulty = false;
|
private usedSuggestedDifficulty = false;
|
||||||
private sessionDifficulty: number = 16384;
|
private sessionDifficulty: number = 16384;
|
||||||
|
|
||||||
private entity: ClientEntity;
|
private clientEntity: ClientEntity;
|
||||||
private creatingEntity: Promise<void>;
|
private creatingEntity: Promise<void>;
|
||||||
|
|
||||||
public extraNonceAndSessionId: string;
|
public extraNonceAndSessionId: string;
|
||||||
@@ -83,7 +83,7 @@ export class StratumV1Client {
|
|||||||
|
|
||||||
public async destroy() {
|
public async destroy() {
|
||||||
|
|
||||||
await this.clientService.delete(this.extraNonceAndSessionId);
|
await this.clientService.delete(this.clientEntity.id);
|
||||||
|
|
||||||
if (this.stratumSubscription != null) {
|
if (this.stratumSubscription != null) {
|
||||||
this.stratumSubscription.unsubscribe();
|
this.stratumSubscription.unsubscribe();
|
||||||
@@ -373,11 +373,11 @@ export class StratumV1Client {
|
|||||||
}, 60 * 1000)
|
}, 60 * 1000)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.backgroundWork.push(
|
// this.backgroundWork.push(
|
||||||
setInterval(async () => {
|
// setInterval(async () => {
|
||||||
await this.statistics.saveShares(this.entity);
|
// await this.statistics.saveShares(this.clientEntity);
|
||||||
}, 60 * 1000)
|
// }, 60 * 1000)
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendNewMiningJob(jobTemplate: IJobTemplate) {
|
private async sendNewMiningJob(jobTemplate: IJobTemplate) {
|
||||||
@@ -386,8 +386,8 @@ export class StratumV1Client {
|
|||||||
const devFeeAddress = this.configService.get('DEV_FEE_ADDRESS');
|
const devFeeAddress = this.configService.get('DEV_FEE_ADDRESS');
|
||||||
//50Th/s
|
//50Th/s
|
||||||
this.noFee = false;
|
this.noFee = false;
|
||||||
if (this.entity) {
|
if (this.clientEntity) {
|
||||||
this.hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientAuthorization.address, this.clientAuthorization.worker, this.extraNonceAndSessionId);
|
this.hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientEntity.id);
|
||||||
this.noFee = this.hashRate != 0 && this.hashRate < 50000000000000;
|
this.noFee = this.hashRate != 0 && this.hashRate < 50000000000000;
|
||||||
}
|
}
|
||||||
if (this.noFee || devFeeAddress == null || devFeeAddress.length < 1) {
|
if (this.noFee || devFeeAddress == null || devFeeAddress.length < 1) {
|
||||||
@@ -438,11 +438,11 @@ export class StratumV1Client {
|
|||||||
|
|
||||||
private async handleMiningSubmission(submission: MiningSubmitMessage) {
|
private async handleMiningSubmission(submission: MiningSubmitMessage) {
|
||||||
|
|
||||||
if (this.entity == null) {
|
if (this.clientEntity == null) {
|
||||||
if (this.creatingEntity == null) {
|
if (this.creatingEntity == null) {
|
||||||
this.creatingEntity = new Promise(async (resolve, reject) => {
|
this.creatingEntity = new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
this.entity = await this.clientService.insert({
|
this.clientEntity = await this.clientService.insert({
|
||||||
sessionId: this.extraNonceAndSessionId,
|
sessionId: this.extraNonceAndSessionId,
|
||||||
address: this.clientAuthorization.address,
|
address: this.clientAuthorization.address,
|
||||||
clientName: this.clientAuthorization.worker,
|
clientName: this.clientAuthorization.worker,
|
||||||
@@ -515,12 +515,12 @@ export class StratumV1Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.statistics.addShares(this.sessionDifficulty);
|
await this.statistics.addShares(this.clientEntity, this.sessionDifficulty);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
// only update every minute
|
// only update every minute
|
||||||
if (this.entity.updatedAt == null || now.getTime() - this.entity.updatedAt.getTime() > 1000 * 60) {
|
if (this.clientEntity.updatedAt == null || now.getTime() - this.clientEntity.updatedAt.getTime() > 1000 * 60) {
|
||||||
await this.clientService.heartbeat(this.entity.address, this.entity.clientName, this.entity.sessionId, this.hashRate, now);
|
await this.clientService.heartbeat(this.clientEntity.id, this.hashRate, now);
|
||||||
this.entity.updatedAt = now;
|
this.clientEntity.updatedAt = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -537,11 +537,11 @@ export class StratumV1Client {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (submissionDifficulty > this.entity.bestDifficulty) {
|
if (submissionDifficulty > this.clientEntity.bestDifficulty) {
|
||||||
await this.clientService.updateBestDifficulty(this.extraNonceAndSessionId, submissionDifficulty);
|
await this.clientService.updateBestDifficulty(this.clientEntity.id, submissionDifficulty);
|
||||||
this.entity.bestDifficulty = submissionDifficulty;
|
this.clientEntity.bestDifficulty = submissionDifficulty;
|
||||||
if (submissionDifficulty > (await this.addressSettingsService.getSettings(this.clientAuthorization.address, true)).bestDifficulty) {
|
if (submissionDifficulty > (await this.addressSettingsService.getSettings(this.clientAuthorization.address, true)).bestDifficulty) {
|
||||||
await this.addressSettingsService.updateBestDifficulty(this.clientAuthorization.address, submissionDifficulty);
|
await this.addressSettingsService.updateBestDifficulty(this.clientAuthorization.address, submissionDifficulty, this.clientEntity.userAgent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ const TARGET_SUBMISSION_PER_SECOND = 10;
|
|||||||
const MIN_DIFF = 0.00001;
|
const MIN_DIFF = 0.00001;
|
||||||
export class StratumV1ClientStatistics {
|
export class StratumV1ClientStatistics {
|
||||||
|
|
||||||
private shareBacklog: number = 0;
|
private shares: number = 0;
|
||||||
|
private acceptedCount: number = 0;
|
||||||
|
|
||||||
private submissionCacheStart: Date;
|
private submissionCacheStart: Date;
|
||||||
private submissionCache = [];
|
private submissionCache: { time: Date, difficulty: number }[] = [];
|
||||||
|
|
||||||
|
private currentTimeSlot: number = null;
|
||||||
|
private lastSave: number = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly clientStatisticsService: ClientStatisticsService
|
private readonly clientStatisticsService: ClientStatisticsService
|
||||||
@@ -20,40 +24,60 @@ export class StratumV1ClientStatistics {
|
|||||||
|
|
||||||
public async saveShares(client: ClientEntity) {
|
public async saveShares(client: ClientEntity) {
|
||||||
|
|
||||||
if (client == null || client.address == null || client.clientName == null || client.sessionId == null) {
|
// if (client == null || client.address == null || client.clientName == null || client.sessionId == null) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 10 min
|
|
||||||
var coeff = 1000 * 60 * 10;
|
|
||||||
var date = new Date();
|
|
||||||
var rounded = new Date(Math.floor(date.getTime() / coeff) * coeff);
|
|
||||||
|
|
||||||
await this.clientStatisticsService.save({
|
await this.clientStatisticsService.save({
|
||||||
time: rounded.getTime(),
|
time: this.currentTimeSlot,
|
||||||
shares: this.shareBacklog,
|
clientId: client.id,
|
||||||
|
shares: this.shares,
|
||||||
|
acceptedCount: this.acceptedCount,
|
||||||
address: client.address,
|
address: client.address,
|
||||||
clientName: client.clientName,
|
clientName: client.clientName,
|
||||||
sessionId: client.sessionId
|
sessionId: client.sessionId
|
||||||
});
|
});
|
||||||
|
|
||||||
this.shareBacklog = 0;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want to save them here because it can be DB intensive, stead do it every once in
|
// We don't want to save them here because it can be DB intensive, instead do it every once in
|
||||||
// awhile with saveShares()
|
// awhile with saveShares()
|
||||||
public async addShares(targetDifficulty: number) {
|
public async addShares(client: ClientEntity, targetDifficulty: number) {
|
||||||
|
|
||||||
|
// 10 min
|
||||||
|
var coeff = 1000 * 60 * 10;
|
||||||
|
var date = new Date();
|
||||||
|
var timeSlot = new Date(Math.floor(date.getTime() / coeff) * coeff).getTime();
|
||||||
|
|
||||||
if (this.submissionCache.length > CACHE_SIZE) {
|
if (this.submissionCache.length > CACHE_SIZE) {
|
||||||
this.submissionCache.shift();
|
this.submissionCache.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.submissionCache.push({
|
this.submissionCache.push({
|
||||||
time: new Date(),
|
time: date,
|
||||||
difficulty: targetDifficulty,
|
difficulty: targetDifficulty,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.shareBacklog += targetDifficulty;
|
|
||||||
|
|
||||||
|
if (this.currentTimeSlot == null) {
|
||||||
|
this.currentTimeSlot = timeSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentTimeSlot != timeSlot) {
|
||||||
|
await this.saveShares(client);
|
||||||
|
this.shares = 0;
|
||||||
|
this.acceptedCount = 0;
|
||||||
|
this.currentTimeSlot = timeSlot;
|
||||||
|
} else if ((date.getTime() - 60 * 1000) > this.lastSave) {
|
||||||
|
await this.saveShares(client);
|
||||||
|
this.lastSave = new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.shares += targetDifficulty;
|
||||||
|
this.acceptedCount++;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { RPCClient } from 'rpc-bitcoin';
|
import { RPCClient } from 'rpc-bitcoin';
|
||||||
import { BehaviorSubject, filter, shareReplay } from 'rxjs';
|
import { BehaviorSubject, filter, shareReplay } from 'rxjs';
|
||||||
import { RpcBlockService } from 'src/ORM/rpc-block/rpc-block.service';
|
import { RpcBlockService } from 'src/ORM/rpc-block/rpc-block.service';
|
||||||
import * as zmq from 'zeromq/v5-compat';
|
import * as zmq from 'zeromq';
|
||||||
|
|
||||||
import { IBlockTemplate } from '../models/bitcoin-rpc/IBlockTemplate';
|
import { IBlockTemplate } from '../models/bitcoin-rpc/IBlockTemplate';
|
||||||
import { IMiningInfo } from '../models/bitcoin-rpc/IMiningInfo';
|
import { IMiningInfo } from '../models/bitcoin-rpc/IMiningInfo';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BitcoinRpcService {
|
export class BitcoinRpcService implements OnModuleInit {
|
||||||
|
|
||||||
private blockHeight = 0;
|
private blockHeight = 0;
|
||||||
private client: RPCClient;
|
private client: RPCClient;
|
||||||
@@ -21,6 +21,10 @@ export class BitcoinRpcService {
|
|||||||
private rpcBlockService: RpcBlockService
|
private rpcBlockService: RpcBlockService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
const url = this.configService.get('BITCOIN_RPC_URL');
|
const url = this.configService.get('BITCOIN_RPC_URL');
|
||||||
const user = this.configService.get('BITCOIN_RPC_USER');
|
const user = this.configService.get('BITCOIN_RPC_USER');
|
||||||
const pass = this.configService.get('BITCOIN_RPC_PASSWORD');
|
const pass = this.configService.get('BITCOIN_RPC_PASSWORD');
|
||||||
@@ -36,19 +40,36 @@ export class BitcoinRpcService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (this.configService.get('BITCOIN_ZMQ_HOST')) {
|
if (this.configService.get('BITCOIN_ZMQ_HOST')) {
|
||||||
const sock = zmq.socket("sub");
|
console.log('Using ZMQ');
|
||||||
sock.connect(this.configService.get('BITCOIN_ZMQ_HOST'));
|
const sock = new zmq.Subscriber;
|
||||||
sock.subscribe("rawblock");
|
|
||||||
sock.on("message", async (topic: Buffer, message: Buffer) => {
|
|
||||||
console.log("new block zmq");
|
sock.connectTimeout = 1000;
|
||||||
await this.pollMiningInfo();
|
sock.events.on('connect', () => {
|
||||||
|
console.log('ZMQ Connected');
|
||||||
});
|
});
|
||||||
this.pollMiningInfo().then(() => { });
|
sock.events.on('connect:retry', () => {
|
||||||
|
console.log('ZMQ Unable to connect, Retrying');
|
||||||
|
});
|
||||||
|
|
||||||
|
sock.connect(this.configService.get('BITCOIN_ZMQ_HOST'));
|
||||||
|
sock.subscribe('rawblock');
|
||||||
|
// Don't await this, otherwise it will block the rest of the program
|
||||||
|
this.listenForNewBlocks(sock);
|
||||||
|
await this.pollMiningInfo();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
setInterval(this.pollMiningInfo.bind(this), 500);
|
setInterval(this.pollMiningInfo.bind(this), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async listenForNewBlocks(sock: zmq.Subscriber) {
|
||||||
|
for await (const [topic, msg] of sock) {
|
||||||
|
console.log("New Block");
|
||||||
|
await this.pollMiningInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async pollMiningInfo() {
|
public async pollMiningInfo() {
|
||||||
const miningInfo = await this.getMiningInfo();
|
const miningInfo = await this.getMiningInfo();
|
||||||
if (miningInfo != null && miningInfo.blocks > this.blockHeight) {
|
if (miningInfo != null && miningInfo.blocks > this.blockHeight) {
|
||||||
|
|||||||
Reference in New Issue
Block a user