Merge branch 'FastAPI' into lnurlpayout

This commit is contained in:
benarc 2021-12-02 11:47:00 +00:00
commit ccc52dc585
15 changed files with 75 additions and 52 deletions

View File

@ -172,6 +172,8 @@ async def pay_invoice(
) )
await delete_payment(temp_id, conn=conn) await delete_payment(temp_id, conn=conn)
else: else:
async with db.connect() as conn:
await delete_payment(temp_id, conn=conn)
raise PaymentFailure( raise PaymentFailure(
payment.error_message payment.error_message
or "Payment failed, but backend didn't give us an error message." or "Payment failed, but backend didn't give us an error message."

View File

@ -72,7 +72,7 @@ async def api_update_wallet(
@core_app.get("/api/v1/payments") @core_app.get("/api/v1/payments")
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)): async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True) await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
pendingPayments = await get_payments(wallet_id=wallet.wallet.id, pending=True) pendingPayments = await get_payments(wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True)
for payment in pendingPayments: for payment in pendingPayments:
await check_invoice_status( await check_invoice_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
@ -193,7 +193,8 @@ async def api_payments_create(
invoiceData: CreateInvoiceData = Body(...), invoiceData: CreateInvoiceData = Body(...),
): ):
if wallet.wallet_type < 0 or wallet.wallet_type > 2: if wallet.wallet_type < 0 or wallet.wallet_type > 2:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid") raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
if invoiceData.out is True and wallet.wallet_type == 0: if invoiceData.out is True and wallet.wallet_type == 0:
if not invoiceData.bolt11: if not invoiceData.bolt11:
@ -204,7 +205,8 @@ async def api_payments_create(
return await api_payments_pay_invoice( return await api_payments_pay_invoice(
invoiceData.bolt11, wallet.wallet invoiceData.bolt11, wallet.wallet
) # admin key ) # admin key
return await api_payments_create_invoice(invoiceData, wallet.wallet) # invoice key # invoice key
return await api_payments_create_invoice(invoiceData, wallet.wallet)
class CreateLNURLData(BaseModel): class CreateLNURLData(BaseModel):
@ -372,14 +374,16 @@ async def api_lnurlscan(code: str):
params.update(callback=url) # with k1 already in it params.update(callback=url) # with k1 already in it
lnurlauth_key = g().wallet.lnurlauth_key(domain) lnurlauth_key = g().wallet.lnurlauth_key(domain)
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex()) params.update(
pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else: else:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.get(url, timeout=5) r = await client.get(url, timeout=5)
if r.is_error: if r.is_error:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE, status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={"domain": domain, "message": "failed to get parameters"}, detail={"domain": domain,
"message": "failed to get parameters"},
) )
try: try:
@ -409,7 +413,8 @@ async def api_lnurlscan(code: str):
if tag == "withdrawRequest": if tag == "withdrawRequest":
params.update(kind="withdraw") params.update(kind="withdraw")
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"]) params.update(fixed=data["minWithdrawable"]
== data["maxWithdrawable"])
# callback with k1 already in it # callback with k1 already in it
parsed_callback: ParseResult = urlparse(data["callback"]) parsed_callback: ParseResult = urlparse(data["callback"])

View File

@ -5,6 +5,7 @@ from urllib.parse import urlparse
from fastapi import HTTPException from fastapi import HTTPException
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse
from lnbits import bolt11 from lnbits import bolt11

View File

@ -176,7 +176,7 @@ async def purge_addresses(domain_id: str):
now = datetime.now().timestamp() now = datetime.now().timestamp()
for row in rows: for row in rows:
r = Addresses(**row)._asdict() r = Addresses(**row).dict()
start = datetime.fromtimestamp(r["time"]) start = datetime.fromtimestamp(r["time"])
paid = r["paid"] paid = r["paid"]

View File

