fix: lnurlp, withdraw

This commit is contained in:
Tiago vasconcelos
2021-10-05 09:19:21 +01:00
parent 6edac8ae8d
commit 81826f3c13
14 changed files with 108 additions and 85 deletions

View File

@@ -27,4 +27,3 @@ print(
- service fee: {SERVICE_FEE} - service fee: {SERVICE_FEE}
""" """
) )

View File

@@ -1,34 +1,48 @@
import asyncio
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from starlette.routing import Mount from starlette.routing import Mount
from lnbits.db import Database from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_lnurlp") db = Database("ext_lnurlp")
lnurlp_static_files = [
{
"path": "/lnurlp/static",
"app": StaticFiles(directory="lnbits/extensions/lnurlp/static"),
"name": "lnurlp_static",
}
]
lnurlp_ext: APIRouter = APIRouter( lnurlp_ext: APIRouter = APIRouter(
prefix="/lnurlp", prefix="/lnurlp",
static_folder="static", tags=["lnurlp"]
# "lnurlp", __name__, static_folder="static", template_folder="templates" # "lnurlp", __name__, static_folder="static", template_folder="templates"
) )
def lnurlp_renderer(): def lnurlp_renderer():
return template_renderer( return template_renderer(
[ [
"lnbits/extensions/lnticket/templates", "lnbits/extensions/lnurlp/templates",
] ]
) )
from .views_api import * # noqa from .views_api import * # noqa
from .views import * # noqa from .views import * # noqa
from .tasks import wait_for_paid_invoices
def lnurlp_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
@lnurlp_ext.on_event("startup")
def _do_it():
register_listeners()
# from .lnurl import * # noqa # from .lnurl import * # noqa
# from .tasks import register_listeners
# from lnbits.tasks import record_async # from lnbits.tasks import record_async

View File

