Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
counter,
tx_limit,
daily_limit,
pin_limit,
pin_try,
pin,
pin_enable,
enable,
k0,
k1,
Expand All @@ -32,7 +36,8 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
)
VALUES (
:id, :uid, :external_id, :wallet, :card_name, :counter,
:tx_limit, :daily_limit, :enable, :k0, :k1, :k2, :otp
:tx_limit, :daily_limit, :pin_limit, :pin_try, :pin, :pin_enable,
:enable, :k0, :k1, :k2, :otp
)
""",
{
Expand All @@ -44,6 +49,10 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
"counter": data.counter,
"tx_limit": data.tx_limit,
"daily_limit": data.daily_limit,
"pin_limit": data.pin_limit,
"pin_try": 0,
"pin": data.pin,
"pin_enable": False,
"enable": True,
"k0": data.k0,
"k1": data.k1,
Expand Down Expand Up @@ -130,6 +139,8 @@ async def enable_disable_card(enable: bool, card_id: str) -> Card | None:
"UPDATE boltcards.cards SET enable = :enable WHERE id = :id",
{"enable": enable, "id": card_id},
)
if enable:
await update_card_pin_try(0, card_id)
return await get_card(card_id)


Expand All @@ -140,6 +151,13 @@ async def update_card_otp(otp: str, card_id: str):
)


async def update_card_pin_try(pin_try: int, card_id: str):
await db.execute(
"UPDATE boltcards.cards SET pin_try = :pin_try WHERE id = :id",
{"pin_try": pin_try, "id": card_id},
)


async def get_hit(hit_id: str) -> Hit | None:
return await db.fetchone(
"SELECT * FROM boltcards.hits WHERE id = :id",
Expand Down
85 changes: 85 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,88 @@ async def m002_correct_typing(db):
"""
)
await db.execute("DROP TABLE boltcards.cards_m001;")


async def m003_add_pin(db):
await db.execute("ALTER TABLE boltcards.cards RENAME TO cards_m002;")
await db.execute(
f"""
CREATE TABLE boltcards.cards (
id TEXT PRIMARY KEY UNIQUE,
wallet TEXT NOT NULL,
card_name TEXT NOT NULL,
uid TEXT NOT NULL UNIQUE,
external_id TEXT NOT NULL UNIQUE,
counter INT NOT NULL DEFAULT 0,
tx_limit INT NOT NULL,
daily_limit INT NOT NULL,
pin_limit INT NOT NULL DEFAULT 0,
pin_try INT NOT NULL DEFAULT 0,
pin TEXT NOT NULL DEFAULT '',
pin_enable BOOL NOT NULL DEFAULT FALSE,
enable BOOL NOT NULL,
k0 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
k1 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
k2 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
prev_k0 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
prev_k1 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
prev_k2 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
otp TEXT NOT NULL DEFAULT '',
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)

await db.execute(
"""
INSERT INTO boltcards.cards (
id,
wallet,
card_name,
uid,
external_id,
counter,
tx_limit,
daily_limit,
pin_limit,
pin_try,
pin,
pin_enable,
enable,
k0,
k1,
k2,
prev_k0,
prev_k1,
prev_k2,
otp,
time
)
SELECT
id,
wallet,
card_name,
uid,
external_id,
counter,
tx_limit,
daily_limit,
0,
0,
'',
FALSE,
enable,
k0,
k1,
k2,
prev_k0,
prev_k1,
prev_k2,
otp,
time
FROM boltcards.cards_m002;
"""
)
await db.execute("DROP TABLE boltcards.cards_m002;")
8 changes: 8 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class Card(BaseModel):
counter: int
tx_limit: int
daily_limit: int
pin_limit: int
pin_try: int
pin: str
pin_enable: bool
enable: bool
k0: str
k1: str
Expand All @@ -45,6 +49,10 @@ class CreateCardData(BaseModel):
counter: int = Query(0)
tx_limit: int = Query(0)
daily_limit: int = Query(0)
pin_limit: int = Query(0)
pin_try: int = Query(0)
pin: str = Query('')
pin_enable: str = Query(False)
enable: bool = Query(True)
k0: str = Query(ZERO_KEY)
k1: str = Query(ZERO_KEY)
Expand Down
34 changes: 34 additions & 0 deletions templates/boltcards/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,40 @@ <h6 class="text-subtitle1 q-my-none">
</q-btn>
</div>
</div>

<q-toggle
v-model="cardDialog.data.pin_enable"
label="Enable pin"
></q-toggle>
<div v-show="cardDialog.data.pin_enable" class="q-gutter-y-md">
<div class="row">
<div class="col">
<q-input
filled
dense
emit-value
v-model="cardDialog.data.pin_limit"
type="number"
label="Min transaction ({{LNBITS_DENOMINATION}})"
class="q-pr-sm"
></q-input>
</div>
<div class="col">
<q-input
filled
dense
emit-value
v-model="cardDialog.data.pin"
type="password"
inputmode="numeric"
mask="####"
label="Pin"
></q-input>
</div>
</div>
</div>


<q-toggle
v-model="toggleAdvanced"
label="Show advanced options"
Expand Down
18 changes: 18 additions & 0 deletions views_lnurl.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
spend_hit,
update_card_counter,
update_card_otp,
update_card_pin_try,
enable_disable_card,
)
from .models import UIDPost
from .nxp424 import decrypt_sun, get_sun_mac
Expand Down Expand Up @@ -93,6 +95,7 @@ async def api_scan(p, c, request: Request, external_id: str):
"maxWithdrawable": int(card.tx_limit) * 1000,
"defaultDescription": f"Boltcard (refund address lnurl://{lnurlpay_bech32})",
"payLink": lnurlpay_nonbech32_lud17, # LUD-19 compatibility
"pinLimit": None if not card.pin_enable else int(card.pin_limit) * 1000, # LUD-21 compatibility
}


Expand All @@ -105,6 +108,7 @@ async def lnurl_callback(
hit_id: str,
k1: str = Query(None),
pr: str = Query(None),
pin: str = Query(None),
):
# TODO: why no hit_id? its not used why is it passed by url?
logger.debug(f"TODO: why no hit_id? {hit_id}")
Expand All @@ -131,6 +135,20 @@ async def lnurl_callback(
card = await get_card(hit.card_id)
assert card
assert invoice.amount_msat, "Invoice amount is missing"

if card.pin_enable and int(card.pin_limit) * 1000 <= int(invoice.amount_msat) :
if card.pin != pin:
tries_left = 2 - card.pin_try
if tries_left == 0:
await enable_disable_card(enable=False, card_id=card.id)
return {"status": "ERROR", "reason": f"Wrong Pin. Card has been disabled."}
else:
await update_card_pin_try(pin_try=card.pin_try + 1, card_id=card.id)
return {"status": "ERROR", "reason": f"Wrong Pin. {tries_left} {'try' if tries_left == 1 else 'tries'} left."}
else:
await update_card_pin_try(pin_try=0, card_id=card.id)


hit = await spend_hit(card_id=hit.id, amount=int(invoice.amount_msat / 1000))
assert hit
try:
Expand Down