inbetween withdraw commit

This commit is contained in:
dni ⚡ 2023-01-04 16:29:47 +01:00
parent cc7c3807dd
commit 2b65682960
6 changed files with 98 additions and 133 deletions

View file

@ -8,9 +8,10 @@ from .models import CreateWithdrawData, HashCheck, WithdrawLink
async def create_withdraw_link( async def create_withdraw_link(
data: CreateWithdrawData, wallet_id: str, usescsv: str data: CreateWithdrawData, wallet_id: str
) -> WithdrawLink: ) -> WithdrawLink:
link_id = urlsafe_short_hash() link_id = urlsafe_short_hash()
available_links = ",".join([str(i) for i in range(data.uses)])
await db.execute( await db.execute(
""" """
INSERT INTO withdraw.withdraw_link ( INSERT INTO withdraw.withdraw_link (
@ -45,7 +46,7 @@ async def create_withdraw_link(
urlsafe_short_hash(), urlsafe_short_hash(),
urlsafe_short_hash(), urlsafe_short_hash(),
int(datetime.now().timestamp()) + data.wait_time, int(datetime.now().timestamp()) + data.wait_time,
usescsv, available_links,
data.webhook_url, data.webhook_url,
data.webhook_headers, data.webhook_headers,
data.webhook_body, data.webhook_body,
@ -94,6 +95,14 @@ async def get_withdraw_links(wallet_ids: Union[str, List[str]]) -> List[Withdraw
return [WithdrawLink(**row) for row in rows] return [WithdrawLink(**row) for row in rows]
async def increment_withdraw_link(link: WithdrawLink) -> None:
await update_withdraw_link(
link.id,
used=link.used + 1,
open_time=link.wait_time + int(datetime.now().timestamp()),
)
async def update_withdraw_link(link_id: str, **kwargs) -> Optional[WithdrawLink]: async def update_withdraw_link(link_id: str, **kwargs) -> Optional[WithdrawLink]:
if "is_unique" in kwargs: if "is_unique" in kwargs:
kwargs["is_unique"] = int(kwargs["is_unique"]) kwargs["is_unique"] = int(kwargs["is_unique"])
@ -129,10 +138,11 @@ async def create_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
(the_hash, lnurl_id), (the_hash, lnurl_id),
) )
hashCheck = await get_hash_check(the_hash, lnurl_id) hashCheck = await get_hash_check(the_hash, lnurl_id)
assert hashCheck
return hashCheck return hashCheck
async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]: async def get_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
rowid = await db.fetchone( rowid = await db.fetchone(
"SELECT * FROM withdraw.hash_check WHERE id = ?", (the_hash,) "SELECT * FROM withdraw.hash_check WHERE id = ?", (the_hash,)
) )
@ -141,10 +151,10 @@ async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]:
) )
if not rowlnurl: if not rowlnurl:
await create_hash_check(the_hash, lnurl_id) await create_hash_check(the_hash, lnurl_id)
return {"lnurl": True, "hash": False} return HashCheck(lnurl=True, hash=False)
else: else:
if not rowid: if not rowid:
await create_hash_check(the_hash, lnurl_id) await create_hash_check(the_hash, lnurl_id)
return {"lnurl": True, "hash": False} return HashCheck(lnurl=True, hash=False)
else: else:
return {"lnurl": True, "hash": True} return HashCheck(lnurl=True, hash=True)

View file

