mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 17:15:03 -07:00
Add payout modes and production switchover schema
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
import { PayoutMode } from '../../types/payout-mode';
|
||||
import { PrimaryGeneratedBigIntColumn } from '../utils/PrimaryGeneratedBigIntColumn';
|
||||
import { TrackedEntity } from '../utils/TrackedEntity.entity';
|
||||
|
||||
@@ -18,6 +19,9 @@ export class BlocksEntity extends TrackedEntity {
|
||||
@Column()
|
||||
worker: string;
|
||||
|
||||
@Column({ length: 16, type: 'varchar', default: 'solo' })
|
||||
payoutMode: PayoutMode;
|
||||
|
||||
@Column({ length: 8, type: 'varchar' })
|
||||
sessionId: string;
|
||||
|
||||
@@ -31,6 +35,6 @@ export class BlocksEntity extends TrackedEntity {
|
||||
blockSubmissionResult?: string;
|
||||
|
||||
@Column({ type: 'bigint', nullable: true })
|
||||
payoutSnapshotId?: string;
|
||||
payoutSnapshotId?: string | null;
|
||||
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ describe('BlocksService', () => {
|
||||
id: 9,
|
||||
blockSubmissionResult: 'SUCCESS!',
|
||||
payoutSnapshotId: '7',
|
||||
minerAddress: 'tb1qminer',
|
||||
worker: 'worker',
|
||||
sessionId: 'abcd1234',
|
||||
});
|
||||
const service = new BlocksService({} as any, repository as any);
|
||||
|
||||
@@ -43,6 +46,88 @@ describe('BlocksService', () => {
|
||||
expect(repository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps real miner attribution when a duplicate TDP submit arrives', async () => {
|
||||
const repository = createRepository();
|
||||
repository.findOne.mockResolvedValue({
|
||||
id: 9,
|
||||
blockSubmissionResult: 'SUCCESS!',
|
||||
payoutSnapshotId: '7',
|
||||
minerAddress: 'tb1qminer',
|
||||
worker: 'sv2gateway',
|
||||
sessionId: 'abcd1234',
|
||||
});
|
||||
const service = new BlocksService({} as any, repository as any);
|
||||
|
||||
await service.save({
|
||||
blockData: createBlockHex(),
|
||||
minerAddress: 'tb1qpool',
|
||||
worker: 'tdp',
|
||||
sessionId: '00000010',
|
||||
blockSubmissionResult: 'SUCCESS!',
|
||||
payoutSnapshotId: '8',
|
||||
});
|
||||
|
||||
expect(repository.update).toHaveBeenCalledWith(9, {
|
||||
blockSubmissionResult: 'SUCCESS!',
|
||||
payoutSnapshotId: '8',
|
||||
});
|
||||
expect(repository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clears payout snapshot metadata when a duplicate successful solo submit arrives', async () => {
|
||||
const repository = createRepository();
|
||||
repository.findOne.mockResolvedValue({
|
||||
id: 9,
|
||||
blockSubmissionResult: 'SUCCESS!',
|
||||
payoutSnapshotId: '7',
|
||||
minerAddress: 'tb1qminer',
|
||||
worker: 'worker',
|
||||
sessionId: 'abcd1234',
|
||||
});
|
||||
const service = new BlocksService({} as any, repository as any);
|
||||
|
||||
await service.save({
|
||||
blockData: createBlockHex(),
|
||||
blockSubmissionResult: 'SUCCESS!',
|
||||
payoutSnapshotId: null,
|
||||
});
|
||||
|
||||
expect(repository.update).toHaveBeenCalledWith(9, {
|
||||
blockSubmissionResult: 'SUCCESS!',
|
||||
payoutSnapshotId: null,
|
||||
});
|
||||
expect(repository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('repairs internal TDP attribution when a duplicate real miner submit arrives', async () => {
|
||||
const repository = createRepository();
|
||||
repository.findOne.mockResolvedValue({
|
||||
id: 9,
|
||||
blockSubmissionResult: 'SUCCESS!',
|
||||
payoutSnapshotId: '7',
|
||||
minerAddress: 'tb1qpool',
|
||||
worker: 'tdp',
|
||||
sessionId: '00000010',
|
||||
});
|
||||
const service = new BlocksService({} as any, repository as any);
|
||||
|
||||
await service.save({
|
||||
blockData: createBlockHex(),
|
||||
minerAddress: 'tb1qminer',
|
||||
worker: 'sv2gateway',
|
||||
sessionId: 'abcd1234',
|
||||
blockSubmissionResult: 'duplicate',
|
||||
payoutSnapshotId: '8',
|
||||
});
|
||||
|
||||
expect(repository.update).toHaveBeenCalledWith(9, {
|
||||
minerAddress: 'tb1qminer',
|
||||
worker: 'sv2gateway',
|
||||
sessionId: 'abcd1234',
|
||||
});
|
||||
expect(repository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns one public found-block row per block hash or legacy height', async () => {
|
||||
const repository = createRepository();
|
||||
repository.find.mockResolvedValue([
|
||||
@@ -57,6 +142,50 @@ describe('BlocksService', () => {
|
||||
{ height: 124, minerAddress: 'addr2', worker: 'b', sessionId: '2222', blockHash: 'hash-b' },
|
||||
{ height: 123, minerAddress: 'addr1', worker: 'a', sessionId: '1111', blockHash: null },
|
||||
]);
|
||||
expect(repository.find).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: expect.arrayContaining([
|
||||
expect.objectContaining({ blockSubmissionResult: expect.objectContaining({ _value: expect.arrayContaining(['SUCCESS!', 'datum-gateway-submit-expected']) }) }),
|
||||
expect.objectContaining({ blockSubmissionResult: expect.objectContaining({ _type: 'isNull' }) }),
|
||||
]),
|
||||
}));
|
||||
});
|
||||
|
||||
it('includes DATUM gateway-submitted candidates for address found-block lookups', async () => {
|
||||
const repository = createRepository();
|
||||
repository.find.mockResolvedValue([
|
||||
{
|
||||
height: 4991348,
|
||||
minerAddress: 'tb1qminer',
|
||||
worker: 'datum',
|
||||
sessionId: '28ff7236',
|
||||
blockHash: 'hash-datum',
|
||||
blockSubmissionResult: 'datum-gateway-submit-expected',
|
||||
},
|
||||
]);
|
||||
const service = new BlocksService({} as any, repository as any);
|
||||
|
||||
await expect(service.getFoundBlocksByAddress('tb1qminer')).resolves.toEqual([
|
||||
{
|
||||
height: 4991348,
|
||||
minerAddress: 'tb1qminer',
|
||||
worker: 'datum',
|
||||
sessionId: '28ff7236',
|
||||
blockHash: 'hash-datum',
|
||||
blockSubmissionResult: 'datum-gateway-submit-expected',
|
||||
},
|
||||
]);
|
||||
expect(repository.find).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
minerAddress: 'tb1qminer',
|
||||
blockSubmissionResult: expect.objectContaining({ _value: expect.arrayContaining(['SUCCESS!', 'datum-gateway-submit-expected']) }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
minerAddress: 'tb1qminer',
|
||||
blockSubmissionResult: expect.objectContaining({ _type: 'isNull' }),
|
||||
}),
|
||||
]),
|
||||
}));
|
||||
});
|
||||
|
||||
function createRepository() {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { DataSource, IsNull, Repository } from 'typeorm';
|
||||
import { DataSource, In, IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { BlocksEntity } from './blocks.entity';
|
||||
import { PayoutMode } from '../../types/payout-mode';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class BlocksService {
|
||||
|
||||
private static readonly PUBLIC_FOUND_BLOCK_RESULTS = ['SUCCESS!', 'datum-gateway-submit-expected'];
|
||||
|
||||
constructor(
|
||||
|
||||
private dataSource: DataSource,
|
||||
@@ -24,15 +27,35 @@ export class BlocksService {
|
||||
const existing = blockHash == null
|
||||
? null
|
||||
: await this.blocksRepository.findOne({
|
||||
select: { id: true, blockSubmissionResult: true, payoutSnapshotId: true },
|
||||
select: {
|
||||
id: true,
|
||||
blockSubmissionResult: true,
|
||||
payoutSnapshotId: true,
|
||||
minerAddress: true,
|
||||
worker: true,
|
||||
sessionId: true,
|
||||
payoutMode: true,
|
||||
},
|
||||
where: { blockHash },
|
||||
});
|
||||
if (existing != null) {
|
||||
const update: Partial<BlocksEntity> = {};
|
||||
if (this.isSuccessfulBlockSubmission(block.blockSubmissionResult)) {
|
||||
await this.blocksRepository.update(existing.id, {
|
||||
blockSubmissionResult: 'SUCCESS!',
|
||||
payoutSnapshotId: block.payoutSnapshotId ?? existing.payoutSnapshotId,
|
||||
});
|
||||
update.blockSubmissionResult = 'SUCCESS!';
|
||||
update.payoutSnapshotId = Object.prototype.hasOwnProperty.call(block, 'payoutSnapshotId')
|
||||
? block.payoutSnapshotId ?? null
|
||||
: existing.payoutSnapshotId;
|
||||
}
|
||||
if (this.shouldReplaceAttribution(existing, block)) {
|
||||
update.minerAddress = block.minerAddress;
|
||||
update.worker = block.worker;
|
||||
update.sessionId = block.sessionId;
|
||||
}
|
||||
if (block.payoutMode != null && existing.payoutMode !== block.payoutMode) {
|
||||
update.payoutMode = block.payoutMode;
|
||||
}
|
||||
if (Object.keys(update).length > 0) {
|
||||
await this.blocksRepository.update(existing.id, update);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -43,20 +66,21 @@ export class BlocksService {
|
||||
});
|
||||
}
|
||||
|
||||
public async getFoundBlocks() {
|
||||
public async getFoundBlocks(payoutMode?: PayoutMode) {
|
||||
const rows = await this.blocksRepository.find({
|
||||
select: {
|
||||
height: true,
|
||||
minerAddress: true,
|
||||
worker: true,
|
||||
sessionId: true,
|
||||
payoutMode: true,
|
||||
blockHash: true,
|
||||
blockSubmissionResult: true,
|
||||
createdAt: true,
|
||||
},
|
||||
where: [
|
||||
{ blockSubmissionResult: 'SUCCESS!' },
|
||||
{ blockSubmissionResult: IsNull() },
|
||||
{ ...(payoutMode == null ? {} : { payoutMode }), blockSubmissionResult: In(BlocksService.PUBLIC_FOUND_BLOCK_RESULTS) },
|
||||
{ ...(payoutMode == null ? {} : { payoutMode }), blockSubmissionResult: IsNull() },
|
||||
],
|
||||
order: {
|
||||
height: 'DESC',
|
||||
@@ -66,20 +90,21 @@ export class BlocksService {
|
||||
return this.uniqueFoundBlocks(rows);
|
||||
}
|
||||
|
||||
public async getFoundBlocksByAddress(address: string) {
|
||||
public async getFoundBlocksByAddress(address: string, payoutMode?: PayoutMode) {
|
||||
const rows = await this.blocksRepository.find({
|
||||
select: {
|
||||
height: true,
|
||||
minerAddress: true,
|
||||
worker: true,
|
||||
sessionId: true,
|
||||
payoutMode: true,
|
||||
blockHash: true,
|
||||
blockSubmissionResult: true,
|
||||
createdAt: true,
|
||||
},
|
||||
where: [
|
||||
{ minerAddress: address, blockSubmissionResult: 'SUCCESS!' },
|
||||
{ minerAddress: address, blockSubmissionResult: IsNull() },
|
||||
{ minerAddress: address, ...(payoutMode == null ? {} : { payoutMode }), blockSubmissionResult: In(BlocksService.PUBLIC_FOUND_BLOCK_RESULTS) },
|
||||
{ minerAddress: address, ...(payoutMode == null ? {} : { payoutMode }), blockSubmissionResult: IsNull() },
|
||||
],
|
||||
order: {
|
||||
height: 'DESC',
|
||||
@@ -122,4 +147,19 @@ export class BlocksService {
|
||||
private isSuccessfulBlockSubmission(result?: string | null): boolean {
|
||||
return result == null || result === 'SUCCESS!';
|
||||
}
|
||||
|
||||
private shouldReplaceAttribution(existing: Partial<BlocksEntity>, incoming: Partial<BlocksEntity>): boolean {
|
||||
if (!incoming.minerAddress || !incoming.worker) {
|
||||
return false;
|
||||
}
|
||||
if (this.isInternalAttribution(incoming)) {
|
||||
return false;
|
||||
}
|
||||
return this.isInternalAttribution(existing);
|
||||
}
|
||||
|
||||
private isInternalAttribution(block: Partial<BlocksEntity>): boolean {
|
||||
const worker = block.worker?.toLowerCase();
|
||||
return worker === 'tdp' || block.minerAddress === 'sv2-tdp';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user