diff --git a/lnbits/extensions/lnurlpos/README.md b/lnbits/extensions/lnurlpos/README.md
new file mode 100644
index 00000000..e7713055
--- /dev/null
+++ b/lnbits/extensions/lnurlpos/README.md
@@ -0,0 +1,3 @@
+# LNURLPoS
+
+For offline LNURL PoS devices
diff --git a/lnbits/extensions/lnurlpos/__init__.py b/lnbits/extensions/lnurlpos/__init__.py
new file mode 100644
index 00000000..4c86c827
--- /dev/null
+++ b/lnbits/extensions/lnurlpos/__init__.py
@@ -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
diff --git a/lnbits/extensions/lnurlpos/config.json b/lnbits/extensions/lnurlpos/config.json
new file mode 100644
index 00000000..2688e5a5
--- /dev/null
+++ b/lnbits/extensions/lnurlpos/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "LNURLPoS",
+ "short_description": "For offline LNURL PoS systems",
+ "icon": "point_of_sale",
+ "contributors": ["arcbtc"]
+}
diff --git a/lnbits/extensions/lnurlpos/crud.py b/lnbits/extensions/lnurlpos/crud.py
new file mode 100644
index 00000000..5a85fa33
--- /dev/null
+++ b/lnbits/extensions/lnurlpos/crud.py
@@ -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
diff --git a/lnbits/extensions/lnurlpos/lnurl.py b/lnbits/extensions/lnurlpos/lnurl.py
new file mode 100644
index 00000000..2cfbc4ba
--- /dev/null
+++ b/lnbits/extensions/lnurlpos/lnurl.py
@@ -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)
diff --git a/lnbits/extensions/lnurlpos/migrations.py b/lnbits/extensions/lnurlpos/migrations.py
new file mode 100644
index 00000000..011cb4a3
--- /dev/null
+++ b/lnbits/extensions/lnurlpos/migrations.py
@@ -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}
+ );
+ """
+ )
diff --git a/lnbits/extensions/lnurlpos/models.py b/lnbits/extensions/lnurlpos/models.py
new file mode 100644
index 00000000..b6924593
--- /dev/null
+++ b/lnbits/extensions/lnurlpos/models.py
@@ -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))
diff --git a/lnbits/extensions/lnurlpos/templates/lnurlpos/_api_docs.html b/lnbits/extensions/lnurlpos/templates/lnurlpos/_api_docs.html
new file mode 100644
index 00000000..071d6d6c
--- /dev/null
+++ b/lnbits/extensions/lnurlpos/templates/lnurlpos/_api_docs.html
@@ -0,0 +1,158 @@
+
+ Register LNURLPoS devices to recieve payments in your LNbits wallet.
+ Build your own here
+ https://github.com/arcbtc/LNURLPoS
+
+ Created by, Ben Arc
+ POST /lnurlpos/api/v1/lnurlpos
+ Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<lnurlpos_object>, ...]
+ Curl example
+ curl -X POST {{ request.url_root }}api/v1/lnurlpos -d '{"title":
+ <string>, "message":<string>, "currency":
+ <integer>}' -H "Content-type: application/json" -H "X-Api-Key:
+ {{user.wallets[0].adminkey }}"
+
+ PUT
+ /lnurlpos/api/v1/lnurlpos/<lnurlpos_id>
+ Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<lnurlpos_object>, ...]
+ Curl example
+ curl -X POST {{ request.url_root
+ }}api/v1/lnurlpos/<lnurlpos_id> -d ''{"title": <string>,
+ "message":<string>, "currency": <integer>} -H
+ "Content-type: application/json" -H "X-Api-Key:
+ {{user.wallets[0].adminkey }}"
+
+ GET
+ /lnurlpos/api/v1/lnurlpos/<lnurlpos_id>
+ Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<lnurlpos_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root
+ }}api/v1/lnurlpos/<lnurlpos_id> -H "X-Api-Key: {{
+ user.wallets[0].inkey }}"
+
+ GET /lnurlpos/api/v1/lnurlposs
+ Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<lnurlpos_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}api/v1/lnurlposs -H "X-Api-Key:
+ {{ user.wallets[0].inkey }}"
+
+ DELETE
+ /lnurlpos/api/v1/lnurlpos/<lnurlpos_id>
+ Headers
+ {"X-Api-Key": <admin_key>}
+ Returns 204 NO CONTENT
+
+ Curl example
+ curl -X DELETE {{ request.url_root
+ }}api/v1/lnurlpos/<lnurlpos_id> -H "X-Api-Key: {{
+ user.wallets[0].adminkey }}"
+
+