LNURLPoS loading, form working

This commit is contained in:
Ben Arc 2021-10-20 06:49:59 +01:00
parent 37b14e3d6d
commit 552fa8edc6
13 changed files with 1190 additions and 0 deletions

View file

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

View file

@ -0,0 +1,20 @@
import asyncio
from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.routing import Mount
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_lnurlpos")
lnurlpos_ext: APIRouter = APIRouter(prefix="/lnurlpos", tags=["lnurlpos"])
def lnurlpos_renderer():
return template_renderer(["lnbits/extensions/lnurlpos/templates"])
from .views_api import * # noqa
from .views import * # noqa
from .lnurl import * # noqa

View file

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

View file

@ -0,0 +1,113 @@
from datetime import datetime
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from typing import List, Optional
from . import db
from .models import lnurlposs, lnurlpospayment, createLnurlpos
###############lnurlposS##########################
async def create_lnurlpos(
data: createLnurlpos,
) -> lnurlposs:
print(data)
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.from_row(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.from_row(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.from_row(row) 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,
sats: Optional[int] = 0,
) -> lnurlpospayment:
lnurlpospayment_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurlpos.lnurlpospayment (
id,
posid,
payload,
pin,
sats
)
VALUES (?, ?, ?, ?, ?)
""",
(lnurlpospayment_id, posid, payload, pin, 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.from_row(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.from_row(row) if row else None

View file

@ -0,0 +1,108 @@
import json
import hashlib
import math
from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore
from lnurl.types import LnurlPayMetadata
from lnbits.core.services import create_invoice
from hashlib import md5
from fastapi import Request
from fastapi.param_functions import Query
from . import lnurlpos_ext
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from http import HTTPStatus
from fastapi.params import Depends
from fastapi.param_functions import Query
from .crud import (
get_lnurlpos,
create_lnurlpospayment,
get_lnurlpospayment,
update_lnurlpospayment,
)
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
@lnurlpos_ext.get("/api/v1/lnurl/{nonce}/{payload}/{pos_id}")
async def lnurl_response(
request: Request,
nonce: str = Query(None),
pos_id: str = Query(None),
payload: str = Query(None),
):
pos = await get_lnurlpos(pos_id)
if not pos:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos not found."
)
nonce1 = bytes.fromhex(nonce)
payload1 = bytes.fromhex(payload)
h = hashlib.sha256(nonce1)
h.update(pos.key.encode())
s = h.digest()
res = bytearray(payload1)
for i in range(len(res)):
res[i] = res[i] ^ s[i]
decryptedAmount = float(int.from_bytes(res[2:6], "little") / 100)
decryptedPin = int.from_bytes(res[:2], "little")
if type(decryptedAmount) != float:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not an amount.")
price_msat = (
await fiat_amount_as_satoshis(decryptedAmount, pos.currency)
if pos.currency != "sat"
else pos.currency
) * 1000
lnurlpospayment = await create_lnurlpospayment(
posid=pos.id, payload=payload, sats=price_msat, pin=decryptedPin
)
if not lnurlpospayment:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Could not create payment"
)
payResponse = {
"tag": "payRequest",
"callback": request.url_for(
"lnurlpos.lnurl_callback",
paymentid=lnurlpospayment.id,
),
"metadata": LnurlPayMetadata(json.dumps([["text/plain", str(pos.title)]])),
"minSendable": price_msat,
"maxSendable": price_msat,
}
return json.dumps(payResponse)
@lnurlpos_ext.get("/api/v1/lnurl/cb/{paymentid}")
async def lnurl_callback(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(
(LnurlPayMetadata(json.dumps([["text/plain", str(pos.title)]]))).encode(
"utf-8"
)
).digest(),
extra={"tag": "lnurlpos"},
)
lnurlpospayment = await update_lnurlpospayment(
lnurlpospayment_id=paymentid, payhash=payment_hash
)
success_action = pos.success_action(paymentid)
payResponse = {
"pr": payment_request,
"success_action": success_action,
"disposable": False,
"routes": [],
}
return json.dumps(payResponse)

View file

@ -0,0 +1,30 @@
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

@ -0,0 +1,65 @@
import json
from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode # type: ignore
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
from lnurl.types import LnurlPayMetadata # type: ignore
from sqlite3 import Row
from typing import NamedTuple, Optional, Dict
import shortuuid # type: ignore
from fastapi.param_functions import Query
from pydantic.main import BaseModel
from pydantic import BaseModel
from typing import Optional
from fastapi import FastAPI, Request
class createLnurlpos(BaseModel):
title: str
wallet: str
currency: str
class lnurlposs(BaseModel):
id: str
key: str
title: str
wallet: str
currency: str
timestamp: str
@classmethod
def from_row(cls, row: Row) -> "lnurlposs":
return cls(**dict(row))
@property
def lnurl(self) -> Lnurl:
url = url_for("lnurlpos.lnurl_response", pos_id=self.id, _external=True)
return lnurl_encode(url)
@property
def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
def success_action(self, paymentid: str, req: Request) -> Optional[Dict]:
url = url_for(
"lnurlpos.displaypin",
paymentid=paymentid,
)
return {
"tag": "url",
"description": "Check the attached link",
"url": url,
}
class lnurlpospayment(BaseModel):
id: str
posid: str
payhash: str
payload: str
pin: int
sats: int
timestamp: str
@classmethod
def from_row(cls, row: Row) -> "lnurlpospayment":
return cls(**dict(row))

View file

@ -0,0 +1,158 @@
<q-card>
<q-card-section>
<p>
Register LNURLPoS devices to recieve payments in your LNbits wallet.<br />
Build your own here
<a href="https://github.com/arcbtc/LNURLPoS"
>https://github.com/arcbtc/LNURLPoS</a
><br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item
group="api"
dense
expand-separator
label="Create lnurlpos"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">POST</span> /lnurlpos/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 />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}api/v1/lnurlpos -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="Update lnurlpos"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">PUT</span>
/lnurlpos/api/v1/lnurlpos/&lt;lnurlpos_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 />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root
}}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:
{{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-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/lnurlpos/api/v1/lnurlpos/&lt;lnurlpos_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 />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}api/v1/lnurlpos/&lt;lnurlpos_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-card>
<q-card-section>
<code
><span class="text-blue">GET</span> /lnurlpos/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 />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}api/v1/lnurlposs -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="Delete a pay link"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/lnurlpos/api/v1/lnurlpos/&lt;lnurlpos_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 />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.url_root
}}api/v1/lnurlpos/&lt;lnurlpos_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
</q-card>

View file

@ -0,0 +1,34 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">LNURL-pay not paid</h3>
<br />
<q-icon
name="warning"
class="text-grey"
style="font-size: 20rem"
></q-icon>
<br />
</center>
</q-card-section>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
}
})
</script>
{% endblock %}
</div>

View file

@ -0,0 +1,472 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
{% raw %}
<q-btn
unelevated
color="primary"
@click="formDialoglnurlpos.show = true"
>New LNURLPoS instance
</q-btn>
</q-card-section>
</q-card>
<q-card>
<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>
</div>
<div class="col-auto">
<q-input
borderless
dense
debounce="300"
v-model="filter"
placeholder="Search"
>
<template v-slot:append>
<q-icon name="search"></q-icon>
</template>
</q-input>
<q-btn flat color="grey" @click="exportlnurlposCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
flat
dense
:data="lnurlposLinks"
row-key="id"
:columns="lnurlpossTable.columns"
:pagination.sync="lnurlpossTable.pagination"
:filter="filter"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th style="width: 5%"></q-th>
<q-th style="width: 5%"></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.label }}</div>
</q-th>
<!-- <q-th auto-width></q-th> -->
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td>
<q-btn
flat
dense
size="xs"
@click="deletelnurlposLink(props.row.id)"
icon="cancel"
color="pink"
>
<q-tooltip> Delete LNURLPoS </q-tooltip>
</q-btn>
</q-td>
<q-td>
<q-btn
flat
dense
size="xs"
@click="openlnurlposSettings(props.row.id)"
icon="perm_data_setting"
color="primary"
>
<q-tooltip> LNURLPoS Settings </q-tooltip>
</q-btn>
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.value }}</div>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} LNURLPoS 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-card-section>
</q-card>
</div>
<q-dialog
v-model="settingsDialog.show"
position="top"
@hide="closeFormDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-h6">Copy to LNURLPoS device</div>
<div class="text-subtitle2">
{% raw %} String server = "{{location}}";<br />
String posId = "{{settingsDialog.data.id}}";<br />
String key = "{{settingsDialog.data.key}}";<br />
String currency = "{{settingsDialog.data.currency}}";{% endraw %}
</div>
</q-card>
</q-dialog>
<q-dialog
v-model="formDialoglnurlpos.show"
position="top"
@hide="closeFormDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormDatalnurlpos" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialoglnurlpos.data.title"
type="text"
label="Title"
></q-input>
<q-select
filled
dense
emit-value
v-model="formDialoglnurlpos.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-select
filled
dense
v-model.trim="formDialoglnurlpos.data.currency"
type="text"
label="Fiat currency for PoS"
:options="currency"
></q-select>
<div class="row q-mt-lg">
<q-btn
v-if="formDialoglnurlpos.data.id"
unelevated
color="primary"
:disable="
formDialoglnurlpos.data.title == ''"
type="submit"
>Update lnurlpos</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="
formDialoglnurlpos.data.title == ''"
type="submit"
>Create lnurlpos</q-btn
>
<q-btn @click="cancellnurlpos" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
Vue.component(VueQrcode.name, VueQrcode)
var maplnurlpos = obj => {
obj._data = _.clone(obj)
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
obj.time = obj.time + 'mins'
if (obj.time_elapsed) {
obj.date = 'Time elapsed'
} else {
obj.date = Quasar.utils.date.formatDate(
new Date((obj.theTime - 3600) * 1000),
'HH:mm:ss'
)
}
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
location: window.location.hostname,
filter: '',
currency: 'USD',
lnurlposLinks: [],
lnurlposLinksObj: [],
lnurlpossTable: {
columns: [
{
name: 'title',
align: 'left',
label: 'title',
field: 'title'
},
{
name: 'theId',
align: 'left',
label: 'id',
field: 'id'
},
{
name: 'key',
align: 'left',
label: 'key',
field: 'key'
},
{
name: 'wallet',
align: 'left',
label: 'wallet',
field: 'wallet'
},
{
name: 'currency',
align: 'left',
label: 'currency',
field: 'currency'
}
],
pagination: {
rowsPerPage: 10
}
},
passedlnurlpos: {},
settingsDialog: {
show: false,
data: {}
},
formDialog: {
show: false,
data: {}
},
formDialoglnurlpos: {
show: false,
data: {
lnurl_toggle: false,
show_message: false,
show_ack: false,
show_price: 'None',
title: ''
}
},
qrCodeDialog: {
show: false,
data: null
}
}
},
methods: {
cancellnurlpos: function (data) {
var self = this
self.formDialoglnurlpos.show = false
self.clearFormDialoglnurlpos()
},
closeFormDialog: function () {
this.clearFormDialoglnurlpos()
this.formDialog.data = {
is_unique: false
}
},
sendFormDatalnurlpos: function () {
var self = this
if (self.formDialoglnurlpos.data.id) {
this.updatelnurlpos(
self.g.user.wallets[0].adminkey,
self.formDialoglnurlpos.data
)
} else {
this.createlnurlpos(
self.g.user.wallets[0].adminkey,
self.formDialoglnurlpos.data
)
}
},
createlnurlpos: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
if (data[property]) {
updatedData[property] = data[property]
}
}
LNbits.api
.request('POST', '/lnurlpos/api/v1/lnurlpos', wallet, updatedData)
.then(function (response) {
self.lnurlposLinks.push(maplnurlpos(response.data))
self.formDialoglnurlpos.show = false
self.clearFormDialoglnurlpos()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getlnurlposs: function () {
var self = this
LNbits.api
.request(
'GET',
'/lnurlpos/api/v1/lnurlpos',
self.g.user.wallets[0].adminkey
)
.then(function (response) {
if (response.data) {
console.log(response.data)
self.lnurlposLinks = response.data.map(maplnurlpos)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getlnurlpos: function (lnurlpos_id) {
var self = this
LNbits.api
.request(
'GET',
'/lnurlpos/api/v1/lnurlpos/' + lnurlpos_id,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
localStorage.setItem('lnurlpos', JSON.stringify(response.data))
localStorage.setItem('inkey', self.g.user.wallets[0].inkey)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deletelnurlposLink: function (lnurlposId) {
var self = this
var link = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this pay link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnurlpos/api/v1/lnurlpos/' + lnurlposId,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.lnurlposLinks = _.reject(self.lnurlposLinks, function (
obj
) {
return obj.id === lnurlposId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
openUpdatelnurlposLink: function (lnurlposId) {
var self = this
var lnurlpos = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
self.formDialoglnurlpos.data = _.clone(lnurlpos._data)
self.formDialoglnurlpos.show = true
},
openlnurlposSettings: function (lnurlposId) {
var self = this
var lnurlpos = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
self.settingsDialog.data = _.clone(lnurlpos._data)
self.settingsDialog.show = true
},
updatelnurlpos: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
if (data[property]) {
updatedData[property] = data[property]
}
}
LNbits.api
.request(
'PUT',
'/lnurlpos/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()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
clearFormDialoglnurlpos() {
this.formDialoglnurlpos.data = {
lnurl_toggle: false,
show_message: false,
show_ack: false,
show_price: 'None',
title: ''
}
},
exportlnurlposCSV: function () {
var self = this
LNbits.utils.exportCSV(self.lnurlpossTable.columns, this.lnurlposLinks)
}
},
created: function () {
var self = this
var getlnurlposs = this.getlnurlposs
getlnurlposs()
self.location = [
window.location.protocol,
'//',
window.location.host
].join('')
LNbits.api
.request('GET', '/api/v1/currencies')
.then(response => {
this.currency = ['USD', ...response.data]
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}
})
</script>
{% endblock %}

View file

@ -0,0 +1,27 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ pin }}</h3>
<br />
</center>
</q-card-section>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
}
})
</script>
{% endblock %}
</div>

View file

@ -0,0 +1,60 @@
from http import HTTPStatus
import httpx
from collections import defaultdict
from lnbits.decorators import check_user_exists
from .crud import get_lnurlpos, get_lnurlpospayment
from functools import wraps
from lnbits.core.crud import get_standalone_payment
import hashlib
from lnbits.core.services import check_invoice_status
from lnbits.core.crud import update_payment_status
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from fastapi.params import Depends
from fastapi.param_functions import Query
import random
from datetime import datetime
from http import HTTPStatus
from . import lnurlpos_ext, lnurlpos_renderer
from lnbits.core.models import User, Payment
templates = Jinja2Templates(directory="templates")
@lnurlpos_ext.get("/")
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}")
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 check_invoice_status(pos.wallet, lnurlpospayment.payhash)
is_paid = not status.pending
if not is_paid:
return lnurlpos_renderer().TemplateResponse(
"lnurlpos/error.html",
{"request": request, "pin": "filler", "not_paid": True},
)
await update_payment_status(checking_id=lnurlpospayment.payhash, pending=True)
return lnurlpos_renderer().TemplateResponse(
"lnurlpos/paid.html", {"request": request, "pin": lnurlpospayment.pin}
)

View file

@ -0,0 +1,94 @@
import hashlib
from fastapi import FastAPI, Request
from fastapi.params import Depends
from http import HTTPStatus
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from fastapi.params import Depends
from fastapi.param_functions import Query
from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type
from lnbits.core.crud import get_user
from lnbits.core.models import User, Payment
from . import lnurlpos_ext
from lnbits.extensions.lnurlpos import lnurlpos_ext
from .crud import (
create_lnurlpos,
update_lnurlpos,
get_lnurlpos,
get_lnurlposs,
delete_lnurlpos,
)
from lnbits.utils.exchange_rates import currencies
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(
request: Request,
data: createLnurlpos,
wallet: WalletTypeInfo = Depends(get_key_type),
lnurlpos_id: str = Query(None),
):
if not lnurlpos_id:
lnurlpos = await create_lnurlpos(data)
print(lnurlpos.dict())
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(
request: Request, 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(
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="Wallet link does not exist."
)
await delete_lnurlpos(lnurlpos_id)
return "", HTTPStatus.NO_CONTENT