Repository navigation
Batch: enforce max_group_num and apply initial_balance (WWT-140) (0.24.0) - #61
Merged
PeterRounce merged 1 commit intoJul 29, 2026
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_numandinitial_balancewere written toprogram_cardsbyProgramBatchand the admin batch dialog, and then read by no code path at all —grep MaxGroupNumhit 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:
Knock-on effects: every extra card row costs an AES decrypt on every tap (
Find_cardlinearly scans allwiped='N'cards), and orphan rows carrying a batch'sgroup_tagget funded by a laterSetupCardAmountForTag, inflatingcardLiabilitySatin the withdraw endpoint.The fix
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 singleBEGIN IMMEDIATEtransaction, so two concurrent dispenses cannot both read a stale count and overshoot.max_group_numof0means no limit, matching thetx_limit_sats/day_limit_satsconvention.initial_balanceis credited in that same transaction as a settledcard_receipt(mirroringadminApiAllocateFunds), so a dispensed card is never visible with the wrong balance.{"status": "ERROR", "reason": ...}with a 4xx —"batch limit reached"vs the existing"program card expired or not found".initialBalance.Behaviour changes to know about
cards_issued = 0, so their allowance starts fresh. Issued counts can't be reconstructed — cards carry only agroup_tag, and the tag is allowed to be empty (the batches on the test hub have empty tags).maxCards=1is now single-use. With WWT-138 still open, the app's preview POST claims that one slot — the write itself still succeeds (writeAgainwrites 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.Server returned 400 Bad Requeston a non-2xx — it ignores the body. A one-line app change (readreasonwhen!response.ok) would surface these messages; worth doing alongside the WWT-138 fix.Testing
0= unlimited, initial balance credited, no receipt at zero balance, expired, unknown secret.initialBalancerejected.go vet ./...clean, full suite green under-race, admin UI builds.program_cardsrows): 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