Swaps lnurlpos for lnurldevice

This commit is contained in:
benarc 2022-01-21 21:23:32 +00:00
parent 9e13b8eb70
commit 57eb27fcee
21 changed files with 784 additions and 599 deletions

View File

@ -0,0 +1,3 @@
# LNURLDevice
For offline LNURL devices

View File

@ -0,0 +1,17 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_lnurldevice")
lnurldevice_ext: APIRouter = APIRouter(prefix="/lnurldevice", tags=["lnurldevice"])
def lnurldevice_renderer():
return template_renderer(["lnbits/extensions/lnurldevice/templates"])
from .lnurl import * # noqa
from .views import * # noqa
from .views_api import * # noqa

View File

@ -0,0 +1,6 @@
{
"name": "LNURLDevice",
"short_description": "For offline LNURL devices",
"icon": "point_of_sale",
"contributors": ["arcbtc"]
}

View File

@ -0,0 +1,120 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import createLnurldevice, lnurldevicepayment, lnurldevices
###############lnurldeviceS##########################
async def create_lnurldevice(data: createLnurldevice,) -> lnurldevices:
lnurldevice_id = urlsafe_short_hash()
lnurldevice_key = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevices (
id,
key,
title,
wallet,
currency,
device,
profit
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(lnurldevice_id, lnurldevice_key, data.title, data.wallet, data.currency, data.device, data.profit,),
)
return await get_lnurldevice(lnurldevice_id)
async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldevices]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurldevice.lnurldevices SET {q} WHERE id = ?",
(*kwargs.values(), lnurldevice_id),
)
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
return lnurldevices(**row) if row else None
async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
return lnurldevices(**row) if row else None
async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevices]:
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids[0]))
rows = await db.fetchall(
f"""
SELECT * FROM lnurldevice.lnurldevices WHERE wallet IN ({q})
ORDER BY id
""",
(*wallet_ids,),
)
return [lnurldevices(**row) if row else None for row in rows]
async def delete_lnurldevice(lnurldevice_id: str) -> None:
await db.execute("DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,))
########################lnuldevice payments###########################
async def create_lnurldevicepayment(
deviceid: str,
payload: Optional[str] = None,
pin: Optional[str] = None,
payhash: Optional[str] = None,
sats: Optional[int] = 0,
) -> lnurldevicepayment:
lnurldevicepayment_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevicepayment (
id,
deviceid,
payload,
pin,
payhash,
sats
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(lnurldevicepayment_id, deviceid, payload, pin, payhash, sats),
)
return await get_lnurldevicepayment(lnurldevicepayment_id)
async def update_lnurldevicepayment(
lnurldevicepayment_id: str, **kwargs
) -> Optional[lnurldevicepayment]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurldevice.lnurldevicepayment SET {q} WHERE id = ?",
(*kwargs.values(), lnurldevicepayment_id),
)
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", (lnurldevicepayment_id,)
)
return lnurldevicepayment(**row) if row else None
async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", (lnurldevicepayment_id,)
)
return lnurldevicepayment(**row) if row else None
async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?", (lnurldevicepayment_payload,)
)
return lnurldevicepayment(**row) if row else None

View File

