feat: PIN limit for BoltCard withdrawals (LUD-XX pinLimit)#67
Open
AxelHamburch wants to merge 8 commits into
Open
feat: PIN limit for BoltCard withdrawals (LUD-XX pinLimit)#67AxelHamburch wants to merge 8 commits into
AxelHamburch wants to merge 8 commits into
Conversation
Implements optional PIN protection per draft LUD-XX spec: - migrations: add pin_limit, pin to cards; pin_attempts to hits - models: extend Card, CreateCardData, Hit with PIN fields - crud: hash_pin/verify_pin via pbkdf2_hmac; invalidate_hit; update_hit_pin_attempts - views_lnurl: include pinLimit in withdrawRequest response; validate PIN in callback - views_api: hash PIN on card create/update; clear PIN when set to null
After 3 wrong PINs the card is marked pin_blocked=True in the DB. Subsequent taps immediately return 'Card blocked' without offering new attempts. Admin resets the block by saving the card (PIN update or clear in the boltcards UI). - migrations.py: m004_add_pin_blocked -- adds pin_blocked column - models.py: pin_blocked: bool = False on Card - crud.py: block_card() / unblock_card() helpers - views_lnurl.py: check pin_blocked in api_scan; call block_card() after 3rd wrong PIN in lnurl_callback - views_api.py: reset pin_blocked=False on PIN change/clear Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After 3 incorrect PIN attempts, the card is set to enable=False via the existing enable_disable_card(). The user re-enables it manually via the DISABLE/ENABLE button in the boltcards UI -- no separate reset mechanism needed. The m004_add_pin_blocked migration column stays in the DB but is no longer used by the model or logic. - models.py: remove pin_blocked field - crud.py: remove block_card() / unblock_card() - views_lnurl.py: call enable_disable_card(False) on 3rd wrong PIN; remove pin_blocked check in api_scan - views_api.py: remove unblock_card import and pin_blocked resets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously pin_attempts was tracked per hit only. Cancelling and re-tapping the card created a new hit with pin_attempts=0, allowing unlimited PIN attempts across taps. Adds pin_total_attempts on the Card (persisted in DB). Every wrong PIN increments this counter regardless of cancel/re-tap. On the 3rd wrong attempt total the card is disabled. Counter resets on correct PIN or when admin changes/clears the PIN. - migrations.py: m005_add_card_pin_attempts - models.py: pin_total_attempts: int = 0 on Card - crud.py: increment_card_pin_attempts() / reset_card_pin_attempts() - views_lnurl.py: use card counter; reset on successful payment - views_api.py: reset counter on PIN change/clear Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After an admin re-enables a blocked card via the toggle, the pin_total_attempts counter was still >= 3. The first wrong PIN on the next tap immediately re-blocked the card. Now enable_disable_card(True) also resets pin_total_attempts to 0 so the card genuinely gets a fresh start. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This was referenced May 25, 2026
Merged
Author
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.

feat: PIN limit for BoltCard withdrawals
Adds optional PIN protection for BoltCard withdrawals above a configurable threshold, implementing the
pinLimitextension to LNURL-withdraw as proposed in lnurl/luds#290 (draft spec).What changed
Backend
Card:pin_limit(sat threshold),pin(PBKDF2-hashed),pin_total_attemptsHit:pin_attempts(per-tap wrong attempts)withdrawRequestresponse includespinLimit(in msat) when set — wallets that support it show a PIN padlnurl_callback) validates the PIN when the invoice amount >=pin_limit:"PIN required.""Invalid PIN"+ card-level counter incrementedenable = false)pin_total_attemptsto 0Frontend
DB migrations
pin_limit,pin,pin_total_attemptscolumns onboltcards.cardspin_attemptscolumn onboltcards.hitsScreenshots
LNbits — PIN threshold and PIN field in card settings

ZapBox Touch 3.5" — PIN entry screen on the device

Spec reference
This implementation follows the draft LUD-XX
pinLimitspecification:The
pinLimitfield in thewithdrawRequestresponse is expressed in millisatoshi, consistent with all other amount fields in the LNURL spec.Live demo / testing hardware
A working end-to-end implementation already exists and can be tested today. The following setup is required:
pinLimitflow and renders the PIN padThis lets reviewers observe the full PIN entry UX on real hardware before the LUD is finalised.
Test plan
pin_total_attemptsresets and card works again