mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-03-03 17:37:06 +01:00
lnurl balanceCheck and balanceNotify.
This commit is contained in:
parent
f08d86c6df
commit
efd9c6917f
9 changed files with 303 additions and 43 deletions
|
@ -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"])
|
||||||
|
|
|
@ -4,8 +4,8 @@ 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 +128,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,24 +179,38 @@ 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,
|
||||||
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=res["maxWithdrawable"],
|
||||||
memo=memo or res.default_description or "",
|
memo=memo or res["defaultDescription"] or "",
|
||||||
extra={"tag": "lnurlwallet"},
|
extra=extra,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
)
|
)
|
||||||
|
|
||||||
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={
|
||||||
**res.callback.query_params,
|
"k1": res["k1"],
|
||||||
**{"k1": res.k1, "pr": payment_request},
|
"pr": payment_request,
|
||||||
|
"balanceNotify": url_for(
|
||||||
|
"core.lnurl_balance_notify",
|
||||||
|
service=urlparse(lnurl_request).netloc,
|
||||||
|
wal=g.wallet.id,
|
||||||
|
_external=True,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -286,3 +299,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:
|
||||||
|
|
|
@ -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,4 @@
|
||||||
import trio # type: ignore
|
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 +11,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 +22,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 +108,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 +183,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,7 +201,7 @@ 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.",
|
||||||
)
|
)
|
||||||
await trio.sleep(3)
|
await trio.sleep(3)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Add table
Reference in a new issue