@ -1,10 +1,10 @@
import json import json
import traceback
from datetime import datetime from datetime import datetime
from http import HTTPStatus from http import HTTPStatus
import httpx import httpx
import shortuuid # type: ignore import shortuuid
from fastapi import HTTPException from fastapi import HTTPException
from fastapi.param_functions import Query from fastapi.param_functions import Query
from loguru import logger from loguru import logger
@ -15,9 +15,8 @@ from lnbits.core.crud import update_payment_extra
from lnbits.core.services import pay_invoice from lnbits.core.services import pay_invoice
from . import withdraw_ext from . import withdraw_ext
from .crud import get_withdraw_link_by_hash, update_withdraw_link from .crud import get_withdraw_link_by_hash, increment_withdraw_link
from .models import WithdrawLink
# FOR LNURLs WHICH ARE NOT UNIQUE
@withdraw_ext.get( @withdraw_ext.get(
@ -53,9 +52,6 @@ async def api_lnurl_response(request: Request, unique_hash):
return json.dumps(withdrawResponse) return json.dumps(withdrawResponse)
# CALLBACK
@withdraw_ext.get( @withdraw_ext.get(
"/api/v1/lnurl/cb/{unique_hash}", "/api/v1/lnurl/cb/{unique_hash}",
name="withdraw.api_lnurl_callback", name="withdraw.api_lnurl_callback",
@ -99,102 +95,75 @@ async def api_lnurl_callback(
detail=f"wait link open_time {link.open_time - now} seconds.", detail=f"wait link open_time {link.open_time - now} seconds.",
) )
usescsv = "" if id_unique_hash:
if check_unique_link(link, id_unique_hash):
for x in range(1, link.uses - link.used): # remove it from usescsv list
usecv = link.usescsv.split(",") pass
usescsv += "," + str(usecv[x]) else:
usecsvback = usescsv
found = False
if id_unique_hash is not None:
useslist = link.usescsv.split(",")
for ind, x in enumerate(useslist):
tohash = link.id + link.unique_hash + str(x)
if id_unique_hash == shortuuid.uuid(name=tohash):
found = True
useslist.pop(ind)
usescsv = ",".join(useslist)
if not found:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found." status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found."
) )
else:
usescsv = usescsv[1:]
changesback = {
"open_time": link.wait_time,
"used": link.used,
"usescsv": usecsvback,
}
try: try:
changes = {
"open_time": link.wait_time + now,
"used": link.used + 1,
"usescsv": usescsv,
}
await update_withdraw_link(link.id, **changes)
payment_request = pr
payment_hash = await pay_invoice( payment_hash = await pay_invoice(
wallet_id=link.wallet, wallet_id=link.wallet,
payment_request=payment_request, payment_request=pr,
max_sat=link.max_withdrawable, max_sat=link.max_withdrawable,
extra={"tag": "withdraw"}, extra={"tag": "withdraw"},
) )
await increment_withdraw_link(link)
if link.webhook_url: if link.webhook_url:
async with httpx.AsyncClient() as client: await dispatch_webhook(link, payment_hash, pr)
try:
kwargs = {
"json": {
"payment_hash": payment_hash,
"payment_request": payment_request,
"lnurlw": link.id,
},
"timeout": 40,
}
if link.webhook_body:
kwargs["json"]["body"] = json.loads(link.webhook_body)
if link.webhook_headers:
kwargs["headers"] = json.loads(link.webhook_headers)
r: httpx.Response = await client.post(link.webhook_url, **kwargs)
await update_payment_extra(
payment_hash=payment_hash,
extra={
"wh_success": r.is_success,
"wh_message": r.reason_phrase,
"wh_response": r.text,
},
outgoing=True,
)
except Exception as exc:
# webhook fails shouldn't cause the lnurlw to fail since invoice is already paid
logger.error(
"Caught exception when dispatching webhook url: " + str(exc)
)
await update_payment_extra(
payment_hash=payment_hash,
extra={"wh_success": False, "wh_message": str(exc)},
outgoing=True,
)
return {"status": "OK"} return {"status": "OK"}
except Exception as e: except Exception as e:
await update_withdraw_link(link.id, **changesback)
logger.error(traceback.format_exc())
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(e)}" status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(e)}"
) )
def check_unique_link(link: WithdrawLink, unique_hash: str) -> bool:
unique_links = link.usescsv.split(",")
return any(unique_hash == shortuuid.uuid(name=link.id + link.unique_hash + x.strip()) for x in unique_links)
async def dispatch_webhook(
link: WithdrawLink, payment_hash: str, payment_request: str
) -> None:
async with httpx.AsyncClient() as client:
try:
r: httpx.Response = await client.post(
link.webhook_url,
json={
"payment_hash": payment_hash,
"payment_request": payment_request,
"lnurlw": link.id,
"body": json.loads(link.webhook_body) if link.webhook_body else "",
},
headers=json.loads(link.webhook_headers)
if link.webhook_headers
else None,
timeout=40,
)
await update_payment_extra(
payment_hash=payment_hash,
extra={
"wh_success": r.is_success,
"wh_message": r.reason_phrase,
"wh_response": r.text,
},
outgoing=True,
)
except Exception as exc:
# webhook fails shouldn't cause the lnurlw to fail since invoice is already paid
logger.error("Caught exception when dispatching webhook url: " + str(exc))
await update_payment_extra(
payment_hash=payment_hash,
extra={"wh_success": False, "wh_message": str(exc)},
outgoing=True,
)
# FOR LNURLs WHICH ARE UNIQUE # FOR LNURLs WHICH ARE UNIQUE
@withdraw_ext.get( @withdraw_ext.get(
"/api/v1/lnurl/{unique_hash}/{id_unique_hash}", "/api/v1/lnurl/{unique_hash}/{id_unique_hash}",
response_class=HTMLResponse, response_class=HTMLResponse,

View file

@ -1,9 +1,8 @@
from sqlite3 import Row import shortuuid
from fastapi import Query
import shortuuid # type: ignore
from fastapi.param_functions import Query
from lnurl import Lnurl, LnurlWithdrawResponse from lnurl import Lnurl, LnurlWithdrawResponse
from lnurl import encode as lnurl_encode # type: ignore from lnurl.models import ClearnetUrl, MilliSatoshi
from lnurl import encode as lnurl_encode
from pydantic import BaseModel from pydantic import BaseModel
from starlette.requests import Request from starlette.requests import Request
@ -67,18 +66,14 @@ class WithdrawLink(BaseModel):
name="withdraw.api_lnurl_callback", unique_hash=self.unique_hash name="withdraw.api_lnurl_callback", unique_hash=self.unique_hash
) )
return LnurlWithdrawResponse( return LnurlWithdrawResponse(
callback=url, callback=ClearnetUrl(url, scheme="https"),
k1=self.k1, k1=self.k1,
min_withdrawable=self.min_withdrawable * 1000, minWithdrawable=MilliSatoshi(self.min_withdrawable * 1000),
max_withdrawable=self.max_withdrawable * 1000, maxWithdrawable=MilliSatoshi(self.max_withdrawable * 1000),
default_description=self.title, defaultDescription=self.title,
) )
class HashCheck(BaseModel): class HashCheck(BaseModel):
id: str hash: bool
lnurl_id: str lnurl: bool
@classmethod
def from_row(cls, row: Row) -> "Hash":
return cls(**dict(row))