@@ -2,25 +2,17 @@ from typing import List, Optional, Union
from lnbits.db import SQLITE from lnbits.db import SQLITE
from . import db from . import db
from .models import PayLink from .models import PayLink, CreatePayLinkData
async def create_pay_link( async def create_pay_link(
*, data: CreatePayLinkData,
wallet_id: str, wallet_id: str
description: str,
min: int,
max: int,
comment_chars: int = 0,
currency: Optional[str] = None,
webhook_url: Optional[str] = None,
success_text: Optional[str] = None,
success_url: Optional[str] = None,
) -> PayLink: ) -> PayLink:
returning = "" if db.type == SQLITE else "RETURNING ID" returning = "" if db.type == SQLITE else "RETURNING ID"
method = db.execute if db.type == SQLITE else db.fetchone method = db.execute if db.type == SQLITE else db.fetchone
print("CPL", wallet_id, data)
result = await (method)( result = await (method)(
f""" f"""
INSERT INTO lnurlp.pay_links ( INSERT INTO lnurlp.pay_links (
@@ -41,14 +33,14 @@ async def create_pay_link(
""", """,
( (
wallet_id, wallet_id,
description, data.description,
min, data.min,
max, data.max,
webhook_url, data.webhook_url,
success_text, data.success_text,
success_url, data.success_url,
comment_chars, data.comment_chars,
currency, data.currency,
), ),
) )
if db.type == SQLITE: if db.type == SQLITE:

View File

@@ -11,7 +11,7 @@ from . import lnurlp_ext
from .crud import increment_pay_link from .crud import increment_pay_link
@lnurlp_ext.get("/api/v1/lnurl/{link_id}", status_code=HTTPStatus.OK) @lnurlp_ext.get("/api/v1/lnurl/{link_id}", status_code=HTTPStatus.OK, name="lnurlp.api_lnurl_response")
async def api_lnurl_response(request: Request, link_id): async def api_lnurl_response(request: Request, link_id):
link = await increment_pay_link(link_id, served_meta=1) link = await increment_pay_link(link_id, served_meta=1)
if not link: if not link:

View File

@@ -1,12 +1,23 @@
import json import json
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
from quart import url_for from starlette.requests import Request
from fastapi.param_functions import Query
from typing import Optional, Dict from typing import Optional, Dict
from lnbits.lnurl import encode as lnurl_encode # type: ignore from lnbits.lnurl import encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore
from sqlite3 import Row from sqlite3 import Row
from pydantic import BaseModel from pydantic import BaseModel
class CreatePayLinkData(BaseModel):
description: str
min: int = Query(0.01, ge=0.01)
max: int = Query(0.01, ge=0.01)
currency: str = Query(None)
comment_chars: int = Query(0, ge=0, lt=800)
webhook_url: str = Query(None)
success_text: str = Query(None)
success_url: str = Query(None)
class PayLink(BaseModel): class PayLink(BaseModel):
id: int id: int
wallet: str wallet: str
@@ -14,10 +25,10 @@ class PayLink(BaseModel):
min: int min: int
served_meta: int served_meta: int
served_pr: int served_pr: int
webhook_url: str webhook_url: Optional[str]
success_text: str success_text: Optional[str]
success_url: str success_url: Optional[str]
currency: str currency: Optional[str]
comment_chars: int comment_chars: int
max: int max: int
@@ -28,7 +39,8 @@ class PayLink(BaseModel):
@property @property
def lnurl(self) -> str: def lnurl(self) -> str:
url = url_for("lnurlp.api_lnurl_response", link_id=self.id, _external=True) r = Request
url = r.url_for("lnurlp.api_lnurl_response", link_id=self.id, _external=True)
return lnurl_encode(url) return lnurl_encode(url)
@property @property

View File

@@ -1,4 +1,4 @@
import trio import asyncio
import json import json
import httpx import httpx
@@ -9,17 +9,14 @@ from lnbits.tasks import register_invoice_listener
from .crud import get_pay_link from .crud import get_pay_link
async def register_listeners(): async def wait_for_paid_invoices():
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_paid_chan_send) register_invoice_listener(invoice_queue)
await wait_for_paid_invoices(invoice_paid_chan_recv)
while True:
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): payment = await invoice_queue.get()
async for payment in invoice_paid_chan:
await on_invoice_paid(payment) await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if "lnurlp" != payment.extra.get("tag"): if "lnurlp" != payment.extra.get("tag"):
# not an lnurlp invoice # not an lnurlp invoice

View File

@@ -18,7 +18,7 @@
<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/v1/links -H "X-Api-Key: {{ >curl -X GET {{ request.url_root }}api/v1/links -H "X-Api-Key: {{
g.user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@@ -27,7 +27,8 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<code <code
><span class="text-blue">GET</span> /lnurlp/api/v1/links/&lt;pay_id&gt;</code ><span class="text-blue">GET</span>
/lnurlp/api/v1/links/&lt;pay_id&gt;</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
@@ -39,7 +40,7 @@
<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/v1/links/&lt;pay_id&gt; -H >curl -X GET {{ request.url_root }}api/v1/links/&lt;pay_id&gt; -H
"X-Api-Key: {{ g.user.wallets[0].inkey }}" "X-Api-Key: {{ user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@@ -56,7 +57,11 @@
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"description": &lt;string&gt; "amount": &lt;integer&gt; "max": &lt;integer&gt; "min": &lt;integer&gt; "comment_chars": &lt;integer&gt;}</code> <code
>{"description": &lt;string&gt; "amount": &lt;integer&gt; "max":
&lt;integer&gt; "min": &lt;integer&gt; "comment_chars":
&lt;integer&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none"> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json) Returns 201 CREATED (application/json)
</h5> </h5>
@@ -64,8 +69,10 @@
<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 POST {{ request.url_root }}api/v1/links -d '{"description": >curl -X POST {{ request.url_root }}api/v1/links -d '{"description":
&lt;string&gt;, "amount": &lt;integer&gt;, "max": &lt;integer&gt;, "min": &lt;integer&gt;, "comment_chars": &lt;integer&gt;}' -H "Content-type: &lt;string&gt;, "amount": &lt;integer&gt;, "max": &lt;integer&gt;,
application/json" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" "min": &lt;integer&gt;, "comment_chars": &lt;integer&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@@ -95,7 +102,7 @@
>curl -X PUT {{ request.url_root }}api/v1/links/&lt;pay_id&gt; -d >curl -X PUT {{ request.url_root }}api/v1/links/&lt;pay_id&gt; -d
'{"description": &lt;string&gt;, "amount": &lt;integer&gt;}' -H '{"description": &lt;string&gt;, "amount": &lt;integer&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{ "Content-type: application/json" -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}" user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@@ -120,7 +127,7 @@
<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 DELETE {{ request.url_root }}api/v1/links/&lt;pay_id&gt; -H >curl -X DELETE {{ request.url_root }}api/v1/links/&lt;pay_id&gt; -H
"X-Api-Key: {{ g.user.wallets[0].adminkey }}" "X-Api-Key: {{ user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>

View File

@@ -1,6 +1,6 @@
from http import HTTPStatus from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists
from . import lnurlp_ext, lnurlp_renderer from . import lnurlp_ext, lnurlp_renderer
from .crud import get_pay_link from .crud import get_pay_link
@@ -15,7 +15,7 @@ from lnbits.core.models import User
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@lnurlp_ext.get("/", response_class=HTMLResponse) @lnurlp_ext.get("/", response_class=HTMLResponse)
@validate_uuids(["usr"], required=True) # @validate_uuids(["usr"], required=True)
# @check_user_exists() # @check_user_exists()
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):
return lnurlp_renderer().TemplateResponse("lnurlp/index.html", {"request": request, "user": user.dict()}) return lnurlp_renderer().TemplateResponse("lnurlp/index.html", {"request": request, "user": user.dict()})

View File

@@ -10,8 +10,9 @@ from starlette.requests import Request
from starlette.responses import HTMLResponse, JSONResponse # type: ignore from starlette.responses import HTMLResponse, JSONResponse # type: ignore
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis
from .models import CreatePayLinkData
from . import lnurlp_ext from . import lnurlp_ext
from .crud import ( from .crud import (
@@ -75,20 +76,11 @@ async def api_link_retrieve(link_id, wallet: WalletTypeInfo = Depends(get_key_ty
return {**link._asdict(), **{"lnurl": link.lnurl}} return {**link._asdict(), **{"lnurl": link.lnurl}}
class CreateData(BaseModel):
description: str
min: int = Query(0.01, ge=0.01)
max: int = Query(0.01, ge=0.01)
currency: Optional[str]
comment_chars: int = Query(0, ge=0, lt=800)
webhook_url: Optional[str]
success_text: Optional[str]
success_url: Optional[str]
@lnurlp_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED) @lnurlp_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
@lnurlp_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) @lnurlp_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
# @api_check_wallet_key("invoice") # @api_check_wallet_key("invoice")
async def api_link_create_or_update(data: CreateData, link_id=None, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_link_create_or_update(data: CreatePayLinkData, link_id=None, wallet: WalletTypeInfo = Depends(get_key_type)):
if data.min > data.max: if data.min > data.max:
raise HTTPException( raise HTTPException(
detail="Min is greater than max.", detail="Min is greater than max.",
@@ -128,18 +120,18 @@ async def api_link_create_or_update(data: CreateData, link_id=None, wallet: Wall
# HTTPStatus.NOT_FOUND, # HTTPStatus.NOT_FOUND,
# ) # )
if link.wallet != g.wallet.id: if link.wallet != wallet.wallet.id:
raise HTTPException( raise HTTPException(
detail="Not your pay link.", detail="Not your pay link.",
status_code=HTTPStatus.FORBIDDEN status_code=HTTPStatus.FORBIDDEN
) )
# return {"message": "Not your pay link."}, HTTPStatus.FORBIDDEN # return {"message": "Not your pay link."}, HTTPStatus.FORBIDDEN
link = await update_pay_link(link_id, **data) link = await update_pay_link(link_id, data)
else: else:
link = await create_pay_link(wallet_id=wallet.wallet.id, **data) link = await create_pay_link(data, wallet_id=wallet.wallet.id)
print("LINK", link)
return {**link._asdict(), **{"lnurl": link.lnurl}} return {**link.dict(), "lnurl": link.lnurl}
@lnurlp_ext.delete("/api/v1/links/{link_id}") @lnurlp_ext.delete("/api/v1/links/{link_id}")

View File

@@ -1,13 +1,23 @@
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_withdraw") db = Database("ext_withdraw")
withdraw_static_files = [
{
"path": "/withdraw/static",
"app": StaticFiles(directory="lnbits/extensions/withdraw/static"),
"name": "withdraw_static",
}
]
withdraw_ext: APIRouter = APIRouter( withdraw_ext: APIRouter = APIRouter(
prefix="/withdraw", prefix="/withdraw",
static_folder="static" tags=["withdraw"],
# "withdraw", __name__, static_folder="static", template_folder="templates" # "withdraw", __name__, static_folder="static", template_folder="templates"
) )
@@ -23,6 +33,7 @@ from .views_api import * # noqa
from .views import * # noqa from .views import * # noqa
from .lnurl import * # noqa from .lnurl import * # noqa
@withdraw_ext.on_event("startup")
def _do_it(): # @withdraw_ext.on_event("startup")
register_listeners() # def _do_it():
# register_listeners()

View File

@@ -1,7 +1,6 @@
import shortuuid # type: ignore import shortuuid # type: ignore
from http import HTTPStatus from http import HTTPStatus
from datetime import datetime from datetime import datetime
from quart import jsonify, request
from lnbits.core.services import pay_invoice from lnbits.core.services import pay_invoice

View File

@@ -1,4 +1,4 @@
from quart import url_for from fastapi import Request
from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode # type: ignore from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode # type: ignore
from sqlite3 import Row from sqlite3 import Row
from pydantic import BaseModel from pydantic import BaseModel
@@ -33,19 +33,19 @@ class WithdrawLink(BaseModel):
return self.used >= self.uses return self.used >= self.uses
@property @property
def lnurl(self) -> Lnurl: def lnurl(self, req: Request) -> Lnurl:
if self.is_unique: if self.is_unique:
usescssv = self.usescsv.split(",") usescssv = self.usescsv.split(",")
tohash = self.id + self.unique_hash + usescssv[self.number] tohash = self.id + self.unique_hash + usescssv[self.number]
multihash = shortuuid.uuid(name=tohash) multihash = shortuuid.uuid(name=tohash)
url = url_for( url = req.url_for(
"withdraw.api_lnurl_multi_response", "withdraw.api_lnurl_multi_response",
unique_hash=self.unique_hash, unique_hash=self.unique_hash,
id_unique_hash=multihash, id_unique_hash=multihash,
_external=True, _external=True,
) )
else: else:
url = url_for( url = req.url_for(
"withdraw.api_lnurl_response", "withdraw.api_lnurl_response",
unique_hash=self.unique_hash, unique_hash=self.unique_hash,
_external=True, _external=True,
@@ -55,7 +55,7 @@ class WithdrawLink(BaseModel):
@property @property
def lnurl_response(self) -> LnurlWithdrawResponse: def lnurl_response(self) -> LnurlWithdrawResponse:
url = url_for( url = req.url_for(
"withdraw.api_lnurl_callback", unique_hash=self.unique_hash, _external=True "withdraw.api_lnurl_callback", unique_hash=self.unique_hash, _external=True
) )
return LnurlWithdrawResponse( return LnurlWithdrawResponse(

View File

@@ -1,7 +1,7 @@
from http import HTTPStatus from http import HTTPStatus
import pyqrcode import pyqrcode
from io import BytesIO from io import BytesIO
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists
from . import withdraw_ext, withdraw_renderer from . import withdraw_ext, withdraw_renderer
from .crud import get_withdraw_link, chunks from .crud import get_withdraw_link, chunks
@@ -16,7 +16,7 @@ from lnbits.core.models import User
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@withdraw_ext.get("/", response_class=HTMLResponse) @withdraw_ext.get("/", response_class=HTMLResponse)
@validate_uuids(["usr"], required=True) # @validate_uuids(["usr"], required=True)
# @check_user_exists() # @check_user_exists()
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):
return withdraw_renderer().TemplateResponse("withdraw/index.html", {"request":request,"user": user.dict()}) return withdraw_renderer().TemplateResponse("withdraw/index.html", {"request":request,"user": user.dict()})
@@ -36,7 +36,7 @@ async def display(request: Request, link_id):
@withdraw_ext.get("/img/{link_id}", response_class=HTMLResponse) @withdraw_ext.get("/img/{link_id}", response_class=HTMLResponse)
async def img(request: Request, link_id, response: Response): async def img(request: Request, link_id):
link = await get_withdraw_link(link_id, 0) link = await get_withdraw_link(link_id, 0)
if not link: if not link:
raise HTTPException( raise HTTPException(

View File

@@ -9,7 +9,7 @@ from starlette.requests import Request
from starlette.responses import HTMLResponse, JSONResponse # type: ignore from starlette.responses import HTMLResponse, JSONResponse # type: ignore
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import WalletTypeInfo, get_key_type
# from fastapi import FastAPI, Query, Response # from fastapi import FastAPI, Query, Response
@@ -83,8 +83,8 @@ class CreateData(BaseModel):
@withdraw_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED) @withdraw_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
@withdraw_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) @withdraw_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
@api_check_wallet_key("admin") # @api_check_wallet_key("admin")
async def api_link_create_or_update(data: CreateData, link_id: str = None, response: Response): async def api_link_create_or_update(data: CreateData, link_id: str = None):
if data.max_withdrawable < data.min_withdrawable: if data.max_withdrawable < data.min_withdrawable:
raise HTTPException( raise HTTPException(
detail="`max_withdrawable` needs to be at least `min_withdrawable`.", detail="`max_withdrawable` needs to be at least `min_withdrawable`.",