diff --git a/lnbits/extensions/satspay/README.md b/lnbits/extensions/satspay/README.md
new file mode 100644
index 00000000..d52547ae
--- /dev/null
+++ b/lnbits/extensions/satspay/README.md
@@ -0,0 +1,27 @@
+# SatsPay Server
+
+## Create onchain and LN charges. Includes webhooks!
+
+Easilly create invoices that support Lightning Network and on-chain BTC payment.
+
+1. Create a "NEW CHARGE"\
+ 
+2. Fill out the invoice fields
+ - set a descprition for the payment
+ - the amount in sats
+ - the time, in minutes, the invoice is valid for, after this period the invoice can't be payed
+ - set a webhook that will get the transaction details after a successful payment
+ - set to where the user should redirect after payment
+ - set the text for the button that will show after payment (not setting this, will display "NONE" in the button)
+ - select if you want onchain payment, LN payment or both
+ - depending on what you select you'll have to choose the respective wallets where to receive your payment\
+ 
+3. The charge will appear on the _Charges_ section\
+ 
+4. Your costumer/payee will get the payment page
+ - they can choose to pay on LN\
+ 
+ - or pay on chain\
+ 
+5. You can check the state of your charges in LNBits\
+ 
diff --git a/lnbits/extensions/satspay/__init__.py b/lnbits/extensions/satspay/__init__.py
new file mode 100644
index 00000000..7b7f0bde
--- /dev/null
+++ b/lnbits/extensions/satspay/__init__.py
@@ -0,0 +1,25 @@
+import asyncio
+
+from fastapi import APIRouter
+
+from lnbits.db import Database
+from lnbits.helpers import template_renderer
+
+db = Database("ext_satspay")
+
+
+satspay_ext: APIRouter = APIRouter(
+ prefix="/satspay",
+ tags=["satspay"]
+)
+
+def satspay_renderer():
+ return template_renderer(
+ [
+ "lnbits/extensions/satspay/templates",
+ ]
+ )
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/satspay/config.json b/lnbits/extensions/satspay/config.json
new file mode 100644
index 00000000..beb0071c
--- /dev/null
+++ b/lnbits/extensions/satspay/config.json
@@ -0,0 +1,8 @@
+{
+ "name": "SatsPay Server",
+ "short_description": "Create onchain and LN charges",
+ "icon": "payment",
+ "contributors": [
+ "arcbtc"
+ ]
+}
diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py
new file mode 100644
index 00000000..e707dc00
--- /dev/null
+++ b/lnbits/extensions/satspay/crud.py
@@ -0,0 +1,122 @@
+from typing import List, Optional, Union
+
+# from lnbits.db import open_ext_db
+from . import db
+from .models import Charges, CreateCharge
+
+from lnbits.helpers import urlsafe_short_hash
+
+import httpx
+from lnbits.core.services import create_invoice, check_invoice_status
+from ..watchonly.crud import get_watch_wallet, get_fresh_address, get_mempool
+
+
+###############CHARGES##########################
+
+
+async def create_charge(
+ user: str,
+ data: CreateCharge
+) -> Charges:
+ charge_id = urlsafe_short_hash()
+ if data.onchainwallet:
+ wallet = await get_watch_wallet(data.onchainwallet)
+ onchain = await get_fresh_address(data.onchainwallet)
+ onchainaddress = onchain.address
+ else:
+ onchainaddress = None
+ if data.lnbitswallet:
+ payment_hash, payment_request = await create_invoice(
+ wallet_id=data.lnbitswallet, amount=data.amount, memo=charge_id
+ )
+ else:
+ payment_hash = None
+ payment_request = None
+ await db.execute(
+ """
+ INSERT INTO satspay.charges (
+ id,
+ "user",
+ description,
+ onchainwallet,
+ onchainaddress,
+ lnbitswallet,
+ payment_request,
+ payment_hash,
+ webhook,
+ completelink,
+ completelinktext,
+ time,
+ amount,
+ balance
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ charge_id,
+ user,
+ data.description,
+ data.onchainwallet,
+ onchainaddress,
+ data.lnbitswallet,
+ payment_request,
+ payment_hash,
+ data.webhook,
+ data.completelink,
+ data.completelinktext,
+ data.time,
+ data.amount,
+ 0,
+ ),
+ )
+ return await get_charge(charge_id)
+
+
+async def update_charge(charge_id: str, **kwargs) -> Optional[Charges]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+ await db.execute(
+ f"UPDATE satspay.charges SET {q} WHERE id = ?", (*kwargs.values(), charge_id)
+ )
+ row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
+ return Charges.from_row(row) if row else None
+
+
+async def get_charge(charge_id: str) -> Charges:
+ row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
+ return Charges.from_row(row) if row else None
+
+
+async def get_charges(user: str) -> List[Charges]:
+ rows = await db.fetchall(
+ """SELECT * FROM satspay.charges WHERE "user" = ?""", (user,)
+ )
+ return [Charges.from_row(row) for row in rows]
+
+
+async def delete_charge(charge_id: str) -> None:
+ await db.execute("DELETE FROM satspay.charges WHERE id = ?", (charge_id,))
+
+
+async def check_address_balance(charge_id: str) -> List[Charges]:
+ charge = await get_charge(charge_id)
+ if not charge.paid:
+ if charge.onchainaddress:
+ mempool = await get_mempool(charge.user)
+ try:
+ async with httpx.AsyncClient() as client:
+ r = await client.get(
+ mempool.endpoint + "/api/address/" + charge.onchainaddress
+ )
+ respAmount = r.json()["chain_stats"]["funded_txo_sum"]
+ if respAmount >= charge.balance:
+ await update_charge(charge_id=charge_id, balance=respAmount)
+ except Exception:
+ pass
+ if charge.lnbitswallet:
+ invoice_status = await check_invoice_status(
+ charge.lnbitswallet, charge.payment_hash
+ )
+ if invoice_status.paid:
+ return await update_charge(charge_id=charge_id, balance=charge.amount)
+ row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
+ return Charges.from_row(row) if row else None
diff --git a/lnbits/extensions/satspay/migrations.py b/lnbits/extensions/satspay/migrations.py
new file mode 100644
index 00000000..87446c80
--- /dev/null
+++ b/lnbits/extensions/satspay/migrations.py
@@ -0,0 +1,28 @@
+async def m001_initial(db):
+ """
+ Initial wallet table.
+ """
+
+ await db.execute(
+ """
+ CREATE TABLE satspay.charges (
+ id TEXT NOT NULL PRIMARY KEY,
+ "user" TEXT,
+ description TEXT,
+ onchainwallet TEXT,
+ onchainaddress TEXT,
+ lnbitswallet TEXT,
+ payment_request TEXT,
+ payment_hash TEXT,
+ webhook TEXT,
+ completelink TEXT,
+ completelinktext TEXT,
+ time INTEGER,
+ amount INTEGER,
+ balance INTEGER DEFAULT 0,
+ timestamp TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
diff --git a/lnbits/extensions/satspay/models.py b/lnbits/extensions/satspay/models.py
new file mode 100644
index 00000000..4cf3efad
--- /dev/null
+++ b/lnbits/extensions/satspay/models.py
@@ -0,0 +1,50 @@
+from sqlite3 import Row
+from typing import Optional
+from fastapi.param_functions import Query
+from pydantic import BaseModel
+import time
+
+class CreateCharge(BaseModel):
+ onchainwallet: str = Query(None)
+ lnbitswallet: str = Query(None)
+ description: str = Query(...)
+ webhook: str = Query(None)
+ completelink: str = Query(None)
+ completelinktext: str = Query(None)
+ time: int = Query(..., ge=1)
+ amount: int = Query(..., ge=1)
+
+class Charges(BaseModel):
+ id: str
+ user: str
+ description: Optional[str]
+ onchainwallet: Optional[str]
+ onchainaddress: Optional[str]
+ lnbitswallet: Optional[str]
+ payment_request: str
+ payment_hash: str
+ webhook: Optional[str]
+ completelink: Optional[str]
+ completelinktext: Optional[str] = "Back to Merchant"
+ time: int
+ amount: int
+ balance: int
+ timestamp: int
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Charges":
+ return cls(**dict(row))
+
+ @property
+ def time_elapsed(self):
+ if (self.timestamp + (self.time * 60)) >= time.time():
+ return False
+ else:
+ return True
+
+ @property
+ def paid(self):
+ if self.balance >= self.amount:
+ return True
+ else:
+ return False
diff --git a/lnbits/extensions/satspay/templates/satspay/_api_docs.html b/lnbits/extensions/satspay/templates/satspay/_api_docs.html
new file mode 100644
index 00000000..af95cbf2
--- /dev/null
+++ b/lnbits/extensions/satspay/templates/satspay/_api_docs.html
@@ -0,0 +1,171 @@
+
+
+
+ SatsPayServer, create Onchain/LN charges.
WARNING: If using with the
+ WatchOnly extension, we highly reccomend using a fresh extended public Key
+ specifically for SatsPayServer!
+
+ Created by, Ben Arc
+
+
+
+
+
+
+ POST /satspay/api/v1/charge
+ Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<charge_object>, ...]
+ Curl example
+ curl -X POST {{ request.url_root }}api/v1/charge -d
+ '{"onchainwallet": <string, watchonly_wallet_id>,
+ "description": <string>, "webhook":<string>, "time":
+ <integer>, "amount": <integer>, "lnbitswallet":
+ <string, lnbits_wallet_id>}' -H "Content-type:
+ application/json" -H "X-Api-Key: {{user.wallets[0].adminkey }}"
+
+
+
+
+
+
+
+ PUT
+ /satspay/api/v1/charge/<charge_id>
+ Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<charge_object>, ...]
+ Curl example
+ curl -X POST {{ request.url_root }}api/v1/charge/<charge_id>
+ -d '{"onchainwallet": <string, watchonly_wallet_id>,
+ "description": <string>, "webhook":<string>, "time":
+ <integer>, "amount": <integer>, "lnbitswallet":
+ <string, lnbits_wallet_id>}' -H "Content-type:
+ application/json" -H "X-Api-Key: {{user.wallets[0].adminkey }}"
+
+
+
+
+
+
+
+
+ GET
+ /satspay/api/v1/charge/<charge_id>
+ Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<charge_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}api/v1/charge/<charge_id>
+ -H "X-Api-Key: {{ user.wallets[0].inkey }}"
+
+
+
+
+
+
+
+ GET /satspay/api/v1/charges
+ Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<charge_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}api/v1/charges -H "X-Api-Key: {{
+ user.wallets[0].inkey }}"
+
+
+
+
+
+
+
+ DELETE
+ /satspay/api/v1/charge/<charge_id>
+ Headers
+ {"X-Api-Key": <admin_key>}
+ Returns 204 NO CONTENT
+
+ Curl example
+ curl -X DELETE {{ request.url_root
+ }}api/v1/charge/<charge_id> -H "X-Api-Key: {{
+ user.wallets[0].adminkey }}"
+
+
+
+
+
+
+
+ GET
+ /satspay/api/v1/charges/balance/<charge_id>
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<charge_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root
+ }}api/v1/charges/balance/<charge_id> -H "X-Api-Key: {{
+ user.wallets[0].inkey }}"
+
+
+
+
+
+
diff --git a/lnbits/extensions/satspay/templates/satspay/display.html b/lnbits/extensions/satspay/templates/satspay/display.html
new file mode 100644
index 00000000..5b0282b6
--- /dev/null
+++ b/lnbits/extensions/satspay/templates/satspay/display.html
@@ -0,0 +1,319 @@
+{% extends "public.html" %} {% block page %}
+
+
+
+
+ {{ charge.description }}
+
+
+
+
Time elapsed
+
+
+
Charge paid
+
+
+
+
+
+ Awaiting payment...
+
+ {% raw %} {{ newTimeLeft }} {% endraw %}
+
+
+
+
+
+
+
+ Charge ID: {{ charge.id }}
+
+ {% raw %} Total to pay: {{ charge_amount }}sats
+ Amount paid: {{ charge_balance }}
+ Amount due: {{ charge_amount - charge_balance }}sats {% endraw %}
+
+
+
+
+
+
+
+
+ bitcoin onchain payment method not available
+
+
+
+ pay with lightning
+
+
+
+
+
+ bitcoin lightning payment method not available
+
+
+
+ pay onchain
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pay this
+ lightning-network invoice
+
+
+
+
+
+
+
+ Copy invoice
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Send {{ charge.amount }}sats
+ to this onchain address
+
+
+
+
+
+
+
+ Copy address
+
+
+
+
+
+
+
+
+{% endblock %} {% block scripts %}
+
+
+
+{% endblock %}
diff --git a/lnbits/extensions/satspay/templates/satspay/index.html b/lnbits/extensions/satspay/templates/satspay/index.html
new file mode 100644
index 00000000..d941e90b
--- /dev/null
+++ b/lnbits/extensions/satspay/templates/satspay/index.html
@@ -0,0 +1,557 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
+
+
+
+
+ {% raw %}
+ New charge
+
+
+
+
+
+
+
+
+
Charges
+
+
+
+
+
+
+
+
+ Export to CSV
+
+
+
+
+
+
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+ Payment link
+
+
+
+
+ Time elapsed
+
+
+
+ PAID!
+
+
+
+ Processing
+
+
+ Delete charge
+
+
+
+
+ {{ col.value }}
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
+ {{SITE_TITLE}} satspay Extension
+
+
+
+
+ {% include "satspay/_api_docs.html" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Watch-Only extension MUST be activated and have a wallet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create Charge
+ Cancel
+
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }}
+
+
+
+{% endblock %}
diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py
new file mode 100644
index 00000000..020b5897
--- /dev/null
+++ b/lnbits/extensions/satspay/views.py
@@ -0,0 +1,30 @@
+from fastapi.param_functions import Depends
+from starlette.exceptions import HTTPException
+from starlette.responses import HTMLResponse
+from starlette.requests import Request
+from lnbits.core.models import User
+from lnbits.core.crud import get_wallet
+from lnbits.decorators import check_user_exists
+from http import HTTPStatus
+
+from fastapi.templating import Jinja2Templates
+
+from . import satspay_ext, satspay_renderer
+from .crud import get_charge
+
+templates = Jinja2Templates(directory="templates")
+
+@satspay_ext.get("/", response_class=HTMLResponse)
+async def index(request: Request, user: User = Depends(check_user_exists)):
+ return satspay_renderer().TemplateResponse("satspay/index.html", {"request": request,"user": user.dict()})
+
+
+@satspay_ext.get("/{charge_id}", response_class=HTMLResponse)
+async def display(request: Request, charge_id):
+ charge = await get_charge(charge_id)
+ if not charge:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Charge link does not exist."
+ )
+ return satspay_renderer().TemplateResponse("satspay/display.html", {"request": request, "charge": charge})
diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py
new file mode 100644
index 00000000..0bfe5f04
--- /dev/null
+++ b/lnbits/extensions/satspay/views_api.py
@@ -0,0 +1,142 @@
+import hashlib
+
+from http import HTTPStatus
+import httpx
+
+from fastapi import Query
+from fastapi.params import Depends
+
+from starlette.exceptions import HTTPException
+from starlette.requests import Request
+from starlette.responses import HTMLResponse, JSONResponse # type: ignore
+
+
+from lnbits.core.crud import get_user
+from lnbits.decorators import WalletTypeInfo, get_key_type
+
+from lnbits.extensions.satspay import satspay_ext
+from .models import CreateCharge
+from .crud import (
+ create_charge,
+ update_charge,
+ get_charge,
+ get_charges,
+ delete_charge,
+ check_address_balance,
+)
+
+#############################CHARGES##########################
+
+
+@satspay_ext.post("/api/v1/charge")
+@satspay_ext.put("/api/v1/charge/{charge_id}")
+
+async def api_charge_create_or_update(data: CreateCharge, wallet: WalletTypeInfo = Depends(get_key_type), charge_id=None):
+ if not charge_id:
+ charge = await create_charge(user=wallet.wallet.user, data=data)
+ return charge.dict()
+ else:
+ charge = await update_charge(charge_id=charge_id, data=data)
+ return charge.dict()
+
+
+@satspay_ext.get("/api/v1/charges")
+async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
+ try:
+ return [
+ {
+ **charge.dict(),
+ **{"time_elapsed": charge.time_elapsed},
+ **{"paid": charge.paid},
+ }
+ for charge in await get_charges(wallet.wallet.user)
+ ]
+ except:
+ return ""
+
+
+@satspay_ext.get("/api/v1/charge/{charge_id}")
+async def api_charge_retrieve(charge_id, wallet: WalletTypeInfo = Depends(get_key_type)):
+ charge = await get_charge(charge_id)
+
+ if not charge:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Charge does not exist."
+ )
+
+ return {
+ **charge.dict(),
+ **{"time_elapsed": charge.time_elapsed},
+ **{"paid": charge.paid},
+ }
+
+
+@satspay_ext.delete("/api/v1/charge/{charge_id}")
+async def api_charge_delete(charge_id, wallet: WalletTypeInfo = Depends(get_key_type)):
+ charge = await get_charge(charge_id)
+
+ if not charge:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Charge does not exist."
+ )
+
+ await delete_charge(charge_id)
+ raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+#############################BALANCE##########################
+
+
+@satspay_ext.get("/api/v1/charges/balance/{charge_id}")
+async def api_charges_balance(charge_id):
+
+ charge = await check_address_balance(charge_id)
+
+ if not charge:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Charge does not exist."
+ )
+
+ if charge.paid and charge.webhook:
+ async with httpx.AsyncClient() as client:
+ try:
+ r = await client.post(
+ charge.webhook,
+ json={
+ "id": charge.id,
+ "description": charge.description,
+ "onchainaddress": charge.onchainaddress,
+ "payment_request": charge.payment_request,
+ "payment_hash": charge.payment_hash,
+ "time": charge.time,
+ "amount": charge.amount,
+ "balance": charge.balance,
+ "paid": charge.paid,
+ "timestamp": charge.timestamp,
+ "completelink": charge.completelink,
+ },
+ timeout=40,
+ )
+ except AssertionError:
+ charge.webhook = None
+ return charge.dict()
+
+
+#############################MEMPOOL##########################
+
+
+@satspay_ext.put("/api/v1/mempool")
+async def api_update_mempool(endpoint: str = Query(...), wallet: WalletTypeInfo = Depends(get_key_type)):
+ mempool = await update_mempool(endpoint, user=wallet.wallet.user)
+ return mempool.dict()
+
+
+@satspay_ext.route("/api/v1/mempool")
+async def api_get_mempool(wallet: WalletTypeInfo = Depends(get_key_type)):
+ mempool = await get_mempool(wallet.wallet.user)
+ if not mempool:
+ mempool = await create_mempool(user=wallet.wallet.user)
+ return mempool.dict()
diff --git a/lnbits/extensions/watchonly/README.md b/lnbits/extensions/watchonly/README.md
new file mode 100644
index 00000000..d93f7162
--- /dev/null
+++ b/lnbits/extensions/watchonly/README.md
@@ -0,0 +1,19 @@
+# Watch Only wallet
+
+## Monitor an onchain wallet and generate addresses for onchain payments
+
+Monitor an extended public key and generate deterministic fresh public keys with this simple watch only wallet. Invoice payments can also be generated, both through a publically shareable page and API.
+
+1. Start by clicking "NEW WALLET"\
+ 
+2. Fill the requested fields:
+ - give the wallet a name
+ - paste an Extended Public Key (xpub, ypub, zpub)
+ - click "CREATE WATCH-ONLY WALLET"\
+ 
+3. You can then access your onchain addresses\
+ 
+4. You can then generate bitcoin onchain adresses from LNbits\
+ 
+
+You can now use this wallet on the LNBits [SatsPayServer](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/satspay/README.md) extension
diff --git a/lnbits/extensions/watchonly/__init__.py b/lnbits/extensions/watchonly/__init__.py
new file mode 100644
index 00000000..8a1632f5
--- /dev/null
+++ b/lnbits/extensions/watchonly/__init__.py
@@ -0,0 +1,25 @@
+import asyncio
+
+from fastapi import APIRouter
+
+from lnbits.db import Database
+from lnbits.helpers import template_renderer
+
+db = Database("ext_watchonly")
+
+
+watchonly_ext: APIRouter = APIRouter(
+ prefix="/watchonly",
+ tags=["watchonly"]
+)
+
+def watchonly_renderer():
+ return template_renderer(
+ [
+ "lnbits/extensions/watchonly/templates",
+ ]
+ )
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/watchonly/config.json b/lnbits/extensions/watchonly/config.json
new file mode 100644
index 00000000..48c19ef0
--- /dev/null
+++ b/lnbits/extensions/watchonly/config.json
@@ -0,0 +1,8 @@
+{
+ "name": "Watch Only",
+ "short_description": "Onchain watch only wallets",
+ "icon": "visibility",
+ "contributors": [
+ "arcbtc"
+ ]
+}
diff --git a/lnbits/extensions/watchonly/crud.py b/lnbits/extensions/watchonly/crud.py
new file mode 100644
index 00000000..f9b92c2f
--- /dev/null
+++ b/lnbits/extensions/watchonly/crud.py
@@ -0,0 +1,214 @@
+from typing import List, Optional
+
+from . import db
+from .models import Wallets, Addresses, Mempool
+
+from lnbits.helpers import urlsafe_short_hash
+
+from embit.descriptor import Descriptor, Key # type: ignore
+from embit.descriptor.arguments import AllowedDerivation # type: ignore
+from embit.networks import NETWORKS # type: ignore
+
+
+##########################WALLETS####################
+
+
+def detect_network(k):
+ version = k.key.version
+ for network_name in NETWORKS:
+ net = NETWORKS[network_name]
+ # not found in this network
+ if version in [net["xpub"], net["ypub"], net["zpub"], net["Zpub"], net["Ypub"]]:
+ return net
+
+
+def parse_key(masterpub: str):
+ """Parses masterpub or descriptor and returns a tuple: (Descriptor, network)
+ To create addresses use descriptor.derive(num).address(network=network)
+ """
+ network = None
+ # probably a single key
+ if "(" not in masterpub:
+ k = Key.from_string(masterpub)
+ if not k.is_extended:
+ raise ValueError("The key is not a master public key")
+ if k.is_private:
+ raise ValueError("Private keys are not allowed")
+ # check depth
+ if k.key.depth != 3:
+ raise ValueError(
+ "Non-standard depth. Only bip44, bip49 and bip84 are supported with bare xpubs. For custom derivation paths use descriptors."
+ )
+ # if allowed derivation is not provided use default /{0,1}/*
+ if k.allowed_derivation is None:
+ k.allowed_derivation = AllowedDerivation.default()
+ # get version bytes
+ version = k.key.version
+ for network_name in NETWORKS:
+ net = NETWORKS[network_name]
+ # not found in this network
+ if version in [net["xpub"], net["ypub"], net["zpub"]]:
+ network = net
+ if version == net["xpub"]:
+ desc = Descriptor.from_string("pkh(%s)" % str(k))
+ elif version == net["ypub"]:
+ desc = Descriptor.from_string("sh(wpkh(%s))" % str(k))
+ elif version == net["zpub"]:
+ desc = Descriptor.from_string("wpkh(%s)" % str(k))
+ break
+ # we didn't find correct version
+ if network is None:
+ raise ValueError("Unknown master public key version")
+ else:
+ desc = Descriptor.from_string(masterpub)
+ if not desc.is_wildcard:
+ raise ValueError("Descriptor should have wildcards")
+ for k in desc.keys:
+ if k.is_extended:
+ net = detect_network(k)
+ if net is None:
+ raise ValueError(f"Unknown version: {k}")
+ if network is not None and network != net:
+ raise ValueError("Keys from different networks")
+ network = net
+ return desc, network
+
+
+async def create_watch_wallet(user: str, masterpub: str, title: str) -> Wallets:
+ # check the masterpub is fine, it will raise an exception if not
+ print("PARSE", parse_key(masterpub))
+ parse_key(masterpub)
+ wallet_id = urlsafe_short_hash()
+ await db.execute(
+ """
+ INSERT INTO watchonly.wallets (
+ id,
+ "user",
+ masterpub,
+ title,
+ address_no,
+ balance
+ )
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ # address_no is -1 so fresh address on empty wallet can get address with index 0
+ (wallet_id, user, masterpub, title, -1, 0),
+ )
+
+ return await get_watch_wallet(wallet_id)
+
+
+async def get_watch_wallet(wallet_id: str) -> Optional[Wallets]:
+ row = await db.fetchone(
+ "SELECT * FROM watchonly.wallets WHERE id = ?", (wallet_id,)
+ )
+ return Wallets.from_row(row) if row else None
+
+
+async def get_watch_wallets(user: str) -> List[Wallets]:
+ rows = await db.fetchall(
+ """SELECT * FROM watchonly.wallets WHERE "user" = ?""", (user,)
+ )
+ return [Wallets(**row) for row in rows]
+
+
+async def update_watch_wallet(wallet_id: str, **kwargs) -> Optional[Wallets]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+
+ await db.execute(
+ f"UPDATE watchonly.wallets SET {q} WHERE id = ?", (*kwargs.values(), wallet_id)
+ )
+ row = await db.fetchone(
+ "SELECT * FROM watchonly.wallets WHERE id = ?", (wallet_id,)
+ )
+ return Wallets.from_row(row) if row else None
+
+
+async def delete_watch_wallet(wallet_id: str) -> None:
+ await db.execute("DELETE FROM watchonly.wallets WHERE id = ?", (wallet_id,))
+
+ ########################ADDRESSES#######################
+
+
+async def get_derive_address(wallet_id: str, num: int):
+ wallet = await get_watch_wallet(wallet_id)
+ key = wallet.masterpub
+ desc, network = parse_key(key)
+ return desc.derive(num).address(network=network)
+
+
+async def get_fresh_address(wallet_id: str) -> Optional[Addresses]:
+ wallet = await get_watch_wallet(wallet_id)
+
+ if not wallet:
+ return None
+
+ address = await get_derive_address(wallet_id, wallet.address_no + 1)
+
+ await update_watch_wallet(wallet_id=wallet_id, address_no=wallet.address_no + 1)
+ masterpub_id = urlsafe_short_hash()
+ await db.execute(
+ """
+ INSERT INTO watchonly.addresses (
+ id,
+ address,
+ wallet,
+ amount
+ )
+ VALUES (?, ?, ?, ?)
+ """,
+ (masterpub_id, address, wallet_id, 0),
+ )
+
+ return await get_address(address)
+
+
+async def get_address(address: str) -> Optional[Addresses]:
+ row = await db.fetchone(
+ "SELECT * FROM watchonly.addresses WHERE address = ?", (address,)
+ )
+ return Addresses.from_row(row) if row else None
+
+
+async def get_addresses(wallet_id: str) -> List[Addresses]:
+ rows = await db.fetchall(
+ "SELECT * FROM watchonly.addresses WHERE wallet = ?", (wallet_id,)
+ )
+ return [Addresses(**row) for row in rows]
+
+
+######################MEMPOOL#######################
+
+
+async def create_mempool(user: str) -> Optional[Mempool]:
+ await db.execute(
+ """
+ INSERT INTO watchonly.mempool ("user",endpoint)
+ VALUES (?, ?)
+ """,
+ (user, "https://mempool.space"),
+ )
+ row = await db.fetchone(
+ """SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,)
+ )
+ return Mempool.from_row(row) if row else None
+
+
+async def update_mempool(user: str, **kwargs) -> Optional[Mempool]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+
+ await db.execute(
+ f"""UPDATE watchonly.mempool SET {q} WHERE "user" = ?""",
+ (*kwargs.values(), user),
+ )
+ row = await db.fetchone(
+ """SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,)
+ )
+ return Mempool.from_row(row) if row else None
+
+
+async def get_mempool(user: str) -> Mempool:
+ row = await db.fetchone(
+ """SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,)
+ )
+ return Mempool.from_row(row) if row else None
diff --git a/lnbits/extensions/watchonly/migrations.py b/lnbits/extensions/watchonly/migrations.py
new file mode 100644
index 00000000..05c229b5
--- /dev/null
+++ b/lnbits/extensions/watchonly/migrations.py
@@ -0,0 +1,36 @@
+async def m001_initial(db):
+ """
+ Initial wallet table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE watchonly.wallets (
+ id TEXT NOT NULL PRIMARY KEY,
+ "user" TEXT,
+ masterpub TEXT NOT NULL,
+ title TEXT NOT NULL,
+ address_no INTEGER NOT NULL DEFAULT 0,
+ balance INTEGER NOT NULL
+ );
+ """
+ )
+
+ await db.execute(
+ """
+ CREATE TABLE watchonly.addresses (
+ id TEXT NOT NULL PRIMARY KEY,
+ address TEXT NOT NULL,
+ wallet TEXT NOT NULL,
+ amount INTEGER NOT NULL
+ );
+ """
+ )
+
+ await db.execute(
+ """
+ CREATE TABLE watchonly.mempool (
+ "user" TEXT NOT NULL,
+ endpoint TEXT NOT NULL
+ );
+ """
+ )
diff --git a/lnbits/extensions/watchonly/models.py b/lnbits/extensions/watchonly/models.py
new file mode 100644
index 00000000..2fc4bf2e
--- /dev/null
+++ b/lnbits/extensions/watchonly/models.py
@@ -0,0 +1,39 @@
+from sqlite3 import Row
+from fastapi.param_functions import Query
+from pydantic import BaseModel
+
+class CreateWallet(BaseModel):
+ masterpub: str = Query("")
+ title: str = Query("")
+
+class Wallets(BaseModel):
+ id: str
+ user: str
+ masterpub: str
+ title: str
+ address_no: int
+ balance: int
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Wallets":
+ return cls(**dict(row))
+
+
+class Mempool(BaseModel):
+ user: str
+ endpoint: str
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Mempool":
+ return cls(**dict(row))
+
+
+class Addresses(BaseModel):
+ id: str
+ address: str
+ wallet: str
+ amount: int
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Addresses":
+ return cls(**dict(row))
diff --git a/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html b/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html
new file mode 100644
index 00000000..9d6bb6ac
--- /dev/null
+++ b/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html
@@ -0,0 +1,244 @@
+
+
+
+ Watch Only extension uses mempool.space
+ For use with "account Extended Public Key"
+ https://iancoleman.io/bip39/
+
+
Created by,
+ Ben Arc (using,
+ Embit)
+
+
+
+
+
+
+
+ GET /watchonly/api/v1/wallet
+ Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<wallets_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}api/v1/wallet -H "X-Api-Key: {{
+ user.wallets[0].inkey }}"
+
+
+
+
+
+
+
+ GET
+ /watchonly/api/v1/wallet/<wallet_id>
+ Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json)
+
+
+ Returns 201 CREATED (application/json)
+
+ [<wallet_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}api/v1/wallet/<wallet_id>
+ -H "X-Api-Key: {{ user.wallets[0].inkey }}"
+
+
+
+
+
+
+
+ POST /watchonly/api/v1/wallet
+ Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json)
+
+
+ Returns 201 CREATED (application/json)
+
+ [<wallet_object>, ...]
+ Curl example
+ curl -X POST {{ request.url_root }}api/v1/wallet -d '{"title":
+ <string>, "masterpub": <string>}' -H "Content-type:
+ application/json" -H "X-Api-Key: {{ user.wallets[0].adminkey }}"
+
+
+
+
+
+
+
+ DELETE
+ /watchonly/api/v1/wallet/<wallet_id>
+ Headers
+ {"X-Api-Key": <admin_key>}
+ Returns 204 NO CONTENT
+
+ Curl example
+ curl -X DELETE {{ request.url_root
+ }}api/v1/wallet/<wallet_id> -H "X-Api-Key: {{
+ user.wallets[0].adminkey }}"
+
+
+
+
+
+
+
+
+ GET
+ /watchonly/api/v1/addresses/<wallet_id>
+ Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<address_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root
+ }}api/v1/addresses/<wallet_id> -H "X-Api-Key: {{
+ user.wallets[0].inkey }}"
+
+
+
+
+
+
+
+
+ GET
+ /watchonly/api/v1/address/<wallet_id>
+ Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<address_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}api/v1/address/<wallet_id>
+ -H "X-Api-Key: {{ user.wallets[0].inkey }}"
+
+
+
+
+
+
+
+
+ GET /watchonly/api/v1/mempool
+ Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json)
+
+
+ Returns 200 OK (application/json)
+
+ [<mempool_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}api/v1/mempool -H "X-Api-Key: {{
+ user.wallets[0].adminkey }}"
+
+
+
+
+
+
+
+
+ POST
+ /watchonly/api/v1/mempool
+ Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json)
+
+
+ Returns 201 CREATED (application/json)
+
+ [<mempool_object>, ...]
+ Curl example
+ curl -X PUT {{ request.url_root }}api/v1/mempool -d '{"endpoint":
+ <string>}' -H "Content-type: application/json" -H "X-Api-Key:
+ {{ user.wallets[0].adminkey }}"
+
+
+
+
+
+
diff --git a/lnbits/extensions/watchonly/templates/watchonly/index.html b/lnbits/extensions/watchonly/templates/watchonly/index.html
new file mode 100644
index 00000000..e70f8a23
--- /dev/null
+++ b/lnbits/extensions/watchonly/templates/watchonly/index.html
@@ -0,0 +1,648 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
+
+
+
+
+ {% raw %}
+ New wallet
+
+
+
+ Point to another Mempool
+ {{ this.mempool.endpoint }}
+
+
+
+ set
+ cancel
+
+
+
+
+
+
+
+
+
+
+
+
Wallets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+ Adresses
+
+
+
+
+ {{ col.value }}
+
+
+
+
+
+
+
+
+
+
{{satBtc(utxos.total)}}
+
+ {{utxos.sats ? ' sats' : ' BTC'}}
+
+
+
+
+
+
+
+
Transactions
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+ {{ col.name == 'value' ? satBtc(col.value) : col.value }}
+
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+ {{SITE_TITLE}} Watch Only Extension
+
+
+
+
+ {% include "watchonly/_api_docs.html" %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Create Watch-only Wallet
+ Cancel
+
+
+
+
+
+
+
+ {% raw %}
+ Addresses
+
+
+ Current:
+ {{ currentaddress }}
+
+
+
+
+
+
+
+
+
+ {{ data.address }}
+
+
+
+
+
+
+
+ Get fresh address
+ Close
+
+
+
+ {% endraw %}
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }}
+
+
+
+{% endblock %}
diff --git a/lnbits/extensions/watchonly/views.py b/lnbits/extensions/watchonly/views.py
new file mode 100644
index 00000000..c56f0b9c
--- /dev/null
+++ b/lnbits/extensions/watchonly/views.py
@@ -0,0 +1,32 @@
+from http import HTTPStatus
+from starlette.exceptions import HTTPException
+from starlette.responses import HTMLResponse
+from starlette.requests import Request
+from fastapi.params import Depends
+
+from lnbits.core.models import User
+from lnbits.decorators import check_user_exists
+
+from . import watchonly_ext, watchonly_renderer
+# from .crud import get_payment
+
+from fastapi.templating import Jinja2Templates
+
+templates = Jinja2Templates(directory="templates")
+
+
+@watchonly_ext.get("/", response_class=HTMLResponse)
+async def index(request: Request, user: User = Depends(check_user_exists)):
+ return watchonly_renderer().TemplateResponse("watchonly/index.html", {"request": request,"user": user.dict()})
+
+
+# @watchonly_ext.get("/{charge_id}", response_class=HTMLResponse)
+# async def display(request: Request, charge_id):
+# link = get_payment(charge_id)
+# if not link:
+# raise HTTPException(
+# status_code=HTTPStatus.NOT_FOUND,
+# detail="Charge link does not exist."
+# )
+#
+# return watchonly_renderer().TemplateResponse("watchonly/display.html", {"request": request,"link": link.dict()})
diff --git a/lnbits/extensions/watchonly/views_api.py b/lnbits/extensions/watchonly/views_api.py
new file mode 100644
index 00000000..8b3d92c5
--- /dev/null
+++ b/lnbits/extensions/watchonly/views_api.py
@@ -0,0 +1,132 @@
+import hashlib
+from http import HTTPStatus
+import httpx
+import json
+
+from lnbits.core.crud import get_user
+from lnbits.decorators import WalletTypeInfo, get_key_type
+
+from fastapi import Query
+from fastapi.params import Depends
+from starlette.exceptions import HTTPException
+from .models import CreateWallet
+
+from lnbits.extensions.watchonly import watchonly_ext
+from .crud import (
+ create_watch_wallet,
+ get_watch_wallet,
+ get_watch_wallets,
+ update_watch_wallet,
+ delete_watch_wallet,
+ get_fresh_address,
+ get_addresses,
+ create_mempool,
+ update_mempool,
+ get_mempool,
+)
+
+###################WALLETS#############################
+
+
+@watchonly_ext.get("/api/v1/wallet")
+async def api_wallets_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
+
+ try:
+ return [wallet.dict() for wallet in await get_watch_wallets(wallet.wallet.user)]
+ except:
+ return ""
+
+
+@watchonly_ext.get("/api/v1/wallet/{wallet_id}")
+async def api_wallet_retrieve(wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)):
+ w_wallet = await get_watch_wallet(wallet_id)
+
+ if not w_wallet:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Wallet does not exist."
+ )
+
+ return w_wallet.dict()
+
+
+@watchonly_ext.post("/api/v1/wallet")
+async def api_wallet_create_or_update(data: CreateWallet, wallet_id=None, w: WalletTypeInfo = Depends(get_key_type)):
+ try:
+ wallet = await create_watch_wallet(
+ user=w.wallet.user, masterpub=data.masterpub, title=data.title
+ )
+ except Exception as e:
+ raise HTTPException(
+ status_code=HTTPStatus.BAD_REQUEST,
+ detail=str(e)
+ )
+
+ mempool = await get_mempool(w.wallet.user)
+ if not mempool:
+ create_mempool(user=w.wallet.user)
+ return wallet.dict()
+
+
+@watchonly_ext.delete("/api/v1/wallet/{wallet_id}")
+async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
+ wallet = await get_watch_wallet(wallet_id)
+
+ if not wallet:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Wallet does not exist."
+ )
+
+ await delete_watch_wallet(wallet_id)
+
+ raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+#############################ADDRESSES##########################
+
+
+@watchonly_ext.get("/api/v1/address/{wallet_id}")
+async def api_fresh_address(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
+ await get_fresh_address(wallet_id)
+
+ addresses = await get_addresses(wallet_id)
+
+ return [address.dict() for address in addresses]
+
+
+@watchonly_ext.get("/api/v1/addresses/{wallet_id}")
+
+async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
+ wallet = await get_watch_wallet(wallet_id)
+
+ if not wallet:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Wallet does not exist."
+ )
+
+ addresses = await get_addresses(wallet_id)
+
+ if not addresses:
+ await get_fresh_address(wallet_id)
+ addresses = await get_addresses(wallet_id)
+
+ return [address.dict() for address in addresses]
+
+
+#############################MEMPOOL##########################
+
+
+@watchonly_ext.put("/api/v1/mempool")
+async def api_update_mempool(endpoint: str = Query(...), w: WalletTypeInfo = Depends(get_key_type)):
+ mempool = await update_mempool(endpoint, user=w.wallet.user)
+ return mempool.dict()
+
+
+@watchonly_ext.get("/api/v1/mempool")
+async def api_get_mempool(w: WalletTypeInfo = Depends(get_key_type)):
+ mempool = await get_mempool(w.wallet.user)
+ if not mempool:
+ mempool = await create_mempool(user=w.wallet.user)
+ return mempool.dict()
diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py
index dadc52e0..3b5f42fc 100644
--- a/lnbits/extensions/withdraw/lnurl.py
+++ b/lnbits/extensions/withdraw/lnurl.py
@@ -1,11 +1,11 @@
+from fastapi.param_functions import Query
+from fastapi import HTTPException
import shortuuid # type: ignore
from http import HTTPStatus
from datetime import datetime
from lnbits.core.services import pay_invoice
-from fastapi.param_functions import Query
from starlette.requests import Request
-from starlette.exceptions import HTTPException
from . import withdraw_ext
from .crud import get_withdraw_link_by_hash, update_withdraw_link
@@ -39,60 +39,19 @@ async def api_lnurl_response(request: Request, unique_hash):
return link.lnurl_response(request).dict()
-# FOR LNURLs WHICH ARE UNIQUE
-
-
-@withdraw_ext.get("/api/v1/lnurl/{unique_hash}/{id_unique_hash}", status_code=HTTPStatus.OK, name="withdraw.api_lnurl_multi_response")
-async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash):
- link = await get_withdraw_link_by_hash(unique_hash)
-
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND,
- detail="LNURL-withdraw not found."
- )
- # return (
- # {"status": "ERROR", "reason": "LNURL-withdraw not found."},
- # HTTPStatus.OK,
- # )
-
- if link.is_spent:
- raise HTTPException(
- # WHAT STATUS_CODE TO USE??
- detail="Withdraw is spent."
- )
- # return (
- # {"status": "ERROR", "reason": "Withdraw is spent."},
- # HTTPStatus.OK,
- # )
-
- useslist = link.usescsv.split(",")
- found = False
- for x in useslist:
- tohash = link.id + link.unique_hash + str(x)
- if id_unique_hash == shortuuid.uuid(name=tohash):
- found = True
- if not found:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND,
- detail="LNURL-withdraw not found."
- )
- # return (
- # {"status": "ERROR", "reason": "LNURL-withdraw not found."},
- # HTTPStatus.OK,
- # )
-
- return link.lnurl_response(req=request).dict()
-
# CALLBACK
-@withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", status_code=HTTPStatus.OK, name="withdraw.api_lnurl_callback")
-async def api_lnurl_callback(unique_hash, k1: str = Query(...), pr: str = Query(...)):
+@withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", name="withdraw.api_lnurl_callback")
+async def api_lnurl_callback(request: Request,
+ unique_hash: str=Query(...),
+ k1: str = Query(...),
+ payment_request: str = Query(..., alias="pr")
+ ):
link = await get_withdraw_link_by_hash(unique_hash)
- payment_request = pr
now = int(datetime.now().timestamp())
+
if not link:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
@@ -163,3 +122,48 @@ async def api_lnurl_callback(unique_hash, k1: str = Query(...), pr: str = Query(
return {"status": "ERROR", "reason": str(e)}
return {"status": "OK"}
+
+# FOR LNURLs WHICH ARE UNIQUE
+
+
+@withdraw_ext.get("/api/v1/lnurl/{unique_hash}/{id_unique_hash}", status_code=HTTPStatus.OK, name="withdraw.api_lnurl_multi_response")
+async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash):
+ link = await get_withdraw_link_by_hash(unique_hash)
+
+ if not link:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="LNURL-withdraw not found."
+ )
+ # return (
+ # {"status": "ERROR", "reason": "LNURL-withdraw not found."},
+ # HTTPStatus.OK,
+ # )
+
+ if link.is_spent:
+ raise HTTPException(
+ # WHAT STATUS_CODE TO USE??
+ detail="Withdraw is spent."
+ )
+ # return (
+ # {"status": "ERROR", "reason": "Withdraw is spent."},
+ # HTTPStatus.OK,
+ # )
+
+ useslist = link.usescsv.split(",")
+ found = False
+ for x in useslist:
+ tohash = link.id + link.unique_hash + str(x)
+ if id_unique_hash == shortuuid.uuid(name=tohash):
+ found = True
+ if not found:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="LNURL-withdraw not found."
+ )
+ # return (
+ # {"status": "ERROR", "reason": "LNURL-withdraw not found."},
+ # HTTPStatus.OK,
+ # )
+
+ return link.lnurl_response(request).dict()