@ -0,0 +1,233 @@
import base64
import hashlib
from http import HTTPStatus
from typing import Optional
from embit import bech32
from embit import compact
import base64
from io import BytesIO
import hmac
from fastapi import Request
from fastapi.param_functions import Query
from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from lnbits.core.views.api import pay_invoice
from . import lnurldevice_ext
from .crud import (
create_lnurldevicepayment,
get_lnurldevice,
get_lnurldevicepayment,
update_lnurldevicepayment,
get_lnurlpayload,
)
def bech32_decode(bech):
"""tweaked version of bech32_decode that ignores length limitations"""
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
bech.lower() != bech and bech.upper() != bech
):
return
bech = bech.lower()
device = bech.rfind("1")
if device < 1 or device + 7 > len(bech):
return
if not all(x in bech32.CHARSET for x in bech[device + 1 :]):
return
hrp = bech[:device]
data = [bech32.CHARSET.find(x) for x in bech[device + 1 :]]
encoding = bech32.bech32_verify_checksum(hrp, data)
if encoding is None:
return
return bytes(bech32.convertbits(data[:-6], 5, 8, False))
def xor_decrypt(key, blob):
s = BytesIO(blob)
variant = s.read(1)[0]
if variant != 1:
raise RuntimeError("Not implemented")
# reading nonce
l = s.read(1)[0]
nonce = s.read(l)
if len(nonce) != l:
raise RuntimeError("Missing nonce bytes")
if l < 8:
raise RuntimeError("Nonce is too short")
# reading payload
l = s.read(1)[0]
payload = s.read(l)
if len(payload) > 32:
raise RuntimeError("Payload is too long for this encryption method")
if len(payload) != l:
raise RuntimeError("Missing payload bytes")
hmacval = s.read()
expected = hmac.new(
key, b"Data:" + blob[: -len(hmacval)], digestmod="sha256"
).digest()
if len(hmacval) < 8:
raise RuntimeError("HMAC is too short")
if hmacval != expected[: len(hmacval)]:
raise RuntimeError("HMAC is invalid")
secret = hmac.new(key, b"Round secret:" + nonce, digestmod="sha256").digest()
payload = bytearray(payload)
for i in range(len(payload)):
payload[i] = payload[i] ^ secret[i]
s = BytesIO(payload)
pin = compact.read_from(s)
amount_in_cent = compact.read_from(s)
return pin, amount_in_cent
@lnurldevice_ext.get(
"/api/v1/lnurl/{device_id}",
status_code=HTTPStatus.OK,
name="lnurldevice.lnurl_v1_params",
)
async def lnurl_v1_params(
request: Request,
device_id: str = Query(None),
p: str = Query(None),
atm: str = Query(None),
):
device = await get_lnurldevice(device_id)
if not device:
return {
"status": "ERROR",
"reason": f"lnurldevice {device_id} not found on this server",
}
paymentcheck = await get_lnurlpayload(p)
if device.device == "atm":
if paymentcheck:
return {
"status": "ERROR",
"reason": f"Payment already claimed",
}
if len(p) % 4 > 0:
p += "=" * (4 - (len(p) % 4))
data = base64.urlsafe_b64decode(p)
pin = 0
amount_in_cent = 0
try:
result = xor_decrypt(device.key.encode(), data)
pin = result[0]
amount_in_cent = result[1]
except Exception as exc:
return {"status": "ERROR", "reason": str(exc)}
price_msat = (
await fiat_amount_as_satoshis(float(amount_in_cent) / 100, device.currency)
if device.currency != "sat"
else amount_in_cent
) * 1000
if atm:
if device.device != "atm":
return {"status": "ERROR", "reason": "Not ATM device."}
price_msat = int(price_msat * (1 - (device.profit / 100)) / 1000)
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
payload=p,
sats=price_msat * 1000,
pin=pin,
payhash="payment_hash",
)
if not lnurldevicepayment:
return {"status": "ERROR", "reason": "Could not create payment."}
return {
"tag": "withdrawRequest",
"callback": request.url_for(
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
),
"k1": lnurldevicepayment.id,
"minWithdrawable": price_msat * 1000,
"maxWithdrawable": price_msat * 1000,
"defaultDescription": device.title,
}
price_msat = int(price_msat * ((device.profit / 100) + 1) / 1000)
print(price_msat)
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
payload=p,
sats=price_msat * 1000,
pin=pin,
payhash="payment_hash",
)
if not lnurldevicepayment:
return {"status": "ERROR", "reason": "Could not create payment."}
return {
"tag": "payRequest",
"callback": request.url_for(
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
),
"minSendable": price_msat * 1000,
"maxSendable": price_msat * 1000,
"metadata": await device.lnurlpay_metadata(),
}
@lnurldevice_ext.get(
"/api/v1/lnurl/cb/{paymentid}",
status_code=HTTPStatus.OK,
name="lnurldevice.lnurl_callback",
)
async def lnurl_callback(request: Request, paymentid: str = Query(None), pr: str = Query(None), k1: str = Query(None)):
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
device = await get_lnurldevice(lnurldevicepayment.deviceid)
if not device:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="lnurldevice not found."
)
if pr:
if lnurldevicepayment.id != k1:
return {"status": "ERROR", "reason": "Bad K1"}
if lnurldevicepayment.payhash != "payment_hash":
return {
"status": "ERROR",
"reason": f"Payment already claimed",
}
lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
)
await pay_invoice(
wallet_id=device.wallet,
payment_request=pr,
max_sat=lnurldevicepayment.sats / 1000,
extra={"tag": "withdraw"},
)
return {"status": "OK"}
print(lnurldevicepayment.sats)
payment_hash, payment_request = await create_invoice(
wallet_id=device.wallet,
amount=lnurldevicepayment.sats / 1000,
memo=device.title,
description_hash=hashlib.sha256(
(await device.lnurlpay_metadata()).encode("utf-8")
).digest(),
extra={"tag": "PoS"},
)
lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=payment_hash
)
return {
"pr": payment_request,
"successAction": {
"tag": "url",
"description": "Check the attached link",
"url": request.url_for("lnurldevice.displaypin", paymentid=paymentid),
},
"routes": [],
}
return resp.dict()

View File

@ -0,0 +1,76 @@
from lnbits.db import Database
db2 = Database("ext_lnurlpos")
async def m001_initial(db):
"""
Initial lnurldevice table.
"""
await db.execute(
f"""
CREATE TABLE lnurldevice.lnurldevices (
id TEXT NOT NULL PRIMARY KEY,
key TEXT NOT NULL,
title TEXT NOT NULL,
wallet TEXT NOT NULL,
currency TEXT NOT NULL,
device TEXT NOT NULL,
profit FLOAT NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
await db.execute(
f"""
CREATE TABLE lnurldevice.lnurldevicepayment (
id TEXT NOT NULL PRIMARY KEY,
deviceid TEXT NOT NULL,
payhash TEXT,
payload TEXT NOT NULL,
pin INT,
sats INT,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
async def m002_redux(db):
"""
Moves everything from lnurlpos to lnurldevices
"""
for row in [
list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlposs")
]:
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevices (
id,
key,
title,
wallet,
currency,
device,
profit
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[2], row[3], row[4], "pos", 0),
)
for row in [
list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlpospayment")
]:
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevicepayment (
id,
deviceid,
payhash,
payload,
pin,
sats
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[3], row[4], row[5], row[6]),
)

View File

