From c4d768cd00d6286cbb7f1b56793104089fd40c13 Mon Sep 17 00:00:00 2001 From: Yggdrasil Date: Wed, 14 Oct 2026 00:17:36 -0400 Subject: [PATCH 1/3] fix(maintenance-pool): report the live balance in the exceeds-balance error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error read pool.balance fetched BEFORE the atomic conditional UPDATE — under the concurrency the guard exists for, it shows a stale (higher) balance. Re-read the live balance for the message so it reports the real current value. Resolves #608 --- src/maintenance-pool/maintenance-pool.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/maintenance-pool/maintenance-pool.service.ts b/src/maintenance-pool/maintenance-pool.service.ts index 13415a62..e649b77c 100644 --- a/src/maintenance-pool/maintenance-pool.service.ts +++ b/src/maintenance-pool/maintenance-pool.service.ts @@ -176,8 +176,17 @@ export class MaintenancePoolService { .execute(); if (updateResult.affected === 0) { + // `pool.balance` was read BEFORE the conditional UPDATE — under the + // concurrency this guard exists for (#274) it's stale and can read + // higher than the real balance. Re-read the live value so the error + // reports the actual current balance, not the pre-update one (#608). + const current = await this.poolRepo.findOne({ + where: { id }, + select: ['balance'], + }); + const balance = current?.balance ?? pool.balance; throw new BadRequestException( - `Requested reward ${amount} exceeds pool balance ${pool.balance}`, + `Requested reward ${amount} exceeds pool balance ${balance}`, ); } From 7ace851862aefeaee0640eb981a7ed8c52ab4142 Mon Sep 17 00:00:00 2001 From: yggdrasilworld Date: Sun, 18 Oct 2026 21:49:56 -0400 Subject: [PATCH 2/3] fix: repair merge glue that left the branch uncompilable The shared base carried half-written merge artifacts: an orphaned invokeOnLockedEscrow block in escrow.service.ts, duplicated returns plus a dead parsePagination in sponsors.controller.ts, and missing describe/it closers in github-webhooks.service.spec.ts. Aligned each with the intended form so the branch typechecks and its specs load. --- src/escrow/escrow.service.ts | 10 ++-------- src/github/github-webhooks.service.spec.ts | 3 +++ src/sponsors/sponsors.controller.ts | 21 --------------------- 3 files changed, 5 insertions(+), 29 deletions(-) diff --git a/src/escrow/escrow.service.ts b/src/escrow/escrow.service.ts index 6d2dd289..c252993f 100644 --- a/src/escrow/escrow.service.ts +++ b/src/escrow/escrow.service.ts @@ -584,20 +584,14 @@ export class EscrowService { recipients: Array<[string, number]>, manager?: EntityManager, ): Promise { - return this.invokeOnLockedEscrow(escrow, operation, () => - this.soroban.invoke( - 'release', - // `release(issue_id: u64, recipients)` — u64-typed on-chain (#301). - [u64(this.onChainKeyFor(escrow)), recipients], - this.contractOpts(escrow), - ), return this.invokeOnLockedEscrow( escrow, operation, () => this.soroban.invoke( 'release', - [this.onChainKeyFor(escrow), recipients], + // `release(issue_id: u64, recipients)` — u64-typed on-chain (#301). + [u64(this.onChainKeyFor(escrow)), recipients], this.contractOpts(escrow), ), manager, diff --git a/src/github/github-webhooks.service.spec.ts b/src/github/github-webhooks.service.spec.ts index 45bb6fe8..a35ac3be 100644 --- a/src/github/github-webhooks.service.spec.ts +++ b/src/github/github-webhooks.service.spec.ts @@ -869,6 +869,9 @@ describe('GithubWebhooksService', () => { expect(bountiesService.markInReview).not.toHaveBeenCalled(); expect(bountiesService.markMergedAndRelease).not.toHaveBeenCalled(); + }); + }); + // #308: a redelivered X-GitHub-Delivery used to hit the unique constraint // on webhook_events.deliveryId and escape handleEvent as a 500, so every // redelivery of that event failed forever. diff --git a/src/sponsors/sponsors.controller.ts b/src/sponsors/sponsors.controller.ts index b7a48cca..c097bf1e 100644 --- a/src/sponsors/sponsors.controller.ts +++ b/src/sponsors/sponsors.controller.ts @@ -26,25 +26,6 @@ function validatePage(limit?: number, offset?: number) { throw new BadRequestException('offset must be >= 0'); } return { limit, offset }; -/** - * Parse optional limit/offset query params, rejecting NaN, negatives, - * non-integers, and limits above MAX_PAGE_LIMIT with a 400 (#280). - */ -function parsePagination(limit?: string, offset?: string) { - const parse = (name: string, raw: string | undefined, min: number, max: number) => { - if (raw === undefined || raw === '') return undefined; - const n = Number(raw); - if (!Number.isInteger(n) || n < min || n > max) { - throw new BadRequestException( - `${name} must be an integer between ${min} and ${max}`, - ); - } - return n; - }; - return { - limit: parse('limit', limit, 1, MAX_PAGE_LIMIT), - offset: parse('offset', offset, 0, Number.MAX_SAFE_INTEGER), - }; } @ApiTags('sponsors') @@ -73,7 +54,6 @@ export class SponsorsController { ) { this.assertOwnsSponsor(user, id); return this.sponsorsService.dashboard(id, validatePage(limit, offset)); - return this.sponsorsService.dashboard(id, parsePagination(limit, offset)); } @ApiBearerAuth() @@ -91,6 +71,5 @@ export class SponsorsController { id, validatePage(limit, offset), ); - return this.sponsorsService.milestoneProgress(id, parsePagination(limit, offset)); } } From 674388bb5f49a1d70fd89701f2ad0e4ae3ff0715 Mon Sep 17 00:00:00 2001 From: Devin Date: Sun, 18 Oct 2026 22:09:38 -0400 Subject: [PATCH 3/3] repair stale specs to match shipped signatures/fixtures --- .../maintenance-pool.service.spec.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/maintenance-pool/maintenance-pool.service.spec.ts b/src/maintenance-pool/maintenance-pool.service.spec.ts index 88ff0c4a..87dc5a27 100644 --- a/src/maintenance-pool/maintenance-pool.service.spec.ts +++ b/src/maintenance-pool/maintenance-pool.service.spec.ts @@ -302,6 +302,7 @@ describe('MaintenancePoolService', () => { it('rejects when the pool has no funded escrow yet', async () => { poolRepo.findOne.mockResolvedValue({ id: 'pool-1', + status: MaintenancePoolStatus.ACTIVE, balance: '100', escrowId: null, }); @@ -315,6 +316,7 @@ describe('MaintenancePoolService', () => { it('rejects when the requested amount exceeds the pool balance', async () => { poolRepo.findOne.mockResolvedValue({ id: 'pool-1', + status: MaintenancePoolStatus.ACTIVE, balance: '50', escrowId: 'escrow-1', }); @@ -337,6 +339,7 @@ describe('MaintenancePoolService', () => { it('releases the reward and atomically decrements the balance', async () => { poolRepo.findOne.mockResolvedValue({ id: 'pool-1', + status: MaintenancePoolStatus.ACTIVE, balance: '100', escrowId: 'escrow-1', }); @@ -373,6 +376,7 @@ describe('MaintenancePoolService', () => { it('rejects a reward for a non-maintenance issue before releasing funds', async () => { poolRepo.findOne.mockResolvedValue({ id: 'pool-1', + status: MaintenancePoolStatus.ACTIVE, balance: '100', escrowId: 'escrow-1', }); @@ -391,6 +395,7 @@ describe('MaintenancePoolService', () => { it('rejects an issue outside the pool repository', async () => { poolRepo.findOne.mockResolvedValue({ id: 'pool-1', + status: MaintenancePoolStatus.ACTIVE, repositoryId: 'repository-1', balance: '100', escrowId: 'escrow-1', @@ -410,6 +415,7 @@ describe('MaintenancePoolService', () => { it('rejects when the issue has already received a reward from this pool (#273)', async () => { poolRepo.findOne.mockResolvedValue({ id: 'pool-1', + status: MaintenancePoolStatus.ACTIVE, balance: '100', escrowId: 'escrow-1', }); @@ -437,9 +443,14 @@ describe('MaintenancePoolService', () => { // time — not a value cached from an earlier read — and neither // decrement is lost. it('two concurrent assignReward calls both apply — no lost decrement (#51)', async () => { - const sharedPoolRow: { balance: string; escrowId: string } = { + const sharedPoolRow: { + balance: string; + escrowId: string; + status: MaintenancePoolStatus; + } = { balance: '1000.0000000', escrowId: 'escrow-1', + status: MaintenancePoolStatus.ACTIVE, }; poolRepo.findOne.mockImplementation(() => Promise.resolve({ id: 'pool-1', ...sharedPoolRow }),