feat: update to lnbits 1.0.0 (#41)
--------- Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
This commit is contained in:
parent
239bfb65ab
commit
7f8b269ecd
12 changed files with 1513 additions and 1457 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Bolt Cards",
|
"name": "Bolt Cards",
|
||||||
"short_description": "Self custody Bolt Cards with one time LNURLw",
|
"short_description": "Self custody Bolt Cards with one time LNURLw",
|
||||||
"tile": "/boltcards/static/image/boltcard.png",
|
"tile": "/boltcards/static/image/boltcard.png",
|
||||||
"min_lnbits_version": "0.12.5",
|
"min_lnbits_version": "1.0.0",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
"name": "dni",
|
"name": "dni",
|
||||||
|
|
|
||||||
230
crud.py
230
crud.py
|
|
@ -1,6 +1,6 @@
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
@ -31,175 +31,157 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
|
||||||
k2,
|
k2,
|
||||||
otp
|
otp
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (
|
||||||
|
:id, :uid, :external_id, :wallet, :card_name, :counter,
|
||||||
|
:tx_limit, :daily_limit, :enable, :k0, :k1, :k2, :otp
|
||||||
|
)
|
||||||
""",
|
""",
|
||||||
(
|
{
|
||||||
card_id,
|
"id": card_id,
|
||||||
data.uid.upper(),
|
"uid": data.uid.upper(),
|
||||||
extenal_id,
|
"external_id": extenal_id,
|
||||||
wallet_id,
|
"wallet": wallet_id,
|
||||||
data.card_name,
|
"card_name": data.card_name,
|
||||||
data.counter,
|
"counter": data.counter,
|
||||||
data.tx_limit,
|
"tx_limit": data.tx_limit,
|
||||||
data.daily_limit,
|
"daily_limit": data.daily_limit,
|
||||||
True,
|
"enable": True,
|
||||||
data.k0,
|
"k0": data.k0,
|
||||||
data.k1,
|
"k1": data.k1,
|
||||||
data.k2,
|
"k2": data.k2,
|
||||||
secrets.token_hex(16),
|
"otp": secrets.token_hex(16),
|
||||||
),
|
},
|
||||||
)
|
)
|
||||||
card = await get_card(card_id)
|
card = await get_card(card_id)
|
||||||
assert card, "Newly created card couldn't be retrieved"
|
assert card, "Newly created card couldn't be retrieved"
|
||||||
return card
|
return card
|
||||||
|
|
||||||
|
|
||||||
async def update_card(card_id: str, **kwargs) -> Optional[Card]:
|
async def update_card(card_id: str, data: CreateCardData) -> Card:
|
||||||
if "is_unique" in kwargs:
|
card = Card(
|
||||||
kwargs["is_unique"] = int(kwargs["is_unique"])
|
id=card_id,
|
||||||
if "uid" in kwargs:
|
**data.dict(),
|
||||||
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 = ?",
|
|
||||||
(*kwargs.values(), card_id),
|
|
||||||
)
|
)
|
||||||
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE id = ?", (card_id,))
|
await db.update("boltcards.cards", card)
|
||||||
return Card(**row) if row else None
|
return card
|
||||||
|
|
||||||
|
|
||||||
async def get_cards(wallet_ids: List[str]) -> List[Card]:
|
async def get_cards(wallet_ids: list[str]) -> list[Card]:
|
||||||
if len(wallet_ids) == 0:
|
if len(wallet_ids) == 0:
|
||||||
return []
|
return []
|
||||||
|
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
|
||||||
q = ",".join(["?"] * len(wallet_ids))
|
return await db.fetchall(
|
||||||
rows = await db.fetchall(
|
f"SELECT * FROM boltcards.cards WHERE wallet IN ({q})",
|
||||||
f"SELECT * FROM boltcards.cards WHERE wallet IN ({q})", (*wallet_ids,)
|
model=Card,
|
||||||
)
|
)
|
||||||
|
|
||||||
return [Card(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_card(card_id: str) -> Optional[Card]:
|
async def get_card(card_id: str) -> Optional[Card]:
|
||||||
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE id = ?", (card_id,))
|
return await db.fetchone(
|
||||||
if not row:
|
"SELECT * FROM boltcards.cards WHERE id = :id",
|
||||||
return None
|
{"id": card_id},
|
||||||
|
Card,
|
||||||
card = dict(**row)
|
)
|
||||||
|
|
||||||
return Card.parse_obj(card)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_card_by_uid(card_uid: str) -> Optional[Card]:
|
async def get_card_by_uid(card_uid: str) -> Optional[Card]:
|
||||||
row = await db.fetchone(
|
return await db.fetchone(
|
||||||
"SELECT * FROM boltcards.cards WHERE uid = ?", (card_uid.upper(),)
|
"SELECT * FROM boltcards.cards WHERE uid = :uid",
|
||||||
|
{"uid": card_uid.upper()},
|
||||||
|
Card,
|
||||||
)
|
)
|
||||||
if not row:
|
|
||||||
return None
|
|
||||||
|
|
||||||
card = dict(**row)
|
|
||||||
|
|
||||||
return Card.parse_obj(card)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_card_by_external_id(external_id: str) -> Optional[Card]:
|
async def get_card_by_external_id(external_id: str) -> Optional[Card]:
|
||||||
row = await db.fetchone(
|
return await db.fetchone(
|
||||||
"SELECT * FROM boltcards.cards WHERE external_id = ?", (external_id.lower(),)
|
"SELECT * FROM boltcards.cards WHERE external_id = :ext_id",
|
||||||
|
{"ext_id": external_id.lower()},
|
||||||
|
Card,
|
||||||
)
|
)
|
||||||
if not row:
|
|
||||||
return None
|
|
||||||
|
|
||||||
card = dict(**row)
|
|
||||||
|
|
||||||
return Card.parse_obj(card)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_card_by_otp(otp: str) -> Optional[Card]:
|
async def get_card_by_otp(otp: str) -> Optional[Card]:
|
||||||
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE otp = ?", (otp,))
|
return await db.fetchone(
|
||||||
if not row:
|
"SELECT * FROM boltcards.cards WHERE otp = :otp",
|
||||||
return None
|
{"otp": otp},
|
||||||
|
Card,
|
||||||
card = dict(**row)
|
)
|
||||||
|
|
||||||
return Card.parse_obj(card)
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_card(card_id: str) -> None:
|
async def delete_card(card_id: str) -> None:
|
||||||
# Delete cards
|
# Delete cards
|
||||||
await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
|
await db.execute("DELETE FROM boltcards.cards WHERE id = :id", {"id": card_id})
|
||||||
# Delete hits
|
# Delete hits
|
||||||
hits = await get_hits([card_id])
|
hits = await get_hits([card_id])
|
||||||
for hit in hits:
|
for hit in hits:
|
||||||
await db.execute("DELETE FROM boltcards.hits WHERE id = ?", (hit.id,))
|
await db.execute("DELETE FROM boltcards.hits WHERE id = :id", {"id": hit.id})
|
||||||
# Delete refunds
|
# Delete refunds
|
||||||
refunds = await get_refunds([hit.id])
|
refunds = await get_refunds([hit.id])
|
||||||
for refund in refunds:
|
for refund in refunds:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"DELETE FROM boltcards.refunds WHERE id = ?", (refund.hit_id,)
|
"DELETE FROM boltcards.refunds WHERE id = :id", {"id": refund.id}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def update_card_counter(counter: int, card_id: str):
|
async def update_card_counter(counter: int, card_id: str):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"UPDATE boltcards.cards SET counter = ? WHERE id = ?",
|
"UPDATE boltcards.cards SET counter = :counter WHERE id = :id",
|
||||||
(counter, card_id),
|
{"counter": counter, "id": card_id},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def enable_disable_card(enable: bool, card_id: str) -> Optional[Card]:
|
async def enable_disable_card(enable: bool, card_id: str) -> Optional[Card]:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"UPDATE boltcards.cards SET enable = ? WHERE id = ?",
|
"UPDATE boltcards.cards SET enable = :enable WHERE id = :id",
|
||||||
(enable, card_id),
|
{"enable": enable, "id": card_id},
|
||||||
)
|
)
|
||||||
return await get_card(card_id)
|
return await get_card(card_id)
|
||||||
|
|
||||||
|
|
||||||
async def update_card_otp(otp: str, card_id: str):
|
async def update_card_otp(otp: str, card_id: str):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"UPDATE boltcards.cards SET otp = ? WHERE id = ?",
|
"UPDATE boltcards.cards SET otp = :otp WHERE id = :id",
|
||||||
(otp, card_id),
|
{"otp": otp, "id": card_id},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_hit(hit_id: str) -> Optional[Hit]:
|
async def get_hit(hit_id: str) -> Optional[Hit]:
|
||||||
row = await db.fetchone("SELECT * FROM boltcards.hits WHERE id = ?", (hit_id,))
|
return await db.fetchone(
|
||||||
if not row:
|
"SELECT * FROM boltcards.hits WHERE id = :id",
|
||||||
return None
|
{"id": hit_id},
|
||||||
|
Hit,
|
||||||
hit = dict(**row)
|
)
|
||||||
|
|
||||||
return Hit.parse_obj(hit)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_hits(cards_ids: List[str]) -> List[Hit]:
|
async def get_hits(cards_ids: list[str]) -> list[Hit]:
|
||||||
if len(cards_ids) == 0:
|
if len(cards_ids) == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
q = ",".join(["?"] * len(cards_ids))
|
q = ",".join([f"'{card_id}'" for card_id in cards_ids])
|
||||||
rows = await db.fetchall(
|
return await db.fetchall(
|
||||||
f"SELECT * FROM boltcards.hits WHERE card_id IN ({q})", (*cards_ids,)
|
f"SELECT * FROM boltcards.hits WHERE card_id IN ({q})",
|
||||||
|
model=Hit,
|
||||||
)
|
)
|
||||||
|
|
||||||
return [Hit(**row) for row in rows]
|
|
||||||
|
|
||||||
|
async def get_hits_today(card_id: str) -> list[Hit]:
|
||||||
async def get_hits_today(card_id: str) -> List[Hit]:
|
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"SELECT * FROM boltcards.hits WHERE card_id = ?",
|
"SELECT * FROM boltcards.hits WHERE card_id = :id",
|
||||||
(card_id,),
|
{"id": card_id},
|
||||||
|
Hit,
|
||||||
)
|
)
|
||||||
updatedrow = []
|
updatedrow = []
|
||||||
for row in rows:
|
for hit in rows:
|
||||||
if datetime.now().date() == datetime.fromtimestamp(row.time).date():
|
if datetime.now().date() == hit.time.date():
|
||||||
updatedrow.append(row)
|
updatedrow.append(hit)
|
||||||
|
|
||||||
return [Hit(**row) for row in updatedrow]
|
return updatedrow
|
||||||
|
|
||||||
|
|
||||||
async def spend_hit(card_id: str, amount: int):
|
async def spend_hit(card_id: str, amount: int):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"UPDATE boltcards.hits SET spent = ?, amount = ? WHERE id = ?",
|
"UPDATE boltcards.hits SET spent = :spent, amount = :amount WHERE id = :id",
|
||||||
(True, amount, card_id),
|
{"spent": True, "amount": amount, "id": card_id},
|
||||||
)
|
)
|
||||||
return await get_hit(card_id)
|
return await get_hit(card_id)
|
||||||
|
|
||||||
|
|
@ -218,18 +200,18 @@ async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
|
||||||
new_ctr,
|
new_ctr,
|
||||||
amount
|
amount
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (:id, :card_id, :ip, :spent, :useragent, :old_ctr, :new_ctr, :amount)
|
||||||
""",
|
""",
|
||||||
(
|
{
|
||||||
hit_id,
|
"id": hit_id,
|
||||||
card_id,
|
"card_id": card_id,
|
||||||
ip,
|
"ip": ip,
|
||||||
False,
|
"spent": False,
|
||||||
useragent,
|
"useragent": useragent,
|
||||||
old_ctr,
|
"old_ctr": old_ctr,
|
||||||
new_ctr,
|
"new_ctr": new_ctr,
|
||||||
0,
|
"amount": 0,
|
||||||
),
|
},
|
||||||
)
|
)
|
||||||
hit = await get_hit(hit_id)
|
hit = await get_hit(hit_id)
|
||||||
assert hit, "Newly recorded hit couldn't be retrieved"
|
assert hit, "Newly recorded hit couldn't be retrieved"
|
||||||
|
|
@ -245,13 +227,13 @@ async def create_refund(hit_id, refund_amount) -> Refund:
|
||||||
hit_id,
|
hit_id,
|
||||||
refund_amount
|
refund_amount
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?)
|
VALUES (:id, :hit_id, :refund_amount)
|
||||||
""",
|
""",
|
||||||
(
|
{
|
||||||
refund_id,
|
"id": refund_id,
|
||||||
hit_id,
|
"hit_id": hit_id,
|
||||||
refund_amount,
|
"refund_amount": refund_amount,
|
||||||
),
|
},
|
||||||
)
|
)
|
||||||
refund = await get_refund(refund_id)
|
refund = await get_refund(refund_id)
|
||||||
assert refund, "Newly recorded hit couldn't be retrieved"
|
assert refund, "Newly recorded hit couldn't be retrieved"
|
||||||
|
|
@ -259,22 +241,18 @@ async def create_refund(hit_id, refund_amount) -> Refund:
|
||||||
|
|
||||||
|
|
||||||
async def get_refund(refund_id: str) -> Optional[Refund]:
|
async def get_refund(refund_id: str) -> Optional[Refund]:
|
||||||
row = await db.fetchone(
|
return await db.fetchone(
|
||||||
"SELECT * FROM boltcards.refunds WHERE id = ?", (refund_id,)
|
"SELECT * FROM boltcards.refunds WHERE id = :id",
|
||||||
|
{"id": refund_id},
|
||||||
|
Refund,
|
||||||
)
|
)
|
||||||
if not row:
|
|
||||||
return None
|
|
||||||
refund = dict(**row)
|
|
||||||
return Refund.parse_obj(refund)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_refunds(hits_ids: List[str]) -> List[Refund]:
|
async def get_refunds(hits_ids: list[str]) -> list[Refund]:
|
||||||
if len(hits_ids) == 0:
|
if len(hits_ids) == 0:
|
||||||
return []
|
return []
|
||||||
|
q = ",".join([f"'{hit_id}'" for hit_id in hits_ids])
|
||||||
q = ",".join(["?"] * len(hits_ids))
|
return await db.fetchall(
|
||||||
rows = await db.fetchall(
|
f"SELECT * FROM boltcards.refunds WHERE hit_id IN ({q})",
|
||||||
f"SELECT * FROM boltcards.refunds WHERE hit_id IN ({q})", (*hits_ids,)
|
model=Refund,
|
||||||
)
|
)
|
||||||
|
|
||||||
return [Refund(**row) for row in rows]
|
|
||||||
|
|
|
||||||
26
models.py
26
models.py
|
|
@ -1,5 +1,5 @@
|
||||||
import json
|
import json
|
||||||
from sqlite3 import Row
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import Query, Request
|
from fastapi import Query, Request
|
||||||
from lnurl import Lnurl
|
from lnurl import Lnurl
|
||||||
|
|
@ -17,8 +17,10 @@ class Card(BaseModel):
|
||||||
uid: str
|
uid: str
|
||||||
external_id: str
|
external_id: str
|
||||||
counter: int
|
counter: int
|
||||||
tx_limit: int
|
# TODO: database column is TEXT should be INT
|
||||||
daily_limit: int
|
tx_limit: str
|
||||||
|
# TODO: database column is TEXT should be INT
|
||||||
|
daily_limit: str
|
||||||
enable: bool
|
enable: bool
|
||||||
k0: str
|
k0: str
|
||||||
k1: str
|
k1: str
|
||||||
|
|
@ -27,11 +29,7 @@ class Card(BaseModel):
|
||||||
prev_k1: str
|
prev_k1: str
|
||||||
prev_k2: str
|
prev_k2: str
|
||||||
otp: str
|
otp: str
|
||||||
time: int
|
time: datetime
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row) -> "Card":
|
|
||||||
return cls(**dict(row))
|
|
||||||
|
|
||||||
def lnurl(self, req: Request) -> Lnurl:
|
def lnurl(self, req: Request) -> Lnurl:
|
||||||
url = str(
|
url = str(
|
||||||
|
|
@ -67,19 +65,11 @@ class Hit(BaseModel):
|
||||||
old_ctr: int
|
old_ctr: int
|
||||||
new_ctr: int
|
new_ctr: int
|
||||||
amount: int
|
amount: int
|
||||||
time: int
|
time: datetime
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row) -> "Hit":
|
|
||||||
return cls(**dict(row))
|
|
||||||
|
|
||||||
|
|
||||||
class Refund(BaseModel):
|
class Refund(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
hit_id: str
|
hit_id: str
|
||||||
refund_amount: int
|
refund_amount: int
|
||||||
time: int
|
time: datetime
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row) -> "Refund":
|
|
||||||
return cls(**dict(row))
|
|
||||||
|
|
|
||||||
2236
poetry.lock
generated
2236
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,7 @@ authors = ["Alan Bits <alan@lnbits.com>"]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10 | ^3.9"
|
python = "^3.10 | ^3.9"
|
||||||
lnbits = "*"
|
lnbits = {version = "*", allow-prereleases = true}
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^24.3.0"
|
black = "^24.3.0"
|
||||||
|
|
@ -14,7 +14,7 @@ pytest-asyncio = "^0.21.0"
|
||||||
pytest = "^7.3.2"
|
pytest = "^7.3.2"
|
||||||
mypy = "^1.5.1"
|
mypy = "^1.5.1"
|
||||||
pre-commit = "^3.2.2"
|
pre-commit = "^3.2.2"
|
||||||
ruff = "^0.3.2"
|
ruff = "^0.6.3"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,15 @@
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
const mapCards = obj => {
|
const mapCards = obj => {
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
obj.date = Quasar.date.formatDate(new Date(obj.time), 'YYYY-MM-DD HH:mm')
|
||||||
new Date(obj.time * 1000),
|
|
||||||
'YYYY-MM-DD HH:mm'
|
|
||||||
)
|
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
new Vue({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
toggleAdvanced: false,
|
toggleAdvanced: false,
|
||||||
nfcTagReading: false,
|
disableNfcButton: true,
|
||||||
lnurlLink: `${window.location.host}/boltcards/api/v1/scan/`,
|
lnurlLink: `${window.location.host}/boltcards/api/v1/scan/`,
|
||||||
cards: [],
|
cards: [],
|
||||||
hits: [],
|
hits: [],
|
||||||
|
|
@ -155,107 +149,77 @@ new Vue({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
readNfcTag: function () {
|
readNfcTag() {
|
||||||
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 ndef = new NDEFReader()
|
||||||
|
|
||||||
const readerAbortController = new AbortController()
|
const readerAbortController = new AbortController()
|
||||||
readerAbortController.signal.onabort = event => {
|
readerAbortController.signal.onabort = event => {
|
||||||
console.log('All NFC Read operations have been aborted.')
|
console.log('All NFC Read operations have been aborted.')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.nfcTagReading = true
|
Quasar.Notify.create({
|
||||||
this.$q.notify({
|
|
||||||
message: 'Tap your NFC tag to copy its UID here.'
|
message: 'Tap your NFC tag to copy its UID here.'
|
||||||
})
|
})
|
||||||
|
|
||||||
return ndef.scan({signal: readerAbortController.signal}).then(() => {
|
return ndef.scan({signal: readerAbortController.signal}).then(() => {
|
||||||
ndef.onreadingerror = () => {
|
ndef.onreadingerror = () => {
|
||||||
self.nfcTagReading = false
|
this.disableNfcButton = false
|
||||||
|
Quasar.Notify.create({
|
||||||
this.$q.notify({
|
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: 'There was an error reading this NFC tag.'
|
message: 'There was an error reading this NFC tag.'
|
||||||
})
|
})
|
||||||
|
|
||||||
readerAbortController.abort()
|
readerAbortController.abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
ndef.onreading = ({message, serialNumber}) => {
|
ndef.onreading = ({message, serialNumber}) => {
|
||||||
|
const uid = serialNumber.toUpperCase().replaceAll(':', '')
|
||||||
//Decode NDEF data from tag
|
//Decode NDEF data from tag
|
||||||
var self = this
|
this.cardDialog.data.uid = uid
|
||||||
self.cardDialog.data.uid = serialNumber
|
Quasar.Notify.create({
|
||||||
.toUpperCase()
|
|
||||||
.replaceAll(':', '')
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'NFC tag read successfully.'
|
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 () {
|
getCards() {
|
||||||
var self = this
|
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/boltcards/api/v1/cards?all_wallets=true',
|
'/boltcards/api/v1/cards?all_wallets=true',
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
self.cards = response.data.map(function (obj) {
|
this.cards = response.data.map(function (obj) {
|
||||||
return mapCards(obj)
|
return mapCards(obj)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(() => {
|
||||||
self.getHits()
|
this.getHits()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getHits: function () {
|
getHits() {
|
||||||
var self = this
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/boltcards/api/v1/hits?all_wallets=true',
|
'/boltcards/api/v1/hits?all_wallets=true',
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
self.hits = response.data.map(function (obj) {
|
this.hits = response.data.map(obj => {
|
||||||
obj.card_name = self.cards.find(d => d.id == obj.card_id).card_name
|
obj.card_name = this.cards.find(d => d.id == obj.card_id).card_name
|
||||||
return mapCards(obj)
|
return mapCards(obj)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getRefunds: function () {
|
getRefunds() {
|
||||||
var self = this
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/boltcards/api/v1/refunds?all_wallets=true',
|
'/boltcards/api/v1/refunds?all_wallets=true',
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
self.refunds = response.data.map(function (obj) {
|
this.refunds = response.data.map(obj => {
|
||||||
return mapCards(obj)
|
return mapCards(obj)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -287,12 +251,11 @@ new Vue({
|
||||||
this.qrCodeDialog.wipe = wipe
|
this.qrCodeDialog.wipe = wipe
|
||||||
this.qrCodeDialog.show = true
|
this.qrCodeDialog.show = true
|
||||||
},
|
},
|
||||||
addCardOpen: function () {
|
addCardOpen() {
|
||||||
this.cardDialog.show = true
|
this.cardDialog.show = true
|
||||||
this.generateKeys()
|
this.generateKeys()
|
||||||
},
|
},
|
||||||
generateKeys: function () {
|
generateKeys() {
|
||||||
var self = this
|
|
||||||
const genRandomHexBytes = size =>
|
const genRandomHexBytes = size =>
|
||||||
crypto
|
crypto
|
||||||
.getRandomValues(new Uint8Array(size))
|
.getRandomValues(new Uint8Array(size))
|
||||||
|
|
@ -302,22 +265,22 @@ new Vue({
|
||||||
typeof this.cardDialog.data.card_name === 'string' &&
|
typeof this.cardDialog.data.card_name === 'string' &&
|
||||||
this.cardDialog.data.card_name.search('debug') > -1
|
this.cardDialog.data.card_name.search('debug') > -1
|
||||||
|
|
||||||
self.cardDialog.data.k0 = debugcard
|
this.cardDialog.data.k0 = debugcard
|
||||||
? '11111111111111111111111111111111'
|
? '11111111111111111111111111111111'
|
||||||
: genRandomHexBytes(16)
|
: genRandomHexBytes(16)
|
||||||
|
|
||||||
self.cardDialog.data.k1 = debugcard
|
this.cardDialog.data.k1 = debugcard
|
||||||
? '22222222222222222222222222222222'
|
? '22222222222222222222222222222222'
|
||||||
: genRandomHexBytes(16)
|
: genRandomHexBytes(16)
|
||||||
|
|
||||||
self.cardDialog.data.k2 = debugcard
|
this.cardDialog.data.k2 = debugcard
|
||||||
? '33333333333333333333333333333333'
|
? '33333333333333333333333333333333'
|
||||||
: genRandomHexBytes(16)
|
: genRandomHexBytes(16)
|
||||||
},
|
},
|
||||||
closeFormDialog: function () {
|
closeFormDialog() {
|
||||||
this.cardDialog.data = {}
|
this.cardDialog.data = {}
|
||||||
},
|
},
|
||||||
sendFormData: function () {
|
sendFormData() {
|
||||||
let wallet = _.findWhere(this.g.user.wallets, {
|
let wallet = _.findWhere(this.g.user.wallets, {
|
||||||
id: this.cardDialog.data.wallet
|
id: this.cardDialog.data.wallet
|
||||||
})
|
})
|
||||||
|
|
@ -328,21 +291,17 @@ new Vue({
|
||||||
this.createCard(wallet, data)
|
this.createCard(wallet, data)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createCard: function (wallet, data) {
|
createCard(wallet, data) {
|
||||||
var self = this
|
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('POST', '/boltcards/api/v1/cards', wallet.adminkey, data)
|
.request('POST', '/boltcards/api/v1/cards', wallet.adminkey, data)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
self.cards.push(mapCards(response.data))
|
this.cards.push(mapCards(response.data))
|
||||||
self.cardDialog.show = false
|
this.cardDialog.show = false
|
||||||
self.cardDialog.data = {}
|
this.cardDialog.data = {}
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
updateCardDialog: function (formId) {
|
updateCardDialog(formId) {
|
||||||
var card = _.findWhere(this.cards, {id: formId})
|
var card = _.findWhere(this.cards, {id: formId})
|
||||||
this.cardDialog.data = _.clone(card)
|
this.cardDialog.data = _.clone(card)
|
||||||
|
|
||||||
|
|
@ -352,9 +311,7 @@ new Vue({
|
||||||
|
|
||||||
this.cardDialog.show = true
|
this.cardDialog.show = true
|
||||||
},
|
},
|
||||||
updateCard: function (wallet, data) {
|
updateCard(wallet, data) {
|
||||||
var self = this
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.cardDialog.temp.k0 != data.k0 ||
|
this.cardDialog.temp.k0 != data.k0 ||
|
||||||
this.cardDialog.temp.k1 != data.k1 ||
|
this.cardDialog.temp.k1 != data.k1 ||
|
||||||
|
|
@ -372,21 +329,20 @@ new Vue({
|
||||||
wallet.adminkey,
|
wallet.adminkey,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
self.cards = _.reject(self.cards, function (obj) {
|
this.cards = _.reject(this.cards, function (obj) {
|
||||||
return obj.id == data.id
|
return obj.id == data.id
|
||||||
})
|
})
|
||||||
self.cards.push(mapCards(response.data))
|
this.cards.push(mapCards(response.data))
|
||||||
self.cardDialog.show = false
|
this.cardDialog.show = false
|
||||||
self.cardDialog.data = {}
|
this.cardDialog.data = {}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
enableCard: function (wallet, card_id, enable) {
|
enableCard(wallet, card_id, enable) {
|
||||||
var self = this
|
let fullWallet = _.findWhere(this.g.user.wallets, {
|
||||||
let fullWallet = _.findWhere(self.g.user.wallets, {
|
|
||||||
id: wallet
|
id: wallet
|
||||||
})
|
})
|
||||||
LNbits.api
|
LNbits.api
|
||||||
|
|
@ -395,19 +351,18 @@ new Vue({
|
||||||
'/boltcards/api/v1/cards/enable/' + card_id + '/' + enable,
|
'/boltcards/api/v1/cards/enable/' + card_id + '/' + enable,
|
||||||
fullWallet.adminkey
|
fullWallet.adminkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
console.log(response.data)
|
console.log(response.data)
|
||||||
self.cards = _.reject(self.cards, function (obj) {
|
this.cards = _.reject(this.cards, function (obj) {
|
||||||
return obj.id == response.data.id
|
return obj.id == response.data.id
|
||||||
})
|
})
|
||||||
self.cards.push(mapCards(response.data))
|
this.cards.push(mapCards(response.data))
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteCard: function (cardId) {
|
deleteCard(cardId) {
|
||||||
let self = this
|
|
||||||
let cards = _.findWhere(this.cards, {id: cardId})
|
let cards = _.findWhere(this.cards, {id: cardId})
|
||||||
|
|
||||||
Quasar.utils.exportFile(
|
Quasar.utils.exportFile(
|
||||||
|
|
@ -420,15 +375,15 @@ new Vue({
|
||||||
.confirmDialog(
|
.confirmDialog(
|
||||||
"Are you sure you want to delete this card? Without access to the card keys you won't be able to reset them in the future!"
|
"Are you sure you want to delete this card? Without access to the card keys you won't be able to reset them in the future!"
|
||||||
)
|
)
|
||||||
.onOk(function () {
|
.onOk(() => {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
'/boltcards/api/v1/cards/' + cardId,
|
'/boltcards/api/v1/cards/' + cardId,
|
||||||
_.findWhere(self.g.user.wallets, {id: cards.wallet}).adminkey
|
_.findWhere(this.g.user.wallets, {id: cards.wallet}).adminkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
self.cards = _.reject(self.cards, function (obj) {
|
this.cards = _.reject(this.cards, function (obj) {
|
||||||
return obj.id == cardId
|
return obj.id == cardId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -437,20 +392,39 @@ new Vue({
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
exportCardsCSV: function () {
|
exportCardsCSV() {
|
||||||
LNbits.utils.exportCSV(this.cardsTable.columns, this.cards)
|
LNbits.utils.exportCSV(this.cardsTable.columns, this.cards)
|
||||||
},
|
},
|
||||||
exportHitsCSV: function () {
|
exportHitsCSV() {
|
||||||
LNbits.utils.exportCSV(this.hitsTable.columns, this.hits)
|
LNbits.utils.exportCSV(this.hitsTable.columns, this.hits)
|
||||||
},
|
},
|
||||||
exportRefundsCSV: function () {
|
exportRefundsCSV() {
|
||||||
LNbits.utils.exportCSV(this.refundsTable.columns, this.refunds)
|
LNbits.utils.exportCSV(this.refundsTable.columns, this.refunds)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created() {
|
||||||
if (this.g.user.wallets.length) {
|
if (this.g.user.wallets.length) {
|
||||||
this.getCards()
|
this.getCards()
|
||||||
this.getRefunds()
|
this.getRefunds()
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
if (typeof NDEFReader == 'undefined') {
|
||||||
|
throw {
|
||||||
|
toString() {
|
||||||
|
return 'NFC not supported on this device or browser.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.disableNfcButton = false
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'NFC is supported on this device. You can now read NFC tags.'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: error ? error.toString() : 'An unexpected error has occurred.'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
9
tasks.py
9
tasks.py
|
|
@ -1,8 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from lnbits.core.crud import update_payment_extra
|
from lnbits.core.crud import update_payment
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.helpers import get_current_extension_name
|
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from .crud import create_refund, get_hit
|
from .crud import create_refund, get_hit
|
||||||
|
|
@ -10,7 +9,7 @@ from .crud import create_refund, get_hit
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
async def wait_for_paid_invoices():
|
||||||
invoice_queue = asyncio.Queue()
|
invoice_queue = asyncio.Queue()
|
||||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
register_invoice_listener(invoice_queue, "ext_boltcards")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
payment = await invoice_queue.get()
|
payment = await invoice_queue.get()
|
||||||
|
|
@ -19,7 +18,7 @@ async def wait_for_paid_invoices():
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
|
|
||||||
if not payment.extra.get("refund"):
|
if not payment.extra or not payment.extra.get("refund"):
|
||||||
return
|
return
|
||||||
|
|
||||||
if payment.extra.get("wh_status"):
|
if payment.extra.get("wh_status"):
|
||||||
|
|
@ -31,4 +30,4 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
if hit:
|
if hit:
|
||||||
await create_refund(hit_id=hit.id, refund_amount=(payment.amount / 1000))
|
await create_refund(hit_id=hit.id, refund_amount=(payment.amount / 1000))
|
||||||
payment.extra["wh_status"] = 1
|
payment.extra["wh_status"] = 1
|
||||||
await update_payment_extra(payment.payment_hash, payment.extra)
|
await update_payment(payment)
|
||||||
|
|
|
||||||
|
|
@ -74,20 +74,19 @@ card.card_name }{% endblock %} {% block page %}
|
||||||
{% endblock %}{% block scripts %}
|
{% endblock %}{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
const mapHits = obj => {
|
const mapHits = obj => {
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
obj.date = Quasar.date.formatDate(
|
||||||
new Date(obj.time * 1000),
|
new Date(obj.time),
|
||||||
'YYYY-MM-DD HH:mm'
|
'YYYY-MM-DD HH:mm'
|
||||||
)
|
)
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
new Vue({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
delimiters: ['${', '}'],
|
delimiters: ['${', '}'],
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
card: null,
|
card: null,
|
||||||
hits: null,
|
hits: null,
|
||||||
|
|
@ -120,11 +119,11 @@ card.card_name }{% endblock %} {% block page %}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.card = JSON.parse('{{ card | tojson}}')
|
this.card = JSON.parse({{ card | tojson }})
|
||||||
let hits = JSON.parse('{{ hits | tojson}}')
|
const hits = {{ hits | tojson | safe }}
|
||||||
let refunds = JSON.parse('{{ refunds | tojson}}')
|
this.hits = hits.map(JSON.parse).map(mapHits)
|
||||||
|
const refunds = {{ refunds | tojson | safe }}
|
||||||
this.refunds = refunds || []
|
this.refunds = refunds || []
|
||||||
this.hits = hits.map(mapHits)
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
enabled() {
|
enabled() {
|
||||||
|
|
@ -133,8 +132,8 @@ card.card_name }{% endblock %} {% block page %}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
copyText(text, message, position) {
|
copyText(text, message, position) {
|
||||||
Quasar.utils.copyToClipboard(text).then(() => {
|
Quasar.copyToClipboard(text).then(() => {
|
||||||
this.$q.notify({
|
Quasar.Notify.create({
|
||||||
message: message || 'Copied to clipboard!',
|
message: message || 'Copied to clipboard!',
|
||||||
position: position || 'bottom'
|
position: position || 'bottom'
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -34,19 +34,21 @@
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
:data="cards"
|
:rows="cards"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:columns="cardsTable.columns"
|
:columns="cardsTable.columns"
|
||||||
:pagination.sync="cardsTable.pagination"
|
v-model:pagination="cardsTable.pagination"
|
||||||
>
|
>
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
<q-th
|
||||||
{{ col.label }}
|
v-for="col in props.cols"
|
||||||
</q-th>
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
v-text="col.label"
|
||||||
|
></q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
|
|
@ -78,9 +80,12 @@
|
||||||
<q-tooltip>Card Stats</q-tooltip>
|
<q-tooltip>Card Stats</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td
|
||||||
{{ col.value }}
|
v-for="col in props.cols"
|
||||||
</q-td>
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
v-text="col.value"
|
||||||
|
></q-td>
|
||||||
<q-td auto-width>
|
<q-td auto-width>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="props.row.enable"
|
v-if="props.row.enable"
|
||||||
|
|
@ -125,7 +130,6 @@
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
</q-table>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -144,27 +148,31 @@
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
:data="hits"
|
:rows="hits"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:columns="hitsTable.columns"
|
:columns="hitsTable.columns"
|
||||||
:pagination.sync="hitsTable.pagination"
|
v-model:pagination="hitsTable.pagination"
|
||||||
>
|
>
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
<q-th
|
||||||
{{ col.label }}
|
v-for="col in props.cols"
|
||||||
</q-th>
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
v-text="col.label"
|
||||||
|
></q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td
|
||||||
{{ col.value }}
|
v-for="col in props.cols"
|
||||||
</q-td>
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
v-text="col.value"
|
||||||
|
></q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
</q-table>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -183,27 +191,31 @@
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
:data="refunds"
|
:rows="refunds"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:columns="refundsTable.columns"
|
:columns="refundsTable.columns"
|
||||||
:pagination.sync="refundsTable.pagination"
|
v-model:pagination="refundsTable.pagination"
|
||||||
>
|
>
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
<q-th
|
||||||
{{ col.label }}
|
v-for="col in props.cols"
|
||||||
</q-th>
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
v-text="col.label"
|
||||||
|
></q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td
|
||||||
{{ col.value }}
|
v-for="col in props.cols"
|
||||||
</q-td>
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
v-text="col.value"
|
||||||
|
></q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
</q-table>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -240,7 +252,7 @@
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
emit-value
|
emit-value
|
||||||
v-model.trim="cardDialog.data.tx_limit"
|
v-model="cardDialog.data.tx_limit"
|
||||||
type="number"
|
type="number"
|
||||||
label="Max transaction (sats)"
|
label="Max transaction (sats)"
|
||||||
class="q-pr-sm"
|
class="q-pr-sm"
|
||||||
|
|
@ -251,7 +263,7 @@
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
emit-value
|
emit-value
|
||||||
v-model.trim="cardDialog.data.daily_limit"
|
v-model="cardDialog.data.daily_limit"
|
||||||
type="number"
|
type="number"
|
||||||
label="Daily limit (sats)"
|
label="Daily limit (sats)"
|
||||||
></q-input>
|
></q-input>
|
||||||
|
|
@ -261,7 +273,7 @@
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
emit-value
|
emit-value
|
||||||
v-model.trim="cardDialog.data.card_name"
|
v-model="cardDialog.data.card_name"
|
||||||
type="text"
|
type="text"
|
||||||
label="Card name "
|
label="Card name "
|
||||||
>
|
>
|
||||||
|
|
@ -272,7 +284,7 @@
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
emit-value
|
emit-value
|
||||||
v-model.trim="cardDialog.data.uid"
|
v-model="cardDialog.data.uid"
|
||||||
type="text"
|
type="text"
|
||||||
label="Card UID "
|
label="Card UID "
|
||||||
>
|
>
|
||||||
|
|
@ -281,36 +293,34 @@
|
||||||
<div class="col-2 q-pl-sm">
|
<div class="col-2 q-pl-sm">
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
disable
|
|
||||||
color="grey"
|
color="grey"
|
||||||
icon="nfc"
|
icon="nfc"
|
||||||
:disable="nfcTagReading"
|
:disable="disableNfcButton"
|
||||||
@click="readNfcTag()"
|
@click="readNfcTag()"
|
||||||
>
|
>
|
||||||
<q-tooltip>Tap card to scan UID</q-tooltip>
|
<q-tooltip>Tap card to scan UID</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="toggleAdvanced"
|
v-model="toggleAdvanced"
|
||||||
label="Show advanced options"
|
label="Show advanced options"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
|
|
||||||
<div v-show="toggleAdvanced" class="q-gutter-y-md">
|
<div v-show="toggleAdvanced" class="q-gutter-y-md">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="cardDialog.data.k0"
|
v-model="cardDialog.data.k0"
|
||||||
type="text"
|
type="text"
|
||||||
label="Card Auth key (K0)"
|
label="Card Auth key (K0)"
|
||||||
hint="Used to authentificate with the card (16 bytes in HEX). "
|
hint="Used to authentificate with the card (16 bytes in HEX). "
|
||||||
@randomkey
|
@randomkey
|
||||||
>
|
></q-input>
|
||||||
</q-input>
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="cardDialog.data.k1"
|
v-model="cardDialog.data.k1"
|
||||||
type="text"
|
type="text"
|
||||||
label="Card Meta key (K1)"
|
label="Card Meta key (K1)"
|
||||||
hint="Used for encypting of the message (16 bytes in HEX)."
|
hint="Used for encypting of the message (16 bytes in HEX)."
|
||||||
|
|
@ -318,12 +328,11 @@
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="cardDialog.data.k2"
|
v-model="cardDialog.data.k2"
|
||||||
type="text"
|
type="text"
|
||||||
label="Card File key (K2)"
|
label="Card File key (K2)"
|
||||||
hint="Used for CMAC of the message (16 bytes in HEX)."
|
hint="Used for CMAC of the message (16 bytes in HEX)."
|
||||||
>
|
></q-input>
|
||||||
</q-input>
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
@ -339,8 +348,7 @@
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
class="q-ml-auto"
|
class="q-ml-auto"
|
||||||
v-on:click="generateKeys"
|
@click="generateKeys"
|
||||||
v-on:click.right="debugKeys"
|
|
||||||
>Generate keys</q-btn
|
>Generate keys</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -370,19 +378,12 @@
|
||||||
|
|
||||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
<q-dialog v-model="qrCodeDialog.show" position="top">
|
||||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||||
{% raw %}
|
|
||||||
<div class="col q-mt-lg text-center">
|
<div class="col q-mt-lg text-center">
|
||||||
<q-responsive
|
<lnbits-qrcode
|
||||||
:ratio="1"
|
|
||||||
class="q-mx-xl q-mb-md"
|
|
||||||
v-show="!qrCodeDialog.wipe"
|
|
||||||
>
|
|
||||||
<qrcode
|
|
||||||
:value="qrCodeDialog.data.link"
|
:value="qrCodeDialog.data.link"
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
v-show="!qrCodeDialog.wipe"
|
||||||
</q-responsive>
|
></lnbits-qrcode>
|
||||||
<p class="text-center" v-show="!qrCodeDialog.wipe">
|
<p class="text-center" v-show="!qrCodeDialog.wipe">
|
||||||
(QR for <strong>create</strong> the card in
|
(QR for <strong>create</strong> the card in
|
||||||
<a
|
<a
|
||||||
|
|
@ -393,17 +394,11 @@
|
||||||
>Boltcard NFC Card Creator</a
|
>Boltcard NFC Card Creator</a
|
||||||
>)
|
>)
|
||||||
</p>
|
</p>
|
||||||
<q-responsive
|
<lnbits-qrcode
|
||||||
:ratio="1"
|
|
||||||
class="q-mx-xl q-mb-md"
|
|
||||||
v-show="qrCodeDialog.wipe"
|
|
||||||
>
|
|
||||||
<qrcode
|
|
||||||
:value="qrCodeDialog.data_wipe"
|
:value="qrCodeDialog.data_wipe"
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
v-show="qrCodeDialog.wipe"
|
||||||
</q-responsive>
|
></lnbits-qrcode>
|
||||||
<p class="text-center" v-show="qrCodeDialog.wipe">
|
<p class="text-center" v-show="qrCodeDialog.wipe">
|
||||||
(QR for <strong>wipe</strong> the card in
|
(QR for <strong>wipe</strong> the card in
|
||||||
<a
|
<a
|
||||||
|
|
@ -430,12 +425,18 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p style="word-break: break-all">
|
<p style="word-break: break-all">
|
||||||
<strong>Name:</strong> {{ qrCodeDialog.data.name }}<br />
|
<strong>Name: </strong><span v-text="qrCodeDialog.data.name"></span
|
||||||
<strong>UID:</strong> {{ qrCodeDialog.data.uid }}<br />
|
><br />
|
||||||
<strong>External ID:</strong> {{ qrCodeDialog.data.external_id }}<br />
|
<strong>UID: </strong> <span v-text="qrCodeDialog.data.uid"></span
|
||||||
<strong>Lock key (K0):</strong> {{ qrCodeDialog.data.k0 }}<br />
|
><br />
|
||||||
<strong>Meta key (K1 & K3):</strong> {{ qrCodeDialog.data.k1 }}<br />
|
<strong>External ID:</strong>
|
||||||
<strong>File key (K2 & K4):</strong> {{ qrCodeDialog.data.k2 }}<br />
|
<span v-text="qrCodeDialog.data.external_id"></span><br />
|
||||||
|
<strong>Lock key (K0):</strong>
|
||||||
|
<span v-text="qrCodeDialog.data.k0"></span><br />
|
||||||
|
<strong>Meta key (K1 & K3):</strong>
|
||||||
|
<span v-text="qrCodeDialog.data.k1"></span><br />
|
||||||
|
<strong>File key (K2 & K4):</strong>
|
||||||
|
<span v-text="qrCodeDialog.data.k2"></span><br />
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Always backup all keys that you're trying to write on the card. Without
|
Always backup all keys that you're trying to write on the card. Without
|
||||||
|
|
@ -472,7 +473,6 @@
|
||||||
>
|
>
|
||||||
<q-tooltip>Backup the keys, or wipe the card first!</q-tooltip>
|
<q-tooltip>Backup the keys, or wipe the card first!</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
{% endraw %}
|
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
<div class="row q-mt-lg q-gutter-sm">
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -481,5 +481,5 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script src="/boltcards/static/js/index.js"></script>
|
<script src="{{ static_url_for('boltcards/static', path='js/index.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
23
views.py
23
views.py
|
|
@ -1,16 +1,13 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.responses import HTMLResponse
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
from lnbits.helpers import template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from .crud import get_card_by_external_id, get_hits, get_refunds
|
from .crud import get_card_by_external_id, get_hits, get_refunds
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
boltcards_generic_router = APIRouter()
|
boltcards_generic_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -21,7 +18,7 @@ def boltcards_renderer():
|
||||||
@boltcards_generic_router.get("/", response_class=HTMLResponse)
|
@boltcards_generic_router.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
return boltcards_renderer().TemplateResponse(
|
return boltcards_renderer().TemplateResponse(
|
||||||
"boltcards/index.html", {"request": request, "user": user.dict()}
|
"boltcards/index.html", {"request": request, "user": user.json()}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -32,15 +29,11 @@ async def display(request: Request, card_id: str):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Card does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Card does not exist."
|
||||||
)
|
)
|
||||||
hits = [hit.dict() for hit in await get_hits([card.id])]
|
hits = await get_hits([card.id])
|
||||||
refunds = [
|
hits_json = [hit.json() for hit in hits]
|
||||||
refund.hit_id for refund in await get_refunds([hit["id"] for hit in hits])
|
refunds = [refund.hit_id for refund in await get_refunds([hit.id for hit in hits])]
|
||||||
]
|
card_json = card.json(exclude={"wallet"})
|
||||||
card_dict = card.dict()
|
|
||||||
# Remove wallet id from card dict
|
|
||||||
del card_dict["wallet"]
|
|
||||||
|
|
||||||
return boltcards_renderer().TemplateResponse(
|
return boltcards_renderer().TemplateResponse(
|
||||||
"boltcards/display.html",
|
"boltcards/display.html",
|
||||||
{"request": request, "card": card_dict, "hits": hits, "refunds": refunds},
|
{"request": request, "card": card_json, "hits": hits_json, "refunds": refunds},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
36
views_api.py
36
views_api.py
|
|
@ -3,7 +3,7 @@ from http import HTTPStatus
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from lnbits.core.crud import get_user
|
from lnbits.core.crud import get_user
|
||||||
from lnbits.core.models import WalletTypeInfo
|
from lnbits.core.models import WalletTypeInfo
|
||||||
from lnbits.decorators import get_key_type, require_admin_key
|
from lnbits.decorators import require_admin_key, require_invoice_key
|
||||||
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_card,
|
create_card,
|
||||||
|
|
@ -16,22 +16,22 @@ from .crud import (
|
||||||
get_refunds,
|
get_refunds,
|
||||||
update_card,
|
update_card,
|
||||||
)
|
)
|
||||||
from .models import Card, CreateCardData
|
from .models import Card, CreateCardData, Hit, Refund
|
||||||
|
|
||||||
boltcards_api_router = APIRouter()
|
boltcards_api_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@boltcards_api_router.get("/api/v1/cards")
|
@boltcards_api_router.get("/api/v1/cards")
|
||||||
async def api_cards(
|
async def api_cards(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = False
|
key_info: WalletTypeInfo = Depends(require_invoice_key), all_wallets: bool = False
|
||||||
):
|
) -> list[Card]:
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [key_info.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
user = await get_user(g.wallet.user)
|
user = await get_user(key_info.wallet.user)
|
||||||
wallet_ids = user.wallet_ids if user else []
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
|
||||||
return [card.dict() for card in await get_cards(wallet_ids)]
|
return await get_cards(wallet_ids)
|
||||||
|
|
||||||
|
|
||||||
def validate_card(data: CreateCardData):
|
def validate_card(data: CreateCardData):
|
||||||
|
|
@ -146,12 +146,13 @@ async def api_card_delete(card_id, wallet: WalletTypeInfo = Depends(require_admi
|
||||||
|
|
||||||
@boltcards_api_router.get("/api/v1/hits")
|
@boltcards_api_router.get("/api/v1/hits")
|
||||||
async def api_hits(
|
async def api_hits(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
):
|
all_wallets: bool = Query(False),
|
||||||
wallet_ids = [g.wallet.id]
|
) -> list[Hit]:
|
||||||
|
wallet_ids = [key_info.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
user = await get_user(g.wallet.user)
|
user = await get_user(key_info.wallet.user)
|
||||||
wallet_ids = user.wallet_ids if user else []
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
|
||||||
cards = await get_cards(wallet_ids)
|
cards = await get_cards(wallet_ids)
|
||||||
|
|
@ -159,17 +160,18 @@ async def api_hits(
|
||||||
for card in cards:
|
for card in cards:
|
||||||
cards_ids.append(card.id)
|
cards_ids.append(card.id)
|
||||||
|
|
||||||
return [hit.dict() for hit in await get_hits(cards_ids)]
|
return await get_hits(cards_ids)
|
||||||
|
|
||||||
|
|
||||||
@boltcards_api_router.get("/api/v1/refunds")
|
@boltcards_api_router.get("/api/v1/refunds")
|
||||||
async def api_refunds(
|
async def api_refunds(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
):
|
all_wallets: bool = Query(False),
|
||||||
wallet_ids = [g.wallet.id]
|
) -> list[Refund]:
|
||||||
|
wallet_ids = [key_info.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
user = await get_user(g.wallet.user)
|
user = await get_user(key_info.wallet.user)
|
||||||
wallet_ids = user.wallet_ids if user else []
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
|
||||||
cards = await get_cards(wallet_ids)
|
cards = await get_cards(wallet_ids)
|
||||||
|
|
@ -181,4 +183,4 @@ async def api_refunds(
|
||||||
for hit in hits:
|
for hit in hits:
|
||||||
hits_ids.append(hit.id)
|
hits_ids.append(hit.id)
|
||||||
|
|
||||||
return [refund.dict() for refund in await get_refunds(hits_ids)]
|
return await get_refunds(hits_ids)
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ async def api_scan(p, c, request: Request, external_id: str):
|
||||||
|
|
||||||
hits_amount = 0
|
hits_amount = 0
|
||||||
for hit in todays_hits:
|
for hit in todays_hits:
|
||||||
hits_amount = hits_amount + hit.amount
|
hits_amount += hit.amount
|
||||||
if hits_amount > card.daily_limit:
|
if hits_amount > int(card.daily_limit):
|
||||||
return {"status": "ERROR", "reason": "Max daily limit spent."}
|
return {"status": "ERROR", "reason": "Max daily limit spent."}
|
||||||
hit = await create_hit(card.id, ip, agent, card.counter, ctr_int)
|
hit = await create_hit(card.id, ip, agent, card.counter, ctr_int)
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ async def api_scan(p, c, request: Request, external_id: str):
|
||||||
"callback": str(request.url_for("boltcards.lnurl_callback", hit_id=hit.id)),
|
"callback": str(request.url_for("boltcards.lnurl_callback", hit_id=hit.id)),
|
||||||
"k1": hit.id,
|
"k1": hit.id,
|
||||||
"minWithdrawable": 1 * 1000,
|
"minWithdrawable": 1 * 1000,
|
||||||
"maxWithdrawable": card.tx_limit * 1000,
|
"maxWithdrawable": int(card.tx_limit) * 1000,
|
||||||
"defaultDescription": f"Boltcard (refund address lnurl://{lnurlpay_bech32})",
|
"defaultDescription": f"Boltcard (refund address lnurl://{lnurlpay_bech32})",
|
||||||
"payLink": lnurlpay_nonbech32_lud17, # LUD-19 compatibility
|
"payLink": lnurlpay_nonbech32_lud17, # LUD-19 compatibility
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +135,7 @@ async def lnurl_callback(
|
||||||
await pay_invoice(
|
await pay_invoice(
|
||||||
wallet_id=card.wallet,
|
wallet_id=card.wallet,
|
||||||
payment_request=pr,
|
payment_request=pr,
|
||||||
max_sat=card.tx_limit,
|
max_sat=int(card.tx_limit),
|
||||||
extra={"tag": "boltcards", "hit": hit.id},
|
extra={"tag": "boltcards", "hit": hit.id},
|
||||||
)
|
)
|
||||||
return {"status": "OK"}
|
return {"status": "OK"}
|
||||||
|
|
@ -201,14 +201,13 @@ async def lnurlp_response(req: Request, hit_id: str):
|
||||||
"callback": str(req.url_for("boltcards.lnurlp_callback", hit_id=hit_id)),
|
"callback": str(req.url_for("boltcards.lnurlp_callback", hit_id=hit_id)),
|
||||||
"metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])),
|
"metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])),
|
||||||
"minSendable": 1 * 1000,
|
"minSendable": 1 * 1000,
|
||||||
"maxSendable": card.tx_limit * 1000,
|
"maxSendable": int(card.tx_limit) * 1000,
|
||||||
}
|
}
|
||||||
return json.dumps(pay_response)
|
return json.dumps(pay_response)
|
||||||
|
|
||||||
|
|
||||||
@boltcards_lnurl_router.get(
|
@boltcards_lnurl_router.get(
|
||||||
"/api/v1/lnurlp/cb/{hit_id}",
|
"/api/v1/lnurlp/cb/{hit_id}",
|
||||||
response_class=HTMLResponse,
|
|
||||||
name="boltcards.lnurlp_callback",
|
name="boltcards.lnurlp_callback",
|
||||||
)
|
)
|
||||||
async def lnurlp_callback(hit_id: str, amount: str = Query(None)):
|
async def lnurlp_callback(hit_id: str, amount: str = Query(None)):
|
||||||
|
|
@ -219,7 +218,7 @@ async def lnurlp_callback(hit_id: str, amount: str = Query(None)):
|
||||||
if not hit:
|
if not hit:
|
||||||
return {"status": "ERROR", "reason": "LNURL-pay record not found."}
|
return {"status": "ERROR", "reason": "LNURL-pay record not found."}
|
||||||
|
|
||||||
_, payment_request = await create_invoice(
|
payment = await create_invoice(
|
||||||
wallet_id=card.wallet,
|
wallet_id=card.wallet,
|
||||||
amount=int(int(amount) / 1000),
|
amount=int(int(amount) / 1000),
|
||||||
memo=f"Refund {hit_id}",
|
memo=f"Refund {hit_id}",
|
||||||
|
|
@ -229,6 +228,4 @@ async def lnurlp_callback(hit_id: str, amount: str = Query(None)):
|
||||||
extra={"refund": hit_id},
|
extra={"refund": hit_id},
|
||||||
)
|
)
|
||||||
|
|
||||||
pay_response = {"pr": payment_request, "routes": []}
|
return {"pr": payment.bolt11, "routes": []}
|
||||||
|
|
||||||
return json.dumps(pay_response)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue