|
| 1 | +import { describe, it, expect } from 'vitest' |
| 2 | + |
| 3 | +/** |
| 4 | + * The shortfall calculation, extracted as the pure decision it is. |
| 5 | + * |
| 6 | + * This exists because the first version of the check shipped a false negative |
| 7 | + * that stopped a real player buying: it read `listAccounts()[0]`, found an |
| 8 | + * empty account, and reported that somebody holding 4,937 NIM elsewhere had |
| 9 | + * nothing. The rules that prevent it coming back are pinned here. |
| 10 | + */ |
| 11 | + |
| 12 | +/** Mirrors the reduction in `useNimPayment.checkBalance`. */ |
| 13 | +function shortfallFrom(balances: (bigint | null)[], requiredLuna: bigint): bigint | null { |
| 14 | + const known = balances.filter((b): b is bigint => b !== null) |
| 15 | + if (known.length === 0) return null |
| 16 | + const richest = known.reduce((a, b) => (b > a ? b : a), 0n) |
| 17 | + return richest >= requiredLuna ? 0n : requiredLuna - richest |
| 18 | +} |
| 19 | + |
| 20 | +/** 58.79 NIM — the quote from the report that exposed the bug. */ |
| 21 | +const REQUIRED = 5_879_000n |
| 22 | +/** 4,936.65 NIM, the balance that was wrongly read as zero. */ |
| 23 | +const FUNDED = 493_665_000n |
| 24 | + |
| 25 | +describe('shortfall across a wallet with several accounts', () => { |
| 26 | + // The regression. The funded account is not first, which is precisely the |
| 27 | + // shape that produced "NOT ENOUGH NIM" for a wallet holding 84x the price. |
| 28 | + it('finds the money when the funded account is not the first one', () => { |
| 29 | + expect(shortfallFrom([0n, 0n, FUNDED], REQUIRED)).toBe(0n) |
| 30 | + }) |
| 31 | + |
| 32 | + it('is not fooled by the first account being empty', () => { |
| 33 | + expect(shortfallFrom([0n, FUNDED], REQUIRED)).toBe(0n) |
| 34 | + }) |
| 35 | + |
| 36 | + // A Nimiq transaction is funded by ONE address, so two accounts that each |
| 37 | + // hold half the price cannot pay it. Summing would wrongly say they can. |
| 38 | + it('does not add accounts together', () => { |
| 39 | + const half = REQUIRED / 2n |
| 40 | + expect(shortfallFrom([half, half], REQUIRED)).toBe(REQUIRED - half) |
| 41 | + }) |
| 42 | + |
| 43 | + it('reports the gap against the richest account, not the poorest', () => { |
| 44 | + expect(shortfallFrom([0n, 1_000_000n], REQUIRED)).toBe(REQUIRED - 1_000_000n) |
| 45 | + }) |
| 46 | + |
| 47 | + /* ---- "cannot tell" must never read as "has nothing" ------------------ */ |
| 48 | + |
| 49 | + it('returns null when every lookup failed', () => { |
| 50 | + expect(shortfallFrom([null, null], REQUIRED)).toBeNull() |
| 51 | + }) |
| 52 | + |
| 53 | + it('returns null when the wallet reported no accounts at all', () => { |
| 54 | + expect(shortfallFrom([], REQUIRED)).toBeNull() |
| 55 | + }) |
| 56 | + |
| 57 | + // A partial failure still answers from what is known, and the known account |
| 58 | + // covering the price is enough — the unknown one cannot make that false. |
| 59 | + it('answers from the accounts it could read', () => { |
| 60 | + expect(shortfallFrom([null, FUNDED], REQUIRED)).toBe(0n) |
| 61 | + }) |
| 62 | + |
| 63 | + // Control: the genuinely-broke wallet still reports a shortfall, so the |
| 64 | + // assertions above are the max working rather than the check being inert. |
| 65 | + it('control: a wallet that really is empty still reports the full amount', () => { |
| 66 | + expect(shortfallFrom([0n, 0n], REQUIRED)).toBe(REQUIRED) |
| 67 | + }) |
| 68 | + |
| 69 | + it('treats exactly enough as enough', () => { |
| 70 | + expect(shortfallFrom([REQUIRED], REQUIRED)).toBe(0n) |
| 71 | + }) |
| 72 | + |
| 73 | + it('reports one Luna short as one Luna short', () => { |
| 74 | + expect(shortfallFrom([REQUIRED - 1n], REQUIRED)).toBe(1n) |
| 75 | + }) |
| 76 | +}) |
0 commit comments