diff --git a/__tests__/proofReservation.test.ts b/__tests__/proofReservation.test.ts index 8e72ad79..ef4b92e9 100644 --- a/__tests__/proofReservation.test.ts +++ b/__tests__/proofReservation.test.ts @@ -366,6 +366,34 @@ describe('Proof reservations', () => { // Restored to PENDING (its original locked state), not to UNSPENT expect(getProofState(db, 'sA')).toBe('PENDING') }) + + test('never un-spends a proof that went SPENT while locked', () => { + const db = freshDb() + insertProof(db, 'sA', 100, 'UNSPENT') + insertProof(db, 'sB', 200, 'UNSPENT') + + const lockedProofs: LockedProofSnapshot[] = [ + {secret: 'sA', originalState: 'UNSPENT', originalTId: 7}, + {secret: 'sB', originalState: 'UNSPENT', originalTId: 7}, + ] + openReservation( + db, + {id: 'r7', transactionId: 14, mintUrl: MINT, unit: 'sat', operationType: 'transfer-swap', lockedProofs}, + ['sA', 'sB'], + ) + + // While locked, a sync learns the mint consumed sA (e.g. a swap whose + // response was lost) and marks it SPENT. + Database.getInstance().execute(`UPDATE proofs SET state = 'SPENT' WHERE secret = 'sA'`) + + rollbackReservation(db, 'r7', lockedProofs) + + expect(getProofState(db, 'sA')).toBe('SPENT') + expect(getProofTId(db, 'sA')).toBe(14) + expect(getProofState(db, 'sB')).toBe('UNSPENT') + expect(getProofTId(db, 'sB')).toBe(7) + expect(reservationCount(db)).toBe(0) + }) }) describe('orphan recovery', () => { diff --git a/src/services/db/reservationsRepo.ts b/src/services/db/reservationsRepo.ts index 951c35d1..28525ec2 100644 --- a/src/services/db/reservationsRepo.ts +++ b/src/services/db/reservationsRepo.ts @@ -303,9 +303,12 @@ export const rollbackReservation = function ( const now = new Date().toISOString() const batch: SQLBatchTuple[] = [] + // A proof that went SPENT while locked (sync saw the mint spend it) is never + // restored: un-spending it would put ecash the mint has already consumed back + // into the balance. Matches the MST mirror, which skips SPENT nodes. for (const snap of lockedProofs) { batch.push([ - `UPDATE proofs SET state = ?, tId = ?, updatedAt = ? WHERE secret = ?`, + `UPDATE proofs SET state = ?, tId = ?, updatedAt = ? WHERE secret = ? AND state != 'SPENT'`, [snap.originalState, snap.originalTId, now, snap.secret], ]) }