mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
Add payout modes and production switchover schema
This commit is contained in:
@@ -16,6 +16,7 @@ describe('PayoutSnapshotService', () => {
|
||||
process.env.PAYOUT_SNAPSHOT_ENABLED = 'true';
|
||||
process.env.PAYOUT_METHOD = 'pplns';
|
||||
process.env.PAYOUT_WINDOW_FACTOR = '4';
|
||||
process.env.PAYOUT_BOOTSTRAP_WINDOW = 'false';
|
||||
process.env.PAYOUT_MAX_COINBASE_OUTPUTS = '10';
|
||||
process.env.PAYOUT_MIN_OUTPUT_SATS = '546';
|
||||
process.env.PAYOUT_FEE_PERCENT = '0';
|
||||
@@ -62,12 +63,13 @@ describe('PayoutSnapshotService', () => {
|
||||
totalCreditedDifficulty: 100,
|
||||
totalAcceptedShareCount: '5',
|
||||
eligibleAddressCount: 2,
|
||||
includedOutputCount: 1,
|
||||
includedOutputCount: 2,
|
||||
distributedSats: '1000',
|
||||
unallocatedRemainderSats: '0',
|
||||
}])
|
||||
.mockResolvedValueOnce([
|
||||
{ address: ADDRESS_A, payoutSats: '1000' },
|
||||
{ address: ADDRESS_A, payoutSats: '600' },
|
||||
{ address: ADDRESS_B, payoutSats: '400' },
|
||||
]);
|
||||
|
||||
const snapshot = await service.createSnapshotForTemplate({
|
||||
@@ -86,10 +88,67 @@ describe('PayoutSnapshotService', () => {
|
||||
unallocatedRemainderSats: '0',
|
||||
}));
|
||||
expect(snapshot.payoutOutputs).toEqual([
|
||||
{ address: ADDRESS_A, amountSats: 1000, percent: 100 },
|
||||
{ address: ADDRESS_A, amountSats: 600, percent: 60 },
|
||||
{ address: ADDRESS_B, amountSats: 400, percent: 40 },
|
||||
]);
|
||||
expect(manager.query).toHaveBeenCalledTimes(9);
|
||||
expect(manager.query.mock.calls[0][1]).toEqual([100]);
|
||||
expect(manager.query.mock.calls[0][1]).toEqual([100, 'pplns']);
|
||||
});
|
||||
|
||||
it('should bootstrap the PPLNS window from paid block count up to the configured factor', async () => {
|
||||
process.env.PAYOUT_BOOTSTRAP_WINDOW = 'true';
|
||||
service = new PayoutSnapshotService(dataSource as unknown as DataSource);
|
||||
manager.query
|
||||
.mockResolvedValueOnce([{ paidBlockCount: 1 }])
|
||||
.mockResolvedValueOnce([{
|
||||
startBatchId: '10',
|
||||
endBatchId: '12',
|
||||
windowStartShareIndex: '1000',
|
||||
windowEndShareIndex: '2000',
|
||||
totalCreditedDifficulty: '50',
|
||||
totalAcceptedShareCount: '3',
|
||||
}])
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([
|
||||
{ address: ADDRESS_A, creditedDifficulty: 30, acceptedShareCount: 2 },
|
||||
{ address: ADDRESS_B, creditedDifficulty: 20, acceptedShareCount: 1 },
|
||||
])
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([{ id: '56' }])
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([{
|
||||
id: '56',
|
||||
method: 'pplns',
|
||||
blockHeight: 900001,
|
||||
coinbaseValueSats: '1000',
|
||||
windowStartShareIndex: '1000',
|
||||
windowEndShareIndex: '2000',
|
||||
totalCreditedDifficulty: 50,
|
||||
totalAcceptedShareCount: '3',
|
||||
eligibleAddressCount: 2,
|
||||
includedOutputCount: 2,
|
||||
distributedSats: '1000',
|
||||
unallocatedRemainderSats: '0',
|
||||
}])
|
||||
.mockResolvedValueOnce([
|
||||
{ address: ADDRESS_A, payoutSats: '600' },
|
||||
{ address: ADDRESS_B, payoutSats: '400' },
|
||||
]);
|
||||
|
||||
await expect(service.createSnapshotForTemplate({
|
||||
blockHeight: 900001,
|
||||
coinbaseValueSats: 1000,
|
||||
networkDifficulty: 25,
|
||||
})).resolves.toEqual(expect.objectContaining({
|
||||
id: '56',
|
||||
totalCreditedDifficulty: 50,
|
||||
}));
|
||||
|
||||
expect(manager.query.mock.calls[0][0]).toContain('COUNT(DISTINCT "blockHeight")');
|
||||
expect(manager.query.mock.calls[1][1]).toEqual([50, 'pplns']);
|
||||
expect(manager.query.mock.calls[5][1][5]).toBe(50);
|
||||
expect(manager.query.mock.calls[5][1][6]).toBe(2);
|
||||
});
|
||||
|
||||
it('should not create a snapshot when there is no finalized rollup work', async () => {
|
||||
@@ -131,12 +190,13 @@ describe('PayoutSnapshotService', () => {
|
||||
totalCreditedDifficulty: 100,
|
||||
totalAcceptedShareCount: '5',
|
||||
eligibleAddressCount: 2,
|
||||
includedOutputCount: 1,
|
||||
includedOutputCount: 2,
|
||||
distributedSats: '1000',
|
||||
unallocatedRemainderSats: '0',
|
||||
}])
|
||||
.mockResolvedValueOnce([
|
||||
{ address: ADDRESS_A, payoutSats: '1000' },
|
||||
{ address: ADDRESS_A, payoutSats: '600' },
|
||||
{ address: ADDRESS_B, payoutSats: '400' },
|
||||
]);
|
||||
|
||||
const snapshot = await service.createSnapshotForTemplate({
|
||||
@@ -147,7 +207,8 @@ describe('PayoutSnapshotService', () => {
|
||||
|
||||
expect(snapshot.id).toBe('55');
|
||||
expect(snapshot.payoutOutputs).toEqual([
|
||||
{ address: ADDRESS_A, amountSats: 1000, percent: 100 },
|
||||
{ address: ADDRESS_A, amountSats: 600, percent: 60 },
|
||||
{ address: ADDRESS_B, amountSats: 400, percent: 40 },
|
||||
]);
|
||||
expect(manager.query).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
@@ -159,21 +220,21 @@ describe('PayoutSnapshotService', () => {
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
address: ADDRESS_A,
|
||||
payoutSats: '1000',
|
||||
payoutSats: '600',
|
||||
balanceBeforeSats: '0',
|
||||
balanceAfterSats: '-400',
|
||||
balanceAfterSats: '0',
|
||||
creditedDifficulty: 60,
|
||||
includedInCoinbase: true,
|
||||
rowType: 'coinbase',
|
||||
},
|
||||
{
|
||||
address: ADDRESS_B,
|
||||
payoutSats: '0',
|
||||
payoutSats: '400',
|
||||
balanceBeforeSats: '0',
|
||||
balanceAfterSats: '400',
|
||||
balanceAfterSats: '0',
|
||||
creditedDifficulty: 40,
|
||||
includedInCoinbase: false,
|
||||
rowType: 'pending',
|
||||
includedInCoinbase: true,
|
||||
rowType: 'coinbase',
|
||||
},
|
||||
])
|
||||
.mockResolvedValueOnce([])
|
||||
@@ -210,4 +271,45 @@ describe('PayoutSnapshotService', () => {
|
||||
});
|
||||
expect(manager.query).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should return the latest current expected PPLNS payout for an address', async () => {
|
||||
dataSource.query.mockResolvedValueOnce([{
|
||||
snapshotId: '55',
|
||||
blockHeight: 900000,
|
||||
coinbaseValueSats: '1000',
|
||||
distributedSats: '1000',
|
||||
totalCreditedDifficulty: '100',
|
||||
includedOutputCount: 2,
|
||||
createdAt: new Date('2026-06-16T12:00:00.000Z'),
|
||||
payoutMode: 'pplns',
|
||||
payoutSats: '600',
|
||||
grossPayoutSats: '600',
|
||||
creditedDifficulty: '60',
|
||||
payoutWeight: '0.6',
|
||||
}]);
|
||||
|
||||
await expect(service.getLatestExpectedPayoutForAddress(ADDRESS_A)).resolves.toEqual({
|
||||
snapshotId: '55',
|
||||
blockHeight: 900000,
|
||||
payoutMode: 'pplns',
|
||||
payoutSats: 600,
|
||||
grossPayoutSats: 600,
|
||||
creditedDifficulty: 60,
|
||||
payoutWeight: 0.6,
|
||||
coinbaseValueSats: 1000,
|
||||
distributedSats: 1000,
|
||||
totalCreditedDifficulty: 100,
|
||||
includedOutputCount: 2,
|
||||
createdAt: new Date('2026-06-16T12:00:00.000Z'),
|
||||
percent: 60,
|
||||
});
|
||||
expect(dataSource.query.mock.calls[0][1]).toEqual([ADDRESS_A, 'pplns']);
|
||||
expect(dataSource.query.mock.calls[0][0]).toContain('WITH latest_snapshot AS');
|
||||
});
|
||||
|
||||
it('should return null when the latest PPLNS snapshot does not pay the address', async () => {
|
||||
dataSource.query.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(service.getLatestExpectedPayoutForAddress(ADDRESS_A)).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user