diff --git a/lnbits/extensions/satspay/__init__.py b/lnbits/extensions/satspay/__init__.py index f33f3aa52..37245c21d 100644 --- a/lnbits/extensions/satspay/__init__.py +++ b/lnbits/extensions/satspay/__init__.py @@ -1,6 +1,7 @@ import asyncio from fastapi import APIRouter +from fastapi.staticfiles import StaticFiles from lnbits.db import Database from lnbits.helpers import template_renderer @@ -11,6 +12,14 @@ db = Database("ext_satspay") satspay_ext: APIRouter = APIRouter(prefix="/satspay", tags=["satspay"]) +satspay_static_files = [ + { + "path": "/satspay/static", + "app": StaticFiles(directory="lnbits/extensions/satspay/static"), + "name": "satspay_static", + } +] + def satspay_renderer(): return template_renderer(["lnbits/extensions/satspay/templates"]) diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py index 9deb32154..47d7a4a8d 100644 --- a/lnbits/extensions/satspay/crud.py +++ b/lnbits/extensions/satspay/crud.py @@ -6,7 +6,7 @@ from lnbits.core.services import create_invoice from lnbits.core.views.api import api_payment from lnbits.helpers import urlsafe_short_hash -from ..watchonly.crud import get_fresh_address, get_mempool, get_watch_wallet +from ..watchonly.crud import get_config, get_fresh_address # from lnbits.db import open_ext_db from . import db @@ -18,7 +18,6 @@ from .models import Charges, CreateCharge 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: @@ -89,7 +88,8 @@ async def get_charge(charge_id: str) -> Charges: async def get_charges(user: str) -> List[Charges]: rows = await db.fetchall( - """SELECT * FROM satspay.charges WHERE "user" = ?""", (user,) + """SELECT * FROM satspay.charges WHERE "user" = ? ORDER BY "timestamp" DESC """, + (user,), ) return [Charges.from_row(row) for row in rows] @@ -102,14 +102,16 @@ 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) + config = await get_config(charge.user) try: async with httpx.AsyncClient() as client: r = await client.get( - mempool.endpoint + "/api/address/" + charge.onchainaddress + config.mempool_endpoint + + "/api/address/" + + charge.onchainaddress ) respAmount = r.json()["chain_stats"]["funded_txo_sum"] - if respAmount >= charge.balance: + if respAmount > charge.balance: await update_charge(charge_id=charge_id, balance=respAmount) except Exception: pass diff --git a/lnbits/extensions/satspay/models.py b/lnbits/extensions/satspay/models.py index 7e8080dc9..e8638d5e2 100644 --- a/lnbits/extensions/satspay/models.py +++ b/lnbits/extensions/satspay/models.py @@ -1,4 +1,4 @@ -import time +from datetime import datetime, timedelta from sqlite3 import Row from typing import Optional @@ -38,12 +38,16 @@ class Charges(BaseModel): def from_row(cls, row: Row) -> "Charges": return cls(**dict(row)) + @property + def time_left(self): + now = datetime.utcnow().timestamp() + start = datetime.fromtimestamp(self.timestamp) + expiration = (start + timedelta(minutes=self.time)).timestamp() + return (expiration - now) / 60 + @property def time_elapsed(self): - if (self.timestamp + (self.time * 60)) >= time.time(): - return False - else: - return True + return self.time_left < 0 @property def paid(self): diff --git a/lnbits/extensions/satspay/static/js/utils.js b/lnbits/extensions/satspay/static/js/utils.js new file mode 100644 index 000000000..9b4abbfca --- /dev/null +++ b/lnbits/extensions/satspay/static/js/utils.js @@ -0,0 +1,31 @@ +const sleep = ms => new Promise(r => setTimeout(r, ms)) +const retryWithDelay = async function (fn, retryCount = 0) { + try { + await sleep(25) + // Do not return the call directly, use result. + // Otherwise the error will not be cought in this try-catch block. + const result = await fn() + return result + } catch (err) { + if (retryCount > 100) throw err + await sleep((retryCount + 1) * 1000) + return retryWithDelay(fn, retryCount + 1) + } +} + +const mapCharge = (obj, oldObj = {}) => { + const charge = _.clone(obj) + + charge.progress = obj.time_left < 0 ? 1 : 1 - obj.time_left / obj.time + charge.time = minutesToTime(obj.time) + charge.timeLeft = minutesToTime(obj.time_left) + + charge.expanded = false + charge.displayUrl = ['/satspay/', obj.id].join('') + charge.expanded = oldObj.expanded + charge.pendingBalance = oldObj.pendingBalance || 0 + return charge +} + +const minutesToTime = min => + min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : '' diff --git a/lnbits/extensions/satspay/templates/satspay/_api_docs.html b/lnbits/extensions/satspay/templates/satspay/_api_docs.html index 336ab8997..ed6587357 100644 --- a/lnbits/extensions/satspay/templates/satspay/_api_docs.html +++ b/lnbits/extensions/satspay/templates/satspay/_api_docs.html @@ -8,172 +8,10 @@ Created by, Ben Arc

