diff --git a/src/models/MiningJob.ts b/src/models/MiningJob.ts index b7349e3..ffd2842 100644 --- a/src/models/MiningJob.ts +++ b/src/models/MiningJob.ts @@ -28,7 +28,6 @@ export class MiningJob { public blockTemplate: IBlockTemplate, public clean_jobs: boolean) { - console.log(JSON.stringify(blockTemplate)) this.jobId = id; this.block.prevHash = this.convertToLittleEndian(blockTemplate.previousblockhash); diff --git a/src/models/StratumV1Client.spec.ts b/src/models/StratumV1Client.spec.ts index 3ea7b56..831f046 100644 --- a/src/models/StratumV1Client.spec.ts +++ b/src/models/StratumV1Client.spec.ts @@ -1,21 +1,44 @@ import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { PromiseSocket } from 'promise-socket'; +import { BehaviorSubject } from 'rxjs'; +import { DataSource } from 'typeorm'; +import { MockRecording1 } from '../../test/models/MockRecording1'; import { BlocksService } from '../ORM/blocks/blocks.service'; +import { ClientStatisticsEntity } from '../ORM/client-statistics/client-statistics.entity'; +import { ClientStatisticsModule } from '../ORM/client-statistics/client-statistics.module'; import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service'; +import { ClientEntity } from '../ORM/client/client.entity'; +import { ClientModule } from '../ORM/client/client.module'; import { ClientService } from '../ORM/client/client.service'; -import { BitcoinRpcService } from '../services/bitcoin-rpc.service'; +import { BitcoinRpcService as MockBitcoinRpcService } from '../services/bitcoin-rpc.service'; import { BlockTemplateService } from '../services/block-template.service'; import { NotificationService } from '../services/notification.service'; import { StratumV1JobsService } from '../services/stratum-v1-jobs.service'; +import { IMiningInfo } from './bitcoin-rpc/IMiningInfo'; import { StratumV1Client } from './StratumV1Client'; + + + +jest.mock('../services/bitcoin-rpc.service') + +jest.mock('./validators/bitcoin-address.validator', () => ({ + IsBitcoinAddress() { + return jest.fn(); + }, +})); + + describe('StratumV1Client', () => { - let promiseSocket: PromiseSocket = new PromiseSocket(); - let stratumV1JobsService: StratumV1JobsService; - let bitcoinRpcService: BitcoinRpcService; + + let promiseSocket: PromiseSocket; + let stratumV1JobsService: StratumV1JobsService = new StratumV1JobsService(); + let bitcoinRpcService: MockBitcoinRpcService; let blockTemplateService: BlockTemplateService; let clientService: ClientService; let clientStatisticsService: ClientStatisticsService; @@ -25,10 +48,79 @@ describe('StratumV1Client', () => { let client: StratumV1Client; + let socketEmitter: (data: Buffer) => void; + + let newBlockEmitter: BehaviorSubject = new BehaviorSubject(null); + + let moduleRef: TestingModule; + + beforeAll(async () => { + moduleRef = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRoot({ + type: 'sqlite', + database: './DB/public-pool.test.sqlite', + synchronize: true, + autoLoadEntities: true, + cache: true, + logging: false + }), + ClientModule, + ClientStatisticsModule + ], + providers: [ + { + provide: ConfigService, + useValue: { + get: jest.fn((key: string) => { + switch (key) { + case 'DEV_FEE_ADDRESS': + return 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4'; + case 'NETWORK': + return 'testnet'; + } + return null; + }) + } + } + ], + }).compile(); + + + }) + beforeEach(async () => { - jest.spyOn(promiseSocket.socket, 'on').mockImplementation(); + + + clientService = moduleRef.get(ClientService); + + const dataSource = moduleRef.get(DataSource); + + dataSource.getRepository(ClientEntity).delete({}); + dataSource.getRepository(ClientStatisticsEntity).delete({}); + + + clientStatisticsService = moduleRef.get(ClientStatisticsService); + + configService = moduleRef.get(ConfigService); + + bitcoinRpcService = new MockBitcoinRpcService(null); + + jest.spyOn(bitcoinRpcService, 'getBlockTemplate').mockReturnValue(Promise.resolve(MockRecording1.BLOCK_TEMPLATE)); + bitcoinRpcService.newBlock$ = newBlockEmitter.asObservable(); + + blockTemplateService = new BlockTemplateService(bitcoinRpcService); + + + promiseSocket = new PromiseSocket(); + jest.spyOn(promiseSocket.socket, 'on').mockImplementation((event: string, fn: (data: Buffer) => void) => { + socketEmitter = fn; + }); + + promiseSocket.end = jest.fn(); + client = new StratumV1Client( promiseSocket, @@ -42,12 +134,126 @@ describe('StratumV1Client', () => { configService ); + client.extraNonceAndSessionId = MockRecording1.EXTRA_NONCE; + + jest.useFakeTimers({ advanceTimers: true }) + }); + + afterEach(async () => { + client.destroy(); + jest.useRealTimers(); + }) + + + it('should subscribe to socket', () => { + expect(promiseSocket.socket.on).toHaveBeenCalled(); + }); + + it('should close socket on invalid JSON', () => { + socketEmitter(Buffer.from('INVALID')); + expect(promiseSocket.end).toHaveBeenCalledTimes(1); + }); + + it('should respond to mining.subscribe', async () => { + jest.spyOn(promiseSocket, 'write').mockImplementation((data) => Promise.resolve(1)); + + expect(promiseSocket.socket.on).toHaveBeenCalled(); + socketEmitter(Buffer.from(MockRecording1.MINING_SUBSCRIBE)); + + await new Promise((r) => setTimeout(r, 1)); + + expect(promiseSocket.write).toHaveBeenCalledWith(`{"id":1,"error":null,"result":[[["mining.notify","${client.extraNonceAndSessionId}"]],"${client.extraNonceAndSessionId}",4]}\n`); }); - it('should parse message', () => { + it('should respond to mining.configure', async () => { + + jest.spyOn(promiseSocket, 'write').mockImplementation((data) => Promise.resolve(1)); + expect(promiseSocket.socket.on).toHaveBeenCalled(); + socketEmitter(Buffer.from(MockRecording1.MINING_CONFIGURE)); + await new Promise((r) => setTimeout(r, 1)); + expect(promiseSocket.write).toHaveBeenCalledWith(`{"id":2,"error":null,"result":{"version-rolling":true,"version-rolling.mask":"1fffe000"}}\n`); + }); + + it('should respond to mining.authorize', async () => { + + jest.spyOn(promiseSocket, 'write').mockImplementation((data) => Promise.resolve(1)); + + expect(promiseSocket.socket.on).toHaveBeenCalled(); + socketEmitter(Buffer.from(MockRecording1.MINING_AUTHORIZE)); + await new Promise((r) => setTimeout(r, 1)); + expect(promiseSocket.write).toHaveBeenCalledWith('{"id":3,"error":null,"result":true}\n'); + }); + + it('should respond to mining.suggest_difficulty', async () => { + jest.spyOn(promiseSocket, 'write').mockImplementation((data) => Promise.resolve(1)); + + expect(promiseSocket.socket.on).toHaveBeenCalled(); + socketEmitter(Buffer.from(MockRecording1.MINING_SUGGEST_DIFFICULTY)); + await new Promise((r) => setTimeout(r, 1)); + expect(promiseSocket.write).toHaveBeenCalledWith(`{"id":4,"method":"mining.set_difficulty","params":[512]}\n`); + }); + + it('should set difficulty', async () => { + jest.spyOn(promiseSocket, 'write').mockImplementation((data) => Promise.resolve(1)); + + socketEmitter(Buffer.from(MockRecording1.MINING_SUBSCRIBE)); + socketEmitter(Buffer.from(MockRecording1.MINING_AUTHORIZE)); + await new Promise((r) => setTimeout(r, 100)); + + expect(promiseSocket.write).toHaveBeenCalledWith(`{"id":null,"method":"mining.set_difficulty","params":[32768]}\n`); + + }); + + it('should save client', async () => { + jest.spyOn(promiseSocket, 'write').mockImplementation((data) => Promise.resolve(1)); + + socketEmitter(Buffer.from(MockRecording1.MINING_SUBSCRIBE)); + socketEmitter(Buffer.from(MockRecording1.MINING_AUTHORIZE)); + await new Promise((r) => setTimeout(r, 100)); + + const clientCount = await clientService.connectedClientCount(); + expect(clientCount).toBe(1); + + }); + + + + + it('should send job and accept submission', async () => { + + + + const date = new Date(parseInt(MockRecording1.TIME, 16) * 1000); + + + jest.setSystemTime(date); + + jest.spyOn(promiseSocket, 'write').mockImplementation((data) => Promise.resolve(1)); + + + socketEmitter(Buffer.from(MockRecording1.MINING_SUBSCRIBE)); + socketEmitter(Buffer.from(MockRecording1.MINING_SUGGEST_DIFFICULTY)); + socketEmitter(Buffer.from(MockRecording1.MINING_AUTHORIZE)); + + + + await new Promise((r) => setTimeout(r, 100)); + + + + + expect(promiseSocket.write).lastCalledWith(`{"id":null,"method":"mining.notify","params":["3","171592f223740e92d223f6e68bff25279af7ac4f2246451e0000000200000000","02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1903c943255c7075626c69632d706f6f6c5c","ffffffff037a90000000000000160014e6f22ca44dc800e9d049621a3b9a42c509f1c4bc3b0f250000000000160014e6f22ca44dc800e9d049621a3b9a42c509f1c4bc0000000000000000266a24aa21a9edbd3d1d916aa0b57326a2d88ebe1b68a1d7c48585f26d8335fe6a94b62755f64c00000000",["175335649d5e8746982969ec88f52e85ac9917106fba5468e699c8879ab974a1","d5644ab3e708c54cd68dc5aedc92b8d3037449687f92ec41ed6e37673d969d4a","5c9ec187517edc0698556cca5ce27e54c96acb014770599ed9df4d4937fbf2b0"],"20000000","192495f8","${MockRecording1.TIME}",false]}\n`); + + + socketEmitter(Buffer.from(MockRecording1.MINING_SUBMIT)); + + jest.useRealTimers(); + await new Promise((r) => setTimeout(r, 100)); + + }); diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index a2bed2d..4fe55ac 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -44,7 +44,7 @@ export class StratumV1Client extends EasyUnsubscribe { private sessionDifficulty: number = 32768; private entity: ClientEntity; - public extraNonce: string; + public extraNonceAndSessionId: string; constructor( public readonly promiseSocket: PromiseSocket, @@ -60,9 +60,9 @@ export class StratumV1Client extends EasyUnsubscribe { super(); this.statistics = new StratumV1ClientStatistics(this.clientStatisticsService); - this.extraNonce = this.getRandomHexString(); + this.extraNonceAndSessionId = this.getRandomHexString(); - console.log(`New client ID: : ${this.extraNonce}`); + console.log(`New client ID: : ${this.extraNonceAndSessionId}`); this.promiseSocket.socket.on('data', (data: Buffer) => { data.toString() @@ -81,7 +81,7 @@ export class StratumV1Client extends EasyUnsubscribe { private async handleMessage(message: string) { - console.log(`Received from ${this.extraNonce}`, message); + console.log(`Received from ${this.extraNonceAndSessionId}`, message); // Parse the message and check if it's the initial subscription message let parsedMessage = null; @@ -111,7 +111,7 @@ export class StratumV1Client extends EasyUnsubscribe { if (errors.length === 0) { this.clientSubscription = subscriptionMessage; - await this.promiseSocket.write(JSON.stringify(this.clientSubscription.response(this.extraNonce)) + '\n'); + await this.promiseSocket.write(JSON.stringify(this.clientSubscription.response(this.extraNonceAndSessionId)) + '\n'); } else { const err = new StratumErrorMessage( subscriptionMessage.id, @@ -257,16 +257,18 @@ export class StratumV1Client extends EasyUnsubscribe { && this.clientAuthorization != null && this.stratumInitialized == false) { + this.stratumInitialized = true; + if (this.clientSuggestedDifficulty == null) { console.log(`Setting difficulty to ${this.sessionDifficulty}`) const setDifficulty = JSON.stringify(new SuggestDifficulty().response(this.sessionDifficulty)); await this.promiseSocket.write(setDifficulty + '\n'); } - this.stratumInitialized = true; + this.entity = await this.clientService.save({ - sessionId: this.extraNonce, + sessionId: this.extraNonceAndSessionId, address: this.clientAuthorization.address, clientName: this.clientAuthorization.worker, startTime: new Date(), @@ -275,8 +277,13 @@ export class StratumV1Client extends EasyUnsubscribe { let lastIntervalCount = undefined; let skipNext = false; combineLatest([this.blockTemplateService.currentBlockTemplate$, interval(60000).pipe(startWith(-1))]) - .pipe(takeUntil(this.easyUnsubscribe)) + .pipe( + takeUntil(this.easyUnsubscribe) + ) .subscribe(async ([{ blockTemplate }, interValCount]) => { + + + let clearJobs = false; if (lastIntervalCount === interValCount) { clearJobs = true; @@ -303,7 +310,7 @@ export class StratumV1Client extends EasyUnsubscribe { private async sendNewMiningJob(blockTemplate: IBlockTemplate, clearJobs: boolean) { - const hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientAuthorization.address, this.clientAuthorization.worker, this.extraNonce); + const hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientAuthorization.address, this.clientAuthorization.worker, this.extraNonceAndSessionId); let payoutInformation; //10Th/s @@ -320,6 +327,7 @@ export class StratumV1Client extends EasyUnsubscribe { ]; } + const job = new MiningJob( this.configService.get('NETWORK') === 'mainnet' ? bitcoinjs.networks.bitcoin : bitcoinjs.networks.testnet, this.stratumV1JobsService.getNextId(), @@ -331,9 +339,14 @@ export class StratumV1Client extends EasyUnsubscribe { this.stratumV1JobsService.addJob(job, clearJobs); - await this.promiseSocket.write(job.response()); - console.log(`Sent new job to ${this.clientAuthorization.worker}.${this.extraNonce}. (clearJobs: ${clearJobs}, fee?: ${!noFee})`) + try { + await this.promiseSocket.write(job.response()); + } catch (e) { + await this.promiseSocket.end(); + } + + console.log(`Sent new job to ${this.clientAuthorization.worker}.${this.extraNonceAndSessionId}. (clearJobs: ${clearJobs}, fee?: ${!noFee})`) } @@ -354,14 +367,14 @@ export class StratumV1Client extends EasyUnsubscribe { const updatedJobBlock = job.copyAndUpdateBlock( parseInt(submission.versionMask, 16), parseInt(submission.nonce, 16), - this.extraNonce, + this.extraNonceAndSessionId, submission.extraNonce2, parseInt(submission.ntime, 16) ); const header = updatedJobBlock.toBuffer(true); const { submissionDifficulty, submissionHash } = this.calculateDifficulty(header); - console.log(`DIFF: ${submissionDifficulty} of ${this.sessionDifficulty} from ${this.clientAuthorization.worker + '.' + this.extraNonce}`); + console.log(`DIFF: ${submissionDifficulty} of ${this.sessionDifficulty} from ${this.clientAuthorization.worker + '.' + this.extraNonceAndSessionId}`); console.log(`Header: ${header.toString('hex')}`); if (submissionDifficulty >= this.sessionDifficulty) { @@ -374,7 +387,7 @@ export class StratumV1Client extends EasyUnsubscribe { height: job.blockTemplate.height, minerAddress: this.clientAuthorization.address, worker: this.clientAuthorization.worker, - sessionId: this.extraNonce, + sessionId: this.extraNonceAndSessionId, blockData: blockHex }); await this.notificationService.notifySubscribersBlockFound(this.clientAuthorization.address, job.blockTemplate.height, updatedJobBlock, result); @@ -382,6 +395,7 @@ export class StratumV1Client extends EasyUnsubscribe { try { await this.statistics.addSubmission(this.entity, submissionHash, this.sessionDifficulty); } catch (e) { + console.log(e); const err = new StratumErrorMessage( submission.id, eStratumErrorCode.DuplicateShare, @@ -392,7 +406,7 @@ export class StratumV1Client extends EasyUnsubscribe { } if (submissionDifficulty > this.entity.bestDifficulty) { - await this.clientService.updateBestDifficulty(this.extraNonce, submissionDifficulty); + await this.clientService.updateBestDifficulty(this.extraNonceAndSessionId, submissionDifficulty); this.entity.bestDifficulty = submissionDifficulty; } diff --git a/src/models/bitcoin-rpc/IBlockTemplate.ts b/src/models/bitcoin-rpc/IBlockTemplate.ts index 245f0e1..baed66d 100644 --- a/src/models/bitcoin-rpc/IBlockTemplate.ts +++ b/src/models/bitcoin-rpc/IBlockTemplate.ts @@ -14,13 +14,13 @@ export interface IBlockTemplate { vbavailable: { // (json object) set of pending, supported versionbit (BIP 9) softfork deployments rulename: number, // (numeric) identifies the bit number as indicating acceptance and readiness for the named softfork rule - }, + } | {}, vbrequired: number, // (numeric) bit mask of versionbits the server requires set in submissions previousblockhash: string, // (string) The hash of current highest block transactions: IBlockTemplateTx[], // (json array) contents of non-coinbase transactions that should be included in the next block coinbaseaux: { // (json object) data that should be included in the coinbase's scriptSig content key: string; //'hex', // (string) values must be in the coinbase (keys may be ignored) - }, + } | {}, coinbasevalue: number, // (numeric) maximum allowable input to coinbase transaction, including the generation award and transaction fees (in satoshis) longpollid: string, // (string) an id to include with a request to longpoll on an update to this template target: string, // (string) The hash target @@ -34,6 +34,6 @@ export interface IBlockTemplate { bits: string, // (string) compressed target of next block height: number, // (numeric) The height of the next block default_witness_commitment: string // (string, optional) a valid witness commitment for the unmodified block template - + capabilities: string[] } \ No newline at end of file diff --git a/src/services/bitcoin-rpc.service.ts b/src/services/bitcoin-rpc.service.ts index 31ef6e4..af78f9a 100644 --- a/src/services/bitcoin-rpc.service.ts +++ b/src/services/bitcoin-rpc.service.ts @@ -15,11 +15,11 @@ export class BitcoinRpcService { public newBlock$ = this._newBlock$.pipe(filter(block => block != null)); constructor(private readonly configService: ConfigService) { - const url = configService.get('BITCOIN_RPC_URL'); - const user = configService.get('BITCOIN_RPC_USER'); - const pass = configService.get('BITCOIN_RPC_PASSWORD'); - const port = parseInt(configService.get('BITCOIN_RPC_PORT')); - const timeout = parseInt(configService.get('BITCOIN_RPC_TIMEOUT')); + const url = this.configService.get('BITCOIN_RPC_URL'); + const user = this.configService.get('BITCOIN_RPC_USER'); + const pass = this.configService.get('BITCOIN_RPC_PASSWORD'); + const port = parseInt(this.configService.get('BITCOIN_RPC_PORT')); + const timeout = parseInt(this.configService.get('BITCOIN_RPC_TIMEOUT')); this.client = new RPCClient({ url, port, timeout, user, pass }); diff --git a/src/services/block-template.service.ts b/src/services/block-template.service.ts index 684cb74..0272f11 100644 --- a/src/services/block-template.service.ts +++ b/src/services/block-template.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { from, map, Observable, shareReplay, switchMap, tap } from 'rxjs'; +import { from, map, Observable, shareReplay, switchMap } from 'rxjs'; import { IBlockTemplate } from '../models/bitcoin-rpc/IBlockTemplate'; import { BitcoinRpcService } from './bitcoin-rpc.service'; @@ -7,14 +7,12 @@ import { BitcoinRpcService } from './bitcoin-rpc.service'; @Injectable() export class BlockTemplateService { - public currentBlockTemplate: IBlockTemplate; public currentBlockTemplate$: Observable<{ blockTemplate: IBlockTemplate }>; constructor(private readonly bitcoinRpcService: BitcoinRpcService) { this.currentBlockTemplate$ = this.bitcoinRpcService.newBlock$.pipe( switchMap((miningInfo) => from(this.bitcoinRpcService.getBlockTemplate()).pipe(map(blockTemplate => { return { miningInfo, blockTemplate } }))), - tap(({ blockTemplate }) => this.currentBlockTemplate = blockTemplate), shareReplay({ refCount: true, bufferSize: 1 }) ); } diff --git a/src/services/stratum-v1.service.ts b/src/services/stratum-v1.service.ts index 1a77b8c..06791d7 100644 --- a/src/services/stratum-v1.service.ts +++ b/src/services/stratum-v1.service.ts @@ -62,16 +62,20 @@ export class StratumV1Service implements OnModuleInit { promiseSocket.socket.on('end', async (error: Error) => { // Handle socket disconnection client.destroy(); - await this.clientService.delete(client.extraNonce); + promiseSocket.destroy(); + await this.clientService.delete(client.extraNonceAndSessionId); const clientCount = await this.clientService.connectedClientCount(); + console.log(`Client disconnected: ${promiseSocket.socket.remoteAddress}, ${clientCount} total clients`); }); promiseSocket.socket.on('error', async (error: Error) => { client.destroy(); - await this.clientService.delete(client.extraNonce); + promiseSocket.destroy(); + await this.clientService.delete(client.extraNonceAndSessionId); + const clientCount = await this.clientService.connectedClientCount(); console.error(`Socket error:`, error); console.log(`Client disconnected: ${promiseSocket.socket.remoteAddress}, ${clientCount} total clients`); diff --git a/test/models/MockRecording1.ts b/test/models/MockRecording1.ts new file mode 100644 index 0000000..861fe0e --- /dev/null +++ b/test/models/MockRecording1.ts @@ -0,0 +1,12 @@ +import { IBlockTemplate } from '../../src/models/bitcoin-rpc/IBlockTemplate'; + +export class MockRecording1 { + public static EXTRA_NONCE = `57a6f098`; + public static MINING_SUBSCRIBE = `{"id": 1, "method": "mining.subscribe", "params": ["bitaxe v2.2"]}`; + public static MINING_CONFIGURE = `{"id": 2, "method": "mining.configure", "params": [["version-rolling"], {"version-rolling.mask": "ffffffff"}]}`; + public static MINING_AUTHORIZE = `{"id": 3, "method": "mining.authorize", "params": ["tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4.bitaxe3", "x"]}`; + public static MINING_SUGGEST_DIFFICULTY = `{"id": 4, "method": "mining.suggest_difficulty", "params": [512]}`; + public static BLOCK_TEMPLATE: IBlockTemplate = { "capabilities": ["proposal"], "version": 536870912, "rules": ["csv", "!segwit", "taproot"], "vbavailable": {}, "vbrequired": 0, "previousblockhash": "00000000000000022246451e9af7ac4f8bff2527d223f6e623740e92171592f2", "transactions": [{ "data": "02000000000101c3efa41c94ee24dd6c6aaccab0f27e9d2baf92e5112bff15cc463b600e4e3a3c0100000000fdffffff024ea319000000000017a914a5b8d32a6388f02204a1445bd623150ee3b851d18765d67b000000000017a914a316e3a4eaeee9cbee9c254c3292830281a8c9c38702473044022065fbcd2918e8b52c3af8c4ae21b3646d7a12f66ddcaa73a68273f5806527c0da0220291f555dd120bb9eadb144ccf4203db659b78d79bffcd37a29016da068f3abb9012102baab4d464b57771f5108f40ffc31b7242c5b430a61d4bfead024d715c441f3f3c7432500", "txid": "a174b99a87c899e66854ba6f101799ac852ef588ec69299846875e9d64355317", "hash": "748f4db5e45dab6a9903f50e7e760470ce80ac219c637d61418667a0ccb6c536", "depends": [], "fee": 14300, "sigops": 1, "weight": 569 }, { "data": "01000000018985719caa78472c8fad7efa88fc109c6faf6df3dc3a690656479e53c6b10fe9030000006b483045022100e7abb2f2f7e6cfad680efe10a7fc218d685a9cce929b1a4e62fcb726228ac9b50220538f3d2eb36d277bcadf00f3ac2abbaf80395c389e1f8048dc95fdf6204cf1840121037435c194e9b01b3d7f7a2802d6684a3af68d05bbf4ec8f17021980d777691f1dfdffffff040000000000000000536a4c5054325b0b15aaff5c59aab47ce39829055c3007922e2934d3b7c9f51238f8d4a98718da4f9c99a6692adacd0e324b7775eebe151c4f3ad37fcc5f9303eed491a02638b8002543c70002002543a7000a4c10270000000000001976a914000000000000000000000000000000000000000088ac10270000000000001976a914000000000000000000000000000000000000000088aca99f6412000000001976a914ba27f99e007c7f605a8305e318c1abde3cd220ac88ac00000000", "txid": "c34ac3f822a0b49f298be1a25fc9e9dd375c23985ed2656f7cf7d87b67fee1c0", "hash": "c34ac3f822a0b49f298be1a25fc9e9dd375c23985ed2656f7cf7d87b67fee1c0", "depends": [], "fee": 9153, "sigops": 12, "weight": 1408 }, { "data": "020000000001013f8e5ebf5620f2c62c9c4e4f77bb4eb903699620da372ad44f666a552e5b9fc801000000171600148442ee1fe3742153a6b40623d363a22ceed677c3ffffffff02bc0200000000000017a91470b93cf7ead31ef24b71159388c2e8e147f755b987aa0415000000000017a9145506e665bcf63e1225100d7c8613a640699fd5d087024730440220215cd74141ebcde3aee383f232dfd2c6b011645012f35714781d28d4a6959829022017ea40a15f4116bbf0a6f86dc0af08758d9a74673498a61164ed13e9d1c712e501210294184e8093958861740c4ce32e724a24b7df7eeb04702cae274652e86b01f83a00000000", "txid": "5aaf2fa707da25c24374b87d89c420c9e4c51022a4bbed4cc91614831f359366", "hash": "2c19268e65cc223c5fac9666883ecc1d10e680c54a4fdbbb3e4775f886b0feec", "depends": [], "fee": 491, "sigops": 1, "weight": 661 }, { "data": "020000000001012c53f5b0a12cadb57602466e703e60272183b75f3366073f4841dac8e616f2740000000000feffffff02e80300000000000016001496e381bd3fe4bae631adc5a5a420fb9124e5480af0a8100000000000160014cd06fc96fea44c6c5e074ba16dd20b222a0a9b32024730440220440ba2252ea6f70f1b8106897984330a997ef504f7f0dbb89679e44309026f2b02207074ad7dbd9a2bd37a41352527b7f3e68612956fe2085a0eb0b41f97f6a3599d012103b3ecf0f44126019a94a818245e35ecff1582c2900a38487a306fcd91bdb28003c8432500", "txid": "69770cf296b64329e784a5666fb0eab034fbb1780bf067f4e1606874c214f01d", "hash": "fadcaa8de64274e63c093592ed6d8c6b7584bb92c0e8d9c49f02a4df11f6f69d", "depends": [], "fee": 141, "sigops": 1, "weight": 561 }, { "data": "01000000019e03cd78895f6876932501b66eab1e44611d48b4c5029d250b7d2d6073b44b68000000006b48304502210088dfbeb842d54893987e5afb87b70fe70e7b91ebad740782515c44464b19a74202205a6bc47ad6c925f85aa29b2be1a174efcc30a4ad8ed6cfe6236b66d7a4f8d96f01210259530ee281106e237ac3aa801d1f7699ada05501186401da78b7d88849bbda8bffffffff0204ce3900000000001976a91419803f0d83a1b58348a4617c743638bbf9ec235588ac44ce5c01000000001976a9149e38aebe02be7cecacdb521f514cffe445c5533788ac00000000", "txid": "2137ca428d6233e680cea25c428b984391314d872a1a0f5f7b8dc8cfc4474c44", "hash": "2137ca428d6233e680cea25c428b984391314d872a1a0f5f7b8dc8cfc4474c44", "depends": [], "fee": 226, "sigops": 8, "weight": 904 }], "coinbaseaux": {}, "coinbasevalue": 2465717, "longpollid": "00000000000000022246451e9af7ac4f8bff2527d223f6e623740e92171592f29370", "target": "000000000000002495f800000000000000000000000000000000000000000000", "mintime": 1689512405, "mutable": ["time", "transactions", "prevblock"], "noncerange": "00000000ffffffff", "sigoplimit": 80000, "sizelimit": 4000000, "weightlimit": 4000000, "curtime": 1689514989, "bits": "192495f8", "height": 2442185, "default_witness_commitment": "6a24aa21a9edbd3d1d916aa0b57326a2d88ebe1b68a1d7c48585f26d8335fe6a94b62755f64c" }; + public static MINING_SUBMIT = `{"id": 5, "method": "mining.submit", "params": ["tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4.bitaxe3", "1", "c7080000", "64b3f3ec", "ed460d91", "00002000"]}`; + public static TIME = '64b3f3ec'; +} \ No newline at end of file