Merge branch 'main' into eclair-backend
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .macaroon import load_macaroon, AESCipher
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user