Merge branch 'ext-boltcards-2' into bensbits

This commit is contained in:
ben 2022-08-27 16:41:16 +01:00
commit 5ba624761c
8 changed files with 170 additions and 40 deletions

View file

@ -20,5 +20,6 @@ def boltcards_renderer():
return template_renderer(["lnbits/extensions/boltcards/templates"])
from .lnurl import * # noqa
from .tasks import * # noqa
from .views import * # noqa
from .views_api import * # noqa

View file

@ -8,28 +8,32 @@ from .models import Card, CreateCardData, Hit, Refund
async def create_card(data: CreateCardData, wallet_id: str) -> Card:
card_id = urlsafe_short_hash()
card_id = urlsafe_short_hash().upper()
await db.execute(
"""
INSERT INTO boltcards.cards (
id,
uid,
wallet,
card_name,
counter,
withdraw,
tx_limit,
daily_limit,
k0,
k1,
k2,
otp
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
card_id,
data.uid.upper(),
wallet_id,
data.card_name,
data.counter,
data.withdraw,
data.tx_limit,
data.daily_limit,
data.k0,
data.k1,
data.k2,
@ -67,12 +71,6 @@ async def get_cards(wallet_ids: Union[str, List[str]]) -> List[Card]:
return [Card(**row) for row in rows]
async def get_all_cards() -> List[Card]:
rows = await db.fetchall(f"SELECT * FROM boltcards.cards")
return [Card(**row) for row in rows]
async def get_card(card_id: str) -> Optional[Card]:
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE id = ?", (card_id,))
if not row:
@ -143,11 +141,16 @@ async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
async def get_hits_today(card_id: Union[str, List[str]]) -> List[Hit]:
rows = await db.fetchall(
f"SELECT * FROM boltcards.hits WHERE card_id = ? AND timestamp >= DATE() AND timestamp < DATE() + INTERVAL ? DAY", (card_id, 1)
f"SELECT * FROM boltcards.hits WHERE card_id = ? AND time >= DATE('now') AND time < DATE('now', '+1 day')", (card_id,)
)
return [Hit(**row) for row in rows]
async def spend_hit(id: str):
await db.execute(
"UPDATE boltcards.hits SET spent = ? WHERE id = ?",
(True, id),
)
async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
hit_id = urlsafe_short_hash()
@ -157,21 +160,63 @@ async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
id,
card_id,
ip,
spent,
useragent,
old_ctr,
new_ctr
new_ctr,
amount
)
VALUES (?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
hit_id,
card_id,
ip,
False,
useragent,
old_ctr,
new_ctr,
0,
),
)
hit = await get_hit(hit_id)
assert hit, "Newly recorded hit couldn't be retrieved"
return hit
async def create_refund(hit_id, refund_amount) -> Refund:
refund_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO boltcards.hits (
id,
hit_id,
refund_amount,
payment_hash
)
VALUES (?, ?, ?, ?)
""",
(
refund_id,
hit_id,
refund_amount,
payment_hash,
),
)
refund = await get_refund(refund_id)
assert refund, "Newly recorded hit couldn't be retrieved"
return refund
async def get_refund(refund_id: str) -> Optional[Refund]:
row = await db.fetchone(f"SELECT * FROM boltcards.refunds WHERE id = ?", (refund_id))
if not row:
return None
refund = dict(**row)
return Refund.parse_obj(refund)
async def get_refunds(hits_ids: Union[str, List[str]]) -> List[Refund]:
q = ",".join(["?"] * len(hits_ids))
rows = await db.fetchall(
f"SELECT * FROM boltcards.refunds WHERE hit_id IN ({q})", (*hits_ids,)
)
return [Refund(**row) for row in rows]

View file

@ -1,10 +1,13 @@
import base64
import hashlib
import hmac
import json
from http import HTTPStatus
from io import BytesIO
from typing import Optional
from loguru import logger
from embit import bech32, compact
from fastapi import Request
from fastapi.param_functions import Query
@ -33,6 +36,7 @@ from .crud import (
get_card,
get_hit,
get_hits_today,
spend_hit,
update_card,
update_card_counter,
update_card_otp,
@ -50,11 +54,11 @@ async def api_scan(p, c, request: Request, card_id: str = None):
c = c.upper()
card = None
counter = b""
try:
card = await get_card(card_id)
card_id, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
if card.uid.upper() != card_id.hex().upper():
try:
card = await get_card_by_uid(card_uid)
card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
if card.uid.upper() != card_uid.hex().upper():
return {"status": "ERROR", "reason": "Card UID mis-match."}
except:
return {"status": "ERROR", "reason": "Error decrypting card."}
@ -67,8 +71,8 @@ async def api_scan(p, c, request: Request, card_id: str = None):
ctr_int = int.from_bytes(counter, "little")
if ctr_int <= card.counter:
return {"status": "ERROR", "reason": "This link is already used."}
# if ctr_int <= card.counter:
# return {"status": "ERROR", "reason": "This link is already used."}
await update_card_counter(ctr_int, card.id)
@ -86,13 +90,13 @@ async def api_scan(p, c, request: Request, card_id: str = None):
for hit in todays_hits:
hits_amount = hits_amount + hit.amount
if (hits_amount + card.tx_limit) > card.daily_limit:
return {"status": "ERROR", "reason": "Max daily liit spent."}
return {"status": "ERROR", "reason": "Max daily limit spent."}
hit = await create_hit(card.id, ip, agent, card.counter, ctr_int)
lnurlpay = lnurl_encode(request.url_for("boltcards.lnurlp_response", hit_id=hit.id))
return {
"tag": "withdrawRequest",
"callback": request.url_for(
"boltcards.lnurl_callback"
"boltcards.lnurl_callback", hitid=hit.id
),
"k1": hit.id,
"minWithdrawable": 1 * 1000,
@ -166,14 +170,15 @@ async def api_auth(a, request: Request):
)
async def lnurlp_response(req: Request, hit_id: str = Query(None)):
hit = await get_hit(hit_id)
card = await get_card(hit.card_id)
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
payResponse = {
"tag": "payRequest",
"callback": req.url_for("boltcards.lnurlp_callback", hit_id=hit_id),
"metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])),
"minSendable": math.ceil(link.min_bet * 1) * 1000,
"maxSendable": round(link.max_bet * 1) * 1000,
"minSendable": 1 * 1000,
"maxSendable": card.tx_limit * 1000,
}
return json.dumps(payResponse)
@ -187,14 +192,15 @@ async def lnurlp_callback(
req: Request, hit_id: str = Query(None), amount: str = Query(None)
):
hit = await get_hit(hit_id)
card = await get_card(hit.card_id)
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
payment_hash, payment_request = await create_invoice(
wallet_id=link.wallet,
amount=int(amount / 1000),
wallet_id=card.wallet,
amount=int(amount) / 1000,
memo=f"Refund {hit_id}",
unhashed_description=LnurlPayMetadata(json.dumps([["text/plain", hit_id]])).encode("utf-8"),
unhashed_description=LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])).encode("utf-8"),
extra={"refund": hit_id},
)

