Skip to content

Batch: enforce max_group_num and apply initial_balance (WWT-140) (0.24.0) - #61

Merged
PeterRounce merged 1 commit into
mainfrom
peter/wwt-140-hub-batchs-ignores-max_group_num-and-initial_balance-a-batch
Jul 29, 2026
Merged

PeterRounce merged 1 commit into
mainfrom
peter/wwt-140-hub-batchs-ignores-max_group_num-and-initial_balance-a-batch

Conversation

@PeterRounce

Copy link
Copy Markdown
Member

Fixes WWT-140. Found while investigating WWT-138.

The bug

POST /batch?s=<secret> minted a card on every call with nothing bounding it. max_group_num and initial_balance were written to program_cards by ProgramBatch and the admin batch dialog, and then read by no code path at all — grep MaxGroupNum hit only the INSERT, the struct, the SELECT scan and a test.

So a batch link was an unlimited card printing press until its expiry, while the admin UI promised otherwise:

  • "Scan this same link once per card — up to N cards" — no cap existed.
  • A Balance (sats) field that did nothing.

Knock-on effects: every extra card row costs an AES decrypt on every tap (Find_card linearly scans all wiped='N' cards), and orphan rows carrying a batch's group_tag get funded by a later SetupCardAmountForTag, inflating cardLiabilitySat in the withdraw endpoint.

The fix

  • Schema v14 — program_cards.cards_issued, the dispensed-slot counter.
  • Db_dispense_batch_card (db/db_tx.go) claims a slot and inserts the card in a single BEGIN IMMEDIATE transaction, so two concurrent dispenses cannot both read a stale count and overshoot. max_group_num of 0 means no limit, matching the tx_limit_sats/day_limit_sats convention.
  • initial_balance is credited in that same transaction as a settled card_receipt (mirroring adminApiAllocateFunds), so a dispensed card is never visible with the wrong balance.
  • Errors are distinguishable: {"status": "ERROR", "reason": ...} with a 4xx — "batch limit reached" vs the existing "program card expired or not found".
  • Admin batch create rejects a negative initialBalance.

Behaviour changes to know about

  • Existing batches migrate with cards_issued = 0, so their allowance starts fresh. Issued counts can't be reconstructed — cards carry only a group_tag, and the tag is allowed to be empty (the batches on the test hub have empty tags).
  • A batch with maxCards=1 is now single-use. With WWT-138 still open, the app's preview POST claims that one slot — the write itself still succeeds (writeAgain writes the previewed keys), but Reset + re-paste now fails with "batch limit reached" instead of silently minting another card. That is the bug surfacing, not a regression.
  • The BCP app currently renders only Server returned 400 Bad Request on a non-2xx — it ignores the body. A one-line app change (read reason when !response.ok) would surface these messages; worth doing alongside the WWT-138 fix.

Testing

  • 7 new DB tests: dispense/count, cap enforcement, 0 = unlimited, initial balance credited, no receipt at zero balance, expired, unknown secret.
  • 3 new web tests: exhausted batch returns 400 and mints exactly one card, initial balance lands on the card, negative initialBalance rejected.
  • go vet ./... clean, full suite green under -race, admin UI builds.
  • Migration verified against a copy of the deployed test-hub database (schema 13, 20 program_cards rows): migrates to 14, rows preserved, counters start at 0.

Not included

Issue item 3 (surfacing issued/remaining on the admin cards page) — the enforcement is server-side and complete without it; happy to add if you want the batch state visible.

🤖 Generated with Claude Code

…4.0)

POST /batch?s=<secret> minted a card on every call with nothing bounding
it: max_group_num and initial_balance were written to program_cards by the
CLI and the admin dialog and then never read by any code path. A batch link
was an unlimited card printing press until its expiry, and the admin UI's
"Scan this same link once per card - up to N cards" and "Balance (sats)"
both promised behaviour the hub did not have.

Every extra card row also costs an AES decrypt on every tap (Find_card
scans all non-wiped cards), and orphans carrying a batch's group_tag get
funded by a later SetupCardAmountForTag, inflating cardLiabilitySat.

- schema v14: program_cards.cards_issued, the dispensed-slot counter
- Db_dispense_batch_card (db/db_tx.go) claims a slot and inserts the card
  in one BEGIN IMMEDIATE transaction, so concurrent dispenses cannot both
  read a stale count and overshoot the limit. max_group_num 0 = no limit,
  matching the tx/day limit convention
- initial_balance is credited in the same transaction as a settled
  card_receipt, mirroring adminApiAllocateFunds, so a dispensed card is
  never visible with the wrong balance
- errors reply {"status": "ERROR", "reason": ...} with a 4xx, so a client
  can tell "batch limit reached" from "program card expired or not found"
- admin batch create rejects a negative initialBalance

Existing batches migrate with cards_issued = 0, so their allowance starts
fresh; issued counts cannot be reconstructed (cards carry a group_tag, and
the tag is allowed to be empty).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PeterRounce
PeterRounce merged commit a56b04a into main Jul 29, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant