From 9315d200ff81920597298aad143c64f09afbab83 Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Sat, 24 Jun 2023 20:20:41 -0400 Subject: [PATCH] cleaning --- .../client-statistics.entity.ts | 6 +- .../client-statistics.service.ts | 115 ++++++++++++++++-- src/ORM/client/client.entity.ts | 3 +- src/ORM/client/client.service.ts | 12 +- src/ORM/utils/DateTimeTransformer.ts | 14 +++ src/ORM/utils/TrackedEntity.entity.ts | 8 +- src/app.controller.ts | 29 ++++- src/models/MiningJob.ts | 52 +++----- src/models/StratumV1Client.ts | 16 +-- src/stratum-v1-jobs.service.ts | 2 +- src/stratum-v1.service.ts | 4 +- 11 files changed, 199 insertions(+), 62 deletions(-) create mode 100644 src/ORM/utils/DateTimeTransformer.ts diff --git a/src/ORM/client-statistics/client-statistics.entity.ts b/src/ORM/client-statistics/client-statistics.entity.ts index ba98b9f..b2bfe40 100644 --- a/src/ORM/client-statistics/client-statistics.entity.ts +++ b/src/ORM/client-statistics/client-statistics.entity.ts @@ -1,5 +1,6 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { DateTimeTransformer } from '../utils/DateTimeTransformer'; import { TrackedEntity } from '../utils/TrackedEntity.entity'; @Entity() @@ -17,7 +18,10 @@ export class ClientStatisticsEntity extends TrackedEntity { @Column({ length: 8, type: 'varchar' }) sessionId: string; - @Column({ type: 'datetime' }) + @Column({ + type: 'datetime', + transformer: new DateTimeTransformer() + }) time: Date; @Column() diff --git a/src/ORM/client-statistics/client-statistics.service.ts b/src/ORM/client-statistics/client-statistics.service.ts index 158ce25..691c389 100644 --- a/src/ORM/client-statistics/client-statistics.service.ts +++ b/src/ORM/client-statistics/client-statistics.service.ts @@ -19,7 +19,7 @@ export class ClientStatisticsService { return await this.clientStatisticsRepository.save(clientStatistic); } - public async getHashRate(sessionId: string) { + public async getHashRateForAddress(address: string) { const query = ` SELECT @@ -28,10 +28,10 @@ export class ClientStatisticsService { FROM client_statistics_entity AS entry WHERE - entry.sessionId = ? AND entry.time > datetime("now", "-1 hour") + entry.address = ? AND entry.time > datetime("now", "-1 hour") `; - const result = await this.clientStatisticsRepository.query(query, [sessionId]); + const result = await this.clientStatisticsRepository.query(query, [address]); const timeDiff = result[0].timeDiff; const difficultySum = result[0].difficultySum; @@ -40,26 +40,125 @@ export class ClientStatisticsService { } - public async getChartData(sessionId: string) { + public async getChartDataForAddress(address: string) { const query = ` WITH result_set AS ( SELECT - MAX(time) AS label, + MAX(time) || 'GMT' AS label, (SUM(difficulty) * 4294967296) / ((JULIANDAY(MAX(time)) - JULIANDAY(MIN(time))) * 24 * 60 * 60 * 1000000000) AS data FROM client_statistics_entity AS entry WHERE - entry.sessionId = ? AND entry.time > datetime("now", "-1 day") + entry.address = ? AND entry.time > datetime("now", "-1 day") GROUP BY strftime('%Y-%m-%d %H', time, 'localtime') || (strftime('%M', time, 'localtime') / 5) + ORDER BY + time + ) + SELECT * + FROM result_set + WHERE label <> (SELECT MAX(label) FROM result_set); + `; + + const result = await this.clientStatisticsRepository.query(query, [address]); + + return result; + } + + + public async getHashRateForGroup(address: string, clientName: string) { + + const query = ` + SELECT + (JULIANDAY(MAX(entry.time)) - JULIANDAY(MIN(entry.time))) * 24 * 60 * 60 AS timeDiff, + SUM(entry.difficulty) AS difficultySum + FROM + client_statistics_entity AS entry + WHERE + entry.address = ? AND entry.clientName = ? AND entry.time > datetime("now", "-1 hour") + `; + + const result = await this.clientStatisticsRepository.query(query, [address, clientName]); + + const timeDiff = result[0].timeDiff; + const difficultySum = result[0].difficultySum; + + return (difficultySum * 4294967296) / (timeDiff * 1000000000); + + } + + public async getChartDataForGroup(address: string, clientName: string) { + const query = ` + WITH result_set AS ( + SELECT + MAX(time) || 'GMT' AS label, + (SUM(difficulty) * 4294967296) / + ((JULIANDAY(MAX(time)) - JULIANDAY(MIN(time))) * 24 * 60 * 60 * 1000000000) AS data + FROM + client_statistics_entity AS entry + WHERE + entry.address = ? AND entry.clientName = ? AND entry.time > datetime("now", "-1 day") + GROUP BY + strftime('%Y-%m-%d %H', time, 'localtime') || (strftime('%M', time, 'localtime') / 5) + ORDER BY + time ) SELECT * FROM result_set - WHERE label <> (SELECT MAX(Label) FROM result_set); + WHERE label <> (SELECT MAX(label) FROM result_set); `; - const result = await this.clientStatisticsRepository.query(query, [sessionId]); + const result = await this.clientStatisticsRepository.query(query, [address, clientName]); + + return result; + } + + + public async getHashRateForSession(address: string, clientName: string, sessionId: string) { + + const query = ` + SELECT + (JULIANDAY(MAX(entry.time)) - JULIANDAY(MIN(entry.time))) * 24 * 60 * 60 AS timeDiff, + SUM(entry.difficulty) AS difficultySum + FROM + client_statistics_entity AS entry + WHERE + entry.address = ? AND entry.clientName = ? AND entry.sessionId = ? AND entry.time > datetime("now", "-1 hour") + + `; + + const result = await this.clientStatisticsRepository.query(query, [address, clientName, sessionId]); + + const timeDiff = result[0].timeDiff; + const difficultySum = result[0].difficultySum; + + return (difficultySum * 4294967296) / (timeDiff * 1000000000); + + } + + public async getChartDataForSession(address: string, clientName: string, sessionId: string) { + const query = ` + WITH result_set AS ( + SELECT + MAX(time) || 'GMT' AS label, + (SUM(difficulty) * 4294967296) / + ((JULIANDAY(MAX(time)) - JULIANDAY(MIN(time))) * 24 * 60 * 60 * 1000000000) AS data + FROM + client_statistics_entity AS entry + WHERE + entry.address = ? AND entry.clientName = ? AND entry.sessionId = ? AND entry.time > datetime("now", "-1 day") + GROUP BY + strftime('%Y-%m-%d %H', time, 'localtime') || (strftime('%M', time, 'localtime') / 5) + ORDER BY + time + ) + SELECT * + FROM result_set + WHERE label <> (SELECT MAX(label) FROM result_set); + `; + + const result = await this.clientStatisticsRepository.query(query, [address, clientName, sessionId]); return result; } diff --git a/src/ORM/client/client.entity.ts b/src/ORM/client/client.entity.ts index deb061c..a82d8a9 100644 --- a/src/ORM/client/client.entity.ts +++ b/src/ORM/client/client.entity.ts @@ -1,5 +1,6 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { DateTimeTransformer } from '../utils/DateTimeTransformer'; import { TrackedEntity } from '../utils/TrackedEntity.entity'; @Entity() @@ -18,7 +19,7 @@ export class ClientEntity extends TrackedEntity { @Column({ length: 8, type: 'varchar' }) sessionId: string; - @Column({ type: 'datetime' }) + @Column({ type: 'datetime', transformer: new DateTimeTransformer() }) startTime: Date; diff --git a/src/ORM/client/client.service.ts b/src/ORM/client/client.service.ts index 4d3f067..a7bd8e7 100644 --- a/src/ORM/client/client.service.ts +++ b/src/ORM/client/client.service.ts @@ -40,7 +40,17 @@ export class ClientService { }) } - public async getById(address: string, clientName: string, sessionId: string): Promise { + + public async getByName(address: string, clientName: string): Promise { + return await this.clientRepository.find({ + where: { + address, + clientName + } + }) + } + + public async getBySessionId(address: string, clientName: string, sessionId: string): Promise { return await this.clientRepository.findOne({ where: { address, diff --git a/src/ORM/utils/DateTimeTransformer.ts b/src/ORM/utils/DateTimeTransformer.ts new file mode 100644 index 0000000..fa2c07e --- /dev/null +++ b/src/ORM/utils/DateTimeTransformer.ts @@ -0,0 +1,14 @@ +import { ValueTransformer } from 'typeorm'; + +export class DateTimeTransformer implements ValueTransformer { + to(value: Date): any { + // Convert the local time to UTC before saving to the database + const utcTime = value?.toLocaleString(); + return utcTime; + } + + from(value: any): Date { + // Convert the UTC time from the database to the local time zone + return value; + } +} \ No newline at end of file diff --git a/src/ORM/utils/TrackedEntity.entity.ts b/src/ORM/utils/TrackedEntity.entity.ts index e6caa85..e314b8c 100644 --- a/src/ORM/utils/TrackedEntity.entity.ts +++ b/src/ORM/utils/TrackedEntity.entity.ts @@ -1,12 +1,14 @@ import { CreateDateColumn, DeleteDateColumn, UpdateDateColumn } from 'typeorm'; +import { DateTimeTransformer } from './DateTimeTransformer'; + export abstract class TrackedEntity { - @DeleteDateColumn({ nullable: true, type: 'datetime' }) + @DeleteDateColumn({ nullable: true, type: 'datetime', transformer: new DateTimeTransformer() }) public deletedAt?: Date; - @CreateDateColumn({ type: 'datetime' }) + @CreateDateColumn({ type: 'datetime', transformer: new DateTimeTransformer() }) public createdAt?: Date - @UpdateDateColumn({ type: 'datetime' }) + @UpdateDateColumn({ type: 'datetime', transformer: new DateTimeTransformer() }) public updatedAt?: Date } \ No newline at end of file diff --git a/src/app.controller.ts b/src/app.controller.ts index ed90af2..cd815d6 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -26,6 +26,7 @@ export class AppController { const workers = await this.clientService.getByAddress(address); + const chartData = await this.clientStatisticsService.getChartDataForAddress(address); return { workersCount: workers.length, @@ -35,27 +36,45 @@ export class AppController { sessionId: worker.sessionId, name: worker.clientName, bestDifficulty: Math.floor(worker.bestDifficulty), - hashRate: Math.floor(await this.clientStatisticsService.getHashRate(worker.sessionId)), + hashRate: Math.floor(await this.clientStatisticsService.getHashRateForSession(worker.address, worker.clientName, worker.sessionId)), startTime: worker.startTime }; }) - ) + ), + chartData } } @Get('client/:address/:workerName') - async getWorkerGroupInfo(@Param('address') address: string, @Param('address') workerName: string) { + async getWorkerGroupInfo(@Param('address') address: string, @Param('workerName') workerName: string) { + const workers = await this.clientService.getByName(address, workerName); + + const bestDifficulty = workers.reduce((pre, cur, idx, arr) => { + if (cur.bestDifficulty > pre) { + return cur.bestDifficulty; + } + return pre; + }, 0); + + const chartData = await this.clientStatisticsService.getChartDataForGroup(address, workerName); + return { + + name: workerName, + bestDifficulty: Math.floor(bestDifficulty), + chartData: chartData, + + } } @Get('client/:address/:workerName/:sessionId') async getWorkerInfo(@Param('address') address: string, @Param('workerName') workerName: string, @Param('sessionId') sessionId: string) { - const worker = await this.clientService.getById(address, workerName, sessionId); + const worker = await this.clientService.getBySessionId(address, workerName, sessionId); if (worker == null) { return new NotFoundException(); } - const chartData = await this.clientStatisticsService.getChartData(sessionId); + const chartData = await this.clientStatisticsService.getChartDataForSession(worker.address, worker.clientName, worker.sessionId); return { sessionId: worker.sessionId, diff --git a/src/models/MiningJob.ts b/src/models/MiningJob.ts index 4c6f341..22a850e 100644 --- a/src/models/MiningJob.ts +++ b/src/models/MiningJob.ts @@ -10,14 +10,8 @@ interface AddressObject { percent: number; } export class MiningJob { - public id: number; - public method: eResponseMethod.MINING_NOTIFY; - public params: string[]; - public target: string; - public merkleRoot: string; - - public job_id: string; // ID of the job. Use this ID while submitting share generated from this job. + public jobId: string; // ID of the job. Use this ID while submitting share generated from this job. public prevhash: string; // The hex-encoded previous block hash. public coinb1: string; // The hex-encoded prefix of the coinbase transaction (to precede extra nonce 2). public coinb2: string; //The hex-encoded suffix of the coinbase transaction (to follow extra nonce 2). @@ -25,20 +19,21 @@ export class MiningJob { public version: number; // The hex-encoded block version. public nbits: number; // The hex-encoded network difficulty required for the block. public ntime: number; // Current ntime/ - //public clean_jobs: boolean; // When true, server indicates that submitting shares from previous jobs don't have a sense and such shares will be rejected. When this flag is set, miner should also drop all previous jobs too. public response: string; - public versionMask: string; - public tree: MerkleTree; + private tree: MerkleTree; + public coinbaseTransaction: bitcoinjs.Transaction; + public block: bitcoinjs.Block = new bitcoinjs.Block(); + constructor(id: string, payoutInformation: AddressObject[], public blockTemplate: IBlockTemplate, public networkDifficulty: number, public clean_jobs: boolean) { - this.job_id = id; - this.target = blockTemplate.target; + this.jobId = id; + //this.target = blockTemplate.target; this.prevhash = this.convertToLittleEndian(blockTemplate.previousblockhash); this.version = blockTemplate.version; @@ -81,8 +76,6 @@ export class MiningJob { this.tree = new MerkleTree(transactionBuffers, this.sha256, { isBitcoinTree: true }); - const rootBuffer = this.tree.getRoot(); - this.merkleRoot = rootBuffer.toString('hex'); this.merkle_branch = this.tree.getProof(this.coinbaseTransaction.getHash(false)).map(p => p.data.toString('hex')); @@ -92,41 +85,36 @@ export class MiningJob { private createCoinbaseTransaction(addresses: AddressObject[], blockHeight: number, reward: number): bitcoinjs.Transaction { // Part 1 - const tx = new bitcoinjs.Transaction(); + const coinbaseTransaction = new bitcoinjs.Transaction(); // Set the version of the transaction - tx.version = 2; + coinbaseTransaction.version = 2; const blockHeightScript = `03${blockHeight.toString(16).padStart(8, '0')}` + '00000000' + '00000000'; - // const inputScriptBytes = ((blockHeightScript.length + 16) / 2).toString(16).padStart(2, '0'); - // const OP_RETURN = '6a'; - // const inputScript = `${OP_RETURN}${inputScriptBytes}${blockHeightScript}` const inputScript = bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.from(blockHeightScript, 'hex')]) // Add the coinbase input (input with no previous output) - tx.addInput(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), 0xffffffff, 0xffffffff, inputScript); + coinbaseTransaction.addInput(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), 0xffffffff, 0xffffffff, inputScript); // Add an output - const recipientAddress = addresses[0].address; - - const scriptPubKey = bitcoinjs.payments.p2wpkh({ address: recipientAddress, network: bitcoinjs.networks.testnet }); - tx.addOutput(scriptPubKey.output, reward); - - + let rewardBalance = reward; + addresses.forEach(recipientAddress => { + const scriptPubKey = bitcoinjs.payments.p2wpkh({ address: recipientAddress.address, network: bitcoinjs.networks.testnet }); + const amount = Math.floor((recipientAddress.percent / 100) * reward); + rewardBalance -= amount; + coinbaseTransaction.addOutput(scriptPubKey.output, amount); + }) const segwitWitnessReservedValue = Buffer.alloc(32, 0); //and the coinbase's input's witness must consist of a single 32-byte array for the witness reserved value + coinbaseTransaction.ins[0].witness = [segwitWitnessReservedValue]; - tx.ins[0].witness = [segwitWitnessReservedValue]; - - - - return tx; + return coinbaseTransaction; } private sha256(data) { @@ -140,7 +128,7 @@ export class MiningJob { id: null, method: eResponseMethod.MINING_NOTIFY, params: [ - this.job_id, + this.jobId, this.prevhash, this.coinb1, this.coinb2, diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index a2da695..51e9555 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -34,7 +34,7 @@ export class StratumV1Client extends EasyUnsubscribe { public statistics: StratumV1ClientStatistics; - public id: string; + public extraNonce: string; public stratumInitialized = false; public refreshInterval: NodeJS.Timer; @@ -57,9 +57,9 @@ export class StratumV1Client extends EasyUnsubscribe { super(); this.startTime = new Date(); this.statistics = new StratumV1ClientStatistics(this.clientStatisticsService); - this.id = this.getRandomHexString(); + this.extraNonce = this.getRandomHexString(); - console.log(`New client ID: : ${this.id}`); + console.log(`New client ID: : ${this.extraNonce}`); this.socket.on('data', this.handleData.bind(this, this.socket)); @@ -110,7 +110,7 @@ export class StratumV1Client extends EasyUnsubscribe { if (errors.length === 0) { this.clientSubscription = subscriptionMessage; - socket.write(JSON.stringify(this.clientSubscription.response(this.id)) + '\n'); + socket.write(JSON.stringify(this.clientSubscription.response(this.extraNonce)) + '\n'); } else { console.error(errors); } @@ -223,7 +223,7 @@ export class StratumV1Client extends EasyUnsubscribe { this.entity = await this.clientService.save({ - sessionId: this.id, + sessionId: this.extraNonce, address: this.clientAuthorization.address, clientName: this.clientAuthorization.worker, startTime: new Date(), @@ -260,14 +260,14 @@ export class StratumV1Client extends EasyUnsubscribe { if (job == null) { return; } - const diff = submission.calculateDifficulty(this.id, job, submission); + const diff = submission.calculateDifficulty(this.extraNonce, job, submission); console.log(`DIFF: ${diff}`); if (diff >= this.clientDifficulty) { const networkDifficulty = this.calculateNetworkDifficulty(parseInt(job.blockTemplate.bits, 16)); await this.statistics.addSubmission(this.entity, this.clientDifficulty); if (diff > this.entity.bestDifficulty) { - await this.clientService.updateBestDifficulty(this.id, diff); + await this.clientService.updateBestDifficulty(this.extraNonce, diff); this.entity.bestDifficulty = diff; } if (diff >= (networkDifficulty / 2)) { @@ -294,7 +294,7 @@ export class StratumV1Client extends EasyUnsubscribe { private constructBlockAndBroadcast(job: MiningJob, submission: MiningSubmitMessage) { const block = new Block(); - const blockHeightScript = `03${job.blockTemplate.height.toString(16).padStart(8, '0')}${job.id}${submission.extraNonce2}`; + const blockHeightScript = `03${job.blockTemplate.height.toString(16).padStart(8, '0')}${this.extraNonce}${submission.extraNonce2}`; const inputScript = bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.from(blockHeightScript, 'hex')]); job.coinbaseTransaction.ins[0].script = inputScript; diff --git a/src/stratum-v1-jobs.service.ts b/src/stratum-v1-jobs.service.ts index 974c359..3184a59 100644 --- a/src/stratum-v1-jobs.service.ts +++ b/src/stratum-v1-jobs.service.ts @@ -21,7 +21,7 @@ export class StratumV1JobsService { } public getJobById(jobId: string) { - return this.jobs.find(job => job.job_id == jobId); + return this.jobs.find(job => job.jobId == jobId); } public getNextId() { diff --git a/src/stratum-v1.service.ts b/src/stratum-v1.service.ts index ffab42a..9047ac3 100644 --- a/src/stratum-v1.service.ts +++ b/src/stratum-v1.service.ts @@ -47,7 +47,7 @@ export class StratumV1Service implements OnModuleInit { socket.on('end', async () => { // Handle socket disconnection - await this.clientService.delete(client.id); + await this.clientService.delete(client.extraNonce); const clientCount = await this.clientService.connectedClientCount(); console.log(`Client disconnected: ${socket.remoteAddress}, ${clientCount} total clients`); @@ -55,7 +55,7 @@ export class StratumV1Service implements OnModuleInit { socket.on('error', async (error: Error) => { - await this.clientService.delete(client.id); + await this.clientService.delete(client.extraNonce); const clientCount = await this.clientService.connectedClientCount(); console.error(`Socket error:`, error);