mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-24 14:51:05 +01:00
Merge branch 'lnbits:main' into main
This commit is contained in:
commit
e6c376921d
83 changed files with 1494 additions and 505 deletions
|
@ -6,14 +6,17 @@ PORT=5000
|
|||
|
||||
DEBUG=false
|
||||
|
||||
# Allow users and admins by user IDs (comma separated list)
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_ADMIN_USERS=""
|
||||
# Extensions only admin can access
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok"
|
||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
|
||||
# csv ad image filepaths or urls, extensions can choose to honor
|
||||
LNBITS_AD_SPACE=""
|
||||
# Ad space description
|
||||
# LNBITS_AD_SPACE_TITLE="Supported by"
|
||||
# csv ad space, format "<url>;<img-light>;<img-dark>, <url>;<img-light>;<img-dark>", extensions can choose to honor
|
||||
# LNBITS_AD_SPACE=""
|
||||
|
||||
# Hides wallet api, extensions can choose to honor
|
||||
LNBITS_HIDE_API=false
|
||||
|
|
|
@ -8,6 +8,7 @@ RUN curl -sSL https://install.python-poetry.org | python3 -
|
|||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
RUN mkdir -p lnbits/data
|
||||
|
||||
COPY . .
|
||||
|
||||
|
|
|
@ -229,6 +229,24 @@ async def get_wallet_payment(
|
|||
return Payment.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: int = 5):
|
||||
rows = await db.fetchall(
|
||||
f"""
|
||||
SELECT * FROM apipayments
|
||||
WHERE pending = 'false'
|
||||
AND extra LIKE ?
|
||||
AND extra LIKE ?
|
||||
ORDER BY time DESC LIMIT {limit}
|
||||
""",
|
||||
(
|
||||
f"%{ext_name}%",
|
||||
f"%{ext_id}%",
|
||||
),
|
||||
)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
async def get_payments(
|
||||
*,
|
||||
wallet_id: Optional[str] = None,
|
||||
|
|
|
@ -51,7 +51,7 @@ async def m001_initial(db):
|
|||
f"""
|
||||
CREATE TABLE IF NOT EXISTS apipayments (
|
||||
payhash TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
fee INTEGER NOT NULL DEFAULT 0,
|
||||
wallet TEXT NOT NULL,
|
||||
pending BOOLEAN NOT NULL,
|
||||
|
|
|
@ -183,6 +183,23 @@
|
|||
<div class="col q-pl-md"> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
|
||||
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
|
||||
<q-btn flat color="secondary" class="full-width q-mb-md"
|
||||
>{{ AD_TITLE }}</q-btn
|
||||
>
|
||||
|
||||
<a href="{{ AD[0] }}" class="q-ma-md">
|
||||
<img
|
||||
v-if="($q.dark.isActive)"
|
||||
src="{{ AD[1] }}"
|
||||
style="max-width: 90%"
|
||||
/>
|
||||
<img v-else src="{{ AD[2] }}" style="max-width: 90%" />
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -388,9 +388,14 @@
|
|||
{% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD =
|
||||
ADS.split(';') %}
|
||||
<q-card>
|
||||
<a href="{{ AD[0] }}"
|
||||
><img width="100%" src="{{ AD[1] }}"
|
||||
/></a> </q-card
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-mt-none q-mb-sm">{{ AD_TITLE }}</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<a href="{{ AD[0] }}" class="q-ma-md">
|
||||
<img v-if="($q.dark.isActive)" src="{{ AD[1] }}" />
|
||||
<img v-else src="{{ AD[2] }}" />
|
||||
</a> </q-card-section></q-card
|
||||
>{% endfor %} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@ from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
|||
import async_timeout
|
||||
import httpx
|
||||
import pyqrcode
|
||||
from fastapi import Depends, Header, Query, Request
|
||||
from fastapi import Depends, Header, Query, Request, Response
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.params import Body
|
||||
from loguru import logger
|
||||
|
@ -155,30 +155,29 @@ class CreateInvoiceData(BaseModel):
|
|||
|
||||
|
||||
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
if data.description_hash:
|
||||
if data.description_hash or data.unhashed_description:
|
||||
try:
|
||||
description_hash = binascii.unhexlify(data.description_hash)
|
||||
description_hash = (
|
||||
binascii.unhexlify(data.description_hash)
|
||||
if data.description_hash
|
||||
else b""
|
||||
)
|
||||
unhashed_description = (
|
||||
binascii.unhexlify(data.unhashed_description)
|
||||
if data.unhashed_description
|
||||
else b""
|
||||
)
|
||||
except binascii.Error:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="'description_hash' must be a valid hex string",
|
||||
detail="'description_hash' and 'unhashed_description' must be a valid hex strings",
|
||||
)
|
||||
unhashed_description = b""
|
||||
memo = ""
|
||||
elif data.unhashed_description:
|
||||
try:
|
||||
unhashed_description = binascii.unhexlify(data.unhashed_description)
|
||||
except binascii.Error:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="'unhashed_description' must be a valid hex string",
|
||||
)
|
||||
description_hash = b""
|
||||
memo = ""
|
||||
else:
|
||||
description_hash = b""
|
||||
unhashed_description = b""
|
||||
memo = data.memo or LNBITS_SITE_TITLE
|
||||
|
||||
if data.unit == "sat":
|
||||
amount = int(data.amount)
|
||||
else:
|
||||
|
@ -585,8 +584,8 @@ class DecodePayment(BaseModel):
|
|||
data: str
|
||||
|
||||
|
||||
@core_app.post("/api/v1/payments/decode")
|
||||
async def api_payments_decode(data: DecodePayment):
|
||||
@core_app.post("/api/v1/payments/decode", status_code=HTTPStatus.OK)
|
||||
async def api_payments_decode(data: DecodePayment, response: Response):
|
||||
payment_str = data.data
|
||||
try:
|
||||
if payment_str[:5] == "LNURL":
|
||||
|
@ -607,6 +606,7 @@ async def api_payments_decode(data: DecodePayment):
|
|||
"min_final_cltv_expiry": invoice.min_final_cltv_expiry,
|
||||
}
|
||||
except:
|
||||
response.status_code = HTTPStatus.BAD_REQUEST
|
||||
return {"message": "Failed to decode"}
|
||||
|
||||
|
||||
|
|
28
lnbits/db.py
28
lnbits/db.py
|
@ -1,6 +1,7 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
@ -73,18 +74,39 @@ class Connection(Compat):
|
|||
query = query.replace("?", "%s")
|
||||
return query
|
||||
|
||||
def rewrite_values(self, values):
|
||||
# strip html
|
||||
CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
|
||||
|
||||
def cleanhtml(raw_html):
|
||||
if isinstance(raw_html, str):
|
||||
cleantext = re.sub(CLEANR, "", raw_html)
|
||||
return cleantext
|
||||
else:
|
||||
return raw_html
|
||||
|
||||
# tuple to list and back to tuple
|
||||
values = tuple([cleanhtml(l) for l in list(values)])
|
||||
return values
|
||||
|
||||
async def fetchall(self, query: str, values: tuple = ()) -> list:
|
||||
result = await self.conn.execute(self.rewrite_query(query), values)
|
||||
result = await self.conn.execute(
|
||||
self.rewrite_query(query), self.rewrite_values(values)
|
||||
)
|
||||
return await result.fetchall()
|
||||
|
||||
async def fetchone(self, query: str, values: tuple = ()):
|
||||
result = await self.conn.execute(self.rewrite_query(query), values)
|
||||
result = await self.conn.execute(
|
||||
self.rewrite_query(query), self.rewrite_values(values)
|
||||
)
|
||||
row = await result.fetchone()
|
||||
await result.close()
|
||||
return row
|
||||
|
||||
async def execute(self, query: str, values: tuple = ()):
|
||||
return await self.conn.execute(self.rewrite_query(query), values)
|
||||
return await self.conn.execute(
|
||||
self.rewrite_query(query), self.rewrite_values(values)
|
||||
)
|
||||
|
||||
|
||||
class Database(Compat):
|
||||
|
|
|
@ -95,4 +95,4 @@ async def api_bleskomat_delete(
|
|||
)
|
||||
|
||||
await delete_bleskomat(bleskomat_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
@ -380,7 +380,11 @@
|
|||
<strong>Lock key:</strong> {{ qrCodeDialog.data.k0 }}<br />
|
||||
<strong>Meta key:</strong> {{ qrCodeDialog.data.k1 }}<br />
|
||||
<strong>File key:</strong> {{ qrCodeDialog.data.k2 }}<br />
|
||||
<br />
|
||||
Always backup all keys that you're trying to write on the card. Without
|
||||
them you may not be able to change them in the future!<br />
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<q-btn
|
||||
unelevated
|
||||
|
|
|
@ -129,7 +129,7 @@ async def api_card_delete(card_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
||||
|
||||
await delete_card(card_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@boltcards_ext.get("/api/v1/hits")
|
||||
|
|
|
@ -65,7 +65,7 @@ async def api_discordbot_users_delete(
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
||||
)
|
||||
await delete_discordbot_user(user_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
# Activate Extension
|
||||
|
@ -129,4 +129,4 @@ async def api_discordbot_wallets_delete(
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
|
||||
)
|
||||
await delete_discordbot_wallet(wallet_id, get_wallet.user)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
@ -6,7 +6,6 @@ from lnbits.db import Database
|
|||
from lnbits.helpers import template_renderer
|
||||
from lnbits.tasks import catch_everything_and_restart
|
||||
|
||||
|
||||
db = Database("ext_events")
|
||||
|
||||
|
||||
|
|
|
@ -5,16 +5,16 @@ from urllib.parse import urlparse
|
|||
|
||||
import httpx
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import pay_invoice
|
||||
from lnbits.extensions.events.models import CreateTicket
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .views_api import api_ticket_send_ticket
|
||||
from loguru import logger
|
||||
from lnbits.extensions.events.models import CreateTicket
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
|
|
|
@ -135,7 +135,14 @@
|
|||
var self = this
|
||||
axios
|
||||
|
||||
.get('/events/api/v1/tickets/' + '{{ event_id }}' + '/' + self.formDialog.data.name + '/' + self.formDialog.data.email)
|
||||
.get(
|
||||
'/events/api/v1/tickets/' +
|
||||
'{{ event_id }}' +
|
||||
'/' +
|
||||
self.formDialog.data.name +
|
||||
'/' +
|
||||
self.formDialog.data.email
|
||||
)
|
||||
.then(function (response) {
|
||||
self.paymentReq = response.data.payment_request
|
||||
self.paymentCheck = response.data.payment_hash
|
||||
|
|
|
@ -260,7 +260,7 @@
|
|||
dense
|
||||
v-model.number="formDialog.data.price_per_ticket"
|
||||
type="number"
|
||||
label="Price per ticket "
|
||||
label="Sats per ticket "
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@ from http import HTTPStatus
|
|||
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
|
||||
|
@ -10,7 +11,6 @@ from lnbits.core.services import create_invoice
|
|||
from lnbits.core.views.api import api_payment
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type
|
||||
from lnbits.extensions.events.models import CreateEvent, CreateTicket
|
||||
from loguru import logger
|
||||
|
||||
from . import events_ext
|
||||
from .crud import (
|
||||
|
@ -79,7 +79,7 @@ async def api_form_delete(event_id, wallet: WalletTypeInfo = Depends(get_key_typ
|
|||
|
||||
await delete_event(event_id)
|
||||
await delete_event_tickets(event_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########Tickets##########
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import List, Optional
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
|
@ -6,11 +6,9 @@ from . import db
|
|||
from .models import CreateJukeboxPayment, CreateJukeLinkData, Jukebox, JukeboxPayment
|
||||
|
||||
|
||||
async def create_jukebox(
|
||||
data: CreateJukeLinkData, inkey: Optional[str] = ""
|
||||
) -> Jukebox:
|
||||
async def create_jukebox(data: CreateJukeLinkData) -> Jukebox:
|
||||
juke_id = urlsafe_short_hash()
|
||||
result = await db.execute(
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
|
@ -36,13 +34,13 @@ async def create_jukebox(
|
|||
|
||||
|
||||
async def update_jukebox(
|
||||
data: CreateJukeLinkData, juke_id: Optional[str] = ""
|
||||
data: Union[CreateJukeLinkData, Jukebox], juke_id: str = ""
|
||||
) -> Optional[Jukebox]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in data])
|
||||
items = [f"{field[1]}" for field in data]
|
||||
items.append(juke_id)
|
||||
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
|
||||
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items))
|
||||
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items,))
|
||||
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
|
||||
return Jukebox(**row) if row else None
|
||||
|
||||
|
@ -72,7 +70,7 @@ async def delete_jukebox(juke_id: str):
|
|||
"""
|
||||
DELETE FROM jukebox.jukebox WHERE id = ?
|
||||
""",
|
||||
(juke_id),
|
||||
(juke_id,),
|
||||
)
|
||||
|
||||
|
||||
|
@ -80,7 +78,7 @@ async def delete_jukebox(juke_id: str):
|
|||
|
||||
|
||||
async def create_jukebox_payment(data: CreateJukeboxPayment) -> JukeboxPayment:
|
||||
result = await db.execute(
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid)
|
||||
VALUES (?, ?, ?, ?)
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
from sqlite3 import Row
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from fastapi.param_functions import Query
|
||||
from pydantic import BaseModel
|
||||
from pydantic.main import BaseModel
|
||||
|
@ -20,19 +17,19 @@ class CreateJukeLinkData(BaseModel):
|
|||
|
||||
|
||||
class Jukebox(BaseModel):
|
||||
id: Optional[str]
|
||||
user: Optional[str]
|
||||
title: Optional[str]
|
||||
wallet: Optional[str]
|
||||
inkey: Optional[str]
|
||||
sp_user: Optional[str]
|
||||
sp_secret: Optional[str]
|
||||
sp_access_token: Optional[str]
|
||||
sp_refresh_token: Optional[str]
|
||||
sp_device: Optional[str]
|
||||
sp_playlists: Optional[str]
|
||||
price: Optional[int]
|
||||
profit: Optional[int]
|
||||
id: str
|
||||
user: str
|
||||
title: str
|
||||
wallet: str
|
||||
inkey: str
|
||||
sp_user: str
|
||||
sp_secret: str
|
||||
sp_access_token: str
|
||||
sp_refresh_token: str
|
||||
sp_device: str
|
||||
sp_playlists: str
|
||||
price: int
|
||||
profit: int
|
||||
|
||||
|
||||
class JukeboxPayment(BaseModel):
|
||||
|
|
|
@ -17,6 +17,7 @@ async def wait_for_paid_invoices():
|
|||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra:
|
||||
if payment.extra.get("tag") != "jukebox":
|
||||
# not a jukebox invoice
|
||||
return
|
||||
|
|
|
@ -17,7 +17,9 @@ templates = Jinja2Templates(directory="templates")
|
|||
|
||||
|
||||
@jukebox_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||
):
|
||||
return jukebox_renderer().TemplateResponse(
|
||||
"jukebox/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
@ -31,6 +33,7 @@ async def connect_to_jukebox(request: Request, juke_id):
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="Jukebox does not exist."
|
||||
)
|
||||
devices = await api_get_jukebox_device_check(juke_id)
|
||||
deviceConnected = False
|
||||
for device in devices["devices"]:
|
||||
if device["id"] == jukebox.sp_device.split("-")[1]:
|
||||
deviceConnected = True
|
||||
|
@ -48,5 +51,5 @@ async def connect_to_jukebox(request: Request, juke_id):
|
|||
else:
|
||||
return jukebox_renderer().TemplateResponse(
|
||||
"jukebox/error.html",
|
||||
{"request": request, "jukebox": jukebox.jukebox(req=request)},
|
||||
{"request": request, "jukebox": jukebox.dict()},
|
||||
)
|
||||
|
|
|
@ -3,7 +3,6 @@ import json
|
|||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
from starlette.exceptions import HTTPException
|
||||
|
@ -29,9 +28,7 @@ from .models import CreateJukeboxPayment, CreateJukeLinkData
|
|||
|
||||
@jukebox_ext.get("/api/v1/jukebox")
|
||||
async def api_get_jukeboxs(
|
||||
req: Request,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
all_wallets: bool = Query(False),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
):
|
||||
wallet_user = wallet.wallet.user
|
||||
|
||||
|
@ -53,54 +50,52 @@ async def api_check_credentials_callbac(
|
|||
access_token: str = Query(None),
|
||||
refresh_token: str = Query(None),
|
||||
):
|
||||
sp_code = ""
|
||||
sp_access_token = ""
|
||||
sp_refresh_token = ""
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
if not jukebox:
|
||||
raise HTTPException(detail="No Jukebox", status_code=HTTPStatus.FORBIDDEN)
|
||||
if code:
|
||||
jukebox.sp_access_token = code
|
||||
jukebox = await update_jukebox(jukebox, juke_id=juke_id)
|
||||
await update_jukebox(jukebox, juke_id=juke_id)
|
||||
if access_token:
|
||||
jukebox.sp_access_token = access_token
|
||||
jukebox.sp_refresh_token = refresh_token
|
||||
jukebox = await update_jukebox(jukebox, juke_id=juke_id)
|
||||
await update_jukebox(jukebox, juke_id=juke_id)
|
||||
return "<h1>Success!</h1><h2>You can close this window</h2>"
|
||||
|
||||
|
||||
@jukebox_ext.get("/api/v1/jukebox/{juke_id}")
|
||||
async def api_check_credentials_check(
|
||||
juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
@jukebox_ext.get("/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)])
|
||||
async def api_check_credentials_check(juke_id: str = Query(None)):
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
return jukebox
|
||||
|
||||
|
||||
@jukebox_ext.post("/api/v1/jukebox", status_code=HTTPStatus.CREATED)
|
||||
@jukebox_ext.post(
|
||||
"/api/v1/jukebox",
|
||||
status_code=HTTPStatus.CREATED,
|
||||
dependencies=[Depends(require_admin_key)],
|
||||
)
|
||||
@jukebox_ext.put("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK)
|
||||
async def api_create_update_jukebox(
|
||||
data: CreateJukeLinkData,
|
||||
juke_id: str = Query(None),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
data: CreateJukeLinkData, juke_id: str = Query(None)
|
||||
):
|
||||
if juke_id:
|
||||
jukebox = await update_jukebox(data, juke_id=juke_id)
|
||||
else:
|
||||
jukebox = await create_jukebox(data, inkey=wallet.wallet.inkey)
|
||||
jukebox = await create_jukebox(data)
|
||||
return jukebox
|
||||
|
||||
|
||||
@jukebox_ext.delete("/api/v1/jukebox/{juke_id}")
|
||||
@jukebox_ext.delete(
|
||||
"/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)]
|
||||
)
|
||||
async def api_delete_item(
|
||||
juke_id=None, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
juke_id: str = Query(None),
|
||||
):
|
||||
await delete_jukebox(juke_id)
|
||||
try:
|
||||
return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)]
|
||||
except:
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox")
|
||||
# try:
|
||||
# return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)]
|
||||
# except:
|
||||
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox")
|
||||
|
||||
|
||||
################JUKEBOX ENDPOINTS##################
|
||||
|
@ -114,9 +109,8 @@ async def api_get_jukebox_song(
|
|||
sp_playlist: str = Query(None),
|
||||
retry: bool = Query(False),
|
||||
):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
||||
tracks = []
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
@ -152,14 +146,13 @@ async def api_get_jukebox_song(
|
|||
}
|
||||
)
|
||||
except:
|
||||
something = None
|
||||
pass
|
||||
return [track for track in tracks]
|
||||
|
||||
|
||||
async def api_get_token(juke_id=None):
|
||||
try:
|
||||
async def api_get_token(juke_id):
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
@ -187,7 +180,7 @@ async def api_get_token(juke_id=None):
|
|||
jukebox.sp_access_token = r.json()["access_token"]
|
||||
await update_jukebox(jukebox, juke_id=juke_id)
|
||||
except:
|
||||
something = None
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
|
@ -198,9 +191,8 @@ async def api_get_token(juke_id=None):
|
|||
async def api_get_jukebox_device_check(
|
||||
juke_id: str = Query(None), retry: bool = Query(False)
|
||||
):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
||||
async with httpx.AsyncClient() as client:
|
||||
rDevice = await client.get(
|
||||
|
@ -221,7 +213,7 @@ async def api_get_jukebox_device_check(
|
|||
status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth"
|
||||
)
|
||||
else:
|
||||
return api_get_jukebox_device_check(juke_id, retry=True)
|
||||
return await api_get_jukebox_device_check(juke_id, retry=True)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="No device connected"
|
||||
|
@ -233,10 +225,8 @@ async def api_get_jukebox_device_check(
|
|||
|
||||
@jukebox_ext.get("/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}")
|
||||
async def api_get_jukebox_invoice(juke_id, song_id):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
|
||||
except:
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
||||
try:
|
||||
|
||||
|
@ -266,8 +256,7 @@ async def api_get_jukebox_invoice(juke_id, song_id):
|
|||
invoice=invoice[1], payment_hash=payment_hash, juke_id=juke_id, song_id=song_id
|
||||
)
|
||||
jukebox_payment = await create_jukebox_payment(data)
|
||||
|
||||
return data
|
||||
return jukebox_payment
|
||||
|
||||
|
||||
@jukebox_ext.get("/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}")
|
||||
|
@ -296,13 +285,12 @@ async def api_get_jukebox_invoice_paid(
|
|||
pay_hash: str = Query(None),
|
||||
retry: bool = Query(False),
|
||||
):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
||||
await api_get_jukebox_invoice_check(pay_hash, juke_id)
|
||||
jukebox_payment = await get_jukebox_payment(pay_hash)
|
||||
if jukebox_payment.paid:
|
||||
if jukebox_payment and jukebox_payment.paid:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
"https://api.spotify.com/v1/me/player/currently-playing?market=ES",
|
||||
|
@ -407,9 +395,8 @@ async def api_get_jukebox_invoice_paid(
|
|||
async def api_get_jukebox_currently(
|
||||
retry: bool = Query(False), juke_id: str = Query(None)
|
||||
):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
|
|
|
@ -60,14 +60,14 @@ async def api_update_track(track_id, g: WalletTypeInfo = Depends(get_key_type)):
|
|||
|
||||
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
||||
await update_current_track(ls.id, id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@livestream_ext.put("/api/v1/livestream/fee/{fee_pct}")
|
||||
async def api_update_fee(fee_pct, g: WalletTypeInfo = Depends(get_key_type)):
|
||||
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
||||
await update_livestream_fee(ls.id, int(fee_pct))
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@livestream_ext.post("/api/v1/livestream/tracks")
|
||||
|
@ -93,8 +93,8 @@ async def api_add_track(
|
|||
return
|
||||
|
||||
|
||||
@livestream_ext.route("/api/v1/livestream/tracks/{track_id}")
|
||||
@livestream_ext.delete("/api/v1/livestream/tracks/{track_id}")
|
||||
async def api_delete_track(track_id, g: WalletTypeInfo = Depends(get_key_type)):
|
||||
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
||||
await delete_track_from_livestream(ls.id, track_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
@ -93,7 +93,7 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)
|
|||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain")
|
||||
|
||||
await delete_domain(domain_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
# ADDRESSES
|
||||
|
@ -253,4 +253,4 @@ async def api_address_delete(address_id, g: WalletTypeInfo = Depends(get_key_typ
|
|||
)
|
||||
|
||||
await delete_address(address_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
@ -78,7 +78,7 @@ async def api_form_delete(form_id, wallet: WalletTypeInfo = Depends(get_key_type
|
|||
|
||||
await delete_form(form_id)
|
||||
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########tickets##########
|
||||
|
@ -160,4 +160,4 @@ async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_
|
|||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.")
|
||||
|
||||
await delete_ticket(ticket_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
@ -487,6 +487,17 @@
|
|||
@click="copyText(lnurlValue, 'LNURL copied to clipboard!')"
|
||||
>Copy LNURL</q-btn
|
||||
>
|
||||
<q-chip
|
||||
v-if="websocketMessage == 'WebSocket NOT supported by your Browser!' || websocketMessage == 'Connection closed'"
|
||||
clickable
|
||||
color="red"
|
||||
text-color="white"
|
||||
icon="error"
|
||||
>{% raw %}{{ wsMessage }}{% endraw %}</q-chip
|
||||
>
|
||||
<q-chip v-else clickable color="green" text-color="white" icon="check"
|
||||
>{% raw %}{{ wsMessage }}{% endraw %}</q-chip
|
||||
>
|
||||
<br />
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
|
@ -534,6 +545,7 @@
|
|||
filter: '',
|
||||
currency: 'USD',
|
||||
lnurlValue: '',
|
||||
websocketMessage: '',
|
||||
switches: 0,
|
||||
lnurldeviceLinks: [],
|
||||
lnurldeviceLinksObj: [],
|
||||
|
@ -622,6 +634,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
wsMessage: function () {
|
||||
return this.websocketMessage
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openQrCodeDialog: function (lnurldevice_id) {
|
||||
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
|
||||
|
@ -631,11 +648,17 @@
|
|||
this.qrCodeDialog.data = _.clone(lnurldevice)
|
||||
this.qrCodeDialog.data.url =
|
||||
window.location.protocol + '//' + window.location.host
|
||||
this.lnurlValueFetch(this.qrCodeDialog.data.switches[0][3])
|
||||
this.lnurlValueFetch(
|
||||
this.qrCodeDialog.data.switches[0][3],
|
||||
this.qrCodeDialog.data.id
|
||||
)
|
||||
this.qrCodeDialog.show = true
|
||||
},
|
||||
lnurlValueFetch: function (lnurl) {
|
||||
lnurlValueFetch: function (lnurl, switchId) {
|
||||
this.lnurlValue = lnurl
|
||||
this.websocketConnector(
|
||||
'wss://' + window.location.host + '/lnurldevice/ws/' + switchId
|
||||
)
|
||||
},
|
||||
addSwitch: function () {
|
||||
var self = this
|
||||
|
@ -797,6 +820,25 @@
|
|||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
websocketConnector: function (websocketUrl) {
|
||||
if ('WebSocket' in window) {
|
||||
self = this
|
||||
var ws = new WebSocket(websocketUrl)
|
||||
self.updateWsMessage('Websocket connected')
|
||||
ws.onmessage = function (evt) {
|
||||
var received_msg = evt.data
|
||||
self.updateWsMessage('Message recieved: ' + received_msg)
|
||||
}
|
||||
ws.onclose = function () {
|
||||
self.updateWsMessage('Connection closed')
|
||||
}
|
||||
} else {
|
||||
self.updateWsMessage('WebSocket NOT supported by your Browser!')
|
||||
}
|
||||
},
|
||||
updateWsMessage: function (message) {
|
||||
this.websocketMessage = message
|
||||
},
|
||||
clearFormDialoglnurldevice() {
|
||||
this.formDialoglnurldevice.data = {
|
||||
lnurl_toggle: false,
|
||||
|
|
|
@ -21,13 +21,15 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
|||
served_meta,
|
||||
served_pr,
|
||||
webhook_url,
|
||||
webhook_headers,
|
||||
webhook_body,
|
||||
success_text,
|
||||
success_url,
|
||||
comment_chars,
|
||||
currency,
|
||||
fiat_base_multiplier
|
||||
)
|
||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
{returning}
|
||||
""",
|
||||
(
|
||||
|
@ -36,6 +38,8 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
|||
data.min,
|
||||
data.max,
|
||||
data.webhook_url,
|
||||
data.webhook_headers,
|
||||
data.webhook_body,
|
||||
data.success_text,
|
||||
data.success_url,
|
||||
data.comment_chars,
|
||||
|
|
|
@ -8,7 +8,7 @@ async def m001_initial(db):
|
|||
id {db.serial_primary_key},
|
||||
wallet TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
served_meta INTEGER NOT NULL,
|
||||
served_pr INTEGER NOT NULL
|
||||
);
|
||||
|
@ -60,3 +60,11 @@ async def m004_fiat_base_multiplier(db):
|
|||
await db.execute(
|
||||
"ALTER TABLE lnurlp.pay_links ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;"
|
||||
)
|
||||
|
||||
|
||||
async def m005_webhook_headers_and_body(db):
|
||||
"""
|
||||
Add headers and body to webhooks
|
||||
"""
|
||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;")
|
||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;")
|
||||
|
|
|
@ -18,6 +18,8 @@ class CreatePayLinkData(BaseModel):
|
|||
currency: str = Query(None)
|
||||
comment_chars: int = Query(0, ge=0, lt=800)
|
||||
webhook_url: str = Query(None)
|
||||
webhook_headers: str = Query(None)
|
||||
webhook_body: str = Query(None)
|
||||
success_text: str = Query(None)
|
||||
success_url: str = Query(None)
|
||||
fiat_base_multiplier: int = Query(100, ge=1)
|
||||
|
@ -31,6 +33,8 @@ class PayLink(BaseModel):
|
|||
served_meta: int
|
||||
served_pr: int
|
||||
webhook_url: Optional[str]
|
||||
webhook_headers: Optional[str]
|
||||
webhook_body: Optional[str]
|
||||
success_text: Optional[str]
|
||||
success_url: Optional[str]
|
||||
currency: Optional[str]
|
||||
|
|
|
@ -33,17 +33,22 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
if pay_link and pay_link.webhook_url:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
pay_link.webhook_url,
|
||||
json={
|
||||
kwargs = {
|
||||
"json": {
|
||||
"payment_hash": payment.payment_hash,
|
||||
"payment_request": payment.bolt11,
|
||||
"amount": payment.amount,
|
||||
"comment": payment.extra.get("comment"),
|
||||
"lnurlp": pay_link.id,
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
"timeout": 40,
|
||||
}
|
||||
if pay_link.webhook_body:
|
||||
kwargs["json"]["body"] = json.loads(pay_link.webhook_body)
|
||||
if pay_link.webhook_headers:
|
||||
kwargs["headers"] = json.loads(pay_link.webhook_headers)
|
||||
|
||||
r = await client.post(pay_link.webhook_url, **kwargs)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
await mark_webhook_sent(payment, -1)
|
||||
|
|
|
@ -213,6 +213,24 @@
|
|||
label="Webhook URL (optional)"
|
||||
hint="A URL to be called whenever this link receives a payment."
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-if="formDialog.data.webhook_url"
|
||||
v-model="formDialog.data.webhook_headers"
|
||||
type="text"
|
||||
label="Webhook headers (optional)"
|
||||
hint="Custom data as JSON string, send headers along with the webhook."
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-if="formDialog.data.webhook_url"
|
||||
v-model="formDialog.data.webhook_body"
|
||||
type="text"
|
||||
label="Webhook custom data (optional)"
|
||||
hint="Custom data as JSON string, will get posted along with webhook 'body' field."
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Request
|
||||
|
@ -90,6 +91,24 @@ async def api_link_create_or_update(
|
|||
detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
if data.webhook_headers:
|
||||
try:
|
||||
json.loads(data.webhook_headers)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
detail="Invalid JSON in webhook_headers.",
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
if data.webhook_body:
|
||||
try:
|
||||
json.loads(data.webhook_body)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
detail="Invalid JSON in webhook_body.",
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
# database only allows int4 entries for min and max. For fiat currencies,
|
||||
# we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents.
|
||||
if data.currency and data.fiat_base_multiplier:
|
||||
|
|
|
@ -80,7 +80,7 @@ async def api_lnurlpayout_delete(
|
|||
)
|
||||
|
||||
await delete_lnurlpayout(lnurlpayout_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# type: ignore
|
||||
from os import getenv
|
||||
|
||||
from fastapi import Request
|
||||
|
@ -34,7 +35,9 @@ ngrok_tunnel = ngrok.connect(port)
|
|||
|
||||
|
||||
@ngrok_ext.get("/")
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||
):
|
||||
return ngrok_renderer().TemplateResponse(
|
||||
"ngrok/index.html", {"request": request, "ngrok": string5, "user": user.dict()}
|
||||
)
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
LNBits Offline Shop allows for merchants to receive Bitcoin payments while offline and without any electronic device.
|
||||
|
||||
Merchant will create items and associate a QR code ([a LNURLp](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) with a price. He can then print the QR codes and display them on their shop. When a costumer chooses an item, scans the QR code, gets the description and price. After payment, the costumer gets a confirmation code that the merchant can validate to be sure the payment was successful.
|
||||
Merchant will create items and associate a QR code ([a LNURLp](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) with a price. He can then print the QR codes and display them on their shop. When a customer chooses an item, scans the QR code, gets the description and price. After payment, the customer gets a confirmation code that the merchant can validate to be sure the payment was successful.
|
||||
|
||||
Costumers must use an LNURL pay capable wallet.
|
||||
Customers must use an LNURL pay capable wallet.
|
||||
|
||||
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
|
||||
|
||||
|
@ -18,18 +18,18 @@ Costumers must use an LNURL pay capable wallet.
|
|||

|
||||
2. Begin by creating an item, click "ADD NEW ITEM"
|
||||
- set the item name and a small description
|
||||
- you can set an optional, preferably square image, that will show up on the costumer wallet - _depending on wallet_
|
||||
- set the item price, if you choose a fiat currency the bitcoin conversion will happen at the time costumer scans to pay\
|
||||
- you can set an optional, preferably square image, that will show up on the customer wallet - _depending on wallet_
|
||||
- set the item price, if you choose a fiat currency the bitcoin conversion will happen at the time customer scans to pay\
|
||||

|
||||
3. After creating some products, click on "PRINT QR CODES"\
|
||||

|
||||
4. You'll see a QR code for each product in your LNBits Offline Shop with a title and price ready for printing\
|
||||

|
||||
5. Place the printed QR codes on your shop, or at the fair stall, or have them as a menu style laminated sheet
|
||||
6. Choose what type of confirmation do you want costumers to report to merchant after a successful payment\
|
||||
6. Choose what type of confirmation do you want customers to report to merchant after a successful payment\
|
||||

|
||||
|
||||
- Wordlist is the default option: after a successful payment the costumer will receive a word from this list, **sequentially**. Starting in _albatross_ as costumers pay for the items they will get the next word in the list until _zebra_, then it starts at the top again. The list can be changed, for example if you think A-Z is a big list to track, you can use _apple_, _banana_, _coconut_\
|
||||
- Wordlist is the default option: after a successful payment the customer will receive a word from this list, **sequentially**. Starting in _albatross_ as customers pay for the items they will get the next word in the list until _zebra_, then it starts at the top again. The list can be changed, for example if you think A-Z is a big list to track, you can use _apple_, _banana_, _coconut_\
|
||||

|
||||
- TOTP (time-based one time password) can be used instead. If you use Google Authenticator just scan the presented QR with the app and after a successful payment the user will get the password that you can check with GA\
|
||||

|
||||
|
|
|
@ -22,7 +22,7 @@ async def m001_initial(db):
|
|||
description TEXT NOT NULL,
|
||||
image TEXT, -- image/png;base64,...
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
price INTEGER NOT NULL,
|
||||
price {db.big_int} NOT NULL,
|
||||
unit TEXT NOT NULL DEFAULT 'sat'
|
||||
);
|
||||
"""
|
||||
|
|
|
@ -93,7 +93,7 @@ async def api_add_or_update_item(
|
|||
async def api_delete_item(item_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
|
||||
await delete_item_from_shop(shop.id, item_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
class CreateMethodData(BaseModel):
|
||||
|
|
|
@ -3,14 +3,14 @@ async def m001_initial(db):
|
|||
Initial paywalls table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE paywall.paywalls (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
memo TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """
|
||||
|
@ -25,14 +25,14 @@ async def m002_redux(db):
|
|||
"""
|
||||
await db.execute("ALTER TABLE paywall.paywalls RENAME TO paywalls_old")
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE paywall.paywalls (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
memo TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
amount INTEGER DEFAULT 0,
|
||||
amount {db.big_int} DEFAULT 0,
|
||||
time TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """,
|
||||
|
|
|
@ -49,7 +49,7 @@ async def api_paywall_delete(
|
|||
)
|
||||
|
||||
await delete_paywall(paywall_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@paywall_ext.post("/api/v1/paywalls/invoice/{paywall_id}")
|
||||
|
|
|
@ -8,7 +8,6 @@ from .models import (
|
|||
CreateSatsDiceLink,
|
||||
CreateSatsDicePayment,
|
||||
CreateSatsDiceWithdraw,
|
||||
HashCheck,
|
||||
satsdiceLink,
|
||||
satsdicePayment,
|
||||
satsdiceWithdraw,
|
||||
|
@ -76,7 +75,7 @@ async def get_satsdice_pays(wallet_ids: Union[str, List[str]]) -> List[satsdiceL
|
|||
return [satsdiceLink(**row) for row in rows]
|
||||
|
||||
|
||||
async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]:
|
||||
async def update_satsdice_pay(link_id: str, **kwargs) -> satsdiceLink:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?",
|
||||
|
@ -85,10 +84,10 @@ async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]:
|
|||
row = await db.fetchone(
|
||||
"SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,)
|
||||
)
|
||||
return satsdiceLink(**row) if row else None
|
||||
return satsdiceLink(**row)
|
||||
|
||||
|
||||
async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]:
|
||||
async def increment_satsdice_pay(link_id: str, **kwargs) -> Optional[satsdiceLink]:
|
||||
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?",
|
||||
|
@ -100,7 +99,7 @@ async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLin
|
|||
return satsdiceLink(**row) if row else None
|
||||
|
||||
|
||||
async def delete_satsdice_pay(link_id: int) -> None:
|
||||
async def delete_satsdice_pay(link_id: str) -> None:
|
||||
await db.execute("DELETE FROM satsdice.satsdice_pay WHERE id = ?", (link_id,))
|
||||
|
||||
|
||||
|
@ -119,9 +118,15 @@ async def create_satsdice_payment(data: CreateSatsDicePayment) -> satsdicePaymen
|
|||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(data["payment_hash"], data["satsdice_pay"], data["value"], False, False),
|
||||
(
|
||||
data.payment_hash,
|
||||
data.satsdice_pay,
|
||||
data.value,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
)
|
||||
payment = await get_satsdice_payment(data["payment_hash"])
|
||||
payment = await get_satsdice_payment(data.payment_hash)
|
||||
assert payment, "Newly created withdraw couldn't be retrieved"
|
||||
return payment
|
||||
|
||||
|
@ -134,9 +139,7 @@ async def get_satsdice_payment(payment_hash: str) -> Optional[satsdicePayment]:
|
|||
return satsdicePayment(**row) if row else None
|
||||
|
||||
|
||||
async def update_satsdice_payment(
|
||||
payment_hash: int, **kwargs
|
||||
) -> Optional[satsdicePayment]:
|
||||
async def update_satsdice_payment(payment_hash: str, **kwargs) -> satsdicePayment:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
await db.execute(
|
||||
|
@ -147,7 +150,7 @@ async def update_satsdice_payment(
|
|||
"SELECT * FROM satsdice.satsdice_payment WHERE payment_hash = ?",
|
||||
(payment_hash,),
|
||||
)
|
||||
return satsdicePayment(**row) if row else None
|
||||
return satsdicePayment(**row)
|
||||
|
||||
|
||||
##################SATSDICE WITHDRAW LINKS
|
||||
|
@ -168,16 +171,16 @@ async def create_satsdice_withdraw(data: CreateSatsDiceWithdraw) -> satsdiceWith
|
|||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
data["payment_hash"],
|
||||
data["satsdice_pay"],
|
||||
data["value"],
|
||||
data.payment_hash,
|
||||
data.satsdice_pay,
|
||||
data.value,
|
||||
urlsafe_short_hash(),
|
||||
urlsafe_short_hash(),
|
||||
int(datetime.now().timestamp()),
|
||||
data["used"],
|
||||
data.used,
|
||||
),
|
||||
)
|
||||
withdraw = await get_satsdice_withdraw(data["payment_hash"], 0)
|
||||
withdraw = await get_satsdice_withdraw(data.payment_hash, 0)
|
||||
assert withdraw, "Newly created withdraw couldn't be retrieved"
|
||||
return withdraw
|
||||
|
||||
|
@ -247,7 +250,7 @@ async def delete_satsdice_withdraw(withdraw_id: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
async def create_withdraw_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
|
||||
async def create_withdraw_hash_check(the_hash: str, lnurl_id: str):
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO satsdice.hash_checkw (
|
||||
|
@ -262,18 +265,14 @@ async def create_withdraw_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
|
|||
return hashCheck
|
||||
|
||||
|
||||
async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str) -> Optional[HashCheck]:
|
||||
async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str):
|
||||
rowid = await db.fetchone(
|
||||
"SELECT * FROM satsdice.hash_checkw WHERE id = ?", (the_hash,)
|
||||
)
|
||||
rowlnurl = await db.fetchone(
|
||||
"SELECT * FROM satsdice.hash_checkw WHERE lnurl_id = ?", (lnurl_id,)
|
||||
)
|
||||
if not rowlnurl:
|
||||
await create_withdraw_hash_check(the_hash, lnurl_id)
|
||||
return {"lnurl": True, "hash": False}
|
||||
else:
|
||||
if not rowid:
|
||||
if not rowlnurl or not rowid:
|
||||
await create_withdraw_hash_check(the_hash, lnurl_id)
|
||||
return {"lnurl": True, "hash": False}
|
||||
else:
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from http import HTTPStatus
|
||||
|
@ -83,15 +82,18 @@ async def api_lnurlp_callback(
|
|||
|
||||
success_action = link.success_action(payment_hash=payment_hash, req=req)
|
||||
|
||||
data: CreateSatsDicePayment = {
|
||||
"satsdice_pay": link.id,
|
||||
"value": amount_received / 1000,
|
||||
"payment_hash": payment_hash,
|
||||
}
|
||||
data = CreateSatsDicePayment(
|
||||
satsdice_pay=link.id,
|
||||
value=amount_received / 1000,
|
||||
payment_hash=payment_hash,
|
||||
)
|
||||
|
||||
await create_satsdice_payment(data)
|
||||
payResponse = {"pr": payment_request, "successAction": success_action, "routes": []}
|
||||
|
||||
payResponse: dict = {
|
||||
"pr": payment_request,
|
||||
"successAction": success_action,
|
||||
"routes": [],
|
||||
}
|
||||
return json.dumps(payResponse)
|
||||
|
||||
|
||||
|
@ -133,9 +135,7 @@ async def api_lnurlw_response(req: Request, unique_hash: str = Query(None)):
|
|||
name="satsdice.api_lnurlw_callback",
|
||||
)
|
||||
async def api_lnurlw_callback(
|
||||
req: Request,
|
||||
unique_hash: str = Query(None),
|
||||
k1: str = Query(None),
|
||||
pr: str = Query(None),
|
||||
):
|
||||
|
||||
|
@ -146,6 +146,7 @@ async def api_lnurlw_callback(
|
|||
return {"status": "ERROR", "reason": "spent"}
|
||||
paylink = await get_satsdice_pay(link.satsdice_pay)
|
||||
|
||||
if paylink:
|
||||
await update_satsdice_withdraw(link.id, used=1)
|
||||
await pay_invoice(
|
||||
wallet_id=paylink.wallet,
|
||||
|
|
|
@ -3,14 +3,14 @@ async def m001_initial(db):
|
|||
Creates an improved satsdice table and migrates the existing data.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE satsdice.satsdice_pay (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT,
|
||||
title TEXT,
|
||||
min_bet INTEGER,
|
||||
max_bet INTEGER,
|
||||
amount INTEGER DEFAULT 0,
|
||||
amount {db.big_int} DEFAULT 0,
|
||||
served_meta INTEGER NOT NULL,
|
||||
served_pr INTEGER NOT NULL,
|
||||
multiplier FLOAT,
|
||||
|
@ -28,11 +28,11 @@ async def m002_initial(db):
|
|||
Creates an improved satsdice table and migrates the existing data.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE satsdice.satsdice_withdraw (
|
||||
id TEXT PRIMARY KEY,
|
||||
satsdice_pay TEXT,
|
||||
value INTEGER DEFAULT 1,
|
||||
value {db.big_int} DEFAULT 1,
|
||||
unique_hash TEXT UNIQUE,
|
||||
k1 TEXT,
|
||||
open_time INTEGER,
|
||||
|
@ -47,11 +47,11 @@ async def m003_initial(db):
|
|||
Creates an improved satsdice table and migrates the existing data.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE satsdice.satsdice_payment (
|
||||
payment_hash TEXT PRIMARY KEY,
|
||||
satsdice_pay TEXT,
|
||||
value INTEGER,
|
||||
value {db.big_int},
|
||||
paid BOOL DEFAULT FALSE,
|
||||
lost BOOL DEFAULT FALSE
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ from typing import Dict, Optional
|
|||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from lnurl import Lnurl, LnurlWithdrawResponse
|
||||
from lnurl import Lnurl
|
||||
from lnurl import encode as lnurl_encode # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from pydantic import BaseModel
|
||||
|
@ -80,8 +80,7 @@ class satsdiceWithdraw(BaseModel):
|
|||
def is_spent(self) -> bool:
|
||||
return self.used >= 1
|
||||
|
||||
@property
|
||||
def lnurl_response(self, req: Request) -> LnurlWithdrawResponse:
|
||||
def lnurl_response(self, req: Request):
|
||||
url = req.url_for("satsdice.api_lnurlw_callback", unique_hash=self.unique_hash)
|
||||
withdrawResponse = {
|
||||
"tag": "withdrawRequest",
|
||||
|
@ -99,7 +98,7 @@ class HashCheck(BaseModel):
|
|||
lnurl_id: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Hash":
|
||||
def from_row(cls, row: Row):
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import random
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
|
||||
import pyqrcode
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
|
@ -20,13 +22,15 @@ from .crud import (
|
|||
get_satsdice_withdraw,
|
||||
update_satsdice_payment,
|
||||
)
|
||||
from .models import CreateSatsDiceWithdraw, satsdiceLink
|
||||
from .models import CreateSatsDiceWithdraw
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@satsdice_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||
):
|
||||
return satsdice_renderer().TemplateResponse(
|
||||
"satsdice/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
@ -67,7 +71,7 @@ async def displaywin(
|
|||
)
|
||||
withdrawLink = await get_satsdice_withdraw(payment_hash)
|
||||
payment = await get_satsdice_payment(payment_hash)
|
||||
if payment.lost:
|
||||
if not payment or payment.lost:
|
||||
return satsdice_renderer().TemplateResponse(
|
||||
"satsdice/error.html",
|
||||
{"request": request, "link": satsdicelink.id, "paid": False, "lost": True},
|
||||
|
@ -96,13 +100,18 @@ async def displaywin(
|
|||
)
|
||||
await update_satsdice_payment(payment_hash, paid=1)
|
||||
paylink = await get_satsdice_payment(payment_hash)
|
||||
if not paylink:
|
||||
return satsdice_renderer().TemplateResponse(
|
||||
"satsdice/error.html",
|
||||
{"request": request, "link": satsdicelink.id, "paid": False, "lost": True},
|
||||
)
|
||||
|
||||
data: CreateSatsDiceWithdraw = {
|
||||
"satsdice_pay": satsdicelink.id,
|
||||
"value": paylink.value * satsdicelink.multiplier,
|
||||
"payment_hash": payment_hash,
|
||||
"used": 0,
|
||||
}
|
||||
data = CreateSatsDiceWithdraw(
|
||||
satsdice_pay=satsdicelink.id,
|
||||
value=paylink.value * satsdicelink.multiplier,
|
||||
payment_hash=payment_hash,
|
||||
used=0,
|
||||
)
|
||||
|
||||
withdrawLink = await create_satsdice_withdraw(data)
|
||||
return satsdice_renderer().TemplateResponse(
|
||||
|
@ -121,9 +130,12 @@ async def displaywin(
|
|||
|
||||
@satsdice_ext.get("/img/{link_id}", response_class=HTMLResponse)
|
||||
async def img(link_id):
|
||||
link = await get_satsdice_pay(link_id) or abort(
|
||||
HTTPStatus.NOT_FOUND, "satsdice link does not exist."
|
||||
link = await get_satsdice_pay(link_id)
|
||||
if not link:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="satsdice link does not exist."
|
||||
)
|
||||
|
||||
qr = pyqrcode.create(link.lnurl)
|
||||
stream = BytesIO()
|
||||
qr.svg(stream, scale=3)
|
||||
|
|
|
@ -15,9 +15,10 @@ from .crud import (
|
|||
delete_satsdice_pay,
|
||||
get_satsdice_pay,
|
||||
get_satsdice_pays,
|
||||
get_withdraw_hash_checkw,
|
||||
update_satsdice_pay,
|
||||
)
|
||||
from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, satsdiceLink
|
||||
from .models import CreateSatsDiceLink
|
||||
|
||||
################LNURL pay
|
||||
|
||||
|
@ -25,13 +26,15 @@ from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, satsdiceLink
|
|||
@satsdice_ext.get("/api/v1/links")
|
||||
async def api_links(
|
||||
request: Request,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
all_wallets: bool = Query(False),
|
||||
):
|
||||
wallet_ids = [wallet.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
||||
user = await get_user(wallet.wallet.user)
|
||||
if user:
|
||||
wallet_ids = user.wallet_ids
|
||||
|
||||
try:
|
||||
links = await get_satsdice_pays(wallet_ids)
|
||||
|
@ -46,7 +49,7 @@ async def api_links(
|
|||
|
||||
@satsdice_ext.get("/api/v1/links/{link_id}")
|
||||
async def api_link_retrieve(
|
||||
link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
link = await get_satsdice_pay(link_id)
|
||||
|
||||
|
@ -67,7 +70,7 @@ async def api_link_retrieve(
|
|||
@satsdice_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
||||
async def api_link_create_or_update(
|
||||
data: CreateSatsDiceLink,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
link_id: str = Query(None),
|
||||
):
|
||||
if data.min_bet > data.max_bet:
|
||||
|
@ -95,10 +98,10 @@ async def api_link_create_or_update(
|
|||
|
||||
@satsdice_ext.delete("/api/v1/links/{link_id}")
|
||||
async def api_link_delete(
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), link_id: str = Query(None)
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
link_id: str = Query(None),
|
||||
):
|
||||
link = await get_satsdice_pay(link_id)
|
||||
|
||||
if not link:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
||||
|
@ -117,11 +120,12 @@ async def api_link_delete(
|
|||
##########LNURL withdraw
|
||||
|
||||
|
||||
@satsdice_ext.get("/api/v1/withdraws/{the_hash}/{lnurl_id}")
|
||||
@satsdice_ext.get(
|
||||
"/api/v1/withdraws/{the_hash}/{lnurl_id}", dependencies=[Depends(get_key_type)]
|
||||
)
|
||||
async def api_withdraw_hash_retrieve(
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
lnurl_id: str = Query(None),
|
||||
the_hash: str = Query(None),
|
||||
):
|
||||
hashCheck = await get_withdraw_hash_check(the_hash, lnurl_id)
|
||||
hashCheck = await get_withdraw_hash_checkw(the_hash, lnurl_id)
|
||||
return hashCheck
|
||||
|
|
|
@ -18,7 +18,7 @@ Easilly create invoices that support Lightning Network and on-chain BTC payment.
|
|||

|
||||
3. The charge will appear on the _Charges_ section\
|
||||

|
||||
4. Your costumer/payee will get the payment page
|
||||
4. Your customer/payee will get the payment page
|
||||
- they can choose to pay on LN\
|
||||

|
||||
- or pay on chain\
|
||||
|
|
|
@ -2,7 +2,5 @@
|
|||
"name": "SatsPay Server",
|
||||
"short_description": "Create onchain and LN charges",
|
||||
"icon": "payment",
|
||||
"contributors": [
|
||||
"arcbtc"
|
||||
]
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
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_config, get_fresh_address
|
||||
|
||||
# from lnbits.db import open_ext_db
|
||||
from . import db
|
||||
from .models import Charges, CreateCharge
|
||||
from .helpers import fetch_onchain_balance
|
||||
from .models import Charges, CreateCharge, SatsPayThemes
|
||||
|
||||
###############CHARGES##########################
|
||||
|
||||
|
@ -18,6 +18,10 @@ from .models import Charges, CreateCharge
|
|||
async def create_charge(user: str, data: CreateCharge) -> Charges:
|
||||
charge_id = urlsafe_short_hash()
|
||||
if data.onchainwallet:
|
||||
config = await get_config(user)
|
||||
data.extra = json.dumps(
|
||||
{"mempool_endpoint": config.mempool_endpoint, "network": config.network}
|
||||
)
|
||||
onchain = await get_fresh_address(data.onchainwallet)
|
||||
onchainaddress = onchain.address
|
||||
else:
|
||||
|
@ -48,9 +52,11 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
|
|||
completelinktext,
|
||||
time,
|
||||
amount,
|
||||
balance
|
||||
balance,
|
||||
extra,
|
||||
custom_css
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
charge_id,
|
||||
|
@ -67,6 +73,8 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
|
|||
data.time,
|
||||
data.amount,
|
||||
0,
|
||||
data.extra,
|
||||
data.custom_css,
|
||||
),
|
||||
)
|
||||
return await get_charge(charge_id)
|
||||
|
@ -98,34 +106,118 @@ 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]:
|
||||
async def check_address_balance(charge_id: str) -> Optional[Charges]:
|
||||
charge = await get_charge(charge_id)
|
||||
|
||||
if not charge.paid:
|
||||
if charge.onchainaddress:
|
||||
config = await get_charge_config(charge_id)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
config.mempool_endpoint
|
||||
+ "/api/address/"
|
||||
+ charge.onchainaddress
|
||||
)
|
||||
respAmount = r.json()["chain_stats"]["funded_txo_sum"]
|
||||
respAmount = await fetch_onchain_balance(charge)
|
||||
if respAmount > charge.balance:
|
||||
await update_charge(charge_id=charge_id, balance=respAmount)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
if charge.lnbitswallet:
|
||||
invoice_status = await api_payment(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
|
||||
return await get_charge(charge_id)
|
||||
|
||||
|
||||
async def get_charge_config(charge_id: str):
|
||||
row = await db.fetchone(
|
||||
"""SELECT "user" FROM satspay.charges WHERE id = ?""", (charge_id,)
|
||||
################## SETTINGS ###################
|
||||
|
||||
|
||||
async def save_theme(data: SatsPayThemes, css_id: str = None):
|
||||
# insert or update
|
||||
if css_id:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ?
|
||||
""",
|
||||
(data.custom_css, data.title, css_id),
|
||||
)
|
||||
else:
|
||||
css_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO satspay.themes (
|
||||
css_id,
|
||||
title,
|
||||
user,
|
||||
custom_css
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
css_id,
|
||||
data.title,
|
||||
data.user,
|
||||
data.custom_css,
|
||||
),
|
||||
)
|
||||
return await get_theme(css_id)
|
||||
|
||||
|
||||
async def get_theme(css_id: str) -> SatsPayThemes:
|
||||
row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,))
|
||||
return SatsPayThemes.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_themes(user_id: str) -> List[SatsPayThemes]:
|
||||
rows = await db.fetchall(
|
||||
"""SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "timestamp" DESC """,
|
||||
(user_id,),
|
||||
)
|
||||
return await get_config(row.user)
|
||||
|
||||
|
||||
################## SETTINGS ###################
|
||||
|
||||
|
||||
async def save_theme(data: SatsPayThemes, css_id: str = None):
|
||||
# insert or update
|
||||
if css_id:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ?
|
||||
""",
|
||||
(data.custom_css, data.title, css_id),
|
||||
)
|
||||
else:
|
||||
css_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO satspay.themes (
|
||||
css_id,
|
||||
title,
|
||||
"user",
|
||||
custom_css
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
css_id,
|
||||
data.title,
|
||||
data.user,
|
||||
data.custom_css,
|
||||
),
|
||||
)
|
||||
return await get_theme(css_id)
|
||||
|
||||
|
||||
async def get_theme(css_id: str) -> SatsPayThemes:
|
||||
row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,))
|
||||
return SatsPayThemes.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_themes(user_id: str) -> List[SatsPayThemes]:
|
||||
rows = await db.fetchall(
|
||||
"""SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "title" DESC """,
|
||||
(user_id,),
|
||||
)
|
||||
return [SatsPayThemes.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def delete_theme(theme_id: str) -> None:
|
||||
await db.execute("DELETE FROM satspay.themes WHERE css_id = ?", (theme_id,))
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from .models import Charges
|
||||
|
||||
|
||||
def compact_charge(charge: Charges):
|
||||
return {
|
||||
def public_charge(charge: Charges):
|
||||
c = {
|
||||
"id": charge.id,
|
||||
"description": charge.description,
|
||||
"onchainaddress": charge.onchainaddress,
|
||||
|
@ -13,5 +16,40 @@ def compact_charge(charge: Charges):
|
|||
"balance": charge.balance,
|
||||
"paid": charge.paid,
|
||||
"timestamp": charge.timestamp,
|
||||
"completelink": charge.completelink, # should be secret?
|
||||
"time_elapsed": charge.time_elapsed,
|
||||
"time_left": charge.time_left,
|
||||
"paid": charge.paid,
|
||||
"custom_css": charge.custom_css,
|
||||
}
|
||||
|
||||
if charge.paid:
|
||||
c["completelink"] = charge.completelink
|
||||
c["completelinktext"] = charge.completelinktext
|
||||
|
||||
return c
|
||||
|
||||
|
||||
async def call_webhook(charge: Charges):
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
charge.webhook,
|
||||
json=public_charge(charge),
|
||||
timeout=40,
|
||||
)
|
||||
return {"webhook_success": r.is_success, "webhook_message": r.reason_phrase}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to call webhook for charge {charge.id}")
|
||||
logger.warning(e)
|
||||
return {"webhook_success": False, "webhook_message": str(e)}
|
||||
|
||||
|
||||
async def fetch_onchain_balance(charge: Charges):
|
||||
endpoint = (
|
||||
f"{charge.config.mempool_endpoint}/testnet"
|
||||
if charge.config.network == "Testnet"
|
||||
else charge.config.mempool_endpoint
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(endpoint + "/api/address/" + charge.onchainaddress)
|
||||
return r.json()["chain_stats"]["funded_txo_sum"]
|
||||
|
|
|
@ -4,7 +4,7 @@ async def m001_initial(db):
|
|||
"""
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE satspay.charges (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
"user" TEXT,
|
||||
|
@ -18,11 +18,47 @@ async def m001_initial(db):
|
|||
completelink TEXT,
|
||||
completelinktext TEXT,
|
||||
time INTEGER,
|
||||
amount INTEGER,
|
||||
balance INTEGER DEFAULT 0,
|
||||
amount {db.big_int},
|
||||
balance {db.big_int} DEFAULT 0,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m002_add_charge_extra_data(db):
|
||||
"""
|
||||
Add 'extra' column for storing various config about the charge (JSON format)
|
||||
"""
|
||||
await db.execute(
|
||||
"""ALTER TABLE satspay.charges
|
||||
ADD COLUMN extra TEXT DEFAULT '{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}';
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m003_add_themes_table(db):
|
||||
"""
|
||||
Themes table
|
||||
"""
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE satspay.themes (
|
||||
css_id TEXT NOT NULL PRIMARY KEY,
|
||||
"user" TEXT,
|
||||
title TEXT,
|
||||
custom_css TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m004_add_custom_css_to_charges(db):
|
||||
"""
|
||||
Add custom css option column to the 'charges' table
|
||||
"""
|
||||
|
||||
await db.execute("ALTER TABLE satspay.charges ADD COLUMN custom_css TEXT;")
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from sqlite3 import Row
|
||||
from typing import Optional
|
||||
|
@ -13,8 +14,17 @@ class CreateCharge(BaseModel):
|
|||
webhook: str = Query(None)
|
||||
completelink: str = Query(None)
|
||||
completelinktext: str = Query(None)
|
||||
custom_css: Optional[str]
|
||||
time: int = Query(..., ge=1)
|
||||
amount: int = Query(..., ge=1)
|
||||
extra: str = "{}"
|
||||
|
||||
|
||||
class ChargeConfig(BaseModel):
|
||||
mempool_endpoint: Optional[str]
|
||||
network: Optional[str]
|
||||
webhook_success: Optional[bool] = False
|
||||
webhook_message: Optional[str]
|
||||
|
||||
|
||||
class Charges(BaseModel):
|
||||
|
@ -28,6 +38,8 @@ class Charges(BaseModel):
|
|||
webhook: Optional[str]
|
||||
completelink: Optional[str]
|
||||
completelinktext: Optional[str] = "Back to Merchant"
|
||||
extra: str = "{}"
|
||||
custom_css: Optional[str]
|
||||
time: int
|
||||
amount: int
|
||||
balance: int
|
||||
|
@ -54,3 +66,22 @@ class Charges(BaseModel):
|
|||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def config(self) -> ChargeConfig:
|
||||
charge_config = json.loads(self.extra)
|
||||
return ChargeConfig(**charge_config)
|
||||
|
||||
def must_call_webhook(self):
|
||||
return self.webhook and self.paid and self.config.webhook_success == False
|
||||
|
||||
|
||||
class SatsPayThemes(BaseModel):
|
||||
css_id: str = Query(None)
|
||||
title: str = Query(None)
|
||||
custom_css: str = Query(None)
|
||||
user: Optional[str]
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "SatsPayThemes":
|
||||
return cls(**dict(row))
|
||||
|
|
|
@ -14,18 +14,22 @@ const retryWithDelay = async function (fn, retryCount = 0) {
|
|||
}
|
||||
|
||||
const mapCharge = (obj, oldObj = {}) => {
|
||||
const charge = _.clone(obj)
|
||||
const charge = {...oldObj, ...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.expanded = oldObj.expanded || false
|
||||
charge.pendingBalance = oldObj.pendingBalance || 0
|
||||
return charge
|
||||
}
|
||||
|
||||
const mapCSS = (obj, oldObj = {}) => {
|
||||
const theme = _.clone(obj)
|
||||
return theme
|
||||
}
|
||||
|
||||
const minutesToTime = min =>
|
||||
min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : ''
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
import json
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
@ -7,7 +8,8 @@ from lnbits.extensions.satspay.crud import check_address_balance, get_charge
|
|||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
# from .crud import get_ticket, set_ticket_paid
|
||||
from .crud import update_charge
|
||||
from .helpers import call_webhook
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
|
@ -30,4 +32,9 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
return
|
||||
|
||||
await payment.set_pending(False)
|
||||
await check_address_balance(charge_id=charge.id)
|
||||
charge = await check_address_balance(charge_id=charge.id)
|
||||
|
||||
if charge.must_call_webhook():
|
||||
resp = await call_webhook(charge)
|
||||
extra = {**charge.config.dict(), **resp}
|
||||
await update_charge(charge_id=charge.id, extra=json.dumps(extra))
|
||||
|
|
|
@ -5,7 +5,13 @@
|
|||
WatchOnly extension, we highly reccomend using a fresh extended public Key
|
||||
specifically for SatsPayServer!<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a>,
|
||||
<a
|
||||
target="_blank"
|
||||
style="color: unset"
|
||||
href="https://github.com/motorina0"
|
||||
>motorina0</a
|
||||
></small
|
||||
>
|
||||
</p>
|
||||
<br />
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
<q-btn
|
||||
flat
|
||||
disable
|
||||
v-if="!charge.lnbitswallet || charge.time_elapsed"
|
||||
v-if="!charge.payment_request || charge.time_elapsed"
|
||||
style="color: primary; width: 100%"
|
||||
label="lightning⚡"
|
||||
>
|
||||
|
@ -131,7 +131,7 @@
|
|||
<q-btn
|
||||
flat
|
||||
disable
|
||||
v-if="!charge.onchainwallet || charge.time_elapsed"
|
||||
v-if="!charge.onchainaddress || charge.time_elapsed"
|
||||
style="color: primary; width: 100%"
|
||||
label="onchain⛓️"
|
||||
>
|
||||
|
@ -170,14 +170,18 @@
|
|||
name="check"
|
||||
style="color: green; font-size: 21.4em"
|
||||
></q-icon>
|
||||
<div class="row text-center q-mt-lg">
|
||||
<div class="col text-center">
|
||||
<q-btn
|
||||
outline
|
||||
v-if="charge.webhook"
|
||||
v-if="charge.completelink"
|
||||
type="a"
|
||||
:href="charge.completelink"
|
||||
:label="charge.completelinktext"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="row text-center q-mb-sm">
|
||||
<div class="col text-center">
|
||||
|
@ -218,7 +222,7 @@
|
|||
<div class="col text-center">
|
||||
<a
|
||||
style="color: unset"
|
||||
:href="mempool_endpoint + '/address/' + charge.onchainaddress"
|
||||
:href="'https://' + mempoolHostname + '/address/' + charge.onchainaddress"
|
||||
target="_blank"
|
||||
><span
|
||||
class="text-subtitle1"
|
||||
|
@ -241,6 +245,8 @@
|
|||
name="check"
|
||||
style="color: green; font-size: 21.4em"
|
||||
></q-icon>
|
||||
<div class="row text-center q-mt-lg">
|
||||
<div class="col text-center">
|
||||
<q-btn
|
||||
outline
|
||||
v-if="charge.webhook"
|
||||
|
@ -249,6 +255,8 @@
|
|||
:label="charge.completelinktext"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="row items-center q-mb-sm">
|
||||
<div class="col text-center">
|
||||
|
@ -289,7 +297,17 @@
|
|||
</div>
|
||||
<div class="col-lg- 4 col-md-3 col-sm-1"></div>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block styles %}
|
||||
<link
|
||||
href="/satspay/css/{{ charge_data.custom_css }}"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<style>
|
||||
header button.q-btn-dropdown {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block scripts %}
|
||||
|
||||
<script src="https://mempool.space/mempool.js"></script>
|
||||
|
@ -303,7 +321,8 @@
|
|||
data() {
|
||||
return {
|
||||
charge: JSON.parse('{{charge_data | tojson}}'),
|
||||
mempool_endpoint: '{{mempool_endpoint}}',
|
||||
mempoolEndpoint: '{{mempool_endpoint}}',
|
||||
network: '{{network}}',
|
||||
pendingFunds: 0,
|
||||
ws: null,
|
||||
newProgress: 0.4,
|
||||
|
@ -316,19 +335,19 @@
|
|||
cancelListener: () => {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startPaymentNotifier() {
|
||||
this.cancelListener()
|
||||
if (!this.lnbitswallet) return
|
||||
this.cancelListener = LNbits.events.onInvoicePaid(
|
||||
this.wallet,
|
||||
payment => {
|
||||
this.checkInvoiceBalance()
|
||||
computed: {
|
||||
mempoolHostname: function () {
|
||||
let hostname = new URL(this.mempoolEndpoint).hostname
|
||||
if (this.network === 'Testnet') {
|
||||
hostname += '/testnet'
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
checkBalances: async function () {
|
||||
if (this.charge.hasStaleBalance) return
|
||||
if (!this.charge.payment_request && this.charge.hasOnchainStaleBalance)
|
||||
return
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
|
@ -345,7 +364,7 @@
|
|||
const {
|
||||
bitcoin: {addresses: addressesAPI}
|
||||
} = mempoolJS({
|
||||
hostname: new URL(this.mempool_endpoint).hostname
|
||||
hostname: new URL(this.mempoolEndpoint).hostname
|
||||
})
|
||||
|
||||
try {
|
||||
|
@ -353,7 +372,8 @@
|
|||
address: this.charge.onchainaddress
|
||||
})
|
||||
const newBalance = utxos.reduce((t, u) => t + u.value, 0)
|
||||
this.charge.hasStaleBalance = this.charge.balance === newBalance
|
||||
this.charge.hasOnchainStaleBalance =
|
||||
this.charge.balance === newBalance
|
||||
|
||||
this.pendingFunds = utxos
|
||||
.filter(u => !u.status.confirmed)
|
||||
|
@ -388,10 +408,10 @@
|
|||
const {
|
||||
bitcoin: {websocket}
|
||||
} = mempoolJS({
|
||||
hostname: new URL(this.mempool_endpoint).hostname
|
||||
hostname: new URL(this.mempoolEndpoint).hostname
|
||||
})
|
||||
|
||||
this.ws = new WebSocket('wss://mempool.space/api/v1/ws')
|
||||
this.ws = new WebSocket(`wss://${this.mempoolHostname}/api/v1/ws`)
|
||||
this.ws.addEventListener('open', x => {
|
||||
if (this.charge.onchainaddress) {
|
||||
this.trackAddress(this.charge.onchainaddress)
|
||||
|
@ -428,13 +448,14 @@
|
|||
}
|
||||
},
|
||||
created: async function () {
|
||||
if (this.charge.lnbitswallet) this.payInvoice()
|
||||
// Remove a user defined theme
|
||||
if (this.charge.custom_css) {
|
||||
document.body.setAttribute('data-theme', '')
|
||||
}
|
||||
if (this.charge.payment_request) this.payInvoice()
|
||||
else this.payOnchain()
|
||||
await this.checkBalances()
|
||||
|
||||
// empty for onchain
|
||||
this.wallet.inkey = '{{ wallet_inkey }}'
|
||||
this.startPaymentNotifier()
|
||||
await this.checkBalances()
|
||||
|
||||
if (!this.charge.paid) {
|
||||
this.loopRefresh()
|
||||
|
|
|
@ -8,6 +8,26 @@
|
|||
<q-btn unelevated color="primary" @click="formDialogCharge.show = true"
|
||||
>New charge
|
||||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
v-if="admin == 'True'"
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="getThemes();formDialogThemes.show = true"
|
||||
>New CSS Theme
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
disable
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="getThemes();formDialogThemes.show = true"
|
||||
>New CSS Theme
|
||||
<q-tooltip
|
||||
>For security reason, custom css is only available to server
|
||||
admins.</q-tooltip
|
||||
></q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
|
@ -203,9 +223,14 @@
|
|||
:href="props.row.webhook"
|
||||
target="_blank"
|
||||
style="color: unset; text-decoration: none"
|
||||
>{{props.row.webhook || props.row.webhook}}</a
|
||||
>{{props.row.webhook}}</a
|
||||
>
|
||||
</div>
|
||||
<div class="col-4 q-pr-lg">
|
||||
<q-badge v-if="props.row.webhook_message" color="blue">
|
||||
{{props.row.webhook_message }}
|
||||
</q-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row items-center q-mt-md q-mb-lg">
|
||||
<div class="col-2 q-pr-lg">ID:</div>
|
||||
|
@ -254,6 +279,63 @@
|
|||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card v-if="admin == 'True'">
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Themes</h5>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="themeLinks"
|
||||
row-key="id"
|
||||
:columns="customCSSTable.columns"
|
||||
:pagination.sync="customCSSTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="updateformDialog(props.row.css_id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteTheme(props.row.css_id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
|
@ -298,32 +380,6 @@
|
|||
>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.webhook"
|
||||
type="url"
|
||||
label="Webhook (URL to send transaction data to once paid)"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.completelink"
|
||||
type="url"
|
||||
label="Completed button URL"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.completelinktext"
|
||||
type="text"
|
||||
label="Completed button text (ie 'Back to merchant')"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div v-if="walletLinks.length > 0">
|
||||
|
@ -372,6 +428,52 @@
|
|||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-toggle
|
||||
v-model="showAdvanced"
|
||||
label="Show advanced options"
|
||||
></q-toggle>
|
||||
<div v-if="showAdvanced" class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.webhook"
|
||||
type="url"
|
||||
label="Webhook (URL to send transaction data to once paid)"
|
||||
class="q-mt-lg"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.completelink"
|
||||
type="url"
|
||||
label="Completed button URL"
|
||||
class="q-mt-lg"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.completelinktext"
|
||||
type="text"
|
||||
label="Completed button text (ie 'Back to merchant')"
|
||||
class="q-mt-lg"
|
||||
>
|
||||
</q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialogCharge.data.custom_css"
|
||||
:options="themeOptions"
|
||||
label="Custom CSS theme (optional)"
|
||||
class="q-mt-lg"
|
||||
>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
|
@ -389,6 +491,43 @@
|
|||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="formDialogThemes.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormDataThemes" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogThemes.data.title"
|
||||
type="text"
|
||||
label="*Title"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogThemes.data.custom_css"
|
||||
type="textarea"
|
||||
label="Custom CSS"
|
||||
>
|
||||
</q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialogThemes.data.css_id"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update CSS theme</q-btn
|
||||
>
|
||||
<q-btn v-else unelevated color="primary" type="submit"
|
||||
>Save CSS theme</q-btn
|
||||
>
|
||||
<q-btn @click="cancelThemes" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<!-- lnbits/static/vendor
|
||||
|
@ -405,15 +544,21 @@
|
|||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
settings: {},
|
||||
filter: '',
|
||||
admin: '{{ admin }}',
|
||||
balance: null,
|
||||
walletLinks: [],
|
||||
chargeLinks: [],
|
||||
themeLinks: [],
|
||||
themeOptions: [],
|
||||
onchainwallet: '',
|
||||
rescanning: false,
|
||||
mempool: {
|
||||
endpoint: ''
|
||||
endpoint: '',
|
||||
network: 'Mainnet'
|
||||
},
|
||||
showAdvanced: false,
|
||||
|
||||
chargesTable: {
|
||||
columns: [
|
||||
|
@ -488,7 +633,25 @@
|
|||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
|
||||
customCSSTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'css_id',
|
||||
align: 'left',
|
||||
label: 'ID',
|
||||
field: 'css_id'
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
align: 'left',
|
||||
label: 'Title',
|
||||
field: 'title'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialogCharge: {
|
||||
show: false,
|
||||
data: {
|
||||
|
@ -496,20 +659,33 @@
|
|||
onchainwallet: '',
|
||||
lnbits: false,
|
||||
description: '',
|
||||
custom_css: '',
|
||||
time: null,
|
||||
amount: null
|
||||
}
|
||||
},
|
||||
formDialogThemes: {
|
||||
show: false,
|
||||
data: {
|
||||
custom_css: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelThemes: function (data) {
|
||||
this.formDialogCharge.data.custom_css = ''
|
||||
this.formDialogThemes.show = false
|
||||
},
|
||||
cancelCharge: function (data) {
|
||||
this.formDialogCharge.data.description = ''
|
||||
this.formDialogCharge.data.onchain = false
|
||||
this.formDialogCharge.data.onchainwallet = ''
|
||||
this.formDialogCharge.data.lnbitswallet = ''
|
||||
this.formDialogCharge.data.time = null
|
||||
this.formDialogCharge.data.amount = null
|
||||
this.formDialogCharge.data.webhook = ''
|
||||
this.formDialogCharge.data.custom_css = ''
|
||||
this.formDialogCharge.data.completelink = ''
|
||||
this.formDialogCharge.show = false
|
||||
},
|
||||
|
@ -518,7 +694,7 @@
|
|||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/wallet',
|
||||
`/watchonly/api/v1/wallet?network=${this.mempool.network}`,
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
this.walletLinks = data.map(w => ({
|
||||
|
@ -538,6 +714,7 @@
|
|||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
this.mempool.endpoint = data.mempool_endpoint
|
||||
this.mempool.network = data.network || 'Mainnet'
|
||||
const url = new URL(this.mempool.endpoint)
|
||||
this.mempool.hostname = url.hostname
|
||||
} catch (error) {
|
||||
|
@ -572,12 +749,43 @@
|
|||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
sendFormDataCharge: function () {
|
||||
|
||||
getThemes: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/satspay/api/v1/themes',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
console.log(data)
|
||||
this.themeLinks = data.map(c =>
|
||||
mapCSS(
|
||||
c,
|
||||
this.themeLinks.find(old => old.css_id === c.css_id)
|
||||
)
|
||||
)
|
||||
this.themeOptions = data.map(w => ({
|
||||
id: w.css_id,
|
||||
label: w.title + ' - ' + w.css_id
|
||||
}))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
|
||||
sendFormDataThemes: function () {
|
||||
const wallet = this.g.user.wallets[0].inkey
|
||||
const data = this.formDialogThemes.data
|
||||
this.createTheme(wallet, data)
|
||||
},
|
||||
sendFormDataCharge: function () {
|
||||
this.formDialogCharge.data.custom_css = this.formDialogCharge.data.custom_css?.id
|
||||
const data = this.formDialogCharge.data
|
||||
const wallet = this.g.user.wallets[0].inkey
|
||||
data.amount = parseInt(data.amount)
|
||||
data.time = parseInt(data.time)
|
||||
data.onchainwallet = this.onchainwallet?.id
|
||||
data.lnbitswallet = data.lnbits ? data.lnbitswallet : null
|
||||
data.onchainwallet = data.onchain ? this.onchainwallet?.id : null
|
||||
this.createCharge(wallet, data)
|
||||
},
|
||||
refreshActiveChargesBalance: async function () {
|
||||
|
@ -642,6 +850,68 @@
|
|||
this.rescanning = false
|
||||
}
|
||||
},
|
||||
updateformDialog: function (themeId) {
|
||||
const theme = _.findWhere(this.themeLinks, {css_id: themeId})
|
||||
console.log(theme.css_id)
|
||||
this.formDialogThemes.data.css_id = theme.css_id
|
||||
this.formDialogThemes.data.title = theme.title
|
||||
this.formDialogThemes.data.custom_css = theme.custom_css
|
||||
this.formDialogThemes.show = true
|
||||
},
|
||||
createTheme: async function (wallet, data) {
|
||||
console.log(data.css_id)
|
||||
try {
|
||||
if (data.css_id) {
|
||||
const resp = await LNbits.api.request(
|
||||
'POST',
|
||||
'/satspay/api/v1/themes/' + data.css_id,
|
||||
wallet,
|
||||
data
|
||||
)
|
||||
this.themeLinks = _.reject(this.themeLinks, function (obj) {
|
||||
return obj.css_id === data.css_id
|
||||
})
|
||||
this.themeLinks.unshift(mapCSS(resp.data))
|
||||
} else {
|
||||
const resp = await LNbits.api.request(
|
||||
'POST',
|
||||
'/satspay/api/v1/themes',
|
||||
wallet,
|
||||
data
|
||||
)
|
||||
this.themeLinks.unshift(mapCSS(resp.data))
|
||||
}
|
||||
this.formDialogThemes.show = false
|
||||
this.formDialogThemes.data = {
|
||||
title: '',
|
||||
custom_css: ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('cun')
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
|
||||
deleteTheme: function (themeId) {
|
||||
const theme = _.findWhere(this.themeLinks, {id: themeId})
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this theme?')
|
||||
.onOk(async () => {
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/satspay/api/v1/themes/' + themeId,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
|
||||
this.themeLinks = _.reject(this.themeLinks, function (obj) {
|
||||
return obj.css_id === themeId
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
createCharge: async function (wallet, data) {
|
||||
try {
|
||||
const resp = await LNbits.api.request(
|
||||
|
@ -694,9 +964,12 @@
|
|||
}
|
||||
},
|
||||
created: async function () {
|
||||
if (this.admin == 'True') {
|
||||
await this.getThemes()
|
||||
}
|
||||
await this.getCharges()
|
||||
await this.getWalletLinks()
|
||||
await this.getWalletConfig()
|
||||
await this.getWalletLinks()
|
||||
setInterval(() => this.refreshActiveChargesBalance(), 10 * 2000)
|
||||
await this.rescanOnchainAddresses()
|
||||
setInterval(() => this.rescanOnchainAddresses(), 10 * 1000)
|
||||
|
|
|
@ -1,25 +1,32 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Response
|
||||
from fastapi.param_functions import Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
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.satspay.helpers import public_charge
|
||||
from lnbits.settings import LNBITS_ADMIN_USERS
|
||||
|
||||
from . import satspay_ext, satspay_renderer
|
||||
from .crud import get_charge, get_charge_config
|
||||
from .crud import get_charge, get_theme
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@satspay_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
admin = False
|
||||
if LNBITS_ADMIN_USERS and user.id in LNBITS_ADMIN_USERS:
|
||||
admin = True
|
||||
return satspay_renderer().TemplateResponse(
|
||||
"satspay/index.html", {"request": request, "user": user.dict()}
|
||||
"satspay/index.html", {"request": request, "user": user.dict(), "admin": admin}
|
||||
)
|
||||
|
||||
|
||||
|
@ -30,18 +37,21 @@ async def display(request: Request, charge_id: str):
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
||||
)
|
||||
wallet = await get_wallet(charge.lnbitswallet)
|
||||
onchainwallet_config = await get_charge_config(charge_id)
|
||||
inkey = wallet.inkey if wallet else None
|
||||
mempool_endpoint = (
|
||||
onchainwallet_config.mempool_endpoint if onchainwallet_config else None
|
||||
)
|
||||
|
||||
return satspay_renderer().TemplateResponse(
|
||||
"satspay/display.html",
|
||||
{
|
||||
"request": request,
|
||||
"charge_data": charge.dict(),
|
||||
"wallet_inkey": inkey,
|
||||
"mempool_endpoint": mempool_endpoint,
|
||||
"charge_data": public_charge(charge),
|
||||
"mempool_endpoint": charge.config.mempool_endpoint,
|
||||
"network": charge.config.network,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@satspay_ext.get("/css/{css_id}")
|
||||
async def display(css_id: str, response: Response):
|
||||
theme = await get_theme(css_id)
|
||||
if theme:
|
||||
return Response(content=theme.custom_css, media_type="text/css")
|
||||
return None
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
from fastapi.params import Depends
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
|
@ -11,17 +14,22 @@ from lnbits.decorators import (
|
|||
require_invoice_key,
|
||||
)
|
||||
from lnbits.extensions.satspay import satspay_ext
|
||||
from lnbits.settings import LNBITS_ADMIN_EXTENSIONS, LNBITS_ADMIN_USERS
|
||||
|
||||
from .crud import (
|
||||
check_address_balance,
|
||||
create_charge,
|
||||
delete_charge,
|
||||
delete_theme,
|
||||
get_charge,
|
||||
get_charges,
|
||||
get_theme,
|
||||
get_themes,
|
||||
save_theme,
|
||||
update_charge,
|
||||
)
|
||||
from .helpers import compact_charge
|
||||
from .models import CreateCharge
|
||||
from .helpers import call_webhook, public_charge
|
||||
from .models import CreateCharge, SatsPayThemes
|
||||
|
||||
#############################CHARGES##########################
|
||||
|
||||
|
@ -58,6 +66,7 @@ async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
|||
**{"time_elapsed": charge.time_elapsed},
|
||||
**{"time_left": charge.time_left},
|
||||
**{"paid": charge.paid},
|
||||
**{"webhook_message": charge.config.webhook_message},
|
||||
}
|
||||
for charge in await get_charges(wallet.wallet.user)
|
||||
]
|
||||
|
@ -94,7 +103,7 @@ async def api_charge_delete(charge_id, wallet: WalletTypeInfo = Depends(get_key_
|
|||
)
|
||||
|
||||
await delete_charge(charge_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#############################BALANCE##########################
|
||||
|
@ -119,19 +128,55 @@ async def api_charge_balance(charge_id):
|
|||
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=compact_charge(charge),
|
||||
timeout=40,
|
||||
if charge.must_call_webhook():
|
||||
resp = await call_webhook(charge)
|
||||
extra = {**charge.config.dict(), **resp}
|
||||
await update_charge(charge_id=charge.id, extra=json.dumps(extra))
|
||||
|
||||
return {**public_charge(charge)}
|
||||
|
||||
|
||||
#############################THEMES##########################
|
||||
|
||||
|
||||
@satspay_ext.post("/api/v1/themes")
|
||||
@satspay_ext.post("/api/v1/themes/{css_id}")
|
||||
async def api_themes_save(
|
||||
data: SatsPayThemes,
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
css_id: str = None,
|
||||
):
|
||||
if LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Only server admins can create themes.",
|
||||
)
|
||||
except AssertionError:
|
||||
charge.webhook = None
|
||||
return {
|
||||
**compact_charge(charge),
|
||||
**{"time_elapsed": charge.time_elapsed},
|
||||
**{"time_left": charge.time_left},
|
||||
**{"paid": charge.paid},
|
||||
}
|
||||
if css_id:
|
||||
theme = await save_theme(css_id=css_id, data=data)
|
||||
else:
|
||||
data.user = wallet.wallet.user
|
||||
theme = await save_theme(data=data)
|
||||
return theme
|
||||
|
||||
|
||||
@satspay_ext.get("/api/v1/themes")
|
||||
async def api_themes_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
try:
|
||||
return await get_themes(wallet.wallet.user)
|
||||
except HTTPException:
|
||||
logger.error("Error loading satspay themes")
|
||||
logger.error(HTTPException)
|
||||
return ""
|
||||
|
||||
|
||||
@satspay_ext.delete("/api/v1/themes/{theme_id}")
|
||||
async def api_charge_delete(theme_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
theme = await get_theme(theme_id)
|
||||
|
||||
if not theme:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Theme does not exist."
|
||||
)
|
||||
|
||||
await delete_theme(theme_id)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
@ -109,4 +109,4 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
)
|
||||
|
||||
await delete_scrub_link(link_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
@ -31,14 +31,20 @@
|
|||
style="flex-wrap: nowrap"
|
||||
v-for="(target, t) in targets"
|
||||
>
|
||||
<q-input
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
:options="g.user.wallets.filter(w => w.id !== selectedWallet.id).map(o => ({name: o.name, value: o.id}))"
|
||||
v-model="target.wallet"
|
||||
label="Wallet"
|
||||
:hint="t === targets.length - 1 ? 'A wallet ID or invoice key.' : undefined"
|
||||
@input="targetChanged(false)"
|
||||
></q-input>
|
||||
option-label="name"
|
||||
style="width: 1000px"
|
||||
new-value-mode="add-unique"
|
||||
use-input
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
></q-select>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
|
|
|
@ -245,7 +245,7 @@ async def api_delete_donation(donation_id, g: WalletTypeInfo = Depends(get_key_t
|
|||
detail="Not authorized to delete this donation!",
|
||||
)
|
||||
await delete_donation(donation_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@streamalerts_ext.delete("/api/v1/services/{service_id}")
|
||||
|
@ -262,4 +262,4 @@ async def api_delete_service(service_id, g: WalletTypeInfo = Depends(get_key_typ
|
|||
detail="Not authorized to delete this service!",
|
||||
)
|
||||
await delete_service(service_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
@ -81,7 +81,7 @@ async def api_domain_delete(
|
|||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain.")
|
||||
|
||||
await delete_domain(domain_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########subdomains##########
|
||||
|
@ -198,4 +198,4 @@ async def api_subdomain_delete(
|
|||
)
|
||||
|
||||
await delete_subdomain(subdomain_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
@ -11,5 +11,5 @@ An easy, fast and secure way to accept Bitcoin, over Lightning Network, at your
|
|||

|
||||
3. Open TPOS on the browser\
|
||||

|
||||
4. Present invoice QR to costumer\
|
||||
4. Present invoice QR to customer\
|
||||

|
||||
|
|
|
@ -139,8 +139,12 @@
|
|||
input-debounce="0"
|
||||
new-value-mode="add-unique"
|
||||
label="Tip % Options (hit enter to add values)"
|
||||
><q-tooltip>Hit enter to add values</q-tooltip></q-select
|
||||
>
|
||||
><q-tooltip>Hit enter to add values</q-tooltip>
|
||||
<template v-slot:hint>
|
||||
You can leave this blank. A default rounding option is available
|
||||
(round amount to a value)
|
||||
</template>
|
||||
</q-select>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<q-page-sticky v-if="exchangeRate" expand position="top">
|
||||
<div class="row justify-center full-width">
|
||||
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
|
||||
<h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
|
||||
<h3 class="q-mb-md">{% raw %}{{ amountFormatted }}{% endraw %}</h3>
|
||||
<h5 class="q-mt-none q-mb-sm">
|
||||
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
|
||||
</h5>
|
||||
|
@ -148,6 +148,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</q-page-sticky>
|
||||
<q-page-sticky position="top-right" :offset="[18, 18]">
|
||||
<q-btn
|
||||
@click="showLastPayments"
|
||||
fab
|
||||
icon="receipt_long"
|
||||
color="primary"
|
||||
/>
|
||||
</q-page-sticky>
|
||||
<q-dialog
|
||||
v-model="invoiceDialog.show"
|
||||
position="top"
|
||||
|
@ -165,12 +173,14 @@
|
|||
></qrcode>
|
||||
</q-responsive>
|
||||
<div class="text-center">
|
||||
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
|
||||
<h3 class="q-my-md">
|
||||
{% raw %}{{ amountWithTipFormatted }}{% endraw %}
|
||||
</h3>
|
||||
<h5 class="q-mt-none">
|
||||
{% raw %}{{ fsat }}
|
||||
<small>sat</small>
|
||||
<span v-show="tip_options" style="font-size: 0.75rem"
|
||||
>( + {{ tipAmountSat }} tip)</span
|
||||
>( + {{ tipAmountFormatted }} tip)</span
|
||||
>
|
||||
{% endraw %}
|
||||
</h5>
|
||||
|
@ -204,19 +214,48 @@
|
|||
style="padding: 10px; margin: 3px"
|
||||
unelevated
|
||||
@click="processTipSelection(tip)"
|
||||
size="xl"
|
||||
size="lg"
|
||||
:outline="!($q.dark.isActive)"
|
||||
rounded
|
||||
color="primary"
|
||||
v-for="tip in this.tip_options"
|
||||
v-for="tip in tip_options.filter(f => f != 'Round')"
|
||||
:key="tip"
|
||||
>{% raw %}{{ tip }}{% endraw %}%</q-btn
|
||||
>
|
||||
<q-btn
|
||||
style="padding: 10px; margin: 3px"
|
||||
unelevated
|
||||
@click="setRounding"
|
||||
size="lg"
|
||||
:outline="!($q.dark.isActive)"
|
||||
rounded
|
||||
color="primary"
|
||||
label="Round to"
|
||||
></q-btn>
|
||||
<div class="row q-my-lg" v-if="rounding">
|
||||
<q-input
|
||||
class="col"
|
||||
ref="inputRounding"
|
||||
v-model.number="tipRounding"
|
||||
:placeholder="roundToSugestion"
|
||||
type="number"
|
||||
hint="Total amount including tip"
|
||||
:prefix="currency"
|
||||
>
|
||||
</q-input>
|
||||
<q-btn
|
||||
class="q-ml-sm"
|
||||
style="margin-bottom: 20px"
|
||||
color="primary"
|
||||
@click="calculatePercent"
|
||||
>Ok</q-btn
|
||||
>
|
||||
</div>
|
||||
<div class="text-center q-mb-xl">
|
||||
<p><a @click="processTipSelection(0)"> No, thanks</a></p>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn flat color="primary" @click="processTipSelection(0)"
|
||||
>No, thanks</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
|
@ -256,6 +295,38 @@
|
|||
style="font-size: min(90vw, 40em)"
|
||||
></q-icon>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="lastPaymentsDialog.show" position="bottom">
|
||||
<q-card class="lnbits__dialog-card">
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-list separator class="q-mb-lg">
|
||||
<q-item v-if="!lastPaymentsDialog.data.length">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-bold">No paid invoices</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-for="(payment, idx) in lastPaymentsDialog.data" :key="idx">
|
||||
{%raw%}
|
||||
<q-item-section>
|
||||
<q-item-label class="text-bold"
|
||||
>{{payment.amount / 1000}} sats</q-item-label
|
||||
>
|
||||
<q-item-label caption lines="2"
|
||||
>Hash: {{payment.checking_id.slice(0, 30)}}...</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
<q-item-section side top>
|
||||
<q-item-label caption>{{payment.dateFrom}}</q-item-label>
|
||||
<q-icon name="check" color="green" />
|
||||
</q-item-section>
|
||||
{%endraw%}
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
{% endblock %} {% block styles %}
|
||||
|
@ -294,8 +365,13 @@
|
|||
exchangeRate: null,
|
||||
stack: [],
|
||||
tipAmount: 0.0,
|
||||
tipRounding: null,
|
||||
hasNFC: false,
|
||||
nfcTagReading: false,
|
||||
lastPaymentsDialog: {
|
||||
show: false,
|
||||
data: []
|
||||
},
|
||||
invoiceDialog: {
|
||||
show: false,
|
||||
data: null,
|
||||
|
@ -310,32 +386,81 @@
|
|||
},
|
||||
complete: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
rounding: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
amount: function () {
|
||||
if (!this.stack.length) return 0.0
|
||||
return (Number(this.stack.join('')) / 100).toFixed(2)
|
||||
return Number(this.stack.join('') / 100)
|
||||
},
|
||||
famount: function () {
|
||||
return LNbits.utils.formatCurrency(this.amount, this.currency)
|
||||
amountFormatted: function () {
|
||||
return LNbits.utils.formatCurrency(
|
||||
this.amount.toFixed(2),
|
||||
this.currency
|
||||
)
|
||||
},
|
||||
amountWithTipFormatted: function () {
|
||||
return LNbits.utils.formatCurrency(
|
||||
(this.amount + this.tipAmount).toFixed(2),
|
||||
this.currency
|
||||
)
|
||||
},
|
||||
sat: function () {
|
||||
if (!this.exchangeRate) return 0
|
||||
return Math.ceil(
|
||||
((this.amount - this.tipAmount) / this.exchangeRate) * 100000000
|
||||
)
|
||||
return Math.ceil((this.amount / this.exchangeRate) * 100000000)
|
||||
},
|
||||
tipAmountSat: function () {
|
||||
if (!this.exchangeRate) return 0
|
||||
return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000)
|
||||
},
|
||||
tipAmountFormatted: function () {
|
||||
return LNbits.utils.formatSat(this.tipAmountSat)
|
||||
},
|
||||
fsat: function () {
|
||||
return LNbits.utils.formatSat(this.sat)
|
||||
},
|
||||
isRoundValid() {
|
||||
return this.tipRounding > this.amount
|
||||
},
|
||||
roundToSugestion() {
|
||||
switch (true) {
|
||||
case this.amount > 50:
|
||||
toNext = 10
|
||||
break
|
||||
case this.amount > 6:
|
||||
toNext = 5
|
||||
break
|
||||
case this.amount > 2.5:
|
||||
toNext = 1
|
||||
break
|
||||
default:
|
||||
toNext = 0.5
|
||||
break
|
||||
}
|
||||
|
||||
return Math.ceil(this.amount / toNext) * toNext
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setRounding() {
|
||||
this.rounding = true
|
||||
this.tipRounding = this.roundToSugestion
|
||||
this.$nextTick(() => this.$refs.inputRounding.focus())
|
||||
},
|
||||
calculatePercent() {
|
||||
let change = ((this.tipRounding - this.amount) / this.amount) * 100
|
||||
if (change < 0) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Amount with tip must be greater than initial amount.'
|
||||
})
|
||||
this.tipRounding = this.roundToSugestion
|
||||
return
|
||||
}
|
||||
this.processTipSelection(change)
|
||||
},
|
||||
closeInvoiceDialog: function () {
|
||||
this.stack = []
|
||||
this.tipAmount = 0.0
|
||||
|
@ -348,30 +473,18 @@
|
|||
processTipSelection: function (selectedTipOption) {
|
||||
this.tipDialog.show = false
|
||||
|
||||
if (selectedTipOption) {
|
||||
const tipAmount = parseFloat(
|
||||
parseFloat((selectedTipOption / 100) * this.amount)
|
||||
)
|
||||
const subtotal = parseFloat(this.amount)
|
||||
const grandTotal = parseFloat((tipAmount + subtotal).toFixed(2))
|
||||
const totalString = grandTotal.toFixed(2).toString()
|
||||
|
||||
this.stack = []
|
||||
for (var i = 0; i < totalString.length; i++) {
|
||||
const char = totalString[i]
|
||||
|
||||
if (char !== '.') {
|
||||
this.stack.push(char)
|
||||
}
|
||||
}
|
||||
|
||||
this.tipAmount = tipAmount
|
||||
if (!selectedTipOption) {
|
||||
this.tipAmount = 0.0
|
||||
return this.showInvoice()
|
||||
}
|
||||
|
||||
this.tipAmount = (selectedTipOption / 100) * this.amount
|
||||
this.showInvoice()
|
||||
},
|
||||
submitForm: function () {
|
||||
if (this.tip_options && this.tip_options.length) {
|
||||
this.rounding = false
|
||||
this.tipRounding = null
|
||||
this.showTipModal()
|
||||
} else {
|
||||
this.showInvoice()
|
||||
|
@ -520,6 +633,24 @@
|
|||
self.exchangeRate =
|
||||
response.data.data['BTC' + self.currency][self.currency]
|
||||
})
|
||||
},
|
||||
getLastPayments() {
|
||||
return axios
|
||||
.get(`/tpos/api/v1/tposs/${this.tposId}/invoices`)
|
||||
.then(res => {
|
||||
if (res.data && res.data.length) {
|
||||
let last = [...res.data]
|
||||
this.lastPaymentsDialog.data = last.map(obj => {
|
||||
obj.dateFrom = moment(obj.time * 1000).fromNow()
|
||||
return obj
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
},
|
||||
showLastPayments() {
|
||||
this.getLastPayments()
|
||||
this.lastPaymentsDialog.show = true
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
|
@ -529,10 +660,26 @@
|
|||
'{{ tpos.tip_options | tojson }}' == 'null'
|
||||
? null
|
||||
: JSON.parse('{{ tpos.tip_options }}')
|
||||
|
||||
if ('{{ tpos.tip_wallet }}') {
|
||||
this.tip_options.push('Round')
|
||||
}
|
||||
setInterval(function () {
|
||||
getRates()
|
||||
}, 120000)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,7 +7,8 @@ from lnurl import decode as decode_lnurl
|
|||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.crud import get_latest_payments_by_extension, get_user
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.core.views.api import api_payment
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
|
@ -51,7 +52,7 @@ async def api_tpos_delete(
|
|||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your TPoS.")
|
||||
|
||||
await delete_tpos(tpos_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices", status_code=HTTPStatus.CREATED)
|
||||
|
@ -81,6 +82,30 @@ async def api_tpos_create_invoice(
|
|||
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
||||
|
||||
|
||||
@tpos_ext.get("/api/v1/tposs/{tpos_id}/invoices")
|
||||
async def api_tpos_get_latest_invoices(tpos_id: str = None):
|
||||
try:
|
||||
payments = [
|
||||
Payment.from_row(row)
|
||||
for row in await get_latest_payments_by_extension(
|
||||
ext_name="tpos", ext_id=tpos_id
|
||||
)
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
return [
|
||||
{
|
||||
"checking_id": payment.checking_id,
|
||||
"amount": payment.amount,
|
||||
"time": payment.time,
|
||||
"pending": payment.pending,
|
||||
}
|
||||
for payment in payments
|
||||
]
|
||||
|
||||
|
||||
@tpos_ext.post(
|
||||
"/api/v1/tposs/{tpos_id}/invoices/{payment_request}/pay", status_code=HTTPStatus.OK
|
||||
)
|
||||
|
|
|
@ -3,25 +3,25 @@ async def m001_initial(db):
|
|||
Initial wallet table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
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
|
||||
balance {db.big_int} NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE watchonly.addresses (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
address TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL
|
||||
amount {db.big_int} NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
filled
|
||||
dense
|
||||
v-model.number="feeRate"
|
||||
step="any"
|
||||
:rules="[val => !!val || 'Field is required']"
|
||||
type="number"
|
||||
label="sats/vbyte"
|
||||
|
|
|
@ -9,7 +9,7 @@ from fastapi import HTTPException
|
|||
from fastapi.param_functions import Query
|
||||
from loguru import logger
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse # type: ignore
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.services import pay_invoice
|
||||
|
||||
|
@ -51,10 +51,24 @@ async def api_lnurl_response(request: Request, unique_hash):
|
|||
# CALLBACK
|
||||
|
||||
|
||||
@withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", name="withdraw.api_lnurl_callback")
|
||||
@withdraw_ext.get(
|
||||
"/api/v1/lnurl/cb/{unique_hash}",
|
||||
name="withdraw.api_lnurl_callback",
|
||||
summary="lnurl withdraw callback",
|
||||
description="""
|
||||
This enpoints allows you to put unique_hash, k1
|
||||
and a payment_request to get your payment_request paid.
|
||||
""",
|
||||
response_description="JSON with status",
|
||||
responses={
|
||||
200: {"description": "status: OK"},
|
||||
400: {"description": "k1 is wrong or link open time or withdraw not working."},
|
||||
404: {"description": "withdraw link not found."},
|
||||
405: {"description": "withdraw link is spent."},
|
||||
},
|
||||
)
|
||||
async def api_lnurl_callback(
|
||||
unique_hash,
|
||||
request: Request,
|
||||
k1: str = Query(...),
|
||||
pr: str = Query(...),
|
||||
id_unique_hash=None,
|
||||
|
@ -63,19 +77,22 @@ async def api_lnurl_callback(
|
|||
now = int(datetime.now().timestamp())
|
||||
if not link:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found"
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found."
|
||||
)
|
||||
|
||||
if link.is_spent:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent."
|
||||
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="withdraw is spent."
|
||||
)
|
||||
|
||||
if link.k1 != k1:
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Bad request.")
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="k1 is wrong.")
|
||||
|
||||
if now < link.open_time:
|
||||
return {"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"wait link open_time {link.open_time - now} seconds.",
|
||||
)
|
||||
|
||||
usescsv = ""
|
||||
|
||||
|
@ -95,7 +112,7 @@ async def api_lnurl_callback(
|
|||
usescsv = ",".join(useslist)
|
||||
if not found:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found."
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found."
|
||||
)
|
||||
else:
|
||||
usescsv = usescsv[1:]
|
||||
|
@ -144,7 +161,9 @@ async def api_lnurl_callback(
|
|||
except Exception as e:
|
||||
await update_withdraw_link(link.id, **changesback)
|
||||
logger.error(traceback.format_exc())
|
||||
return {"status": "ERROR", "reason": "Link not working"}
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# FOR LNURLs WHICH ARE UNIQUE
|
||||
|
|
|
@ -3,13 +3,13 @@ async def m001_initial(db):
|
|||
Creates an improved withdraw table and migrates the existing data.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE withdraw.withdraw_links (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT,
|
||||
title TEXT,
|
||||
min_withdrawable INTEGER DEFAULT 1,
|
||||
max_withdrawable INTEGER DEFAULT 1,
|
||||
min_withdrawable {db.big_int} DEFAULT 1,
|
||||
max_withdrawable {db.big_int} DEFAULT 1,
|
||||
uses INTEGER DEFAULT 1,
|
||||
wait_time INTEGER,
|
||||
is_unique INTEGER DEFAULT 0,
|
||||
|
@ -28,13 +28,13 @@ async def m002_change_withdraw_table(db):
|
|||
Creates an improved withdraw table and migrates the existing data.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE withdraw.withdraw_link (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT,
|
||||
title TEXT,
|
||||
min_withdrawable INTEGER DEFAULT 1,
|
||||
max_withdrawable INTEGER DEFAULT 1,
|
||||
min_withdrawable {db.big_int} DEFAULT 1,
|
||||
max_withdrawable {db.big_int} DEFAULT 1,
|
||||
uses INTEGER DEFAULT 1,
|
||||
wait_time INTEGER,
|
||||
is_unique INTEGER DEFAULT 0,
|
||||
|
|
|
@ -290,8 +290,12 @@ new Vue({
|
|||
})
|
||||
}
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls)
|
||||
exportCSV() {
|
||||
LNbits.utils.exportCSV(
|
||||
this.withdrawLinksTable.columns,
|
||||
this.withdrawLinks,
|
||||
'withdraw-links'
|
||||
)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
|
|
|
@ -163,6 +163,7 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates:
|
|||
)
|
||||
|
||||
if settings.LNBITS_AD_SPACE:
|
||||
t.env.globals["AD_TITLE"] = settings.LNBITS_AD_SPACE_TITLE
|
||||
t.env.globals["AD_SPACE"] = settings.LNBITS_AD_SPACE
|
||||
t.env.globals["HIDE_API"] = settings.LNBITS_HIDE_API
|
||||
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import time
|
||||
|
||||
import click
|
||||
import uvicorn
|
||||
|
||||
from lnbits.settings import HOST, PORT
|
||||
from lnbits.settings import FORWARDED_ALLOW_IPS, HOST, PORT
|
||||
|
||||
|
||||
@click.command(
|
||||
|
@ -14,10 +12,20 @@ from lnbits.settings import HOST, PORT
|
|||
)
|
||||
@click.option("--port", default=PORT, help="Port to listen on")
|
||||
@click.option("--host", default=HOST, help="Host to run LNBits on")
|
||||
@click.option(
|
||||
"--forwarded-allow-ips", default=FORWARDED_ALLOW_IPS, help="Allowed proxy servers"
|
||||
)
|
||||
@click.option("--ssl-keyfile", default=None, help="Path to SSL keyfile")
|
||||
@click.option("--ssl-certfile", default=None, help="Path to SSL certificate")
|
||||
@click.pass_context
|
||||
def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str):
|
||||
def main(
|
||||
ctx,
|
||||
port: int,
|
||||
host: str,
|
||||
forwarded_allow_ips: str,
|
||||
ssl_keyfile: str,
|
||||
ssl_certfile: str,
|
||||
):
|
||||
"""Launched with `poetry run lnbits` at root level"""
|
||||
# this beautiful beast parses all command line arguments and passes them to the uvicorn server
|
||||
d = dict()
|
||||
|
@ -37,6 +45,7 @@ def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str):
|
|||
"lnbits.__main__:app",
|
||||
port=port,
|
||||
host=host,
|
||||
forwarded_allow_ips=forwarded_allow_ips,
|
||||
ssl_keyfile=ssl_keyfile,
|
||||
ssl_certfile=ssl_certfile,
|
||||
**d
|
||||
|
|
|
@ -18,6 +18,8 @@ DEBUG = env.bool("DEBUG", default=False)
|
|||
HOST = env.str("HOST", default="127.0.0.1")
|
||||
PORT = env.int("PORT", default=5000)
|
||||
|
||||
FORWARDED_ALLOW_IPS = env.str("FORWARDED_ALLOW_IPS", default="127.0.0.1")
|
||||
|
||||
LNBITS_PATH = path.dirname(path.realpath(__file__))
|
||||
LNBITS_DATA_FOLDER = env.str(
|
||||
"LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data")
|
||||
|
@ -38,6 +40,9 @@ LNBITS_DISABLED_EXTENSIONS: List[str] = [
|
|||
for x in env.list("LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str)
|
||||
]
|
||||
|
||||
LNBITS_AD_SPACE_TITLE = env.str(
|
||||
"LNBITS_AD_SPACE_TITLE", default="Optional Advert Space"
|
||||
)
|
||||
LNBITS_AD_SPACE = [x.strip(" ") for x in env.list("LNBITS_AD_SPACE", default=[])]
|
||||
LNBITS_HIDE_API = env.bool("LNBITS_HIDE_API", default=False)
|
||||
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
|
||||
|
|
BIN
lnbits/static/images/lnbits-shop-dark.png
Normal file
BIN
lnbits/static/images/lnbits-shop-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
lnbits/static/images/lnbits-shop-light.png
Normal file
BIN
lnbits/static/images/lnbits-shop-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -124,7 +124,7 @@ async def check_pending_payments():
|
|||
|
||||
while True:
|
||||
async with db.connect() as conn:
|
||||
logger.debug(
|
||||
logger.info(
|
||||
f"Task: checking all pending payments (incoming={incoming}, outgoing={outgoing}) of last 15 days"
|
||||
)
|
||||
start_time: float = time.time()
|
||||
|
@ -140,15 +140,15 @@ async def check_pending_payments():
|
|||
for payment in pending_payments:
|
||||
await payment.check_status(conn=conn)
|
||||
|
||||
logger.debug(
|
||||
logger.info(
|
||||
f"Task: pending check finished for {len(pending_payments)} payments (took {time.time() - start_time:0.3f} s)"
|
||||
)
|
||||
# we delete expired invoices once upon the first pending check
|
||||
if incoming:
|
||||
logger.debug("Task: deleting all expired invoices")
|
||||
logger.info("Task: deleting all expired invoices")
|
||||
start_time: float = time.time()
|
||||
await delete_expired_invoices(conn=conn)
|
||||
logger.debug(
|
||||
logger.info(
|
||||
f"Task: expired invoice deletion finished (took {time.time() - start_time:0.3f} s)"
|
||||
)
|
||||
|
||||
|
|
|
@ -199,6 +199,18 @@
|
|||
>
|
||||
</q-toolbar-title>
|
||||
<q-space></q-space>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
:color="($q.dark.isActive) ? 'white' : 'primary'"
|
||||
type="a"
|
||||
href="/docs"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
API DOCS
|
||||
<q-tooltip>View LNbits Swagger API docs</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
|
|
|
@ -95,7 +95,6 @@ exclude = """(?x)(
|
|||
| ^lnbits/extensions/events.
|
||||
| ^lnbits/extensions/hivemind.
|
||||
| ^lnbits/extensions/invoices.
|
||||
| ^lnbits/extensions/jukebox.
|
||||
| ^lnbits/extensions/livestream.
|
||||
| ^lnbits/extensions/lnaddress.
|
||||
| ^lnbits/extensions/lndhub.
|
||||
|
@ -103,10 +102,8 @@ exclude = """(?x)(
|
|||
| ^lnbits/extensions/lnurldevice.
|
||||
| ^lnbits/extensions/lnurlp.
|
||||
| ^lnbits/extensions/lnurlpayout.
|
||||
| ^lnbits/extensions/ngrok.
|
||||
| ^lnbits/extensions/offlineshop.
|
||||
| ^lnbits/extensions/paywall.
|
||||
| ^lnbits/extensions/satsdice.
|
||||
| ^lnbits/extensions/satspay.
|
||||
| ^lnbits/extensions/scrub.
|
||||
| ^lnbits/extensions/splitpayments.
|
||||
|
|
127
requirements.txt
127
requirements.txt
|
@ -1,55 +1,72 @@
|
|||
aiofiles==0.8.0
|
||||
anyio==3.6.1
|
||||
asyncio==3.4.3
|
||||
attrs==21.4.0
|
||||
bech32==1.2.0
|
||||
bitstring==3.1.9
|
||||
cerberus==1.3.4
|
||||
certifi==2022.6.15
|
||||
cffi==1.15.0
|
||||
click==8.1.3
|
||||
ecdsa==0.18.0
|
||||
embit==0.5.0
|
||||
environs==9.5.0
|
||||
fastapi==0.79.0
|
||||
h11==0.12.0
|
||||
httpcore==0.15.0
|
||||
httptools==0.4.0
|
||||
httpx==0.23.0
|
||||
idna==3.3
|
||||
jinja2==3.0.1
|
||||
lnurl==0.3.6
|
||||
loguru==0.6.0
|
||||
markupsafe==2.1.1
|
||||
marshmallow==3.17.0
|
||||
outcome==1.2.0
|
||||
packaging==21.3
|
||||
psycopg2-binary==2.9.3
|
||||
pycparser==2.21
|
||||
pycryptodomex==3.15.0
|
||||
pydantic==1.9.1
|
||||
pyngrok==5.1.0
|
||||
pyparsing==3.0.9
|
||||
pypng==0.20220715.0
|
||||
pyqrcode==1.2.1
|
||||
pyscss==1.4.0
|
||||
python-dotenv==0.20.0
|
||||
pyyaml==6.0
|
||||
represent==1.6.0.post0
|
||||
rfc3986==1.5.0
|
||||
secp256k1==0.14.0
|
||||
shortuuid==1.0.9
|
||||
six==1.16.0
|
||||
sniffio==1.2.0
|
||||
sqlalchemy-aio==0.17.0
|
||||
sqlalchemy==1.3.23
|
||||
sse-starlette==0.10.3
|
||||
starlette==0.19.1
|
||||
typing-extensions==4.3.0
|
||||
uvicorn==0.18.2
|
||||
uvloop==0.16.0
|
||||
watchfiles==0.16.0
|
||||
websockets==10.3
|
||||
websocket-client==1.3.3
|
||||
async-timeout==4.0.2
|
||||
setuptools==65.4.0
|
||||
aiofiles==0.8.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
anyio==3.6.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
asgiref==3.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
asn1crypto==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
async-timeout==4.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
attrs==21.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
certifi==2021.5.30 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cffi==1.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
charset-normalizer==2.0.6 ; python_version >= "3.7" and python_version < "4.0"
|
||||
click==8.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
coincurve==17.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
colorama==0.4.5 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||
cryptography==36.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
ecdsa==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
embit==0.4.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
enum34==1.1.10 ; python_version >= "3.7" and python_version < "4.0"
|
||||
environs==9.3.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
fastapi==0.78.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
grpcio==1.49.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
httpcore==0.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
httptools==0.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
httpx==0.23.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
idna==3.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
importlib-metadata==4.8.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
jinja2==3.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
lnurl==0.3.6 ; python_version >= "3.7" and python_version < "4.0"
|
||||
loguru==0.5.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
markupsafe==2.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
marshmallow==3.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
outcome==1.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
packaging==21.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pathlib2==2.3.7.post1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
protobuf==4.21.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||
psycopg2-binary==2.9.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pycryptodomex==3.14.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pydantic==1.8.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyln-bolt7==1.0.246 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyln-client==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyln-proto==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pypng==0.0.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyqrcode==1.2.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyscss==1.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
python-dotenv==0.19.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyyaml==5.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
represent==1.6.0.post0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
rfc3986==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
rfc3986[idna2008]==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
setuptools==65.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
shortuuid==1.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sniffio==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sqlalchemy==1.3.23 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sse-starlette==0.6.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
typing-extensions==3.10.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
uvicorn==0.18.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
uvloop==0.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
watchgod==0.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||
websocket-client==1.3.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
websockets==10.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||
zipp==3.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
|
|
@ -60,7 +60,7 @@ async def from_wallet(from_user):
|
|||
wallet = await create_wallet(user_id=user.id, wallet_name="test_wallet_from")
|
||||
await credit_wallet(
|
||||
wallet_id=wallet.id,
|
||||
amount=99999999,
|
||||
amount=999999999,
|
||||
)
|
||||
yield wallet
|
||||
|
||||
|
@ -77,7 +77,7 @@ async def to_wallet(to_user):
|
|||
wallet = await create_wallet(user_id=user.id, wallet_name="test_wallet_to")
|
||||
await credit_wallet(
|
||||
wallet_id=wallet.id,
|
||||
amount=99999999,
|
||||
amount=999999999,
|
||||
)
|
||||
yield wallet
|
||||
|
||||
|
|
Binary file not shown.
|
@ -1,6 +1,7 @@
|
|||
import argparse
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
import psycopg2
|
||||
|
|
Loading…
Add table
Reference in a new issue