Merge remote-tracking branch 'origin/master' into StreamerCopilot
This commit is contained in:
commit
fe6e6764fa
|
@ -2,13 +2,14 @@ import json
|
||||||
import datetime
|
import datetime
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.settings import DEFAULT_WALLET_NAME
|
from lnbits.settings import DEFAULT_WALLET_NAME
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import User, Wallet, Payment
|
from .models import User, Wallet, Payment, BalanceCheck
|
||||||
|
|
||||||
|
|
||||||
# accounts
|
# accounts
|
||||||
|
@ -379,3 +380,77 @@ async def check_internal(
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return row["checking_id"]
|
return row["checking_id"]
|
||||||
|
|
||||||
|
|
||||||
|
# balance_check
|
||||||
|
# -------------
|
||||||
|
|
||||||
|
|
||||||
|
async def save_balance_check(
|
||||||
|
wallet_id: str,
|
||||||
|
url: str,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
domain = urlparse(url).netloc
|
||||||
|
|
||||||
|
await (conn or db).execute(
|
||||||
|
"""
|
||||||
|
INSERT OR REPLACE INTO balance_check (wallet, service, url)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(wallet_id, domain, url),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_balance_check(
|
||||||
|
wallet_id: str,
|
||||||
|
domain: str,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> Optional[BalanceCheck]:
|
||||||
|
row = await (conn or db).fetchone(
|
||||||
|
"""
|
||||||
|
SELECT wallet, service, url
|
||||||
|
FROM balance_check
|
||||||
|
WHERE wallet = ? AND service = ?
|
||||||
|
""",
|
||||||
|
(wallet_id, domain),
|
||||||
|
)
|
||||||
|
return BalanceCheck.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_balance_checks(conn: Optional[Connection] = None) -> List[BalanceCheck]:
|
||||||
|
rows = await (conn or db).fetchall("SELECT wallet, service, url FROM balance_check")
|
||||||
|
return [BalanceCheck.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
# balance_notify
|
||||||
|
# --------------
|
||||||
|
|
||||||
|
|
||||||
|
async def save_balance_notify(
|
||||||
|
wallet_id: str,
|
||||||
|
url: str,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
await (conn or db).execute(
|
||||||
|
"""
|
||||||
|
INSERT OR REPLACE INTO balance_notify (wallet, url)
|
||||||
|
VALUES (?, ?)
|
||||||
|
""",
|
||||||
|
(wallet_id, url),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_balance_notify(
|
||||||
|
wallet_id: str,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
row = await (conn or db).fetchone(
|
||||||
|
"""
|
||||||
|
SELECT url
|
||||||
|
FROM balance_notify
|
||||||
|
WHERE wallet = ?
|
||||||
|
""",
|
||||||
|
(wallet_id,),
|
||||||
|
)
|
||||||
|
return row[0] if row else None
|
||||||
|
|
|
@ -161,3 +161,32 @@ async def m004_ensure_fees_are_always_negative(db):
|
||||||
GROUP BY wallet;
|
GROUP BY wallet;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m005_balance_check_balance_notify(db):
|
||||||
|
"""
|
||||||
|
Keep track of balanceCheck-enabled lnurl-withdrawals to be consumed by an LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE balance_check (
|
||||||
|
wallet INTEGER NOT NULL REFERENCES wallets (id),
|
||||||
|
service TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE(wallet, service)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE balance_notify (
|
||||||
|
wallet INTEGER NOT NULL REFERENCES wallets (id),
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE(wallet, url)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import json
|
import json
|
||||||
import hmac
|
import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from quart import url_for
|
||||||
from ecdsa import SECP256k1, SigningKey # type: ignore
|
from ecdsa import SECP256k1, SigningKey # type: ignore
|
||||||
|
from lnurl import encode as lnurl_encode # type: ignore
|
||||||
from typing import List, NamedTuple, Optional, Dict
|
from typing import List, NamedTuple, Optional, Dict
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
|
|
||||||
|
@ -36,6 +38,22 @@ class Wallet(NamedTuple):
|
||||||
def balance(self) -> int:
|
def balance(self) -> int:
|
||||||
return self.balance_msat // 1000
|
return self.balance_msat // 1000
|
||||||
|
|
||||||
|
@property
|
||||||
|
def withdrawable_balance(self) -> int:
|
||||||
|
from .services import fee_reserve
|
||||||
|
|
||||||
|
return self.balance_msat - fee_reserve(self.balance_msat)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lnurlwithdraw_full(self) -> str:
|
||||||
|
url = url_for(
|
||||||
|
"core.lnurl_full_withdraw",
|
||||||
|
usr=self.user,
|
||||||
|
wal=self.id,
|
||||||
|
_external=True,
|
||||||
|
)
|
||||||
|
return lnurl_encode(url)
|
||||||
|
|
||||||
def lnurlauth_key(self, domain: str) -> SigningKey:
|
def lnurlauth_key(self, domain: str) -> SigningKey:
|
||||||
hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
|
hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
|
||||||
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
|
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
|
||||||
|
@ -158,3 +176,13 @@ class Payment(NamedTuple):
|
||||||
from .crud import delete_payment
|
from .crud import delete_payment
|
||||||
|
|
||||||
await delete_payment(self.checking_id)
|
await delete_payment(self.checking_id)
|
||||||
|
|
||||||
|
|
||||||
|
class BalanceCheck(NamedTuple):
|
||||||
|
wallet: str
|
||||||
|
service: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row):
|
||||||
|
return cls(wallet=row["wallet"], service=row["service"], url=row["url"])
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
|
import trio # type: ignore
|
||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from typing import Optional, Tuple, Dict
|
from typing import Optional, Tuple, Dict
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
from quart import g
|
from quart import g, url_for
|
||||||
from lnurl import LnurlErrorResponse, LnurlWithdrawResponse # type: ignore
|
from lnurl import LnurlErrorResponse, decode as decode_lnurl # type: ignore
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import TypedDict # type: ignore
|
from typing import TypedDict # type: ignore
|
||||||
|
@ -128,10 +129,9 @@ async def pay_invoice(
|
||||||
else:
|
else:
|
||||||
# create a temporary payment here so we can check if
|
# create a temporary payment here so we can check if
|
||||||
# the balance is enough in the next step
|
# the balance is enough in the next step
|
||||||
fee_reserve = max(1000, int(invoice.amount_msat * 0.01))
|
|
||||||
await create_payment(
|
await create_payment(
|
||||||
checking_id=temp_id,
|
checking_id=temp_id,
|
||||||
fee=-fee_reserve,
|
fee=-fee_reserve(invoice.amount_msat),
|
||||||
conn=conn,
|
conn=conn,
|
||||||
**payment_kwargs,
|
**payment_kwargs,
|
||||||
)
|
)
|
||||||
|
@ -180,25 +180,49 @@ async def pay_invoice(
|
||||||
|
|
||||||
async def redeem_lnurl_withdraw(
|
async def redeem_lnurl_withdraw(
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
res: LnurlWithdrawResponse,
|
lnurl_request: str,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
|
extra: Optional[Dict] = None,
|
||||||
|
wait_seconds: int = 0,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
res = {}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
lnurl = decode_lnurl(lnurl_request)
|
||||||
|
r = await client.get(str(lnurl))
|
||||||
|
res = r.json()
|
||||||
|
|
||||||
_, payment_request = await create_invoice(
|
_, payment_request = await create_invoice(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
amount=res.max_sats,
|
amount=int(res["maxWithdrawable"] / 1000),
|
||||||
memo=memo or res.default_description or "",
|
memo=memo or res["defaultDescription"] or "",
|
||||||
extra={"tag": "lnurlwallet"},
|
extra=extra,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if wait_seconds:
|
||||||
|
await trio.sleep(wait_seconds)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"k1": res["k1"],
|
||||||
|
"pr": payment_request,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
params["balanceNotify"] = url_for(
|
||||||
|
"core.lnurl_balance_notify",
|
||||||
|
service=urlparse(lnurl_request).netloc,
|
||||||
|
wal=wallet_id,
|
||||||
|
_external=True,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
await client.get(
|
await client.get(
|
||||||
res.callback.base,
|
res["callback"],
|
||||||
params={
|
params=params,
|
||||||
**res.callback.query_params,
|
|
||||||
**{"k1": res.k1, "pr": payment_request},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -286,3 +310,7 @@ async def check_invoice_status(
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
return await WALLET.get_invoice_status(payment.checking_id)
|
return await WALLET.get_invoice_status(payment.checking_id)
|
||||||
|
|
||||||
|
|
||||||
|
def fee_reserve(amount_msat: int) -> int:
|
||||||
|
return max(1000, int(amount_msat * 0.01))
|
||||||
|
|
|
@ -3,7 +3,9 @@ import httpx
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
|
from .crud import get_balance_notify
|
||||||
from .models import Payment
|
from .models import Payment
|
||||||
|
|
||||||
sse_listeners: List[trio.MemorySendChannel] = []
|
sse_listeners: List[trio.MemorySendChannel] = []
|
||||||
|
@ -24,6 +26,19 @@ async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||||
if payment.webhook and not payment.webhook_status:
|
if payment.webhook and not payment.webhook_status:
|
||||||
await dispatch_webhook(payment)
|
await dispatch_webhook(payment)
|
||||||
|
|
||||||
|
# dispatch balance_notify
|
||||||
|
url = await get_balance_notify(payment.wallet_id)
|
||||||
|
if url:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
r = await client.post(
|
||||||
|
url,
|
||||||
|
timeout=4,
|
||||||
|
)
|
||||||
|
await mark_webhook_sent(payment, r.status_code)
|
||||||
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_sse(payment: Payment):
|
async def dispatch_sse(payment: Payment):
|
||||||
for send_channel in sse_listeners:
|
for send_channel in sse_listeners:
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<!---->
|
<!---->
|
||||||
{% block scripts %} {{ window_vars(user, wallet) }}
|
{% block scripts %} {{ window_vars(user, wallet) }}
|
||||||
<script src="/core/static/js/wallet.js"></script>
|
<script src="/core/static/js/wallet.js"></script>
|
||||||
<link rel="manifest" href="/manifest/{{ user.id }}.webmanifest">
|
<link rel="manifest" href="/manifest/{{ user.id }}.webmanifest" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<!---->
|
<!---->
|
||||||
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
|
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
|
||||||
|
@ -231,13 +231,39 @@
|
||||||
<q-list>
|
<q-list>
|
||||||
{% include "core/_api_docs.html" %}
|
{% include "core/_api_docs.html" %}
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
<q-expansion-item
|
||||||
|
group="extras"
|
||||||
|
icon="crop_free"
|
||||||
|
label="Drain Funds"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="text-center">
|
||||||
|
<p>
|
||||||
|
This is an LNURL-withdraw QR code for slurping everything from
|
||||||
|
this wallet. Do not share with anyone.
|
||||||
|
</p>
|
||||||
|
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
||||||
|
<qrcode
|
||||||
|
value="{{wallet.lnurlwithdraw_full}}"
|
||||||
|
:options="{width:240}"
|
||||||
|
></qrcode>
|
||||||
|
</a>
|
||||||
|
<p>
|
||||||
|
It is compatible with <code>balanceCheck</code> and
|
||||||
|
<code>balanceNotify</code> so your wallet may keep pulling the
|
||||||
|
funds continuously from here after the first withdraw.
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-separator></q-separator>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
group="extras"
|
group="extras"
|
||||||
icon="settings_cell"
|
icon="settings_cell"
|
||||||
label="Export to Phone with QR Code"
|
label="Export to Phone with QR Code"
|
||||||
>
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section class="text-center">
|
||||||
<p>
|
<p>
|
||||||
This QR code contains your wallet URL with full access. You
|
This QR code contains your wallet URL with full access. You
|
||||||
can scan it from your phone to open your wallet from there.
|
can scan it from your phone to open your wallet from there.
|
||||||
|
|
|
@ -3,7 +3,7 @@ import json
|
||||||
import lnurl # type: ignore
|
import lnurl # type: ignore
|
||||||
import httpx
|
import httpx
|
||||||
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
||||||
from quart import g, jsonify, make_response
|
from quart import g, jsonify, make_response, url_for
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union
|
||||||
|
@ -12,6 +12,7 @@ from lnbits import bolt11
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||||
|
|
||||||
from .. import core_app, db
|
from .. import core_app, db
|
||||||
|
from ..crud import save_balance_check
|
||||||
from ..services import (
|
from ..services import (
|
||||||
PaymentFailure,
|
PaymentFailure,
|
||||||
InvoiceFailure,
|
InvoiceFailure,
|
||||||
|
@ -60,6 +61,7 @@ async def api_payments():
|
||||||
"excludes": "memo",
|
"excludes": "memo",
|
||||||
},
|
},
|
||||||
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
|
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
|
||||||
|
"lnurl_balance_check": {"type": "string", "required": False},
|
||||||
"extra": {"type": "dict", "nullable": True, "required": False},
|
"extra": {"type": "dict", "nullable": True, "required": False},
|
||||||
"webhook": {"type": "string", "empty": False, "required": False},
|
"webhook": {"type": "string", "empty": False, "required": False},
|
||||||
}
|
}
|
||||||
|
@ -92,11 +94,22 @@ async def api_payments_create_invoice():
|
||||||
|
|
||||||
lnurl_response: Union[None, bool, str] = None
|
lnurl_response: Union[None, bool, str] = None
|
||||||
if g.data.get("lnurl_callback"):
|
if g.data.get("lnurl_callback"):
|
||||||
|
if "lnurl_balance_check" in g.data:
|
||||||
|
save_balance_check(g.wallet.id, g.data["lnurl_balance_check"])
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
g.data["lnurl_callback"],
|
g.data["lnurl_callback"],
|
||||||
params={"pr": payment_request},
|
params={
|
||||||
|
"pr": payment_request,
|
||||||
|
"balanceNotify": url_for(
|
||||||
|
"core.lnurl_balance_notify",
|
||||||
|
service=urlparse(g.data["lnurl_callback"]).netloc,
|
||||||
|
wal=g.wallet.id,
|
||||||
|
_external=True,
|
||||||
|
),
|
||||||
|
},
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
|
@ -387,6 +400,12 @@ async def api_lnurlscan(code: str):
|
||||||
parsed_callback: ParseResult = urlparse(data.callback)
|
parsed_callback: ParseResult = urlparse(data.callback)
|
||||||
qs: Dict = parse_qs(parsed_callback.query)
|
qs: Dict = parse_qs(parsed_callback.query)
|
||||||
qs["k1"] = data.k1
|
qs["k1"] = data.k1
|
||||||
|
|
||||||
|
# balanceCheck/balanceNotify
|
||||||
|
if "balanceCheck" in jdata:
|
||||||
|
params.update(balanceCheck=jdata["balanceCheck"])
|
||||||
|
|
||||||
|
# format callback url and send to client
|
||||||
parsed_callback = parsed_callback._replace(query=urlencode(qs, doseq=True))
|
parsed_callback = parsed_callback._replace(query=urlencode(qs, doseq=True))
|
||||||
params.update(callback=urlunparse(parsed_callback))
|
params.update(callback=urlunparse(parsed_callback))
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import trio # type: ignore
|
|
||||||
import httpx
|
|
||||||
from os import path
|
from os import path
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from quart import (
|
from quart import (
|
||||||
|
@ -12,11 +10,10 @@ from quart import (
|
||||||
send_from_directory,
|
send_from_directory,
|
||||||
url_for,
|
url_for,
|
||||||
)
|
)
|
||||||
from lnurl import LnurlResponse, LnurlWithdrawResponse, decode as decode_lnurl # type: ignore
|
|
||||||
|
|
||||||
from lnbits.core import core_app, db
|
from lnbits.core import core_app, db
|
||||||
from lnbits.decorators import check_user_exists, validate_uuids
|
from lnbits.decorators import check_user_exists, validate_uuids
|
||||||
from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE
|
from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE, LNBITS_SITE_TITLE
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_account,
|
create_account,
|
||||||
|
@ -24,8 +21,10 @@ from ..crud import (
|
||||||
update_user_extension,
|
update_user_extension,
|
||||||
create_wallet,
|
create_wallet,
|
||||||
delete_wallet,
|
delete_wallet,
|
||||||
|
get_balance_check,
|
||||||
|
save_balance_notify,
|
||||||
)
|
)
|
||||||
from ..services import redeem_lnurl_withdraw
|
from ..services import redeem_lnurl_withdraw, pay_invoice
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/favicon.ico")
|
@core_app.route("/favicon.ico")
|
||||||
|
@ -108,6 +107,62 @@ async def wallet():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.route("/withdraw")
|
||||||
|
@validate_uuids(["usr", "wal"], required=True)
|
||||||
|
async def lnurl_full_withdraw():
|
||||||
|
user = await get_user(request.args.get("usr"))
|
||||||
|
if not user:
|
||||||
|
return jsonify({"status": "ERROR", "reason": "User does not exist."})
|
||||||
|
|
||||||
|
wallet = user.get_wallet(request.args.get("wal"))
|
||||||
|
if not wallet:
|
||||||
|
return jsonify({"status": "ERROR", "reason": "Wallet does not exist."})
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"tag": "withdrawRequest",
|
||||||
|
"callback": url_for(
|
||||||
|
"core.lnurl_full_withdraw_callback",
|
||||||
|
usr=user.id,
|
||||||
|
wal=wallet.id,
|
||||||
|
_external=True,
|
||||||
|
),
|
||||||
|
"k1": "0",
|
||||||
|
"minWithdrawable": 1 if wallet.withdrawable_balance else 0,
|
||||||
|
"maxWithdrawable": wallet.withdrawable_balance,
|
||||||
|
"defaultDescription": f"{LNBITS_SITE_TITLE} balance withdraw from {wallet.id[0:5]}",
|
||||||
|
"balanceCheck": url_for(
|
||||||
|
"core.lnurl_full_withdraw", usr=user.id, wal=wallet.id, _external=True
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.route("/withdraw/cb")
|
||||||
|
@validate_uuids(["usr", "wal"], required=True)
|
||||||
|
async def lnurl_full_withdraw_callback():
|
||||||
|
user = await get_user(request.args.get("usr"))
|
||||||
|
if not user:
|
||||||
|
return jsonify({"status": "ERROR", "reason": "User does not exist."})
|
||||||
|
|
||||||
|
wallet = user.get_wallet(request.args.get("wal"))
|
||||||
|
if not wallet:
|
||||||
|
return jsonify({"status": "ERROR", "reason": "Wallet does not exist."})
|
||||||
|
|
||||||
|
pr = request.args.get("pr")
|
||||||
|
|
||||||
|
async def pay():
|
||||||
|
await pay_invoice(wallet_id=wallet.id, payment_request=pr)
|
||||||
|
|
||||||
|
g.nursery.start_soon(pay)
|
||||||
|
|
||||||
|
balance_notify = request.args.get("balanceNotify")
|
||||||
|
if balance_notify:
|
||||||
|
await save_balance_notify(wallet.id, balance_notify)
|
||||||
|
|
||||||
|
return jsonify({"status": "OK"})
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/deletewallet")
|
@core_app.route("/deletewallet")
|
||||||
@validate_uuids(["usr", "wal"], required=True)
|
@validate_uuids(["usr", "wal"], required=True)
|
||||||
@check_user_exists()
|
@check_user_exists()
|
||||||
|
@ -127,31 +182,16 @@ async def deletewallet():
|
||||||
return redirect(url_for("core.home"))
|
return redirect(url_for("core.home"))
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.route("/withdraw/notify/<service>")
|
||||||
|
@validate_uuids(["wal"], required=True)
|
||||||
|
async def lnurl_balance_notify(service: str):
|
||||||
|
bc = await get_balance_check(request.args.get("wal"), service)
|
||||||
|
if bc:
|
||||||
|
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/lnurlwallet")
|
@core_app.route("/lnurlwallet")
|
||||||
async def lnurlwallet():
|
async def lnurlwallet():
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
try:
|
|
||||||
lnurl = decode_lnurl(request.args.get("lightning"))
|
|
||||||
r = await client.get(str(lnurl))
|
|
||||||
withdraw_res = LnurlResponse.from_dict(r.json())
|
|
||||||
|
|
||||||
if not withdraw_res.ok:
|
|
||||||
return (
|
|
||||||
f"Could not process lnurl-withdraw: {withdraw_res.error_msg}",
|
|
||||||
HTTPStatus.BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(withdraw_res, LnurlWithdrawResponse):
|
|
||||||
return (
|
|
||||||
f"Expected an lnurl-withdraw code, got {withdraw_res.tag}",
|
|
||||||
HTTPStatus.BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
return (
|
|
||||||
f"Could not process lnurl-withdraw: {exc}",
|
|
||||||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
)
|
|
||||||
|
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
account = await create_account(conn=conn)
|
account = await create_account(conn=conn)
|
||||||
user = await get_user(account.id, conn=conn)
|
user = await get_user(account.id, conn=conn)
|
||||||
|
@ -160,10 +200,11 @@ async def lnurlwallet():
|
||||||
g.nursery.start_soon(
|
g.nursery.start_soon(
|
||||||
redeem_lnurl_withdraw,
|
redeem_lnurl_withdraw,
|
||||||
wallet.id,
|
wallet.id,
|
||||||
withdraw_res,
|
request.args.get("lightning"),
|
||||||
"LNbits initial funding: voucher redeem.",
|
"LNbits initial funding: voucher redeem.",
|
||||||
|
{"tag": "lnurlwallet"},
|
||||||
|
5, # wait 5 seconds before sending the invoice to the service
|
||||||
)
|
)
|
||||||
await trio.sleep(3)
|
|
||||||
|
|
||||||
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ var ciframeLoaded = !1,
|
||||||
captchaStyleAdded = !1
|
captchaStyleAdded = !1
|
||||||
|
|
||||||
function ccreateIframeElement(t = {}) {
|
function ccreateIframeElement(t = {}) {
|
||||||
const e = document.createElement('iframe')
|
const e = document.createElement('iframe')
|
||||||
// e.style.marginLeft = "25px",
|
// e.style.marginLeft = "25px",
|
||||||
;(e.style.border = 'none'),
|
;(e.style.border = 'none'),
|
||||||
(e.style.width = '100%'),
|
(e.style.width = '100%'),
|
||||||
|
@ -12,11 +12,9 @@ function ccreateIframeElement(t = {}) {
|
||||||
t.dest, t.amount, t.currency, t.label, t.opReturn
|
t.dest, t.amount, t.currency, t.label, t.opReturn
|
||||||
var captchaid = document
|
var captchaid = document
|
||||||
.getElementById('captchascript')
|
.getElementById('captchascript')
|
||||||
.getAttribute('data-captchaid');
|
.getAttribute('data-captchaid')
|
||||||
var lnbhostsrc = document
|
var lnbhostsrc = document.getElementById('captchascript').getAttribute('src')
|
||||||
.getElementById('captchascript')
|
var lnbhost = lnbhostsrc.split('/captcha/static/js/captcha.js')[0]
|
||||||
.getAttribute('src');
|
|
||||||
var lnbhost = lnbhostsrc.split("/captcha/static/js/captcha.js")[0];
|
|
||||||
return (e.src = lnbhost + '/captcha/' + captchaid), e
|
return (e.src = lnbhost + '/captcha/' + captchaid), e
|
||||||
}
|
}
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import unicodedata
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from lnbits.core.crud import create_account, create_wallet
|
from lnbits.core.crud import create_account, create_wallet
|
||||||
|
@ -65,22 +64,36 @@ async def add_track(
|
||||||
name: str,
|
name: str,
|
||||||
download_url: Optional[str],
|
download_url: Optional[str],
|
||||||
price_msat: int,
|
price_msat: int,
|
||||||
producer_name: Optional[str],
|
producer: Optional[int],
|
||||||
producer_id: Optional[int],
|
|
||||||
) -> int:
|
) -> int:
|
||||||
if producer_id:
|
|
||||||
p_id = producer_id
|
|
||||||
elif producer_name:
|
|
||||||
p_id = await add_producer(livestream, producer_name)
|
|
||||||
else:
|
|
||||||
raise TypeError("need either producer_id or producer_name arguments")
|
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO tracks (livestream, name, download_url, price_msat, producer)
|
INSERT INTO tracks (livestream, name, download_url, price_msat, producer)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(livestream, name, download_url, price_msat, p_id),
|
(livestream, name, download_url, price_msat, producer),
|
||||||
|
)
|
||||||
|
return result._result_proxy.lastrowid
|
||||||
|
|
||||||
|
|
||||||
|
async def update_track(
|
||||||
|
livestream: int,
|
||||||
|
track_id: int,
|
||||||
|
name: str,
|
||||||
|
download_url: Optional[str],
|
||||||
|
price_msat: int,
|
||||||
|
producer: int,
|
||||||
|
) -> int:
|
||||||
|
result = await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE tracks SET
|
||||||
|
name = ?,
|
||||||
|
download_url = ?,
|
||||||
|
price_msat = ?,
|
||||||
|
producer = ?
|
||||||
|
WHERE livestream = ? AND id = ?
|
||||||
|
""",
|
||||||
|
(name, download_url, price_msat, producer, livestream, track_id),
|
||||||
)
|
)
|
||||||
return result._result_proxy.lastrowid
|
return result._result_proxy.lastrowid
|
||||||
|
|
||||||
|
@ -120,7 +133,7 @@ async def delete_track_from_livestream(livestream: int, track_id: int):
|
||||||
|
|
||||||
|
|
||||||
async def add_producer(livestream: int, name: str) -> int:
|
async def add_producer(livestream: int, name: str) -> int:
|
||||||
name = "".join([unicodedata.normalize("NFD", l)[0] for l in name if l]).strip()
|
name = name.strip()
|
||||||
|
|
||||||
existing = await db.fetchall(
|
existing = await db.fetchall(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -10,7 +10,7 @@ from .crud import get_livestream, get_livestream_by_track, get_track
|
||||||
|
|
||||||
|
|
||||||
@livestream_ext.route("/lnurl/<ls_id>", methods=["GET"])
|
@livestream_ext.route("/lnurl/<ls_id>", methods=["GET"])
|
||||||
async def lnurl_response(ls_id):
|
async def lnurl_livestream(ls_id):
|
||||||
ls = await get_livestream(ls_id)
|
ls = await get_livestream(ls_id)
|
||||||
if not ls:
|
if not ls:
|
||||||
return jsonify({"status": "ERROR", "reason": "Livestream not found."})
|
return jsonify({"status": "ERROR", "reason": "Livestream not found."})
|
||||||
|
@ -34,6 +34,27 @@ async def lnurl_response(ls_id):
|
||||||
return jsonify(params)
|
return jsonify(params)
|
||||||
|
|
||||||
|
|
||||||
|
@livestream_ext.route("/lnurl/t/<track_id>", methods=["GET"])
|
||||||
|
async def lnurl_track(track_id):
|
||||||
|
track = await get_track(track_id)
|
||||||
|
if not track:
|
||||||
|
return jsonify({"status": "ERROR", "reason": "Track not found."})
|
||||||
|
|
||||||
|
resp = LnurlPayResponse(
|
||||||
|
callback=url_for(
|
||||||
|
"livestream.lnurl_callback", track_id=track.id, _external=True
|
||||||
|
),
|
||||||
|
min_sendable=track.min_sendable,
|
||||||
|
max_sendable=track.max_sendable,
|
||||||
|
metadata=await track.lnurlpay_metadata(),
|
||||||
|
)
|
||||||
|
|
||||||
|
params = resp.dict()
|
||||||
|
params["commentAllowed"] = 300
|
||||||
|
|
||||||
|
return jsonify(params)
|
||||||
|
|
||||||
|
|
||||||
@livestream_ext.route("/lnurl/cb/<track_id>", methods=["GET"])
|
@livestream_ext.route("/lnurl/cb/<track_id>", methods=["GET"])
|
||||||
async def lnurl_callback(track_id):
|
async def lnurl_callback(track_id):
|
||||||
track = await get_track(track_id)
|
track = await get_track(track_id)
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Livestream(NamedTuple):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lnurl(self) -> Lnurl:
|
def lnurl(self) -> Lnurl:
|
||||||
url = url_for("livestream.lnurl_response", ls_id=self.id, _external=True)
|
url = url_for("livestream.lnurl_livestream", ls_id=self.id, _external=True)
|
||||||
return lnurl_encode(url)
|
return lnurl_encode(url)
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,6 +33,11 @@ class Track(NamedTuple):
|
||||||
def max_sendable(self) -> int:
|
def max_sendable(self) -> int:
|
||||||
return max(50_000_000, self.price_msat * 5)
|
return max(50_000_000, self.price_msat * 5)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lnurl(self) -> Lnurl:
|
||||||
|
url = url_for("livestream.lnurl_track", track_id=self.id, _external=True)
|
||||||
|
return lnurl_encode(url)
|
||||||
|
|
||||||
async def fullname(self) -> str:
|
async def fullname(self) -> str:
|
||||||
from .crud import get_producer
|
from .crud import get_producer
|
||||||
|
|
||||||
|
|
|
@ -93,24 +93,21 @@ new Vue({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
addTrack() {
|
addTrack() {
|
||||||
let {name, producer, price_sat, download_url} = this.trackDialog.data
|
let {id, name, producer, price_sat, download_url} = this.trackDialog.data
|
||||||
|
|
||||||
|
const [method, path] = id
|
||||||
|
? ['PUT', `/livestream/api/v1/livestream/tracks/${id}`]
|
||||||
|
: ['POST', '/livestream/api/v1/livestream/tracks']
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(method, path, this.selectedWallet.inkey, {
|
||||||
'POST',
|
download_url:
|
||||||
'/livestream/api/v1/livestream/tracks',
|
download_url && download_url.length > 0 ? download_url : undefined,
|
||||||
this.selectedWallet.inkey,
|
name,
|
||||||
{
|
price_msat: price_sat * 1000 || 0,
|
||||||
download_url:
|
producer_name: typeof producer === 'string' ? producer : undefined,
|
||||||
download_url && download_url.length > 0
|
producer_id: typeof producer === 'object' ? producer.id : undefined
|
||||||
? download_url
|
})
|
||||||
: undefined,
|
|
||||||
name,
|
|
||||||
price_msat: price_sat * 1000 || 0,
|
|
||||||
producer_name: typeof producer === 'string' ? producer : undefined,
|
|
||||||
producer_id: typeof producer === 'object' ? producer.id : undefined
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: `Track '${this.trackDialog.data.name}' added.`,
|
message: `Track '${this.trackDialog.data.name}' added.`,
|
||||||
|
@ -124,6 +121,21 @@ new Vue({
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
openAddTrackDialog() {
|
||||||
|
this.trackDialog.show = true
|
||||||
|
this.trackDialog.data = {}
|
||||||
|
},
|
||||||
|
openUpdateDialog(itemId) {
|
||||||
|
this.trackDialog.show = true
|
||||||
|
let item = this.livestream.tracks.find(item => item.id === itemId)
|
||||||
|
this.trackDialog.data = {
|
||||||
|
...item,
|
||||||
|
producer: this.livestream.producers.find(
|
||||||
|
prod => prod.id === item.producer
|
||||||
|
),
|
||||||
|
price_sat: Math.round(item.price_msat / 1000)
|
||||||
|
}
|
||||||
|
},
|
||||||
deleteTrack(trackId) {
|
deleteTrack(trackId) {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this track?')
|
.confirmDialog('Are you sure you want to delete this track?')
|
||||||
|
|
|
@ -61,10 +61,7 @@
|
||||||
<h5 class="text-subtitle1 q-my-none">Tracks</h5>
|
<h5 class="text-subtitle1 q-my-none">Tracks</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="col q-ml-lg">
|
<div class="col q-ml-lg">
|
||||||
<q-btn
|
<q-btn unelevated color="deep-purple" @click="openAddTrackDialog"
|
||||||
unelevated
|
|
||||||
color="deep-purple"
|
|
||||||
@click="trackDialog.show = true"
|
|
||||||
>Add new track</q-btn
|
>Add new track</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,12 +104,20 @@
|
||||||
{{ producersMap[props.row.producer].name }}
|
{{ producersMap[props.row.producer].name }}
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td class="text-right" auto-width
|
<q-td class="text-right" auto-width
|
||||||
>{{ props.row.price_msat }}</q-td
|
>{{ Math.round(props.row.price_msat / 1000) }}</q-td
|
||||||
>
|
>
|
||||||
<q-td class="text-center" auto-width
|
<q-td class="text-center" auto-width
|
||||||
>{{ props.row.download_url }}</q-td
|
>{{ props.row.download_url }}</q-td
|
||||||
>
|
>
|
||||||
<q-td auto-width>
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="openUpdateDialog(props.row.id)"
|
||||||
|
icon="edit"
|
||||||
|
color="light-blue"
|
||||||
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
dense
|
dense
|
||||||
|
@ -186,7 +191,7 @@
|
||||||
</q-select>
|
</q-select>
|
||||||
</q-form>
|
</q-form>
|
||||||
|
|
||||||
<a :href="livestream.url">
|
<a :href="'lightning:' + livestream.lnurl">
|
||||||
<q-responsive :ratio="1" class="q-mx-sm">
|
<q-responsive :ratio="1" class="q-mx-sm">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="livestream.lnurl"
|
:value="livestream.lnurl"
|
||||||
|
@ -221,6 +226,31 @@
|
||||||
|
|
||||||
<q-dialog v-model="trackDialog.show">
|
<q-dialog v-model="trackDialog.show">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-card-section
|
||||||
|
v-if="trackDialog.data.lnurl"
|
||||||
|
class="q-pa-none text-center"
|
||||||
|
>
|
||||||
|
<p class="text-subtitle1 q-my-none">
|
||||||
|
Standalone QR Code for this track
|
||||||
|
</p>
|
||||||
|
<a :href="'lightning:' + trackDialog.data.lnurl">
|
||||||
|
<q-responsive :ratio="1" class="q-mx-sm">
|
||||||
|
<qrcode
|
||||||
|
:value="trackDialog.data.lnurl"
|
||||||
|
:options="{width: 800}"
|
||||||
|
class="rounded-borders"
|
||||||
|
></qrcode>
|
||||||
|
</q-responsive>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
@click="copyText(trackDialog.data.lnurl)"
|
||||||
|
class="text-center q-mb-md"
|
||||||
|
>Copy LNURL-pay code</q-btn
|
||||||
|
>
|
||||||
|
</q-card-section>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-form @submit="addTrack" class="q-gutter-md">
|
<q-form @submit="addTrack" class="q-gutter-md">
|
||||||
<q-select
|
<q-select
|
||||||
|
@ -269,8 +299,10 @@
|
||||||
color="deep-purple"
|
color="deep-purple"
|
||||||
:disable="disabledAddTrackButton()"
|
:disable="disabledAddTrackButton()"
|
||||||
type="submit"
|
type="submit"
|
||||||
>Add track</q-btn
|
|
||||||
>
|
>
|
||||||
|
<span v-if="trackDialog.data.id">Update track</span>
|
||||||
|
<span v-else>Add track</span>
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="col q-ml-lg">
|
<div class="col q-ml-lg">
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
|
|
@ -9,9 +9,11 @@ from .crud import (
|
||||||
get_or_create_livestream_by_wallet,
|
get_or_create_livestream_by_wallet,
|
||||||
add_track,
|
add_track,
|
||||||
get_tracks,
|
get_tracks,
|
||||||
|
update_track,
|
||||||
|
add_producer,
|
||||||
get_producers,
|
get_producers,
|
||||||
update_livestream_fee,
|
|
||||||
update_current_track,
|
update_current_track,
|
||||||
|
update_livestream_fee,
|
||||||
delete_track_from_livestream,
|
delete_track_from_livestream,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,7 +32,10 @@ async def api_livestream_from_wallet():
|
||||||
**ls._asdict(),
|
**ls._asdict(),
|
||||||
**{
|
**{
|
||||||
"lnurl": ls.lnurl,
|
"lnurl": ls.lnurl,
|
||||||
"tracks": [track._asdict() for track in tracks],
|
"tracks": [
|
||||||
|
dict(lnurl=track.lnurl, **track._asdict())
|
||||||
|
for track in tracks
|
||||||
|
],
|
||||||
"producers": [producer._asdict() for producer in producers],
|
"producers": [producer._asdict() for producer in producers],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -72,6 +77,7 @@ async def api_update_fee(fee_pct):
|
||||||
|
|
||||||
|
|
||||||
@livestream_ext.route("/api/v1/livestream/tracks", methods=["POST"])
|
@livestream_ext.route("/api/v1/livestream/tracks", methods=["POST"])
|
||||||
|
@livestream_ext.route("/api/v1/livestream/tracks/<id>", methods=["PUT"])
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
@api_validate_post_request(
|
@api_validate_post_request(
|
||||||
schema={
|
schema={
|
||||||
|
@ -90,17 +96,35 @@ async def api_update_fee(fee_pct):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
async def api_add_track():
|
async def api_add_track(id=None):
|
||||||
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
||||||
await add_track(
|
|
||||||
ls.id,
|
if "producer_id" in g.data:
|
||||||
g.data["name"],
|
p_id = g.data["producer_id"]
|
||||||
g.data.get("download_url"),
|
elif "producer_name" in g.data:
|
||||||
g.data.get("price_msat", 0),
|
p_id = await add_producer(ls.id, g.data["producer_name"])
|
||||||
g.data.get("producer_name"),
|
else:
|
||||||
g.data.get("producer_id"),
|
raise TypeError("need either producer_id or producer_name arguments")
|
||||||
)
|
|
||||||
return "", HTTPStatus.CREATED
|
if id:
|
||||||
|
await update_track(
|
||||||
|
ls.id,
|
||||||
|
id,
|
||||||
|
g.data["name"],
|
||||||
|
g.data.get("download_url"),
|
||||||
|
g.data.get("price_msat", 0),
|
||||||
|
p_id,
|
||||||
|
)
|
||||||
|
return "", HTTPStatus.OK
|
||||||
|
else:
|
||||||
|
await add_track(
|
||||||
|
ls.id,
|
||||||
|
g.data["name"],
|
||||||
|
g.data.get("download_url"),
|
||||||
|
g.data.get("price_msat", 0),
|
||||||
|
p_id,
|
||||||
|
)
|
||||||
|
return "", HTTPStatus.CREATED
|
||||||
|
|
||||||
|
|
||||||
@livestream_ext.route("/api/v1/livestream/tracks/<track_id>", methods=["DELETE"])
|
@livestream_ext.route("/api/v1/livestream/tracks/<track_id>", methods=["DELETE"])
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<code>[<pay_link_object>, ...]</code>
|
<code>[<pay_link_object>, ...]</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.url_root }}api/v0/links -H "X-Api-Key: {{
|
>curl -X GET {{ request.url_root }}api/v1/links -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].inkey }}"
|
g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
@ -9,7 +9,9 @@ from lnbits.core.crud import (
|
||||||
get_payments,
|
get_payments,
|
||||||
get_standalone_payment,
|
get_standalone_payment,
|
||||||
delete_expired_invoices,
|
delete_expired_invoices,
|
||||||
|
get_balance_checks,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.services import redeem_lnurl_withdraw
|
||||||
|
|
||||||
main_app: Optional[QuartTrio] = None
|
main_app: Optional[QuartTrio] = None
|
||||||
|
|
||||||
|
@ -93,6 +95,14 @@ async def check_pending_payments():
|
||||||
await trio.sleep(60 * 30) # every 30 minutes
|
await trio.sleep(60 * 30) # every 30 minutes
|
||||||
|
|
||||||
|
|
||||||
|
async def perform_balance_checks():
|
||||||
|
while True:
|
||||||
|
for bc in await get_balance_checks():
|
||||||
|
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
||||||
|
|
||||||
|
await trio.sleep(60 * 60 * 6) # every 6 hours
|
||||||
|
|
||||||
|
|
||||||
async def invoice_callback_dispatcher(checking_id: str):
|
async def invoice_callback_dispatcher(checking_id: str):
|
||||||
payment = await get_standalone_payment(checking_id)
|
payment = await get_standalone_payment(checking_id)
|
||||||
if payment and payment.is_in:
|
if payment and payment.is_in:
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||||
/>
|
/>
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
{% block head_scripts %}{% endblock %}
|
{% block head_scripts %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import trio # type: ignore
|
||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
@ -116,16 +117,25 @@ class LNbitsWallet(Wallet):
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
url = f"{self.endpoint}/api/v1/payments/sse"
|
url = f"{self.endpoint}/api/v1/payments/sse"
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=None, headers=self.key) as client:
|
while True:
|
||||||
async with client.stream("GET", url) as r:
|
try:
|
||||||
async for line in r.aiter_lines():
|
async with httpx.AsyncClient(timeout=None, headers=self.key) as client:
|
||||||
if line.startswith("data:"):
|
async with client.stream("GET", url) as r:
|
||||||
try:
|
async for line in r.aiter_lines():
|
||||||
data = json.loads(line[5:])
|
if line.startswith("data:"):
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if type(data) is not list or len(data) < 9:
|
try:
|
||||||
continue
|
data = json.loads(line[5:])
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
yield data[8] # payment_hash
|
if type(data) is not dict:
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield data["payment_hash"] # payment_hash
|
||||||
|
|
||||||
|
except (OSError, httpx.ReadError, httpx.ConnectError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
|
||||||
|
await trio.sleep(5)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user