From 4fab2d31013dca9eb549e4cfb8e8e93320d1c24f Mon Sep 17 00:00:00 2001
From: iWarpBTC
Date: Mon, 13 Jun 2022 21:08:06 +0200
Subject: [PATCH 01/73] new extension
just proof of concept
---
lnbits/extensions/boltcards/README.md | 11 +
lnbits/extensions/boltcards/__init__.py | 19 +
lnbits/extensions/boltcards/config.json | 6 +
lnbits/extensions/boltcards/crud.py | 91 +++++
lnbits/extensions/boltcards/migrations.py | 20 +
lnbits/extensions/boltcards/models.py | 21 +
lnbits/extensions/boltcards/nxp424.py | 31 ++
.../templates/boltcards/_api_docs.html | 27 ++
.../boltcards/templates/boltcards/index.html | 375 ++++++++++++++++++
lnbits/extensions/boltcards/views.py | 18 +
lnbits/extensions/boltcards/views_api.py | 161 ++++++++
11 files changed, 780 insertions(+)
create mode 100644 lnbits/extensions/boltcards/README.md
create mode 100644 lnbits/extensions/boltcards/__init__.py
create mode 100644 lnbits/extensions/boltcards/config.json
create mode 100644 lnbits/extensions/boltcards/crud.py
create mode 100644 lnbits/extensions/boltcards/migrations.py
create mode 100644 lnbits/extensions/boltcards/models.py
create mode 100644 lnbits/extensions/boltcards/nxp424.py
create mode 100644 lnbits/extensions/boltcards/templates/boltcards/_api_docs.html
create mode 100644 lnbits/extensions/boltcards/templates/boltcards/index.html
create mode 100644 lnbits/extensions/boltcards/views.py
create mode 100644 lnbits/extensions/boltcards/views_api.py
diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md
new file mode 100644
index 000000000..7b9bd7218
--- /dev/null
+++ b/lnbits/extensions/boltcards/README.md
@@ -0,0 +1,11 @@
+boltcards Extension
+*tagline*
+This is an boltcards extension to help you organise and build you own.
+
+Try to include an image
+
+
+
+If your extension has API endpoints, include useful ones here
+
+curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/boltcards -d '{"amount":"100","memo":"boltcards"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"
diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py
new file mode 100644
index 000000000..69326708f
--- /dev/null
+++ b/lnbits/extensions/boltcards/__init__.py
@@ -0,0 +1,19 @@
+from fastapi import APIRouter
+
+from lnbits.db import Database
+from lnbits.helpers import template_renderer
+
+db = Database("ext_boltcards")
+
+boltcards_ext: APIRouter = APIRouter(
+ prefix="/boltcards",
+ tags=["boltcards"]
+)
+
+
+def boltcards_renderer():
+ return template_renderer(["lnbits/extensions/boltcards/templates"])
+
+
+from .views import * # noqa
+from .views_api import * # noqa
diff --git a/lnbits/extensions/boltcards/config.json b/lnbits/extensions/boltcards/config.json
new file mode 100644
index 000000000..ef98a35ad
--- /dev/null
+++ b/lnbits/extensions/boltcards/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Bolt Cards",
+ "short_description": "Self custody Bolt Cards with one time LNURLw",
+ "icon": "payment",
+ "contributors": ["iwarpbtc"]
+}
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
new file mode 100644
index 000000000..e8fb5477a
--- /dev/null
+++ b/lnbits/extensions/boltcards/crud.py
@@ -0,0 +1,91 @@
+from optparse import Option
+from typing import List, Optional, Union
+from lnbits.helpers import urlsafe_short_hash
+
+from . import db
+from .models import Card, CreateCardData
+
+async def create_card(
+ data: CreateCardData, wallet_id: str
+) -> Card:
+ card_id = urlsafe_short_hash()
+ await db.execute(
+ """
+ INSERT INTO boltcards.cards (
+ id,
+ wallet,
+ card_name,
+ uid,
+ counter,
+ withdraw,
+ file_key,
+ meta_key
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ card_id,
+ wallet_id,
+ data.name,
+ data.uid,
+ data.counter,
+ data.withdraw,
+ data.file_key,
+ data.meta_key,
+ ),
+ )
+ link = await get_card(card_id, 0)
+ assert link, "Newly created card couldn't be retrieved"
+ return link
+
+async def update_card(card_id: str, **kwargs) -> Optional[Card]:
+ if "is_unique" in kwargs:
+ kwargs["is_unique"] = int(kwargs["is_unique"])
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+ await db.execute(
+ f"UPDATE boltcards.cards SET {q} WHERE id = ?",
+ (*kwargs.values(), card_id),
+ )
+ row = await db.fetchone(
+ "SELECT * FROM boltcards.cards WHERE id = ?", (card_id,)
+ )
+ return Card(**row) if row else None
+
+async def get_cards(wallet_ids: Union[str, List[str]]) -> List[Card]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM boltcards.cards WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+
+ 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, id_is_uid: bool=False) -> Optional[Card]:
+ sql = "SELECT * FROM boltcards.cards WHERE {} = ?".format("uid" if id_is_uid else "id")
+ row = await db.fetchone(
+ sql, card_id,
+ )
+ if not row:
+ return None
+
+ card = dict(**row)
+
+ return Card.parse_obj(card)
+
+async def delete_card(card_id: str) -> None:
+ await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
+
+async def update_card_counter(counter: int, id: str):
+ await db.execute(
+ "UPDATE boltcards.cards SET counter = ? WHERE id = ?",
+ (counter, id),
+ )
\ No newline at end of file
diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py
new file mode 100644
index 000000000..eedbb5d34
--- /dev/null
+++ b/lnbits/extensions/boltcards/migrations.py
@@ -0,0 +1,20 @@
+from lnbits.helpers import urlsafe_short_hash
+
+async def m001_initial(db):
+ await db.execute(
+ """
+ CREATE TABLE boltcards.cards (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ card_name TEXT NOT NULL,
+ uid TEXT NOT NULL,
+ counter INT NOT NULL DEFAULT 0,
+ withdraw TEXT NOT NULL,
+ file_key TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
+ meta_key TEXT NOT NULL DEFAULT '',
+ time TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
new file mode 100644
index 000000000..6ef25d0c2
--- /dev/null
+++ b/lnbits/extensions/boltcards/models.py
@@ -0,0 +1,21 @@
+from pydantic import BaseModel
+from fastapi.params import Query
+
+class Card(BaseModel):
+ id: str
+ wallet: str
+ card_name: str
+ uid: str
+ counter: int
+ withdraw: str
+ file_key: str
+ meta_key: str
+ time: int
+
+class CreateCardData(BaseModel):
+ card_name: str = Query(...)
+ uid: str = Query(...)
+ counter: str = Query(...)
+ withdraw: str = Query(...)
+ file_key: str = Query(...)
+ meta_key: str = Query(...)
\ No newline at end of file
diff --git a/lnbits/extensions/boltcards/nxp424.py b/lnbits/extensions/boltcards/nxp424.py
new file mode 100644
index 000000000..a67b896f5
--- /dev/null
+++ b/lnbits/extensions/boltcards/nxp424.py
@@ -0,0 +1,31 @@
+from typing import Tuple
+from Cryptodome.Hash import CMAC
+from Cryptodome.Cipher import AES
+
+SV2 = "3CC300010080"
+
+def myCMAC(key: bytes, msg: bytes=b'') -> bytes:
+ cobj = CMAC.new(key, ciphermod=AES)
+ if msg != b'':
+ cobj.update(msg)
+ return cobj.digest()
+
+def decryptSUN(sun: bytes, key: bytes) -> Tuple[bytes, bytes]:
+ IVbytes = b"\x00" * 16
+
+ cipher = AES.new(key, AES.MODE_CBC, IVbytes)
+ sun_plain = cipher.decrypt(sun)
+
+ UID = sun_plain[1:8]
+ counter = sun_plain[8:11]
+
+ return UID, counter
+
+def getSunMAC(UID: bytes, counter: bytes, key: bytes) -> bytes:
+ sv2prefix = bytes.fromhex(SV2)
+ sv2bytes = sv2prefix + UID + counter
+
+ mac1 = myCMAC(key, sv2bytes)
+ mac2 = myCMAC(mac1)
+
+ return mac2[1::2]
diff --git a/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html b/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html
new file mode 100644
index 000000000..f49392558
--- /dev/null
+++ b/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html
@@ -0,0 +1,27 @@
+
+
+
+
+ Be your own card association
+
+
+ Manage your Bolt Cards self custodian way
+
+ More details
+
+
+ Created by,
+ iWarp
+
+
+
+
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
new file mode 100644
index 000000000..4910cb66f
--- /dev/null
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -0,0 +1,375 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
+
+
+
+
+
+ Add Card
+
+
+
+
+
+
+
Cards
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+
+ {{ col.value }}
+
+
+
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+ {{SITE_TITLE}} Bolt Cards extension
+
+
+
+
+ {% include "boltcards/_api_docs.html" %}
+
+
+
+
+
+
+
+
+
+
+ The domain to use ex: "example.com"
+
+
+
+ Create a "Edit zone DNS" API token in cloudflare
+
+
+ How much to charge per day
+
+ Update Form
+ Create Card
+ Cancel
+
+
+
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }}
+
+{% endblock %}
diff --git a/lnbits/extensions/boltcards/views.py b/lnbits/extensions/boltcards/views.py
new file mode 100644
index 000000000..8fcbb7def
--- /dev/null
+++ b/lnbits/extensions/boltcards/views.py
@@ -0,0 +1,18 @@
+from fastapi import FastAPI, Request
+from fastapi.params import Depends
+from fastapi.templating import Jinja2Templates
+from starlette.responses import HTMLResponse
+
+from lnbits.core.models import User
+from lnbits.decorators import check_user_exists
+
+from . import boltcards_ext, boltcards_renderer
+
+templates = Jinja2Templates(directory="templates")
+
+
+@boltcards_ext.get("/", response_class=HTMLResponse)
+async def index(request: Request, user: User = Depends(check_user_exists)):
+ return boltcards_renderer().TemplateResponse(
+ "boltcards/index.html", {"request": request, "user": user.dict()}
+ )
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
new file mode 100644
index 000000000..0acfb6858
--- /dev/null
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -0,0 +1,161 @@
+# views_api.py is for you API endpoints that could be hit by another service
+
+# add your dependencies here
+
+# import httpx
+# (use httpx just like requests, except instead of response.ok there's only the
+# response.is_error that is its inverse)
+
+from http import HTTPStatus
+
+from fastapi.params import Depends, Query
+from starlette.exceptions import HTTPException
+from starlette.requests import Request
+
+from lnbits.core.crud import get_user
+from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
+from lnbits.extensions.withdraw import get_withdraw_link
+
+from . import boltcards_ext
+from .nxp424 import decryptSUN, getSunMAC
+from .crud import (
+ get_all_cards,
+ get_cards,
+ get_card,
+ create_card,
+ update_card,
+ delete_card,
+ update_card_counter
+)
+from .models import CreateCardData
+
+@boltcards_ext.get("/api/v1/cards")
+async def api_cards(
+ g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
+):
+ wallet_ids = [g.wallet.id]
+
+ if all_wallets:
+ wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+
+ return [card.dict() for card in await get_cards(wallet_ids)]
+
+@boltcards_ext.post("/api/v1/cards", status_code=HTTPStatus.CREATED)
+@boltcards_ext.put("/api/v1/cards/{card_id}", status_code=HTTPStatus.OK)
+async def api_link_create_or_update(
+ req: Request,
+ data: CreateCardData,
+ card_id: str = None,
+ wallet: WalletTypeInfo = Depends(require_admin_key),
+):
+ '''
+ if data.uses > 250:
+ raise HTTPException(
+ detail="250 uses max.", status_code=HTTPStatus.BAD_REQUEST
+ )
+
+ if data.min_withdrawable < 1:
+ raise HTTPException(
+ detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
+ )
+
+ if data.max_withdrawable < data.min_withdrawable:
+ raise HTTPException(
+ detail="`max_withdrawable` needs to be at least `min_withdrawable`.",
+ status_code=HTTPStatus.BAD_REQUEST,
+ )
+ '''
+ if card_id:
+ card = await get_card(card_id)
+ if not card:
+ raise HTTPException(
+ detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
+ )
+ if card.wallet != wallet.wallet.id:
+ raise HTTPException(
+ detail="Not your card.", status_code=HTTPStatus.FORBIDDEN
+ )
+ card = await update_card(
+ card_id, **data.dict()
+ )
+ else:
+ card = await create_card(
+ wallet_id=wallet.wallet.id, data=data
+ )
+ return card.dict()
+
+@boltcards_ext.delete("/api/v1/cards/{card_id}")
+async def api_link_delete(card_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
+ card = await get_card(card_id)
+
+ if not card:
+ raise HTTPException(
+ detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
+ )
+
+ if card.wallet != wallet.wallet.id:
+ raise HTTPException(
+ detail="Not your card.", status_code=HTTPStatus.FORBIDDEN
+ )
+
+ await delete_card(card_id)
+ raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+@boltcards_ext.get("/api/v1/scan/") # pay.btcslovnik.cz/boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000
+async def api_scan(
+ uid, ctr, c,
+ request: Request
+):
+ card = await get_card(uid, id_is_uid=True)
+
+ if card == None:
+ return {"status": "ERROR", "reason": "Unknown card."}
+
+ if c != getSunMAC(bytes.fromhex(uid), bytes.fromhex(ctr)[::-1], bytes.fromhex(card.file_key)).hex().upper():
+ print(c)
+ print(getSunMAC(bytes.fromhex(uid), bytes.fromhex(ctr)[::-1], bytes.fromhex(card.file_key)).hex().upper())
+ return {"status": "ERROR", "reason": "CMAC does not check."}
+
+ ctr_int = int(ctr, 16)
+
+ if ctr_int <= card.counter:
+ return {"status": "ERROR", "reason": "This link is already used."}
+
+ await update_card_counter(ctr_int, card.id)
+
+ link = await get_withdraw_link(card.withdraw, 0)
+
+ return link.lnurl_response(request)
+
+@boltcards_ext.get("/api/v1/scane/")
+async def api_scane(
+ e, c,
+ request: Request
+):
+ card = None
+ counter = b''
+
+ for cand in await get_all_cards():
+ if cand.meta_key:
+ card_uid, counter = decryptSUN(bytes.fromhex(e), bytes.fromhex(cand.meta_key))
+
+ if card_uid.hex().upper() == cand.uid:
+ card = cand
+ break
+
+ if card == None:
+ return {"status": "ERROR", "reason": "Unknown card."}
+
+ if c != getSunMAC(card_uid, counter, bytes.fromhex(card.file_key)).hex().upper():
+ print(c)
+ print(getSunMAC(card_uid, counter, bytes.fromhex(card.file_key)).hex().upper())
+ return {"status": "ERROR", "reason": "CMAC does not check."}
+
+ counter_int = int.from_bytes(counter, "little")
+ if counter_int <= card.counter:
+ return {"status": "ERROR", "reason": "This link is already used."}
+
+ await update_card_counter(counter_int, card.id)
+
+ link = await get_withdraw_link(card.withdraw, 0)
+ return link.lnurl_response(request)
From 3cb62d1899d6a15895d97cc30ec0ac8483649d5b Mon Sep 17 00:00:00 2001
From: iWarpBTC
Date: Tue, 21 Jun 2022 18:03:20 +0200
Subject: [PATCH 02/73] recording card tapping
---
lnbits/extensions/boltcards/crud.py | 61 +++++++++++++++++--
lnbits/extensions/boltcards/migrations.py | 16 +++++
lnbits/extensions/boltcards/models.py | 20 +++++-
.../boltcards/templates/boltcards/index.html | 1 +
lnbits/extensions/boltcards/views_api.py | 11 +++-
5 files changed, 102 insertions(+), 7 deletions(-)
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index e8fb5477a..62aad3560 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -3,7 +3,7 @@ from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
-from .models import Card, CreateCardData
+from .models import Card, CreateCardData, Hit
async def create_card(
data: CreateCardData, wallet_id: str
@@ -34,9 +34,9 @@ async def create_card(
data.meta_key,
),
)
- link = await get_card(card_id, 0)
- assert link, "Newly created card couldn't be retrieved"
- return link
+ card = await get_card(card_id, 0)
+ assert card, "Newly created card couldn't be retrieved"
+ return card
async def update_card(card_id: str, **kwargs) -> Optional[Card]:
if "is_unique" in kwargs:
@@ -88,4 +88,55 @@ async def update_card_counter(counter: int, id: str):
await db.execute(
"UPDATE boltcards.cards SET counter = ? WHERE id = ?",
(counter, id),
- )
\ No newline at end of file
+ )
+
+async def get_hit(hit_id: str) -> Optional[Hit]:
+ row = await db.fetchone(
+ f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id)
+ )
+ if not row:
+ return None
+
+ hit = dict(**row)
+
+ return Hit.parse_obj(hit)
+
+async def get_hits(wallet_ids: Union[str, List[str]]) -> List[Hit]:
+
+ cards = get_cards(wallet_ids)
+
+ q = ",".join(["?"] * len(cards))
+ rows = await db.fetchall(
+ f"SELECT * FROM boltcards.hits WHERE wallet IN ({q})", (*(card.card_id for card in cards),)
+ )
+
+ return [Card(**row) for row in rows]
+
+async def create_hit(
+ card_id, ip, useragent, old_ctr, new_ctr
+) -> Hit:
+ hit_id = urlsafe_short_hash()
+ await db.execute(
+ """
+ INSERT INTO boltcards.hits (
+ id,
+ card_id,
+ ip,
+ useragent,
+ old_ctr,
+ new_ctr
+ )
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ (
+ hit_id,
+ card_id,
+ ip,
+ useragent,
+ old_ctr,
+ new_ctr,
+ ),
+ )
+ hit = await get_hit(hit_id)
+ assert hit, "Newly recorded hit couldn't be retrieved"
+ return hit
diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py
index eedbb5d34..e7236ce7a 100644
--- a/lnbits/extensions/boltcards/migrations.py
+++ b/lnbits/extensions/boltcards/migrations.py
@@ -18,3 +18,19 @@ async def m001_initial(db):
);
"""
)
+
+ await db.execute(
+ """
+ CREATE TABLE boltcards.hits (
+ id TEXT PRIMARY KEY,
+ card_id TEXT NOT NULL,
+ ip TEXT NOT NULL,
+ useragent TEXT,
+ old_ctr INT NOT NULL DEFAULT 0,
+ new_ctr INT NOT NULL DEFAULT 0,
+ time TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 6ef25d0c2..75621269f 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -18,4 +18,22 @@ class CreateCardData(BaseModel):
counter: str = Query(...)
withdraw: str = Query(...)
file_key: str = Query(...)
- meta_key: str = Query(...)
\ No newline at end of file
+ meta_key: str = Query(...)
+
+class Hit(BaseModel):
+ id: str
+ card_id: str
+ ip: str
+ useragent: str
+ old_ctr: int
+ new_ctr: int
+ time: int
+
+'''
+class CreateHitData(BaseModel):
+ card_id: str = Query(...)
+ ip: str = Query(...)
+ useragent: str = Query(...)
+ old_ctr: int = Query(...)
+ new_ctr: int = Query(...)
+'''
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 4910cb66f..a6997a5d1 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -368,6 +368,7 @@
if (this.g.user.wallets.length) {
this.getCards()
this.getWithdraws()
+ this.getHits()
}
}
})
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 0acfb6858..75cfe129e 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -19,6 +19,7 @@ from lnbits.extensions.withdraw import get_withdraw_link
from . import boltcards_ext
from .nxp424 import decryptSUN, getSunMAC
from .crud import (
+ create_hit,
get_all_cards,
get_cards,
get_card,
@@ -43,7 +44,7 @@ async def api_cards(
@boltcards_ext.post("/api/v1/cards", status_code=HTTPStatus.CREATED)
@boltcards_ext.put("/api/v1/cards/{card_id}", status_code=HTTPStatus.OK)
async def api_link_create_or_update(
- req: Request,
+# req: Request,
data: CreateCardData,
card_id: str = None,
wallet: WalletTypeInfo = Depends(require_admin_key),
@@ -157,5 +158,13 @@ async def api_scane(
await update_card_counter(counter_int, card.id)
+ ip = request.client.host
+ if request.headers['x-real-ip']:
+ ip = request.headers['x-real-ip']
+ elif request.headers['x-forwarded-for']:
+ ip = request.headers['x-forwarded-for']
+
+ await create_hit(card.id, ip, request.headers['user-agent'], card.counter, counter_int)
+
link = await get_withdraw_link(card.withdraw, 0)
return link.lnurl_response(request)
From 2f497ac0eeee7b72ce24f6342f30694f5bebb353 Mon Sep 17 00:00:00 2001
From: iWarpBTC
Date: Tue, 21 Jun 2022 22:04:43 +0200
Subject: [PATCH 03/73] retreiving hits
---
lnbits/extensions/boltcards/crud.py | 11 +++---
lnbits/extensions/boltcards/models.py | 9 -----
.../boltcards/templates/boltcards/index.html | 1 -
lnbits/extensions/boltcards/views_api.py | 36 ++++++++++++++++---
4 files changed, 36 insertions(+), 21 deletions(-)
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index 62aad3560..f34ce6594 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -101,16 +101,13 @@ async def get_hit(hit_id: str) -> Optional[Hit]:
return Hit.parse_obj(hit)
-async def get_hits(wallet_ids: Union[str, List[str]]) -> List[Hit]:
-
- cards = get_cards(wallet_ids)
-
- q = ",".join(["?"] * len(cards))
+async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
+ q = ",".join(["?"] * len(cards_ids))
rows = await db.fetchall(
- f"SELECT * FROM boltcards.hits WHERE wallet IN ({q})", (*(card.card_id for card in cards),)
+ f"SELECT * FROM boltcards.hits WHERE card_id IN ({q})", (*cards_ids,)
)
- return [Card(**row) for row in rows]
+ return [Hit(**row) for row in rows]
async def create_hit(
card_id, ip, useragent, old_ctr, new_ctr
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 75621269f..728aa2bb8 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -28,12 +28,3 @@ class Hit(BaseModel):
old_ctr: int
new_ctr: int
time: int
-
-'''
-class CreateHitData(BaseModel):
- card_id: str = Query(...)
- ip: str = Query(...)
- useragent: str = Query(...)
- old_ctr: int = Query(...)
- new_ctr: int = Query(...)
-'''
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index a6997a5d1..4910cb66f 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -368,7 +368,6 @@
if (this.g.user.wallets.length) {
this.getCards()
this.getWithdraws()
- this.getHits()
}
}
})
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 75cfe129e..8a8e33a2f 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -24,6 +24,7 @@ from .crud import (
get_cards,
get_card,
create_card,
+ get_hits,
update_card,
delete_card,
update_card_counter
@@ -102,6 +103,22 @@ async def api_link_delete(card_id, wallet: WalletTypeInfo = Depends(require_admi
await delete_card(card_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+@boltcards_ext.get("/api/v1/hits")
+async def api_hits(
+ g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
+):
+ wallet_ids = [g.wallet.id]
+
+ if all_wallets:
+ wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+
+ cards = await get_cards(wallet_ids)
+ cards_ids = []
+ for card in cards:
+ cards_ids.append(card.id)
+
+ return [hit.dict() for hit in await get_hits(cards_ids)]
+
@boltcards_ext.get("/api/v1/scan/") # pay.btcslovnik.cz/boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000
async def api_scan(
uid, ctr, c,
@@ -124,8 +141,17 @@ async def api_scan(
await update_card_counter(ctr_int, card.id)
- link = await get_withdraw_link(card.withdraw, 0)
+ ip = request.client.host
+ if request.headers['x-real-ip']:
+ ip = request.headers['x-real-ip']
+ elif request.headers['x-forwarded-for']:
+ ip = request.headers['x-forwarded-for']
+ agent = request.headers['user-agent'] if 'user-agent' in request.headers else ''
+
+ await create_hit(card.id, ip, agent, card.counter, ctr_int)
+
+ link = await get_withdraw_link(card.withdraw, 0)
return link.lnurl_response(request)
@boltcards_ext.get("/api/v1/scane/")
@@ -152,8 +178,8 @@ async def api_scane(
print(getSunMAC(card_uid, counter, bytes.fromhex(card.file_key)).hex().upper())
return {"status": "ERROR", "reason": "CMAC does not check."}
- counter_int = int.from_bytes(counter, "little")
- if counter_int <= card.counter:
+ ctr_int = int.from_bytes(counter, "little")
+ if ctr_int <= card.counter:
return {"status": "ERROR", "reason": "This link is already used."}
await update_card_counter(counter_int, card.id)
@@ -164,7 +190,9 @@ async def api_scane(
elif request.headers['x-forwarded-for']:
ip = request.headers['x-forwarded-for']
- await create_hit(card.id, ip, request.headers['user-agent'], card.counter, counter_int)
+ agent = request.headers['user-agent'] if 'user-agent' in request.headers else ''
+
+ await create_hit(card.id, ip, agent, card.counter, ctr_int)
link = await get_withdraw_link(card.withdraw, 0)
return link.lnurl_response(request)
From 5af49e38018594b16f46e9ab92acd575be9f42d8 Mon Sep 17 00:00:00 2001
From: iWarpBTC
Date: Tue, 21 Jun 2022 23:41:08 +0200
Subject: [PATCH 04/73] comments and hints
---
lnbits/extensions/boltcards/nxp424.py | 1 +
.../boltcards/templates/boltcards/index.html | 12 ++++--------
lnbits/extensions/boltcards/views_api.py | 15 +++++++++++----
3 files changed, 16 insertions(+), 12 deletions(-)
diff --git a/lnbits/extensions/boltcards/nxp424.py b/lnbits/extensions/boltcards/nxp424.py
index a67b896f5..effa987d4 100644
--- a/lnbits/extensions/boltcards/nxp424.py
+++ b/lnbits/extensions/boltcards/nxp424.py
@@ -1,3 +1,4 @@
+# https://www.nxp.com/docs/en/application-note/AN12196.pdf
from typing import Tuple
from Cryptodome.Hash import CMAC
from Cryptodome.Cipher import AES
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 4910cb66f..21ac4a45a 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -126,17 +126,15 @@
v-model.trim="cardDialog.data.card_name"
type="text"
label="Card name "
- >The domain to use ex: "example.com"
- Create a "Edit zone DNS" API token in cloudflare
How much to charge per dayZero if you don't know.
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 8a8e33a2f..b13d9c351 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -51,6 +51,7 @@ async def api_link_create_or_update(
wallet: WalletTypeInfo = Depends(require_admin_key),
):
'''
+ TODO: some checks
if data.uses > 250:
raise HTTPException(
detail="250 uses max.", status_code=HTTPStatus.BAD_REQUEST
@@ -119,7 +120,8 @@ async def api_hits(
return [hit.dict() for hit in await get_hits(cards_ids)]
-@boltcards_ext.get("/api/v1/scan/") # pay.btcslovnik.cz/boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000
+# /boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000
+@boltcards_ext.get("/api/v1/scan/")
async def api_scan(
uid, ctr, c,
request: Request
@@ -141,6 +143,7 @@ async def api_scan(
await update_card_counter(ctr_int, card.id)
+ # gathering some info for hit record
ip = request.client.host
if request.headers['x-real-ip']:
ip = request.headers['x-real-ip']
@@ -154,6 +157,7 @@ async def api_scan(
link = await get_withdraw_link(card.withdraw, 0)
return link.lnurl_response(request)
+# /boltcards/api/v1/scane/?e=00000000000000000000000000000000&c=0000000000000000
@boltcards_ext.get("/api/v1/scane/")
async def api_scane(
e, c,
@@ -162,6 +166,8 @@ async def api_scane(
card = None
counter = b''
+ # since this route is common to all cards I don't know whitch 'meta key' to use
+ # so I try one by one until decrypted uid matches
for cand in await get_all_cards():
if cand.meta_key:
card_uid, counter = decryptSUN(bytes.fromhex(e), bytes.fromhex(cand.meta_key))
@@ -182,12 +188,13 @@ async def api_scane(
if ctr_int <= card.counter:
return {"status": "ERROR", "reason": "This link is already used."}
- await update_card_counter(counter_int, card.id)
+ await update_card_counter(ctr_int, card.id)
+ # gathering some info for hit record
ip = request.client.host
- if request.headers['x-real-ip']:
+ if 'x-real-ip' in request.headers:
ip = request.headers['x-real-ip']
- elif request.headers['x-forwarded-for']:
+ elif 'x-forwarded-for' in request.headers:
ip = request.headers['x-forwarded-for']
agent = request.headers['user-agent'] if 'user-agent' in request.headers else ''
From 5b8d317441b1df27068b6005dbbb95f27c6ecdbc Mon Sep 17 00:00:00 2001
From: Gene Takavic
Date: Fri, 15 Jul 2022 16:43:06 +0200
Subject: [PATCH 05/73] black & isort
---
lnbits/extensions/boltcards/__init__.py | 5 +-
lnbits/extensions/boltcards/crud.py | 40 ++++----
lnbits/extensions/boltcards/migrations.py | 1 +
lnbits/extensions/boltcards/models.py | 5 +-
lnbits/extensions/boltcards/nxp424.py | 12 ++-
lnbits/extensions/boltcards/views_api.py | 111 ++++++++++------------
6 files changed, 88 insertions(+), 86 deletions(-)
diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py
index 69326708f..f1ef972eb 100644
--- a/lnbits/extensions/boltcards/__init__.py
+++ b/lnbits/extensions/boltcards/__init__.py
@@ -5,10 +5,7 @@ from lnbits.helpers import template_renderer
db = Database("ext_boltcards")
-boltcards_ext: APIRouter = APIRouter(
- prefix="/boltcards",
- tags=["boltcards"]
-)
+boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"])
def boltcards_renderer():
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index f34ce6594..7cf5cad18 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -1,13 +1,13 @@
from optparse import Option
from typing import List, Optional, Union
+
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import Card, CreateCardData, Hit
-async def create_card(
- data: CreateCardData, wallet_id: str
-) -> Card:
+
+async def create_card(data: CreateCardData, wallet_id: str) -> Card:
card_id = urlsafe_short_hash()
await db.execute(
"""
@@ -38,6 +38,7 @@ async def create_card(
assert card, "Newly created card couldn't be retrieved"
return card
+
async def update_card(card_id: str, **kwargs) -> Optional[Card]:
if "is_unique" in kwargs:
kwargs["is_unique"] = int(kwargs["is_unique"])
@@ -46,11 +47,10 @@ async def update_card(card_id: str, **kwargs) -> Optional[Card]:
f"UPDATE boltcards.cards SET {q} WHERE id = ?",
(*kwargs.values(), card_id),
)
- row = await db.fetchone(
- "SELECT * FROM boltcards.cards WHERE id = ?", (card_id,)
- )
+ row = await db.fetchone("SELECT * FROM boltcards.cards WHERE id = ?", (card_id,))
return Card(**row) if row else None
+
async def get_cards(wallet_ids: Union[str, List[str]]) -> List[Card]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
@@ -62,17 +62,20 @@ 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"
- )
+ rows = await db.fetchall(f"SELECT * FROM boltcards.cards")
return [Card(**row) for row in rows]
-async def get_card(card_id: str, id_is_uid: bool=False) -> Optional[Card]:
- sql = "SELECT * FROM boltcards.cards WHERE {} = ?".format("uid" if id_is_uid else "id")
+
+async def get_card(card_id: str, id_is_uid: bool = False) -> Optional[Card]:
+ sql = "SELECT * FROM boltcards.cards WHERE {} = ?".format(
+ "uid" if id_is_uid else "id"
+ )
row = await db.fetchone(
- sql, card_id,
+ sql,
+ card_id,
)
if not row:
return None
@@ -81,19 +84,20 @@ async def get_card(card_id: str, id_is_uid: bool=False) -> Optional[Card]:
return Card.parse_obj(card)
+
async def delete_card(card_id: str) -> None:
await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
+
async def update_card_counter(counter: int, id: str):
await db.execute(
"UPDATE boltcards.cards SET counter = ? WHERE id = ?",
(counter, id),
)
+
async def get_hit(hit_id: str) -> Optional[Hit]:
- row = await db.fetchone(
- f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id)
- )
+ row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id))
if not row:
return None
@@ -101,6 +105,7 @@ async def get_hit(hit_id: str) -> Optional[Hit]:
return Hit.parse_obj(hit)
+
async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
q = ",".join(["?"] * len(cards_ids))
rows = await db.fetchall(
@@ -109,9 +114,8 @@ async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
return [Hit(**row) for row in rows]
-async def create_hit(
- card_id, ip, useragent, old_ctr, new_ctr
-) -> Hit:
+
+async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
hit_id = urlsafe_short_hash()
await db.execute(
"""
diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py
index e7236ce7a..6e0fa0723 100644
--- a/lnbits/extensions/boltcards/migrations.py
+++ b/lnbits/extensions/boltcards/migrations.py
@@ -1,5 +1,6 @@
from lnbits.helpers import urlsafe_short_hash
+
async def m001_initial(db):
await db.execute(
"""
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 728aa2bb8..b6d521c3c 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -1,5 +1,6 @@
-from pydantic import BaseModel
from fastapi.params import Query
+from pydantic import BaseModel
+
class Card(BaseModel):
id: str
@@ -12,6 +13,7 @@ class Card(BaseModel):
meta_key: str
time: int
+
class CreateCardData(BaseModel):
card_name: str = Query(...)
uid: str = Query(...)
@@ -20,6 +22,7 @@ class CreateCardData(BaseModel):
file_key: str = Query(...)
meta_key: str = Query(...)
+
class Hit(BaseModel):
id: str
card_id: str
diff --git a/lnbits/extensions/boltcards/nxp424.py b/lnbits/extensions/boltcards/nxp424.py
index effa987d4..83f4e50d5 100644
--- a/lnbits/extensions/boltcards/nxp424.py
+++ b/lnbits/extensions/boltcards/nxp424.py
@@ -1,18 +1,21 @@
# https://www.nxp.com/docs/en/application-note/AN12196.pdf
from typing import Tuple
-from Cryptodome.Hash import CMAC
+
from Cryptodome.Cipher import AES
+from Cryptodome.Hash import CMAC
SV2 = "3CC300010080"
-def myCMAC(key: bytes, msg: bytes=b'') -> bytes:
+
+def myCMAC(key: bytes, msg: bytes = b"") -> bytes:
cobj = CMAC.new(key, ciphermod=AES)
- if msg != b'':
+ if msg != b"":
cobj.update(msg)
return cobj.digest()
+
def decryptSUN(sun: bytes, key: bytes) -> Tuple[bytes, bytes]:
- IVbytes = b"\x00" * 16
+ IVbytes = b"\x00" * 16
cipher = AES.new(key, AES.MODE_CBC, IVbytes)
sun_plain = cipher.decrypt(sun)
@@ -22,6 +25,7 @@ def decryptSUN(sun: bytes, key: bytes) -> Tuple[bytes, bytes]:
return UID, counter
+
def getSunMAC(UID: bytes, counter: bytes, key: bytes) -> bytes:
sv2prefix = bytes.fromhex(SV2)
sv2bytes = sv2prefix + UID + counter
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index b13d9c351..fbd05cce0 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -17,19 +17,20 @@ from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.extensions.withdraw import get_withdraw_link
from . import boltcards_ext
-from .nxp424 import decryptSUN, getSunMAC
from .crud import (
- create_hit,
- get_all_cards,
- get_cards,
- get_card,
create_card,
+ create_hit,
+ delete_card,
+ get_all_cards,
+ get_card,
+ get_cards,
get_hits,
update_card,
- delete_card,
- update_card_counter
+ update_card_counter,
)
from .models import CreateCardData
+from .nxp424 import decryptSUN, getSunMAC
+
@boltcards_ext.get("/api/v1/cards")
async def api_cards(
@@ -42,32 +43,15 @@ async def api_cards(
return [card.dict() for card in await get_cards(wallet_ids)]
+
@boltcards_ext.post("/api/v1/cards", status_code=HTTPStatus.CREATED)
@boltcards_ext.put("/api/v1/cards/{card_id}", status_code=HTTPStatus.OK)
async def api_link_create_or_update(
-# req: Request,
+ # req: Request,
data: CreateCardData,
card_id: str = None,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
- '''
- TODO: some checks
- if data.uses > 250:
- raise HTTPException(
- detail="250 uses max.", status_code=HTTPStatus.BAD_REQUEST
- )
-
- if data.min_withdrawable < 1:
- raise HTTPException(
- detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
- )
-
- if data.max_withdrawable < data.min_withdrawable:
- raise HTTPException(
- detail="`max_withdrawable` needs to be at least `min_withdrawable`.",
- status_code=HTTPStatus.BAD_REQUEST,
- )
- '''
if card_id:
card = await get_card(card_id)
if not card:
@@ -78,15 +62,12 @@ async def api_link_create_or_update(
raise HTTPException(
detail="Not your card.", status_code=HTTPStatus.FORBIDDEN
)
- card = await update_card(
- card_id, **data.dict()
- )
+ card = await update_card(card_id, **data.dict())
else:
- card = await create_card(
- wallet_id=wallet.wallet.id, data=data
- )
+ card = await create_card(wallet_id=wallet.wallet.id, data=data)
return card.dict()
+
@boltcards_ext.delete("/api/v1/cards/{card_id}")
async def api_link_delete(card_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
card = await get_card(card_id)
@@ -97,13 +78,12 @@ async def api_link_delete(card_id, wallet: WalletTypeInfo = Depends(require_admi
)
if card.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your card.", status_code=HTTPStatus.FORBIDDEN
- )
+ raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
await delete_card(card_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
@boltcards_ext.get("/api/v1/hits")
async def api_hits(
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
@@ -120,20 +100,33 @@ async def api_hits(
return [hit.dict() for hit in await get_hits(cards_ids)]
+
# /boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000
-@boltcards_ext.get("/api/v1/scan/")
-async def api_scan(
- uid, ctr, c,
- request: Request
-):
+@boltcards_ext.get("/api/v1/scan/")
+async def api_scan(uid, ctr, c, request: Request):
card = await get_card(uid, id_is_uid=True)
if card == None:
return {"status": "ERROR", "reason": "Unknown card."}
- if c != getSunMAC(bytes.fromhex(uid), bytes.fromhex(ctr)[::-1], bytes.fromhex(card.file_key)).hex().upper():
+ if (
+ c
+ != getSunMAC(
+ bytes.fromhex(uid), bytes.fromhex(ctr)[::-1], bytes.fromhex(card.file_key)
+ )
+ .hex()
+ .upper()
+ ):
print(c)
- print(getSunMAC(bytes.fromhex(uid), bytes.fromhex(ctr)[::-1], bytes.fromhex(card.file_key)).hex().upper())
+ print(
+ getSunMAC(
+ bytes.fromhex(uid),
+ bytes.fromhex(ctr)[::-1],
+ bytes.fromhex(card.file_key),
+ )
+ .hex()
+ .upper()
+ )
return {"status": "ERROR", "reason": "CMAC does not check."}
ctr_int = int(ctr, 16)
@@ -145,32 +138,32 @@ async def api_scan(
# gathering some info for hit record
ip = request.client.host
- if request.headers['x-real-ip']:
- ip = request.headers['x-real-ip']
- elif request.headers['x-forwarded-for']:
- ip = request.headers['x-forwarded-for']
+ if request.headers["x-real-ip"]:
+ ip = request.headers["x-real-ip"]
+ elif request.headers["x-forwarded-for"]:
+ ip = request.headers["x-forwarded-for"]
- agent = request.headers['user-agent'] if 'user-agent' in request.headers else ''
+ agent = request.headers["user-agent"] if "user-agent" in request.headers else ""
await create_hit(card.id, ip, agent, card.counter, ctr_int)
link = await get_withdraw_link(card.withdraw, 0)
return link.lnurl_response(request)
+
# /boltcards/api/v1/scane/?e=00000000000000000000000000000000&c=0000000000000000
@boltcards_ext.get("/api/v1/scane/")
-async def api_scane(
- e, c,
- request: Request
-):
+async def api_scane(e, c, request: Request):
card = None
- counter = b''
+ counter = b""
# since this route is common to all cards I don't know whitch 'meta key' to use
# so I try one by one until decrypted uid matches
for cand in await get_all_cards():
if cand.meta_key:
- card_uid, counter = decryptSUN(bytes.fromhex(e), bytes.fromhex(cand.meta_key))
+ card_uid, counter = decryptSUN(
+ bytes.fromhex(e), bytes.fromhex(cand.meta_key)
+ )
if card_uid.hex().upper() == cand.uid:
card = cand
@@ -187,17 +180,17 @@ async def api_scane(
ctr_int = int.from_bytes(counter, "little")
if ctr_int <= card.counter:
return {"status": "ERROR", "reason": "This link is already used."}
-
+
await update_card_counter(ctr_int, card.id)
# gathering some info for hit record
ip = request.client.host
- if 'x-real-ip' in request.headers:
- ip = request.headers['x-real-ip']
- elif 'x-forwarded-for' in request.headers:
- ip = request.headers['x-forwarded-for']
+ if "x-real-ip" in request.headers:
+ ip = request.headers["x-real-ip"]
+ elif "x-forwarded-for" in request.headers:
+ ip = request.headers["x-forwarded-for"]
- agent = request.headers['user-agent'] if 'user-agent' in request.headers else ''
+ agent = request.headers["user-agent"] if "user-agent" in request.headers else ""
await create_hit(card.id, ip, agent, card.counter, ctr_int)
From c04b0a19055d6b29f5ac38e314d783e9498b6768 Mon Sep 17 00:00:00 2001
From: iWarpBTC
Date: Sun, 17 Jul 2022 12:23:56 +0200
Subject: [PATCH 06/73] Update index.html
prettier
---
.../boltcards/templates/boltcards/index.html | 27 ++++++++-----------
1 file changed, 11 insertions(+), 16 deletions(-)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 21ac4a45a..165d72fba 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -126,8 +126,7 @@
v-model.trim="cardDialog.data.card_name"
type="text"
label="Card name "
- >
+ >
-
{% endblock %} {% block scripts %} {{ window_vars(user) }}
-{% endblock %}
+{% endblock %}
\ No newline at end of file
From 293e5394a81fa4040acddf64b31c77a006939c1f Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 14 Aug 2022 10:58:35 -0600
Subject: [PATCH 09/73] run make format
---
lnbits/extensions/boltcards/templates/boltcards/index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 8ce57398c..61a962fec 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -458,4 +458,4 @@
}
})
-{% endblock %}
\ No newline at end of file
+{% endblock %}
From 0e5f6ac586d03288f42887b63eeb1694c66adf61 Mon Sep 17 00:00:00 2001
From: Gene Takavic
Date: Sun, 14 Aug 2022 23:52:55 +0200
Subject: [PATCH 10/73] adapt to bolt-nfc-android-app
---
lnbits/extensions/boltcards/README.md | 61 ++--
lnbits/extensions/boltcards/__init__.py | 9 +
lnbits/extensions/boltcards/crud.py | 45 ++-
lnbits/extensions/boltcards/migrations.py | 9 +-
lnbits/extensions/boltcards/models.py | 21 +-
.../extensions/boltcards/static/js/index.js | 299 +++++++++++++++++
.../boltcards/templates/boltcards/index.html | 314 ++++--------------
lnbits/extensions/boltcards/views_api.py | 97 ++----
8 files changed, 504 insertions(+), 351 deletions(-)
create mode 100644 lnbits/extensions/boltcards/static/js/index.js
diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md
index ca239e42e..5fa6a9784 100644
--- a/lnbits/extensions/boltcards/README.md
+++ b/lnbits/extensions/boltcards/README.md
@@ -2,13 +2,50 @@
This extension allows you to link your Bolt card with a LNbits instance and use it more securely then just with a static LNURLw on it. A technology called [Secure Unique NFC](https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow.
-***In order to use this extension you need to be able setup your card first.*** There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with your computer. Or it can be done with [https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter](TagWriter app by NXP) Android app.
+**Disclaim:** ***Use this only if you either know what you are doing or are enough reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
-## Setting the outside the extension - android
-- Write tags
+***In order to use this extension you need to be able setup your card.*** That is writting on the URL template pointing to your LNBits instance, configure some SUN (SDM) setting and optionaly changing the card keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [bolt-nfc-android-app](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes.
+
+## About the keys
+
+Up to five 16bytes keys can be stored on the card, numbered from 00 to 04. In the empty state they all should be set to zeros (00000000000000000000000000000000). For this extension only two keys need to be set:
+
+One for encrypting the card UID and the counter (p parameter), let's called it meta key, key #01or K1.
+
+One for calculating CMAC (c parameter), let's called it file key, key #02 or K2.
+
+The key #00, K0 or also auth key is skipped to be use as authentification key. Is not needed by this extension, but can be filled in order to write the keys in cooperation with bolt-nfc-android-app.
+
+***Always backup all keys that you're trying to write on the card. Without them you may not be able to change them in the future!***
+
+## LNURLw
+Create a withdraw link within the LNURLw extension before adding a card. Enable the `Use unique withdraw QR codes to reduce 'assmilking'` option.
+
+## Setting the card - bolt-nfc-android-app (easy way)
+So far, regarding the keys, the app can only write a new key set on an empty card (with zero keys). **When you write non zero (and 'non debug') keys, they can't be rewrite with this app.** You have to do it on your computer.
+
+- Read the card with the app. Note UID so you can fill it in the extension later.
+- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan`
+- Add new card in the extension.
+ - Leaving any key array empty means that key is 16bytes of zero (00000000000000000000000000000000).
+ - GENERATE KEY button fill the keys randomly. If there is "debug" in the card name, a debug set of keys is filled instead.
+ - Leaving initial counter empty means zero.
+- Open the card details. **Backup the keys.** Scan the QR with the app to write the keys on the card.
+
+## Setting the card - computer (hard way)
+
+Follow the guide.
+
+The URI should be `lnurlw://YOUR-DOMAIN.COM/boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000`
+
+Then fill up the card parameters in the extension. Card Auth key (K0) can be omitted. Initical counter can be 0.
+
+## Setting the card - android NXP app (hard way)
+- If you don't know the card ID, use NXP TagInfo app to find it out.
+- In the TagWriter app tap Write tags
- New Data Set > Link
- Set URI type to Custom URL
-- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scane?e=00000000000000000000000000000000&c=0000000000000000
+- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
- click Configure mirroring options
- Select Card Type NTAG 424 DNA
- Check Enable SDM Mirroring
@@ -23,18 +60,4 @@ This extension allows you to link your Bolt card with a LNbits instance and use
- Save & Write
- Scan with compatible Wallet
-## Setting the outside the extension - computer
-
-Follow the guide.
-
-The URI should be `lnurlw://YOUR-DOMAIN.COM/boltcards/api/v1/scane/?e=00000000000000000000000000000000&c=0000000000000000`
-
-(At this point the link is common to all cards. So the extension grabs one by one every added card's key and tries to decrypt the e parameter until there's a match.)
-
-Choose and note your Meta key and File key.
-
-## Adding the into the extension
-
-Create a withdraw link within the LNURLw extension before adding a card. Enable the `Use unique withdraw QR codes to reduce 'assmilking'` option.
-
-The card UID can be retrieve with `NFC TagInfo` mobile app or from `NXP TagXplorer` log. Use the keys you've set before. You can leave the counter zero, it gets synchronized with the first use.
\ No newline at end of file
+This app afaik cannot change the keys. If you cannot change them any other way, leave them empty in the extension dialog and remember you're not secure. Card Auth key (K0) can be omitted anyway. Initical counter can be 0.
diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py
index f1ef972eb..f53363411 100644
--- a/lnbits/extensions/boltcards/__init__.py
+++ b/lnbits/extensions/boltcards/__init__.py
@@ -1,10 +1,19 @@
from fastapi import APIRouter
+from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_boltcards")
+boltcards_static_files = [
+ {
+ "path": "/boltcards/static",
+ "app": StaticFiles(packages=[("lnbits", "extensions/boltcards/static")]),
+ "name": "boltcards_static",
+ }
+]
+
boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"])
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index 5c2824f4f..5affe3122 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -1,4 +1,4 @@
-from optparse import Option
+import secrets
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
@@ -18,10 +18,12 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
uid,
counter,
withdraw,
- file_key,
- meta_key
+ k0,
+ k1,
+ k2,
+ otp
)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
card_id,
@@ -30,11 +32,13 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
data.uid,
data.counter,
data.withdraw,
- data.file_key,
- data.meta_key,
+ data.k0,
+ data.k1,
+ data.k2,
+ secrets.token_hex(16),
),
)
- card = await get_card(card_id, 0)
+ card = await get_card(card_id)
assert card, "Newly created card couldn't be retrieved"
return card
@@ -69,14 +73,18 @@ async def get_all_cards() -> List[Card]:
return [Card(**row) for row in rows]
-async def get_card(card_id: str, id_is_uid: bool = False) -> Optional[Card]:
- sql = "SELECT * FROM boltcards.cards WHERE {} = ?".format(
- "uid" if id_is_uid else "id"
- )
- row = await db.fetchone(
- sql,
- card_id,
- )
+async def get_card(card_id: str) -> Optional[Card]:
+ row = await db.fetchone("SELECT * FROM boltcards.cards WHERE id = ?", (card_id,))
+ if not row:
+ return None
+
+ card = dict(**row)
+
+ return Card.parse_obj(card)
+
+
+async def get_card_by_otp(otp: str) -> Optional[Card]:
+ row = await db.fetchone("SELECT * FROM boltcards.cards WHERE otp = ?", (otp,))
if not row:
return None
@@ -96,6 +104,13 @@ async def update_card_counter(counter: int, id: str):
)
+async def update_card_otp(otp: str, id: str):
+ await db.execute(
+ "UPDATE boltcards.cards SET otp = ? WHERE id = ?",
+ (otp, id),
+ )
+
+
async def get_hit(hit_id: str) -> Optional[Hit]:
row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id))
if not row:
diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py
index 6e0fa0723..7dc5acb44 100644
--- a/lnbits/extensions/boltcards/migrations.py
+++ b/lnbits/extensions/boltcards/migrations.py
@@ -11,8 +11,13 @@ async def m001_initial(db):
uid TEXT NOT NULL,
counter INT NOT NULL DEFAULT 0,
withdraw TEXT NOT NULL,
- file_key TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
- meta_key TEXT NOT NULL DEFAULT '',
+ 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
+ """
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index b6d521c3c..6e1997545 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -1,6 +1,8 @@
from fastapi.params import Query
from pydantic import BaseModel
+ZERO_KEY = "00000000000000000000000000000000"
+
class Card(BaseModel):
id: str
@@ -9,18 +11,27 @@ class Card(BaseModel):
uid: str
counter: int
withdraw: str
- file_key: str
- meta_key: str
+ k0: str
+ k1: str
+ k2: str
+ prev_k0: str
+ prev_k1: str
+ prev_k2: str
+ otp: str
time: int
class CreateCardData(BaseModel):
card_name: str = Query(...)
uid: str = Query(...)
- counter: str = Query(...)
+ counter: int = Query(0)
withdraw: str = Query(...)
- file_key: str = Query(...)
- meta_key: str = Query(...)
+ k0: str = Query(ZERO_KEY)
+ k1: str = Query(ZERO_KEY)
+ k2: str = Query(ZERO_KEY)
+ prev_k0: str = Query(ZERO_KEY)
+ prev_k1: str = Query(ZERO_KEY)
+ prev_k2: str = Query(ZERO_KEY)
class Hit(BaseModel):
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
new file mode 100644
index 000000000..e2afbf1e4
--- /dev/null
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -0,0 +1,299 @@
+Vue.component(VueQrcode.name, VueQrcode)
+
+const mapCards = obj => {
+ obj.date = Quasar.utils.date.formatDate(
+ new Date(obj.time * 1000),
+ 'YYYY-MM-DD HH:mm'
+ )
+
+ return obj
+}
+
+new Vue({
+ el: '#vue',
+ mixins: [windowMixin],
+ data: function () {
+ return {
+ cards: [],
+ hits: [],
+ withdrawsOptions: [],
+ cardDialog: {
+ show: false,
+ data: {},
+ temp: {}
+ },
+ cardsTable: {
+ columns: [
+ {
+ name: 'card_name',
+ align: 'left',
+ label: 'Card name',
+ field: 'card_name'
+ },
+ {
+ name: 'counter',
+ align: 'left',
+ label: 'Counter',
+ field: 'counter'
+ },
+ {
+ name: 'withdraw',
+ align: 'left',
+ label: 'Withdraw ID',
+ field: 'withdraw'
+ }
+ ],
+ pagination: {
+ rowsPerPage: 10
+ }
+ },
+ hitsTable: {
+ columns: [
+ {
+ name: 'card_name',
+ align: 'left',
+ label: 'Card name',
+ field: 'card_name'
+ },
+ {
+ name: 'old_ctr',
+ align: 'left',
+ label: 'Old counter',
+ field: 'old_ctr'
+ },
+ {
+ name: 'new_ctr',
+ align: 'left',
+ label: 'New counter',
+ field: 'new_ctr'
+ },
+ {
+ name: 'date',
+ align: 'left',
+ label: 'Time',
+ field: 'date'
+ },
+ {
+ name: 'ip',
+ align: 'left',
+ label: 'IP',
+ field: 'ip'
+ },
+ {
+ name: 'useragent',
+ align: 'left',
+ label: 'User agent',
+ field: 'useragent'
+ }
+ ],
+ pagination: {
+ rowsPerPage: 10,
+ sortBy: 'date',
+ descending: true
+ }
+ },
+ qrCodeDialog: {
+ show: false,
+ data: null
+ }
+ }
+ },
+ methods: {
+ getCards: function () {
+ var self = this
+
+ LNbits.api
+ .request(
+ 'GET',
+ '/boltcards/api/v1/cards?all_wallets=true',
+ this.g.user.wallets[0].inkey
+ )
+ .then(function (response) {
+ self.cards = response.data.map(function (obj) {
+ return mapCards(obj)
+ })
+ console.log(self.cards)
+ })
+ },
+ getHits: function () {
+ var self = this
+
+ LNbits.api
+ .request(
+ 'GET',
+ '/boltcards/api/v1/hits?all_wallets=true',
+ this.g.user.wallets[0].inkey
+ )
+ .then(function (response) {
+ self.hits = response.data.map(function (obj) {
+ obj.card_name = self.cards.find(d => d.id == obj.card_id).card_name
+ return mapCards(obj)
+ })
+ console.log(self.hits)
+ })
+ },
+ getWithdraws: function () {
+ var self = this
+
+ LNbits.api
+ .request(
+ 'GET',
+ '/withdraw/api/v1/links?all_wallets=true',
+ this.g.user.wallets[0].inkey
+ )
+ .then(function (response) {
+ self.withdrawsOptions = response.data.map(function (obj) {
+ return {
+ label: [obj.title, ' - ', obj.id].join(''),
+ value: obj.id
+ }
+ })
+ console.log(self.withdraws)
+ })
+ },
+ openQrCodeDialog(cardId) {
+ var card = _.findWhere(this.cards, {id: cardId})
+
+ this.qrCodeDialog.data = {
+ link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
+ name: card.card_name,
+ uid: card.uid,
+ k0: card.k0,
+ k1: card.k1,
+ k2: card.k2
+ }
+ this.qrCodeDialog.show = true
+ },
+ generateKeys: function () {
+ const genRanHex = size =>
+ [...Array(size)]
+ .map(() => Math.floor(Math.random() * 16).toString(16))
+ .join('')
+
+ debugcard =
+ typeof this.cardDialog.data.card_name === 'string' &&
+ this.cardDialog.data.card_name.search('debug') > -1
+
+ this.cardDialog.data.k0 = debugcard
+ ? '11111111111111111111111111111111'
+ : genRanHex(32)
+ this.$refs['k0'].value = this.cardDialog.data.k0
+
+ this.cardDialog.data.k1 = debugcard
+ ? '22222222222222222222222222222222'
+ : genRanHex(32)
+ this.$refs['k1'].value = this.cardDialog.data.k1
+
+ this.cardDialog.data.k2 = debugcard
+ ? '33333333333333333333333333333333'
+ : genRanHex(32)
+ this.$refs['k2'].value = this.cardDialog.data.k2
+ },
+ closeFormDialog: function () {
+ this.cardDialog.data = {}
+ },
+ sendFormData: function () {
+ let wallet = _.findWhere(this.g.user.wallets, {
+ id: this.cardDialog.data.wallet
+ })
+ let data = this.cardDialog.data
+ if (data.id) {
+ this.updateCard(wallet, data)
+ } else {
+ this.createCard(wallet, data)
+ }
+ },
+ createCard: function (wallet, data) {
+ var self = this
+
+ LNbits.api
+ .request('POST', '/boltcards/api/v1/cards', wallet.adminkey, data)
+ .then(function (response) {
+ self.cards.push(mapCards(response.data))
+ self.cardDialog.show = false
+ self.cardDialog.data = {}
+ })
+ .catch(function (error) {
+ LNbits.utils.notifyApiError(error)
+ })
+ },
+ updateCardDialog: function (formId) {
+ var card = _.findWhere(this.cards, {id: formId})
+ console.log(card.id)
+ this.cardDialog.data = _.clone(card)
+
+ this.cardDialog.temp.k0 = this.cardDialog.data.k0
+ this.cardDialog.temp.k1 = this.cardDialog.data.k1
+ this.cardDialog.temp.k2 = this.cardDialog.data.k2
+
+ this.cardDialog.show = true
+ },
+ updateCard: function (wallet, data) {
+ var self = this
+
+ if (
+ this.cardDialog.temp.k0 != data.k0 ||
+ this.cardDialog.temp.k1 != data.k1 ||
+ this.cardDialog.temp.k2 != data.k2
+ ) {
+ data.prev_k0 = this.cardDialog.temp.k0
+ data.prev_k1 = this.cardDialog.temp.k1
+ data.prev_k2 = this.cardDialog.temp.k2
+ }
+
+ console.log(data)
+
+ LNbits.api
+ .request(
+ 'PUT',
+ '/boltcards/api/v1/cards/' + data.id,
+ wallet.adminkey,
+ data
+ )
+ .then(function (response) {
+ self.cards = _.reject(self.cards, function (obj) {
+ return obj.id == data.id
+ })
+ self.cards.push(mapCards(response.data))
+ self.cardDialog.show = false
+ self.cardDialog.data = {}
+ })
+ .catch(function (error) {
+ LNbits.utils.notifyApiError(error)
+ })
+ },
+ deleteCard: function (cardId) {
+ let self = this
+ let cards = _.findWhere(this.cards, {id: cardId})
+
+ LNbits.utils
+ .confirmDialog('Are you sure you want to delete this card')
+ .onOk(function () {
+ LNbits.api
+ .request(
+ 'DELETE',
+ '/boltcards/api/v1/cards/' + cardId,
+ _.findWhere(self.g.user.wallets, {id: cards.wallet}).adminkey
+ )
+ .then(function (response) {
+ self.cards = _.reject(self.cards, function (obj) {
+ return obj.id == cardId
+ })
+ })
+ .catch(function (error) {
+ LNbits.utils.notifyApiError(error)
+ })
+ })
+ },
+ exportCardsCSV: function () {
+ LNbits.utils.exportCSV(this.cardsTable.columns, this.cards)
+ }
+ },
+ created: function () {
+ if (this.g.user.wallets.length) {
+ this.getCards()
+ this.getHits()
+ this.getWithdraws()
+ }
+ }
+})
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 61a962fec..a6961fe59 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -33,6 +33,7 @@
{% raw %}
+
{{ col.label }}
@@ -42,6 +43,16 @@
+
+
+
{{ col.value }}
@@ -166,20 +177,33 @@
+
+
Update FormUpdate Card
Create Card
+ Generate keys
Cancel
@@ -213,249 +245,35 @@
+
+
+
+ {% raw %}
+
+
+
+
+ (QR code is for setting the keys with bolt-nfc-android-app)
+
+
+ Name: {{ qrCodeDialog.data.name }}
+ UID: {{ qrCodeDialog.data.uid }}
+ Lock key: {{ qrCodeDialog.data.k0 }}
+ Meta key: {{ qrCodeDialog.data.k1 }}
+ File key: {{ qrCodeDialog.data.k2 }}
+
+ {% endraw %}
+
+ Close
+
+
+
{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
+
{% endblock %}
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index fbd05cce0..2ac824d0c 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -6,6 +6,7 @@
# (use httpx just like requests, except instead of response.ok there's only the
# response.is_error that is its inverse)
+import secrets
from http import HTTPStatus
from fastapi.params import Depends, Query
@@ -23,10 +24,12 @@ from .crud import (
delete_card,
get_all_cards,
get_card,
+ get_card_by_otp,
get_cards,
get_hits,
update_card,
update_card_counter,
+ update_card_otp,
)
from .models import CreateCardData
from .nxp424 import decryptSUN, getSunMAC
@@ -46,7 +49,7 @@ async def api_cards(
@boltcards_ext.post("/api/v1/cards", status_code=HTTPStatus.CREATED)
@boltcards_ext.put("/api/v1/cards/{card_id}", status_code=HTTPStatus.OK)
-async def api_link_create_or_update(
+async def api_card_create_or_update(
# req: Request,
data: CreateCardData,
card_id: str = None,
@@ -69,7 +72,7 @@ async def api_link_create_or_update(
@boltcards_ext.delete("/api/v1/cards/{card_id}")
-async def api_link_delete(card_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
+async def api_card_delete(card_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
card = await get_card(card_id)
if not card:
@@ -101,69 +104,17 @@ async def api_hits(
return [hit.dict() for hit in await get_hits(cards_ids)]
-# /boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000
-@boltcards_ext.get("/api/v1/scan/")
-async def api_scan(uid, ctr, c, request: Request):
- card = await get_card(uid, id_is_uid=True)
-
- if card == None:
- return {"status": "ERROR", "reason": "Unknown card."}
-
- if (
- c
- != getSunMAC(
- bytes.fromhex(uid), bytes.fromhex(ctr)[::-1], bytes.fromhex(card.file_key)
- )
- .hex()
- .upper()
- ):
- print(c)
- print(
- getSunMAC(
- bytes.fromhex(uid),
- bytes.fromhex(ctr)[::-1],
- bytes.fromhex(card.file_key),
- )
- .hex()
- .upper()
- )
- return {"status": "ERROR", "reason": "CMAC does not check."}
-
- ctr_int = int(ctr, 16)
-
- if ctr_int <= card.counter:
- return {"status": "ERROR", "reason": "This link is already used."}
-
- await update_card_counter(ctr_int, card.id)
-
- # gathering some info for hit record
- ip = request.client.host
- if request.headers["x-real-ip"]:
- ip = request.headers["x-real-ip"]
- elif request.headers["x-forwarded-for"]:
- ip = request.headers["x-forwarded-for"]
-
- agent = request.headers["user-agent"] if "user-agent" in request.headers else ""
-
- await create_hit(card.id, ip, agent, card.counter, ctr_int)
-
- link = await get_withdraw_link(card.withdraw, 0)
- return link.lnurl_response(request)
-
-
-# /boltcards/api/v1/scane/?e=00000000000000000000000000000000&c=0000000000000000
-@boltcards_ext.get("/api/v1/scane/")
-async def api_scane(e, c, request: Request):
+# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
+@boltcards_ext.get("/api/v1/scan")
+async def api_scane(p, c, request: Request):
card = None
counter = b""
# since this route is common to all cards I don't know whitch 'meta key' to use
# so I try one by one until decrypted uid matches
for cand in await get_all_cards():
- if cand.meta_key:
- card_uid, counter = decryptSUN(
- bytes.fromhex(e), bytes.fromhex(cand.meta_key)
- )
+ if cand.k1:
+ card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(cand.k1))
if card_uid.hex().upper() == cand.uid:
card = cand
@@ -172,9 +123,7 @@ async def api_scane(e, c, request: Request):
if card == None:
return {"status": "ERROR", "reason": "Unknown card."}
- if c != getSunMAC(card_uid, counter, bytes.fromhex(card.file_key)).hex().upper():
- print(c)
- print(getSunMAC(card_uid, counter, bytes.fromhex(card.file_key)).hex().upper())
+ if c != getSunMAC(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
return {"status": "ERROR", "reason": "CMAC does not check."}
ctr_int = int.from_bytes(counter, "little")
@@ -196,3 +145,27 @@ async def api_scane(e, c, request: Request):
link = await get_withdraw_link(card.withdraw, 0)
return link.lnurl_response(request)
+
+
+# /boltcards/api/v1/auth?a=00000000000000000000000000000000
+@boltcards_ext.get("/api/v1/auth")
+async def api_auth(a, request: Request):
+ if a == "00000000000000000000000000000000":
+ response = {"k0": "0" * 32, "k1": "1" * 32, "k2": "2" * 32}
+ return response
+
+ card = await get_card_by_otp(a)
+
+ if not card:
+ raise HTTPException(
+ detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
+ )
+
+ new_otp = secrets.token_hex(16)
+ print(card.otp)
+ print(new_otp)
+ await update_card_otp(new_otp, card.id)
+
+ response = {"k0": card.k0, "k1": card.k1, "k2": card.k2}
+
+ return response
From 89130bd1fb8b585b657bad4c56f78be792f3a224 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 14 Aug 2022 18:34:19 -0600
Subject: [PATCH 11/73] once over on README
---
lnbits/extensions/boltcards/README.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md
index 5fa6a9784..71e4bcb54 100644
--- a/lnbits/extensions/boltcards/README.md
+++ b/lnbits/extensions/boltcards/README.md
@@ -1,20 +1,20 @@
# Bolt cards (NXP NTAG) Extension
-This extension allows you to link your Bolt card with a LNbits instance and use it more securely then just with a static LNURLw on it. A technology called [Secure Unique NFC](https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow.
+This extension allows you to link your Bolt Card (or other compatible NXP NTAG device) with a LNbits instance and use it in a more secure way than a static LNURLw. A technology called [Secure Unique NFC](https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow.
-**Disclaim:** ***Use this only if you either know what you are doing or are enough reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
+**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
-***In order to use this extension you need to be able setup your card.*** That is writting on the URL template pointing to your LNBits instance, configure some SUN (SDM) setting and optionaly changing the card keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [bolt-nfc-android-app](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes.
+***In order to use this extension you need to be able to setup your own card.*** That means writing a URL template pointing to your LNBits instance, configuring some SUN (SDM) settings and optionally changing the card's keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [bolt-nfc-android-app](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes.
## About the keys
-Up to five 16bytes keys can be stored on the card, numbered from 00 to 04. In the empty state they all should be set to zeros (00000000000000000000000000000000). For this extension only two keys need to be set:
+Up to five 16-byte keys can be stored on the card, numbered from 00 to 04. In the empty state they all should be set to zeros (00000000000000000000000000000000). For this extension only two keys need to be set:
-One for encrypting the card UID and the counter (p parameter), let's called it meta key, key #01or K1.
+One for encrypting the card UID and the counter (p parameter), let's called it meta key, key #01 or K1.
One for calculating CMAC (c parameter), let's called it file key, key #02 or K2.
-The key #00, K0 or also auth key is skipped to be use as authentification key. Is not needed by this extension, but can be filled in order to write the keys in cooperation with bolt-nfc-android-app.
+The key #00, K0 (also know as auth key) is skipped to be use as authentification key. Is not needed by this extension, but can be filled in order to write the keys in cooperation with bolt-nfc-android-app.
***Always backup all keys that you're trying to write on the card. Without them you may not be able to change them in the future!***
From 43db91641a4b11daa0377233961337505bb8a865 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Fri, 19 Aug 2022 07:34:46 -0600
Subject: [PATCH 12/73] .upper() on p and c to handle breez wallet lower-casing
the whole url
---
lnbits/extensions/boltcards/views_api.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 2ac824d0c..d1f3cb395 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -107,6 +107,9 @@ async def api_hits(
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
@boltcards_ext.get("/api/v1/scan")
async def api_scane(p, c, request: Request):
+ # some wallets send everything as lower case, no bueno
+ p = p.upper()
+ c = c.upper()
card = None
counter = b""
From a28bedf430bcd333e1f94dbeb8245e0c1631274a Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Fri, 19 Aug 2022 08:45:27 -0600
Subject: [PATCH 13/73] update link, this will work once merged to main
---
lnbits/extensions/boltcards/templates/boltcards/_api_docs.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html b/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html
index 26d37a73e..e67ba14bf 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html
@@ -11,7 +11,7 @@
Manage your Bolt Cards self custodian way
More details
From b80f2f8e4d6c325b9d39a8cd297bd79f2fd85db0 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Fri, 19 Aug 2022 16:47:33 -0600
Subject: [PATCH 14/73] error handling if bad input data in db
---
lnbits/extensions/boltcards/views_api.py | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index d1f3cb395..2271f1504 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -117,11 +117,14 @@ async def api_scane(p, c, request: Request):
# so I try one by one until decrypted uid matches
for cand in await get_all_cards():
if cand.k1:
- card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(cand.k1))
+ try:
+ card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(cand.k1))
- if card_uid.hex().upper() == cand.uid:
- card = cand
- break
+ if card_uid.hex().upper() == cand.uid:
+ card = cand
+ break
+ except:
+ continue
if card == None:
return {"status": "ERROR", "reason": "Unknown card."}
From 302ccfd429226d43f31dd1b189de64df0f67e0be Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Fri, 19 Aug 2022 16:52:06 -0600
Subject: [PATCH 15/73] add better error handling
---
lnbits/extensions/boltcards/views_api.py | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 2271f1504..d58369f7f 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -55,6 +55,26 @@ async def api_card_create_or_update(
card_id: str = None,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
+ if(len(bytes.fromhex(data.uid)) != 7):
+ raise HTTPException(
+ detail="Invalid bytes for card uid.", status_code=HTTPStatus.BAD_REQUEST
+ )
+
+ if(len(bytes.fromhex(data.k0)) != 16):
+ raise HTTPException(
+ detail="Invalid bytes for k0.", status_code=HTTPStatus.BAD_REQUEST
+ )
+
+ if(len(bytes.fromhex(data.k1)) != 16):
+ raise HTTPException(
+ detail="Invalid bytes for k1.", status_code=HTTPStatus.BAD_REQUEST
+ )
+
+ if(len(bytes.fromhex(data.k2)) != 16):
+ raise HTTPException(
+ detail="Invalid bytes for k2.", status_code=HTTPStatus.BAD_REQUEST
+ )
+
if card_id:
card = await get_card(card_id)
if not card:
From 6898412e6cfcaa0e07d926e179aa4c366ba8960f Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Fri, 19 Aug 2022 16:54:06 -0600
Subject: [PATCH 16/73] more validation
---
lnbits/extensions/boltcards/views_api.py | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index d58369f7f..5611d5b74 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -55,25 +55,30 @@ async def api_card_create_or_update(
card_id: str = None,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
- if(len(bytes.fromhex(data.uid)) != 7):
- raise HTTPException(
+ try:
+ if len(bytes.fromhex(data.uid)) != 7:
+ raise HTTPException(
detail="Invalid bytes for card uid.", status_code=HTTPStatus.BAD_REQUEST
)
- if(len(bytes.fromhex(data.k0)) != 16):
- raise HTTPException(
+ if len(bytes.fromhex(data.k0)) != 16:
+ raise HTTPException(
detail="Invalid bytes for k0.", status_code=HTTPStatus.BAD_REQUEST
)
- if(len(bytes.fromhex(data.k1)) != 16):
- raise HTTPException(
+ if len(bytes.fromhex(data.k1)) != 16:
+ raise HTTPException(
detail="Invalid bytes for k1.", status_code=HTTPStatus.BAD_REQUEST
)
- if(len(bytes.fromhex(data.k2)) != 16):
- raise HTTPException(
+ if len(bytes.fromhex(data.k2)) != 16:
+ raise HTTPException(
detail="Invalid bytes for k2.", status_code=HTTPStatus.BAD_REQUEST
)
+ except:
+ raise HTTPException(
+ detail="Invalid byte data provided.", status_code=HTTPStatus.BAD_REQUEST
+ )
if card_id:
card = await get_card(card_id)
From db83d803f87011c113a3d1798af8e04497390a56 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 07:32:11 -0600
Subject: [PATCH 17/73] add .upper() to other side of the condition
---
lnbits/extensions/boltcards/views_api.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 5611d5b74..151aebcb6 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -145,7 +145,7 @@ async def api_scane(p, c, request: Request):
try:
card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(cand.k1))
- if card_uid.hex().upper() == cand.uid:
+ if card_uid.hex().upper() == cand.uid.upper():
card = cand
break
except:
From 4242a8202924d8de0f23fef83d233bda7f52a698 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 08:58:38 -0600
Subject: [PATCH 18/73] Add a secondary route with the card_uid appended to it.
---
lnbits/extensions/boltcards/crud.py | 8 +++++++
lnbits/extensions/boltcards/views_api.py | 29 ++++++++++++++----------
2 files changed, 25 insertions(+), 12 deletions(-)
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index 5affe3122..2e19c0793 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -82,6 +82,14 @@ async def get_card(card_id: str) -> Optional[Card]:
return Card.parse_obj(card)
+async def get_card_by_uid(card_uid: str) -> Optional[Card]:
+ row = await db.fetchone("SELECT * FROM boltcards.cards WHERE uid = ?", (card_uid,))
+ if not row:
+ return None
+
+ card = dict(**row)
+
+ return Card.parse_obj(card)
async def get_card_by_otp(otp: str) -> Optional[Card]:
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE otp = ?", (otp,))
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 151aebcb6..7c3a2e03d 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -25,6 +25,7 @@ from .crud import (
get_all_cards,
get_card,
get_card_by_otp,
+ get_card_by_uid,
get_cards,
get_hits,
update_card,
@@ -131,25 +132,29 @@ async def api_hits(
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
@boltcards_ext.get("/api/v1/scan")
-async def api_scane(p, c, request: Request):
+@boltcards_ext.get("/api/v1/scan/{card_uid}")
+async def api_scane(p, c, card_uid: str = None, request: Request):
# some wallets send everything as lower case, no bueno
p = p.upper()
c = c.upper()
card = None
counter = b""
- # since this route is common to all cards I don't know whitch 'meta key' to use
- # so I try one by one until decrypted uid matches
- for cand in await get_all_cards():
- if cand.k1:
- try:
- card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(cand.k1))
+ if not card_uid:
+ # since this route is common to all cards I don't know whitch 'meta key' to use
+ # so I try one by one until decrypted uid matches
+ for cand in await get_all_cards():
+ if cand.k1:
+ try:
+ card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(cand.k1))
- if card_uid.hex().upper() == cand.uid.upper():
- card = cand
- break
- except:
- continue
+ if card_uid.hex().upper() == cand.uid.upper():
+ card = cand
+ break
+ except:
+ continue
+ else:
+ card = await get_card_by_uid(card_uid)
if card == None:
return {"status": "ERROR", "reason": "Unknown card."}
From 9dd7d30716e45fefbddbde01d8cd2fe5a2c14906 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 09:02:20 -0600
Subject: [PATCH 19/73] format
---
lnbits/extensions/boltcards/crud.py | 2 ++
lnbits/extensions/boltcards/views_api.py | 6 ++++--
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index 2e19c0793..a91561755 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -82,6 +82,7 @@ async def get_card(card_id: str) -> Optional[Card]:
return Card.parse_obj(card)
+
async def get_card_by_uid(card_uid: str) -> Optional[Card]:
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE uid = ?", (card_uid,))
if not row:
@@ -91,6 +92,7 @@ async def get_card_by_uid(card_uid: str) -> Optional[Card]:
return Card.parse_obj(card)
+
async def get_card_by_otp(otp: str) -> Optional[Card]:
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE otp = ?", (otp,))
if not row:
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 7c3a2e03d..ada7c7087 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -133,7 +133,7 @@ async def api_hits(
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
@boltcards_ext.get("/api/v1/scan")
@boltcards_ext.get("/api/v1/scan/{card_uid}")
-async def api_scane(p, c, card_uid: str = None, request: Request):
+async def api_scane(p, c, request: Request, card_uid: str = None):
# some wallets send everything as lower case, no bueno
p = p.upper()
c = c.upper()
@@ -146,7 +146,9 @@ async def api_scane(p, c, card_uid: str = None, request: Request):
for cand in await get_all_cards():
if cand.k1:
try:
- card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(cand.k1))
+ card_uid, counter = decryptSUN(
+ bytes.fromhex(p), bytes.fromhex(cand.k1)
+ )
if card_uid.hex().upper() == cand.uid.upper():
card = cand
From f41c43cbf4976b8b408af505712ad0cd5b94aae3 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 09:09:01 -0600
Subject: [PATCH 20/73] get the uid back into bytes
---
lnbits/extensions/boltcards/views_api.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index ada7c7087..960ce43e1 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -133,7 +133,7 @@ async def api_hits(
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
@boltcards_ext.get("/api/v1/scan")
@boltcards_ext.get("/api/v1/scan/{card_uid}")
-async def api_scane(p, c, request: Request, card_uid: str = None):
+async def api_scan(p, c, request: Request, card_uid: str = None):
# some wallets send everything as lower case, no bueno
p = p.upper()
c = c.upper()
@@ -156,7 +156,11 @@ async def api_scane(p, c, request: Request, card_uid: str = None):
except:
continue
else:
- card = await get_card_by_uid(card_uid)
+ try:
+ card = await get_card_by_uid(card_uid)
+ card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
+ except:
+ return {"status": "ERROR", "reason": "Error decrypting card."}
if card == None:
return {"status": "ERROR", "reason": "Unknown card."}
From a8bc3ea8704118161a5ddb44f2c46737937dc9ae Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 09:46:18 -0600
Subject: [PATCH 21/73] card_uid is always upper
---
lnbits/extensions/boltcards/crud.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index a91561755..8707a9697 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -29,7 +29,7 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
card_id,
wallet_id,
data.card_name,
- data.uid,
+ data.uid.upper(),
data.counter,
data.withdraw,
data.k0,
@@ -46,6 +46,8 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
async def update_card(card_id: str, **kwargs) -> Optional[Card]:
if "is_unique" in kwargs:
kwargs["is_unique"] = int(kwargs["is_unique"])
+ if "uid" in kwargs:
+ kwargs["uid"] = kwargs["uid"].upper()
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE boltcards.cards SET {q} WHERE id = ?",
@@ -84,7 +86,9 @@ async def get_card(card_id: str) -> Optional[Card]:
async def get_card_by_uid(card_uid: str) -> Optional[Card]:
- row = await db.fetchone("SELECT * FROM boltcards.cards WHERE uid = ?", (card_uid,))
+ row = await db.fetchone(
+ "SELECT * FROM boltcards.cards WHERE uid = ?", (card_uid.upper(),)
+ )
if not row:
return None
From 971d8f34e80c69edbede455037bae0028c7cc482 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 10:00:16 -0600
Subject: [PATCH 22/73] convert to using lnbits card id instead of card uid
---
lnbits/extensions/boltcards/crud.py | 12 ------------
lnbits/extensions/boltcards/views_api.py | 9 ++++-----
2 files changed, 4 insertions(+), 17 deletions(-)
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index 8707a9697..f6ab22396 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -85,18 +85,6 @@ async def get_card(card_id: str) -> Optional[Card]:
return Card.parse_obj(card)
-async def get_card_by_uid(card_uid: str) -> Optional[Card]:
- row = await db.fetchone(
- "SELECT * FROM boltcards.cards WHERE uid = ?", (card_uid.upper(),)
- )
- if not row:
- return None
-
- card = dict(**row)
-
- return Card.parse_obj(card)
-
-
async def get_card_by_otp(otp: str) -> Optional[Card]:
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE otp = ?", (otp,))
if not row:
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 960ce43e1..d4a990f20 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -25,7 +25,6 @@ from .crud import (
get_all_cards,
get_card,
get_card_by_otp,
- get_card_by_uid,
get_cards,
get_hits,
update_card,
@@ -132,15 +131,15 @@ async def api_hits(
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
@boltcards_ext.get("/api/v1/scan")
-@boltcards_ext.get("/api/v1/scan/{card_uid}")
-async def api_scan(p, c, request: Request, card_uid: str = None):
+@boltcards_ext.get("/api/v1/scan/{card_id}")
+async def api_scan(p, c, request: Request, card_id: str = None):
# some wallets send everything as lower case, no bueno
p = p.upper()
c = c.upper()
card = None
counter = b""
- if not card_uid:
+ if not card_id:
# since this route is common to all cards I don't know whitch 'meta key' to use
# so I try one by one until decrypted uid matches
for cand in await get_all_cards():
@@ -157,7 +156,7 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
continue
else:
try:
- card = await get_card_by_uid(card_uid)
+ card = await get_card(card_id)
card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
except:
return {"status": "ERROR", "reason": "Error decrypting card."}
From b11ddf3d61a326aa2545b7618548c0b11a075964 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 10:06:07 -0600
Subject: [PATCH 23/73] surface the ID
---
lnbits/extensions/boltcards/static/js/index.js | 7 +++++++
lnbits/extensions/boltcards/templates/boltcards/index.html | 1 +
2 files changed, 8 insertions(+)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index e2afbf1e4..078ed40b8 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -24,6 +24,12 @@ new Vue({
},
cardsTable: {
columns: [
+ {
+ name: 'id',
+ align: 'left',
+ label: 'ID',
+ field: 'id'
+ },
{
name: 'card_name',
align: 'left',
@@ -156,6 +162,7 @@ new Vue({
this.qrCodeDialog.data = {
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
+ id: card.id,
name: card.card_name,
uid: card.uid,
k0: card.k0,
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index a6961fe59..5d9a2f884 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -260,6 +260,7 @@
(QR code is for setting the keys with bolt-nfc-android-app)
+ ID: {{qrCodeDialog.data.id}}
Name: {{ qrCodeDialog.data.name }}
UID: {{ qrCodeDialog.data.uid }}
Lock key: {{ qrCodeDialog.data.k0 }}
From ec9005ffcac4455c9e2c298844dbc23b75166f8c Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 10:18:28 -0600
Subject: [PATCH 24/73] add info about {card_id} in readme
---
lnbits/extensions/boltcards/README.md | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md
index 71e4bcb54..31a8dc50f 100644
--- a/lnbits/extensions/boltcards/README.md
+++ b/lnbits/extensions/boltcards/README.md
@@ -25,7 +25,11 @@ Create a withdraw link within the LNURLw extension before adding a card. Enable
So far, regarding the keys, the app can only write a new key set on an empty card (with zero keys). **When you write non zero (and 'non debug') keys, they can't be rewrite with this app.** You have to do it on your computer.
- Read the card with the app. Note UID so you can fill it in the extension later.
-- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan`
+- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{card_id}`
+ - `{card_id}` is optional
+ - If you include the `{card_id}`, there is a slight potential privacy leak where each place you tap your card will see this `{card_id}` in plain-text. As one example, merchants could potentially use this static identifier for the card to build a profile around you. If you are on a shared, large LNBits host this option is probably best for you as the system is more efficient this way.
+ - If you don't include `{card_id}`, then you will have gained a tiny bit more privacy by not exposing any static identifier to anyone. The downside to this is that the processing LNBits does after you tap your card is more computationally expensive. If you are on a dedicated, private LNBits instance this option is probably best for you as the efficiency gains of adding `{card_id}` are irrelevant in this use case.
+
- Add new card in the extension.
- Leaving any key array empty means that key is 16bytes of zero (00000000000000000000000000000000).
- GENERATE KEY button fill the keys randomly. If there is "debug" in the card name, a debug set of keys is filled instead.
From c4ffd4548517b306a3c0340452f46f79f8b705f2 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 10:19:34 -0600
Subject: [PATCH 25/73] more readme
---
lnbits/extensions/boltcards/README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md
index 31a8dc50f..da0b36aa7 100644
--- a/lnbits/extensions/boltcards/README.md
+++ b/lnbits/extensions/boltcards/README.md
@@ -26,7 +26,7 @@ So far, regarding the keys, the app can only write a new key set on an empty car
- Read the card with the app. Note UID so you can fill it in the extension later.
- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{card_id}`
- - `{card_id}` is optional
+ - `{card_id}` is optional. This field denotes the internal LNBits ID for the card you set up.
- If you include the `{card_id}`, there is a slight potential privacy leak where each place you tap your card will see this `{card_id}` in plain-text. As one example, merchants could potentially use this static identifier for the card to build a profile around you. If you are on a shared, large LNBits host this option is probably best for you as the system is more efficient this way.
- If you don't include `{card_id}`, then you will have gained a tiny bit more privacy by not exposing any static identifier to anyone. The downside to this is that the processing LNBits does after you tap your card is more computationally expensive. If you are on a dedicated, private LNBits instance this option is probably best for you as the efficiency gains of adding `{card_id}` are irrelevant in this use case.
@@ -49,7 +49,8 @@ Then fill up the card parameters in the extension. Card Auth key (K0) can be omi
- In the TagWriter app tap Write tags
- New Data Set > Link
- Set URI type to Custom URL
-- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
+- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{YOUR_CARD_ID}?p=00000000000000000000000000000000&c=0000000000000000
+ - See note above about including `{card_id}` in the URL.
- click Configure mirroring options
- Select Card Type NTAG 424 DNA
- Check Enable SDM Mirroring
From 38ef4f3a781edbf38173ebe96dcfadc1831248ef Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 10:30:10 -0600
Subject: [PATCH 26/73] Revert "surface the ID"
This reverts commit b11ddf3d61a326aa2545b7618548c0b11a075964.
---
lnbits/extensions/boltcards/static/js/index.js | 7 -------
lnbits/extensions/boltcards/templates/boltcards/index.html | 1 -
2 files changed, 8 deletions(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 078ed40b8..e2afbf1e4 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -24,12 +24,6 @@ new Vue({
},
cardsTable: {
columns: [
- {
- name: 'id',
- align: 'left',
- label: 'ID',
- field: 'id'
- },
{
name: 'card_name',
align: 'left',
@@ -162,7 +156,6 @@ new Vue({
this.qrCodeDialog.data = {
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
- id: card.id,
name: card.card_name,
uid: card.uid,
k0: card.k0,
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 5d9a2f884..a6961fe59 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -260,7 +260,6 @@
(QR code is for setting the keys with bolt-nfc-android-app)
- ID: {{qrCodeDialog.data.id}}
Name: {{ qrCodeDialog.data.name }}
UID: {{ qrCodeDialog.data.uid }}
Lock key: {{ qrCodeDialog.data.k0 }}
From f6a301668b8eb4939c55e23d39f4c21052db980a Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 10:30:25 -0600
Subject: [PATCH 27/73] Revert "convert to using lnbits card id instead of card
uid"
This reverts commit 971d8f34e80c69edbede455037bae0028c7cc482.
---
lnbits/extensions/boltcards/crud.py | 12 ++++++++++++
lnbits/extensions/boltcards/views_api.py | 9 +++++----
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index f6ab22396..8707a9697 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -85,6 +85,18 @@ async def get_card(card_id: str) -> Optional[Card]:
return Card.parse_obj(card)
+async def get_card_by_uid(card_uid: str) -> Optional[Card]:
+ row = await db.fetchone(
+ "SELECT * FROM boltcards.cards WHERE uid = ?", (card_uid.upper(),)
+ )
+ if not row:
+ return None
+
+ card = dict(**row)
+
+ return Card.parse_obj(card)
+
+
async def get_card_by_otp(otp: str) -> Optional[Card]:
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE otp = ?", (otp,))
if not row:
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index d4a990f20..960ce43e1 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -25,6 +25,7 @@ from .crud import (
get_all_cards,
get_card,
get_card_by_otp,
+ get_card_by_uid,
get_cards,
get_hits,
update_card,
@@ -131,15 +132,15 @@ async def api_hits(
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
@boltcards_ext.get("/api/v1/scan")
-@boltcards_ext.get("/api/v1/scan/{card_id}")
-async def api_scan(p, c, request: Request, card_id: str = None):
+@boltcards_ext.get("/api/v1/scan/{card_uid}")
+async def api_scan(p, c, request: Request, card_uid: str = None):
# some wallets send everything as lower case, no bueno
p = p.upper()
c = c.upper()
card = None
counter = b""
- if not card_id:
+ if not card_uid:
# since this route is common to all cards I don't know whitch 'meta key' to use
# so I try one by one until decrypted uid matches
for cand in await get_all_cards():
@@ -156,7 +157,7 @@ async def api_scan(p, c, request: Request, card_id: str = None):
continue
else:
try:
- card = await get_card(card_id)
+ card = await get_card_by_uid(card_uid)
card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
except:
return {"status": "ERROR", "reason": "Error decrypting card."}
From a6f0acc03097e47b748bc91c1512529e202afa9a Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 10:31:47 -0600
Subject: [PATCH 28/73] update readme
---
lnbits/extensions/boltcards/README.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md
index da0b36aa7..9646925e0 100644
--- a/lnbits/extensions/boltcards/README.md
+++ b/lnbits/extensions/boltcards/README.md
@@ -25,10 +25,10 @@ Create a withdraw link within the LNURLw extension before adding a card. Enable
So far, regarding the keys, the app can only write a new key set on an empty card (with zero keys). **When you write non zero (and 'non debug') keys, they can't be rewrite with this app.** You have to do it on your computer.
- Read the card with the app. Note UID so you can fill it in the extension later.
-- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{card_id}`
- - `{card_id}` is optional. This field denotes the internal LNBits ID for the card you set up.
- - If you include the `{card_id}`, there is a slight potential privacy leak where each place you tap your card will see this `{card_id}` in plain-text. As one example, merchants could potentially use this static identifier for the card to build a profile around you. If you are on a shared, large LNBits host this option is probably best for you as the system is more efficient this way.
- - If you don't include `{card_id}`, then you will have gained a tiny bit more privacy by not exposing any static identifier to anyone. The downside to this is that the processing LNBits does after you tap your card is more computationally expensive. If you are on a dedicated, private LNBits instance this option is probably best for you as the efficiency gains of adding `{card_id}` are irrelevant in this use case.
+- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{card_uid}`
+ - `{card_uid}` is optional. This field denotes the 14-character (7 byte) UID unique to each NFC cardr from the factory.
+ - If you include the `{card_uid}`, there is a slight potential privacy leak where each place you tap your card will see this `{card_uid}` in plain-text. As one example, merchants could potentially use this static identifier for the card to build a profile around you. If you are on a shared, large LNBits host this option is probably best for you as the system is more efficient this way.
+ - If you don't include `{card_uid}`, then you will have gained a tiny bit more privacy by not exposing any static identifier to anyone. The downside to this is that the processing LNBits does after you tap your card is more computationally expensive. If you are on a dedicated, private LNBits instance this option is probably best for you as the efficiency gains of adding `{card_uid}` are irrelevant in this use case.
- Add new card in the extension.
- Leaving any key array empty means that key is 16bytes of zero (00000000000000000000000000000000).
@@ -49,8 +49,8 @@ Then fill up the card parameters in the extension. Card Auth key (K0) can be omi
- In the TagWriter app tap Write tags
- New Data Set > Link
- Set URI type to Custom URL
-- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{YOUR_CARD_ID}?p=00000000000000000000000000000000&c=0000000000000000
- - See note above about including `{card_id}` in the URL.
+- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{YOUR_card_uid}?p=00000000000000000000000000000000&c=0000000000000000
+ - See note above about including `{card_uid}` in the URL.
- click Configure mirroring options
- Select Card Type NTAG 424 DNA
- Check Enable SDM Mirroring
From a3f910acf4002695ef251229af8acd2ee3d72cc2 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Sun, 21 Aug 2022 13:17:44 -0600
Subject: [PATCH 29/73] additional validation
---
lnbits/extensions/boltcards/views_api.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 960ce43e1..37a796262 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -159,6 +159,9 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
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."}
From 4e68c114fd2ec6c5b5d8b7b53eb32f01a7c66c77 Mon Sep 17 00:00:00 2001
From: ben
Date: Mon, 22 Aug 2022 20:14:19 +0100
Subject: [PATCH 30/73] UI updated
---
.../extensions/boltcards/static/js/index.js | 6 +++--
.../boltcards/templates/boltcards/index.html | 25 +++++++++++--------
2 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index e2afbf1e4..d8beaa083 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -13,13 +13,14 @@ new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
- return {
+ return {
+ toggleAdvanced: false,
cards: [],
hits: [],
withdrawsOptions: [],
cardDialog: {
show: false,
- data: {},
+ data: {counter:1},
temp: {}
},
cardsTable: {
@@ -193,6 +194,7 @@ new Vue({
this.cardDialog.data = {}
},
sendFormData: function () {
+ this.generateKeys()
let wallet = _.findWhere(this.g.user.wallets, {
id: this.cardDialog.data.wallet
})
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index a6961fe59..8b06b7500 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -171,9 +171,13 @@
v-model.trim="cardDialog.data.uid"
type="text"
label="Card UID"
- hint="Card unique identificator (7 bytes in HEX)."
- >
+ >From the NFC 424 ntag card that will be loaded
+
+
Zero if you don't know.
+ Generate keys
+
Create Card
-
Generate keys
Cancel
From 56c9234aff636aec73acf935ee2631102628bd26 Mon Sep 17 00:00:00 2001
From: ben
Date: Mon, 22 Aug 2022 22:33:20 +0100
Subject: [PATCH 31/73] LNURLs handled internally
Has bugs
---
lnbits/extensions/boltcards/__init__.py | 2 +-
lnbits/extensions/boltcards/lnurl.py | 197 ++++++++++++++++++
lnbits/extensions/boltcards/migrations.py | 18 +-
lnbits/extensions/boltcards/models.py | 25 ++-
.../extensions/boltcards/static/js/index.js | 29 +--
.../boltcards/templates/boltcards/index.html | 77 ++++++-
lnbits/extensions/boltcards/views_api.py | 97 +--------
7 files changed, 312 insertions(+), 133 deletions(-)
create mode 100644 lnbits/extensions/boltcards/lnurl.py
diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py
index f53363411..5fcbd12e1 100644
--- a/lnbits/extensions/boltcards/__init__.py
+++ b/lnbits/extensions/boltcards/__init__.py
@@ -20,6 +20,6 @@ boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"])
def boltcards_renderer():
return template_renderer(["lnbits/extensions/boltcards/templates"])
-
+from .lnurl import * # noqa
from .views import * # noqa
from .views_api import * # noqa
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
new file mode 100644
index 000000000..51a85e8a9
--- /dev/null
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -0,0 +1,197 @@
+import base64
+import hashlib
+import hmac
+from http import HTTPStatus
+from io import BytesIO
+from typing import Optional
+
+from embit import bech32, compact
+from fastapi import Request
+from fastapi.param_functions import Query
+from starlette.exceptions import HTTPException
+
+from lnbits.core.services import create_invoice
+from lnbits.core.views.api import pay_invoice
+
+from lnurl import Lnurl, LnurlWithdrawResponse
+from lnurl import encode as lnurl_encode # type: ignore
+from lnurl.types import LnurlPayMetadata # type: ignore
+
+from . import boltcards_ext
+from .crud import (
+ create_hit,
+ get_card,
+ get_card_by_otp,
+ get_card_by_uid,
+ get_hit,
+ update_card,
+ update_card_counter,
+ update_card_otp,
+)
+from .models import CreateCardData
+from .nxp424 import decryptSUN, getSunMAC
+
+# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
+@boltcards_ext.get("/api/v1/scan/{card_uid}")
+async def api_scan(p, c, request: Request, card_uid: str = None):
+ # some wallets send everything as lower case, no bueno
+ p = p.upper()
+ c = c.upper()
+ card = None
+ counter = b""
+ 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."}
+
+ if card == None:
+ return {"status": "ERROR", "reason": "Unknown card."}
+
+ if c != getSunMAC(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
+ return {"status": "ERROR", "reason": "CMAC does not check."}
+
+ ctr_int = int.from_bytes(counter, "little")
+
+ if ctr_int <= card.counter:
+ return {"status": "ERROR", "reason": "This link is already used."}
+
+ await update_card_counter(ctr_int, card.id)
+
+ # gathering some info for hit record
+ ip = request.client.host
+ if "x-real-ip" in request.headers:
+ ip = request.headers["x-real-ip"]
+ elif "x-forwarded-for" in request.headers:
+ ip = request.headers["x-forwarded-for"]
+
+ agent = request.headers["user-agent"] if "user-agent" in request.headers else ""
+ todays_hits = await get_hits_today(card.id)
+ int hits_amount = 0
+ 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."}
+ hit = await create_hit(card.id, ip, agent, card.counter, ctr_int)
+
+ # link = await get_withdraw_link(card.withdraw, 0)
+ return link.lnurl_response(request)
+ return {
+ "tag": "withdrawRequest",
+ "callback": request.url_for(
+ "boltcards.lnurl_callback"
+ ),
+ "k1": hit.id,
+ "minWithdrawable": 1 * 1000,
+ "maxWithdrawable": card.tx_limit * 1000,
+ "defaultDescription": f"Boltcard (Refunds address {lnurl_encode(req.url_for("boltcards.lnurlp_response", hit_id=hit.id))})",
+ }
+
+@boltcards_ext.get(
+ "/api/v1/lnurl/cb/{hitid}",
+ status_code=HTTPStatus.OK,
+ name="boltcards.lnurl_callback",
+)
+async def lnurl_callback(
+ request: Request,
+ pr: str = Query(None),
+ k1: str = Query(None),
+):
+ hit = await get_hit(k1)
+ card = await get_card(hit.id)
+ if not hit:
+ return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
+
+ if pr:
+ if hit.id != k1:
+ return {"status": "ERROR", "reason": "Bad K1"}
+ if hit.spent:
+ return {"status": "ERROR", "reason": f"Payment already claimed"}
+ hit = await spend_hit(hit.id)
+ if not hit:
+ return {"status": "ERROR", "reason": f"Payment failed"}
+ await pay_invoice(
+ wallet_id=card.wallet,
+ payment_request=pr,
+ max_sat=card.tx_limit / 1000,
+ extra={"tag": "boltcard"},
+ )
+ return {"status": "OK"}
+ else:
+ return {"status": "ERROR", "reason": f"Payment failed"}
+
+
+# /boltcards/api/v1/auth?a=00000000000000000000000000000000
+@boltcards_ext.get("/api/v1/auth")
+async def api_auth(a, request: Request):
+ if a == "00000000000000000000000000000000":
+ response = {"k0": "0" * 32, "k1": "1" * 32, "k2": "2" * 32}
+ return response
+
+ card = await get_card_by_otp(a)
+
+ if not card:
+ raise HTTPException(
+ detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
+ )
+
+ new_otp = secrets.token_hex(16)
+ print(card.otp)
+ print(new_otp)
+ await update_card_otp(new_otp, card.id)
+
+ response = {"k0": card.k0, "k1": card.k1, "k2": card.k2}
+
+ return response
+
+###############LNURLPAY REFUNDS#################
+
+@satsdice_ext.get(
+ "/api/v1/lnurlp/{hit_id}",
+ response_class=HTMLResponse,
+ name="boltcards.lnurlp_response",
+)
+async def api_lnurlp_response(req: Request, hit_id: str = Query(None)):
+ hit = await get_hit(hit_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,
+ }
+ return json.dumps(payResponse)
+
+
+@satsdice_ext.get(
+ "/api/v1/lnurlp/cb/{hit_id}",
+ response_class=HTMLResponse,
+ name="boltcards.lnurlp_callback",
+)
+async def api_lnurlp_callback(
+ req: Request, hit_id: str = Query(None), amount: str = Query(None)
+):
+ hit = await get_hit(hit_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),
+ memo=f"Refund {hit_id}",
+ unhashed_description=LnurlPayMetadata(json.dumps([["text/plain", hit_id]])).encode("utf-8"),
+ extra={"refund": hit_id},
+ )
+
+ payResponse = {"pr": payment_request, "successAction": success_action, "routes": []}
+
+ return json.dumps(payResponse)
+
+
+
+
diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py
index 7dc5acb44..99f425837 100644
--- a/lnbits/extensions/boltcards/migrations.py
+++ b/lnbits/extensions/boltcards/migrations.py
@@ -10,7 +10,8 @@ async def m001_initial(db):
card_name TEXT NOT NULL,
uid TEXT NOT NULL,
counter INT NOT NULL DEFAULT 0,
- withdraw TEXT NOT NULL,
+ tx_limit TEXT NOT NULL,
+ daily_limit TEXT NOT NULL,
k0 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
k1 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
k2 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
@@ -31,9 +32,24 @@ async def m001_initial(db):
id TEXT PRIMARY KEY,
card_id TEXT NOT NULL,
ip TEXT NOT NULL,
+ spent BOOL NOT NULL DEFAULT True,
useragent TEXT,
old_ctr INT NOT NULL DEFAULT 0,
new_ctr INT NOT NULL DEFAULT 0,
+ amount INT NOT NULL,
+ time TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
+
+ await db.execute(
+ """
+ CREATE TABLE boltcards.refunds (
+ id TEXT PRIMARY KEY,
+ hit_id TEXT NOT NULL,
+ refund_amount INT NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 6e1997545..e272d2f91 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -10,7 +10,8 @@ class Card(BaseModel):
card_name: str
uid: str
counter: int
- withdraw: str
+ tx_limit: int
+ daily_limit: int
k0: str
k1: str
k2: str
@@ -20,12 +21,24 @@ class Card(BaseModel):
otp: str
time: int
+ def from_row(cls, row: Row) -> "Card":
+ return cls(**dict(row))
+
+ def lnurl(self, req: Request) -> Lnurl:
+ url = req.url_for(
+ "boltcard.lnurl_response", device_id=self.id, _external=True
+ )
+ return lnurl_encode(url)
+
+ async def lnurlpay_metadata(self) -> LnurlPayMetadata:
+ return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
class CreateCardData(BaseModel):
card_name: str = Query(...)
uid: str = Query(...)
counter: int = Query(0)
- withdraw: str = Query(...)
+ tx_limit: int = Query(0)
+ daily_limit: int = Query(0)
k0: str = Query(ZERO_KEY)
k1: str = Query(ZERO_KEY)
k2: str = Query(ZERO_KEY)
@@ -33,12 +46,18 @@ class CreateCardData(BaseModel):
prev_k1: str = Query(ZERO_KEY)
prev_k2: str = Query(ZERO_KEY)
-
class Hit(BaseModel):
id: str
card_id: str
ip: str
+ spent: bool
useragent: str
old_ctr: int
new_ctr: int
time: int
+
+class Refund(BaseModel):
+ id: str
+ hit_id: str
+ refund_amount: int
+ time: int
\ No newline at end of file
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index d8beaa083..55f2d178d 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -17,10 +17,14 @@ new Vue({
toggleAdvanced: false,
cards: [],
hits: [],
- withdrawsOptions: [],
cardDialog: {
show: false,
- data: {counter:1},
+ data: {
+ counter:1,
+ k0: '',
+ k1: '',
+ k2: '',
+ card_name:''},
temp: {}
},
cardsTable: {
@@ -133,25 +137,6 @@ new Vue({
console.log(self.hits)
})
},
- getWithdraws: function () {
- var self = this
-
- LNbits.api
- .request(
- 'GET',
- '/withdraw/api/v1/links?all_wallets=true',
- this.g.user.wallets[0].inkey
- )
- .then(function (response) {
- self.withdrawsOptions = response.data.map(function (obj) {
- return {
- label: [obj.title, ' - ', obj.id].join(''),
- value: obj.id
- }
- })
- console.log(self.withdraws)
- })
- },
openQrCodeDialog(cardId) {
var card = _.findWhere(this.cards, {id: cardId})
@@ -166,6 +151,7 @@ new Vue({
this.qrCodeDialog.show = true
},
generateKeys: function () {
+ this.cardDialog.show = true
const genRanHex = size =>
[...Array(size)]
.map(() => Math.floor(Math.random() * 16).toString(16))
@@ -194,7 +180,6 @@ new Vue({
this.cardDialog.data = {}
},
sendFormData: function () {
- this.generateKeys()
let wallet = _.findWhere(this.g.user.wallets, {
id: this.cardDialog.data.wallet
})
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 8b06b7500..4c409387b 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -5,7 +5,7 @@
- Add Card
@@ -122,6 +122,45 @@
+
+
+
+
+
Refunds
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+ {{ col.value }}
+
+
+
+ {% endraw %}
+
+
+
@@ -148,15 +187,32 @@
label="Wallet *"
>
-
-
+
+
+
+
From the NFC 424 ntag card that will be loaded
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 37a796262..ee91cffb4 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -1,11 +1,3 @@
-# views_api.py is for you API endpoints that could be hit by another service
-
-# add your dependencies here
-
-# import httpx
-# (use httpx just like requests, except instead of response.ok there's only the
-# response.is_error that is its inverse)
-
import secrets
from http import HTTPStatus
@@ -15,7 +7,6 @@ from starlette.requests import Request
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
-from lnbits.extensions.withdraw import get_withdraw_link
from . import boltcards_ext
from .crud import (
@@ -127,90 +118,4 @@ async def api_hits(
for card in cards:
cards_ids.append(card.id)
- return [hit.dict() for hit in await get_hits(cards_ids)]
-
-
-# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
-@boltcards_ext.get("/api/v1/scan")
-@boltcards_ext.get("/api/v1/scan/{card_uid}")
-async def api_scan(p, c, request: Request, card_uid: str = None):
- # some wallets send everything as lower case, no bueno
- p = p.upper()
- c = c.upper()
- card = None
- counter = b""
-
- if not card_uid:
- # since this route is common to all cards I don't know whitch 'meta key' to use
- # so I try one by one until decrypted uid matches
- for cand in await get_all_cards():
- if cand.k1:
- try:
- card_uid, counter = decryptSUN(
- bytes.fromhex(p), bytes.fromhex(cand.k1)
- )
-
- if card_uid.hex().upper() == cand.uid.upper():
- card = cand
- break
- except:
- continue
- else:
- 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."}
-
- if card == None:
- return {"status": "ERROR", "reason": "Unknown card."}
-
- if c != getSunMAC(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
- return {"status": "ERROR", "reason": "CMAC does not check."}
-
- ctr_int = int.from_bytes(counter, "little")
- if ctr_int <= card.counter:
- return {"status": "ERROR", "reason": "This link is already used."}
-
- await update_card_counter(ctr_int, card.id)
-
- # gathering some info for hit record
- ip = request.client.host
- if "x-real-ip" in request.headers:
- ip = request.headers["x-real-ip"]
- elif "x-forwarded-for" in request.headers:
- ip = request.headers["x-forwarded-for"]
-
- agent = request.headers["user-agent"] if "user-agent" in request.headers else ""
-
- await create_hit(card.id, ip, agent, card.counter, ctr_int)
-
- link = await get_withdraw_link(card.withdraw, 0)
- return link.lnurl_response(request)
-
-
-# /boltcards/api/v1/auth?a=00000000000000000000000000000000
-@boltcards_ext.get("/api/v1/auth")
-async def api_auth(a, request: Request):
- if a == "00000000000000000000000000000000":
- response = {"k0": "0" * 32, "k1": "1" * 32, "k2": "2" * 32}
- return response
-
- card = await get_card_by_otp(a)
-
- if not card:
- raise HTTPException(
- detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
- )
-
- new_otp = secrets.token_hex(16)
- print(card.otp)
- print(new_otp)
- await update_card_otp(new_otp, card.id)
-
- response = {"k0": card.k0, "k1": card.k1, "k2": card.k2}
-
- return response
+ return [hit.dict() for hit in await get_hits(cards_ids)]
\ No newline at end of file
From c10f89e1d60cba5cb64d414d8937f1986fdfef78 Mon Sep 17 00:00:00 2001
From: ben
Date: Mon, 22 Aug 2022 23:29:42 +0100
Subject: [PATCH 32/73] Boots, errors, needs work
---
lnbits/extensions/boltcards/__init__.py | 1 -
lnbits/extensions/boltcards/crud.py | 9 +++++-
lnbits/extensions/boltcards/lnurl.py | 28 +++++++++++++------
lnbits/extensions/boltcards/models.py | 18 +++++++++++-
.../extensions/boltcards/static/js/index.js | 27 ++++++++++++++++++
5 files changed, 71 insertions(+), 12 deletions(-)
diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py
index 5fcbd12e1..63b1252d3 100644
--- a/lnbits/extensions/boltcards/__init__.py
+++ b/lnbits/extensions/boltcards/__init__.py
@@ -16,7 +16,6 @@ boltcards_static_files = [
boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"])
-
def boltcards_renderer():
return template_renderer(["lnbits/extensions/boltcards/templates"])
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index 8707a9697..1a846d685 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -4,7 +4,7 @@ from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
-from .models import Card, CreateCardData, Hit
+from .models import Card, CreateCardData, Hit, Refund
async def create_card(data: CreateCardData, wallet_id: str) -> Card:
@@ -143,6 +143,13 @@ async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
return [Hit(**row) for row in rows]
+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)
+ )
+
+ return [Hit(**row) for row in rows]
+
async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
hit_id = urlsafe_short_hash()
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 51a85e8a9..5d98d28db 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -10,6 +10,14 @@ from fastapi import Request
from fastapi.param_functions import Query
from starlette.exceptions import HTTPException
+import secrets
+from http import HTTPStatus
+
+from fastapi.params import Depends, Query
+from starlette.exceptions import HTTPException
+from starlette.requests import Request
+from starlette.responses import HTMLResponse
+
from lnbits.core.services import create_invoice
from lnbits.core.views.api import pay_invoice
@@ -24,6 +32,7 @@ from .crud import (
get_card_by_otp,
get_card_by_uid,
get_hit,
+ get_hits_today,
update_card,
update_card_counter,
update_card_otp,
@@ -31,6 +40,8 @@ from .crud import (
from .models import CreateCardData
from .nxp424 import decryptSUN, getSunMAC
+###############LNURLWITHDRAW#################
+
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
@boltcards_ext.get("/api/v1/scan/{card_uid}")
async def api_scan(p, c, request: Request, card_uid: str = None):
@@ -70,15 +81,14 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
agent = request.headers["user-agent"] if "user-agent" in request.headers else ""
todays_hits = await get_hits_today(card.id)
- int hits_amount = 0
+
+ hits_amount = 0
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."}
hit = await create_hit(card.id, ip, agent, card.counter, ctr_int)
-
- # link = await get_withdraw_link(card.withdraw, 0)
- return link.lnurl_response(request)
+ lnurlpay = lnurl_encode(request.url_for("boltcards.lnurlp_response", hit_id=hit.id))
return {
"tag": "withdrawRequest",
"callback": request.url_for(
@@ -87,7 +97,7 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
"k1": hit.id,
"minWithdrawable": 1 * 1000,
"maxWithdrawable": card.tx_limit * 1000,
- "defaultDescription": f"Boltcard (Refunds address {lnurl_encode(req.url_for("boltcards.lnurlp_response", hit_id=hit.id))})",
+ "defaultDescription": f"Boltcard (refund address {lnurlpay})",
}
@boltcards_ext.get(
@@ -149,12 +159,12 @@ async def api_auth(a, request: Request):
###############LNURLPAY REFUNDS#################
-@satsdice_ext.get(
+@boltcards_ext.get(
"/api/v1/lnurlp/{hit_id}",
response_class=HTMLResponse,
name="boltcards.lnurlp_response",
)
-async def api_lnurlp_response(req: Request, hit_id: str = Query(None)):
+async def lnurlp_response(req: Request, hit_id: str = Query(None)):
hit = await get_hit(hit_id)
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
@@ -168,12 +178,12 @@ async def api_lnurlp_response(req: Request, hit_id: str = Query(None)):
return json.dumps(payResponse)
-@satsdice_ext.get(
+@boltcards_ext.get(
"/api/v1/lnurlp/cb/{hit_id}",
response_class=HTMLResponse,
name="boltcards.lnurlp_callback",
)
-async def api_lnurlp_callback(
+async def lnurlp_callback(
req: Request, hit_id: str = Query(None), amount: str = Query(None)
):
hit = await get_hit(hit_id)
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index e272d2f91..022d636ca 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -1,5 +1,15 @@
from fastapi.params import Query
from pydantic import BaseModel
+from sqlite3 import Row
+from typing import Optional
+
+from fastapi import Request
+from lnurl import Lnurl
+from lnurl import encode as lnurl_encode # type: ignore
+from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
+from lnurl.types import LnurlPayMetadata # type: ignore
+from pydantic import BaseModel
+from pydantic.main import BaseModel
ZERO_KEY = "00000000000000000000000000000000"
@@ -56,8 +66,14 @@ class Hit(BaseModel):
new_ctr: int
time: int
+ def from_row(cls, row: Row) -> "Hit":
+ return cls(**dict(row))
+
class Refund(BaseModel):
id: str
hit_id: str
refund_amount: int
- time: int
\ No newline at end of file
+ time: int
+
+ def from_row(cls, row: Row) -> "Refund":
+ return cls(**dict(row))
\ No newline at end of file
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 55f2d178d..7757ffb44 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -52,6 +52,33 @@ new Vue({
rowsPerPage: 10
}
},
+ refundsTable: {
+ columns: [
+ {
+ name: 'hit_id',
+ align: 'left',
+ label: 'Hit ID',
+ field: 'hit_id'
+ },
+ {
+ name: 'refund_amount',
+ align: 'left',
+ label: 'Refund Amount',
+ field: 'oldrefund_amount_ctr'
+ },
+ {
+ name: 'time',
+ align: 'left',
+ label: 'Time',
+ field: 'time'
+ }
+ ],
+ pagination: {
+ rowsPerPage: 10,
+ sortBy: 'date',
+ descending: true
+ }
+ },
hitsTable: {
columns: [
{
From 59d6b654c573b65997ba762d53279a6624fca6cd Mon Sep 17 00:00:00 2001
From: ben
Date: Wed, 24 Aug 2022 09:33:38 +0100
Subject: [PATCH 33/73] push before removing uuid
---
.../extensions/boltcards/static/js/index.js | 27 +++++++++++++++++--
.../boltcards/templates/boltcards/index.html | 5 ++--
lnbits/extensions/boltcards/views_api.py | 16 +++++++++++
3 files changed, 44 insertions(+), 4 deletions(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 7757ffb44..ff23c4d1f 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -17,6 +17,7 @@ new Vue({
toggleAdvanced: false,
cards: [],
hits: [],
+ refunds: [],
cardDialog: {
show: false,
data: {
@@ -164,6 +165,23 @@ new Vue({
console.log(self.hits)
})
},
+ getRefunds: function () {
+ var self = this
+
+ LNbits.api
+ .request(
+ 'GET',
+ '/boltcards/api/v1/refunds?all_wallets=true',
+ this.g.user.wallets[0].inkey
+ )
+ .then(function (response) {
+ self.refunds = response.data.map(function (obj) {
+ obj.card_name = self.cards.find(d => d.id == obj.card_id).card_name
+ return mapCards(obj)
+ })
+ console.log(self.hits)
+ })
+ },
openQrCodeDialog(cardId) {
var card = _.findWhere(this.cards, {id: cardId})
@@ -177,8 +195,13 @@ new Vue({
}
this.qrCodeDialog.show = true
},
- generateKeys: function () {
+ addCardOpen: function () {
this.cardDialog.show = true
+ var elem = this.$els.myBtn
+ elem.click()
+ },
+ generateKeys: function () {
+
const genRanHex = size =>
[...Array(size)]
.map(() => Math.floor(Math.random() * 16).toString(16))
@@ -307,7 +330,7 @@ new Vue({
if (this.g.user.wallets.length) {
this.getCards()
this.getHits()
- this.getWithdraws()
+ this.getRefunds()
}
}
})
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 4c409387b..d8421754d 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -5,7 +5,7 @@
- Add Card
@@ -234,7 +234,7 @@
v-model="toggleAdvanced"
label="Show advanced options"
>
-
+
Generate keys
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index ee91cffb4..cccc6a289 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -110,6 +110,22 @@ async def api_hits(
):
wallet_ids = [g.wallet.id]
+ if all_wallets:
+ wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+
+ cards = await get_cards(wallet_ids)
+ cards_ids = []
+ for card in cards:
+ cards_ids.append(card.id)
+
+ return [hit.dict() for hit in await get_hits(cards_ids)]
+
+@boltcards_ext.get("/api/v1/refunds")
+async def api_hits(
+ g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
+):
+ wallet_ids = [g.wallet.id]
+
if all_wallets:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
From 4e8766ce08d5487a7b2952cadb9f9bccc098940f Mon Sep 17 00:00:00 2001
From: ben
Date: Thu, 25 Aug 2022 13:07:18 +0100
Subject: [PATCH 34/73] removed uid for justa card id
---
lnbits/extensions/boltcards/crud.py | 2 --
lnbits/extensions/boltcards/lnurl.py | 14 +++++++-------
lnbits/extensions/boltcards/static/js/index.js | 7 +------
.../boltcards/templates/boltcards/index.html | 13 +------------
4 files changed, 9 insertions(+), 27 deletions(-)
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index 1a846d685..6d9f8b1c5 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -15,7 +15,6 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
id,
wallet,
card_name,
- uid,
counter,
withdraw,
k0,
@@ -29,7 +28,6 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
card_id,
wallet_id,
data.card_name,
- data.uid.upper(),
data.counter,
data.withdraw,
data.k0,
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 5d98d28db..1d62199f3 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -30,7 +30,7 @@ from .crud import (
create_hit,
get_card,
get_card_by_otp,
- get_card_by_uid,
+ get_card,
get_hit,
get_hits_today,
update_card,
@@ -43,18 +43,18 @@ from .nxp424 import decryptSUN, getSunMAC
###############LNURLWITHDRAW#################
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
-@boltcards_ext.get("/api/v1/scan/{card_uid}")
-async def api_scan(p, c, request: Request, card_uid: str = None):
+@boltcards_ext.get("/api/v1/scan/{card_id}")
+async def api_scan(p, c, request: Request, card_id: str = None):
# some wallets send everything as lower case, no bueno
p = p.upper()
c = c.upper()
card = None
counter = b""
try:
- card = await get_card_by_uid(card_uid)
- card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
+ card = await get_card(card_id)
+ card_id, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
- if card.uid.upper() != card_uid.hex().upper():
+ if card.uid.upper() != card_id.hex().upper():
return {"status": "ERROR", "reason": "Card UID mis-match."}
except:
return {"status": "ERROR", "reason": "Error decrypting card."}
@@ -62,7 +62,7 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
if card == None:
return {"status": "ERROR", "reason": "Unknown card."}
- if c != getSunMAC(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
+ if c != getSunMAC(card_id, counter, bytes.fromhex(card.k2)).hex().upper():
return {"status": "ERROR", "reason": "CMAC does not check."}
ctr_int = int.from_bytes(counter, "little")
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index ff23c4d1f..4ce6d7596 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -150,7 +150,6 @@ new Vue({
},
getHits: function () {
var self = this
-
LNbits.api
.request(
'GET',
@@ -167,7 +166,6 @@ new Vue({
},
getRefunds: function () {
var self = this
-
LNbits.api
.request(
'GET',
@@ -184,7 +182,6 @@ new Vue({
},
openQrCodeDialog(cardId) {
var card = _.findWhere(this.cards, {id: cardId})
-
this.qrCodeDialog.data = {
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
name: card.card_name,
@@ -197,11 +194,9 @@ new Vue({
},
addCardOpen: function () {
this.cardDialog.show = true
- var elem = this.$els.myBtn
- elem.click()
+ this.generateKeys()
},
generateKeys: function () {
-
const genRanHex = size =>
[...Array(size)]
.map(() => Math.floor(Math.random() * 16).toString(16))
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index d8421754d..1c58f8e57 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -5,7 +5,7 @@
- Add Card
@@ -187,7 +187,6 @@
label="Wallet *"
>
-
-
-
- From the NFC 424 ntag card that will be loaded
-
Date: Thu, 25 Aug 2022 14:17:03 +0100
Subject: [PATCH 35/73] Added UID scan for form
---
.../boltcards/templates/boltcards/index.html | 52 ++++++++++++-------
1 file changed, 34 insertions(+), 18 deletions(-)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index d8421754d..1c0c54f26 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -5,7 +5,7 @@
- Add Card
@@ -187,7 +187,6 @@
label="Wallet *"
>
-
+
+
+
+
+ Get from the card you'll use, using an NFC app
+
+
+
+
+ Tap card to scan UID (coming soon)
+
+
-
- From the NFC 424 ntag card that will be loaded
-
+
Date: Fri, 26 Aug 2022 19:22:03 +0100
Subject: [PATCH 36/73] Pretty much works
---
lnbits/extensions/boltcards/crud.py | 29 +++++++++++++------
lnbits/extensions/boltcards/lnurl.py | 26 ++++++++++-------
lnbits/extensions/boltcards/models.py | 1 +
.../extensions/boltcards/static/js/index.js | 14 ++++-----
.../boltcards/templates/boltcards/index.html | 11 ++++++-
lnbits/extensions/boltcards/views_api.py | 2 ++
6 files changed, 54 insertions(+), 29 deletions(-)
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index 1a846d685..ebcaf9d95 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -8,30 +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,
- uid,
counter,
- withdraw,
+ tx_limit,
+ daily_limit,
k0,
k1,
k2,
otp
)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
card_id,
+ data.uid.upper(),
wallet_id,
data.card_name,
- data.uid.upper(),
data.counter,
- data.withdraw,
+ data.tx_limit,
+ data.daily_limit,
data.k0,
data.k1,
data.k2,
@@ -145,11 +147,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()
@@ -159,19 +166,23 @@ 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)
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 5d98d28db..3fe09d875 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -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_by_uid,
get_hit,
get_hits_today,
+ spend_hit,
update_card,
update_card_counter,
update_card_otp,
@@ -50,10 +54,10 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
c = c.upper()
card = None
counter = b""
+
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:
@@ -67,8 +71,8 @@ async def api_scan(p, c, request: Request, card_uid: 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_uid: 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},
)
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 022d636ca..4f23b745e 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -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":
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index ff23c4d1f..7e824ea91 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -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: {
@@ -150,7 +151,6 @@ new Vue({
},
getHits: function () {
var self = this
-
LNbits.api
.request(
'GET',
@@ -167,7 +167,6 @@ new Vue({
},
getRefunds: function () {
var self = this
-
LNbits.api
.request(
'GET',
@@ -184,7 +183,6 @@ new Vue({
},
openQrCodeDialog(cardId) {
var card = _.findWhere(this.cards, {id: cardId})
-
this.qrCodeDialog.data = {
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
name: card.card_name,
@@ -197,11 +195,9 @@ new Vue({
},
addCardOpen: function () {
this.cardDialog.show = true
- var elem = this.$els.myBtn
- elem.click()
+ this.generateKeys()
},
generateKeys: function () {
-
const genRanHex = size =>
[...Array(size)]
.map(() => Math.floor(Math.random() * 16).toString(16))
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 1c0c54f26..0ba64f0ec 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -34,6 +34,7 @@
+ Base URL
{{ col.label }}
@@ -53,6 +54,14 @@
@click="openQrCodeDialog(props.row.id)"
>
+
+ lnurl://...Click to copy, then add to NFC card
+
+
{{ col.value }}
@@ -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"
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index cccc6a289..4585c6984 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -26,6 +26,7 @@ from .crud import (
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(
From 99595ba96b3511c01ca05d6e4432472168761c15 Mon Sep 17 00:00:00 2001
From: ben
Date: Sat, 27 Aug 2022 16:37:31 +0100
Subject: [PATCH 37/73] Added refunds
---
lnbits/extensions/boltcards/__init__.py | 1 +
lnbits/extensions/boltcards/crud.py | 44 ++++++++++++++++---
.../extensions/boltcards/static/js/index.js | 2 +-
lnbits/extensions/boltcards/tasks.py | 34 ++++++++++++++
lnbits/extensions/boltcards/views_api.py | 8 +++-
5 files changed, 80 insertions(+), 9 deletions(-)
create mode 100644 lnbits/extensions/boltcards/tasks.py
diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py
index 63b1252d3..fe99ee2e0 100644
--- a/lnbits/extensions/boltcards/__init__.py
+++ b/lnbits/extensions/boltcards/__init__.py
@@ -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
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index ebcaf9d95..ab6fde09b 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -71,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:
@@ -188,3 +182,41 @@ async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
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]
\ No newline at end of file
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 7e824ea91..ceea05088 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -140,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) {
diff --git a/lnbits/extensions/boltcards/tasks.py b/lnbits/extensions/boltcards/tasks.py
new file mode 100644
index 000000000..30a290e9d
--- /dev/null
+++ b/lnbits/extensions/boltcards/tasks.py
@@ -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)
+
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 4585c6984..c49d20bc1 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -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,6 +21,7 @@ from .crud import (
update_card,
update_card_counter,
update_card_otp,
+ get_refunds,
)
from .models import CreateCardData
from .nxp424 import decryptSUN, getSunMAC
@@ -135,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)]
\ No newline at end of file
+ return [refund.dict() for refund in await get_refunds(hits_ids)]
\ No newline at end of file
From 20dbd879b1b8ffdb435c44eca7b3e5ae52df6153 Mon Sep 17 00:00:00 2001
From: ben
Date: Sat, 27 Aug 2022 17:26:17 +0100
Subject: [PATCH 38/73] bug
---
.../extensions/boltcards/static/js/index.js | 27 +++++++++----------
1 file changed, 13 insertions(+), 14 deletions(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index ceea05088..0575f3424 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -9,6 +9,7 @@ const mapCards = obj => {
return obj
}
+
new Vue({
el: '#vue',
mixins: [windowMixin],
@@ -18,7 +19,6 @@ new Vue({
cards: [],
hits: [],
refunds: [],
- lnurlLink: location.hostname + '/boltcards/api/v1/scan/',
cardDialog: {
show: false,
data: {
@@ -44,10 +44,10 @@ new Vue({
field: 'counter'
},
{
- name: 'uid',
+ name: 'withdraw',
align: 'left',
- label: 'Card ID',
- field: 'uid'
+ label: 'Withdraw ID',
+ field: 'withdraw'
}
],
pagination: {
@@ -66,7 +66,7 @@ new Vue({
name: 'refund_amount',
align: 'left',
label: 'Refund Amount',
- field: 'oldrefund_amount_ctr'
+ field: 'refund_amount'
},
{
name: 'time',
@@ -140,13 +140,12 @@ new Vue({
.request(
'GET',
'/boltcards/api/v1/cards?all_wallets=true',
- this.g.user.wallets[0].adminkey
+ this.g.user.wallets[0].inkey
)
.then(function (response) {
self.cards = response.data.map(function (obj) {
return mapCards(obj)
})
- console.log(self.cards)
})
},
getHits: function () {
@@ -162,7 +161,6 @@ new Vue({
obj.card_name = self.cards.find(d => d.id == obj.card_id).card_name
return mapCards(obj)
})
- console.log(self.hits)
})
},
getRefunds: function () {
@@ -175,10 +173,8 @@ new Vue({
)
.then(function (response) {
self.refunds = response.data.map(function (obj) {
- obj.card_name = self.cards.find(d => d.id == obj.card_id).card_name
return mapCards(obj)
})
- console.log(self.hits)
})
},
openQrCodeDialog(cardId) {
@@ -252,7 +248,6 @@ new Vue({
},
updateCardDialog: function (formId) {
var card = _.findWhere(this.cards, {id: formId})
- console.log(card.id)
this.cardDialog.data = _.clone(card)
this.cardDialog.temp.k0 = this.cardDialog.data.k0
@@ -274,8 +269,6 @@ new Vue({
data.prev_k2 = this.cardDialog.temp.k2
}
- console.log(data)
-
LNbits.api
.request(
'PUT',
@@ -320,7 +313,13 @@ new Vue({
},
exportCardsCSV: function () {
LNbits.utils.exportCSV(this.cardsTable.columns, this.cards)
- }
+ },
+ exportHitsCSV: function () {
+ LNbits.utils.exportCSV(this.hitsTable.columns, this.hits)
+ },
+ exportRefundsCSV: function () {
+ LNbits.utils.exportCSV(this.refundsTable.columns, this.refunds)
+ },
},
created: function () {
if (this.g.user.wallets.length) {
From 977f9bb8c44a84f55866441ed65130839680db7b Mon Sep 17 00:00:00 2001
From: ben
Date: Sun, 28 Aug 2022 10:58:17 +0100
Subject: [PATCH 39/73] Added enable disable card
---
lnbits/extensions/boltcards/crud.py | 20 ++++++++++-
lnbits/extensions/boltcards/lnurl.py | 4 +++
lnbits/extensions/boltcards/migrations.py | 1 +
lnbits/extensions/boltcards/models.py | 2 ++
.../extensions/boltcards/static/js/index.js | 33 +++++++++++++++----
.../boltcards/templates/boltcards/index.html | 26 +++++++++++----
lnbits/extensions/boltcards/views_api.py | 20 +++++++++++
7 files changed, 92 insertions(+), 14 deletions(-)
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index ab6fde09b..c6b09694b 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -19,12 +19,13 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
counter,
tx_limit,
daily_limit,
+ enable,
k0,
k1,
k2,
otp
)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
card_id,
@@ -34,6 +35,7 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
data.counter,
data.tx_limit,
data.daily_limit,
+ True,
data.k0,
data.k1,
data.k2,
@@ -104,7 +106,17 @@ async def get_card_by_otp(otp: str) -> Optional[Card]:
async def delete_card(card_id: str) -> None:
+ # Delete cards
+ card = await get_card(card_id)
await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
+ # Delete hits
+ hits = await get_hits([card_id])
+ for hit in hits:
+ await db.execute("DELETE FROM boltcards.hits WHERE id = ?", (hit.id,))
+ # Delete refunds
+ refunds = await get_refunds([hit])
+ for refund in refunds:
+ await db.execute("DELETE FROM boltcards.refunds WHERE id = ?", (refund.hit_id,))
async def update_card_counter(counter: int, id: str):
@@ -113,6 +125,12 @@ async def update_card_counter(counter: int, id: str):
(counter, id),
)
+async def enable_disable_card(enable: bool, id: str) -> Optional[Card]:
+ row = await db.execute(
+ "UPDATE boltcards.cards SET enable = ? WHERE id = ?",
+ (enable, id),
+ )
+ return await get_card(id)
async def update_card_otp(otp: str, id: str):
await db.execute(
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index a41cc5d16..398f218e2 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -57,6 +57,8 @@ async def api_scan(p, c, request: Request, card_id: str = None):
try:
card = await get_card_by_uid(card_uid)
+ if not card.enable:
+ return {"status": "ERROR", "reason": "Card is disabled."}
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."}
@@ -173,6 +175,8 @@ async def lnurlp_response(req: Request, hit_id: str = Query(None)):
card = await get_card(hit.card_id)
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
+ if not card.enable:
+ return {"status": "ERROR", "reason": "Card is disabled."}
payResponse = {
"tag": "payRequest",
"callback": req.url_for("boltcards.lnurlp_callback", hit_id=hit_id),
diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py
index 99f425837..c20ef449c 100644
--- a/lnbits/extensions/boltcards/migrations.py
+++ b/lnbits/extensions/boltcards/migrations.py
@@ -12,6 +12,7 @@ async def m001_initial(db):
counter INT NOT NULL DEFAULT 0,
tx_limit TEXT NOT NULL,
daily_limit TEXT NOT NULL,
+ enable BOOL NOT NULL,
k0 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
k1 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
k2 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 4f23b745e..cdba5dd3a 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -22,6 +22,7 @@ class Card(BaseModel):
counter: int
tx_limit: int
daily_limit: int
+ enable: bool
k0: str
k1: str
k2: str
@@ -49,6 +50,7 @@ class CreateCardData(BaseModel):
counter: int = Query(0)
tx_limit: int = Query(0)
daily_limit: int = Query(0)
+ enable: bool = Query(...)
k0: str = Query(ZERO_KEY)
k1: str = Query(ZERO_KEY)
k2: str = Query(ZERO_KEY)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 0575f3424..167d22c76 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -16,6 +16,7 @@ new Vue({
data: function () {
return {
toggleAdvanced: false,
+ nfcTagReading: false,
cards: [],
hits: [],
refunds: [],
@@ -194,6 +195,7 @@ new Vue({
this.generateKeys()
},
generateKeys: function () {
+ var self = this
const genRanHex = size =>
[...Array(size)]
.map(() => Math.floor(Math.random() * 16).toString(16))
@@ -203,20 +205,17 @@ new Vue({
typeof this.cardDialog.data.card_name === 'string' &&
this.cardDialog.data.card_name.search('debug') > -1
- this.cardDialog.data.k0 = debugcard
+ self.cardDialog.data.k0 = debugcard
? '11111111111111111111111111111111'
: genRanHex(32)
- this.$refs['k0'].value = this.cardDialog.data.k0
- this.cardDialog.data.k1 = debugcard
+ self.cardDialog.data.k1 = debugcard
? '22222222222222222222222222222222'
: genRanHex(32)
- this.$refs['k1'].value = this.cardDialog.data.k1
- this.cardDialog.data.k2 = debugcard
+ self.cardDialog.data.k2 = debugcard
? '33333333333333333333333333333333'
: genRanHex(32)
- this.$refs['k2'].value = this.cardDialog.data.k2
},
closeFormDialog: function () {
this.cardDialog.data = {}
@@ -288,6 +287,28 @@ new Vue({
LNbits.utils.notifyApiError(error)
})
},
+ enableCard: function (wallet, card_id, enable) {
+ var self = this
+ let fullWallet = _.findWhere(self.g.user.wallets, {
+ id: wallet
+ })
+ LNbits.api
+ .request(
+ 'GET',
+ '/boltcards/api/v1/cards/enable/' + card_id + '/' + enable,
+ fullWallet.adminkey
+ )
+ .then(function (response) {
+ console.log(response.data)
+ self.cards = _.reject(self.cards, function (obj) {
+ return obj.id == response.data.id
+ })
+ self.cards.push(mapCards(response.data))
+ })
+ .catch(function (error) {
+ LNbits.utils.notifyApiError(error)
+ })
+ },
deleteCard: function (cardId) {
let self = this
let cards = _.findWhere(this.cards, {id: cardId})
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 0ba64f0ec..cbb869800 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -65,6 +65,20 @@
{{ col.value }}
+
+ DISABLE
+ ENABLE
+
-
+ >Edit card
+ >Deleting card will also delete all records
@@ -255,7 +268,6 @@
@@ -263,7 +275,7 @@
Date: Sun, 28 Aug 2022 11:05:26 +0100
Subject: [PATCH 40/73] Fixed enable
---
lnbits/extensions/boltcards/models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index cdba5dd3a..c1b113af3 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -50,7 +50,7 @@ class CreateCardData(BaseModel):
counter: int = Query(0)
tx_limit: int = Query(0)
daily_limit: int = Query(0)
- enable: bool = Query(...)
+ enable: bool = Query(True)
k0: str = Query(ZERO_KEY)
k1: str = Query(ZERO_KEY)
k2: str = Query(ZERO_KEY)
From 8cabd7b5a4d81796a02e72b079330afe58ae120e Mon Sep 17 00:00:00 2001
From: ben
Date: Sun, 28 Aug 2022 11:59:21 +0100
Subject: [PATCH 41/73] withdraws working
---
lnbits/extensions/boltcards/lnurl.py | 37 ++++++++++--------------
lnbits/extensions/boltcards/views_api.py | 3 --
2 files changed, 16 insertions(+), 24 deletions(-)
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 398f218e2..93ca6390a 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -32,6 +32,7 @@ from . import boltcards_ext
from .crud import (
create_hit,
get_card,
+ get_card_by_uid,
get_card_by_otp,
get_card,
get_hit,
@@ -47,34 +48,31 @@ from .nxp424 import decryptSUN, getSunMAC
###############LNURLWITHDRAW#################
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
-@boltcards_ext.get("/api/v1/scan/{card_id}")
-async def api_scan(p, c, request: Request, card_id: str = None):
+@boltcards_ext.get("/api/v1/scan/{card_uid}")
+async def api_scan(p, c, request: Request, card_uid: str = None):
# some wallets send everything as lower case, no bueno
p = p.upper()
c = c.upper()
card = None
counter = b""
-
+ card = await get_card_by_uid(card_uid)
+ if not card:
+ return {"status": "ERROR", "reason": "No card."}
+ if not card.enable:
+ return {"status": "ERROR", "reason": "Card is disabled."}
try:
- card = await get_card_by_uid(card_uid)
- if not card.enable:
- return {"status": "ERROR", "reason": "Card is disabled."}
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."}
+ if c != getSunMAC(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
+ return {"status": "ERROR", "reason": "CMAC does not check."}
except:
return {"status": "ERROR", "reason": "Error decrypting card."}
- if card == None:
- return {"status": "ERROR", "reason": "Unknown card."}
-
- if c != getSunMAC(card_id, counter, bytes.fromhex(card.k2)).hex().upper():
- return {"status": "ERROR", "reason": "CMAC does not check."}
-
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)
@@ -117,26 +115,23 @@ async def lnurl_callback(
k1: str = Query(None),
):
hit = await get_hit(k1)
- card = await get_card(hit.id)
+ card = await get_card(hit.card_id)
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
-
- if pr:
+ try:
if hit.id != k1:
return {"status": "ERROR", "reason": "Bad K1"}
if hit.spent:
return {"status": "ERROR", "reason": f"Payment already claimed"}
hit = await spend_hit(hit.id)
- if not hit:
- return {"status": "ERROR", "reason": f"Payment failed"}
await pay_invoice(
wallet_id=card.wallet,
payment_request=pr,
- max_sat=card.tx_limit / 1000,
+ max_sat=card.tx_limit,
extra={"tag": "boltcard"},
)
return {"status": "OK"}
- else:
+ except:
return {"status": "ERROR", "reason": f"Payment failed"}
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 3891d1fe1..4d2db17db 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -49,7 +49,6 @@ 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(
@@ -106,8 +105,6 @@ async def enable_card(
detail="Not your card.", status_code=HTTPStatus.FORBIDDEN
)
card = await enable_disable_card(enable=enable, id=card_id)
- logger.debug(enable)
- logger.debug(card)
return card.dict()
@boltcards_ext.delete("/api/v1/cards/{card_id}")
From 6e9fd28d286a329b9a88fb976ec11ef65b182c2f Mon Sep 17 00:00:00 2001
From: ben
Date: Sun, 28 Aug 2022 12:16:23 +0100
Subject: [PATCH 42/73] UI tidy
---
lnbits/extensions/boltcards/static/js/index.js | 18 +++++++++++++++---
.../boltcards/templates/boltcards/index.html | 5 ++---
2 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 167d22c76..8bd9c411d 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -45,10 +45,22 @@ new Vue({
field: 'counter'
},
{
- name: 'withdraw',
+ name: 'wallet',
align: 'left',
- label: 'Withdraw ID',
- field: 'withdraw'
+ label: 'Wallet',
+ field: 'wallet'
+ },
+ {
+ name: 'tx_limit',
+ align: 'left',
+ label: 'Max tx',
+ field: 'tx_limit'
+ },
+ {
+ name: 'daily_limit',
+ align: 'left',
+ label: 'Daily tx limit',
+ field: 'daily_limit'
}
],
pagination: {
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index cbb869800..8943d5ddd 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -48,11 +48,10 @@
+ >Card key credentials
Date: Sun, 28 Aug 2022 12:29:27 +0100
Subject: [PATCH 43/73] Added pretty "add card" button
---
.../boltcards/templates/boltcards/index.html | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 8943d5ddd..96f4f994b 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -3,18 +3,20 @@
-
-
- Add Card
-
-
-
Cards
+
+
+
Cards
+
+
+
+ Add card
+
+
+
Date: Mon, 29 Aug 2022 14:18:18 +0100
Subject: [PATCH 44/73] Black/prettier
---
lnbits/extensions/boltcards/__init__.py | 2 +
lnbits/extensions/boltcards/config.json | 2 +-
lnbits/extensions/boltcards/crud.py | 21 +-
lnbits/extensions/boltcards/lnurl.py | 31 ++-
lnbits/extensions/boltcards/models.py | 9 +-
.../extensions/boltcards/static/js/index.js | 14 +-
lnbits/extensions/boltcards/tasks.py | 7 +-
.../templates/boltcards/_api_docs.html | 4 -
.../boltcards/templates/boltcards/index.html | 193 ++++++++++--------
lnbits/extensions/boltcards/views_api.py | 14 +-
10 files changed, 161 insertions(+), 136 deletions(-)
diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py
index fe99ee2e0..11b8dd2d1 100644
--- a/lnbits/extensions/boltcards/__init__.py
+++ b/lnbits/extensions/boltcards/__init__.py
@@ -16,9 +16,11 @@ boltcards_static_files = [
boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"])
+
def boltcards_renderer():
return template_renderer(["lnbits/extensions/boltcards/templates"])
+
from .lnurl import * # noqa
from .tasks import * # noqa
from .views import * # noqa
diff --git a/lnbits/extensions/boltcards/config.json b/lnbits/extensions/boltcards/config.json
index ef98a35ad..e46070d30 100644
--- a/lnbits/extensions/boltcards/config.json
+++ b/lnbits/extensions/boltcards/config.json
@@ -2,5 +2,5 @@
"name": "Bolt Cards",
"short_description": "Self custody Bolt Cards with one time LNURLw",
"icon": "payment",
- "contributors": ["iwarpbtc"]
+ "contributors": ["iwarpbtc", "arcbtc", "leesalminen"]
}
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index c6b09694b..1c48500da 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -116,7 +116,9 @@ async def delete_card(card_id: str) -> None:
# Delete refunds
refunds = await get_refunds([hit])
for refund in refunds:
- await db.execute("DELETE FROM boltcards.refunds WHERE id = ?", (refund.hit_id,))
+ await db.execute(
+ "DELETE FROM boltcards.refunds WHERE id = ?", (refund.hit_id,)
+ )
async def update_card_counter(counter: int, id: str):
@@ -125,6 +127,7 @@ async def update_card_counter(counter: int, id: str):
(counter, id),
)
+
async def enable_disable_card(enable: bool, id: str) -> Optional[Card]:
row = await db.execute(
"UPDATE boltcards.cards SET enable = ? WHERE id = ?",
@@ -132,6 +135,7 @@ async def enable_disable_card(enable: bool, id: str) -> Optional[Card]:
)
return await get_card(id)
+
async def update_card_otp(otp: str, id: str):
await db.execute(
"UPDATE boltcards.cards SET otp = ? WHERE id = ?",
@@ -157,19 +161,23 @@ async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
return [Hit(**row) for row in rows]
+
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 time >= DATE('now') AND time < DATE('now', '+1 day')", (card_id,)
+ 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()
await db.execute(
@@ -201,6 +209,7 @@ async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
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(
@@ -224,17 +233,21 @@ async def create_refund(hit_id, refund_amount) -> Refund:
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))
+ 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]
\ No newline at end of file
+ return [Refund(**row) for row in rows]
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 93ca6390a..a1630e2be 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -59,7 +59,7 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
if not card:
return {"status": "ERROR", "reason": "No card."}
if not card.enable:
- return {"status": "ERROR", "reason": "Card is disabled."}
+ return {"status": "ERROR", "reason": "Card is disabled."}
try:
card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
if card.uid.upper() != card_uid.hex().upper():
@@ -70,7 +70,7 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
return {"status": "ERROR", "reason": "Error decrypting card."}
ctr_int = int.from_bytes(counter, "little")
-
+
if ctr_int <= card.counter:
return {"status": "ERROR", "reason": "This link is already used."}
@@ -95,15 +95,14 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
lnurlpay = lnurl_encode(request.url_for("boltcards.lnurlp_response", hit_id=hit.id))
return {
"tag": "withdrawRequest",
- "callback": request.url_for(
- "boltcards.lnurl_callback", hitid=hit.id
- ),
+ "callback": request.url_for("boltcards.lnurl_callback", hitid=hit.id),
"k1": hit.id,
"minWithdrawable": 1 * 1000,
"maxWithdrawable": card.tx_limit * 1000,
"defaultDescription": f"Boltcard (refund address {lnurlpay})",
}
+
@boltcards_ext.get(
"/api/v1/lnurl/cb/{hitid}",
status_code=HTTPStatus.OK,
@@ -114,8 +113,8 @@ async def lnurl_callback(
pr: str = Query(None),
k1: str = Query(None),
):
- hit = await get_hit(k1)
- card = await get_card(hit.card_id)
+ hit = await get_hit(k1)
+ card = await get_card(hit.card_id)
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
try:
@@ -158,16 +157,18 @@ async def api_auth(a, request: Request):
return response
+
###############LNURLPAY REFUNDS#################
+
@boltcards_ext.get(
"/api/v1/lnurlp/{hit_id}",
response_class=HTMLResponse,
name="boltcards.lnurlp_response",
)
async def lnurlp_response(req: Request, hit_id: str = Query(None)):
- hit = await get_hit(hit_id)
- card = await get_card(hit.card_id)
+ 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."}
if not card.enable:
@@ -190,8 +191,8 @@ async def lnurlp_response(req: Request, hit_id: str = Query(None)):
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)
+ 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."}
@@ -199,14 +200,12 @@ async def lnurlp_callback(
wallet_id=card.wallet,
amount=int(amount) / 1000,
memo=f"Refund {hit_id}",
- unhashed_description=LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])).encode("utf-8"),
+ unhashed_description=LnurlPayMetadata(
+ json.dumps([["text/plain", "Refund"]])
+ ).encode("utf-8"),
extra={"refund": hit_id},
)
payResponse = {"pr": payment_request, "successAction": success_action, "routes": []}
return json.dumps(payResponse)
-
-
-
-
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index c1b113af3..80e3b9734 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -36,14 +36,13 @@ class Card(BaseModel):
return cls(**dict(row))
def lnurl(self, req: Request) -> Lnurl:
- url = req.url_for(
- "boltcard.lnurl_response", device_id=self.id, _external=True
- )
+ url = req.url_for("boltcard.lnurl_response", device_id=self.id, _external=True)
return lnurl_encode(url)
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
+
class CreateCardData(BaseModel):
card_name: str = Query(...)
uid: str = Query(...)
@@ -58,6 +57,7 @@ class CreateCardData(BaseModel):
prev_k1: str = Query(ZERO_KEY)
prev_k2: str = Query(ZERO_KEY)
+
class Hit(BaseModel):
id: str
card_id: str
@@ -72,6 +72,7 @@ class Hit(BaseModel):
def from_row(cls, row: Row) -> "Hit":
return cls(**dict(row))
+
class Refund(BaseModel):
id: str
hit_id: str
@@ -79,4 +80,4 @@ class Refund(BaseModel):
time: int
def from_row(cls, row: Row) -> "Refund":
- return cls(**dict(row))
\ No newline at end of file
+ return cls(**dict(row))
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 8bd9c411d..33704f3a2 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -9,12 +9,11 @@ const mapCards = obj => {
return obj
}
-
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
- return {
+ return {
toggleAdvanced: false,
nfcTagReading: false,
cards: [],
@@ -23,11 +22,12 @@ new Vue({
cardDialog: {
show: false,
data: {
- counter:1,
+ counter: 1,
k0: '',
k1: '',
k2: '',
- card_name:''},
+ card_name: ''
+ },
temp: {}
},
cardsTable: {
@@ -190,7 +190,7 @@ new Vue({
})
})
},
- openQrCodeDialog(cardId) {
+ openQrCodeDialog (cardId) {
var card = _.findWhere(this.cards, {id: cardId})
this.qrCodeDialog.data = {
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
@@ -217,7 +217,7 @@ new Vue({
typeof this.cardDialog.data.card_name === 'string' &&
this.cardDialog.data.card_name.search('debug') > -1
- self.cardDialog.data.k0 = debugcard
+ self.cardDialog.data.k0 = debugcard
? '11111111111111111111111111111111'
: genRanHex(32)
@@ -352,7 +352,7 @@ new Vue({
},
exportRefundsCSV: function () {
LNbits.utils.exportCSV(this.refundsTable.columns, this.refunds)
- },
+ }
},
created: function () {
if (this.g.user.wallets.length) {
diff --git a/lnbits/extensions/boltcards/tasks.py b/lnbits/extensions/boltcards/tasks.py
index 30a290e9d..bfe4f257b 100644
--- a/lnbits/extensions/boltcards/tasks.py
+++ b/lnbits/extensions/boltcards/tasks.py
@@ -27,8 +27,9 @@ async def on_invoice_paid(payment: Payment) -> None:
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"))])
+ 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"))
+ refund = await create_refund(
+ hit_id=hit.id, refund_amount=payment.extra.get("amount")
+ )
await mark_webhook_sent(payment, 1)
-
diff --git a/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html b/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html
index e67ba14bf..be4e2ae86 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html
@@ -15,10 +15,6 @@
>More details
-
- Created by,
- iWarp
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 96f4f994b..2a613fda2 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -7,12 +7,19 @@
-
-
+
+
Cards
-
+
Add card
@@ -53,32 +60,38 @@
icon="qr_code"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.id)"
- >
Card key credentials
+ >
Card key credentials
lnurl://...Click to copy, then add to NFC card
-
+ lnurlLink
+ >lnurl://...Click to copy, then add to NFC card
+
{{ col.value }}
- DISABLE
+ >DISABLE
ENABLE
+ v-else
+ dense
+ @click="enableCard(props.row.wallet, props.row.id, true)"
+ color="green"
+ >ENABLE
Edit card
+ >Edit card
Deleting card will also delete all records
+ >Deleting card will also delete all records
@@ -220,7 +237,7 @@
type="number"
label="Max transaction (sats)"
class="q-pr-sm"
- >
+ >
+ filled
+ dense
+ emit-value
+ v-model.trim="cardDialog.data.card_name"
+ type="text"
+ label="Card name "
+ >
-
-
- Get from the card you'll use, using an NFC app
-
-
+ Get from the card you'll use, using an NFC app
Tap card to scan UID (coming soon)
+ outline
+ disable
+ color="grey"
+ icon="nfc"
+ :disable="nfcTagReading"
+ >Tap card to scan UID (coming soon)
+
-
-
-
+
-
-
-
-
-
-
-
Zero if you don't know.
-
+
+
+
+
+
+
+ Zero if you don't know.
+ Generate keys
-
+
Date: Mon, 29 Aug 2022 07:34:56 -0600
Subject: [PATCH 45/73] make format
---
lnbits/extensions/boltcards/lnurl.py | 19 ++++++-------------
lnbits/extensions/boltcards/models.py | 3 +--
.../extensions/boltcards/static/js/index.js | 2 +-
lnbits/extensions/boltcards/tasks.py | 2 +-
.../boltcards/templates/boltcards/index.html | 2 +-
lnbits/extensions/boltcards/views_api.py | 7 +++----
6 files changed, 13 insertions(+), 22 deletions(-)
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index a1630e2be..9399fb369 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -2,21 +2,19 @@ import base64
import hashlib
import hmac
import json
+import secrets
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
-from starlette.exceptions import HTTPException
-
-import secrets
-from http import HTTPStatus
-
from fastapi.params import Depends, Query
+from lnurl import Lnurl, LnurlWithdrawResponse
+from lnurl import encode as lnurl_encode # type: ignore
+from lnurl.types import LnurlPayMetadata # type: ignore
+from loguru import logger
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse
@@ -24,17 +22,12 @@ from starlette.responses import HTMLResponse
from lnbits.core.services import create_invoice
from lnbits.core.views.api import pay_invoice
-from lnurl import Lnurl, LnurlWithdrawResponse
-from lnurl import encode as lnurl_encode # type: ignore
-from lnurl.types import LnurlPayMetadata # type: ignore
-
from . import boltcards_ext
from .crud import (
create_hit,
get_card,
- get_card_by_uid,
get_card_by_otp,
- get_card,
+ get_card_by_uid,
get_hit,
get_hits_today,
spend_hit,
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 80e3b9734..21096640a 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -1,9 +1,8 @@
-from fastapi.params import Query
-from pydantic import BaseModel
from sqlite3 import Row
from typing import Optional
from fastapi import Request
+from fastapi.params import Query
from lnurl import Lnurl
from lnurl import encode as lnurl_encode # type: ignore
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 33704f3a2..27536304f 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -190,7 +190,7 @@ new Vue({
})
})
},
- openQrCodeDialog (cardId) {
+ openQrCodeDialog(cardId) {
var card = _.findWhere(this.cards, {id: cardId})
this.qrCodeDialog.data = {
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
diff --git a/lnbits/extensions/boltcards/tasks.py b/lnbits/extensions/boltcards/tasks.py
index bfe4f257b..a7eea026d 100644
--- a/lnbits/extensions/boltcards/tasks.py
+++ b/lnbits/extensions/boltcards/tasks.py
@@ -7,7 +7,7 @@ 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
+from .crud import create_refund, get_hit
async def wait_for_paid_invoices():
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 2a613fda2..73e5820b0 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -7,7 +7,7 @@
-
+
Cards
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index f1e02810f..698e10948 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -2,6 +2,7 @@ import secrets
from http import HTTPStatus
from fastapi.params import Depends, Query
+from loguru import logger
from starlette.exceptions import HTTPException
from starlette.requests import Request
@@ -13,22 +14,20 @@ from .crud import (
create_card,
create_hit,
delete_card,
+ enable_disable_card,
get_card,
get_card_by_otp,
get_card_by_uid,
get_cards,
get_hits,
+ get_refunds,
update_card,
update_card_counter,
update_card_otp,
- enable_disable_card,
- 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(
From c8b725830bee3e35302c454acbacd50e17208006 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 07:55:51 -0600
Subject: [PATCH 46/73] make copy lnurl to clipboard work
---
lnbits/extensions/boltcards/static/js/index.js | 1 +
lnbits/extensions/boltcards/templates/boltcards/index.html | 3 +--
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 27536304f..e6c052ace 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -16,6 +16,7 @@ new Vue({
return {
toggleAdvanced: false,
nfcTagReading: false,
+ lnurlLink: `lnurlw://${window.location.host}/boltcards/api/v1/scan/`,
cards: [],
hits: [],
refunds: [],
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 73e5820b0..6938eb46a 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -68,8 +68,7 @@
outline
color="grey"
@click="copyText(lnurlLink + props.row.uid)"
- lnurlLink
- >lnurl://...lnurlw://...Click to copy, then add to NFC card
From 7400d7f43046c660060977c858a35c96cda76b16 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 07:58:29 -0600
Subject: [PATCH 47/73] link to play store app in qr code dialog
---
lnbits/extensions/boltcards/templates/boltcards/index.html | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 6938eb46a..0561cc018 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -370,7 +370,12 @@
>
- (QR code is for setting the keys with bolt-nfc-android-app)
+ (QR code is for setting the keys with
+ bolt-nfc-android-app)
Name: {{ qrCodeDialog.data.name }}
From 7ea3830fed2f6028263b8b91fdc06fe7ce44c20f Mon Sep 17 00:00:00 2001
From: ben
Date: Mon, 29 Aug 2022 15:11:25 +0100
Subject: [PATCH 48/73] Added NFC to get UID
---
.../extensions/boltcards/static/js/index.js | 59 +++++++++++++++
.../boltcards/templates/boltcards/index.html | 74 +++++++++++--------
2 files changed, 104 insertions(+), 29 deletions(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 33704f3a2..8f387f95d 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -26,6 +26,7 @@ new Vue({
k0: '',
k1: '',
k2: '',
+ uid: '',
card_name: ''
},
temp: {}
@@ -146,6 +147,64 @@ new Vue({
}
},
methods: {
+ readNfcTag: function () {
+ try {
+ const self = this
+
+ if (typeof NDEFReader == 'undefined') {
+ throw {
+ toString: function () {
+ return 'NFC not supported on this device or browser.'
+ }
+ }
+ }
+
+ const ndef = new NDEFReader()
+
+ const readerAbortController = new AbortController()
+ readerAbortController.signal.onabort = event => {
+ console.log('All NFC Read operations have been aborted.')
+ }
+
+ this.nfcTagReading = true
+ this.$q.notify({
+ message: 'Tap your NFC tag to pay this invoice with LNURLw.'
+ })
+
+ return ndef.scan({signal: readerAbortController.signal}).then(() => {
+ ndef.onreadingerror = () => {
+ self.nfcTagReading = false
+
+ this.$q.notify({
+ type: 'negative',
+ message: 'There was an error reading this NFC tag.'
+ })
+
+ readerAbortController.abort()
+ }
+
+ ndef.onreading = ({message, serialNumber}) => {
+ //Decode NDEF data from tag
+ var self = this
+ self.cardDialog.data.uid = serialNumber
+ .toUpperCase()
+ .replaceAll(':', '')
+ this.$q.notify({
+ type: 'positive',
+ message: 'NFC tag read successfully.'
+ })
+ }
+ })
+ } catch (error) {
+ this.nfcTagReading = false
+ this.$q.notify({
+ type: 'negative',
+ message: error
+ ? error.toString()
+ : 'An unexpected error has occurred.'
+ })
+ }
+ },
getCards: function () {
var self = this
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 2a613fda2..ec2611a98 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -43,7 +43,6 @@
- Base URL
{{ col.label }}
@@ -60,18 +59,8 @@
icon="qr_code"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.id)"
- >Card key credentials
-
-
- lnurl://...Click to copy, then add to NFC card
+ Card key credentials
@@ -90,8 +79,8 @@
dense
@click="enableCard(props.row.wallet, props.row.id, true)"
color="green"
- >ENABLE
+ >ENABLE
+
Edit card
+ Edit card
+
Deleting card will also delete all records
+ Deleting card will also delete all records
+
@@ -257,7 +248,8 @@
v-model.trim="cardDialog.data.card_name"
type="text"
label="Card name "
- >
+ >
+
Get from the card you'll use, using an NFC app
+ Get from the card you'll use, using an NFC app
+
Tap card to scan UID (coming soon)
+ Tap card to scan UID
+
@@ -322,10 +317,11 @@
v-model.number="cardDialog.data.counter"
type="number"
label="Initial counter"
- >Zero if you don't know.
+ Zero if you don't know.
+
Create Card
+ >Create Card
+
Cancel
@@ -371,7 +367,7 @@
>
- (QR code is for setting the keys with bolt-nfc-android-app)
+ (Keys for bolt-nfc-android-app)
Name: {{ qrCodeDialog.data.name }}
@@ -380,6 +376,26 @@
Meta key: {{ qrCodeDialog.data.k1 }}
File key: {{ qrCodeDialog.data.k2 }}
+
+
+
+
+
+
+ Click to copy, then add to NFC card
+
{% endraw %}
Close
From a8ac90da5c79a76b16cc07c9bbf3cd6474700f4d Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:19:39 -0600
Subject: [PATCH 49/73] make scan nfc button work
---
.../extensions/boltcards/static/js/index.js | 63 +++++++++++++++++++
.../boltcards/templates/boltcards/index.html | 3 +-
2 files changed, 65 insertions(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index e6c052ace..315ed59fd 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -230,6 +230,69 @@ new Vue({
? '33333333333333333333333333333333'
: genRanHex(32)
},
+ readNfcTag: function () {
+ try {
+ const self = this
+
+ if (typeof NDEFReader == 'undefined') {
+ throw {
+ toString: function () {
+ return 'NFC not supported on this device or browser.'
+ }
+ }
+ }
+
+ const ndef = new NDEFReader()
+
+ const readerAbortController = new AbortController()
+ readerAbortController.signal.onabort = event => {
+ console.log('All NFC Read operations have been aborted.')
+ }
+
+ this.nfcTagReading = true
+ this.$q.notify({
+ message: 'Tap your NFC tag to read its UID'
+ })
+
+ return ndef.scan({signal: readerAbortController.signal}).then(() => {
+ ndef.onreadingerror = () => {
+ self.nfcTagReading = false
+
+ this.$q.notify({
+ type: 'negative',
+ message: 'There was an error reading this NFC tag.'
+ })
+
+ readerAbortController.abort()
+ }
+
+ ndef.onreading = ({message, serialNumber}) => {
+ self.nfcTagReading = false
+
+ self.cardDialog.data.uid = serialNumber
+ .replaceAll(':', '')
+ .toUpperCase()
+
+ this.$q.notify({
+ type: 'positive',
+ message: 'NFC tag read successfully.'
+ })
+
+ setTimeout(() => {
+ readerAbortController.abort()
+ }, 1000)
+ }
+ })
+ } catch (error) {
+ this.nfcTagReading = false
+ this.$q.notify({
+ type: 'negative',
+ message: error
+ ? error.toString()
+ : 'An unexpected error has occurred.'
+ })
+ }
+ },
closeFormDialog: function () {
this.cardDialog.data = {}
},
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 0561cc018..eb29e5436 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -278,7 +278,8 @@
color="grey"
icon="nfc"
:disable="nfcTagReading"
- >Tap card to scan UID (coming soon)Tap card to scan UID
From c35ae109a4bb91b0d0beb004f49c0de44ca6943b Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:21:47 -0600
Subject: [PATCH 50/73] remove v-el from element, this was removed in Vue v2,
ref does not appear to be used anywhere
---
lnbits/extensions/boltcards/templates/boltcards/index.html | 1 -
1 file changed, 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index eb29e5436..c4a53bd7b 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -332,7 +332,6 @@
class="q-ml-auto"
v-on:click="generateKeys"
v-on:click.right="debugKeys"
- v-el:keybtn
>Generate keys
From 630cc296fb56ddb33995b6c659add7c8d26f4163 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:22:51 -0600
Subject: [PATCH 51/73] do not add lnurlw:// to the string
---
lnbits/extensions/boltcards/static/js/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 315ed59fd..b59b712b7 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -16,7 +16,7 @@ new Vue({
return {
toggleAdvanced: false,
nfcTagReading: false,
- lnurlLink: `lnurlw://${window.location.host}/boltcards/api/v1/scan/`,
+ lnurlLink: `${window.location.host}/boltcards/api/v1/scan/`,
cards: [],
hits: [],
refunds: [],
From 33f9ae9f66f8e1185f907691d467a8a9486d2f64 Mon Sep 17 00:00:00 2001
From: ben
Date: Mon, 29 Aug 2022 15:37:31 +0100
Subject: [PATCH 52/73] Added some unique checks so only 1 record can be made
per card
---
lnbits/extensions/boltcards/migrations.py | 8 +-
.../boltcards/templates/boltcards/index.html | 272 ++++--------------
lnbits/extensions/boltcards/views_api.py | 6 +-
3 files changed, 59 insertions(+), 227 deletions(-)
diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py
index c20ef449c..5be3d08fb 100644
--- a/lnbits/extensions/boltcards/migrations.py
+++ b/lnbits/extensions/boltcards/migrations.py
@@ -5,10 +5,10 @@ async def m001_initial(db):
await db.execute(
"""
CREATE TABLE boltcards.cards (
- id TEXT PRIMARY KEY,
+ id TEXT PRIMARY KEY UNIQUE,
wallet TEXT NOT NULL,
card_name TEXT NOT NULL,
- uid TEXT NOT NULL,
+ uid TEXT NOT NULL UNIQUE,
counter INT NOT NULL DEFAULT 0,
tx_limit TEXT NOT NULL,
daily_limit TEXT NOT NULL,
@@ -30,7 +30,7 @@ async def m001_initial(db):
await db.execute(
"""
CREATE TABLE boltcards.hits (
- id TEXT PRIMARY KEY,
+ id TEXT PRIMARY KEY UNIQUE,
card_id TEXT NOT NULL,
ip TEXT NOT NULL,
spent BOOL NOT NULL DEFAULT True,
@@ -48,7 +48,7 @@ async def m001_initial(db):
await db.execute(
"""
CREATE TABLE boltcards.refunds (
- id TEXT PRIMARY KEY,
+ id TEXT PRIMARY KEY UNIQUE,
hit_id TEXT NOT NULL,
refund_amount INT NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index ec2611a98..43901f724 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -12,33 +12,18 @@
Cards
-
+
Add card
- Export to CSV
+ Export to CSV
-
+
{% raw %}
@@ -53,13 +38,8 @@
-
+
Card key credentials
@@ -67,45 +47,19 @@
{{ col.value }}
- DISABLE
- ENABLE
+ DISABLE
+ ENABLE
-
+
Edit card
-
- Deleting card will also delete all records
+
+ Deleting card will also delete all records
@@ -121,19 +75,11 @@
Hits
- Export to CSV
+ Export to CSV
-
+
{% raw %}
@@ -160,19 +106,11 @@
Refunds
- Export to CSV
+ Export to CSV
-
+
{% raw %}
@@ -209,148 +147,55 @@
-
+
-
+
-
- Get from the card you'll use, using an NFC app
+
+ Get from the card you'll use, using an NFC app
-
+
Tap card to scan UID
-
+
-
+
-
-
+
+
-
- Zero if you don't know.
+
+ Zero if you don't know.
- Generate keys
+ Generate keys
- Update Card
- Create Card
+ Update Card
+ Create Card
- Cancel
+ Cancel
@@ -360,11 +205,7 @@
{% raw %}
-
+
(Keys for bolt-nfc-android-app)
@@ -375,27 +216,14 @@
Lock key: {{ qrCodeDialog.data.k0 }}
Meta key: {{ qrCodeDialog.data.k1 }}
File key: {{ qrCodeDialog.data.k2 }}
-
-
-
-
-
-
+
+
+
+
+
Click to copy, then add to NFC card
-
+
{% endraw %}
Close
@@ -406,4 +234,4 @@
{% endblock %} {% block scripts %} {{ window_vars(user) }}
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index f1e02810f..358dd0b4d 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -74,7 +74,11 @@ async def api_card_create_or_update(
raise HTTPException(
detail="Invalid byte data provided.", status_code=HTTPStatus.BAD_REQUEST
)
-
+ checkUid = await get_card_by_uid(data.uid)
+ if checkUid:
+ raise HTTPException(
+ detail="UID already registered. Delete registered card and try again.", status_code=HTTPStatus.BAD_REQUEST
+ )
if card_id:
card = await get_card(card_id)
if not card:
From a086db43baec5c5dd2c7f2f57487a61a5e1f13c1 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:38:48 -0600
Subject: [PATCH 53/73] Revert "do not add lnurlw:// to the string"
This reverts commit 630cc296fb56ddb33995b6c659add7c8d26f4163.
---
lnbits/extensions/boltcards/static/js/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index b59b712b7..315ed59fd 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -16,7 +16,7 @@ new Vue({
return {
toggleAdvanced: false,
nfcTagReading: false,
- lnurlLink: `${window.location.host}/boltcards/api/v1/scan/`,
+ lnurlLink: `lnurlw://${window.location.host}/boltcards/api/v1/scan/`,
cards: [],
hits: [],
refunds: [],
From d7696fdc0f9c25924955d24088a0f0905a082168 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:38:54 -0600
Subject: [PATCH 54/73] Revert "remove v-el from element, this was removed in
Vue v2, ref does not appear to be used anywhere"
This reverts commit c35ae109a4bb91b0d0beb004f49c0de44ca6943b.
---
lnbits/extensions/boltcards/templates/boltcards/index.html | 1 +
1 file changed, 1 insertion(+)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index c4a53bd7b..eb29e5436 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -332,6 +332,7 @@
class="q-ml-auto"
v-on:click="generateKeys"
v-on:click.right="debugKeys"
+ v-el:keybtn
>Generate keys
From 5dd6c0d2f41a17ab1432f911a6431ecec1ae342d Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:39:02 -0600
Subject: [PATCH 55/73] Revert "make scan nfc button work"
This reverts commit a8ac90da5c79a76b16cc07c9bbf3cd6474700f4d.
---
.../extensions/boltcards/static/js/index.js | 63 -------------------
.../boltcards/templates/boltcards/index.html | 3 +-
2 files changed, 1 insertion(+), 65 deletions(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 315ed59fd..e6c052ace 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -230,69 +230,6 @@ new Vue({
? '33333333333333333333333333333333'
: genRanHex(32)
},
- readNfcTag: function () {
- try {
- const self = this
-
- if (typeof NDEFReader == 'undefined') {
- throw {
- toString: function () {
- return 'NFC not supported on this device or browser.'
- }
- }
- }
-
- const ndef = new NDEFReader()
-
- const readerAbortController = new AbortController()
- readerAbortController.signal.onabort = event => {
- console.log('All NFC Read operations have been aborted.')
- }
-
- this.nfcTagReading = true
- this.$q.notify({
- message: 'Tap your NFC tag to read its UID'
- })
-
- return ndef.scan({signal: readerAbortController.signal}).then(() => {
- ndef.onreadingerror = () => {
- self.nfcTagReading = false
-
- this.$q.notify({
- type: 'negative',
- message: 'There was an error reading this NFC tag.'
- })
-
- readerAbortController.abort()
- }
-
- ndef.onreading = ({message, serialNumber}) => {
- self.nfcTagReading = false
-
- self.cardDialog.data.uid = serialNumber
- .replaceAll(':', '')
- .toUpperCase()
-
- this.$q.notify({
- type: 'positive',
- message: 'NFC tag read successfully.'
- })
-
- setTimeout(() => {
- readerAbortController.abort()
- }, 1000)
- }
- })
- } catch (error) {
- this.nfcTagReading = false
- this.$q.notify({
- type: 'negative',
- message: error
- ? error.toString()
- : 'An unexpected error has occurred.'
- })
- }
- },
closeFormDialog: function () {
this.cardDialog.data = {}
},
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index eb29e5436..0561cc018 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -278,8 +278,7 @@
color="grey"
icon="nfc"
:disable="nfcTagReading"
- @click="readNfcTag"
- >Tap card to scan UIDTap card to scan UID (coming soon)
From e715ff52d274f56c8f4bfc14d4f1b3e8ea1fe6f7 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:39:16 -0600
Subject: [PATCH 56/73] Revert "link to play store app in qr code dialog"
This reverts commit 7400d7f43046c660060977c858a35c96cda76b16.
---
lnbits/extensions/boltcards/templates/boltcards/index.html | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 0561cc018..6938eb46a 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -370,12 +370,7 @@
>
- (QR code is for setting the keys with
- bolt-nfc-android-app)
+ (QR code is for setting the keys with bolt-nfc-android-app)
Name: {{ qrCodeDialog.data.name }}
From 93483f76ab98680f67dc04de748933991044de62 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:39:30 -0600
Subject: [PATCH 57/73] Revert "make copy lnurl to clipboard work"
This reverts commit c8b725830bee3e35302c454acbacd50e17208006.
---
lnbits/extensions/boltcards/static/js/index.js | 1 -
lnbits/extensions/boltcards/templates/boltcards/index.html | 3 ++-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index e6c052ace..27536304f 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -16,7 +16,6 @@ new Vue({
return {
toggleAdvanced: false,
nfcTagReading: false,
- lnurlLink: `lnurlw://${window.location.host}/boltcards/api/v1/scan/`,
cards: [],
hits: [],
refunds: [],
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 6938eb46a..73e5820b0 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -68,7 +68,8 @@
outline
color="grey"
@click="copyText(lnurlLink + props.row.uid)"
- >lnurlw://...lnurl://...Click to copy, then add to NFC card
From 8e41f7867eccf43b6e9f0e41cea8d757ab22e485 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:39:37 -0600
Subject: [PATCH 58/73] Revert "make format"
This reverts commit 8832d6e4ebc868a861786ddb89ab1df82075f45c.
---
lnbits/extensions/boltcards/lnurl.py | 19 +++++++++++++------
lnbits/extensions/boltcards/models.py | 3 ++-
.../extensions/boltcards/static/js/index.js | 2 +-
lnbits/extensions/boltcards/tasks.py | 2 +-
.../boltcards/templates/boltcards/index.html | 2 +-
lnbits/extensions/boltcards/views_api.py | 7 ++++---
6 files changed, 22 insertions(+), 13 deletions(-)
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 9399fb369..a1630e2be 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -2,19 +2,21 @@ import base64
import hashlib
import hmac
import json
-import secrets
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
+from starlette.exceptions import HTTPException
+
+import secrets
+from http import HTTPStatus
+
from fastapi.params import Depends, Query
-from lnurl import Lnurl, LnurlWithdrawResponse
-from lnurl import encode as lnurl_encode # type: ignore
-from lnurl.types import LnurlPayMetadata # type: ignore
-from loguru import logger
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse
@@ -22,12 +24,17 @@ from starlette.responses import HTMLResponse
from lnbits.core.services import create_invoice
from lnbits.core.views.api import pay_invoice
+from lnurl import Lnurl, LnurlWithdrawResponse
+from lnurl import encode as lnurl_encode # type: ignore
+from lnurl.types import LnurlPayMetadata # type: ignore
+
from . import boltcards_ext
from .crud import (
create_hit,
get_card,
- get_card_by_otp,
get_card_by_uid,
+ get_card_by_otp,
+ get_card,
get_hit,
get_hits_today,
spend_hit,
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 21096640a..80e3b9734 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -1,8 +1,9 @@
+from fastapi.params import Query
+from pydantic import BaseModel
from sqlite3 import Row
from typing import Optional
from fastapi import Request
-from fastapi.params import Query
from lnurl import Lnurl
from lnurl import encode as lnurl_encode # type: ignore
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 27536304f..33704f3a2 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -190,7 +190,7 @@ new Vue({
})
})
},
- openQrCodeDialog(cardId) {
+ openQrCodeDialog (cardId) {
var card = _.findWhere(this.cards, {id: cardId})
this.qrCodeDialog.data = {
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
diff --git a/lnbits/extensions/boltcards/tasks.py b/lnbits/extensions/boltcards/tasks.py
index a7eea026d..bfe4f257b 100644
--- a/lnbits/extensions/boltcards/tasks.py
+++ b/lnbits/extensions/boltcards/tasks.py
@@ -7,7 +7,7 @@ from lnbits.core import db as core_db
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
-from .crud import create_refund, get_hit
+from .crud import get_hit, create_refund
async def wait_for_paid_invoices():
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 73e5820b0..2a613fda2 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -7,7 +7,7 @@
-
+
Cards
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 698e10948..f1e02810f 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -2,7 +2,6 @@ import secrets
from http import HTTPStatus
from fastapi.params import Depends, Query
-from loguru import logger
from starlette.exceptions import HTTPException
from starlette.requests import Request
@@ -14,20 +13,22 @@ from .crud import (
create_card,
create_hit,
delete_card,
- enable_disable_card,
get_card,
get_card_by_otp,
get_card_by_uid,
get_cards,
get_hits,
- get_refunds,
update_card,
update_card_counter,
update_card_otp,
+ enable_disable_card,
+ 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(
From 51f9166744f9e1c850de0c872a42a62cf0ddcec7 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:49:04 -0600
Subject: [PATCH 59/73] define lnurlLink
---
lnbits/extensions/boltcards/static/js/index.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 8f387f95d..336a5a527 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -16,6 +16,7 @@ new Vue({
return {
toggleAdvanced: false,
nfcTagReading: false,
+ lnurlLink: `${window.location.host}/boltcards/api/v1/scan/`,
cards: [],
hits: [],
refunds: [],
From b7c7b28dfcb3998f2fd8c4bcbc280011fe8656f0 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:49:57 -0600
Subject: [PATCH 60/73] remove v-el:keybtn, was removed in vue v2. unnecessary
ref anyways
---
lnbits/extensions/boltcards/templates/boltcards/index.html | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 43901f724..20746d5b6 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -188,8 +188,7 @@
Zero if you don't know.
- Generate keys
+ Generate keys
Update Card
From 6d9d037c068f3ea8382df10bfd6d4d1e310f7776 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:51:15 -0600
Subject: [PATCH 61/73] add link to play store for bolt card app listing
---
lnbits/extensions/boltcards/templates/boltcards/index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 20746d5b6..8f5a7b0e6 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -207,7 +207,7 @@
- (Keys for bolt-nfc-android-app)
+ (Keys for bolt-nfc-android-app)
Name: {{ qrCodeDialog.data.name }}
From 56b4eb4a5eb97909eed57be31c4f79eee3226782 Mon Sep 17 00:00:00 2001
From: ben
Date: Mon, 29 Aug 2022 15:51:22 +0100
Subject: [PATCH 62/73] Added "lnurl://" to refund
---
lnbits/extensions/boltcards/lnurl.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index a1630e2be..ce2e8773c 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -99,7 +99,7 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
"k1": hit.id,
"minWithdrawable": 1 * 1000,
"maxWithdrawable": card.tx_limit * 1000,
- "defaultDescription": f"Boltcard (refund address {lnurlpay})",
+ "defaultDescription": f"Boltcard (refund address lnurl://{lnurlpay})",
}
From fe83cad7a36f890f6fd8254989f54f2da8e3efb2 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 08:51:32 -0600
Subject: [PATCH 63/73] formatting
---
lnbits/extensions/boltcards/lnurl.py | 19 +-
lnbits/extensions/boltcards/models.py | 3 +-
.../extensions/boltcards/static/js/index.js | 2 +-
lnbits/extensions/boltcards/tasks.py | 2 +-
.../boltcards/templates/boltcards/index.html | 279 ++++++++++++++----
lnbits/extensions/boltcards/views_api.py | 10 +-
6 files changed, 242 insertions(+), 73 deletions(-)
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index a1630e2be..9399fb369 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -2,21 +2,19 @@ import base64
import hashlib
import hmac
import json
+import secrets
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
-from starlette.exceptions import HTTPException
-
-import secrets
-from http import HTTPStatus
-
from fastapi.params import Depends, Query
+from lnurl import Lnurl, LnurlWithdrawResponse
+from lnurl import encode as lnurl_encode # type: ignore
+from lnurl.types import LnurlPayMetadata # type: ignore
+from loguru import logger
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse
@@ -24,17 +22,12 @@ from starlette.responses import HTMLResponse
from lnbits.core.services import create_invoice
from lnbits.core.views.api import pay_invoice
-from lnurl import Lnurl, LnurlWithdrawResponse
-from lnurl import encode as lnurl_encode # type: ignore
-from lnurl.types import LnurlPayMetadata # type: ignore
-
from . import boltcards_ext
from .crud import (
create_hit,
get_card,
- get_card_by_uid,
get_card_by_otp,
- get_card,
+ get_card_by_uid,
get_hit,
get_hits_today,
spend_hit,
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 80e3b9734..21096640a 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -1,9 +1,8 @@
-from fastapi.params import Query
-from pydantic import BaseModel
from sqlite3 import Row
from typing import Optional
from fastapi import Request
+from fastapi.params import Query
from lnurl import Lnurl
from lnurl import encode as lnurl_encode # type: ignore
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 336a5a527..254f9d88b 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -250,7 +250,7 @@ new Vue({
})
})
},
- openQrCodeDialog (cardId) {
+ openQrCodeDialog(cardId) {
var card = _.findWhere(this.cards, {id: cardId})
this.qrCodeDialog.data = {
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
diff --git a/lnbits/extensions/boltcards/tasks.py b/lnbits/extensions/boltcards/tasks.py
index bfe4f257b..a7eea026d 100644
--- a/lnbits/extensions/boltcards/tasks.py
+++ b/lnbits/extensions/boltcards/tasks.py
@@ -7,7 +7,7 @@ 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
+from .crud import create_refund, get_hit
async def wait_for_paid_invoices():
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 8f5a7b0e6..1338b859d 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -7,23 +7,38 @@
-
- Export to CSV
+ Export to CSV
-
+
{% raw %}
@@ -38,8 +53,13 @@
-
+
Card key credentials
@@ -47,19 +67,45 @@
{{ col.value }}
- DISABLE
- ENABLE
+ DISABLE
+ ENABLE
-
+
Edit card
-
- Deleting card will also delete all records
+
+ Deleting card will also delete all records
@@ -75,11 +121,19 @@
Hits
- Export to CSV
+ Export to CSV
-
+
{% raw %}
@@ -106,11 +160,19 @@
Refunds
- Export to CSV
+ Export to CSV
-
+
{% raw %}
@@ -147,54 +209,147 @@
-
+
-
+
-
- Get from the card you'll use, using an NFC app
+
+ Get from the card you'll use, using an NFC app
-
+
Tap card to scan UID
-
+
-
+
-
-
+
+
-
- Zero if you don't know.
+
+ Zero if you don't know.
- Generate keys
+ Generate keys
- Update Card
- Create Card
+ Update Card
+ Create Card
- Cancel
+ Cancel
@@ -204,10 +359,19 @@
{% raw %}
-
+
- (Keys for bolt-nfc-android-app)
+ (Keys for
+ bolt-nfc-android-app)
Name: {{ qrCodeDialog.data.name }}
@@ -215,14 +379,27 @@
Lock key: {{ qrCodeDialog.data.k0 }}
Meta key: {{ qrCodeDialog.data.k1 }}
File key: {{ qrCodeDialog.data.k2 }}
-
-
-
-
-
+
+
+
+
+
+
Click to copy, then add to NFC card
-
+
{% endraw %}
Close
@@ -233,4 +410,4 @@
{% endblock %} {% block scripts %} {{ window_vars(user) }}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 358dd0b4d..5f0570366 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -2,6 +2,7 @@ import secrets
from http import HTTPStatus
from fastapi.params import Depends, Query
+from loguru import logger
from starlette.exceptions import HTTPException
from starlette.requests import Request
@@ -13,22 +14,20 @@ from .crud import (
create_card,
create_hit,
delete_card,
+ enable_disable_card,
get_card,
get_card_by_otp,
get_card_by_uid,
get_cards,
get_hits,
+ get_refunds,
update_card,
update_card_counter,
update_card_otp,
- enable_disable_card,
- 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(
@@ -77,7 +76,8 @@ async def api_card_create_or_update(
checkUid = await get_card_by_uid(data.uid)
if checkUid:
raise HTTPException(
- detail="UID already registered. Delete registered card and try again.", status_code=HTTPStatus.BAD_REQUEST
+ detail="UID already registered. Delete registered card and try again.",
+ status_code=HTTPStatus.BAD_REQUEST,
)
if card_id:
card = await get_card(card_id)
From 7baec1de157fb60455775f33e01bc155c25d33de Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 09:05:00 -0600
Subject: [PATCH 64/73] success_action was not defined, does not appear to be
necessary for this
---
lnbits/extensions/boltcards/lnurl.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 18197ba5e..8dba51bf0 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -199,6 +199,6 @@ async def lnurlp_callback(
extra={"refund": hit_id},
)
- payResponse = {"pr": payment_request, "successAction": success_action, "routes": []}
+ payResponse = {"pr": payment_request, "routes": []}
return json.dumps(payResponse)
From 88210129b70880f0f5e2715f76d19737b97fde80 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 09:32:13 -0600
Subject: [PATCH 65/73] make refunds work properly
---
lnbits/extensions/boltcards/__init__.py | 10 ++++++++++
lnbits/extensions/boltcards/crud.py | 8 +++-----
.../extensions/boltcards/static/js/index.js | 4 ++--
lnbits/extensions/boltcards/tasks.py | 20 +++++++++++++++----
4 files changed, 31 insertions(+), 11 deletions(-)
diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py
index 11b8dd2d1..bfdc7492e 100644
--- a/lnbits/extensions/boltcards/__init__.py
+++ b/lnbits/extensions/boltcards/__init__.py
@@ -1,8 +1,11 @@
+import asyncio
+
from fastapi import APIRouter
from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
+from lnbits.tasks import catch_everything_and_restart
db = Database("ext_boltcards")
@@ -23,5 +26,12 @@ def boltcards_renderer():
from .lnurl import * # noqa
from .tasks import * # noqa
+
+
+def boltcards_start():
+ loop = asyncio.get_event_loop()
+ loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+
+
from .views import * # noqa
from .views_api import * # noqa
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index 1c48500da..be9b29618 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -214,19 +214,17 @@ async def create_refund(hit_id, refund_amount) -> Refund:
refund_id = urlsafe_short_hash()
await db.execute(
"""
- INSERT INTO boltcards.hits (
+ INSERT INTO boltcards.refunds (
id,
hit_id,
- refund_amount,
- payment_hash
+ refund_amount
)
- VALUES (?, ?, ?, ?)
+ VALUES (?, ?, ?)
""",
(
refund_id,
hit_id,
refund_amount,
- payment_hash,
),
)
refund = await get_refund(refund_id)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 254f9d88b..4d6134f9e 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -84,10 +84,10 @@ new Vue({
field: 'refund_amount'
},
{
- name: 'time',
+ name: 'date',
align: 'left',
label: 'Time',
- field: 'time'
+ field: 'date'
}
],
pagination: {
diff --git a/lnbits/extensions/boltcards/tasks.py b/lnbits/extensions/boltcards/tasks.py
index a7eea026d..1b51c98ba 100644
--- a/lnbits/extensions/boltcards/tasks.py
+++ b/lnbits/extensions/boltcards/tasks.py
@@ -20,16 +20,28 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if payment.extra.get("tag")[0:6] != "Refund":
- # not an lnurlp invoice
+ if not payment.extra.get("refund"):
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"))])
+ hit = await get_hit(payment.extra.get("refund"))
+
if hit:
refund = await create_refund(
- hit_id=hit.id, refund_amount=payment.extra.get("amount")
+ hit_id=hit.id, refund_amount=(payment.amount / 1000)
)
await mark_webhook_sent(payment, 1)
+
+
+async def mark_webhook_sent(payment: Payment, status: int) -> None:
+ payment.extra["wh_status"] = status
+
+ await core_db.execute(
+ """
+ UPDATE apipayments SET extra = ?
+ WHERE hash = ?
+ """,
+ (json.dumps(payment.extra), payment.payment_hash),
+ )
From a6cf19c29df85a67b6abe803bf2dddf1767a8049 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 09:33:28 -0600
Subject: [PATCH 66/73] potential race condition here, hits is dependent on
cards to get the card name in hits
---
lnbits/extensions/boltcards/static/js/index.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 4d6134f9e..c23a2583a 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -220,6 +220,9 @@ new Vue({
return mapCards(obj)
})
})
+ .then(function () {
+ self.getHits()
+ })
},
getHits: function () {
var self = this
@@ -417,7 +420,6 @@ new Vue({
created: function () {
if (this.g.user.wallets.length) {
this.getCards()
- this.getHits()
this.getRefunds()
}
}
From 993511bcd4fd49c512d031f5b8c74dc8d4b668ac Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 09:40:35 -0600
Subject: [PATCH 67/73] fix validation check for existing card UID, was
preventing all edits of cards. we should validate a bit differently for
updates vs. creates
---
lnbits/extensions/boltcards/views_api.py | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index 5f0570366..7b8357cf8 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -73,12 +73,6 @@ async def api_card_create_or_update(
raise HTTPException(
detail="Invalid byte data provided.", status_code=HTTPStatus.BAD_REQUEST
)
- checkUid = await get_card_by_uid(data.uid)
- if checkUid:
- raise HTTPException(
- detail="UID already registered. Delete registered card and try again.",
- status_code=HTTPStatus.BAD_REQUEST,
- )
if card_id:
card = await get_card(card_id)
if not card:
@@ -89,8 +83,20 @@ async def api_card_create_or_update(
raise HTTPException(
detail="Not your card.", status_code=HTTPStatus.FORBIDDEN
)
+ checkUid = await get_card_by_uid(data.uid)
+ if checkUid and checkUid.id != card_id:
+ raise HTTPException(
+ detail="UID already registered. Delete registered card and try again.",
+ status_code=HTTPStatus.BAD_REQUEST,
+ )
card = await update_card(card_id, **data.dict())
else:
+ checkUid = await get_card_by_uid(data.uid)
+ if checkUid:
+ raise HTTPException(
+ detail="UID already registered. Delete registered card and try again.",
+ status_code=HTTPStatus.BAD_REQUEST,
+ )
card = await create_card(wallet_id=wallet.wallet.id, data=data)
return card.dict()
From 81ed71a2e446857e16e6ad1a7138909c689fcbbe Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 12:04:08 -0600
Subject: [PATCH 68/73] update README to reflect latest changes
---
lnbits/extensions/boltcards/README.md | 28 ++++++++++++++-------------
1 file changed, 15 insertions(+), 13 deletions(-)
diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md
index 9646925e0..896fb54bc 100644
--- a/lnbits/extensions/boltcards/README.md
+++ b/lnbits/extensions/boltcards/README.md
@@ -4,7 +4,7 @@ This extension allows you to link your Bolt Card (or other compatible NXP NTAG d
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
-***In order to use this extension you need to be able to setup your own card.*** That means writing a URL template pointing to your LNBits instance, configuring some SUN (SDM) settings and optionally changing the card's keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [bolt-nfc-android-app](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes.
+***In order to use this extension you need to be able to setup your own card.*** That means writing a URL template pointing to your LNBits instance, configuring some SUN (SDM) settings and optionally changing the card's keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [bolt-nfc-android-app](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes. It's available from Google Play [here](https://play.google.com/store/apps/details?id=com.lightningnfcapp).
## About the keys
@@ -18,29 +18,32 @@ The key #00, K0 (also know as auth key) is skipped to be use as authentification
***Always backup all keys that you're trying to write on the card. Without them you may not be able to change them in the future!***
-## LNURLw
-Create a withdraw link within the LNURLw extension before adding a card. Enable the `Use unique withdraw QR codes to reduce 'assmilking'` option.
-
## Setting the card - bolt-nfc-android-app (easy way)
So far, regarding the keys, the app can only write a new key set on an empty card (with zero keys). **When you write non zero (and 'non debug') keys, they can't be rewrite with this app.** You have to do it on your computer.
- Read the card with the app. Note UID so you can fill it in the extension later.
- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{card_uid}`
- - `{card_uid}` is optional. This field denotes the 14-character (7 byte) UID unique to each NFC cardr from the factory.
- - If you include the `{card_uid}`, there is a slight potential privacy leak where each place you tap your card will see this `{card_uid}` in plain-text. As one example, merchants could potentially use this static identifier for the card to build a profile around you. If you are on a shared, large LNBits host this option is probably best for you as the system is more efficient this way.
- - If you don't include `{card_uid}`, then you will have gained a tiny bit more privacy by not exposing any static identifier to anyone. The downside to this is that the processing LNBits does after you tap your card is more computationally expensive. If you are on a dedicated, private LNBits instance this option is probably best for you as the efficiency gains of adding `{card_uid}` are irrelevant in this use case.
- Add new card in the extension.
- - Leaving any key array empty means that key is 16bytes of zero (00000000000000000000000000000000).
- - GENERATE KEY button fill the keys randomly. If there is "debug" in the card name, a debug set of keys is filled instead.
- - Leaving initial counter empty means zero.
-- Open the card details. **Backup the keys.** Scan the QR with the app to write the keys on the card.
+ - Set a max sats per transaction. Any transaction greater than this amount will be rejected.
+ - Set a max sats per day. After the card spends this amount of sats in a day, additional transactions will be rejected.
+ - Set a card name. This is just for your reference inside LNBits.
+ - Set the card UID. This is the unique identifier on your NFC card and is 7 bytes.
+ - If on an Android device with a newish version of Chrome, you can click the icon next to the input and tap your card to autofill this field.
+ - Advanced Options
+ - Card Keys (k0, k1, k2) will be automatically generated if not explicitly set.
+ - Set to 16 bytes of 0s (00000000000000000000000000000000) to leave the keys in debug mode.
+ - GENERATE KEY button fill the keys randomly. If there is "debug" in the card name, a debug set of keys is filled instead.
+ - Click CREATE CARD button
+- Click the QR code button next to a card to view its details. You can scan the QR code with the Android app to import the keys.
+- Click the "KEYS / AUTH LINK" button to copy the auth URL to the clipboard. You can then paste this into the Android app to import the keys.
+- Tap the NFC card to write the keys to the card.
## Setting the card - computer (hard way)
Follow the guide.
-The URI should be `lnurlw://YOUR-DOMAIN.COM/boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000`
+The URI should be `lnurlw://YOUR-DOMAIN.COM/boltcards/api/v1/scan/{YOUR_card_uid}?p=00000000000000000000000000000000&c=0000000000000000`
Then fill up the card parameters in the extension. Card Auth key (K0) can be omitted. Initical counter can be 0.
@@ -50,7 +53,6 @@ Then fill up the card parameters in the extension. Card Auth key (K0) can be omi
- New Data Set > Link
- Set URI type to Custom URL
- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{YOUR_card_uid}?p=00000000000000000000000000000000&c=0000000000000000
- - See note above about including `{card_uid}` in the URL.
- click Configure mirroring options
- Select Card Type NTAG 424 DNA
- Check Enable SDM Mirroring
From 4e63662f42fc87a77c61cdcdbe92ae664f586349 Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Mon, 29 Aug 2022 14:51:18 -0600
Subject: [PATCH 69/73] add external_id field for external use
---
lnbits/extensions/boltcards/README.md | 7 ++++---
lnbits/extensions/boltcards/crud.py | 18 +++++++++++++++++-
lnbits/extensions/boltcards/lnurl.py | 8 ++++----
lnbits/extensions/boltcards/migrations.py | 1 +
lnbits/extensions/boltcards/models.py | 1 +
lnbits/extensions/boltcards/static/js/index.js | 1 +
.../boltcards/templates/boltcards/index.html | 3 ++-
7 files changed, 30 insertions(+), 9 deletions(-)
diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md
index 896fb54bc..ca40f44fb 100644
--- a/lnbits/extensions/boltcards/README.md
+++ b/lnbits/extensions/boltcards/README.md
@@ -22,7 +22,8 @@ The key #00, K0 (also know as auth key) is skipped to be use as authentification
So far, regarding the keys, the app can only write a new key set on an empty card (with zero keys). **When you write non zero (and 'non debug') keys, they can't be rewrite with this app.** You have to do it on your computer.
- Read the card with the app. Note UID so you can fill it in the extension later.
-- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{card_uid}`
+- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{external_id}`
+ - `{external_id}` should be replaced with the External ID found in the LNBits dialog.
- Add new card in the extension.
- Set a max sats per transaction. Any transaction greater than this amount will be rejected.
@@ -43,7 +44,7 @@ So far, regarding the keys, the app can only write a new key set on an empty car
Follow the guide.
-The URI should be `lnurlw://YOUR-DOMAIN.COM/boltcards/api/v1/scan/{YOUR_card_uid}?p=00000000000000000000000000000000&c=0000000000000000`
+The URI should be `lnurlw://YOUR-DOMAIN.COM/boltcards/api/v1/scan/{YOUR_card_external_id}?p=00000000000000000000000000000000&c=0000000000000000`
Then fill up the card parameters in the extension. Card Auth key (K0) can be omitted. Initical counter can be 0.
@@ -52,7 +53,7 @@ Then fill up the card parameters in the extension. Card Auth key (K0) can be omi
- In the TagWriter app tap Write tags
- New Data Set > Link
- Set URI type to Custom URL
-- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{YOUR_card_uid}?p=00000000000000000000000000000000&c=0000000000000000
+- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{YOUR_card_external_id}?p=00000000000000000000000000000000&c=0000000000000000
- click Configure mirroring options
- Select Card Type NTAG 424 DNA
- Check Enable SDM Mirroring
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index be9b29618..63c04d73a 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -9,11 +9,14 @@ from .models import Card, CreateCardData, Hit, Refund
async def create_card(data: CreateCardData, wallet_id: str) -> Card:
card_id = urlsafe_short_hash().upper()
+ extenal_id = urlsafe_short_hash().lower()
+
await db.execute(
"""
INSERT INTO boltcards.cards (
id,
uid,
+ external_id,
wallet,
card_name,
counter,
@@ -25,11 +28,12 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
k2,
otp
)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
card_id,
data.uid.upper(),
+ extenal_id,
wallet_id,
data.card_name,
data.counter,
@@ -95,6 +99,18 @@ async def get_card_by_uid(card_uid: str) -> Optional[Card]:
return Card.parse_obj(card)
+async def get_card_by_external_id(external_id: str) -> Optional[Card]:
+ row = await db.fetchone(
+ "SELECT * FROM boltcards.cards WHERE external_id = ?", (external_id.lower(),)
+ )
+ if not row:
+ return None
+
+ card = dict(**row)
+
+ return Card.parse_obj(card)
+
+
async def get_card_by_otp(otp: str) -> Optional[Card]:
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE otp = ?", (otp,))
if not row:
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 8dba51bf0..64efdd2dc 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -26,8 +26,8 @@ from . import boltcards_ext
from .crud import (
create_hit,
get_card,
+ get_card_by_external_id,
get_card_by_otp,
- get_card_by_uid,
get_hit,
get_hits_today,
spend_hit,
@@ -41,14 +41,14 @@ from .nxp424 import decryptSUN, getSunMAC
###############LNURLWITHDRAW#################
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
-@boltcards_ext.get("/api/v1/scan/{card_uid}")
-async def api_scan(p, c, request: Request, card_uid: str = None):
+@boltcards_ext.get("/api/v1/scan/{external_id}")
+async def api_scan(p, c, request: Request, external_id: str = None):
# some wallets send everything as lower case, no bueno
p = p.upper()
c = c.upper()
card = None
counter = b""
- card = await get_card_by_uid(card_uid)
+ card = await get_card_by_external_id(external_id)
if not card:
return {"status": "ERROR", "reason": "No card."}
if not card.enable:
diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py
index 5be3d08fb..081260139 100644
--- a/lnbits/extensions/boltcards/migrations.py
+++ b/lnbits/extensions/boltcards/migrations.py
@@ -9,6 +9,7 @@ async def m001_initial(db):
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 TEXT NOT NULL,
daily_limit TEXT NOT NULL,
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 21096640a..47ca1df09 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -18,6 +18,7 @@ class Card(BaseModel):
wallet: str
card_name: str
uid: str
+ external_id: str
counter: int
tx_limit: int
daily_limit: int
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index c23a2583a..a2376de3f 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -259,6 +259,7 @@ new Vue({
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
name: card.card_name,
uid: card.uid,
+ external_id: card.external_id,
k0: card.k0,
k1: card.k1,
k2: card.k2
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 1338b859d..96d9c9d0a 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -376,6 +376,7 @@
Name: {{ qrCodeDialog.data.name }}
UID: {{ qrCodeDialog.data.uid }}
+ External ID: {{ qrCodeDialog.data.external_id }}
Lock key: {{ qrCodeDialog.data.k0 }}
Meta key: {{ qrCodeDialog.data.k1 }}
File key: {{ qrCodeDialog.data.k2 }}
@@ -386,7 +387,7 @@
unelevated
outline
color="grey"
- @click="copyText(lnurlLink + qrCodeDialog.data.uid)"
+ @click="copyText(lnurlLink + qrCodeDialog.data.external_id)"
label="Base url (LNURL://)"
>
From 7a7413bb6f8ac1dbabfc2e23c7c12e0a90b6247c Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Tue, 30 Aug 2022 07:35:08 -0600
Subject: [PATCH 70/73] add lnurlw_base to auth response, per developers of the
android app a future version will use this to pull the base url directly into
the app
---
lnbits/extensions/boltcards/lnurl.py | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 64efdd2dc..e422c4636 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -6,6 +6,7 @@ import secrets
from http import HTTPStatus
from io import BytesIO
from typing import Optional
+from urllib.parse import urlparse
from embit import bech32, compact
from fastapi import Request
@@ -142,11 +143,18 @@ async def api_auth(a, request: Request):
)
new_otp = secrets.token_hex(16)
- print(card.otp)
- print(new_otp)
await update_card_otp(new_otp, card.id)
- response = {"k0": card.k0, "k1": card.k1, "k2": card.k2}
+ lnurlw_base = (
+ f"{urlparse(str(request.url)).netloc}/boltcards/api/v1/scan/{card.external_id}"
+ )
+
+ response = {
+ "k0": card.k0,
+ "k1": card.k1,
+ "k2": card.k2,
+ "lnurlw_base": lnurlw_base,
+ }
return response
From ad60c1614e6cb47724d17697da55605b3807abbb Mon Sep 17 00:00:00 2001
From: Lee Salminen
Date: Tue, 30 Aug 2022 08:06:05 -0600
Subject: [PATCH 71/73] hide nfc button if not supported on device, per issue
#907
---
lnbits/extensions/boltcards/static/js/index.js | 1 +
lnbits/extensions/boltcards/templates/boltcards/index.html | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index a2376de3f..68dc319f1 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -16,6 +16,7 @@ new Vue({
return {
toggleAdvanced: false,
nfcTagReading: false,
+ nfcSupported: typeof NDEFReader != 'undefined',
lnurlLink: `${window.location.host}/boltcards/api/v1/scan/`,
cards: [],
hits: [],
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html
index 96d9c9d0a..26af61f28 100644
--- a/lnbits/extensions/boltcards/templates/boltcards/index.html
+++ b/lnbits/extensions/boltcards/templates/boltcards/index.html
@@ -251,7 +251,7 @@
>
-
+
-
+
Date: Tue, 30 Aug 2022 08:08:11 -0600
Subject: [PATCH 72/73] text change, fix copy pasta
---
lnbits/extensions/boltcards/static/js/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js
index 68dc319f1..84c2fca58 100644
--- a/lnbits/extensions/boltcards/static/js/index.js
+++ b/lnbits/extensions/boltcards/static/js/index.js
@@ -170,7 +170,7 @@ new Vue({
this.nfcTagReading = true
this.$q.notify({
- message: 'Tap your NFC tag to pay this invoice with LNURLw.'
+ message: 'Tap your NFC tag to copy its UID here.'
})
return ndef.scan({signal: readerAbortController.signal}).then(() => {
From b3093cafdbc20256abd884c2b495c34218cd313a Mon Sep 17 00:00:00 2001
From: ben
Date: Tue, 30 Aug 2022 23:57:28 +0100
Subject: [PATCH 73/73] Added video tutorial
---
lnbits/extensions/boltcards/README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md
index ca40f44fb..f9c594093 100644
--- a/lnbits/extensions/boltcards/README.md
+++ b/lnbits/extensions/boltcards/README.md
@@ -2,6 +2,8 @@
This extension allows you to link your Bolt Card (or other compatible NXP NTAG device) with a LNbits instance and use it in a more secure way than a static LNURLw. A technology called [Secure Unique NFC](https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow.
+Tutorial
+
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
***In order to use this extension you need to be able to setup your own card.*** That means writing a URL template pointing to your LNBits instance, configuring some SUN (SDM) settings and optionally changing the card's keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [bolt-nfc-android-app](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes. It's available from Google Play [here](https://play.google.com/store/apps/details?id=com.lightningnfcapp).