@ -1,4 +1,5 @@
import hashlib import hashlib
import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
import httpx import httpx
@ -9,6 +10,7 @@ from lnurl import ( # type: ignore
LnurlPayResponse, LnurlPayResponse,
) )
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse
from . import lnaddress_ext from . import lnaddress_ext
from .crud import get_address, get_address_by_username, get_domain from .crud import get_address, get_address_by_username, get_domain
@ -28,20 +30,22 @@ async def lnurl_response(username: str, domain: str, request: Request):
if now > expiration: if now > expiration:
return LnurlErrorResponse(reason="Address has expired.").dict() return LnurlErrorResponse(reason="Address has expired.").dict()
resp = LnurlPayResponse( resp = {
callback=request.url_for("lnaddress.lnurl_callback", address_id=address.id), "tag": "payRequest",
min_sendable=1000, "callback": request.url_for("lnaddress.lnurl_callback", address_id=address.id),
max_sendable=1000000000, "metadata": await address.lnurlpay_metadata(domain=domain),
metadata=await address.lnurlpay_metadata(), "minSendable": 1000,
) "maxSendable": 1000000000,
}
return resp.dict() print("RESP", resp)
return resp
@lnaddress_ext.get("/lnurl/cb/{address_id}", name="lnaddress.lnurl_callback") @lnaddress_ext.get("/lnurl/cb/{address_id}", name="lnaddress.lnurl_callback")
async def lnurl_callback(address_id, amount: int = Query(...)): async def lnurl_callback(address_id, amount: int = Query(...)):
print("PING")
address = await get_address(address_id) address = await get_address(address_id)
if not address: if not address:
return LnurlErrorResponse(reason=f"Address not found").dict() return LnurlErrorResponse(reason=f"Address not found").dict()
@ -67,7 +71,7 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
"out": False, "out": False,
"amount": int(amount_received / 1000), "amount": int(amount_received / 1000),
"description_hash": hashlib.sha256( "description_hash": hashlib.sha256(
(await address.lnurlpay_metadata()).encode("utf-8") (await address.lnurlpay_metadata(domain=domain.domain)).encode("utf-8")
).hexdigest(), ).hexdigest(),
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"}, "extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
}, },
@ -78,6 +82,7 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
except AssertionError as e: except AssertionError as e:
return LnurlErrorResponse(reason="ERROR") return LnurlErrorResponse(reason="ERROR")
resp = LnurlPayActionResponse(pr=r["payment_request"], routes=[]) # resp = LnurlPayActionResponse(pr=r["payment_request"], routes=[])
resp = {"pr": r["payment_request"], "routes": []}
return resp.dict() return resp

View File

@ -3,7 +3,7 @@ from typing import Optional
from fastapi.params import Query from fastapi.params import Query
from lnurl.types import LnurlPayMetadata from lnurl.types import LnurlPayMetadata
from pydantic.main import BaseModel # type: ignore from pydantic.main import BaseModel
class CreateDomain(BaseModel): class CreateDomain(BaseModel):
@ -49,8 +49,9 @@ class Addresses(BaseModel):
paid: bool paid: bool
time: int time: int
async def lnurlpay_metadata(self) -> LnurlPayMetadata: async def lnurlpay_metadata(self, domain) -> LnurlPayMetadata:
text = f"Payment to {self.username}" text = f"Payment to {self.username}"
metadata = [["text/plain", text]] identifier = f"{self.username}@{domain}"
metadata = [["text/plain", text], ["text/identifier", identifier]]
return LnurlPayMetadata(json.dumps(metadata)) return LnurlPayMetadata(json.dumps(metadata))

View File

