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

This commit is contained in:
benarc
2021-12-16 21:18:38 +00:00
27 changed files with 450 additions and 106 deletions
+10 -9
View File
@@ -65,15 +65,16 @@ async def fetch_fiat_exchange_rate(currency: str, provider: str):
}
url = exchange_rate_providers[provider]["api_url"]
for key in replacements.keys():
url = url.replace("{" + key + "}", replacements[key])
if url:
for key in replacements.keys():
url = url.replace("{" + key + "}", replacements[key])
async with httpx.AsyncClient() as client:
r = await client.get(url)
r.raise_for_status()
data = r.json()
else:
data = {}
getter = exchange_rate_providers[provider]["getter"]
async with httpx.AsyncClient() as client:
r = await client.get(url)
r.raise_for_status()
data = r.json()
rate = float(getter(data, replacements))
rate = float(getter(data, replacements))
return rate
+2 -2
View File
@@ -121,8 +121,8 @@ async def api_bleskomat_lnurl(req: Request):
except LnurlHttpError as e:
return {"status": "ERROR", "reason": str(e)}
except Exception:
traceback.print_exc()
except Exception as e:
print(str(e))
return {"status": "ERROR", "reason": "Unexpected error"}
return {"status": "OK"}
+2 -1
View File
@@ -124,7 +124,8 @@ class BleskomatLnurl(BaseModel):
)
except (ValueError, PermissionError, PaymentFailure) as e:
raise LnurlValidationError("Failed to pay invoice: " + str(e))
except Exception:
except Exception as e:
print(str(e))
raise LnurlValidationError("Unexpected error")
async def use(self, conn) -> bool:
+1 -1
View File
@@ -166,7 +166,7 @@ new Vue({
LNbits.api
.request('POST', '/lnurlp/api/v1/links', wallet.adminkey, data)
.then(response => {
this.payLinks.push(mapPayLink(response.data))
this.getPayLinks()
this.formDialog.show = false
this.resetFormData()
})
+92 -34
View File
@@ -1,9 +1,10 @@
import base64
import hashlib
from http import HTTPStatus
from typing import Optional
from fastapi import Request
from fastapi.param_functions import Query
from lnurl import LnurlPayActionResponse, LnurlPayResponse # type: ignore
from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice
@@ -28,53 +29,106 @@ async def lnurl_response(
nonce: str = Query(None),
pos_id: 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)
if not pos:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos not found."
)
nonce1 = bytes.fromhex(nonce)
payload1 = bytes.fromhex(payload)
h = hashlib.sha256(nonce1)
h.update(pos.key.encode())
s = h.digest()
res = bytearray(payload1)
return {
"status": "ERROR",
"reason": f"lnurlpos {pos_id} not found on this server",
}
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}",
}
# check payload and nonce sizes
if len(payloadb) != 8 or len(nonceb) != 8:
return {"status": "ERROR", "reason": "Expected 8 bytes"}
# verify hmac
if verify_checksum:
expected = hmac.new(
pos.key.encode(), payloadb[:-2], digestmod="sha256"
).digest()
if expected[:2] != payloadb[-2:]:
return {"status": "ERROR", "reason": "Invalid HMAC"}
# decrypt
s = hmac.new(pos.key.encode(), nonceb, digestmod="sha256").digest()
res = bytearray(payloadb)
for i in range(len(res)):
res[i] = res[i] ^ s[i]
decryptedAmount = float(int.from_bytes(res[2:6], "little") / 100)
decryptedPin = int.from_bytes(res[:2], "little")
if type(decryptedAmount) != float:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not an amount.")
pin = int.from_bytes(res[0:2], "little")
amount = int.from_bytes(res[2:6], "little")
price_msat = (
await fiat_amount_as_satoshis(decryptedAmount, pos.currency)
await fiat_amount_as_satoshis(float(amount) / 100, pos.currency)
if pos.currency != "sat"
else pos.currency
else amount
) * 1000
lnurlpospayment = await create_lnurlpospayment(
posid=pos.id,
payload=payload,
sats=price_msat,
pin=decryptedPin,
pin=pin,
payhash="payment_hash",
)
if not lnurlpospayment:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Could not create payment"
)
return {"status": "ERROR", "reason": "Could not create payment."}
resp = LnurlPayResponse(
callback=request.url_for(
return {
"tag": "payRequest",
"callback": request.url_for(
"lnurlpos.lnurl_callback", paymentid=lnurlpospayment.id
),
min_sendable=price_msat,
max_sendable=price_msat,
metadata=await pos.lnurlpay_metadata(),
)
return resp.dict()
"minSendable": price_msat,
"maxSendable": price_msat,
"metadata": await pos.lnurlpay_metadata(),
}
@lnurlpos_ext.get(
@@ -102,10 +156,14 @@ async def lnurl_callback(request: Request, paymentid: str = Query(None)):
lnurlpospayment_id=paymentid, payhash=payment_hash
)
resp = LnurlPayActionResponse(
pr=payment_request,
success_action=pos.success_action(paymentid, request),
routes=[],
)
return {
"pr": payment_request,
"successAction": {
"tag": "url",
"description": "Check the attached link",
"url": req.url_for("lnurlpos.displaypin", paymentid=paymentid),
},
"routes": [],
}
return resp.dict()
-10
View File
@@ -35,16 +35,6 @@ class lnurlposs(BaseModel):
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
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):
id: str
posid: str
@@ -1,7 +1,7 @@
<q-card>
<q-card-section>
<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
<a href="https://github.com/arcbtc/LNURLPoS"
>https://github.com/arcbtc/LNURLPoS</a
@@ -127,11 +127,14 @@
position="top"
@hide="closeFormDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-card
style="width: 700px; max-width: 80vw"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<div class="text-h6">Copy to LNURLPoS device</div>
<div class="text-subtitle2">
{% raw %} String server = "{{location}}";<br />
String posId = "{{settingsDialog.data.id}}";<br />
{% raw %} String baseURL =
"{{location}}/lnurlpos/api/v1/lnurl/{{settingsDialog.data.id}}";<br />
String key = "{{settingsDialog.data.key}}";<br />
String currency = "{{settingsDialog.data.currency}}";{% endraw %}
</div>
+1
View File
@@ -253,6 +253,7 @@ async def btc_price(currency: str) -> float:
await send_channel.put(rate)
except (
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.ConnectError,
httpx.ReadTimeout,
+2 -1
View File
@@ -117,8 +117,9 @@ class LndRestWallet(Wallet):
data = r.json()
payment_hash = data["payment_hash"]
checking_id = payment_hash
fee_msat = int(data["payment_route"]["total_fees_msat"])
preimage = base64.b64decode(data["payment_preimage"]).hex()
return PaymentResponse(True, checking_id, 0, preimage, None)
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
checking_id = checking_id.replace("_", "/")