@ -11,33 +11,37 @@ from pydantic import BaseModel
from pydantic.main import BaseModel
class createLnurlpos(BaseModel):
class createLnurldevice(BaseModel):
title: str
wallet: str
currency: str
device: str
profit: float
class lnurlposs(BaseModel):
class lnurldevices(BaseModel):
id: str
key: str
title: str
wallet: str
currency: str
device: str
profit: float
timestamp: str
def from_row(cls, row: Row) -> "lnurlposs":
def from_row(cls, row: Row) -> "lnurldevices":
return cls(**dict(row))
def lnurl(self, req: Request) -> Lnurl:
url = req.url_for("lnurlpos.lnurl_response", pos_id=self.id, _external=True)
url = req.url_for("lnurldevice.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 lnurlpospayment(BaseModel):
class lnurldevicepayment(BaseModel):
id: str
posid: str
deviceid: str
payhash: str
payload: str
pin: int
@ -45,5 +49,5 @@ class lnurlpospayment(BaseModel):
timestamp: str
@classmethod
def from_row(cls, row: Row) -> "lnurlpospayment":
def from_row(cls, row: Row) -> "lnurldevicepayment":
return cls(**dict(row))

View File

@ -1,10 +1,10 @@
<q-card>
<q-card-section>
<p>
Register LNURLPoS devices to receive payments in your LNbits wallet.<br />
Register LNURLDevice devices to receive payments in your LNbits wallet.<br />
Build your own here
<a href="https://github.com/arcbtc/LNURLPoS"
>https://github.com/arcbtc/LNURLPoS</a
<a href="https://github.com/arcbtc/bitcoinpos"
>https://github.com/arcbtc/bitcoinpos</a
><br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
@ -21,12 +21,12 @@
group="api"
dense
expand-separator
label="Create lnurlpos"
label="Create lnurldevice"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">POST</span> /lnurlpos/api/v1/lnurlpos</code
><span class="text-blue"></span> /lnurldevice/api/v1/lnurlpos</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
@ -36,10 +36,10 @@
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<code>[&lt;lnurldevice_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}api/v1/lnurlpos -d '{"title":
>curl -X POST {{ request.base_url }}api/v1/lnurldevice -d '{"title":
&lt;string&gt;, "message":&lt;string&gt;, "currency":
&lt;integer&gt;}' -H "Content-type: application/json" -H "X-Api-Key:
{{user.wallets[0].adminkey }}"
@ -51,13 +51,13 @@
group="api"
dense
expand-separator
label="Update lnurlpos"
label="Update lnurldevice"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">PUT</span>
/lnurlpos/api/v1/lnurlpos/&lt;lnurlpos_id&gt;</code
/lnurldevice/api/v1/lnurlpos/&lt;lnurldevice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
@ -67,25 +67,30 @@
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<code>[&lt;lnurldevice_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url
}}api/v1/lnurlpos/&lt;lnurlpos_id&gt; -d ''{"title": &lt;string&gt;,
"message":&lt;string&gt;, "currency": &lt;integer&gt;} -H
"Content-type: application/json" -H "X-Api-Key:
}}api/v1/lnurlpos/&lt;lnurldevice_id&gt; -d ''{"title":
&lt;string&gt;, "message":&lt;string&gt;, "currency":
&lt;integer&gt;} -H "Content-type: application/json" -H "X-Api-Key:
{{user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Get lnurlpos">
<q-expansion-item
group="api"
dense
expand-separator
label="Get lnurldevice"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/lnurlpos/api/v1/lnurlpos/&lt;lnurlpos_id&gt;</code
/lnurldevice/api/v1/lnurlpos/&lt;lnurldevice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
@ -95,21 +100,27 @@
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<code>[&lt;lnurldevice_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}api/v1/lnurlpos/&lt;lnurlpos_id&gt; -H "X-Api-Key: {{
}}api/v1/lnurlpos/&lt;lnurldevice_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Get lnurlposs">
<q-expansion-item
group="api"
dense
expand-separator
label="Get lnurldevices"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span> /lnurlpos/api/v1/lnurlposs</code
><span class="text-blue">GET</span>
/lnurldevice/api/v1/lnurlposs</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
@ -119,11 +130,11 @@
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<code>[&lt;lnurldevice_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}api/v1/lnurlposs -H "X-Api-Key:
{{ user.wallets[0].inkey }}"
>curl -X GET {{ request.base_url }}api/v1/lnurldevices -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
@ -139,7 +150,7 @@
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/lnurlpos/api/v1/lnurlpos/&lt;lnurlpos_id&gt;</code
/lnurldevice/api/v1/lnurlpos/&lt;lnurldevice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
@ -148,7 +159,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}api/v1/lnurlpos/&lt;lnurlpos_id&gt; -H "X-Api-Key: {{
}}api/v1/lnurlpos/&lt;lnurldevice_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>

View File