@ -47,7 +47,7 @@ async def on_invoice_paid(payment: Payment) -> None:
await payment.set_pending(False) await payment.set_pending(False)
await set_address_paid(payment_hash=payment.payment_hash) await set_address_paid(payment_hash=payment.payment_hash)
await call_webhook_on_paid(payment.payment_hash) await call_webhook_on_paid(payment_hash=payment.payment_hash)
elif "renew lnaddress" == payment.extra.get("tag"): elif "renew lnaddress" == payment.extra.get("tag"):
@ -55,7 +55,7 @@ async def on_invoice_paid(payment: Payment) -> None:
await set_address_renewed( await set_address_renewed(
address_id=payment.extra["id"], duration=payment.extra["duration"] address_id=payment.extra["id"], duration=payment.extra["duration"]
) )
await call_webhook_on_paid(payment.payment_hash) await call_webhook_on_paid(payment_hash=payment.payment_hash)
else: else:
return return

View File

@ -370,9 +370,8 @@
if (data.wallet_endpoint == '') { if (data.wallet_endpoint == '') {
data.wallet_endpoint = null data.wallet_endpoint = null
} }
data.wallet_endpoint = data.wallet_endpoint ?? '{{ request.url_root }}' data.wallet_endpoint = data.wallet_endpoint ?? '{{ root_url }}'
data.duration = parseInt(data.duration) data.duration = parseInt(data.duration)
console.log('data', data)
axios axios
.post('/lnaddress/api/v1/address/{{ domain_id }}', data) .post('/lnaddress/api/v1/address/{{ domain_id }}', data)

View File

@ -186,10 +186,14 @@
<q-input <q-input
filled filled
dense dense
bottom-slots
v-model.trim="domainDialog.data.cf_token" v-model.trim="domainDialog.data.cf_token"
type="text" type="text"
label="Cloudflare API token" label="Cloudflare API token"
> >
<template v-slot:hint>
Check extension <a href="https://github.com/lnbits/lnbits-legend/tree/master/lnbits/extensions/lnaddress">documentation!</a>
</template>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left" <q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>Your API key in cloudflare</q-tooltip >Your API key in cloudflare</q-tooltip
> >
@ -489,18 +493,6 @@
this.getDomains() this.getDomains()
this.getAddresses() this.getAddresses()
} }
// var self = this
//
// // axios is available for making requests
// axios({
// method: 'GET',
// url: '/example/api/v1/tools',
// headers: {
// 'X-example-header': 'not-used'
// }
// }).then(function (response) {
// self.tools = response.data
// })
} }
}) })
</script> </script>

View File

@ -1,4 +1,5 @@
from http import HTTPStatus from http import HTTPStatus
from urllib.parse import urlparse
from fastapi import Request from fastapi import Request
from fastapi.params import Depends from fastapi.params import Depends
@ -34,6 +35,7 @@ async def display(domain_id, request: Request):
await purge_addresses(domain_id) await purge_addresses(domain_id)
wallet = await get_wallet(domain.wallet) wallet = await get_wallet(domain.wallet)
url = urlparse(str(request.url))
return lnaddress_renderer().TemplateResponse( return lnaddress_renderer().TemplateResponse(
"lnaddress/display.html", "lnaddress/display.html",
@ -43,5 +45,6 @@ async def display(domain_id, request: Request):
"domain_domain": domain.domain, "domain_domain": domain.domain,
"domain_cost": domain.cost, "domain_cost": domain.cost,
"domain_wallet_inkey": wallet.inkey, "domain_wallet_inkey": wallet.inkey,
"root_url": f"{url.scheme}://{url.netloc}"
}, },
) )

View File

@ -2,6 +2,7 @@ from sqlite3 import Row
from fastapi.param_functions import Query from fastapi.param_functions import Query
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional
class CreateUserData(BaseModel): class CreateUserData(BaseModel):
@ -22,8 +23,8 @@ class Users(BaseModel):
id: str id: str
name: str name: str
admin: str admin: str
email: str email: Optional[str] = None
password: str password: Optional[str] = None
class Wallets(BaseModel): class Wallets(BaseModel):

View File

