Merge remote-tracking branch 'origin/main' into qrcodemaker

This commit is contained in:
ben
2022-07-28 10:48:12 +01:00
231 changed files with 7526 additions and 1859 deletions
+22 -10
View File
@@ -1,15 +1,15 @@
import json
import datetime
from uuid import uuid4
from typing import List, Optional, Dict, Any
import json
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse
from uuid import uuid4
from lnbits import bolt11
from lnbits.db import Connection, POSTGRES, COCKROACH
from lnbits.db import COCKROACH, POSTGRES, Connection
from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
from . import db
from .models import User, Wallet, Payment, BalanceCheck
from .models import BalanceCheck, Payment, User, Wallet
# accounts
# --------
@@ -113,7 +113,7 @@ async def create_wallet(
async def update_wallet(
wallet_id: str, new_name: str, conn: Optional[Connection] = None
) -> Optional[Wallet]:
await (conn or db).execute(
return await (conn or db).execute(
"""
UPDATE wallets SET
name = ?
@@ -180,16 +180,28 @@ async def get_wallet_for_key(
async def get_standalone_payment(
checking_id_or_hash: str, conn: Optional[Connection] = None
checking_id_or_hash: str,
conn: Optional[Connection] = None,
incoming: Optional[bool] = False,
wallet_id: Optional[str] = None,
) -> Optional[Payment]:
clause: str = "checking_id = ? OR hash = ?"
values = [checking_id_or_hash, checking_id_or_hash]
if incoming:
clause = f"({clause}) AND amount > 0"
if wallet_id:
clause = f"({clause}) AND wallet = ?"
values.append(wallet_id)
row = await (conn or db).fetchone(
"""
f"""
SELECT *
FROM apipayments
WHERE checking_id = ? OR hash = ?
WHERE {clause}
LIMIT 1
""",
(checking_id_or_hash, checking_id_or_hash),
tuple(values),
)
return Payment.from_row(row) if row else None
+14 -7
View File
@@ -1,12 +1,15 @@
import json
import hmac
import hashlib
from lnbits.helpers import url_for
import hmac
import json
from sqlite3 import Row
from typing import Dict, List, NamedTuple, Optional
from ecdsa import SECP256k1, SigningKey # type: ignore
from lnurl import encode as lnurl_encode # type: ignore
from typing import List, NamedTuple, Optional, Dict
from sqlite3 import Row
from loguru import logger
from pydantic import BaseModel
from lnbits.helpers import url_for
from lnbits.settings import WALLET
@@ -103,6 +106,8 @@ class Payment(BaseModel):
@property
def tag(self) -> Optional[str]:
if self.extra is None:
return ""
return self.extra.get("tag")
@property
@@ -142,10 +147,12 @@ class Payment(BaseModel):
status = await WALLET.get_invoice_status(self.checking_id)
if self.is_out and status.failed:
print(f" - deleting outgoing failed payment {self.checking_id}: {status}")
logger.info(
f" - deleting outgoing failed payment {self.checking_id}: {status}"
)
await self.delete()
elif not status.pending:
print(
logger.info(
f" - marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
)
await self.set_pending(status.pending)
+44 -22
View File
@@ -6,14 +6,22 @@ from typing import Dict, Optional, Tuple
from urllib.parse import parse_qs, urlparse
import httpx
from fastapi import Depends
from lnurl import LnurlErrorResponse
from lnurl import decode as decode_lnurl # type: ignore
from loguru import logger
from lnbits import bolt11
from lnbits.db import Connection
from lnbits.decorators import (
WalletTypeInfo,
get_key_type,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.requestvars import g
from lnbits.settings import WALLET
from lnbits.settings import FAKE_WALLET, WALLET
from lnbits.wallets.base import PaymentResponse, PaymentStatus
from . import db
@@ -48,15 +56,19 @@ async def create_invoice(
description_hash: Optional[bytes] = None,
extra: Optional[Dict] = None,
webhook: Optional[str] = None,
internal: Optional[bool] = False,
conn: Optional[Connection] = None,
) -> Tuple[str, str]:
invoice_memo = None if description_hash else memo
ok, checking_id, payment_request, error_message = await WALLET.create_invoice(
# use the fake wallet if the invoice is for internal use only
wallet = FAKE_WALLET if internal else WALLET
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
amount=amount, memo=invoice_memo, description_hash=description_hash
)
if not ok:
raise InvoiceFailure(error_message or "Unexpected backend error.")
raise InvoiceFailure(error_message or "unexpected backend error.")
invoice = bolt11.decode(payment_request)
@@ -97,18 +109,15 @@ async def pay_invoice(
raise ValueError("Amount in invoice is too high.")
# put all parameters that don't change here
PaymentKwargs = TypedDict(
"PaymentKwargs",
{
"wallet_id": str,
"payment_request": str,
"payment_hash": str,
"amount": int,
"memo": str,
"extra": Optional[Dict],
},
)
payment_kwargs: PaymentKwargs = dict(
class PaymentKwargs(TypedDict):
wallet_id: str
payment_request: str
payment_hash: str
amount: int
memo: str
extra: Optional[Dict]
payment_kwargs: PaymentKwargs = PaymentKwargs(
wallet_id=wallet_id,
payment_request=payment_request,
payment_hash=invoice.payment_hash,
@@ -120,6 +129,7 @@ async def pay_invoice(
# check_internal() returns the checking_id of the invoice we're waiting for
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
if internal_checking_id:
logger.debug(f"creating temporary internal payment with id {internal_id}")
# create a new payment from this wallet
await create_payment(
checking_id=internal_id,
@@ -129,6 +139,7 @@ async def pay_invoice(
**payment_kwargs,
)
else:
logger.debug(f"creating temporary payment with id {temp_id}")
# create a temporary payment here so we can check if
# the balance is enough in the next step
await create_payment(
@@ -142,6 +153,7 @@ async def pay_invoice(
wallet = await get_wallet(wallet_id, conn=conn)
assert wallet
if wallet.balance_msat < 0:
logger.debug("balance is too low, deleting temporary payment")
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
raise PaymentFailure(
f"You must reserve at least 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
@@ -149,6 +161,7 @@ async def pay_invoice(
raise PermissionError("Insufficient balance.")
if internal_checking_id:
logger.debug(f"marking temporary payment as not pending {internal_checking_id}")
# mark the invoice from the other side as not pending anymore
# so the other side only has access to his new money when we are sure
# the payer has enough to deduct from
@@ -163,11 +176,14 @@ async def pay_invoice(
await internal_invoice_queue.put(internal_checking_id)
else:
logger.debug(f"backend: sending payment {temp_id}")
# actually pay the external invoice
payment: PaymentResponse = await WALLET.pay_invoice(
payment_request, fee_reserve_msat
)
logger.debug(f"backend: pay_invoice finished {temp_id}")
if payment.checking_id:
logger.debug(f"creating final payment {payment.checking_id}")
async with db.connect() as conn:
await create_payment(
checking_id=payment.checking_id,
@@ -177,15 +193,18 @@ async def pay_invoice(
conn=conn,
**payment_kwargs,
)
logger.debug(f"deleting temporary payment {temp_id}")
await delete_payment(temp_id, conn=conn)
else:
logger.debug(f"backend payment failed, no checking_id {temp_id}")
async with db.connect() as conn:
logger.debug(f"deleting temporary payment {temp_id}")
await delete_payment(temp_id, conn=conn)
raise PaymentFailure(
payment.error_message
or "Payment failed, but backend didn't give us an error message."
)
logger.debug(f"payment successful {payment.checking_id}")
return invoice.payment_hash
@@ -216,7 +235,7 @@ async def redeem_lnurl_withdraw(
conn=conn,
)
except:
print(
logger.warning(
f"failed to create invoice on redeem_lnurl_withdraw from {lnurl}. params: {res}"
)
return None
@@ -243,12 +262,15 @@ async def redeem_lnurl_withdraw(
async def perform_lnurlauth(
callback: str, conn: Optional[Connection] = None
callback: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
conn: Optional[Connection] = None,
) -> Optional[LnurlErrorResponse]:
cb = urlparse(callback)
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
key = g().wallet.lnurlauth_key(cb.netloc)
key = wallet.wallet.lnurlauth_key(cb.netloc)
def int_to_bytes_suitable_der(x: int) -> bytes:
"""for strict DER we need to encode the integer with some quirks"""
@@ -325,11 +347,11 @@ async def check_invoice_status(
if not payment.pending:
return status
if payment.is_out and status.failed:
print(f" - deleting outgoing failed payment {payment.checking_id}: {status}")
logger.info(f"deleting outgoing failed payment {payment.checking_id}: {status}")
await payment.delete()
elif not status.pending:
print(
f" - marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}"
logger.info(
f"marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}"
)
await payment.set_pending(status.pending)
return status
+32
View File
@@ -1,4 +1,36 @@
new Vue({
el: '#vue',
data: function () {
return {
searchTerm: '',
filteredExtensions: null
}
},
mounted() {
this.filteredExtensions = this.g.extensions
},
watch: {
searchTerm(term) {
// Reset the filter
this.filteredExtensions = this.g.extensions
if (term !== '') {
// Filter the extensions list
function extensionNameContains(searchTerm) {
return function (extension) {
return (
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
extension.shortDescription
.toLowerCase()
.includes(searchTerm.toLowerCase())
)
}
}
this.filteredExtensions = this.filteredExtensions.filter(
extensionNameContains(term)
)
}
}
},
mixins: [windowMixin]
})
+51
View File
@@ -0,0 +1,51 @@
// the cache version gets updated every time there is a new deployment
const CACHE_VERSION = 1
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => {
return request.headers.get('X-Api-Key') || 'none'
}
// on activation we clean up the previously registered service workers
self.addEventListener('activate', evt =>
evt.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
const currentCacheVersion = cacheName.split('-').slice(-2, 2)
if (currentCacheVersion !== CACHE_VERSION) {
return caches.delete(cacheName)
}
})
)
})
)
)
// The fetch handler serves responses for same-origin resources from a cache.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
if (
event.request.url.startsWith(self.location.origin) &&
event.request.method == 'GET'
) {
// Open the cache
event.respondWith(
caches.open(CURRENT_CACHE + getApiKey(event.request)).then(cache => {
// Go to the network first
return fetch(event.request)
.then(fetchedResponse => {
cache.put(event.request, fetchedResponse.clone())
return fetchedResponse
})
.catch(() => {
// If the network is unavailable, get
return cache.match(event.request.url)
})
})
)
}
})
+11 -6
View File
@@ -618,10 +618,10 @@ new Vue({
},
updateWalletName: function () {
let newName = this.newName
let adminkey = this.g.wallet.adminkey
if (!newName || !newName.length) return
// let data = {name: newName}
LNbits.api
.request('PUT', '/api/v1/wallet/' + newName, this.g.wallet.adminkey, {})
.request('PUT', '/api/v1/wallet/' + newName, adminkey, {})
.then(res => {
this.newName = ''
this.$q.notify({
@@ -691,10 +691,7 @@ new Vue({
},
mounted: function () {
// show disclaimer
if (
this.$refs.disclaimer &&
!this.$q.localStorage.getItem('lnbits.disclaimerShown')
) {
if (!this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
this.disclaimerDialog.show = true
this.$q.localStorage.set('lnbits.disclaimerShown', true)
}
@@ -705,3 +702,11 @@ new Vue({
)
}
})
if (navigator.serviceWorker != null) {
navigator.serviceWorker
.register('/service-worker.js')
.then(function (registration) {
console.log('Registered events at scope: ', registration.scope)
})
}
+7 -4
View File
@@ -1,7 +1,9 @@
import asyncio
import httpx
from typing import List
import httpx
from loguru import logger
from lnbits.tasks import register_invoice_listener
from . import db
@@ -20,7 +22,7 @@ async def register_task_listeners():
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
while True:
payment = await invoice_paid_queue.get()
logger.debug("received invoice paid event")
# send information to sse channel
await dispatch_invoice_listener(payment)
@@ -44,7 +46,7 @@ async def dispatch_invoice_listener(payment: Payment):
try:
send_channel.put_nowait(payment)
except asyncio.QueueFull:
print("removing sse listener", send_channel)
logger.debug("removing sse listener", send_channel)
api_invoice_listeners.remove(send_channel)
@@ -52,7 +54,8 @@ async def dispatch_webhook(payment: Payment):
async with httpx.AsyncClient() as client:
data = payment.dict()
try:
r = await client.post(payment.webhook, json=data, timeout=40)
logger.debug("sending webhook", payment.webhook)
r = await client.post(payment.webhook, json=data, timeout=40) # type: ignore
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
await mark_webhook_sent(payment, -1)
+5 -3
View File
@@ -48,7 +48,9 @@
<code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;}</code
>{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;, "unit":
&lt;string&gt;, "webhook": &lt;url:string&gt;, "internal":
&lt;bool&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
@@ -61,8 +63,8 @@
<code
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
"amount": &lt;int&gt;, "memo": &lt;string&gt;, "webhook":
&lt;url:string&gt;, "unit": &lt;string&gt;}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
"Content-type: application/json"</code
&lt;url:string&gt;, "unit": &lt;string&gt;}' -H "X-Api-Key:
<i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
>
</q-card-section>
</q-card>
+14 -1
View File
@@ -2,10 +2,23 @@
%} {% block scripts %} {{ window_vars(user) }}
<script src="/core/static/js/extensions.js"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-sm-3 col-xs-8 q-ml-auto">
<q-input v-model="searchTerm" label="Search extensions">
<q-icon
v-if="searchTerm !== ''"
name="close"
@click="searchTerm = ''"
class="cursor-pointer q-mt-lg"
/>
</q-input>
</div>
</div>
<div class="row q-col-gutter-md">
<div
class="col-6 col-md-4 col-lg-3"
v-for="extension in g.extensions"
v-for="extension in filteredExtensions"
:key="extension.code"
>
<q-card>
+7 -9
View File
@@ -4,7 +4,6 @@
<!---->
{% block scripts %} {{ window_vars(user, wallet) }}
<script src="/core/static/js/wallet.js"></script>
<link rel="manifest" href="/manifest/{{ user.id }}.webmanifest" />
{% endblock %}
<!---->
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
@@ -706,11 +705,10 @@
<q-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab>
</q-tabs>
{% if service_fee > 0 %}
<div ref="disclaimer"></div>
<q-dialog v-model="disclaimerDialog.show">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-deep-purple">Warning</h6>
<h6 class="q-my-md text-primary">Warning</h6>
<p>
Login functionality to be released in v0.2, for now,
<strong
@@ -720,10 +718,10 @@
</p>
<p>
This service is in BETA, and we hold no responsibility for people losing
access to funds. To encourage you to run your own LNbits installation,
any balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %}
will incur a charge of
<strong>{{ service_fee }}% service fee</strong> per week.
access to funds. {% if service_fee > 0 %} To encourage you to run your
own LNbits installation, any balance on {% raw %}{{
disclaimerDialog.location.host }}{% endraw %} will incur a charge of
<strong>{{ service_fee }}% service fee</strong> per week. {% endif %}
</p>
<div class="row q-mt-lg">
<q-btn
@@ -738,5 +736,5 @@
</div>
</q-card>
</q-dialog>
{% endif %} {% endblock %}
{% endblock %}
</div>
+94 -53
View File
@@ -3,33 +3,30 @@ import hashlib
import json
from binascii import unhexlify
from http import HTTPStatus
from typing import Dict, List, Optional, Union
from typing import Dict, List, Optional, Tuple, Union
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import httpx
from fastapi import Depends, Header, Query, Request
import pyqrcode
from io import BytesIO
from starlette.responses import HTMLResponse, StreamingResponse
from fastapi import Query, Request, Header
from fastapi.exceptions import HTTPException
from fastapi.param_functions import Depends
from fastapi.params import Body
from loguru import logger
from pydantic import BaseModel
from pydantic.fields import Field
from sse_starlette.sse import EventSourceResponse
from lnbits import bolt11, lnurl
from lnbits.bolt11 import Invoice
from lnbits.core.models import Payment, Wallet
from lnbits.decorators import (
WalletAdminKeyChecker,
WalletInvoiceKeyChecker,
WalletTypeInfo,
get_key_type,
require_admin_key
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.requestvars import g
from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE
from lnbits.utils.exchange_rates import (
currencies,
@@ -102,7 +99,7 @@ async def api_update_balance(
@core_app.put("/api/v1/wallet/{new_name}")
async def api_update_wallet(
new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())
new_name: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
await update_wallet(wallet.wallet.id, new_name)
return {
@@ -113,16 +110,29 @@ async def api_update_wallet(
@core_app.get("/api/v1/payments")
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
async def api_payments(
limit: Optional[int] = None,
offset: Optional[int] = None,
wallet: WalletTypeInfo = Depends(get_key_type),
):
pendingPayments = await get_payments(
wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True
wallet_id=wallet.wallet.id,
pending=True,
exclude_uncheckable=True,
limit=limit,
offset=offset,
)
for payment in pendingPayments:
await check_invoice_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
)
return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
return await get_payments(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
limit=limit,
offset=offset,
)
class CreateInvoiceData(BaseModel):
@@ -135,6 +145,7 @@ class CreateInvoiceData(BaseModel):
lnurl_balance_check: Optional[str] = None
extra: Optional[dict] = None
webhook: Optional[str] = None
internal: Optional[bool] = False
bolt11: Optional[str] = None
@@ -148,6 +159,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
if data.unit == "sat":
amount = int(data.amount)
else:
assert data.unit is not None, "unit not set"
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
amount = price_in_sats
@@ -160,6 +172,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
description_hash=description_hash,
extra=data.extra,
webhook=data.webhook,
internal=data.internal,
conn=conn,
)
except InvoiceFailure as e:
@@ -172,7 +185,10 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
lnurl_response: Union[None, bool, str] = None
if data.lnurl_callback:
if "lnurl_balance_check" in data:
save_balance_check(wallet.id, data.lnurl_balance_check)
assert (
data.lnurl_balance_check is not None
), "lnurl_balance_check is required"
await save_balance_check(wallet.id, data.lnurl_balance_check)
async with httpx.AsyncClient() as client:
try:
@@ -234,12 +250,9 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
status_code=HTTPStatus.CREATED,
)
async def api_payments_create(
wallet: WalletTypeInfo = Depends(get_key_type),
invoiceData: CreateInvoiceData = Body(...),
wallet: WalletTypeInfo = Depends(require_invoice_key),
invoiceData: CreateInvoiceData = Body(...), # type: ignore
):
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
if invoiceData.out is True and wallet.wallet_type == 0:
if not invoiceData.bolt11:
raise HTTPException(
@@ -249,8 +262,14 @@ async def api_payments_create(
return await api_payments_pay_invoice(
invoiceData.bolt11, wallet.wallet
) # admin key
# invoice key
return await api_payments_create_invoice(invoiceData, wallet.wallet)
elif not invoiceData.out:
# invoice key
return await api_payments_create_invoice(invoiceData, wallet.wallet)
else:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Invoice (or Admin) key required.",
)
class CreateLNURLData(BaseModel):
@@ -263,7 +282,7 @@ class CreateLNURLData(BaseModel):
@core_app.post("/api/v1/payments/lnurl")
async def api_payments_pay_lnurl(
data: CreateLNURLData, wallet: WalletTypeInfo = Depends(get_key_type)
data: CreateLNURLData, wallet: WalletTypeInfo = Depends(require_admin_key)
):
domain = urlparse(data.callback).netloc
@@ -275,7 +294,7 @@ async def api_payments_pay_lnurl(
timeout=40,
)
if r.is_error:
raise httpx.ConnectError
raise httpx.ConnectError("LNURL callback connection error")
except (httpx.ConnectError, httpx.RequestError):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
@@ -289,6 +308,12 @@ async def api_payments_pay_lnurl(
detail=f"{domain} said: '{params.get('reason', '')}'",
)
if not params.get("pr"):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} did not return a payment request.",
)
invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != data.amount:
raise HTTPException(
@@ -296,11 +321,11 @@ async def api_payments_pay_lnurl(
detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.",
)
# if invoice.description_hash != data.description_hash:
# raise HTTPException(
# status_code=HTTPStatus.BAD_REQUEST,
# detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
# )
if invoice.description_hash != data.description_hash:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
)
extra = {}
@@ -308,7 +333,7 @@ async def api_payments_pay_lnurl(
extra["success_action"] = params["successAction"]
if data.comment:
extra["comment"] = data.comment
assert data.description is not None, "description is required"
payment_hash = await pay_invoice(
wallet_id=wallet.wallet.id,
payment_request=params["pr"],
@@ -325,19 +350,20 @@ async def api_payments_pay_lnurl(
async def subscribe(request: Request, wallet: Wallet):
this_wallet_id = wallet.wallet.id
this_wallet_id = wallet.id
payment_queue = asyncio.Queue(0)
payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0)
print("adding sse listener", payment_queue)
logger.debug("adding sse listener", payment_queue)
api_invoice_listeners.append(payment_queue)
send_queue = asyncio.Queue(0)
send_queue: asyncio.Queue[Tuple[str, Payment]] = asyncio.Queue(0)
async def payment_received() -> None:
while True:
payment: Payment = await payment_queue.get()
if payment.wallet_id == this_wallet_id:
logger.debug("payment receieved", payment)
await send_queue.put(("payment-received", payment))
asyncio.create_task(payment_received())
@@ -362,21 +388,29 @@ async def api_payments_sse(
request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
):
return EventSourceResponse(
subscribe(request, wallet), ping=20, media_type="text/event-stream"
subscribe(request, wallet.wallet), ping=20, media_type="text/event-stream"
)
@core_app.get("/api/v1/payments/{payment_hash}")
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
wallet = None
try:
if X_Api_Key.extra:
print("No key")
except:
wallet = await get_wallet_for_key(X_Api_Key)
payment = await get_standalone_payment(payment_hash)
# We use X_Api_Key here because we want this call to work with and without keys
# If a valid key is given, we also return the field "details", otherwise not
wallet = await get_wallet_for_key(X_Api_Key) if type(X_Api_Key) == str else None
# we have to specify the wallet id here, because postgres and sqlite return internal payments in different order
# and get_standalone_payment otherwise just fetches the first one, causing unpredictable results
payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
)
if payment is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
await check_invoice_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(payment_hash)
payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
)
if not payment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
@@ -394,14 +428,16 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
return {"paid": False}
if wallet and wallet.id == payment.wallet_id:
return {"paid": not payment.pending, "preimage": payment.preimage, "details": payment}
return {
"paid": not payment.pending,
"preimage": payment.preimage,
"details": payment,
}
return {"paid": not payment.pending, "preimage": payment.preimage}
@core_app.get(
"/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())]
)
async def api_lnurlscan(code: str):
@core_app.get("/api/v1/lnurlscan/{code}")
async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type)):
try:
url = lnurl.decode(code)
domain = urlparse(url).netloc
@@ -429,7 +465,7 @@ async def api_lnurlscan(code: str):
params.update(kind="auth")
params.update(callback=url) # with k1 already in it
lnurlauth_key = g().wallet.lnurlauth_key(domain)
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
async with httpx.AsyncClient() as client:
@@ -545,14 +581,19 @@ async def api_payments_decode(data: DecodePayment):
return {"message": "Failed to decode"}
@core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())])
async def api_perform_lnurlauth(callback: str):
err = await perform_lnurlauth(callback)
class Callback(BaseModel):
callback: str = Query(...)
@core_app.post("/api/v1/lnurlauth")
async def api_perform_lnurlauth(
callback: Callback, wallet: WalletTypeInfo = Depends(require_admin_key)
):
err = await perform_lnurlauth(callback.callback, wallet=wallet)
if err:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
)
return ""
@@ -571,8 +612,8 @@ class ConversionData(BaseModel):
async def api_fiat_as_sats(data: ConversionData):
output = {}
if data.from_ == "sat":
output["sats"] = int(data.amount)
output["BTC"] = data.amount / 100000000
output["sats"] = int(data.amount)
for currency in data.to.split(","):
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
data.amount, currency.strip()
+60 -32
View File
@@ -7,6 +7,7 @@ from fastapi.exceptions import HTTPException
from fastapi.params import Depends, Query
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.routing import APIRouter
from loguru import logger
from pydantic.types import UUID4
from starlette.responses import HTMLResponse, JSONResponse
@@ -17,10 +18,12 @@ from lnbits.helpers import template_renderer, url_for
from lnbits.settings import (
LNBITS_ADMIN_USERS,
LNBITS_ALLOWED_USERS,
LNBITS_CUSTOM_LOGO,
LNBITS_SITE_TITLE,
SERVICE_FEE,
)
from ...helpers import get_valid_extensions
from ..crud import (
create_account,
create_wallet,
@@ -52,9 +55,9 @@ async def home(request: Request, lightning: str = None):
)
async def extensions(
request: Request,
user: User = Depends(check_user_exists),
enable: str = Query(None),
disable: str = Query(None),
user: User = Depends(check_user_exists), # type: ignore
enable: str = Query(None), # type: ignore
disable: str = Query(None), # type: ignore
):
extension_to_enable = enable
extension_to_disable = disable
@@ -64,18 +67,28 @@ async def extensions(
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
)
# check if extension exists
if extension_to_enable or extension_to_disable:
ext = extension_to_enable or extension_to_disable
if ext not in [e.code for e in get_valid_extensions()]:
raise HTTPException(
HTTPStatus.BAD_REQUEST, f"Extension '{ext}' doesn't exist."
)
if extension_to_enable:
logger.info(f"Enabling extension: {extension_to_enable} for user {user.id}")
await update_user_extension(
user_id=user.id, extension=extension_to_enable, active=True
)
elif extension_to_disable:
logger.info(f"Disabling extension: {extension_to_disable} for user {user.id}")
await update_user_extension(
user_id=user.id, extension=extension_to_disable, active=False
)
# Update user as his extensions have been updated
if extension_to_enable or extension_to_disable:
user = await get_user(user.id)
user = await get_user(user.id) # type: ignore
return template_renderer().TemplateResponse(
"core/extensions.html", {"request": request, "user": user.dict()}
@@ -96,10 +109,10 @@ nothing: create everything<br>
""",
)
async def wallet(
request: Request = Query(None),
nme: Optional[str] = Query(None),
usr: Optional[UUID4] = Query(None),
wal: Optional[UUID4] = Query(None),
request: Request = Query(None), # type: ignore
nme: Optional[str] = Query(None), # type: ignore
usr: Optional[UUID4] = Query(None), # type: ignore
wal: Optional[UUID4] = Query(None), # type: ignore
):
user_id = usr.hex if usr else None
wallet_id = wal.hex if wal else None
@@ -108,6 +121,7 @@ async def wallet(
if not user_id:
user = await get_user((await create_account()).id)
logger.info(f"Create user {user.id}") # type: ignore
else:
user = await get_user(user_id)
if not user:
@@ -121,18 +135,22 @@ async def wallet(
if LNBITS_ADMIN_USERS and user_id in LNBITS_ADMIN_USERS:
user.admin = True
if not wallet_id:
if user.wallets and not wallet_name:
wallet = user.wallets[0]
if user.wallets and not wallet_name: # type: ignore
wallet = user.wallets[0] # type: ignore
else:
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) # type: ignore
logger.info(
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}" # type: ignore
)
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}",
f"/wallet?usr={user.id}&wal={wallet.id}", # type: ignore
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
wallet = user.get_wallet(wallet_id)
if not wallet:
logger.debug(f"Access wallet {wallet_name}{'of user '+ user.id if user else ''}")
userwallet = user.get_wallet(wallet_id) # type: ignore
if not userwallet:
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "Wallet not found"}
)
@@ -141,9 +159,10 @@ async def wallet(
"core/wallet.html",
{
"request": request,
"user": user.dict(),
"wallet": wallet.dict(),
"user": user.dict(), # type: ignore
"wallet": userwallet.dict(),
"service_fee": service_fee,
"web_manifest": f"/manifest/{user.id}.webmanifest", # type: ignore
},
)
@@ -197,20 +216,20 @@ async def lnurl_full_withdraw_callback(request: Request):
@core_html_routes.get("/deletewallet", response_class=RedirectResponse)
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)):
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)): # type: ignore
user = await get_user(usr)
user_wallet_ids = [u.id for u in user.wallets]
print("USR", user_wallet_ids)
user_wallet_ids = [u.id for u in user.wallets] # type: ignore
if wal not in user_wallet_ids:
raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
else:
await delete_wallet(user_id=user.id, wallet_id=wal)
await delete_wallet(user_id=user.id, wallet_id=wal) # type: ignore
user_wallet_ids.remove(wal)
logger.debug("Deleted wallet {wal} of user {user.id}")
if user_wallet_ids:
return RedirectResponse(
url_for("/wallet", usr=user.id, wal=user_wallet_ids[0]),
url_for("/wallet", usr=user.id, wal=user_wallet_ids[0]), # type: ignore
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
@@ -223,15 +242,17 @@ async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query
async def lnurl_balance_notify(request: Request, service: str):
bc = await get_balance_check(request.query_params.get("wal"), service)
if bc:
redeem_lnurl_withdraw(bc.wallet, bc.url)
await redeem_lnurl_withdraw(bc.wallet, bc.url)
@core_html_routes.get("/lnurlwallet", response_class=RedirectResponse, name="core.lnurlwallet")
@core_html_routes.get(
"/lnurlwallet", response_class=RedirectResponse, name="core.lnurlwallet"
)
async def lnurlwallet(request: Request):
async with db.connect() as conn:
account = await create_account(conn=conn)
user = await get_user(account.id, conn=conn)
wallet = await create_wallet(user_id=user.id, conn=conn)
wallet = await create_wallet(user_id=user.id, conn=conn) # type: ignore
asyncio.create_task(
redeem_lnurl_withdraw(
@@ -244,11 +265,16 @@ async def lnurlwallet(request: Request):
)
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}",
f"/wallet?usr={user.id}&wal={wallet.id}", # type: ignore
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
@core_html_routes.get("/service-worker.js", response_class=FileResponse)
async def service_worker():
return FileResponse("lnbits/core/static/js/service-worker.js")
@core_html_routes.get("/manifest/{usr}.webmanifest")
async def manifest(usr: str):
user = await get_user(usr)
@@ -256,21 +282,23 @@ async def manifest(usr: str):
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return {
"short_name": "LNbits",
"name": "LNbits Wallet",
"short_name": LNBITS_SITE_TITLE,
"name": LNBITS_SITE_TITLE + " Wallet",
"icons": [
{
"src": "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
"src": LNBITS_CUSTOM_LOGO
if LNBITS_CUSTOM_LOGO
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
"type": "image/png",
"sizes": "900x900",
}
],
"start_url": "/wallet?usr=" + usr,
"background_color": "#3367D6",
"description": "Weather forecast information",
"start_url": "/wallet?usr=" + usr + "&wal=" + user.wallets[0].id,
"background_color": "#1F2234",
"description": "Bitcoin Lightning Wallet",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"theme_color": "#1F2234",
"shortcuts": [
{
"name": wallet.name,
+2 -1
View File
@@ -4,6 +4,7 @@ from http import HTTPStatus
from urllib.parse import urlparse
from fastapi import HTTPException
from loguru import logger
from starlette.requests import Request
from starlette.responses import HTMLResponse
@@ -45,7 +46,7 @@ async def api_public_payment_longpolling(payment_hash):
payment_queue = asyncio.Queue(0)
print("adding standalone invoice listener", payment_hash, payment_queue)
logger.debug("adding standalone invoice listener", payment_hash, payment_queue)
api_invoice_listeners.append(payment_queue)
response = None