diff --git a/docs/guide/wallets.md b/docs/guide/wallets.md
index 80fb54c0..10724f34 100644
--- a/docs/guide/wallets.md
+++ b/docs/guide/wallets.md
@@ -79,3 +79,8 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
- `OPENNODE_KEY`: opennodeAdminApiKey
+
+
+### Cliche Wallet
+
+- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
diff --git a/lnbits/app.py b/lnbits/app.py
index 8b9cf798..075828ef 100644
--- a/lnbits/app.py
+++ b/lnbits/app.py
@@ -91,7 +91,6 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
)
app.add_middleware(GZipMiddleware, minimum_size=1000)
- # app.add_middleware(ASGIProxyFix)
check_funding_source(app)
register_assets(app)
diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js
index 76d82ad4..66801313 100644
--- a/lnbits/core/static/js/wallet.js
+++ b/lnbits/core/static/js/wallet.js
@@ -361,6 +361,35 @@ new Vue({
this.receive.status = 'pending'
})
},
+ onInitQR: async function (promise) {
+ try {
+ await promise
+ } catch (error) {
+ let mapping = {
+ NotAllowedError: 'ERROR: you need to grant camera access permission',
+ NotFoundError: 'ERROR: no camera on this device',
+ NotSupportedError:
+ 'ERROR: secure context required (HTTPS, localhost)',
+ NotReadableError: 'ERROR: is the camera already in use?',
+ OverconstrainedError: 'ERROR: installed cameras are not suitable',
+ StreamApiNotSupportedError:
+ 'ERROR: Stream API is not supported in this browser',
+ InsecureContextError:
+ 'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
+ }
+ let valid_error = Object.keys(mapping).filter(key => {
+ return error.name === key
+ })
+ let camera_error = valid_error
+ ? mapping[valid_error]
+ : `ERROR: Camera error (${error.name})`
+ this.parse.camera.show = false
+ this.$q.notify({
+ message: camera_error,
+ type: 'negative'
+ })
+ }
+ },
decodeQR: function (res) {
this.parse.data.request = res
this.decodeRequest()
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html
index bccdc2b4..4bf6067c 100644
--- a/lnbits/core/templates/core/wallet.html
+++ b/lnbits/core/templates/core/wallet.html
@@ -653,6 +653,7 @@
@@ -671,6 +672,7 @@
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index c07df568..983d5a26 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -476,7 +476,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
except:
# parse internet identifier (user@domain.com)
name_domain = code.split("@")
- if len(name_domain) == 2 and len(name_domain[1].split(".")) == 2:
+ if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
name, domain = name_domain
url = (
("http://" if domain.endswith(".onion") else "https://")
diff --git a/lnbits/extensions/boltz/tasks.py b/lnbits/extensions/boltz/tasks.py
index ace94557..d1ace04b 100644
--- a/lnbits/extensions/boltz/tasks.py
+++ b/lnbits/extensions/boltz/tasks.py
@@ -57,7 +57,7 @@ async def check_for_pending_swaps():
swap_status = get_swap_status(swap)
# should only happen while development when regtest is reset
if swap_status.exists is False:
- logger.warning(f"Boltz - swap: {swap.boltz_id} does not exist.")
+ logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.")
await update_swap_status(swap.id, "failed")
continue
@@ -73,7 +73,7 @@ async def check_for_pending_swaps():
else:
if swap_status.hit_timeout:
if not swap_status.has_lockup:
- logger.warning(
+ logger.debug(
f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..."
)
await update_swap_status(swap.id, "timeout")
diff --git a/lnbits/extensions/copilot/crud.py b/lnbits/extensions/copilot/crud.py
index d0da044e..5ecb5cd4 100644
--- a/lnbits/extensions/copilot/crud.py
+++ b/lnbits/extensions/copilot/crud.py
@@ -10,7 +10,7 @@ from .models import Copilots, CreateCopilotData
async def create_copilot(
data: CreateCopilotData, inkey: Optional[str] = ""
-) -> Copilots:
+) -> Optional[Copilots]:
copilot_id = urlsafe_short_hash()
await db.execute(
"""
@@ -67,19 +67,19 @@ async def create_copilot(
async def update_copilot(
- data: CreateCopilotData, copilot_id: Optional[str] = ""
+ data: CreateCopilotData, copilot_id: str
) -> Optional[Copilots]:
q = ", ".join([f"{field[0]} = ?" for field in data])
items = [f"{field[1]}" for field in data]
items.append(copilot_id)
- await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items))
+ await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items,))
row = await db.fetchone(
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
)
return Copilots(**row) if row else None
-async def get_copilot(copilot_id: str) -> Copilots:
+async def get_copilot(copilot_id: str) -> Optional[Copilots]:
row = await db.fetchone(
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
)
diff --git a/lnbits/extensions/copilot/tasks.py b/lnbits/extensions/copilot/tasks.py
index c59ef4cc..48ad7813 100644
--- a/lnbits/extensions/copilot/tasks.py
+++ b/lnbits/extensions/copilot/tasks.py
@@ -26,7 +26,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
webhook = None
data = None
- if payment.extra.get("tag") != "copilot":
+ if not payment.extra or payment.extra.get("tag") != "copilot":
# not an copilot invoice
return
@@ -71,12 +71,12 @@ async def on_invoice_paid(payment: Payment) -> None:
async def mark_webhook_sent(payment: Payment, status: int) -> None:
- payment.extra["wh_status"] = status
-
- await core_db.execute(
- """
- UPDATE apipayments SET extra = ?
- WHERE hash = ?
- """,
- (json.dumps(payment.extra), payment.payment_hash),
- )
+ if payment.extra:
+ payment.extra["wh_status"] = status
+ await core_db.execute(
+ """
+ UPDATE apipayments SET extra = ?
+ WHERE hash = ?
+ """,
+ (json.dumps(payment.extra), payment.payment_hash),
+ )
diff --git a/lnbits/extensions/copilot/views.py b/lnbits/extensions/copilot/views.py
index 7ee7f590..b4a2354a 100644
--- a/lnbits/extensions/copilot/views.py
+++ b/lnbits/extensions/copilot/views.py
@@ -15,7 +15,9 @@ templates = Jinja2Templates(directory="templates")
@copilot_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 copilot_renderer().TemplateResponse(
"copilot/index.html", {"request": request, "user": user.dict()}
)
@@ -44,7 +46,7 @@ class ConnectionManager:
async def connect(self, websocket: WebSocket, copilot_id: str):
await websocket.accept()
- websocket.id = copilot_id
+ websocket.id = copilot_id # type: ignore
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
@@ -52,7 +54,7 @@ class ConnectionManager:
async def send_personal_message(self, message: str, copilot_id: str):
for connection in self.active_connections:
- if connection.id == copilot_id:
+ if connection.id == copilot_id: # type: ignore
await connection.send_text(message)
async def broadcast(self, message: str):
diff --git a/lnbits/extensions/copilot/views_api.py b/lnbits/extensions/copilot/views_api.py
index 91b0572a..46611a2e 100644
--- a/lnbits/extensions/copilot/views_api.py
+++ b/lnbits/extensions/copilot/views_api.py
@@ -23,7 +23,7 @@ from .views import updater
@copilot_ext.get("/api/v1/copilot")
async def api_copilots_retrieve(
- req: Request, wallet: WalletTypeInfo = Depends(get_key_type)
+ req: Request, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
):
wallet_user = wallet.wallet.user
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
@@ -37,7 +37,7 @@ async def api_copilots_retrieve(
async def api_copilot_retrieve(
req: Request,
copilot_id: str = Query(None),
- wallet: WalletTypeInfo = Depends(get_key_type),
+ wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
):
copilot = await get_copilot(copilot_id)
if not copilot:
@@ -54,7 +54,7 @@ async def api_copilot_retrieve(
async def api_copilot_create_or_update(
data: CreateCopilotData,
copilot_id: str = Query(None),
- wallet: WalletTypeInfo = Depends(require_admin_key),
+ wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
):
data.user = wallet.wallet.user
data.wallet = wallet.wallet.id
@@ -67,7 +67,8 @@ async def api_copilot_create_or_update(
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
async def api_copilot_delete(
- copilot_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
+ copilot_id: str = Query(None),
+ wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
):
copilot = await get_copilot(copilot_id)
diff --git a/lnbits/extensions/discordbot/crud.py b/lnbits/extensions/discordbot/crud.py
index 5661fcb4..629a5c00 100644
--- a/lnbits/extensions/discordbot/crud.py
+++ b/lnbits/extensions/discordbot/crud.py
@@ -98,21 +98,21 @@ async def get_discordbot_wallet(wallet_id: str) -> Optional[Wallets]:
return Wallets(**row) if row else None
-async def get_discordbot_wallets(admin_id: str) -> Optional[Wallets]:
+async def get_discordbot_wallets(admin_id: str) -> List[Wallets]:
rows = await db.fetchall(
"SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,)
)
return [Wallets(**row) for row in rows]
-async def get_discordbot_users_wallets(user_id: str) -> Optional[Wallets]:
+async def get_discordbot_users_wallets(user_id: str) -> List[Wallets]:
rows = await db.fetchall(
"""SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,)
)
return [Wallets(**row) for row in rows]
-async def get_discordbot_wallet_transactions(wallet_id: str) -> Optional[Payment]:
+async def get_discordbot_wallet_transactions(wallet_id: str) -> List[Payment]:
return await get_payments(
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
)
diff --git a/lnbits/extensions/discordbot/views.py b/lnbits/extensions/discordbot/views.py
index a5395e21..ec7d18cc 100644
--- a/lnbits/extensions/discordbot/views.py
+++ b/lnbits/extensions/discordbot/views.py
@@ -9,7 +9,9 @@ from . import discordbot_ext, discordbot_renderer
@discordbot_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 discordbot_renderer().TemplateResponse(
"discordbot/index.html", {"request": request, "user": user.dict()}
)
diff --git a/lnbits/extensions/discordbot/views_api.py b/lnbits/extensions/discordbot/views_api.py
index 6f213a89..e6d004db 100644
--- a/lnbits/extensions/discordbot/views_api.py
+++ b/lnbits/extensions/discordbot/views_api.py
@@ -27,32 +27,37 @@ from .models import CreateUserData, CreateUserWallet
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
-async def api_discordbot_users(wallet: WalletTypeInfo = Depends(get_key_type)):
+async def api_discordbot_users(
+ wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
+):
user_id = wallet.wallet.user
return [user.dict() for user in await get_discordbot_users(user_id)]
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
-async def api_discordbot_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
+async def api_discordbot_user(
+ user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
+):
user = await get_discordbot_user(user_id)
- return user.dict()
+ if user:
+ return user.dict()
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
async def api_discordbot_users_create(
- data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)
+ data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
):
user = await create_discordbot_user(data)
full = user.dict()
- full["wallets"] = [
- wallet.dict() for wallet in await get_discordbot_users_wallets(user.id)
- ]
+ wallets = await get_discordbot_users_wallets(user.id)
+ if wallets:
+ full["wallets"] = [wallet for wallet in wallets]
return full
@discordbot_ext.delete("/api/v1/users/{user_id}")
async def api_discordbot_users_delete(
- user_id, wallet: WalletTypeInfo = Depends(get_key_type)
+ user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
):
user = await get_discordbot_user(user_id)
if not user:
@@ -75,7 +80,7 @@ async def api_discordbot_activate_extension(
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
)
- update_user_extension(user_id=userid, extension=extension, active=active)
+ await update_user_extension(user_id=userid, extension=extension, active=active)
return {"extension": "updated"}
@@ -84,7 +89,7 @@ async def api_discordbot_activate_extension(
@discordbot_ext.post("/api/v1/wallets")
async def api_discordbot_wallets_create(
- data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type)
+ data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
):
user = await create_discordbot_wallet(
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
@@ -93,28 +98,30 @@ async def api_discordbot_wallets_create(
@discordbot_ext.get("/api/v1/wallets")
-async def api_discordbot_wallets(wallet: WalletTypeInfo = Depends(get_key_type)):
+async def api_discordbot_wallets(
+ wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
+):
admin_id = wallet.wallet.user
- return [wallet.dict() for wallet in await get_discordbot_wallets(admin_id)]
+ return await get_discordbot_wallets(admin_id)
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
async def api_discordbot_wallet_transactions(
- wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
+ wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
):
return await get_discordbot_wallet_transactions(wallet_id)
@discordbot_ext.get("/api/v1/wallets/{user_id}")
async def api_discordbot_users_wallets(
- user_id, wallet: WalletTypeInfo = Depends(get_key_type)
+ user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
):
- return [s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)]
+ return await get_discordbot_users_wallets(user_id)
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
async def api_discordbot_wallets_delete(
- wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
+ wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
):
get_wallet = await get_discordbot_wallet(wallet_id)
if not get_wallet:
diff --git a/lnbits/extensions/example/views.py b/lnbits/extensions/example/views.py
index 252b4726..29b257f4 100644
--- a/lnbits/extensions/example/views.py
+++ b/lnbits/extensions/example/views.py
@@ -12,7 +12,10 @@ templates = Jinja2Templates(directory="templates")
@example_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 example_renderer().TemplateResponse(
"example/index.html", {"request": request, "user": user.dict()}
)
diff --git a/lnbits/extensions/livestream/tasks.py b/lnbits/extensions/livestream/tasks.py
index 626c698c..d081332f 100644
--- a/lnbits/extensions/livestream/tasks.py
+++ b/lnbits/extensions/livestream/tasks.py
@@ -4,10 +4,10 @@ import json
from loguru import logger
from lnbits.core import db as core_db
-from lnbits.core.crud import create_payment
from lnbits.core.models import Payment
-from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
-from lnbits.tasks import internal_invoice_listener, register_invoice_listener
+from lnbits.core.services import create_invoice, pay_invoice
+from lnbits.helpers import get_current_extension_name
+from lnbits.tasks import register_invoice_listener
from .crud import get_livestream_by_track, get_producer, get_track
@@ -44,44 +44,20 @@ async def on_invoice_paid(payment: Payment) -> None:
# now we make a special kind of internal transfer
amount = int(payment.amount * (100 - ls.fee_pct) / 100)
- # mark the original payment with two extra keys, "shared_with" and "received"
- # (this prevents us from doing this process again and it's informative)
- # and reduce it by the amount we're going to send to the producer
- await core_db.execute(
- """
- UPDATE apipayments
- SET extra = ?, amount = ?
- WHERE hash = ?
- AND checking_id NOT LIKE 'internal_%'
- """,
- (
- json.dumps(
- dict(
- **payment.extra,
- shared_with=[producer.name, producer.id],
- received=payment.amount,
- )
- ),
- payment.amount - amount,
- payment.payment_hash,
- ),
- )
-
- # perform an internal transfer using the same payment_hash to the producer wallet
- internal_checking_id = f"internal_{urlsafe_short_hash()}"
- await create_payment(
- wallet_id=producer.wallet,
- checking_id=internal_checking_id,
- payment_request="",
- payment_hash=payment.payment_hash,
- amount=amount,
+ payment_hash, payment_request = await create_invoice(
+ wallet_id=tpos.tip_wallet,
+ amount=amount, # sats
+ internal=True,
memo=f"Revenue from '{track.name}'.",
- pending=False,
)
+ logger.debug(f"livestream: producer invoice created: {payment_hash}")
- # manually send this for now
- # await internal_invoice_paid.send(internal_checking_id)
- await internal_invoice_listener.put(internal_checking_id)
+ checking_id = await pay_invoice(
+ payment_request=payment_request,
+ wallet_id=payment.wallet_id,
+ extra={"tag": "livestream"},
+ )
+ logger.debug(f"livestream: producer invoice paid: {checking_id}")
# so the flow is the following:
# - we receive, say, 1000 satoshis
diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py
index 47d7a4a8..23d391b7 100644
--- a/lnbits/extensions/satspay/crud.py
+++ b/lnbits/extensions/satspay/crud.py
@@ -102,7 +102,7 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
charge = await get_charge(charge_id)
if not charge.paid:
if charge.onchainaddress:
- config = await get_config(charge.user)
+ config = await get_charge_config(charge_id)
try:
async with httpx.AsyncClient() as client:
r = await client.get(
@@ -122,3 +122,10 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
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
+
+
+async def get_charge_config(charge_id: str):
+ row = await db.fetchone(
+ """SELECT "user" FROM satspay.charges WHERE id = ?""", (charge_id,)
+ )
+ return await get_config(row.user)
diff --git a/lnbits/extensions/satspay/helpers.py b/lnbits/extensions/satspay/helpers.py
new file mode 100644
index 00000000..2d15b557
--- /dev/null
+++ b/lnbits/extensions/satspay/helpers.py
@@ -0,0 +1,17 @@
+from .models import Charges
+
+
+def compact_charge(charge: Charges):
+ return {
+ "id": charge.id,
+ "description": charge.description,
+ "onchainaddress": charge.onchainaddress,
+ "payment_request": charge.payment_request,
+ "payment_hash": charge.payment_hash,
+ "time": charge.time,
+ "amount": charge.amount,
+ "balance": charge.balance,
+ "paid": charge.paid,
+ "timestamp": charge.timestamp,
+ "completelink": charge.completelink, # should be secret?
+ }
diff --git a/lnbits/extensions/satspay/models.py b/lnbits/extensions/satspay/models.py
index e8638d5e..daf63f42 100644
--- a/lnbits/extensions/satspay/models.py
+++ b/lnbits/extensions/satspay/models.py
@@ -19,7 +19,6 @@ class CreateCharge(BaseModel):
class Charges(BaseModel):
id: str
- user: str
description: Optional[str]
onchainwallet: Optional[str]
onchainaddress: Optional[str]
diff --git a/lnbits/extensions/satspay/templates/satspay/display.html b/lnbits/extensions/satspay/templates/satspay/display.html
index f34ac509..12288c80 100644
--- a/lnbits/extensions/satspay/templates/satspay/display.html
+++ b/lnbits/extensions/satspay/templates/satspay/display.html
@@ -328,7 +328,7 @@
)
},
checkBalances: async function () {
- if (!this.charge.hasStaleBalance) await this.refreshCharge()
+ if (this.charge.hasStaleBalance) return
try {
const {data} = await LNbits.api.request(
'GET',
@@ -339,18 +339,9 @@
LNbits.utils.notifyApiError(error)
}
},
- refreshCharge: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- `/satspay/api/v1/charge/${this.charge.id}`
- )
- this.charge = mapCharge(data, this.charge)
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
checkPendingOnchain: async function () {
+ if (!this.charge.onchainaddress) return
+
const {
bitcoin: {addresses: addressesAPI}
} = mempoolJS({
diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py
index 69d81dad..b789bf8f 100644
--- a/lnbits/extensions/satspay/views.py
+++ b/lnbits/extensions/satspay/views.py
@@ -9,10 +9,9 @@ from starlette.responses import HTMLResponse
from lnbits.core.crud import get_wallet
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
-from lnbits.extensions.watchonly.crud import get_config
from . import satspay_ext, satspay_renderer
-from .crud import get_charge
+from .crud import get_charge, get_charge_config
templates = Jinja2Templates(directory="templates")
@@ -32,7 +31,7 @@ async def display(request: Request, charge_id: str):
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
)
wallet = await get_wallet(charge.lnbitswallet)
- onchainwallet_config = await get_config(charge.user)
+ 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
diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py
index f94b970a..73c87e7c 100644
--- a/lnbits/extensions/satspay/views_api.py
+++ b/lnbits/extensions/satspay/views_api.py
@@ -20,6 +20,7 @@ from .crud import (
get_charges,
update_charge,
)
+from .helpers import compact_charge
from .models import CreateCharge
#############################CHARGES##########################
@@ -123,25 +124,13 @@ async def api_charge_balance(charge_id):
try:
r = await client.post(
charge.webhook,
- json={
- "id": charge.id,
- "description": charge.description,
- "onchainaddress": charge.onchainaddress,
- "payment_request": charge.payment_request,
- "payment_hash": charge.payment_hash,
- "time": charge.time,
- "amount": charge.amount,
- "balance": charge.balance,
- "paid": charge.paid,
- "timestamp": charge.timestamp,
- "completelink": charge.completelink,
- },
+ json=compact_charge(charge),
timeout=40,
)
except AssertionError:
charge.webhook = None
return {
- **charge.dict(),
+ **compact_charge(charge),
**{"time_elapsed": charge.time_elapsed},
**{"time_left": charge.time_left},
**{"paid": charge.paid},
diff --git a/lnbits/extensions/splitpayments/models.py b/lnbits/extensions/splitpayments/models.py
index 4b95ed18..6338d97f 100644
--- a/lnbits/extensions/splitpayments/models.py
+++ b/lnbits/extensions/splitpayments/models.py
@@ -14,7 +14,7 @@ class Target(BaseModel):
class TargetPutList(BaseModel):
wallet: str = Query(...)
alias: str = Query("")
- percent: float = Query(..., ge=0.01)
+ percent: float = Query(..., ge=0.01, lt=100)
class TargetPut(BaseModel):
diff --git a/lnbits/extensions/splitpayments/tasks.py b/lnbits/extensions/splitpayments/tasks.py
index cfc6c226..53378b20 100644
--- a/lnbits/extensions/splitpayments/tasks.py
+++ b/lnbits/extensions/splitpayments/tasks.py
@@ -1,13 +1,11 @@
import asyncio
-import json
from loguru import logger
-from lnbits.core import db as core_db
-from lnbits.core.crud import create_payment
from lnbits.core.models import Payment
-from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
-from lnbits.tasks import internal_invoice_queue, register_invoice_listener
+from lnbits.core.services import create_invoice, pay_invoice
+from lnbits.helpers import get_current_extension_name
+from lnbits.tasks import register_invoice_listener
from .crud import get_targets
@@ -22,60 +20,36 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if payment.extra.get("tag") == "splitpayments" or payment.extra.get("splitted"):
- # already splitted, ignore
+ if payment.extra.get("tag") == "splitpayments":
+ # already a splitted payment, ignore
return
- # now we make some special internal transfers (from no one to the receiver)
targets = await get_targets(payment.wallet_id)
if not targets:
return
- transfers = [
- (target.wallet, int(target.percent * payment.amount / 100))
- for target in targets
- ]
- transfers = [(wallet, amount) for wallet, amount in transfers if amount > 0]
- amount_left = payment.amount - sum([amount for _, amount in transfers])
+ total_percent = sum([target.percent for target in targets])
- if amount_left < 0:
- logger.error(
- "splitpayments failure: amount_left is negative.", payment.payment_hash
- )
+ if total_percent > 100:
+ logger.error("splitpayment failure: total percent adds up to more than 100%")
return
- # mark the original payment with one extra key, "splitted"
- # (this prevents us from doing this process again and it's informative)
- # and reduce it by the amount we're going to send to the producer
- await core_db.execute(
- """
- UPDATE apipayments
- SET extra = ?, amount = ?
- WHERE hash = ?
- AND checking_id NOT LIKE 'internal_%'
- """,
- (
- json.dumps(dict(**payment.extra, splitted=True)),
- amount_left,
- payment.payment_hash,
- ),
- )
-
- # perform the internal transfer using the same payment_hash
- for wallet, amount in transfers:
- internal_checking_id = f"internal_{urlsafe_short_hash()}"
- await create_payment(
- wallet_id=wallet,
- checking_id=internal_checking_id,
- payment_request="",
- payment_hash=payment.payment_hash,
- amount=amount,
- memo=payment.memo,
- pending=False,
+ logger.debug(f"performing split payments to {len(targets)} targets")
+ for target in targets:
+ amount = int(payment.amount * target.percent / 100) # msats
+ payment_hash, payment_request = await create_invoice(
+ wallet_id=target.wallet,
+ amount=int(amount / 1000), # sats
+ internal=True,
+ memo=f"split payment: {target.percent}% for {target.alias or target.wallet}",
extra={"tag": "splitpayments"},
)
+ logger.debug(f"created split invoice: {payment_hash}")
- # manually send this for now
- await internal_invoice_queue.put(internal_checking_id)
- return
+ checking_id = await pay_invoice(
+ payment_request=payment_request,
+ wallet_id=payment.wallet_id,
+ extra={"tag": "splitpayments"},
+ )
+ logger.debug(f"paid split invoice: {checking_id}")
diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py
index 207e2d1d..aa358d11 100644
--- a/lnbits/extensions/subdomains/crud.py
+++ b/lnbits/extensions/subdomains/crud.py
@@ -3,10 +3,10 @@ from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
-from .models import CreateDomain, Domains, Subdomains
+from .models import CreateDomain, CreateSubdomain, Domains, Subdomains
-async def create_subdomain(payment_hash, wallet, data: CreateDomain) -> Subdomains:
+async def create_subdomain(payment_hash, wallet, data: CreateSubdomain) -> Subdomains:
await db.execute(
"""
INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py
index 17004504..39e17615 100644
--- a/lnbits/extensions/subdomains/models.py
+++ b/lnbits/extensions/subdomains/models.py
@@ -3,24 +3,24 @@ from pydantic.main import BaseModel
class CreateDomain(BaseModel):
- wallet: str = Query(...)
- domain: str = Query(...)
- cf_token: str = Query(...)
- cf_zone_id: str = Query(...)
- webhook: str = Query("")
- description: str = Query(..., min_length=0)
- cost: int = Query(..., ge=0)
- allowed_record_types: str = Query(...)
+ wallet: str = Query(...) # type: ignore
+ domain: str = Query(...) # type: ignore
+ cf_token: str = Query(...) # type: ignore
+ cf_zone_id: str = Query(...) # type: ignore
+ webhook: str = Query("") # type: ignore
+ description: str = Query(..., min_length=0) # type: ignore
+ cost: int = Query(..., ge=0) # type: ignore
+ allowed_record_types: str = Query(...) # type: ignore
class CreateSubdomain(BaseModel):
- domain: str = Query(...)
- subdomain: str = Query(...)
- email: str = Query(...)
- ip: str = Query(...)
- sats: int = Query(..., ge=0)
- duration: int = Query(...)
- record_type: str = Query(...)
+ domain: str = Query(...) # type: ignore
+ subdomain: str = Query(...) # type: ignore
+ email: str = Query(...) # type: ignore
+ ip: str = Query(...) # type: ignore
+ sats: int = Query(..., ge=0) # type: ignore
+ duration: int = Query(...) # type: ignore
+ record_type: str = Query(...) # type: ignore
class Domains(BaseModel):
diff --git a/lnbits/extensions/subdomains/tasks.py b/lnbits/extensions/subdomains/tasks.py
index 04ee2dd4..c5a7f47b 100644
--- a/lnbits/extensions/subdomains/tasks.py
+++ b/lnbits/extensions/subdomains/tasks.py
@@ -20,7 +20,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if payment.extra.get("tag") != "lnsubdomain":
+ if not payment.extra or payment.extra.get("tag") != "lnsubdomain":
# not an lnurlp invoice
return
@@ -37,7 +37,7 @@ async def on_invoice_paid(payment: Payment) -> None:
)
### Use webhook to notify about cloudflare registration
- if domain.webhook:
+ if domain and domain.webhook:
async with httpx.AsyncClient() as client:
try:
r = await client.post(
diff --git a/lnbits/extensions/subdomains/views.py b/lnbits/extensions/subdomains/views.py
index df387ba8..962f850d 100644
--- a/lnbits/extensions/subdomains/views.py
+++ b/lnbits/extensions/subdomains/views.py
@@ -16,7 +16,9 @@ templates = Jinja2Templates(directory="templates")
@subdomains_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 subdomains_renderer().TemplateResponse(
"subdomains/index.html", {"request": request, "user": user.dict()}
)
diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py
index b01e6ffb..34d8e75b 100644
--- a/lnbits/extensions/subdomains/views_api.py
+++ b/lnbits/extensions/subdomains/views_api.py
@@ -29,12 +29,15 @@ from .crud import (
@subdomains_ext.get("/api/v1/domains")
async def api_domains(
- g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
+ g: WalletTypeInfo = Depends(get_key_type), # type: ignore
+ all_wallets: bool = Query(False),
):
wallet_ids = [g.wallet.id]
if all_wallets:
- wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+ user = await get_user(g.wallet.user)
+ if user is not None:
+ wallet_ids = user.wallet_ids
return [domain.dict() for domain in await get_domains(wallet_ids)]
@@ -42,7 +45,9 @@ async def api_domains(
@subdomains_ext.post("/api/v1/domains")
@subdomains_ext.put("/api/v1/domains/{domain_id}")
async def api_domain_create(
- data: CreateDomain, domain_id=None, g: WalletTypeInfo = Depends(get_key_type)
+ data: CreateDomain,
+ domain_id=None,
+ g: WalletTypeInfo = Depends(get_key_type), # type: ignore
):
if domain_id:
domain = await get_domain(domain_id)
@@ -63,7 +68,9 @@ async def api_domain_create(
@subdomains_ext.delete("/api/v1/domains/{domain_id}")
-async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)):
+async def api_domain_delete(
+ domain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
+):
domain = await get_domain(domain_id)
if not domain:
@@ -82,12 +89,14 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)
@subdomains_ext.get("/api/v1/subdomains")
async def api_subdomains(
- all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type)
+ all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type) # type: ignore
):
wallet_ids = [g.wallet.id]
if all_wallets:
- wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+ user = await get_user(g.wallet.user)
+ if user is not None:
+ wallet_ids = user.wallet_ids
return [domain.dict() for domain in await get_subdomains(wallet_ids)]
@@ -173,7 +182,9 @@ async def api_subdomain_send_subdomain(payment_hash):
@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}")
-async def api_subdomain_delete(subdomain_id, g: WalletTypeInfo = Depends(get_key_type)):
+async def api_subdomain_delete(
+ subdomain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
+):
subdomain = await get_subdomain(subdomain_id)
if not subdomain:
diff --git a/lnbits/extensions/tpos/tasks.py b/lnbits/extensions/tpos/tasks.py
index f18d1689..6369bbc7 100644
--- a/lnbits/extensions/tpos/tasks.py
+++ b/lnbits/extensions/tpos/tasks.py
@@ -1,11 +1,11 @@
import asyncio
-import json
-from lnbits.core import db as core_db
-from lnbits.core.crud import create_payment
+from loguru import logger
+
from lnbits.core.models import Payment
-from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
-from lnbits.tasks import internal_invoice_queue, register_invoice_listener
+from lnbits.core.services import create_invoice, pay_invoice
+from lnbits.helpers import get_current_extension_name
+from lnbits.tasks import register_invoice_listener
from .crud import get_tpos
@@ -20,11 +20,9 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if payment.extra.get("tag") == "tpos" and payment.extra.get("tipSplitted"):
- # already splitted, ignore
+ if payment.extra.get("tag") != "tpos":
return
- # now we make some special internal transfers (from no one to the receiver)
tpos = await get_tpos(payment.extra.get("tposId"))
tipAmount = payment.extra.get("tipAmount")
@@ -32,39 +30,17 @@ async def on_invoice_paid(payment: Payment) -> None:
# no tip amount
return
- tipAmount = tipAmount * 1000
- amount = payment.amount - tipAmount
-
- # mark the original payment with one extra key, "splitted"
- # (this prevents us from doing this process again and it's informative)
- # and reduce it by the amount we're going to send to the producer
- await core_db.execute(
- """
- UPDATE apipayments
- SET extra = ?, amount = ?
- WHERE hash = ?
- AND checking_id NOT LIKE 'internal_%'
- """,
- (
- json.dumps(dict(**payment.extra, tipSplitted=True)),
- amount,
- payment.payment_hash,
- ),
- )
-
- # perform the internal transfer using the same payment_hash
- internal_checking_id = f"internal_{urlsafe_short_hash()}"
- await create_payment(
+ payment_hash, payment_request = await create_invoice(
wallet_id=tpos.tip_wallet,
- checking_id=internal_checking_id,
- payment_request="",
- payment_hash=payment.payment_hash,
- amount=tipAmount,
- memo=f"Tip for {payment.memo}",
- pending=False,
- extra={"tipSplitted": True},
+ amount=int(tipAmount), # sats
+ internal=True,
+ memo=f"tpos tip",
)
+ logger.debug(f"tpos: tip invoice created: {payment_hash}")
- # manually send this for now
- await internal_invoice_queue.put(internal_checking_id)
- return
+ checking_id = await pay_invoice(
+ payment_request=payment_request,
+ wallet_id=payment.wallet_id,
+ extra={"tag": "tpos"},
+ )
+ logger.debug(f"tpos: tip invoice paid: {checking_id}")
diff --git a/lnbits/extensions/watchonly/crud.py b/lnbits/extensions/watchonly/crud.py
index de338b91..c4a1df72 100644
--- a/lnbits/extensions/watchonly/crud.py
+++ b/lnbits/extensions/watchonly/crud.py
@@ -10,7 +10,7 @@ from .models import Address, Config, WalletAccount
##########################WALLETS####################
-async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
+async def create_watch_wallet(user: str, w: WalletAccount) -> WalletAccount:
wallet_id = urlsafe_short_hash()
await db.execute(
"""
@@ -30,7 +30,7 @@ async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
""",
(
wallet_id,
- w.user,
+ user,
w.masterpub,
w.fingerprint,
w.title,
diff --git a/lnbits/extensions/watchonly/models.py b/lnbits/extensions/watchonly/models.py
index 622f5ec8..d8c278ff 100644
--- a/lnbits/extensions/watchonly/models.py
+++ b/lnbits/extensions/watchonly/models.py
@@ -14,7 +14,6 @@ class CreateWallet(BaseModel):
class WalletAccount(BaseModel):
id: str
- user: str
masterpub: str
fingerprint: str
title: str
diff --git a/lnbits/extensions/watchonly/views_api.py b/lnbits/extensions/watchonly/views_api.py
index 750d46c9..9030b9c3 100644
--- a/lnbits/extensions/watchonly/views_api.py
+++ b/lnbits/extensions/watchonly/views_api.py
@@ -86,7 +86,6 @@ async def api_wallet_create_or_update(
new_wallet = WalletAccount(
id="none",
- user=w.wallet.user,
masterpub=data.masterpub,
fingerprint=descriptor.keys[0].fingerprint.hex(),
type=descriptor.scriptpubkey_type(),
@@ -115,7 +114,7 @@ async def api_wallet_create_or_update(
)
)
- wallet = await create_watch_wallet(new_wallet)
+ wallet = await create_watch_wallet(w.wallet.user, new_wallet)
await api_get_addresses(wallet.id, w)
except Exception as e:
diff --git a/lnbits/proxy_fix.py b/lnbits/proxy_fix.py
deleted file mode 100644
index 897835e0..00000000
--- a/lnbits/proxy_fix.py
+++ /dev/null
@@ -1,95 +0,0 @@
-from functools import partial
-from typing import Callable, List, Optional
-from urllib.parse import urlparse
-from urllib.request import parse_http_list as _parse_list_header
-
-from quart import Request
-from quart_trio.asgi import TrioASGIHTTPConnection
-from werkzeug.datastructures import Headers
-
-
-class ASGIProxyFix(TrioASGIHTTPConnection):
- def _create_request_from_scope(self, send: Callable) -> Request:
- headers = Headers()
- headers["Remote-Addr"] = (self.scope.get("client") or [""])[0]
- for name, value in self.scope["headers"]:
- headers.add(name.decode("latin1").title(), value.decode("latin1"))
- if self.scope["http_version"] < "1.1":
- headers.setdefault("Host", self.app.config["SERVER_NAME"] or "")
-
- path = self.scope["path"]
- path = path if path[0] == "/" else urlparse(path).path
-
- x_proto = self._get_real_value(1, headers.get("X-Forwarded-Proto"))
- if x_proto:
- self.scope["scheme"] = x_proto
-
- x_host = self._get_real_value(1, headers.get("X-Forwarded-Host"))
- if x_host:
- headers["host"] = x_host.lower()
-
- return self.app.request_class(
- self.scope["method"],
- self.scope["scheme"],
- path,
- self.scope["query_string"],
- headers,
- self.scope.get("root_path", ""),
- self.scope["http_version"],
- max_content_length=self.app.config["MAX_CONTENT_LENGTH"],
- body_timeout=self.app.config["BODY_TIMEOUT"],
- send_push_promise=partial(self._send_push_promise, send),
- scope=self.scope,
- )
-
- def _get_real_value(self, trusted: int, value: Optional[str]) -> Optional[str]:
- """Get the real value from a list header based on the configured
- number of trusted proxies.
- :param trusted: Number of values to trust in the header.
- :param value: Comma separated list header value to parse.
- :return: The real value, or ``None`` if there are fewer values
- than the number of trusted proxies.
- .. versionchanged:: 1.0
- Renamed from ``_get_trusted_comma``.
- .. versionadded:: 0.15
- """
- if not (trusted and value):
- return None
-
- values = self.parse_list_header(value)
- if len(values) >= trusted:
- return values[-trusted]
-
- return None
-
- def parse_list_header(self, value: str) -> List[str]:
- result = []
- for item in _parse_list_header(value):
- if item[:1] == item[-1:] == '"':
- item = self.unquote_header_value(item[1:-1])
- result.append(item)
- return result
-
- def unquote_header_value(self, value: str, is_filename: bool = False) -> str:
- r"""Unquotes a header value. (Reversal of :func:`quote_header_value`).
- This does not use the real unquoting but what browsers are actually
- using for quoting.
- .. versionadded:: 0.5
- :param value: the header value to unquote.
- :param is_filename: The value represents a filename or path.
- """
- if value and value[0] == value[-1] == '"':
- # this is not the real unquoting, but fixing this so that the
- # RFC is met will result in bugs with internet explorer and
- # probably some other browsers as well. IE for example is
- # uploading files with "C:\foo\bar.txt" as filename
- value = value[1:-1]
-
- # if this is a filename and the starting characters look like
- # a UNC path, then just return the value without quotes. Using the
- # replace sequence below on a UNC path has the effect of turning
- # the leading double slash into a single slash and then
- # _fix_ie_filename() doesn't work correctly. See #458.
- if not is_filename or value[:2] != "\\\\":
- return value.replace("\\\\", "\\").replace('\\"', '"')
- return value
diff --git a/pyproject.toml b/pyproject.toml
index 19dac860..7418de27 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -89,8 +89,34 @@ profile = "black"
ignore_missing_imports = "True"
files = "lnbits"
exclude = """(?x)(
- ^lnbits/extensions.
- | ^lnbits/wallets/lnd_grpc_files.
+ ^lnbits/extensions/bleskomat.
+ | ^lnbits/extensions/boltz.
+ | ^lnbits/extensions/boltcards.
+ | ^lnbits/extensions/events.
+ | ^lnbits/extensions/hivemind.
+ | ^lnbits/extensions/invoices.
+ | ^lnbits/extensions/jukebox.
+ | ^lnbits/extensions/livestream.
+ | ^lnbits/extensions/lnaddress.
+ | ^lnbits/extensions/lndhub.
+ | ^lnbits/extensions/lnticket.
+ | ^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.
+ | ^lnbits/extensions/streamalerts.
+ | ^lnbits/extensions/tipjar.
+ | ^lnbits/extensions/tpos.
+ | ^lnbits/extensions/usermanager.
+ | ^lnbits/extensions/watchonly.
+ | ^lnbits/extensions/withdraw.
+ | ^lnbits/wallets/lnd_grpc_files.
)"""
[tool.pytest.ini_options]