View file

@ -64,6 +64,7 @@ class Hit(BaseModel):
useragent: str
old_ctr: int
new_ctr: int
amount: int
time: int
def from_row(cls, row: Row) -> "Hit":

View file

@ -18,6 +18,7 @@ new Vue({
cards: [],
hits: [],
refunds: [],
lnurlLink: location.hostname + '/boltcards/api/v1/scan/',
cardDialog: {
show: false,
data: {
@ -43,10 +44,10 @@ new Vue({
field: 'counter'
},
{
name: 'withdraw',
name: 'uid',
align: 'left',
label: 'Withdraw ID',
field: 'withdraw'
label: 'Card ID',
field: 'uid'
}
],
pagination: {
@ -139,7 +140,7 @@ new Vue({
.request(
'GET',
'/boltcards/api/v1/cards?all_wallets=true',
this.g.user.wallets[0].inkey
this.g.user.wallets[0].adminkey
)
.then(function (response) {
self.cards = response.data.map(function (obj) {

View file

@ -0,0 +1,34 @@
import asyncio
import json
import httpx
from lnbits.core import db as core_db
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
from .crud import get_hit, create_refund
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue)
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag")[0:6] != "Refund":
# not an lnurlp invoice
return
if payment.extra.get("wh_status"):
# this webhook has already been sent
return
hit = await get_hit(payment.extra.get("tag")[7:len(payment.extra.get("tag"))])
if hit:
refund = await create_refund(hit_id=hit.id, refund_amount=payment.extra.get("amount"))
await mark_webhook_sent(payment, 1)

View file

@ -34,6 +34,7 @@
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th auto-width>Base URL</q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
@ -53,6 +54,14 @@
@click="openQrCodeDialog(props.row.id)"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
outline
color="grey"
@click="copyText(lnurlLink + props.row.uid)"
lnurlLink >lnurl://...<q-tooltip>Click to copy, then add to NFC card</q-tooltip>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
@ -193,7 +202,7 @@
filled
dense
emit-value
v-model.trim="cardDialog.data.trans_limit"
v-model.trim="cardDialog.data.tx_limit"
type="number"
label="Max transaction (sats)"
class="q-pr-sm"
@ -218,6 +227,33 @@
type="text"
label="Card name "
></q-input>
<div class="row">
<div class="col-10">
<q-input
filled
dense
emit-value
v-model.trim="cardDialog.data.uid"
type="text"
label="Card UID "
><q-tooltip>Get from the card you'll use, using an NFC app</q-tooltip></q-input>
</div>
<div class="col-2 q-pl-sm">
<q-btn
outline
disable
color="grey"
icon="nfc"
:disable="nfcTagReading"
><q-tooltip>Tap card to scan UID (coming soon)</q-tooltip></q-btn>
</div>
</div>
<q-toggle
@click="toggleKeys"
v-model="toggleAdvanced"

View file

@ -13,7 +13,6 @@ from .crud import (
create_card,
create_hit,
delete_card,
get_all_cards,
get_card,
get_card_by_otp,
get_card_by_uid,
@ -22,10 +21,12 @@ from .crud import (
update_card,
update_card_counter,
update_card_otp,
get_refunds,
)
from .models import CreateCardData
from .nxp424 import decryptSUN, getSunMAC
from loguru import logger
@boltcards_ext.get("/api/v1/cards")
async def api_cards(
@ -47,6 +48,7 @@ async def api_card_create_or_update(
card_id: str = None,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
logger.debug(len(bytes.fromhex(data.uid)))
try:
if len(bytes.fromhex(data.uid)) != 7:
raise HTTPException(
@ -133,5 +135,9 @@ async def api_hits(
cards_ids = []
for card in cards:
cards_ids.append(card.id)
hits = await get_hits(cards_ids)
hits_ids = []
for hit in hits:
hits_ids.append(hit.id)
return [hit.dict() for hit in await get_hits(cards_ids)]
return [refund.dict() for refund in await get_refunds(hits_ids)]