Repository navigation
feat: PIN limit for BoltCard withdrawals (LUD-24 pinLimit) - #67
AxelHamburch wants to merge 9 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>
The UI blanks the PIN field when opening the edit dialog and sends null when it stays empty. api_card_update() treated that as a request to clear the PIN, silently disabling PIN protection on every edit. Restore the stored hash and attempt counter whenever no new PIN is submitted. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Pushed one more commit (f7ca324) after testing on my own LNbits instance: Fix: editing a card no longer removes the PIN The edit dialog blanks the PIN field ("Leave empty to keep existing PIN") and sends Now the stored hash and Note: a PIN can no longer be removed via the edit dialog, since an empty field always means "keep". Tested on a VPS LNbits instance: edit without PIN keeps the PIN, a new PIN replaces it. |
|
Update on the spec this PR implements: the Why this matters for the boltcards extension: the field is already implemented on the wallet/terminal side and in other services, so LNbits is currently the missing piece for Bolt Card users on LNbits:
Cards provisioned against this extension work end-to-end with those terminals and wallets. Without this PR, the PIN flow cannot be used with LNbits-hosted cards. |

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 LUD-24 (lnurl/luds#290) (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 LUD-24
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