View file

@ -2,8 +2,7 @@ from http import HTTPStatus
from io import BytesIO from io import BytesIO
import pyqrcode import pyqrcode
from fastapi import Request from fastapi import Request, Depends
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse, StreamingResponse from starlette.responses import HTMLResponse, StreamingResponse

View file

@ -1,8 +1,9 @@
from http import HTTPStatus from http import HTTPStatus
from fastapi.param_functions import Query from typing import Optional
from fastapi.params import Depends
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore from fastapi import Query, Depends
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
@ -30,7 +31,8 @@ async def api_links(
wallet_ids = [wallet.wallet.id] wallet_ids = [wallet.wallet.id]
if all_wallets: if all_wallets:
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else []
try: try:
return [ return [
@ -47,7 +49,7 @@ async def api_links(
@withdraw_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) @withdraw_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
async def api_link_retrieve( async def api_link_retrieve(
link_id, request: Request, wallet: WalletTypeInfo = Depends(get_key_type) link_id: str, request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
link = await get_withdraw_link(link_id, 0) link = await get_withdraw_link(link_id, 0)
@ -68,7 +70,7 @@ async def api_link_retrieve(
async def api_link_create_or_update( async def api_link_create_or_update(
req: Request, req: Request,
data: CreateWithdrawData, data: CreateWithdrawData,
link_id: str = None, link_id: Optional[str] = Query(),
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(require_admin_key),
): ):
if data.uses > 250: if data.uses > 250:
@ -85,14 +87,6 @@ async def api_link_create_or_update(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
) )
usescsv = ""
for i in range(data.uses):
if data.is_unique:
usescsv += "," + str(i + 1)
else:
usescsv += "," + str(1)
usescsv = usescsv[1:]
if link_id: if link_id:
link = await get_withdraw_link(link_id, 0) link = await get_withdraw_link(link_id, 0)
if not link: if not link:
@ -103,13 +97,10 @@ async def api_link_create_or_update(
raise HTTPException( raise HTTPException(
detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN
) )
link = await update_withdraw_link( link = await update_withdraw_link(link_id, **data.dict())
link_id, **data.dict(), usescsv=usescsv, used=0
)
else: else:
link = await create_withdraw_link( link = await create_withdraw_link(wallet_id=wallet.wallet.id, data=data)
wallet_id=wallet.wallet.id, data=data, usescsv=usescsv assert link
)
return {**link.dict(), **{"lnurl": link.lnurl(req)}} return {**link.dict(), **{"lnurl": link.lnurl(req)}}
@ -131,9 +122,11 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admi
return {"success": True} return {"success": True}
@withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}", status_code=HTTPStatus.OK) @withdraw_ext.get(
async def api_hash_retrieve( "/api/v1/links/{the_hash}/{lnurl_id}",
the_hash, lnurl_id, wallet: WalletTypeInfo = Depends(get_key_type) status_code=HTTPStatus.OK,
): dependencies=[Depends(get_key_type)],
)
async def api_hash_retrieve(the_hash, lnurl_id):
hashCheck = await get_hash_check(the_hash, lnurl_id) hashCheck = await get_hash_check(the_hash, lnurl_id)
return hashCheck return hashCheck

View file

@ -104,7 +104,6 @@ exclude = """(?x)(
| ^lnbits/extensions/streamalerts. | ^lnbits/extensions/streamalerts.
| ^lnbits/extensions/tpos. | ^lnbits/extensions/tpos.
| ^lnbits/extensions/watchonly. | ^lnbits/extensions/watchonly.
| ^lnbits/extensions/withdraw.
| ^lnbits/wallets/lnd_grpc_files. | ^lnbits/wallets/lnd_grpc_files.
)""" )"""