Merge remote-tracking branch 'origin/FastAPI' into FastAPI

This commit is contained in:
benarc 2021-12-15 13:00:06 +00:00
commit b968a0c13f
5 changed files with 88 additions and 45 deletions

View File

@ -1,9 +1,10 @@
import base64
import hashlib import hashlib
from http import HTTPStatus from http import HTTPStatus
from typing import Optional
from fastapi import Request from fastapi import Request
from fastapi.param_functions import Query from fastapi.param_functions import Query
from lnurl import LnurlPayActionResponse, LnurlPayResponse # type: ignore
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
@ -28,53 +29,101 @@ async def lnurl_response(
nonce: str = Query(None), nonce: str = Query(None),
pos_id: str = Query(None), pos_id: str = Query(None),
payload: str = Query(None), payload: str = Query(None),
):
return await handle_lnurl_firstrequest(
request, pos_id, nonce, payload, verify_checksum=False
)
@lnurlpos_ext.get(
"/api/v2/lnurl/{pos_id}",
status_code=HTTPStatus.OK,
name="lnurlpos.lnurl_v2_params",
)
async def lnurl_v2_params(
request: Request,
pos_id: str = Query(None),
n: str = Query(None),
p: str = Query(None),
):
return await handle_lnurl_firstrequest(request, pos_id, n, p, verify_checksum=True)
async def handle_lnurl_firstrequest(
request: Request, pos_id: str, nonce: str, payload: str, verify_checksum: bool
): ):
pos = await get_lnurlpos(pos_id) pos = await get_lnurlpos(pos_id)
if not pos: if not pos:
raise HTTPException( return {
status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos not found." "status": "ERROR",
) "reason": f"lnurlpos {pos_id} not found on this server.",
nonce1 = bytes.fromhex(nonce) }
payload1 = bytes.fromhex(payload)
h = hashlib.sha256(nonce1) try:
nonceb = bytes.fromhex(nonce)
except ValueError:
try:
nonce += "=" * ((4 - len(nonce) % 4) % 4)
nonceb = base64.urlsafe_b64decode(nonce)
except:
return {
"status": "ERROR",
"reason": f"Invalid hex or base64 nonce: {nonce}",
}
try:
payloadb = bytes.fromhex(payload)
except ValueError:
try:
payload += "=" * ((4 - len(payload) % 4) % 4)
payloadb = base64.urlsafe_b64decode(payload)
except:
return {
"status": "ERROR",
"reason": f"Invalid hex or base64 payload: {payload}",
}
h = hashlib.sha256(nonceb)
h.update(pos.key.encode()) h.update(pos.key.encode())
s = h.digest() s = h.digest()
res = bytearray(payload1)
res = bytearray(payloadb)
for i in range(len(res)): for i in range(len(res)):
res[i] = res[i] ^ s[i] res[i] = res[i] ^ s[i]
decryptedAmount = float(int.from_bytes(res[2:6], "little") / 100)
decryptedPin = int.from_bytes(res[:2], "little") if verify_checksum:
if type(decryptedAmount) != float: checksum = res[6:8]
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not an amount.") if hashlib.sha256(res[0:6]).digest()[0:2] != checksum:
return {"status": "ERROR", "reason": "Invalid checksum!"}
pin = int.from_bytes(res[0:2], "little")
amount = int.from_bytes(res[2:6], "little")
price_msat = ( price_msat = (
await fiat_amount_as_satoshis(decryptedAmount, pos.currency) await fiat_amount_as_satoshis(float(amount) / 100, pos.currency)
if pos.currency != "sat" if pos.currency != "sat"
else pos.currency else amount
) * 1000 ) * 1000
lnurlpospayment = await create_lnurlpospayment( lnurlpospayment = await create_lnurlpospayment(
posid=pos.id, posid=pos.id,
payload=payload, payload=payload,
sats=price_msat, sats=price_msat,
pin=decryptedPin, pin=pin,
payhash="payment_hash", payhash="payment_hash",
) )
if not lnurlpospayment: if not lnurlpospayment:
raise HTTPException( return {"status": "ERROR", "reason": "Could not create payment."}
status_code=HTTPStatus.FORBIDDEN, detail="Could not create payment"
)
resp = LnurlPayResponse( return {
callback=request.url_for( "tag": "payRequest",
"callback": request.url_for(
"lnurlpos.lnurl_callback", paymentid=lnurlpospayment.id "lnurlpos.lnurl_callback", paymentid=lnurlpospayment.id
), ),
min_sendable=price_msat, "minSendable": price_msat,
max_sendable=price_msat, "maxSendable": price_msat,
metadata=await pos.lnurlpay_metadata(), "metadata": await pos.lnurlpay_metadata(),
) }
return resp.dict()
@lnurlpos_ext.get( @lnurlpos_ext.get(
@ -102,10 +151,14 @@ async def lnurl_callback(request: Request, paymentid: str = Query(None)):
lnurlpospayment_id=paymentid, payhash=payment_hash lnurlpospayment_id=paymentid, payhash=payment_hash
) )
resp = LnurlPayActionResponse( return {
pr=payment_request, "pr": payment_request,
success_action=pos.success_action(paymentid, request), "successAction": {
routes=[], "tag": "url",
) "description": "Check the attached link",
"url": req.url_for("lnurlpos.displaypin", paymentid=paymentid),
},
"routes": [],
}
return resp.dict() return resp.dict()