@ -23,7 +23,7 @@ from .crud import (
) )
from .models import CreateUserData, CreateUserWallet from .models import CreateUserData, CreateUserWallet
### Users # Users
@usermanager_ext.get("/api/v1/users", status_code=HTTPStatus.OK) @usermanager_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
@ -63,7 +63,7 @@ async def api_usermanager_users_delete(
raise HTTPException(status_code=HTTPStatus.NO_CONTENT) raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
###Activate Extension # Activate Extension
@usermanager_ext.post("/api/v1/extensions") @usermanager_ext.post("/api/v1/extensions")
@ -79,7 +79,7 @@ async def api_usermanager_activate_extension(
return {"extension": "updated"} return {"extension": "updated"}
###Wallets # Wallets
@usermanager_ext.post("/api/v1/wallets") @usermanager_ext.post("/api/v1/wallets")
@ -98,7 +98,7 @@ async def api_usermanager_wallets(wallet: WalletTypeInfo = Depends(get_key_type)
return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)] return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)]
@usermanager_ext.get("/api/v1/wallets/{wallet_id}") @usermanager_ext.get("/api/v1/transactions/{wallet_id}")
async def api_usermanager_wallet_transactions( async def api_usermanager_wallet_transactions(
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
): ):

View File

@ -23,7 +23,7 @@ class Jinja2Templates(templating.Jinja2Templates):
def get_environment(self, loader: "jinja2.BaseLoader") -> "jinja2.Environment": def get_environment(self, loader: "jinja2.BaseLoader") -> "jinja2.Environment":
@jinja2.contextfunction @jinja2.contextfunction
def url_for(context: dict, name: str, **path_params: typing.Any) -> str: def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
request: Request = context["request"] # type: starlette.requests.Request request: Request = context["request"]
return request.app.url_path_for(name, **path_params) return request.app.url_path_for(name, **path_params)
def url_params_update(init: QueryParams, **new: typing.Any) -> QueryParams: def url_params_update(init: QueryParams, **new: typing.Any) -> QueryParams:

View File

@ -5,6 +5,8 @@ import base64
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from lnbits import bolt11 as lnbits_bolt11
from .base import ( from .base import (
StatusResponse, StatusResponse,
InvoiceResponse, InvoiceResponse,
@ -21,7 +23,8 @@ class LndRestWallet(Wallet):
endpoint = getenv("LND_REST_ENDPOINT") endpoint = getenv("LND_REST_ENDPOINT")
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
endpoint = ( endpoint = (
"https://" + endpoint if not endpoint.startswith("http") else endpoint "https://" +
endpoint if not endpoint.startswith("http") else endpoint
) )
self.endpoint = endpoint self.endpoint = endpoint
@ -89,10 +92,21 @@ class LndRestWallet(Wallet):
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async with httpx.AsyncClient(verify=self.cert) as client: async with httpx.AsyncClient(verify=self.cert) as client:
# set the fee limit for the payment
invoice = lnbits_bolt11.decode(bolt11)
lnrpcFeeLimit = dict()
if invoice.amount_msat > 1000_000:
lnrpcFeeLimit["percent"] = "1" # in percent
else:
lnrpcFeeLimit["fixed"] = "10" # in sat
r = await client.post( r = await client.post(
url=f"{self.endpoint}/v1/channels/transactions", url=f"{self.endpoint}/v1/channels/transactions",
headers=self.auth, headers=self.auth,
json={"payment_request": bolt11}, json={
"payment_request": bolt11,
"fee_limit": lnrpcFeeLimit,
},
timeout=180, timeout=180,
) )
@ -168,7 +182,8 @@ class LndRestWallet(Wallet):
except: except:
continue continue
payment_hash = base64.b64decode(inv["r_hash"]).hex() payment_hash = base64.b64decode(
inv["r_hash"]).hex()
yield payment_hash yield payment_hash
except (OSError, httpx.ConnectError, httpx.ReadError): except (OSError, httpx.ConnectError, httpx.ReadError):
pass pass

View File

@ -1,2 +1 @@
[mypy] [mypy]
plugins = trio_typing.plugin