@ -8,8 +8,8 @@
<q-btn
unelevated
color="primary"
@click="formDialoglnurlpos.show = true"
>New LNURLPoS instance
@click="formDialoglnurldevice.show = true"
>New LNURLDevice instance
</q-btn>
</q-card-section>
</q-card>
@ -18,7 +18,7 @@
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">lNURLPoS</h5>
<h5 class="text-subtitle1 q-my-none">lNURLdevice</h5>
</div>
<div class="col-auto">
@ -33,7 +33,7 @@
<q-icon name="search"></q-icon>
</template>
</q-input>
<q-btn flat color="grey" @click="exportlnurlposCSV"
<q-btn flat color="grey" @click="exportlnurldeviceCSV"
>Export to CSV</q-btn
>
</div>
@ -41,10 +41,10 @@
<q-table
flat
dense
:data="lnurlposLinks"
:data="lnurldeviceLinks"
row-key="id"
:columns="lnurlpossTable.columns"
:pagination.sync="lnurlpossTable.pagination"
:columns="lnurldevicesTable.columns"
:pagination.sync="lnurldevicesTable.pagination"
:filter="filter"
>
<template v-slot:header="props">
@ -72,11 +72,11 @@
flat
dense
size="xs"
@click="deletelnurlposLink(props.row.id)"
@click="deletelnurldeviceLink(props.row.id)"
icon="cancel"
color="pink"
>
<q-tooltip> Delete LNURLPoS </q-tooltip>
<q-tooltip> Delete LNURLDevice </q-tooltip>
</q-btn>
</q-td>
<q-td>
@ -84,11 +84,11 @@
flat
dense
size="xs"
@click="openlnurlposSettings(props.row.id)"
@click="openlnurldeviceSettings(props.row.id)"
icon="perm_data_setting"
color="primary"
>
<q-tooltip> LNURLPoS Settings </q-tooltip>
<q-tooltip> LNURLDevice Settings </q-tooltip>
</q-btn>
</q-td>
<q-td
@ -112,46 +112,57 @@
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} LNURLPoS Extension
{{SITE_TITLE}} LNURLDevice Extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "lnurlpos/_api_docs.html" %} </q-list>
<q-list> {% include "lnurldevice/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog
v-model="settingsDialog.show"
position="top"
deviceition="top"
@hide="closeFormDialog"
>
<q-card
style="width: 700px; max-width: 80vw"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<div class="text-h6">Copy to LNURLPoS device</div>
<div class="text-h6">LNURLDevice device string</div>
<q-btn
dense
outline
unelevated
color="primary"
size="md"
@click="copyText(location + '/lnurldevice/api/v1/lnurl/' + settingsDialog.data.id + ',' +
settingsDialog.data.key + ',' + settingsDialog.data.currency, 'Link copied to clipboard!')"
>{% raw
%}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}},
{{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw
%}<q-tooltip> Click to copy URL </q-tooltip>
</q-btn>
<div class="text-subtitle2">
{% raw %} String baseURL =
"{{location}}/lnurlpos/api/v1/lnurl/{{settingsDialog.data.id}}";<br />
String key = "{{settingsDialog.data.key}}";<br />
String currency = "{{settingsDialog.data.currency}}";{% endraw %}
<small> </small>
</div>
</q-card>
</q-dialog>
<q-dialog
v-model="formDialoglnurlpos.show"
position="top"
v-model="formDialoglnurldevice.show"
deviceition="top"
@hide="closeFormDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormDatalnurlpos" class="q-gutter-md">
<q-form @submit="sendFormDatalnurldevice" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialoglnurlpos.data.title"
v-model.trim="formDialoglnurldevice.data.title"
type="text"
label="Title"
></q-input>
@ -160,7 +171,7 @@
filled
dense
emit-value
v-model="formDialoglnurlpos.data.wallet"
v-model="formDialoglnurldevice.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
@ -168,32 +179,46 @@
<q-select
filled
dense
v-model.trim="formDialoglnurlpos.data.currency"
v-model.trim="formDialoglnurldevice.data.currency"
type="text"
label="Fiat currency for PoS"
label="Fiat currency for device"
:options="currency"
></q-select>
<q-option-group
v-model.trim="formDialoglnurldevice.data.device"
:options="devices"
color="primary"
label="Type of device"
></q-option-group>
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.profit"
type="number"
max="90"
label="Profit margin (% added to invoices/deducted from faucets)"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialoglnurlpos.data.id"
v-if="formDialoglnurldevice.data.id"
unelevated
color="primary"
:disable="
formDialoglnurlpos.data.title == ''"
formDialoglnurldevice.data.title == ''"
type="submit"
>Update lnurlpos</q-btn
>Update lnurldevice</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="
formDialoglnurlpos.data.title == ''"
formDialoglnurldevice.data.title == ''"
type="submit"
>Create lnurlpos</q-btn
>Create lnurldevice</q-btn
>
<q-btn @click="cancellnurlpos" flat color="grey" class="q-ml-auto"
<q-btn @click="cancellnurldevice" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
@ -206,7 +231,7 @@
<script>
Vue.component(VueQrcode.name, VueQrcode)
var maplnurlpos = obj => {
var maplnurldevice = obj => {
obj._data = _.clone(obj)
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
obj.time = obj.time + 'mins'
@ -230,9 +255,19 @@
location: window.location.hostname,
filter: '',
currency: 'USD',
lnurlposLinks: [],
lnurlposLinksObj: [],
lnurlpossTable: {
lnurldeviceLinks: [],
lnurldeviceLinksObj: [],
devices: [
{
label: 'PoS',
value: 'pos'
},
{
label: 'ATM',
value: 'atm'
}
],
lnurldevicesTable: {
columns: [
{
name: 'title',
@ -258,6 +293,18 @@
label: 'wallet',
field: 'wallet'
},
{
name: 'device',
align: 'left',
label: 'device',
field: 'device'
},
{
name: 'profit',
align: 'left',
label: 'profit',
field: 'profit'
},
{
name: 'currency',
align: 'left',
@ -269,7 +316,7 @@
rowsPerPage: 10
}
},
passedlnurlpos: {},
passedlnurldevice: {},
settingsDialog: {
show: false,
data: {}
@ -278,13 +325,15 @@
show: false,
data: {}
},
formDialoglnurlpos: {
formDialoglnurldevice: {
show: false,
data: {
lnurl_toggle: false,
show_message: false,
show_ack: false,
show_price: 'None',
device: 'pos',
profit: 2,
title: ''
}
},
@ -295,33 +344,33 @@
}
},
methods: {
cancellnurlpos: function (data) {
cancellnurldevice: function (data) {
var self = this
self.formDialoglnurlpos.show = false
self.clearFormDialoglnurlpos()
self.formDialoglnurldevice.show = false
self.clearFormDialoglnurldevice()
},
closeFormDialog: function () {
this.clearFormDialoglnurlpos()
this.clearFormDialoglnurldevice()
this.formDialog.data = {
is_unique: false
}
},
sendFormDatalnurlpos: function () {
sendFormDatalnurldevice: function () {
var self = this
if (self.formDialoglnurlpos.data.id) {
this.updatelnurlpos(
if (self.formDialoglnurldevice.data.id) {
this.updatelnurldevice(
self.g.user.wallets[0].adminkey,
self.formDialoglnurlpos.data
self.formDialoglnurldevice.data
)
} else {
this.createlnurlpos(
this.createlnurldevice(
self.g.user.wallets[0].adminkey,
self.formDialoglnurlpos.data
self.formDialoglnurldevice.data
)
}
},
createlnurlpos: function (wallet, data) {
createlnurldevice: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
@ -330,86 +379,91 @@
}
}
LNbits.api
.request('POST', '/lnurlpos/api/v1/lnurlpos', wallet, updatedData)
.request('POST', '/lnurldevice/api/v1/lnurlpos', wallet, updatedData)
.then(function (response) {
self.lnurlposLinks.push(maplnurlpos(response.data))
self.formDialoglnurlpos.show = false
self.clearFormDialoglnurlpos()
self.lnurldeviceLinks.push(maplnurldevice(response.data))
self.formDialoglnurldevice.show = false
self.clearFormDialoglnurldevice()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getlnurlposs: function () {
getlnurldevices: function () {
var self = this
LNbits.api
.request(
'GET',
'/lnurlpos/api/v1/lnurlpos',
'/lnurldevice/api/v1/lnurlpos',
self.g.user.wallets[0].adminkey
)
.then(function (response) {
if (response.data) {
self.lnurlposLinks = response.data.map(maplnurlpos)
self.lnurldeviceLinks = response.data.map(maplnurldevice)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getlnurlpos: function (lnurlpos_id) {
getlnurldevice: function (lnurldevice_id) {
var self = this
LNbits.api
.request(
'GET',
'/lnurlpos/api/v1/lnurlpos/' + lnurlpos_id,
'/lnurldevice/api/v1/lnurlpos/' + lnurldevice_id,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
localStorage.setItem('lnurlpos', JSON.stringify(response.data))
localStorage.setItem('lnurldevice', JSON.stringify(response.data))
localStorage.setItem('inkey', self.g.user.wallets[0].inkey)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deletelnurlposLink: function (lnurlposId) {
deletelnurldeviceLink: function (lnurldeviceId) {
var self = this
var link = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
var link = _.findWhere(this.lnurldeviceLinks, {id: lnurldeviceId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this pay link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnurlpos/api/v1/lnurlpos/' + lnurlposId,
'/lnurldevice/api/v1/lnurlpos/' + lnurldeviceId,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.lnurlposLinks = _.reject(self.lnurlposLinks, function (
obj
) {
return obj.id === lnurlposId
})
self.lnurldeviceLinks = _.reject(
self.lnurldeviceLinks,
function (obj) {
return obj.id === lnurldeviceId
}
)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
openUpdatelnurlposLink: function (lnurlposId) {
openUpdatelnurldeviceLink: function (lnurldeviceId) {
var self = this
var lnurlpos = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
self.formDialoglnurlpos.data = _.clone(lnurlpos._data)
self.formDialoglnurlpos.show = true
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
id: lnurldeviceId
})
self.formDialoglnurldevice.data = _.clone(lnurldevice._data)
self.formDialoglnurldevice.show = true
},
openlnurlposSettings: function (lnurlposId) {
openlnurldeviceSettings: function (lnurldeviceId) {
var self = this
var lnurlpos = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
self.settingsDialog.data = _.clone(lnurlpos._data)
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
id: lnurldeviceId
})
self.settingsDialog.data = _.clone(lnurldevice._data)
self.settingsDialog.show = true
},
updatelnurlpos: function (wallet, data) {
updatelnurldevice: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
@ -421,24 +475,27 @@
LNbits.api
.request(
'PUT',
'/lnurlpos/api/v1/lnurlpos/' + updatedData.id,
'/lnurldevice/api/v1/lnurlpos/' + updatedData.id,
wallet,
updatedData
)
.then(function (response) {
self.lnurlposLinks = _.reject(self.lnurlposLinks, function (obj) {
return obj.id === updatedData.id
})
self.lnurlposLinks.push(maplnurlpos(response.data))
self.formDialoglnurlpos.show = false
self.clearFormDialoglnurlpos()
self.lnurldeviceLinks = _.reject(
self.lnurldeviceLinks,
function (obj) {
return obj.id === updatedData.id
}
)
self.lnurldeviceLinks.push(maplnurldevice(response.data))
self.formDialoglnurldevice.show = false
self.clearFormDialoglnurldevice()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
clearFormDialoglnurlpos() {
this.formDialoglnurlpos.data = {
clearFormDialoglnurldevice() {
this.formDialoglnurldevice.data = {
lnurl_toggle: false,
show_message: false,
show_ack: false,
@ -446,15 +503,18 @@
title: ''
}
},
exportlnurlposCSV: function () {
exportlnurldeviceCSV: function () {
var self = this
LNbits.utils.exportCSV(self.lnurlpossTable.columns, this.lnurlposLinks)
LNbits.utils.exportCSV(
self.lnurldevicesTable.columns,
this.lnurldeviceLinks
)
}
},
created: function () {
var self = this
var getlnurlposs = this.getlnurlposs
getlnurlposs()
var getlnurldevices = this.getlnurldevices
getlnurldevices()
self.location = [
window.location.protocol,
'//',

View File

@ -0,0 +1,50 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.crud import update_payment_status
from lnbits.core.models import User
from lnbits.core.views.api import api_payment
from lnbits.decorators import check_user_exists
from . import lnurldevice_ext, lnurldevice_renderer
from .crud import get_lnurldevice, get_lnurldevicepayment
templates = Jinja2Templates(directory="templates")
@lnurldevice_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/index.html", {"request": request, "user": user.dict()}
)
@lnurldevice_ext.get(
"/{paymentid}", name="lnurldevice.displaypin", response_class=HTMLResponse
)
async def displaypin(request: Request, paymentid: str = Query(None)):
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
if not lnurldevicepayment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="No lmurldevice payment"
)
device = await get_lnurldevice(lnurldevicepayment.deviceid)
if not device:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice not found."
)
status = await api_payment(lnurldevicepayment.payhash)
if status["paid"]:
await update_payment_status(checking_id=lnurldevicepayment.payhash, pending=True)
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/paid.html", {"request": request, "pin": lnurldevicepayment.pin}
)
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/error.html", {"request": request, "pin": "filler", "not_paid": True}
)

View File

@ -0,0 +1,85 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.extensions.lnurldevice import lnurldevice_ext
from lnbits.utils.exchange_rates import currencies
from . import lnurldevice_ext
from .crud import (
create_lnurldevice,
delete_lnurldevice,
get_lnurldevice,
get_lnurldevices,
update_lnurldevice,
)
from .models import createLnurldevice
@lnurldevice_ext.get("/api/v1/currencies")
async def api_list_currencies_available():
return list(currencies.keys())
#######################lnurldevice##########################
@lnurldevice_ext.post("/api/v1/lnurlpos")
@lnurldevice_ext.put("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_create_or_update(
data: createLnurldevice,
wallet: WalletTypeInfo = Depends(require_admin_key),
lnurldevice_id: str = Query(None),
):
if not lnurldevice_id:
lnurldevice = await create_lnurldevice(data)
return lnurldevice.dict()
else:
lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id)
return lnurldevice.dict()
@lnurldevice_ext.get("/api/v1/lnurlpos")
async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return [{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)]
except:
return ""
@lnurldevice_ext.get("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_retrieve(
request: Request,
wallet: WalletTypeInfo = Depends(get_key_type),
lnurldevice_id: str = Query(None),
):
lnurldevice = await get_lnurldevice(lnurldevice_id)
if not lnurldevice:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice does not exist"
)
if not lnurldevice.lnurl_toggle:
return {**lnurldevice.dict()}
return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(request=request)}}
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_delete(
wallet: WalletTypeInfo = Depends(require_admin_key), lnurldevice_id: str = Query(None)
):
lnurldevice = await get_lnurldevice(lnurldevice_id)
if not lnurldevice:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist."
)
await delete_lnurldevice(lnurldevice_id)
return "", HTTPStatus.NO_CONTENT

View File

@ -1,3 +0,0 @@
# LNURLPoS
For offline LNURL PoS devices

View File

@ -1,17 +0,0 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_lnurlpos")
lnurlpos_ext: APIRouter = APIRouter(prefix="/lnurlpos", tags=["lnurlpos"])
def lnurlpos_renderer():
return template_renderer(["lnbits/extensions/lnurlpos/templates"])
from .lnurl import * # noqa
from .views import * # noqa
from .views_api import * # noqa

View File

@ -1,6 +0,0 @@
{
"name": "LNURLPoS",
"short_description": "For offline LNURL PoS systems",
"icon": "point_of_sale",
"contributors": ["arcbtc"]
}

View File

@ -1,112 +0,0 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import createLnurlpos, lnurlpospayment, lnurlposs
###############lnurlposS##########################
async def create_lnurlpos(data: createLnurlpos,) -> lnurlposs:
lnurlpos_id = urlsafe_short_hash()
lnurlpos_key = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurlpos.lnurlposs (
id,
key,
title,
wallet,
currency
)
VALUES (?, ?, ?, ?, ?)
""",
(lnurlpos_id, lnurlpos_key, data.title, data.wallet, data.currency),
)
return await get_lnurlpos(lnurlpos_id)
async def update_lnurlpos(lnurlpos_id: str, **kwargs) -> Optional[lnurlposs]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurlpos.lnurlposs SET {q} WHERE id = ?",
(*kwargs.values(), lnurlpos_id),
)
row = await db.fetchone(
"SELECT * FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,)
)
return lnurlposs(**row) if row else None
async def get_lnurlpos(lnurlpos_id: str) -> lnurlposs:
row = await db.fetchone(
"SELECT * FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,)
)
return lnurlposs(**row) if row else None
async def get_lnurlposs(wallet_ids: Union[str, List[str]]) -> List[lnurlposs]:
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids[0]))
rows = await db.fetchall(
f"""
SELECT * FROM lnurlpos.lnurlposs WHERE wallet IN ({q})
ORDER BY id
""",
(*wallet_ids,),
)
return [lnurlposs(**row) if row else None for row in rows]
async def delete_lnurlpos(lnurlpos_id: str) -> None:
await db.execute("DELETE FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,))
########################lnulpos payments###########################
async def create_lnurlpospayment(
posid: str,
payload: Optional[str] = None,
pin: Optional[str] = None,
payhash: Optional[str] = None,
sats: Optional[int] = 0,
) -> lnurlpospayment:
lnurlpospayment_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurlpos.lnurlpospayment (
id,
posid,
payload,
pin,
payhash,
sats
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(lnurlpospayment_id, posid, payload, pin, payhash, sats),
)
return await get_lnurlpospayment(lnurlpospayment_id)
async def update_lnurlpospayment(
lnurlpospayment_id: str, **kwargs
) -> Optional[lnurlpospayment]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurlpos.lnurlpospayment SET {q} WHERE id = ?",
(*kwargs.values(), lnurlpospayment_id),
)
row = await db.fetchone(
"SELECT * FROM lnurlpos.lnurlpospayment WHERE id = ?", (lnurlpospayment_id,)
)
return lnurlpospayment(**row) if row else None
async def get_lnurlpospayment(lnurlpospayment_id: str) -> lnurlpospayment:
row = await db.fetchone(
"SELECT * FROM lnurlpos.lnurlpospayment WHERE id = ?", (lnurlpospayment_id,)
)
return lnurlpospayment(**row) if row else None

View File

@ -1,177 +0,0 @@
import base64
import hashlib
from http import HTTPStatus
from typing import Optional
from embit import bech32
from embit import compact
import base64
from io import BytesIO
import hmac
from fastapi import Request
from fastapi.param_functions import Query
from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from . import lnurlpos_ext
from .crud import (
create_lnurlpospayment,
get_lnurlpos,
get_lnurlpospayment,
update_lnurlpospayment,
)
def bech32_decode(bech):
"""tweaked version of bech32_decode that ignores length limitations"""
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
bech.lower() != bech and bech.upper() != bech
):
return
bech = bech.lower()
pos = bech.rfind("1")
if pos < 1 or pos + 7 > len(bech):
return
if not all(x in bech32.CHARSET for x in bech[pos + 1 :]):
return
hrp = bech[:pos]
data = [bech32.CHARSET.find(x) for x in bech[pos + 1 :]]
encoding = bech32.bech32_verify_checksum(hrp, data)
if encoding is None:
return
return bytes(bech32.convertbits(data[:-6], 5, 8, False))
def xor_decrypt(key, blob):
s = BytesIO(blob)
variant = s.read(1)[0]
if variant != 1:
raise RuntimeError("Not implemented")
# reading nonce
l = s.read(1)[0]
nonce = s.read(l)
if len(nonce) != l:
raise RuntimeError("Missing nonce bytes")
if l < 8:
raise RuntimeError("Nonce is too short")
# reading payload
l = s.read(1)[0]
payload = s.read(l)
if len(payload) > 32:
raise RuntimeError("Payload is too long for this encryption method")
if len(payload) != l:
raise RuntimeError("Missing payload bytes")
hmacval = s.read()
expected = hmac.new(
key, b"Data:" + blob[: -len(hmacval)], digestmod="sha256"
).digest()
if len(hmacval) < 8:
raise RuntimeError("HMAC is too short")
if hmacval != expected[: len(hmacval)]:
raise RuntimeError("HMAC is invalid")
secret = hmac.new(key, b"Round secret:" + nonce, digestmod="sha256").digest()
payload = bytearray(payload)
for i in range(len(payload)):
payload[i] = payload[i] ^ secret[i]
s = BytesIO(payload)
pin = compact.read_from(s)
amount_in_cent = compact.read_from(s)
return pin, amount_in_cent
@lnurlpos_ext.get(
"/api/v1/lnurl/{pos_id}",
status_code=HTTPStatus.OK,
name="lnurlpos.lnurl_v1_params",
)
async def lnurl_v1_params(
request: Request,
pos_id: str = Query(None),
p: str = Query(None),
):
pos = await get_lnurlpos(pos_id)
if not pos:
return {
"status": "ERROR",
"reason": f"lnurlpos {pos_id} not found on this server",
}
if len(p) % 4 > 0:
p += "=" * (4 - (len(p) % 4))
data = base64.urlsafe_b64decode(p)
pin = 0
amount_in_cent = 0
try:
result = xor_decrypt(pos.key.encode(), data)
pin = result[0]
amount_in_cent = result[1]
except Exception as exc:
return {"status": "ERROR", "reason": str(exc)}
price_msat = (
await fiat_amount_as_satoshis(float(amount_in_cent) / 100, pos.currency)
if pos.currency != "sat"
else amount_in_cent
) * 1000
lnurlpospayment = await create_lnurlpospayment(
posid=pos.id,
payload=p,
sats=price_msat,
pin=pin,
payhash="payment_hash",
)
if not lnurlpospayment:
return {"status": "ERROR", "reason": "Could not create payment."}
return {
"tag": "payRequest",
"callback": request.url_for(
"lnurlpos.lnurl_callback", paymentid=lnurlpospayment.id
),
"minSendable": price_msat,
"maxSendable": price_msat,
"metadata": await pos.lnurlpay_metadata(),
}
@lnurlpos_ext.get(
"/api/v1/lnurl/cb/{paymentid}",
status_code=HTTPStatus.OK,
name="lnurlpos.lnurl_callback",
)
async def lnurl_callback(request: Request, paymentid: str = Query(None)):
lnurlpospayment = await get_lnurlpospayment(paymentid)
pos = await get_lnurlpos(lnurlpospayment.posid)
if not pos:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="lnurlpos not found."
)
payment_hash, payment_request = await create_invoice(
wallet_id=pos.wallet,
amount=int(lnurlpospayment.sats / 1000),
memo=pos.title,
description_hash=hashlib.sha256(
(await pos.lnurlpay_metadata()).encode("utf-8")
).digest(),
extra={"tag": "lnurlpos"},
)
lnurlpospayment = await update_lnurlpospayment(
lnurlpospayment_id=paymentid, payhash=payment_hash
)
return {
"pr": payment_request,
"successAction": {
"tag": "url",
"description": "Check the attached link",
"url": request.url_for("lnurlpos.displaypin", paymentid=paymentid),
},
"routes": [],
}
return resp.dict()

View File

@ -1,30 +0,0 @@
async def m001_initial(db):
"""
Initial lnurlpos table.
"""
await db.execute(
f"""
CREATE TABLE lnurlpos.lnurlposs (
id TEXT NOT NULL PRIMARY KEY,
key TEXT NOT NULL,
title TEXT NOT NULL,
wallet TEXT NOT NULL,
currency TEXT NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
await db.execute(
f"""
CREATE TABLE lnurlpos.lnurlpospayment (
id TEXT NOT NULL PRIMARY KEY,
posid TEXT NOT NULL,
payhash TEXT,
payload TEXT NOT NULL,
pin INT,
sats INT,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)

View File

@ -1,50 +0,0 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.crud import update_payment_status
from lnbits.core.models import User
from lnbits.core.views.api import api_payment
from lnbits.decorators import check_user_exists
from . import lnurlpos_ext, lnurlpos_renderer
from .crud import get_lnurlpos, get_lnurlpospayment
templates = Jinja2Templates(directory="templates")
@lnurlpos_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return lnurlpos_renderer().TemplateResponse(
"lnurlpos/index.html", {"request": request, "user": user.dict()}
)
@lnurlpos_ext.get(
"/{paymentid}", name="lnurlpos.displaypin", response_class=HTMLResponse
)
async def displaypin(request: Request, paymentid: str = Query(None)):
lnurlpospayment = await get_lnurlpospayment(paymentid)
if not lnurlpospayment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="No lmurlpos payment"
)
pos = await get_lnurlpos(lnurlpospayment.posid)
if not pos:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos not found."
)
status = await api_payment(lnurlpospayment.payhash)
if status["paid"]:
await update_payment_status(checking_id=lnurlpospayment.payhash, pending=True)
return lnurlpos_renderer().TemplateResponse(
"lnurlpos/paid.html", {"request": request, "pin": lnurlpospayment.pin}
)
return lnurlpos_renderer().TemplateResponse(
"lnurlpos/error.html", {"request": request, "pin": "filler", "not_paid": True}
)

View File

@ -1,85 +0,0 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.extensions.lnurlpos import lnurlpos_ext
from lnbits.utils.exchange_rates import currencies
from . import lnurlpos_ext
from .crud import (
create_lnurlpos,
delete_lnurlpos,
get_lnurlpos,
get_lnurlposs,
update_lnurlpos,
)
from .models import createLnurlpos
@lnurlpos_ext.get("/api/v1/currencies")
async def api_list_currencies_available():
return list(currencies.keys())
#######################lnurlpos##########################
@lnurlpos_ext.post("/api/v1/lnurlpos")
@lnurlpos_ext.put("/api/v1/lnurlpos/{lnurlpos_id}")
async def api_lnurlpos_create_or_update(
data: createLnurlpos,
wallet: WalletTypeInfo = Depends(require_admin_key),
lnurlpos_id: str = Query(None),
):
if not lnurlpos_id:
lnurlpos = await create_lnurlpos(data)
return lnurlpos.dict()
else:
lnurlpos = await update_lnurlpos(data, lnurlpos_id=lnurlpos_id)
return lnurlpos.dict()
@lnurlpos_ext.get("/api/v1/lnurlpos")
async def api_lnurlposs_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return [{**lnurlpos.dict()} for lnurlpos in await get_lnurlposs(wallet_ids)]
except:
return ""
@lnurlpos_ext.get("/api/v1/lnurlpos/{lnurlpos_id}")
async def api_lnurlpos_retrieve(
request: Request,
wallet: WalletTypeInfo = Depends(get_key_type),
lnurlpos_id: str = Query(None),
):
lnurlpos = await get_lnurlpos(lnurlpos_id)
if not lnurlpos:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos does not exist"
)
if not lnurlpos.lnurl_toggle:
return {**lnurlpos.dict()}
return {**lnurlpos.dict(), **{"lnurl": lnurlpos.lnurl(request=request)}}
@lnurlpos_ext.delete("/api/v1/lnurlpos/{lnurlpos_id}")
async def api_lnurlpos_delete(
wallet: WalletTypeInfo = Depends(require_admin_key), lnurlpos_id: str = Query(None)
):
lnurlpos = await get_lnurlpos(lnurlpos_id)
if not lnurlpos:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist."
)
await delete_lnurlpos(lnurlpos_id)
return "", HTTPStatus.NO_CONTENT