+
+
+ Swagger REST API Documentation - - - - - - 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.base_url }}satspay/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.base_url - }}satspay/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.base_url - }}satspay/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.base_url }}satspay/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.base_url - }}satspay/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.base_url - }}satspay/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 index 8c577fbed..f34ac5095 100644 --- a/lnbits/extensions/satspay/templates/satspay/display.html +++ b/lnbits/extensions/satspay/templates/satspay/display.html @@ -1,223 +1,299 @@ {% 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 lightning payment method not available - - - - pay with lightning - + Time elapsed
-
- - - bitcoin onchain payment method not available - - - - pay onchain - -
-
- -
-
- - -
-
- -
-
- - +
+ Charge paid
-
- Pay this
- lightning-network invoice
+ + + Awaiting payment... + + {% raw %} {{ charge.timeLeft }} {% endraw %} + + + +
+
+
+
+
+
+
Charge Id:
+
+ +
+
+
+
Total to pay:
+
+ + sat + +
+
+
+
Amount paid:
+
+ + + sat - - - - - - -
- Copy invoice +
+
+
Amount pending:
+
+ + sat + +
+
+
+
Amount due:
+
+ + + sat + + + none
+
+ +
+
+
+
+ + + bitcoin lightning payment method not available + + + + pay with lightning + +
+
+ + + bitcoin onchain payment method not available + + + + pay onchain + +
+
+ +
+
+ + + +
+
+
+
+ +
+
+ + +
+
+
+
+ Pay this lightning-network invoice: +
+
+ + + + + + +
+
+ Copy invoice +
+
+
+
+
+
-
-
- -
-
- - -
-
-
- Send {{ charge.amount }}sats
- to this onchain address
-
- - - - +
+
+ -
+
+
+
+
+
+
+ +
+
+ Copy address + v-if="charge.webhook" + type="a" + :href="charge.completelink" + :label="charge.completelinktext" + > +
+
+
+
+ Send + + + sats to this onchain address +
+
+ + + + + + +
+
+ Copy address +
+
+
- +
+
{% endblock %} {% block scripts %} - + + diff --git a/lnbits/extensions/satspay/templates/satspay/index.html b/lnbits/extensions/satspay/templates/satspay/index.html index 551b81b8e..396200cf1 100644 --- a/lnbits/extensions/satspay/templates/satspay/index.html +++ b/lnbits/extensions/satspay/templates/satspay/index.html @@ -18,46 +18,54 @@
Charges
-
+
- Export to CSV +
+
+ + + + + Export to CSV + + + +
@@ -66,73 +74,179 @@ + + + + + expired + + + + paid + + + waiting + + + + {{props.row.description}} - Payment link - - - +
{{props.row.timeLeft}}
+ - Time elapsed -
- - - PAID! - - - - Processing - - - Delete charge - +
- -
-
{{ col.value }}
+ +
{{props.row.time}}
+
+ +
{{props.row.amount}}
+
+ +
{{props.row.balance}}
+
+ +
+ {{props.row.pendingBalance ? props.row.pendingBalance : ''}} +
+
+ + {{props.row.onchainaddress}} + +
+ + +
+
Onchain Wallet:
+
+ {{getOnchainWalletName(props.row.onchainwallet)}} +
+
+
+
LNbits Wallet:
+
+ {{getLNbitsWalletName(props.row.lnbitswallet)}} +
+
+ + + +
+
ID:
+
{{props.row.id}}
+
+
+
+
+ Details + Refresh Balance +
+
+ Delete +
+
+
+
@@ -155,11 +269,7 @@
- + @@ -284,49 +394,28 @@ + + + diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py index d33d5c17e..5b641510a 100644 --- a/lnbits/extensions/satspay/views.py +++ b/lnbits/extensions/satspay/views.py @@ -9,6 +9,7 @@ from starlette.responses import HTMLResponse from lnbits.core.crud import get_wallet from lnbits.core.models import User from lnbits.decorators import check_user_exists +from lnbits.extensions.watchonly.crud import get_config from . import satspay_ext, satspay_renderer from .crud import get_charge @@ -24,14 +25,21 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @satspay_ext.get("/{charge_id}", response_class=HTMLResponse) -async def display(request: Request, charge_id): +async def display(request: Request, charge_id: str): charge = await get_charge(charge_id) if not charge: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist." ) wallet = await get_wallet(charge.lnbitswallet) + onchainwallet_config = await get_config(charge.user) + inkey = wallet.inkey if wallet else None return satspay_renderer().TemplateResponse( "satspay/display.html", - {"request": request, "charge": charge, "wallet_key": wallet.inkey}, + { + "request": request, + "charge_data": charge.dict(), + "wallet_inkey": inkey, + "mempool_endpoint": onchainwallet_config.mempool_endpoint, + }, ) diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index c3e38f0cd..f94b970af 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -1,7 +1,6 @@ from http import HTTPStatus import httpx -from fastapi import Query from fastapi.params import Depends from starlette.exceptions import HTTPException @@ -31,7 +30,12 @@ async def api_charge_create( data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key) ): charge = await create_charge(user=wallet.wallet.user, data=data) - return charge.dict() + return { + **charge.dict(), + **{"time_elapsed": charge.time_elapsed}, + **{"time_left": charge.time_left}, + **{"paid": charge.paid}, + } @satspay_ext.put("/api/v1/charge/{charge_id}") @@ -51,6 +55,7 @@ async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)): { **charge.dict(), **{"time_elapsed": charge.time_elapsed}, + **{"time_left": charge.time_left}, **{"paid": charge.paid}, } for charge in await get_charges(wallet.wallet.user) @@ -73,6 +78,7 @@ async def api_charge_retrieve( return { **charge.dict(), **{"time_elapsed": charge.time_elapsed}, + **{"time_left": charge.time_left}, **{"paid": charge.paid}, } @@ -93,9 +99,18 @@ async def api_charge_delete(charge_id, wallet: WalletTypeInfo = Depends(get_key_ #############################BALANCE########################## -@satspay_ext.get("/api/v1/charges/balance/{charge_id}") -async def api_charges_balance(charge_id): +@satspay_ext.get("/api/v1/charges/balance/{charge_ids}") +async def api_charges_balance(charge_ids): + charge_id_list = charge_ids.split(",") + charges = [] + for charge_id in charge_id_list: + charge = await api_charge_balance(charge_id) + charges.append(charge) + return charges + +@satspay_ext.get("/api/v1/charge/balance/{charge_id}") +async def api_charge_balance(charge_id): charge = await check_address_balance(charge_id) if not charge: @@ -125,23 +140,9 @@ async def api_charges_balance(charge_id): ) 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() + return { + **charge.dict(), + **{"time_elapsed": charge.time_elapsed}, + **{"time_left": charge.time_left}, + **{"paid": charge.paid}, + } diff --git a/lnbits/extensions/watchonly/crud.py b/lnbits/extensions/watchonly/crud.py index b88a7df7e..0d28eb702 100644 --- a/lnbits/extensions/watchonly/crud.py +++ b/lnbits/extensions/watchonly/crud.py @@ -238,41 +238,3 @@ async def get_config(user: str) -> Optional[Config]: """SELECT json_data FROM watchonly.config WHERE "user" = ?""", (user,) ) return json.loads(row[0], object_hook=lambda d: Config(**d)) if row else None - - -######################MEMPOOL####################### -### TODO: fix statspay dependcy and remove -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 - - -### TODO: fix statspay dependcy and remove -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 - - -### TODO: fix statspay dependcy and remove -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/static/js/index.js b/lnbits/extensions/watchonly/static/js/index.js index 5eee21761..f44d30cd1 100644 --- a/lnbits/extensions/watchonly/static/js/index.js +++ b/lnbits/extensions/watchonly/static/js/index.js @@ -647,7 +647,9 @@ new Vue({ getAddressTxsDelayed: async function (addrData) { const { bitcoin: {addresses: addressesAPI} - } = mempoolJS() + } = mempoolJS({ + hostname: new URL(this.config.data.mempool_endpoint).hostname + }) const fn = async () => addressesAPI.getAddressTxs({ @@ -660,7 +662,9 @@ new Vue({ refreshRecommendedFees: async function () { const { bitcoin: {fees: feesAPI} - } = mempoolJS() + } = mempoolJS({ + hostname: new URL(this.config.data.mempool_endpoint).hostname + }) const fn = async () => feesAPI.getFeesRecommended() this.payment.recommededFees = await retryWithDelay(fn) @@ -668,7 +672,9 @@ new Vue({ getAddressTxsUtxoDelayed: async function (address) { const { bitcoin: {addresses: addressesAPI} - } = mempoolJS() + } = mempoolJS({ + hostname: new URL(this.config.data.mempool_endpoint).hostname + }) const fn = async () => addressesAPI.getAddressTxsUtxo({ @@ -679,7 +685,9 @@ new Vue({ fetchTxHex: async function (txId) { const { bitcoin: {transactions: transactionsAPI} - } = mempoolJS() + } = mempoolJS({ + hostname: new URL(this.config.data.mempool_endpoint).hostname + }) try { const response = await transactionsAPI.getTxHex({txid: txId}) diff --git a/lnbits/extensions/watchonly/templates/watchonly/index.html b/lnbits/extensions/watchonly/templates/watchonly/index.html index 0ab2a67be..ff596699b 100644 --- a/lnbits/extensions/watchonly/templates/watchonly/index.html +++ b/lnbits/extensions/watchonly/templates/watchonly/index.html @@ -1198,6 +1198,7 @@
{% endblock %} {% block scripts %} {{ window_vars(user) }} + diff --git a/lnbits/extensions/watchonly/views_api.py b/lnbits/extensions/watchonly/views_api.py index f9055a207..ae6565403 100644 --- a/lnbits/extensions/watchonly/views_api.py +++ b/lnbits/extensions/watchonly/views_api.py @@ -15,19 +15,16 @@ from lnbits.extensions.watchonly import watchonly_ext from .crud import ( create_config, create_fresh_addresses, - create_mempool, create_watch_wallet, delete_addresses_for_wallet, delete_watch_wallet, get_addresses, get_config, get_fresh_address, - get_mempool, get_watch_wallet, get_watch_wallets, update_address, update_config, - update_mempool, update_watch_wallet, ) from .helpers import parse_key @@ -281,23 +278,3 @@ async def api_get_config(w: WalletTypeInfo = Depends(get_key_type)): if not config: config = await create_config(user=w.wallet.user) return config.dict() - - -#############################MEMPOOL########################## - -### TODO: fix statspay dependcy and remove -@watchonly_ext.put("/api/v1/mempool") -async def api_update_mempool( - endpoint: str = Query(...), w: WalletTypeInfo = Depends(require_admin_key) -): - mempool = await update_mempool(**{"endpoint": endpoint}, user=w.wallet.user) - return mempool.dict() - - -### TODO: fix statspay dependcy and remove -@watchonly_ext.get("/api/v1/mempool") -async def api_get_mempool(w: WalletTypeInfo = Depends(require_admin_key)): - mempool = await get_mempool(w.wallet.user) - if not mempool: - mempool = await create_mempool(user=w.wallet.user) - return mempool.dict()