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 %}
+
+
+
+
+
+
+
+
+
+
+
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:
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
{% 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
+
+
+
+
-
-
-
-
- {{ col.label }}
-
+ Status
+ Title
+ Time Left (hh:mm)
+ Time To Pay (hh:mm)
+ Amount To Pay
+ Balance
+ Pending Balance
+ Onchain Address
@@ -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()