View File

@ -35,16 +35,6 @@ class lnurlposs(BaseModel):
async def lnurlpay_metadata(self) -> LnurlPayMetadata: async def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]])) return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
def success_action(
self, paymentid: str, req: Request
) -> Optional[LnurlPaySuccessAction]:
return UrlAction(
url=req.url_for("lnurlpos.displaypin", paymentid=paymentid),
description="Check the attached link",
)
class lnurlpospayment(BaseModel): class lnurlpospayment(BaseModel):
id: str id: str
posid: str posid: str

View File

@ -1,7 +1,7 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<p> <p>
Register LNURLPoS devices to recieve payments in your LNbits wallet.<br /> Register LNURLPoS devices to receive payments in your LNbits wallet.<br />
Build your own here Build your own here
<a href="https://github.com/arcbtc/LNURLPoS" <a href="https://github.com/arcbtc/LNURLPoS"
>https://github.com/arcbtc/LNURLPoS</a >https://github.com/arcbtc/LNURLPoS</a

View File

@ -130,8 +130,7 @@
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-h6">Copy to LNURLPoS device</div> <div class="text-h6">Copy to LNURLPoS device</div>
<div class="text-subtitle2"> <div class="text-subtitle2">
{% raw %} String server = "{{location}}";<br /> {% raw %} String server = "{{location}}/lnurlpos/api/v2/lnurl/{{settingsDialog.data.id}}";<br />
String posId = "{{settingsDialog.data.id}}";<br />
String key = "{{settingsDialog.data.key}}";<br /> String key = "{{settingsDialog.data.key}}";<br />
String currency = "{{settingsDialog.data.currency}}";{% endraw %} String currency = "{{settingsDialog.data.currency}}";{% endraw %}
</div> </div>

View File

@ -253,6 +253,7 @@ async def btc_price(currency: str) -> float:
await send_channel.put(rate) await send_channel.put(rate)
except ( except (
TypeError, # CoinMate returns HTTPStatus 200 but no data when a currency pair is not found TypeError, # CoinMate returns HTTPStatus 200 but no data when a currency pair is not found
KeyError, # Kraken's response dictionary doesn't include keys we look up for
httpx.ConnectTimeout, httpx.ConnectTimeout,
httpx.ConnectError, httpx.ConnectError,
httpx.ReadTimeout, httpx.ReadTimeout,