Merge branch 'main' into eclair-backend

This commit is contained in:
Tiago vasconcelos
2022-04-28 15:30:26 +01:00
399 changed files with 48748 additions and 10215 deletions
+1 -1
View File
@@ -2,7 +2,6 @@
from .void import VoidWallet
from .clightning import CLightningWallet
from .lndgrpc import LndWallet
from .lntxbot import LntxbotWallet
from .opennode import OpenNodeWallet
from .lnpay import LNPayWallet
@@ -10,3 +9,4 @@ from .lnbits import LNbitsWallet
from .lndrest import LndRestWallet
from .spark import SparkWallet
from .eclair import EclairWallet
from .fake import FakeWallet
+3 -1
View File
@@ -60,7 +60,9 @@ class Wallet(ABC):
pass
@abstractmethod
def pay_invoice(self, bolt11: str) -> Coroutine[None, None, PaymentResponse]:
def pay_invoice(
self, bolt11: str, fee_limit_msat: int
) -> Coroutine[None, None, PaymentResponse]:
pass
@abstractmethod
+41 -30
View File
@@ -3,21 +3,41 @@ try:
except ImportError: # pragma: nocover
LightningRpc = None
import trio
import asyncio
import random
import json
from functools import partial, wraps
from os import getenv
from typing import Optional, AsyncGenerator
from typing import AsyncGenerator, Optional
import time
from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
StatusResponse,
Unsupported,
Wallet,
)
from lnbits import bolt11 as lnbits_bolt11
def async_wrap(func):
@wraps(func)
async def run(*args, loop=None, executor=None, **kwargs):
if loop is None:
loop = asyncio.get_event_loop()
partial_func = partial(func, *args, **kwargs)
return await loop.run_in_executor(executor, partial_func)
return run
def _pay_invoice(ln, payload):
return ln.call("pay", payload)
def _paid_invoices_stream(ln, last_pay_index):
return ln.waitanyinvoice(last_pay_index)
class CLightningWallet(Wallet):
@@ -35,7 +55,7 @@ class CLightningWallet(Wallet):
try:
answer = self.ln.help("invoicewithdescriptionhash")
if answer["help"][0]["command"].startswith(
"invoicewithdescriptionhash msatoshi label description_hash",
"invoicewithdescriptionhash msatoshi label description_hash"
):
self.supports_description_hash = True
except:
@@ -53,8 +73,7 @@ class CLightningWallet(Wallet):
try:
funds = self.ln.listfunds()
return StatusResponse(
None,
sum([ch["channel_sat"] * 1000 for ch in funds["channels"]]),
None, sum([ch["channel_sat"] * 1000 for ch in funds["channels"]])
)
except RpcError as exc:
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
@@ -84,9 +103,18 @@ class CLightningWallet(Wallet):
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
return InvoiceResponse(False, label, None, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
invoice = lnbits_bolt11.decode(bolt11)
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
payload = {
"bolt11": bolt11,
"maxfeepercent": "{:.11}".format(fee_limit_percent),
"exemptfee": 0, # so fee_limit_percent is applied even on payments with fee under 5000 millisatoshi (which is default value of exemptfee)
}
try:
r = self.ln.pay(bolt11)
wrapped = async_wrap(_pay_invoice)
r = await wrapped(self.ln, payload)
except RpcError as exc:
return PaymentResponse(False, None, 0, None, str(exc))
@@ -116,25 +144,8 @@ class CLightningWallet(Wallet):
raise KeyError("supplied an invalid checking_id")
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
stream = await trio.open_unix_socket(self.rpc)
i = 0
while True:
call = json.dumps(
{
"method": "waitanyinvoice",
"id": 0,
"params": [self.last_pay_index],
}
)
await stream.send_all(call.encode("utf-8"))
data = await stream.receive_some()
paid = json.loads(data.decode("ascii"))
paid = self.ln.waitanyinvoice(self.last_pay_index)
wrapped = async_wrap(_paid_invoices_stream)
paid = await wrapped(self.ln, self.last_pay_index)
self.last_pay_index = paid["pay_index"]
yield paid["label"]
i += 1
+96
View File
@@ -0,0 +1,96 @@
import asyncio
import json
import httpx
from os import getenv
from datetime import datetime, timedelta
from typing import Optional, Dict, AsyncGenerator
import random
import string
from lnbits.helpers import urlsafe_short_hash
import hashlib
from ..bolt11 import encode, decode
from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
)
class FakeWallet(Wallet):
async def status(self) -> StatusResponse:
print(
"FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr."
)
return StatusResponse(None, float("inf"))
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse:
secret = getenv("FAKE_WALLET_SECRET")
data: Dict = {
"out": False,
"amount": amount,
"currency": "bc",
"privkey": hashlib.pbkdf2_hmac(
"sha256",
secret.encode("utf-8"),
("FakeWallet").encode("utf-8"),
2048,
32,
).hex(),
"memo": None,
"description_hash": None,
"description": "",
"fallback": None,
"expires": None,
"route": None,
}
data["amount"] = amount * 1000
data["timestamp"] = datetime.now().timestamp()
if description_hash:
data["tags_set"] = ["h"]
data["description_hash"] = description_hash.hex()
else:
data["tags_set"] = ["d"]
data["memo"] = memo
data["description"] = memo
randomHash = (
data["privkey"][:6]
+ hashlib.sha256(str(random.getrandbits(256)).encode("utf-8")).hexdigest()[
6:
]
)
data["paymenthash"] = randomHash
payment_request = encode(data)
checking_id = randomHash
return InvoiceResponse(True, checking_id, payment_request)
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
invoice = decode(bolt11)
if (
hasattr(invoice, "checking_id")
and invoice.checking_id[6:] == data["privkey"][:6]
):
return PaymentResponse(True, invoice.payment_hash, 0)
else:
return PaymentResponse(
ok=False, error_message="Only internal invoices can be used!"
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(False)
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(False)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.queue = asyncio.Queue(0)
while True:
value = await self.queue.get()
yield value
+26 -18
View File
@@ -1,4 +1,4 @@
import trio
import asyncio
import json
import httpx
from os import getenv
@@ -28,7 +28,14 @@ class LNbitsWallet(Wallet):
async def status(self) -> StatusResponse:
async with httpx.AsyncClient() as client:
r = await client.get(url=f"{self.endpoint}/api/v1/wallet", headers=self.key)
try:
r = await client.get(
url=f"{self.endpoint}/api/v1/wallet", headers=self.key, timeout=15
)
except Exception as exc:
return StatusResponse(
f"Failed to connect to {self.endpoint} due to: {exc}", 0
)
try:
data = r.json()
@@ -38,7 +45,7 @@ class LNbitsWallet(Wallet):
)
if r.is_error:
return StatusResponse(data["message"], 0)
return StatusResponse(data["detail"], 0)
return StatusResponse(None, data["balance"])
@@ -56,9 +63,7 @@ class LNbitsWallet(Wallet):
async with httpx.AsyncClient() as client:
r = await client.post(
url=f"{self.endpoint}/api/v1/payments",
headers=self.key,
json=data,
url=f"{self.endpoint}/api/v1/payments", headers=self.key, json=data
)
ok, checking_id, payment_request, error_message = (
not r.is_error,
@@ -68,24 +73,25 @@ class LNbitsWallet(Wallet):
)
if r.is_error:
error_message = r.json()["message"]
error_message = r.json()["detail"]
else:
data = r.json()
checking_id, payment_request = data["checking_id"], data["payment_request"]
return InvoiceResponse(ok, checking_id, payment_request, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client:
r = await client.post(
url=f"{self.endpoint}/api/v1/payments",
headers=self.key,
json={"out": True, "bolt11": bolt11},
timeout=100,
)
ok, checking_id, fee_msat, error_message = not r.is_error, None, 0, None
if r.is_error:
error_message = r.json()["message"]
error_message = r.json()["detail"]
else:
data = r.json()
checking_id = data["checking_id"]
@@ -93,16 +99,18 @@ class LNbitsWallet(Wallet):
return PaymentResponse(ok, checking_id, fee_msat, error_message)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
async with httpx.AsyncClient() as client:
r = await client.get(
url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.key
)
if r.is_error:
try:
async with httpx.AsyncClient() as client:
r = await client.get(
url=f"{self.endpoint}/api/v1/payments/{checking_id}",
headers=self.key,
)
if r.is_error:
return PaymentStatus(None)
return PaymentStatus(r.json()["paid"])
except:
return PaymentStatus(None)
return PaymentStatus(r.json()["paid"])
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
async with httpx.AsyncClient() as client:
r = await client.get(
@@ -137,4 +145,4 @@ class LNbitsWallet(Wallet):
pass
print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
await trio.sleep(5)
await asyncio.sleep(5)
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+52 -65
View File
@@ -1,19 +1,21 @@
imports_ok = True
try:
import lndgrpc # type: ignore
from lndgrpc.common import ln # type: ignore
from google import protobuf
import grpc
except ImportError: # pragma: nocover
lndgrpc = None
imports_ok = False
try:
import purerpc # type: ignore
except ImportError: # pragma: nocover
purerpc = None
import binascii
import base64
import hashlib
from os import getenv
from os import environ, error, getenv
from typing import Optional, Dict, AsyncGenerator
from .macaroon import load_macaroon, AESCipher
if imports_ok:
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
from .base import (
StatusResponse,
@@ -57,38 +59,25 @@ def get_ssl_context(cert_path: str):
return context
def load_macaroon(macaroon_path: str):
with open(macaroon_path, "rb") as f:
macaroon_bytes = f.read()
return macaroon_bytes.hex()
def parse_checking_id(checking_id: str) -> bytes:
return base64.b64decode(
checking_id.replace("_", "/"),
)
return base64.b64decode(checking_id.replace("_", "/"))
def stringify_checking_id(r_hash: bytes) -> str:
return (
base64.b64encode(
r_hash,
)
.decode("utf-8")
.replace("/", "_")
)
return base64.b64encode(r_hash).decode("utf-8").replace("/", "_")
# Due to updated ECDSA generated tls.cert we need to let gprc know that
# we need to use that cipher suite otherwise there will be a handhsake
# error when we communicate with the lnd rpc server.
environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA"
class LndWallet(Wallet):
def __init__(self):
if lndgrpc is None: # pragma: nocover
if not imports_ok: # pragma: nocover
raise ImportError(
"The `lndgrpc` library must be installed to use `LndWallet`."
)
if purerpc is None: # pragma: nocover
raise ImportError(
"The `purerpc` library must be installed to use `LndWallet`."
"The `grpcio` and `protobuf` library must be installed to use `GRPC LndWallet`. Alternatively try using the LndRESTWallet."
)
endpoint = getenv("LND_GRPC_ENDPOINT")
@@ -96,25 +85,36 @@ class LndWallet(Wallet):
self.port = int(getenv("LND_GRPC_PORT"))
self.cert_path = getenv("LND_GRPC_CERT") or getenv("LND_CERT")
self.macaroon_path = (
macaroon = (
getenv("LND_GRPC_MACAROON")
or getenv("LND_GRPC_ADMIN_MACAROON")
or getenv("LND_ADMIN_MACAROON")
or getenv("LND_GRPC_INVOICE_MACAROON")
or getenv("LND_INVOICE_MACAROON")
)
network = getenv("LND_GRPC_NETWORK", "mainnet")
self.rpc = lndgrpc.LNDClient(
f"{self.endpoint}:{self.port}",
cert_filepath=self.cert_path,
network=network,
macaroon_filepath=self.macaroon_path,
encrypted_macaroon = getenv("LND_GRPC_MACAROON_ENCRYPTED")
if encrypted_macaroon:
macaroon = AESCipher(description="macaroon decryption").decrypt(
encrypted_macaroon
)
self.macaroon = load_macaroon(macaroon)
cert = open(self.cert_path, "rb").read()
creds = grpc.ssl_channel_credentials(cert)
auth_creds = grpc.metadata_call_credentials(self.metadata_callback)
composite_creds = grpc.composite_channel_credentials(creds, auth_creds)
channel = grpc.aio.secure_channel(
f"{self.endpoint}:{self.port}", composite_creds
)
self.rpc = lnrpc.LightningStub(channel)
def metadata_callback(self, _, callback):
callback([("macaroon", self.macaroon)], None)
async def status(self) -> StatusResponse:
try:
resp = self.rpc._ln_stub.ChannelBalance(ln.ChannelBalanceRequest())
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest())
except Exception as exc:
return StatusResponse(str(exc), 0)
@@ -135,7 +135,7 @@ class LndWallet(Wallet):
try:
req = ln.Invoice(**params)
resp = self.rpc._ln_stub.AddInvoice(req)
resp = await self.rpc.AddInvoice(req)
except Exception as exc:
error_message = str(exc)
return InvoiceResponse(False, None, None, error_message)
@@ -144,8 +144,10 @@ class LndWallet(Wallet):
payment_request = str(resp.payment_request)
return InvoiceResponse(True, checking_id, payment_request, None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
resp = self.rpc.send_payment(payment_request=bolt11)
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
req = ln.SendRequest(payment_request=bolt11, fee_limit=fee_limit_fixed)
resp = await self.rpc.SendPaymentSync(req)
if resp.payment_error:
return PaymentResponse(False, "", 0, None, resp.payment_error)
@@ -166,7 +168,7 @@ class LndWallet(Wallet):
# that use different checking_id formats
return PaymentStatus(None)
resp = self.rpc.lookup_invoice(r_hash.hex())
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
if resp.settled:
return PaymentStatus(True)
@@ -176,30 +178,15 @@ class LndWallet(Wallet):
return PaymentStatus(True)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
async with purerpc.secure_channel(
self.endpoint,
self.port,
get_ssl_context(self.cert_path),
) as channel:
client = purerpc.Client("lnrpc.Lightning", channel)
subscribe_invoices = client.get_method_stub(
"SubscribeInvoices",
purerpc.RPCSignature(
purerpc.Cardinality.UNARY_STREAM,
ln.InvoiceSubscription,
ln.Invoice,
),
)
macaroon = load_macaroon(self.macaroon_path)
async for inv in subscribe_invoices(
ln.InvoiceSubscription(),
metadata=[("macaroon", macaroon)],
):
if not inv.settled:
request = ln.InvoiceSubscription()
try:
async for i in self.rpc.SubscribeInvoices(request):
if not i.settled:
continue
checking_id = stringify_checking_id(inv.r_hash)
checking_id = stringify_checking_id(i.r_hash)
yield checking_id
except error:
print(error)
print("lost connection to lnd InvoiceSubscription, please restart lnbits.")
+30 -21
View File
@@ -1,10 +1,14 @@
import trio
import asyncio
from pydoc import describe
import httpx
import json
import base64
from os import getenv
from typing import Optional, Dict, AsyncGenerator
from lnbits import bolt11 as lnbits_bolt11
from .macaroon import load_macaroon, AESCipher
from .base import (
StatusResponse,
InvoiceResponse,
@@ -32,15 +36,22 @@ class LndRestWallet(Wallet):
or getenv("LND_INVOICE_MACAROON")
or getenv("LND_REST_INVOICE_MACAROON")
)
self.auth = {"Grpc-Metadata-macaroon": macaroon}
self.cert = getenv("LND_REST_CERT")
encrypted_macaroon = getenv("LND_REST_MACAROON_ENCRYPTED")
if encrypted_macaroon:
macaroon = AESCipher(description="macaroon decryption").decrypt(
encrypted_macaroon
)
self.macaroon = load_macaroon(macaroon)
self.auth = {"Grpc-Metadata-macaroon": self.macaroon}
self.cert = getenv("LND_REST_CERT", True)
async def status(self) -> StatusResponse:
try:
async with httpx.AsyncClient(verify=self.cert) as client:
r = await client.get(
f"{self.endpoint}/v1/balance/channels",
headers=self.auth,
f"{self.endpoint}/v1/balance/channels", headers=self.auth
)
except (httpx.ConnectError, httpx.RequestError):
return StatusResponse(f"Unable to connect to {self.endpoint}.", 0)
@@ -60,10 +71,7 @@ class LndRestWallet(Wallet):
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse:
data: Dict = {
"value": amount,
"private": True,
}
data: Dict = {"value": amount, "private": True}
if description_hash:
data["description_hash"] = base64.b64encode(description_hash).decode(
"ascii"
@@ -73,9 +81,7 @@ class LndRestWallet(Wallet):
async with httpx.AsyncClient(verify=self.cert) as client:
r = await client.post(
url=f"{self.endpoint}/v1/invoices",
headers=self.auth,
json=data,
url=f"{self.endpoint}/v1/invoices", headers=self.auth, json=data
)
if r.is_error:
@@ -93,12 +99,16 @@ class LndRestWallet(Wallet):
return InvoiceResponse(True, checking_id, payment_request, None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient(verify=self.cert) as client:
# set the fee limit for the payment
lnrpcFeeLimit = dict()
lnrpcFeeLimit["fixed_msat"] = "{}".format(fee_limit_msat)
r = await client.post(
url=f"{self.endpoint}/v1/channels/transactions",
headers=self.auth,
json={"payment_request": bolt11},
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
timeout=180,
)
@@ -109,16 +119,16 @@ 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("_", "/")
async with httpx.AsyncClient(verify=self.cert) as client:
r = await client.get(
url=f"{self.endpoint}/v1/invoice/{checking_id}",
headers=self.auth,
url=f"{self.endpoint}/v1/invoice/{checking_id}", headers=self.auth
)
if r.is_error or not r.json().get("settled"):
@@ -150,6 +160,7 @@ class LndRestWallet(Wallet):
# for some reason our checking_ids are in base64 but the payment hashes
# returned here are in hex, lnd is weird
checking_id = checking_id.replace("_", "/")
checking_id = base64.b64decode(checking_id).hex()
for p in r.json()["payments"]:
@@ -164,9 +175,7 @@ class LndRestWallet(Wallet):
while True:
try:
async with httpx.AsyncClient(
timeout=None,
headers=self.auth,
verify=self.cert,
timeout=None, headers=self.auth, verify=self.cert
) as client:
async with client.stream("GET", url) as r:
async for line in r.aiter_lines():
@@ -183,4 +192,4 @@ class LndRestWallet(Wallet):
pass
print("lost connection to lnd invoices stream, retrying in 5 seconds")
await trio.sleep(5)
await asyncio.sleep(5)
+10 -10
View File
@@ -1,10 +1,10 @@
import json
import trio
import asyncio
from fastapi.exceptions import HTTPException
import httpx
from os import getenv
from http import HTTPStatus
from typing import Optional, Dict, AsyncGenerator
from quart import request
from .base import (
StatusResponse,
@@ -76,7 +76,7 @@ class LNPayWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.endpoint}/wallet/{self.wallet_key}/withdraw",
@@ -117,8 +117,9 @@ class LNPayWallet(Wallet):
return PaymentStatus(statuses[r.json()["settled"]])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.send, receive = trio.open_memory_channel(0)
async for value in receive:
self.queue = asyncio.Queue(0)
while True:
value = await self.queue.get()
yield value
async def webhook_listener(self):
@@ -133,16 +134,15 @@ class LNPayWallet(Wallet):
or "event" not in data
or data["event"].get("name") != "wallet_receive"
):
return "", HTTPStatus.NO_CONTENT
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
lntx_id = data["data"]["wtx"]["lnTx"]["id"]
async with httpx.AsyncClient() as client:
r = await client.get(
f"{self.endpoint}/lntx/{lntx_id}?fields=settled",
headers=self.auth,
f"{self.endpoint}/lntx/{lntx_id}?fields=settled", headers=self.auth
)
data = r.json()
if data["settled"]:
await self.send.send(lntx_id)
await self.queue.put(lntx_id)
return "", HTTPStatus.NO_CONTENT
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+8 -14
View File
@@ -1,4 +1,4 @@
import trio
import asyncio
import json
import httpx
from os import getenv
@@ -30,9 +30,7 @@ class LntxbotWallet(Wallet):
async def status(self) -> StatusResponse:
async with httpx.AsyncClient() as client:
r = await client.get(
f"{self.endpoint}/balance",
headers=self.auth,
timeout=40,
f"{self.endpoint}/balance", headers=self.auth, timeout=40
)
try:
data = r.json()
@@ -60,10 +58,7 @@ class LntxbotWallet(Wallet):
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.endpoint}/addinvoice",
headers=self.auth,
json=data,
timeout=40,
f"{self.endpoint}/addinvoice", headers=self.auth, json=data, timeout=40
)
if r.is_error:
@@ -79,16 +74,16 @@ class LntxbotWallet(Wallet):
data = r.json()
return InvoiceResponse(True, data["payment_hash"], data["pay_req"], None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.endpoint}/payinvoice",
headers=self.auth,
json={"invoice": bolt11},
timeout=40,
timeout=100,
)
if r.is_error:
if "error" in r.json():
try:
data = r.json()
error_message = data["message"]
@@ -123,8 +118,7 @@ class LntxbotWallet(Wallet):
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
async with httpx.AsyncClient() as client:
r = await client.post(
url=f"{self.endpoint}/paymentstatus/{checking_id}",
headers=self.auth,
url=f"{self.endpoint}/paymentstatus/{checking_id}", headers=self.auth
)
data = r.json()
@@ -150,4 +144,4 @@ class LntxbotWallet(Wallet):
pass
print("lost connection to lntxbot /payments/stream, retrying in 5 seconds")
await trio.sleep(5)
await asyncio.sleep(5)
+1
View File
@@ -0,0 +1 @@
from .macaroon import load_macaroon, AESCipher
+103
View File
@@ -0,0 +1,103 @@
from Cryptodome import Random
from Cryptodome.Cipher import AES
import base64
from hashlib import md5
import getpass
BLOCK_SIZE = 16
import getpass
def load_macaroon(macaroon: str) -> str:
"""Returns hex version of a macaroon encoded in base64 or the file path.
:param macaroon: Macaroon encoded in base64 or file path.
:type macaroon: str
:return: Hex version of macaroon.
:rtype: str
"""
# if the macaroon is a file path, load it
if macaroon.split(".")[-1] == "macaroon":
with open(macaroon, "rb") as f:
macaroon_bytes = f.read()
return macaroon_bytes.hex()
else:
# convert the bas64 macaroon to hex
try:
macaroon = base64.b64decode(macaroon).hex()
except:
pass
return macaroon
class AESCipher(object):
"""This class is compatible with crypto-js/aes.js
Encrypt and decrypt in Javascript using:
import AES from "crypto-js/aes.js";
import Utf8 from "crypto-js/enc-utf8.js";
AES.encrypt(decrypted, password).toString()
AES.decrypt(encrypted, password).toString(Utf8);
"""
def __init__(self, key=None, description=""):
self.key = key
self.description = description + " "
def pad(self, data):
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
return data + (chr(length) * length).encode()
def unpad(self, data):
return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]
@property
def passphrase(self):
passphrase = self.key if self.key is not None else None
if passphrase is None:
passphrase = getpass.getpass(f"Enter {self.description}password:")
return passphrase
def bytes_to_key(self, data, salt, output=48):
# extended from https://gist.github.com/gsakkis/4546068
assert len(salt) == 8, len(salt)
data += salt
key = md5(data).digest()
final_key = key
while len(final_key) < output:
key = md5(key + data).digest()
final_key += key
return final_key[:output]
def decrypt(self, encrypted: str) -> str:
"""Decrypts a string using AES-256-CBC.
"""
passphrase = self.passphrase
encrypted = base64.b64decode(encrypted)
assert encrypted[0:8] == b"Salted__"
salt = encrypted[8:16]
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
try:
return self.unpad(aes.decrypt(encrypted[16:])).decode()
except UnicodeDecodeError:
raise ValueError("Wrong passphrase")
def encrypt(self, message: bytes) -> str:
passphrase = self.passphrase
salt = Random.new().read(8)
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
return base64.b64encode(b"Salted__" + salt + aes.encrypt(self.pad(message))).decode()
# if this file is executed directly, ask for a macaroon and encrypt it
if __name__ == "__main__":
macaroon = input("Enter macaroon: ")
macaroon = load_macaroon(macaroon)
macaroon = AESCipher(description="encryption").encrypt(macaroon.encode())
print("Encrypted macaroon:")
print(macaroon)
+14 -13
View File
@@ -1,10 +1,12 @@
import trio
import asyncio
from fastapi.exceptions import HTTPException
from lnbits.helpers import url_for
import hmac
import httpx
from http import HTTPStatus
from os import getenv
from typing import Optional, AsyncGenerator
from quart import request, url_for
from .base import (
StatusResponse,
@@ -34,9 +36,7 @@ class OpenNodeWallet(Wallet):
try:
async with httpx.AsyncClient() as client:
r = await client.get(
f"{self.endpoint}/v1/account/balance",
headers=self.auth,
timeout=40,
f"{self.endpoint}/v1/account/balance", headers=self.auth, timeout=40
)
except (httpx.ConnectError, httpx.RequestError):
return StatusResponse(f"Unable to connect to '{self.endpoint}'", 0)
@@ -63,7 +63,7 @@ class OpenNodeWallet(Wallet):
json={
"amount": amount,
"description": memo or "",
"callback_url": url_for("webhook_listener", _external=True),
"callback_url": url_for("/webhook_listener", _external=True),
},
timeout=40,
)
@@ -77,7 +77,7 @@ class OpenNodeWallet(Wallet):
payment_request = data["lightning_invoice"]["payreq"]
return InvoiceResponse(True, checking_id, payment_request, None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.endpoint}/v2/withdrawals",
@@ -125,21 +125,22 @@ class OpenNodeWallet(Wallet):
return PaymentStatus(statuses[r.json()["data"]["status"]])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.send, receive = trio.open_memory_channel(0)
async for value in receive:
self.queue = asyncio.Queue(0)
while True:
value = await self.queue.get()
yield value
async def webhook_listener(self):
data = await request.form
if "status" not in data or data["status"] != "paid":
return "", HTTPStatus.NO_CONTENT
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
charge_id = data["id"]
x = hmac.new(self.auth["Authorization"].encode("ascii"), digestmod="sha256")
x.update(charge_id.encode("ascii"))
if x.hexdigest() != data["hashed_order"]:
print("invalid webhook, not from opennode")
return "", HTTPStatus.NO_CONTENT
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
await self.send.send(charge_id)
return "", HTTPStatus.NO_CONTENT
await self.queue.put(charge_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+9 -8
View File
@@ -1,4 +1,4 @@
import trio
import asyncio
import json
import httpx
import random
@@ -75,8 +75,7 @@ class SparkWallet(Wallet):
return StatusResponse(str(e), 0)
return StatusResponse(
None,
sum([ch["channel_sat"] * 1000 for ch in funds["channels"]]),
None, sum([ch["channel_sat"] * 1000 for ch in funds["channels"]])
)
async def create_invoice(
@@ -108,7 +107,7 @@ class SparkWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
try:
r = await self.pay(bolt11)
except (SparkError, UnknownError) as exc:
@@ -153,9 +152,11 @@ class SparkWallet(Wallet):
if not r or not r.get("invoices"):
return PaymentStatus(None)
if r["invoices"][0]["status"] == "unpaid":
if r["invoices"][0]["status"] == "paid":
return PaymentStatus(True)
else:
return PaymentStatus(False)
return PaymentStatus(True)
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
# check if it's 32 bytes hex
@@ -184,7 +185,7 @@ class SparkWallet(Wallet):
raise KeyError("supplied an invalid checking_id")
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
url = self.url + "/stream?access-key=" + self.token
url = f"{self.url}/stream?access-key={self.token}"
while True:
try:
@@ -199,4 +200,4 @@ class SparkWallet(Wallet):
pass
print("lost connection to spark /stream, retrying in 5 seconds")
await trio.sleep(5)
await asyncio.sleep(5)
+7 -7
View File
@@ -1,12 +1,12 @@
from typing import Optional, AsyncGenerator
from typing import AsyncGenerator, Optional
from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
StatusResponse,
Unsupported,
Wallet,
)
@@ -20,12 +20,12 @@ class VoidWallet(Wallet):
raise Unsupported("")
async def status(self) -> StatusResponse:
return StatusResponse(
"This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits.",
0,
print(
"This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits."
)
return StatusResponse(None, 0)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
raise Unsupported("")
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: